mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1828 words
5 minutes
Spring @Async 实战:异步方法、线程池配置与常见失效场景
2025-04-21

Spring @Async 实战:异步方法、线程池配置与常见失效场景#

@Async 是 Spring 里很常用的异步注解。

它的作用很直观:把一个方法交给线程池执行,让调用方不用一直等这个方法执行完。

比如下单成功后:

  • 发短信;
  • 发邮件;
  • 推送站内信;
  • 写操作日志;
  • 同步第三方系统;
  • 生成报表;
  • 发送业务通知。

这些任务通常不是主流程必须立即完成的事情,就可以考虑异步执行。

不过,@Async 看起来简单,实际项目里坑不少。比如:同一个类里调用不生效、线程池没配置、异常丢了、事务边界不清楚、异步任务失败后没有补偿。

这篇专门把 @Async 拎出来整理一下。

如果你想先了解线程池基础,可以看:并发编程二:线程池、JUC 工具与异步编程

@Async 适合做什么#

@Async 适合处理“不影响主流程立即返回”的任务。

典型场景:

场景说明
短信 / 邮件通知下单成功后通知用户
操作日志记录用户行为,不阻塞主接口
数据同步同步到搜索、统计、第三方系统
文件处理上传后异步解析或生成缩略图
报表导出提交导出任务后后台慢慢处理
站内消息业务成功后推送通知

但有些逻辑不适合随便异步化:

  • 扣库存;
  • 创建订单;
  • 支付状态变更;
  • 核心事务提交;
  • 需要强一致返回结果的逻辑。

一句话:

能慢一点完成,但不能影响主流程返回的任务,才适合优先考虑 @Async

最小使用示例#

第一步,开启异步支持:

@EnableAsync
@Configuration
public class AsyncConfig {
}

第二步,在 Spring 管理的 Bean 方法上加 @Async

@Service
public class MessageService {
@Async
public void sendOrderMessage(Long orderId) {
System.out.println("发送订单通知:" + orderId);
}
}

第三步,在其他 Bean 里调用:

@Service
public class OrderService {
private final MessageService messageService;
public OrderService(MessageService messageService) {
this.messageService = messageService;
}
public void createOrder(Long orderId) {
// 创建订单主流程
messageService.sendOrderMessage(orderId);
}
}

调用 sendOrderMessage() 时,主线程不会等待它执行完成,而是把任务交给异步线程处理。

一定要配置自定义线程池#

生产环境不建议直接裸用 @Async

更推荐配置一个明确的线程池:

@EnableAsync
@Configuration
public 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 返回值的异步方法,可以配置异常处理器:

@Configuration
public 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. 同一个类内部调用#

错误示例:

@Service
public class OrderService {
public void createOrder(Long orderId) {
sendMessage(orderId);
}
@Async
public void sendMessage(Long orderId) {
System.out.println("发送消息");
}
}

这种写法通常不会异步执行。

原因是 createOrder() 里调用的是 this.sendMessage(),没有经过 Spring 代理。

更推荐拆到另一个 Bean:

@Service
public class MessageService {
@Async("notificationExecutor")
public void sendMessage(Long orderId) {
System.out.println("发送消息");
}
}

再由 OrderService 注入调用。

2. 方法不是 public#

@Async 通常应该放在 public 方法上。

@Async
private 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
@Configuration
public class AsyncConfig {
}

这是最基础但也很常见的遗漏。

5. 事务边界理解错#

看这个例子:

@Transactional
public 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 都能做异步,但定位不一样。

对比@AsyncMQ
部署复杂度
可靠性一般更强
跨服务不适合适合
失败重试需要自己做通常有机制
削峰能力
适合场景单服务内轻量异步跨服务、可靠异步、削峰

简单任务可以用 @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 硬扛。

Share

If this article helped you, please share it with others!

Spring @Async 实战:异步方法、线程池配置与常见失效场景
https://mizuki.mysqil.com/posts/spring-async/
Author
梦幻晨风
Published at
2025-04-21
License
CC BY-NC-SA 4.0

Some information may be outdated

Table of Contents