SpringBoot的四种异步处理,写这篇文章,我自己先学到了

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: SpringBoot的四种异步处理,写这篇文章,我自己先学到了

最近更新了一系列关于异步和回调的文章,比如《一篇文章,搞明白异步和多线程的区别》、《两个经典例子让你彻底理解java回调机制》、《异步请求和异步调用有区别?》,大家感兴趣的话可温习一下。


今天再带大家学习汇总一下SpringBoot中异步处理的4种形式,下面开始正文:


前言

在网络上有关于SpringBoot的异步请求和异步调有两种说法,经过调用这两种说法本质上就是一回事,在《异步请求和异步调用有区别?》一种,已经做过解释了。


同时,我们也知道了“服务实现的异步与同步特性完全独立于客户端调用的异步和同步特性。也就是说客户端可以异步的去调用同步服务,而且客户端也可以同步的去调用异步服务。”


本篇文章我们以SpringBoot中异步的使用(包括:异步调用和异步方法两个维度)来进行讲解。


异步请求与同步请求

我们先通过一张图来区分一下异步请求和同步请求的区别:


image.png在上图中有三个角色:客户端、Web容器和业务处理线程。


两个流程中客户端对Web容器的请求,都是同步的。因为它们在请求客户端时都处于阻塞等待状态,并没有进行异步处理。


在Web容器部分,第一个流程采用同步请求,第二个流程采用异步回调的形式。


通过异步处理,可以先释放容器分配给请求的线程与相关资源,减轻系统负担,从而增加了服务器对客户端请求的吞吐量。但并发请求量较大时,通常会通过负载均衡的方案来解决,而不是异步。


Servlet3.0中的异步

Servlet 3.0之前,Servlet采用Thread-Per-Request的方式处理请求,即每一次Http请求都由一个线程从头到尾处理。当涉及到耗时操作时,性能问题便比较明显。


Servlet 3.0中提供了异步处理请求。可以先释放容器分配给请求的线程与相关资源,减轻系统负担,从而增加服务的吞吐量。


Servlet 3.0的异步是通过AsyncContext对象来完成的,它可以从当前线程传给另一个线程,并归还初始线程。新的线程处理完业务可以直接返回结果给客户端。


AsyncContext对象可以从HttpServletRequest中获取:


@RequestMapping("/async")

public void async(HttpServletRequest request) {

   AsyncContext asyncContext = request.getAsyncContext();

}


在AsyncContext中提供了获取ServletRequest、ServletResponse和添加监听(addListener)等功能:


public interface AsyncContext {
    ServletRequest getRequest();
    ServletResponse getResponse();
    void addListener(AsyncListener var1);
    void setTimeout(long var1);
    // 省略其他方法
}

不仅可以通过AsyncContext获取Request和Response等信息,还可以设置异步处理超时时间。通常,超时时间(单位毫秒)是需要设置的,不然无限等下去不就与同步处理一样了。


通过AsyncContext的addListener还可以添加监听事件,用来处理异步线程的开始、完成、异常、超时等事件回调。


addListener方法的参数AsyncListener的源码如下:


public interface AsyncListener extends EventListener {
    // 异步执行完毕时调用
    void onComplete(AsyncEvent var1) throws IOException;
    // 异步线程执行超时调用
    void onTimeout(AsyncEvent var1) throws IOException;
    // 异步线程出错时调用
    void onError(AsyncEvent var1) throws IOException;
    // 异步线程开始时调用
    void onStartAsync(AsyncEvent var1) throws IOException;
}

通常,异常或超时时返回调用方错误信息,而异常时会处理一些清理和关闭操作或记录异常日志等。

基于Servlet方式实现异步请求

下面直接看一个基于Servlet方式的异步请求示例:

@GetMapping(value = "/email/send")
public void servletReq(HttpServletRequest request) {
    AsyncContext asyncContext = request.startAsync();
    // 设置监听器:可设置其开始、完成、异常、超时等事件的回调处理
    asyncContext.addListener(new AsyncListener() {
        @Override
        public void onTimeout(AsyncEvent event) {
            System.out.println("处理超时了...");
        }
        @Override
        public void onStartAsync(AsyncEvent event) {
            System.out.println("线程开始执行");
        }
        @Override
        public void onError(AsyncEvent event) {
            System.out.println("执行过程中发生错误:" + event.getThrowable().getMessage());
        }
        @Override
        public void onComplete(AsyncEvent event) {
            System.out.println("执行完成,释放资源");
        }
    });
    //设置超时时间
    asyncContext.setTimeout(6000);
    asyncContext.start(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(5000);
                System.out.println("内部线程:" + Thread.currentThread().getName());
                asyncContext.getResponse().getWriter().println("async processing");
            } catch (Exception e) {
                System.out.println("异步处理发生异常:" + e.getMessage());
            }
            // 异步请求完成通知,整个请求完成
            asyncContext.complete();
        }
    });
    //此时request的线程连接已经释放了
    System.out.println("主线程:" + Thread.currentThread().getName());
}

启动项目,访问对应的URL,打印日志如下:


主线程:http-nio-8080-exec-4

内部线程:http-nio-8080-exec-5

执行完成,释放资源

1

2

3

可以看出,上述代码先执行完了主线程,也就是程序的最后一行代码的日志打印,然后才是内部线程的执行。内部线程执行完成,AsyncContext的onComplete方法被调用。


如果通过浏览器访问对应的URL,还可以看到该方法的返回值“async processing”。说明内部线程的结果同样正常的返回到客户端了。


基于Spring实现异步请求

基于Spring可以通过Callable、DeferredResult或者WebAsyncTask等方式实现异步请求。


基于Callable实现

对于一次请求(/email),基于Callable的处理流程如下:


1、Spring MVC开启副线程处理业务(将Callable提交到TaskExecutor);


2、DispatcherServlet和所有的Filter退出Web容器的线程,但是response保持打开状态;


3、Callable返回结果,SpringMVC将原始请求重新派发给容器(再重新请求一次/email),恢复之前的处理;


4、DispatcherServlet重新被调用,将结果返回给用户;


代码实现示例如下:


\

@GetMapping("/email")
public Callable<String> order() {
    System.out.println("主线程开始:" + Thread.currentThread().getName());
    Callable<String> result = () -> {
        System.out.println("副线程开始:" + Thread.currentThread().getName());
        Thread.sleep(1000);
        System.out.println("副线程返回:" + Thread.currentThread().getName());
        return "success";
    };
    System.out.println("主线程返回:" + Thread.currentThread().getName());
    return result;
}

访问对应URL,控制台输入日志如下:


主线程开始:http-nio-8080-exec-1

主线程返回:http-nio-8080-exec-1

副线程开始:task-1

副线程返回:task-1

1

2

3

4

通过日志可以看出,主线程已经完成了,副线程才进行执行。同时,URL返回结果“success”。这也说明一个问题,服务器端的异步处理对客户端来说是不可见的。


Callable默认使用SimpleAsyncTaskExecutor类来执行,这个类非常简单而且没有重用线程。在实践中,需要使用AsyncTaskExecutor类来对线程进行配置。


这里通过实现WebMvcConfigurer接口来完成线程池的配置。


@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Resource
    private ThreadPoolTaskExecutor myThreadPoolTaskExecutor;
    /**
     * 配置线程池
     */
    @Bean(name = "asyncPoolTaskExecutor")
    public ThreadPoolTaskExecutor getAsyncThreadPoolTaskExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(2);
        taskExecutor.setMaxPoolSize(10);
        taskExecutor.setQueueCapacity(25);
        taskExecutor.setKeepAliveSeconds(200);
        taskExecutor.setThreadNamePrefix("thread-pool-");
        // 线程池对拒绝任务(无线程可用)的处理策略,目前只支持AbortPolicy、CallerRunsPolicy;默认为后者
        taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        taskExecutor.initialize();
        return taskExecutor;
    }
    @Override
    public void configureAsyncSupport(final AsyncSupportConfigurer configurer) {
        // 处理callable超时
        configurer.setDefaultTimeout(60 * 1000);
        configurer.setTaskExecutor(myThreadPoolTaskExecutor);
        configurer.registerCallableInterceptors(timeoutCallableProcessingInterceptor());
    }
    @Bean
    public TimeoutCallableProcessingInterceptor timeoutCallableProcessingInterceptor() {
        return new TimeoutCallableProcessingInterceptor();
    }
}

为了验证打印的线程,我们将实例代码中的System.out.println替换成日志输出,会发现在使用线程池之前,打印日志如下:

2021-02-21 09:45:37.144  INFO 8312 --- [nio-8080-exec-1] c.s.learn.controller.AsynController      : 主线程开始:http-nio-8080-exec-1
2021-02-21 09:45:37.144  INFO 8312 --- [nio-8080-exec-1] c.s.learn.controller.AsynController      : 主线程返回:http-nio-8080-exec-1
2021-02-21 09:45:37.148  INFO 8312 --- [         task-1] c.s.learn.controller.AsynController      : 副线程开始:task-1
2021-02-21 09:45:38.153  INFO 8312 --- [         task-1] c.s.learn.controller.AsynController      : 副线程返回:task-1

线程名称为“task-1”。让线程池生效之后,打印日志如下:

2021-02-21 09:50:28.950  INFO 8339 --- [nio-8080-exec-1] c.s.learn.controller.AsynController      : 主线程开始:http-nio-8080-exec-1
2021-02-21 09:50:28.951  INFO 8339 --- [nio-8080-exec-1] c.s.learn.controller.AsynController      : 主线程返回:http-nio-8080-exec-1
2021-02-21 09:50:28.955  INFO 8339 --- [  thread-pool-1] c.s.learn.controller.AsynController      : 副线程开始:thread-pool-1
2021-02-21 09:50:29.956  INFO 8339 --- [  thread-pool-1] c.s.learn.controller.AsynController      : 副线程返回:thread-pool-1

线程名称为“thread-pool-1”,其中前面的“thread-pool”正是我们配置的线程池前缀。


除了线程池的配置,还可以配置统一异常处理,这里就不再演示了。


基于WebAsyncTask实现

Spring提供的WebAsyncTask是对Callable的包装,提供了更强大的功能,比如:处理超时回调、错误回调、完成回调等。


@GetMapping("/webAsyncTask")
public WebAsyncTask<String> webAsyncTask() {
    log.info("外部线程:" + Thread.currentThread().getName());
    WebAsyncTask<String> result = new WebAsyncTask<>(60 * 1000L, new Callable<String>() {
        @Override
        public String call() {
            log.info("内部线程:" + Thread.currentThread().getName());
            return "success";
        }
    });
    result.onTimeout(new Callable<String>() {
        @Override
        public String call() {
            log.info("timeout callback");
            return "timeout callback";
        }
    });
    result.onCompletion(new Runnable() {
        @Override
        public void run() {
            log.info("finish callback");
        }
    });
    return result;
}

访问对应请求,打印日志:


2021-02-21 10:22:33.028  INFO 8547 --- [nio-8080-exec-1] c.s.learn.controller.AsynController      : 外部线程:http-nio-8080-exec-1

2021-02-21 10:22:33.033  INFO 8547 --- [  thread-pool-1] c.s.learn.controller.AsynController      : 内部线程:thread-pool-1

2021-02-21 10:22:33.055  INFO 8547 --- [nio-8080-exec-2] c.s.learn.controller.AsynController      : finish callback

1

2

3

基于DeferredResult实现

DeferredResult使用方式与Callable类似,但在返回结果时不一样,它返回的时实际结果可能没有生成,实际的结果可能会在另外的线程里面设置到DeferredResult中去。


DeferredResult的这个特性对实现服务端推技术、订单过期时间处理、长轮询、模拟MQ的功能等高级应用非常重要。


关于DeferredResult的使用先来看一下官方的例子和说明:


@RequestMapping("/quotes")
@ResponseBody
public DeferredResult<String> quotes() {
  DeferredResult<String> deferredResult = new DeferredResult<String>();
  // Save the deferredResult in in-memory queue ...
  return deferredResult;
}
// In some other thread...
deferredResult.setResult(data);

上述示例中我们可以发现DeferredResult的调用并不一定在Spring MVC当中,它可以是别的线程。官方的解释也是如此:


In this case the return value will also be produced from a separate thread. However, that thread is not known to Spring MVC. For example the result may be produced in response to some external event such as a JMS message, a scheduled task, etc.


也就是说,DeferredResult返回的结果也可能是由MQ、定时任务或其他线程触发。来个实例:


@Controller
@RequestMapping("/async/controller")
public class AsyncHelloController {
    private List<DeferredResult<String>> deferredResultList = new ArrayList<>();
    @ResponseBody
    @GetMapping("/hello")
    public DeferredResult<String> helloGet() throws Exception {
        DeferredResult<String> deferredResult = new DeferredResult<>();
        //先存起来,等待触发
        deferredResultList.add(deferredResult);
        return deferredResult;
    }
    @ResponseBody
    @GetMapping("/setHelloToAll")
    public void helloSet() throws Exception {
        // 让所有hold住的请求给与响应
        deferredResultList.forEach(d -> d.setResult("say hello to all"));
    }
}

第一个请求/hello,会先将deferredResult存起来,前端页面是一直等待(转圈)状态。直到发第二个请求:setHelloToAll,所有的相关页面才会有响应。


整个执行流程如下:


controller返回一个DeferredResult,把它保存到内存里或者List里面(供后续访问);

Spring MVC调用request.startAsync(),开启异步处理;

与此同时将DispatcherServlet里的拦截器、Filter等等都马上退出主线程,但是response仍然保持打开的状态;

应用通过另外一个线程(可能是MQ消息、定时任务等)给DeferredResult#setResult值。然后SpringMVC会把这个请求再次派发给servlet容器;

DispatcherServlet再次被调用,然后处理后续的标准流程;

通过上述流程可以发现:利用DeferredResult可实现一些长连接的功能,比如当某个操作是异步时,可以先保存对应的DeferredResult对象,当异步通知回来时,再找到这个DeferredResult对象,在setResult处理结果即可。从而提高性能。


SpringBoot中的异步实现

在SpringBoot中将一个方法声明为异步方法非常简单,只需两个注解即可@EnableAsync和@Async。其中@EnableAsync用于开启SpringBoot支持异步的功能,用在SpringBoot的启动类上。@Async用于方法上,标记该方法为异步处理方法。


需要注意的是@Async并不支持用于被@Configuration注解的类的方法上。同一个类中,一个方法调用另外一个有@Async的方法,注解也是不会生效的。


@EnableAsync的使用示例:


@SpringBootApplication

@EnableAsync

public class App {


   public static void main(String[] args) {

       SpringApplication.run(App.class, args);

   }

}


@Async的使用示例:


@Service

public class SyncService {

 

   @Async

   public void asyncEvent() {

       // 业务处理

   }

}


@Async注解的使用与Callable有类似之处,在默认情况下使用的都是SimpleAsyncTaskExecutor线程池,可参考Callable中的方式来自定义线程池。


下面通过一个实例来验证一下,启动类上使用@EnableAsync,然后定义Controller类:


@RestController
public class IndexController {
    @Resource
    private UserService userService;
    @RequestMapping("/async")
    public String async(){
        System.out.println("--IndexController--1");
        userService.sendSms();
        System.out.println("--IndexController--4");
        return "success";
    }
}

定义Service及异步方法:

@Service
public class UserService {
    @Async
    public void sendSms(){
        System.out.println("--sendSms--2");
        IntStream.range(0, 5).forEach(d -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println("--sendSms--3");
    }
}

如果先注释掉@EnableAsync和@Async注解,即正常情况下的业务请求,打印日志为:

--IndexController--1
--sendSms--2
--sendSms--3
--IndexController--4

使用@EnableAsync和@Async注解时,打印日志如下:

--IndexController--1
--IndexController--4
--sendSms--2
--sendSms--3

通过日志的对比我们可以看出,使用了@Async的方法,会被当成一个子线程。所以,整个sendSms方法会在主线程执行完了之后执行。


这样的效果是不是跟我们上面使用的其他形式的异步异曲同工?所以在文章最开始已经说到,网络上所谓的“异步调用与异步请求的区别”是并不存储在的,本质上都是一回事,只不过实现形式不同而已。这里所提到异步方法,也就是将方法进行异步处理而已。


@Async、WebAsyncTask、Callable、DeferredResult的区别

所在的包不同:


@Async:org.springframework.scheduling.annotation;

WebAsyncTask:org.springframework.web.context.request.async;

Callable:java.util.concurrent;

DeferredResult:org.springframework.web.context.request.async;

通过所在的包,我们应该隐隐约约感到一些区别,比如@Async是位于scheduling包中,而WebAsyncTask和DeferredResult是用于Web(Spring MVC)的,而Callable是用于concurrent(并发)处理的。


对于Callable,通常用于Controller方法的异步请求,当然也可以用于替换Runable的方式。在方法的返回上与正常的方法有所区别:


// 普通方法
public String aMethod(){
}
// 对照Callable方法
public Callable<String>  aMethod(){
}

而WebAsyncTask是对Callable的封装,提供了一些事件回调的处理,本质上区别不大。


DeferredResult使用方式与Callable类似,重点在于跨线程之间的通信。


@Async也是替换Runable的一种方式,可以代替我们自己创建线程。而且适用的范围更广,并不局限于Controller层,而可以是任何层的方法上。


当然,大家也可以从返回结果,异常处理等角度来分析一下,这里就不再展开了。


小结

经过上述的一步步分析,大家应该对于Servlet3.0及Spring中对异步从处理有所了解。当了解了这些基础理论,实战实例,使用方法及注意事项之后,想必更能够对网络上的相关知识能够进一步的去伪存真。


尽信书则不如无书,带大家一起学习,一起研究,一起去伪存真,追求真正有用的知识。


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
3月前
|
前端开发 Java 应用服务中间件
SpringBoot异步接口怎么实现?
### 前言 Servlet 3.0以前,每个HTTP请求由单一线程全程处理;3.0版本引入异步处理,允许提前释放容器线程,提升系统吞吐量。
|
5月前
|
监控 Java 应用服务中间件
SpringBoot3 快速入门及原理分析
SpringBoot3 快速入门及原理分析
|
5月前
|
Java API 开发者
springboot 多线程的使用原理与实战
在Spring Boot中实现多线程,主要依赖于Spring框架的@Async注解以及底层Java的并发框架。这里将深入剖析Spring Boot多线程的原理,包括@Async注解的工作方式、任务执行器的角色以及如何通过配置来调整线程行为。
553 5
|
5月前
|
Java Spring
SpringBoot - 优雅的实现【异步编程】
SpringBoot - 优雅的实现【异步编程】
87 0
SpringBoot进阶:4招优雅实现SpringBoot异步线程间数据传递
Spring Boot 自定义线程池实现异步开发相信看过陈某的文章都了解,但是在实际开发中需要在父子线程之间传递一些数据,比如用户信息,链路信息等等 比如用户登录信息使用ThreadLocal存放保证线程隔离,代码如下:
|
开发框架 监控 Java
SpringBoot多线程极简教程
往往当我们谈论多线程的时候,总会提起一个词“异步”,其实多线程不应该等于异步,异步意味着解耦,虽然多线程本身解耦,但是能实现异步的方式实在太多太多了,如发布/订阅,mq......,所以,一般来说我更倾向于将多线程划归实现异步的一种方式。
1279 0
|
消息中间件 运维 Java
SpringBoot开发秘籍 - 事件异步处理
SpringBoot开发秘籍 - 事件异步处理
195 0
|
缓存 NoSQL Java
SpringBoot 如何异步编程,老鸟们都这么玩的
首先我们来看看在Spring中为什么要使用异步编程,它能解决什么问题?
472 1
SpringBoot 如何异步编程,老鸟们都这么玩的
|
存储 负载均衡 前端开发
SpringBoot的四种异步处理,写这篇文章,我自己先学到了
SpringBoot的四种异步处理,写这篇文章,我自己先学到了
261 0
SpringBoot的四种异步处理,写这篇文章,我自己先学到了
|
消息中间件 缓存 Java
SpringBoot学习笔记(十七:异步调用)
SpringBoot学习笔记(十七:异步调用)
156 0
SpringBoot学习笔记(十七:异步调用)