第一章 @Async注解.
第二章 @Retryable注解.
前言
在平时的项目开发中大家应该遇到过下面类似的场景:
- 在用户注册通过的同时给用户发短信。
- 批量推送消息给用户。
- 凌晨系统自动化统计数据。
大家先想几个问题:
- 在用户注册的这个接口中,发送短信的逻辑是否应该占用用户注册的时间?
- 当用户量很大时,如何快速推送?
- 当统计的数据量很大,统计的指标很多,如何加快定时器统计效率?
相信大家应该想到了一些办法,没错,可以用多线程处理。但是手写代码创建和管理线程未免有些繁琐,而且还会让代码变的更复杂。那有没有什么好的办法去实现方法异步呢?SpringBoot提供了一个@Asnyc注解可以很方便快捷的实现异步。
一、@Async是什么?
在Spring中,基于@Async标注的方法,称之为异步方法;这些方法将在执行的时候,将会在独立的线程中被执行,调用者无需等待它的完成,即可继续其他的操作。
二、使用步骤
1.启用@Async
package com.test.async;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync
public class AsyncApplication {
public static void main(String[] args) {
SpringApplication.run(AsyncApplication.class, args);
}
}
2.在方法上添加@Async
@Async
public void sendEmail(SendEmailReq req) {
// do something
// send email
}
这样就能让方法实现异步了,是不是很简单。那有人就会说,你说的这些场景都是无返回值的方法,那如果是有返回值的方法想要异步怎么办?
3.带返回值方法实现异步
/**
* 上传文件
*
* @param fileName
* @param inputStream
* @return 上传后的文件url
*/
@Async
public Future<String> upload(String fileName, InputStream inputStream) {
String key = System.currentTimeMillis() + "/" + fileName;
return new AsyncResult<>(putObject(key,inputStream));
}
获取异步方法的返回值:future.get(),如果线程未执行完成,阻塞调用get()的线程,等待返回结果;
Future<String> future = fileService.upload(sourceFileName, file.getInputStream());
url = future.get();
其实这样调用的话和同步调用就没什么区别了,那是不是带返回值的方法就没办法异步了呢?还是可以的。
- 虽然方法有返回值,当不需要用到这个返回值时,不调用future.get(),那还是能实现异步的效果。(这种场景比较少)
- 既然带了返回值,那一般情况还是需要用到这个返回值的。可能存在这种场景:查询大量数据时速度很慢,我们可以通过@Async实现异步调用,将一次查询分成多次,根据查询的维度,例如查询一个月的数据,可以按天分批查询,最后将数据汇总返回。同步查询消耗的时间为sum(1…N),异步查询的时间为max(1…N)。
List<Future> list=new ArrayList<>(); for (int i = 0; i < days; i++) { // 按天查询数据 Future<String> future = queryService.queryData(i); list.add(future); } // 检测所有查询是否都以执行完成 for (;;) { if (list.stream().allMatch(Future::isDone)) { break; } }
4.线程池
- 默认线程池的弊端
@Async默认异步配置使用的是SimpleAsyncTaskExecutor,该线程池默认来一个任务创建一个线程,若系统中不断的创建线程,最终会导致系统占用内存过高,引发OutOfMemoryError错误。针对线程创建问题,SimpleAsyncTaskExecutor提供了限流机制,通过concurrencyLimit属性来控制开关,当concurrencyLimit>=0时开启限流机制,默认关闭限流机制即concurrencyLimit=-1,当关闭情况下,会不断创建新的线程来处理任务。基于默认配置,SimpleAsyncTaskExecutor并不是严格意义的线程池,达不到线程复用的功能。 - 自定义线程池,可对系统中线程池更加细粒度的控制,方便调整线程池大小配置,线程执行异常控制和处理。
package com.test.async; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableAsync; @SpringBootApplication @EnableAsync public class AsyncApplication { public static void main(String[] args) { SpringApplication.run(AsyncApplication.class, args); } @Configuration class TaskPoolConfig { @Bean("asyncExecutor") public Executor asyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(20); executor.setQueueCapacity(200); executor.setKeepAliveSeconds(60); executor.setThreadNamePrefix("asyncExecutor-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } } }
5. 注意事项
-
默认情况下(即@EnableAsync注解的mode=AdviceMode.PROXY),同一个类内部没有使用@Async注解修饰的方法调用@Async注解修饰的方法,是不会异步执行的,这点跟 @Transitional 注解类似,底层都是通过动态代理实现的。如果想实现类内部自调用也可以异步,则需要切换@EnableAsync注解的mode=AdviceMode.ASPECTJ,详见@EnableAsync注解。
public class TestServiceImpl implements TestService{ @Override @Async public void test() { System.out.println("测试Spring 异步调用!"); } @Override public void test2() { test();//自调用test()方法 } }
或者先获取其代理类在进行类的内部调用(只能是public方法)
@Service public class XxxService{ public void methodA(){ ... XxxService xxxServiceProxy = SpringUtil.getBean(XxxService.class); xxxServiceProxy.methodB(); ... } @Async public void methodB() { ... } }
-
@Async所修饰的函数不要定义为static类型,这样异步调用不会生效。
-
任意参数类型都是支持的,但是方法返回值必须是void或者Future类型。当使用Future时,你可以使用 实现了Future接口的ListenableFuture接口或者CompletableFuture类与异步任务做更好的交互。如果异步方法有返回值,没有使用Future类型的话,调用方获取不到返回值。
总结
如果在业务场景中我们有异步以及需要知道异步方法执行结果的需求,那么@Async以及Future的组合会是个不错的选择。