最近在实现一个聚合搜索的需求时,由于需要从五个索引中查询数据,然后再将搜索结果组合返回给前端app展现,显然这个地方不能再用同步的方式来操作了,如果有一个索引查询出现耗时较长,那么其余的请求都会排同步等待这一个慢查询,这个时候就考虑采用线程池+异步任务来实现这个聚合搜索的功能,顺便借助这次异步实现来加强下并发编程的学习。

一.SpringBoot中的异步操作

  异步操作根据是否有返回值可以派生为Callable、Future两类接口,我们知道在阿里巴巴的开发规约中并不推荐直接从当前线程中实例化一个线程来进行异步操作,这样主要是考虑JVM线程资源是宝贵的开销,线程应当取之于“线程池”,用完即当归还于“线程池”,而线程池的生命周期也是交给Spring容器来托管最佳,如果每个请求都随意的挥霍线程资源,没有一个统一调度的容器池,服务器将不堪重负,因此线程池资源需要首先进行合理配置。

1.配置Springboot线程池

  采用外部配置的形式将线程池参数进行初始化,然后注入到Spring容器中。

@Configuration
@EnableAsync
public class AsyncConfig {
@Autowired
private ExcutorProperties excutorProperties;
@Bean
public Executor taskExecutor() {
// Spring 默认配置是核心线程数大小为1,最大线程容量大小不受限制,队列容量也不受限制。
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数
executor.setCorePoolSize(excutorProperties.getCorePoolSize());
// 最大线程数
executor.setMaxPoolSize(excutorProperties.getMaxPoolSize());
// 队列大小
executor.setQueueCapacity(excutorProperties.getMaxPoolSize());
// 当最大池已满时,此策略保证不会丢失任务请求,但是可能会影响应用程序整体性能。
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setThreadNamePrefix("My ThreadPoolTaskExecutor-");
executor.initialize();
return executor;
}
}

这里需要注意下ThreadPoolTaskExecutor 饱和策略,有四种方式:

  • ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
  • ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务。这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。
  • ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉。
  • ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。

2.注解异步方法

在刚开始学Java多线程基础的时候,常常是在main方法中实例化一个线程或者Runnable接口的形式来实现异步操作,但是在Spring中,不建议直接在方法内部再去实例化新线程(Thread或Runnable),通常要将需要异步的操作抽象成一个方法,同时给这个方法加上了@Async注解来告诉 Spring 它是一个异步的方法。另外,这个方法的返回值 CompletableFuture.completedFuture(results)这代表我们需要返回结果,也就是说程序必须把任务执行完成之后再返回给用户。

  模拟一个多异步请求的场景,一个搜索接口如下,根据用户输入的内容去反馈搜索结构


public interface ISearchService {
/**
* 搜索接口定义
* @param text 内容
* @return
*/
SearchResult search(String text);
}

  而具体的实现按照类型进行区分,可以分为文本、新闻、图片以及音乐等等类型,还可以继续扩展:

 

  每一个类型的搜索由服务本身提供,聚合搜索的调用者并不关心。聚合搜索去异步查询不同接口时为了避免编写大量的if else代码,这里可以采用策略模式,根据传入的类型的服务来实现具体的调用,即便新增搜索接口时也不需要对以下代码重构。同时为了显示声明一个异步操作,查询方法需要加上@Async注解即可,返回的结果用CompletableFuture包装起来。

@Service
public class AsyncService { @Autowired
private final Map<String, ISearchService> searchServiceMap = new ConcurrentHashMap<>(); public AsyncService(Map<String, ISearchService> searchServiceMap) {
this.searchServiceMap.clear();
searchServiceMap.forEach((k, v)-> this.searchServiceMap.put(k, v));
} /**
* 异步查询
* @param type 采用何种查询类型
* @param text 查询内容
* @return 将结果放入future中可采用异步回调获取
*/
@Async
public CompletableFuture<SearchResult> search(String type, String text){
SearchResult result = searchServiceMap.get(type).search(text);
return CompletableFuture.completedFuture(result);
} }

3.异步回调与阻塞等待

  异步方法提供以后,就可以在其他服务中进行调用。在聚合查询这里场景是,需要异步并行的去查询多个类型的接口,查完以后组装成一个统一的结果返回客户端,这样类似的开发场景也比较常见,比如需要主线程等待多个线程完成以后才能继续下一阶段的任务,或线程之间相互等待全部完成才弄继续,或某线程的执行依赖另外一个线程的结果,Java对这些场景都提供了很好的支持——JUC并发包,主线程等待多个线程执行可以采用CountDownLaunch、线程相互等待可以使用CycleBarier、线程互调可以采用CompletableFuture
  此处即是采用的CompletableFuture来实现异步回调,并行搜索了每种类型的结果以后,通过CompletableFuture回调函数放到一个线程安全的Map中。因为每种查询类型的耗时不同,只有等最后一个查询结束以后才能放行,这个地方可以用CompletableFuture.allOf().join()来实现阻塞等待。

    

@Service
public class SearchFacade {

@Resource
    private AsyncService asyncService;

@Autowired
    private final Map<String, ISearchService> searchServiceMap = new ConcurrentHashMap<>();

public SearchFacade(Map<String, ISearchService> searchServiceMap) {
        this.searchServiceMap.clear();
        searchServiceMap.forEach((k, v)-> this.searchServiceMap.put(k, v));
    }

/**
     * 聚合搜索
     * @param context
     * @return
     */
    public SearchResult searchAll(String context){
        log.info("search method begin, context={}", context);
        //聚合结果Map,线程安全
        Map<String, Object> resultMap = new ConcurrentHashMap<>(searchServiceMap.size());
        try{
            //Future集合
            List<CompletableFuture<SearchResult>> futureList = new ArrayList<>();
            //遍历执行异步查询
            for(Map.Entry<String, ISearchService> entry : searchServiceMap.entrySet()){
                String type = entry.getKey();
                CompletableFuture<SearchResult> future = asyncService.search(type, context);
                futureList.add(future);
                //异步回调:聚合搜索结果
                future.thenAccept(searchResult -> resultMap.put(type, searchResult.getData()));
            }
            //阻塞等待最后一个返回
            CompletableFuture.allOf(futureList.toArray(new CompletableFuture[futureList.size()])).join();
            log.info("<=====search all end=====>\n resultMap = {}, time = {}", resultMap, System.currentTimeMillis());
            return new SearchResult(resultMap);
        }catch (Exception e){
            log.error("method error, e={}", e);
        }
        return new SearchResult(500, "service error");
    }
}

 注意:

  1. 多个线程操作同一个变量需考虑线程安全问题,此处组装结果的Map就采用的 ConcurrentHashMap结构。
  2. JDK1.8中CompletableFuture继承自Future接口,通过get()方法也能获取请求结果,但是为阻塞的。
  3. Boot编程常常会将登录态或Request信息放入ThreadLocal中,此处用了异步编程以后,在异步线程中无法获取主线程的信息,如果需要登录态信息则需通过参数往下传递(一次血泪Bug吐槽)。

4.测试验证

  SpringBoot服务起来后,请求聚合搜索接口,可以看到接口的入口处打印了的NIO线程号——[nio-8080-exec-2],后面分别从线程池中取了ecutor-thread-1、2、3、4等四个线程来进行了异步搜索操作:

  在代码中由于是执行完成后才打印的时间,所以搜索查询的耗时有大有小,而且是先完成的先打印,耗时较长的则靠后,直到最后一个查询完成后,整个聚合搜索才算完毕,输出了查询结果。

5.后续优化

  异步线程耗时过长,在主线程上需加上超时时间控制。

  最后附上Github源码地址:https://github.com/tisonkong/JavaAssemble/tree/master/basic

参考列表:

  1. SnailClimb.SpringBoot 异步编程指南
  2. 醉眼识朦胧.使用CompletableFuture优化你的代码执行效率

SpringBoot-技术专区-异步编程的更多相关文章

  1. Java-技术专区-异步编程指南

    通过本文你可以了解到下面这些知识点: Future 模式介绍以及核心思想 核心线程数.最大线程数的区别,队列容量代表什么: ThreadPoolTaskExecutor 饱和策略: SpringBoo ...

  2. SpringBoot 如何实现异步编程,老鸟们都这么玩的!

    镜像下载.域名解析.时间同步请点击 阿里巴巴开源镜像站 首先我们来看看在Spring中为什么要使用异步编程,它能解决什么问题? 为什么要用异步框架,它解决什么问题? 在SpringBoot的日常开发中 ...

  3. SpringBoot中的异步编程

    @Async 是什么 void test() { A(); B(); C(); } 复制代码 在没有Async的情况下,上面的方法是顺序执行的,也可以称为同步调用. B要在A执行完毕之后执行,C需要在 ...

  4. 关于如何提高Web服务端并发效率的异步编程技术

    最近我研究技术的一个重点是java的多线程开发,在我早期学习java的时候,很多书上把java的多线程开发标榜为简单易用,这个简单易用是以C语言作为参照的,不过我也没有使用过C语言开发过多线程,我只知 ...

  5. Atitit.异步编程技术原理与实践attilax总结

    Atitit.异步编程技术原理与实践attilax总结 1. 俩种实现模式 类库方式,以及语言方式,java futuretask ,c# await1 2. 事件(中断)机制1 3. Await 模 ...

  6. 如何提高Web服务端并发效率的异步编程技术

    作为一名web工程师都希望自己做的web应用能被越来越多的人使用,如果我们所做的web应用随着用户的增多而宕机了,那么越来越多的人就会变得越来越少了,为了让我们的web应用能有更多人使用,我们就得提升 ...

  7. 新手也能看懂的 SpringBoot 异步编程指南

    本文已经收录自 springboot-guide : https://github.com/Snailclimb/springboot-guide (Spring Boot 核心知识点整理. 基于 S ...

  8. 异步编程实现技术:回调、promise、协程序?

    异步编程实现技术:回调.promise.协程序?

  9. SpringBoot异步编程

    异步调用:当我们执行一个方法时,假如这个方法中有多个耗时的任务需要同时去做,而且又不着急等待这个结果时可以让客户端立即返回然后,后台慢慢去计算任务.当然你也可以选择等这些任务都执行完了,再返回给客户端 ...

随机推荐

  1. 20180105-Python中dict的使用方法

    字典是Python中常用的内置数据类型之一. 字典是无序的对象集合,只能通过key-value的方式存取数据,字典是一种映射类型,其次key的必须是可hash的不可变类型.字典中的key必须唯一. 1 ...

  2. 使用Unsafe来实现自定义锁

    1.使用Unsafe类 import sun.misc.Unsafe; class UnsafePackage { private static Unsafe unsafe; static { try ...

  3. rmdir -删除空目录

    总览 rmdir[options]directory... POSIX 选项: [-p] GNU 选项(缩写): [-p] [--ignore-fail-on-non-empty] [--help] ...

  4. python进行两个大数相加

    python进行两个大数相加:由于int类型32位或64位都有长度限制,超出会内存溢出,无法计算,那么解决方法如下: 思路: 1.将超长数转换成字符串 2.进行长度补零,即让两个要计算的字符串长度一样 ...

  5. Kotlin定义静态变量、静态方法

    Kotlin定义静态变量.静态方法kotlin定义静态变量.方法可以采用伴生对象companion object的方式. 经典例子,实例化Fragment. java写法: public class ...

  6. Codefroces 958C2 - Encryption (medium) 区间dp

    转自:https://www.cnblogs.com/widsom/p/8857777.html     略有修改 题目大意: n个数,划分为k段,每一段的和mod p,求出每一段的并相加,求最大是多 ...

  7. 十分钟理解Redux核心思想,过目不忘。

    白话Redux工作原理.浅显易懂. 如有纰漏或疑问,欢迎交流. Redux 约法三章 唯一数据源(state) 虽然redux中的state与react没有联系,但可以简单理解为react组件中的th ...

  8. Facebook被指控通过其应用程序进行监视用户照片

    Facebook被批使用其应用程序收集有关用户及其朋友的信息,其中包括一些尚未注册社交网络,阅读短信,跟踪其位置并在手机上查看照片的人. 有关大众监督的声称是前创业公司Six4Three对该公司提起的 ...

  9. SPOJ7258 SUBLEX - Lexicographical Substring Search

    传送门[洛谷] 心态崩了我有妹子 靠 我写的记忆化搜索 莫名WA了 然后心态崩了 当我正要改成bfs排序的时候 我灵光一动 md我写的i=0;i<25;i++??? 然后 改过来就A掉了T^T ...

  10. python将文件导入字典

    a={}i=0f = open("filepath","r")for line in f.readlines(): a[i] =line i=i+1 a是字典, ...