Spring @Async 实战:异步方法、线程池配置与常见失效场景
@Async 是 Spring 里很常用的异步注解。
它的作用很直观:把一个方法交给线程池执行,让调用方不用一直等这个方法执行完。
比如下单成功后:
- 发短信;
- 发邮件;
- 推送站内信;
- 写操作日志;
- 同步第三方系统;
- 生成报表;
- 发送业务通知。
这些任务通常不是主流程必须立即完成的事情,就可以考虑异步执行。
不过,@Async 看起来简单,实际项目里坑不少。比如:同一个类里调用不生效、线程池没配置、异常丢了、事务边界不清楚、异步任务失败后没有补偿。
这篇专门把 @Async 拎出来整理一下。
如果你想先了解线程池基础,可以看:并发编程二:线程池、JUC 工具与异步编程。
@Async 适合做什么
@Async 适合处理“不影响主流程立即返回”的任务。
典型场景:
| 场景 | 说明 |
|---|---|
| 短信 / 邮件通知 | 下单成功后通知用户 |
| 操作日志 | 记录用户行为,不阻塞主接口 |
| 数据同步 | 同步到搜索、统计、第三方系统 |
| 文件处理 | 上传后异步解析或生成缩略图 |
| 报表导出 | 提交导出任务后后台慢慢处理 |
| 站内消息 | 业务成功后推送通知 |
但有些逻辑不适合随便异步化:
- 扣库存;
- 创建订单;
- 支付状态变更;
- 核心事务提交;
- 需要强一致返回结果的逻辑。
一句话:
能慢一点完成,但不能影响主流程返回的任务,才适合优先考虑
@Async。
最小使用示例
第一步,开启异步支持:
@EnableAsync@Configurationpublic class AsyncConfig {}第二步,在 Spring 管理的 Bean 方法上加 @Async:
@Servicepublic class MessageService {
@Async public void sendOrderMessage(Long orderId) { System.out.println("发送订单通知:" + orderId); }}第三步,在其他 Bean 里调用:
@Servicepublic class OrderService {
private final MessageService messageService;
public OrderService(MessageService messageService) { this.messageService = messageService; }
public void createOrder(Long orderId) { // 创建订单主流程 messageService.sendOrderMessage(orderId); }}调用 sendOrderMessage() 时,主线程不会等待它执行完成,而是把任务交给异步线程处理。
一定要配置自定义线程池
生产环境不建议直接裸用 @Async。
更推荐配置一个明确的线程池:
@EnableAsync@Configurationpublic class AsyncConfig {
@Bean("notificationExecutor") public ThreadPoolTaskExecutor notificationExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(4); executor.setMaxPoolSize(8); executor.setQueueCapacity(500); executor.setThreadNamePrefix("notification-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; }}然后在 @Async 上指定线程池:
@Async("notificationExecutor")public void sendOrderMessage(Long orderId) { System.out.println("发送订单通知:" + orderId);}这样做有几个好处:
- 线程数量可控;
- 队列长度可控;
- 线程名方便排查日志;
- 不同业务可以隔离线程池;
- 拒绝策略更清楚。
不要把所有异步任务都丢进同一个线程池。短信、报表、文件处理、日志写入最好按业务拆开,否则一个慢任务可能拖垮所有异步任务。
返回值怎么处理
@Async 方法可以没有返回值:
@Async("notificationExecutor")public void sendMessage(Long userId) { // 发送消息}也可以返回 CompletableFuture:
@Async("notificationExecutor")public CompletableFuture<String> queryMessageStatus(Long messageId) { String status = "SUCCESS"; return CompletableFuture.completedFuture(status);}调用方可以这样拿结果:
CompletableFuture<String> future = messageService.queryMessageStatus(1001L);String status = future.join();不过,如果调用方马上 join(),其实就又等回来了。
所以需要返回值时要想清楚:
- 是不是必须等待结果;
- 能不能前端轮询任务状态;
- 是否应该用 MQ 或任务表;
- 失败后如何重试。
异常不会像同步方法那样抛回来
这是 @Async 很容易忽略的地方。
同步方法抛异常,调用方可以直接感知:
messageService.sendOrderMessage(orderId);但异步方法在另一个线程里执行,异常不会直接抛回主线程。
对于 void 返回值的异步方法,可以配置异常处理器:
@Configurationpublic class AsyncExceptionConfig implements AsyncConfigurer {
@Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return (ex, method, params) -> { log.error("async method failed, method={}", method.getName(), ex); }; }}对于 CompletableFuture,可以在任务链上处理:
messageService.queryMessageStatus(1001L) .exceptionally(ex -> { log.error("query message status failed", ex); return "UNKNOWN"; });生产环境里,异步任务失败不能只打一行日志就结束。
更稳的做法是结合:
- 失败记录表;
- 重试机制;
- 告警;
- 幂等处理;
- 人工补偿入口。
常见失效场景
@Async 失效,大多数都和 Spring AOP 代理有关。
1. 同一个类内部调用
错误示例:
@Servicepublic class OrderService {
public void createOrder(Long orderId) { sendMessage(orderId); }
@Async public void sendMessage(Long orderId) { System.out.println("发送消息"); }}这种写法通常不会异步执行。
原因是 createOrder() 里调用的是 this.sendMessage(),没有经过 Spring 代理。
更推荐拆到另一个 Bean:
@Servicepublic class MessageService {
@Async("notificationExecutor") public void sendMessage(Long orderId) { System.out.println("发送消息"); }}再由 OrderService 注入调用。
2. 方法不是 public
@Async 通常应该放在 public 方法上。
@Asyncprivate void sendMessage(Long orderId) { // 不推荐}非 public 方法可能无法被代理正常拦截。
3. 类没有交给 Spring 管理
如果类不是 Spring Bean,@Async 不会生效。
错误示例:
MessageService messageService = new MessageService();messageService.sendMessage(orderId);这种对象是自己 new 出来的,不受 Spring 容器管理。
应该通过依赖注入获取:
private final MessageService messageService;4. 没有开启 @EnableAsync
没有 @EnableAsync,Spring 不会启用异步注解处理。
@EnableAsync@Configurationpublic class AsyncConfig {}这是最基础但也很常见的遗漏。
5. 事务边界理解错
看这个例子:
@Transactionalpublic void createOrder(Order order) { orderMapper.insert(order); messageService.sendOrderMessage(order.getId());}sendOrderMessage() 是异步的,它可能在事务提交前就开始执行。
如果异步方法里立刻查订单,可能查不到,或者读到未完成状态。
更稳的方式是:
- 事务提交后再触发异步任务;
- 使用事务事件
@TransactionalEventListener; - 或者写本地消息表,提交后由后台任务发送。
例如:
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)public void handleOrderCreated(OrderCreatedEvent event) { messageService.sendOrderMessage(event.orderId());}这个点很关键:
@Async只负责换线程,不负责保证事务已经提交。
@Async 和 MQ 怎么选
@Async 和 MQ 都能做异步,但定位不一样。
| 对比 | @Async | MQ |
|---|---|---|
| 部署复杂度 | 低 | 高 |
| 可靠性 | 一般 | 更强 |
| 跨服务 | 不适合 | 适合 |
| 失败重试 | 需要自己做 | 通常有机制 |
| 削峰能力 | 弱 | 强 |
| 适合场景 | 单服务内轻量异步 | 跨服务、可靠异步、削峰 |
简单任务可以用 @Async,比如发通知、写日志、刷新缓存。
如果是订单创建后驱动多个系统,或者任务不能丢,优先考虑 MQ 或本地消息表。
生产环境建议
使用 @Async 时,我会重点检查这些点:
- 是否配置了自定义线程池;
- 线程池是否按业务隔离;
- 队列是否有上限;
- 拒绝策略是否明确;
- 异常是否能记录和告警;
- 失败后是否能重试;
- 异步任务是否幂等;
- 是否依赖事务提交后的数据;
- 是否存在同类内部调用导致不生效;
- 日志里是否能看到线程名和业务 ID。
一个比较稳的异步任务日志:
log.info("send order message start, orderId={}", orderId);try { // do send log.info("send order message success, orderId={}", orderId);} catch (Exception e) { log.error("send order message failed, orderId={}", orderId, e); throw e;}有业务 ID,后面排查问题会轻松很多。
总结
@Async 本身不复杂,但它不是“加个注解就万事大吉”。
它真正要注意的是这些问题:
- 是否经过 Spring 代理;
- 是否配置了合适的线程池;
- 异常有没有处理;
- 任务失败能不能重试;
- 任务是否幂等;
- 事务是否已经提交;
- 是否应该升级成 MQ 或任务表。
简单异步可以用 @Async,可靠异步要考虑 MQ、本地消息表和补偿机制。
记住一句话:
@Async只负责让方法换个线程执行,不负责帮你保证可靠性。
如果任务失败会影响业务结果,就不要只靠 @Async 硬扛。
If this article helped you, please share it with others!
Some information may be outdated






