Spring Boot中异步请求的使用
一、异步请求与同步请求


特点:
可以先释放容器分配给请求的线程与相关资源,减轻系统负担,释放了容器所分配线程的请求,其响应将被延后,可以在耗时处理完成(例如长时间的运算)时再对客户端进行响应。
一句话:增加了服务器对客户端请求的吞吐量(实际生产上我们用的比较少,如果并发请求量很大的情况下,我们会通过nginx把请求负载到集群服务的各个节点上来分摊请求压力,当然还可以通过消息队列来做请求的缓冲)。
二、异步请求的实现
方式一:Servlet方式实现异步请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| @GetMapping("/servletReq") public void servletReq (HttpServletRequest request, HttpServletResponse response) { AsyncContext asyncContext = request.startAsync(); asyncContext.addListener(new AsyncListener() { @Override public void onTimeout(AsyncEvent event) throws IOException { System.out.println("超时了..."); } @Override public void onStartAsync(AsyncEvent event) throws IOException { System.out.println("线程开始"); } @Override public void onError(AsyncEvent event) throws IOException { System.out.println("发生错误:"+event.getThrowable()); } @Override public void onComplete(AsyncEvent event) throws IOException { System.out.println("执行完成"); } }); asyncContext.setTimeout(20000); asyncContext.start(new Runnable() { @Override public void run() { try { Thread.sleep(10000); System.out.println("内部线程:" + Thread.currentThread().getName()); asyncContext.getResponse().setCharacterEncoding("utf-8"); asyncContext.getResponse().setContentType("text/html;charset=UTF-8"); asyncContext.getResponse().getWriter().println("这是异步的请求返回"); } catch (Exception e) { System.out.println("异常:"+e); } asyncContext.complete(); } }); System.out.println("主线程:" + Thread.currentThread().getName()); }
|

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @GetMapping("/callableReq") @ResponseBody public Callable<String> callableReq () { System.out.println("外部线程:" + Thread.currentThread().getName());
return new Callable<String>() {
@Override public String call() throws Exception { Thread.sleep(10000); System.out.println("内部线程:" + Thread.currentThread().getName()); return "callable!"; } }; }
|

方式三:和方式二差不多,在Callable外包一层,给WebAsyncTask设置一个超时回调,即可实现超时处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| @GetMapping("/webAsyncReq") @ResponseBody public WebAsyncTask<String> webAsyncReq () { System.out.println("外部线程:" + Thread.currentThread().getName()); Callable<String> result = () -> { System.out.println("内部线程开始:" + Thread.currentThread().getName()); try { TimeUnit.SECONDS.sleep(4); } catch (Exception e) { } System.out.println("副线程返回"); System.out.println("内部线程返回:" + Thread.currentThread().getName()); return "success"; }; WebAsyncTask<String> wat = new WebAsyncTask<String>(3000L, result); wat.onTimeout(new Callable<String>() {
@Override public String call() throws Exception { return "超时"; } }); return wat; }
|

方式四:DeferredResult可以处理一些相对复杂一些的业务逻辑,最主要还是可以在另一个线程里面进行业务处理及返回,即可在两个完全不相干的线程间的通信。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| @Resource private ThreadPoolTaskExecutor myThreadPoolTaskExecutor;
@GetMapping("/deferredResultReq") @ResponseBody public DeferredResult<String> deferredResultReq () { System.out.println("外部线程:" + Thread.currentThread().getName()); DeferredResult<String> result = new DeferredResult<String>(60*1000L); result.onTimeout(new Runnable() {
@Override public void run() { System.out.println("DeferredResult超时"); result.setResult("超时了!"); } }); result.onCompletion(new Runnable() {
@Override public void run() { System.out.println("调用完成"); } }); myThreadPoolTaskExecutor.execute(new Runnable() {
@Override public void run() { System.out.println("内部线程:" + Thread.currentThread().getName()); result.setResult("DeferredResult!!"); } }); return result; }
|

Spring Boot中异步调用的使用
一、介绍
异步请求的处理。除了异步请求,一般上我们用的比较多的应该是异步调用。通常在开发过程中,会遇到一个方法是和实际业务无关的,没有紧密性的。比如记录日志信息等业务。这个时候正常就是启一个新线程去做一些业务处理,让主线程异步的执行其他业务。
二、使用方式
需要在启动类加入@EnableAsync使异步调用@Async注解生效
在需要异步执行的方法上加入此注解即可@Async(“threadPool”),threadPool为自定义线程池。
三、注意事项
在默认情况下,未设置TaskExecutor时,默认是使用SimpleAsyncTaskExecutor这个线程池,但此线程不是真正意义上的线程池,因为线程不重用,每次调用都会创建一个新的线程。可通过控制台日志输出可以看出,每次输出线程名都是递增的。所以最好我们来自定义一个线程池。
调用的异步方法,不能为同一个类的方法(包括同一个类的内部类),简单来说,因为Spring在启动扫描时会为其创建一个代理类,而同类调用时,还是调用本身的代理类的,所以和平常调用是一样的。
其他的注解如@Cache等也是一样的道理,说白了,就是Spring的代理机制造成的。所以在开发中,最好把异步服务单独抽出一个类来管理
四、什么情况下会导致@Async异步方法会失效?
调用同一个类下注有@Async异步方法:
在spring中像@Async和@Transactional、cache等注解本质使用的是动态代理,其实Spring容器在初始化的时候Spring容器会将含有AOP注解的类对象“替换”为代理对象(简单这么理解),那么注解失效的原因就很明显了,就是因为调用方法的是对象本身而不是代理对象,因为没有经过Spring容器,那么解决方法也会沿着这个思路来解决。
调用的是静态(static )方法
调用(private)私有化方法
五、解决四中问题1的方式
将要异步执行的方法单独抽取成一个类,原理就是当你把执行异步的方法单独抽取成一个类的时候,这个类肯定是被Spring管理的,其他Spring组件需要调用的时候肯定会注入进去,这时候实际上注入进去的就是代理类了。
其实我们的注入对象都是从Spring容器中给当前Spring组件进行成员变量的赋值,由于某些类使用了AOP注解,那么实际上在Spring容器中实际存在的是它的代理对象。那么我们就可以通过上下文获取自己的代理对象调用异步方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| @Autowired private ApplicationContext applicationContext;
@GetMapping("/asyncCall") @ResponseBody public Map<String, Object> asyncCall () { Map<String, Object> resMap = new HashMap<String, Object>(); try{ AsynchronousController proxy = applicationContext.getBean(AsynchronousController.class); proxy.testAsyncTask(); resMap.put("code",200); }catch (Exception e) { resMap.put("code",400); System.out.println("error!"+e.getMessage()); } return resMap; }
@Async public void testAsyncTask() throws InterruptedException { Thread.sleep(10000); System.out.println("异步任务执行完成!"); }
|
开启cglib代理,手动获取Spring代理类,从而调用同类下的异步方法。首先,在启动类上加上@EnableAspectJAutoProxy(exposeProxy = true)注解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| @Service @Transactional(value = "transactionManager", readOnly = false, propagation = Propagation.REQUIRED, rollbackFor = Throwable.class) public class AsynService {
@Autowired private ApplicationContext applicationContext;
@Async public void testSyncTask() throws InterruptedException { Thread.sleep(10000); System.out.println("异步任务执行完成!"); }
public void asyncCallTwo() throws InterruptedException { boolean isAop = AopUtils.isAopProxy(AsynchronousController.class); boolean isCglib = AopUtils.isCglibProxy(AsynchronousController.class); boolean isJdk = AopUtils.isJdkDynamicProxy(AsynchronousController.class); AsynService emailService = (AsynService)applicationContext.getBean(AsynService.class); AsynService proxy = (AsynService) AopContext.currentProxy(); System.out.println(emailService == proxy ? true : false); proxy.testSyncTask(); System.out.println("end!!!"); } }
|

异步请求与异步调用的区别
两者的使用场景不同,异步请求用来解决并发请求对服务器造成的压力,从而提高对请求的吞吐量;而异步调用是用来做一些非主线流程且不需要实时计算和响应的任务,比如同步日志到kafka中做日志分析等。
异步请求是会一直等待response相应的,需要返回结果给客户端的;而异步调用我们往往会马上返回给客户端响应,完成这次整个的请求,至于异步调用的任务后台自己慢慢跑就行,客户端不会关心。
参考文档:
作者:Java圈子
链接:https://juejin.cn/post/6844904150841425934
来源:稀土掘金
仅用于学习交流,如有侵权,请联系删除。