在开发企业级应用时,我们经常遇到需要执行耗时任务的场景:发送邮件、处理报表、同步第三方数据等。如果这些任务都在主线程中同步执行,不仅会影响用户体验,还可能导致系统超时崩溃。
Spring Boot 提供了强大的异步任务处理能力,通过 @Async 注解就能轻松实现。但看似简单的背后,却隐藏着不少坑。今天我们就来聊聊 Spring Boot 异步任务的最佳实践。
一、基础配置:启用异步支持
首先,需要在启动类或配置类上添加 @EnableAsync 注解:
@SpringBootApplication
@EnableAsync
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
然后,在需要异步执行的方法上添加 @Async 注解即可:
@Service
public class EmailService {
@Async
public void sendEmail(String to, String subject, String content) {
emailSender.send(to, subject, content);
}
}
二、第一个坑:同类调用失效
这是最常见的坑。如果你在同一个类中调用 @Async 方法,异步会失效:
@Service
public class OrderService {
@Async
public void processOrder(Order order) {
// 异步处理订单
}
public void createOrder(Order order) {
// 错误:同类调用,异步失效
this.processOrder(order);
}
}
原因:Spring AOP 基于代理实现,同类内部方法调用不会经过代理,所以 @Async 失效。
解决方案:将异步方法提取到独立的 Service 中,或者通过 ApplicationContext 获取代理对象。
三、线程池配置:拒绝默认
默认情况下,Spring Boot 使用 SimpleAsyncTaskExecutor,每次调用都创建新线程。在高并发场景下,这会导致线程数量爆炸。
最佳实践:自定义线程池,控制并发数量:
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("Async-");
executor.setRejectedExecutionHandler(
new ThreadPoolExecutor.CallerRunsPolicy()
);
executor.initialize();
return executor;
}
}
四、异常处理:别让错误静默消失
@Async 方法抛出的异常不会传播到调用者,如果不处理,错误会静默消失。
解决方案:实现 AsyncUncaughtExceptionHandler:
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) -> {
log.error("异步任务执行异常: {} - {}",
method.getName(), ex.getMessage(), ex);
};
}
五、返回值处理:需要等待结果怎么办?
如果需要等待异步任务的结果,可以返回 CompletableFuture:
@Async
public CompletableFuture processData(String data) {
String result = heavyProcess(data);
return CompletableFuture.completedFuture(result);
}
// 调用方
CompletableFuture future = service.processData("test");
String result = future.get(10, TimeUnit.SECONDS);
六、最佳实践总结
- 禁止同类调用:异步方法必须通过外部调用
- 自定义线程池:根据业务场景配置合理的线程数
- 异常必须处理:实现全局异常处理器
- 合理设置超时:避免无限等待导致资源耗尽
- 监控线程池状态:使用 Micrometer 或自定义监控
- 任务幂等设计:考虑任务重试和重复执行的场景
写在最后
异步任务看起来简单,但要用好并不容易。在实际项目中,建议根据业务场景选择合适的方案:简单的定时任务可以用 @Async,复杂的分布式任务调度可以考虑 XXL-Job、Elastic-Job 等框架。
记住:异步不是为了炫技,而是为了解决实际问题。在享受性能提升的同时,别忘了处理异常、监控状态、设计降级方案。
希望这篇文章对你有帮助,欢迎留言讨论你在异步任务处理中遇到的坑。