最近在实现一个聚合搜索的需求时,由于需要从五个索引中查询数据,然后再将搜索结果组合返回给前端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. node egg | 部署报错:server got error:bind EADDRNOTAVAIL

    egg框架实现的服务,部署在阿里云服务器上报出以下错误: 解决方案: config.js中 exports.cluster = { "listen": { "path&q ...

  2. python 异常处理【转载】

    什么是异常?异常即是一个事件,该事件会在程序执行过程中发生,影响了程序的正常执行.一般情况下,在Python无法正常处理程序时就会发生一个异常.异常是Python对象,表示一个错误.当Python脚本 ...

  3. 03.LNMP架构-PHP源码包编译部署详细步骤

    一.环境准备 操作系统:CentOS_Server_7.5_x64_1804.iso 部署组件:yasm+libmcrypt+libvpx+tiff+libpng+freetype+jpeg+libg ...

  4. 二、TortoiseSVN 合并、打分支、合并分支、切换分支

    一.合并 点击Edit conflict来编辑冲突: 在合并后的枝干对应栏中编辑后,Save保存后关闭. 二.TortoiseSVN 打分支.合并分支.切换分支 1.SVN打分支 方式一:先检出,再打 ...

  5. Sass-乘法

    Sass 中的乘法运算和前面介绍的加法与减法运算还略有不同.虽然他也能够支持多种单位(比如 em ,px , %),但当一个单位同时声明两个值时会有问题.比如下面的示例: 编译的时候报“20px*px ...

  6. mongodb 自增序列实现

    MongoDB没有像SQL数据库外开箱即用自动递增功能.默认情况下,它采用了12字节的ObjectId为_id字段作为主键来唯一地标识文档.然而,可能存在的情况,我们可能希望_id字段有一些其它的自动 ...

  7. html5 固定边栏滚动特效

    <script src="https://code.jquery.com/jquery.js"></script> //引入jquery <scrip ...

  8. springBoot 连接数据库

    心得:1.先添加依赖. 2.在application.yml文件中创建mybatis的连接,与项目连接起来. 3.application.yml文件中可以自行配置服务器的端口,配置mybatis的路径 ...

  9. Bootstrap的本地引入

    今天用前端框架时选择了Bootstrap,然后东西都下好了本地就是引入不进去. 查了一下发现必须jquery要在BootStrap之前引入,然后我更改了引入顺序,发现还是不行 <script s ...

  10. 10.14.1-linux设置时间等

    设置时间[root@wen /]# date -s "20171014 15:42:00"2017年 10月 14日 星期六 15:42:00 CST 格式化时间[root@wen ...