news 2026/9/9 22:48:02

Spring @Async异步任务深度解析:线程池配置与常见坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring @Async异步任务深度解析:线程池配置与常见坑

Spring 里做异步任务,很多人第一反应就是 @Async。这个注解确实省事,一个注解扔上去,方法调用就自动丢进线程池跑,看起来人畜无害。但我在实际项目里见过太多人在这上面栽跟头,有的是方法内部调用不生效,有的是线程池直接被打满,还有的是异常静默吞掉排查了半天。这篇文章我把 @Async 背后的原理、正确用法、线程池设计,还有常年踩坑的问题一次讲透,希望能帮各位少走弯路。

1. 先搞清楚 @Async 到底做了什么

1.1 一个注解背后的代理机制

先抛开 Spring 的使用细节,想想异步的本质。同步调用就是调用方发一个请求,一直等着被调用方返回结果;异步调用是调用方先发一个消息,不等待对方处理完,直接干自己的事情。Spring 的 @Async 干的活,就是把一个普通方法从“同步执行”变成“丢进线程池异步执行”。

这里有个关键点:@Async 不是方法自己“变”成了异步,而是 Spring 在运行时给 Bean 创建了一个代理对象。当你调用一个被 @Async 标注的方法时,调用方拿到的其实不是这个类的原始对象,而是 Spring 用 CGLIB 或 JDK 动态代理生成的代理子类。代理对象接住调用以后,并不会直接执行目标方法,而是把任务封装成一个 Runnable/ Callable 提交给线程池,线程池里的某个线程再来真正执行目标逻辑。

换句话说,@Async 的底层就是“Spring AOP + 线程池”。Spring 用 AOP 拦截到方法调用,然后委托给任务执行器。理解这一点非常重要,因为后面讲到的“自调用失效”问题,本质上就是代理失效导致的。如果你绕过代理对象,直接调用原始对象的方法,Spring 根本拦不到这一次调用,异步自然也就不会生效。

1.2 核心组件:TaskExecutor 的任务分工

再往底层看一层。Spring 执行异步任务时,负责真正干活的组件是 TaskExecutor,它是 Spring 对 JDK 自带 ExecutorService 的抽象。默认情况下,Spring 会尝试查找一个名为 applicationTaskExecutor 的 Bean,如果找不到,就用 SimpleAsyncTaskExecutor 兜底。

这个 SimpleAsyncTaskExecutor 名字听着挺简单,但它其实是“异步大坑”的源头。它每次提交任务都会创建一个新线程,而且不回收线程,也没有并发上限。想象一下,如果你的接口被刷了 1000 次,那就会创建 1000 个线程,系统资源不够了就直接 OOM 或者把 CPU 打满。这也是我在 4.1 节要详细拆解的默认线程池缺陷。

所以核心结论先放在这里:用 @Async 之前,必须手动配置线程池。命名、核心线程数、队列容量、拒绝策略,这些参数都要根据你的业务场景认真设计,不能抱着“能用就行”的心态。

2. 快速搭建:从 @EnableAsync 到第一个异步方法

2.1 开启异步支持

不管项目用的是 Spring Boot 还是传统 Spring 工程,第一步都是在配置类上加上 @EnableAsync。这个注解的作用是开启 Spring 对 @Async 注解的解析能力。没有它,你在任何方法上写 @Async 都不会生效。

@Configuration @EnableAsync public class AsyncConfig { // 线程池相关配置 }

其实在 Spring Boot 项目中,很多团队习惯把 @EnableAsync 直接加在主启动类上,这样也能生效,但我个人更推荐放在单独的配置类上。原因有两个:一是职责更清晰,异步相关的配置都收敛到一起;二是有时候你需要在配置类里同时定义线程池 Bean、异步异常处理器 Bean,放一起管理起来更顺手。

2.2 最简单的异步方法

先看一个最基础的用法。假设我们有一个通知服务,需要给用户发短信和推送,这两个操作都比较耗时,而且不需要等结果。

@Service public class NotificationService { private static final Logger log = LoggerFactory.getLogger(NotificationService.class); @Async public void sendSms(String mobile, String content) { // 模拟耗时操作 try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } log.info("短信发送成功: {}, 内容: {}", mobile, content); } @Async public void sendPush(Long userId, String content) { try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } log.info("推送发送成功: userId={}, 内容: {}", userId, content); } }

调用方在 Controller 或者其他 Service 里直接注入 NotificationService,正常调用 sendSms 和 sendPush,这两个方法会立刻返回,真正的短信发送逻辑会在线程池里执行。

@RestController @RequestMapping("/api/notification") public class NotificationController { private final NotificationService notificationService; public NotificationController(NotificationService notificationService) { this.notificationService = notificationService; } @PostMapping("/send") public void send(@RequestParam String mobile, @RequestParam Long userId) { notificationService.sendSms(mobile, "您的验证码是123456"); notificationService.sendPush(userId, "您有一条新消息"); // 这里的方法会立即返回,不用等短信和推送都执行完 } }

2.3 有返回值的异步方法怎么写

有些场景你确实需要异步执行,但最终还想拿回一个结果,比如并发调用多个外部接口后聚合数据,或者异步执行一个耗时校验,后面还要用到它的结果。

2.3.1 使用 Future 作为返回值

比较传统的写法是返回 Future 接口。比如:

@Async public Future<String> fetchUserInfo(Long userId) { // 模拟远程调用 try { Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return new AsyncResult<>("user_" + userId); }

调用方可以拿到这个 Future,然后调用 get() 方法阻塞等待结果。AsyncResult 是 Spring 提供的一个简单实现,你也可以直接返回 CompletableFuture.completedFuture(...)。

2.3.2 进阶:CompletableFuture 的正确打开方式

更推荐的做法是配合 Java 8 的 CompletableFuture。原因很简单:Future.get() 是阻塞的,拿到 Future 之后你只能干等;而 CompletableFuture 支持回调、组合、异常处理,编程模型更灵活。

@Async public CompletableFuture<OrderInfo> queryOrderInfo(Long orderId) { // 模拟耗时查询 try { Thread.sleep(1500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } OrderInfo order = new OrderInfo(); order.setId(orderId); order.setTotalAmount(new BigDecimal("199.00")); return CompletableFuture.completedFuture(order); }

调用方组合多个异步任务:

public void aggregateOrderDetails(Long orderId) { CompletableFuture<OrderInfo> orderFuture = orderService.queryOrderInfo(orderId); CompletableFuture<List<OrderItem>> itemFuture = orderItemService.queryOrderItems(orderId); CompletableFuture<UserInfo> userFuture = userService.queryBuyer(orderId); CompletableFuture.allOf(orderFuture, itemFuture, userFuture).join(); // 三个异步任务都完成后,再分别 get 结果 }

这里有个注意点:@Async 方法返回 CompletableFuture 时,方法内部一定要把结果包装到 CompletableFuture 里返回,不能直接返回 null,否则调用方拿到的 CompletableFuture 是 null,后面调 allOf 会直接 NPE。

3. 线程池配置:最容易翻车的环节

如果说 @Async 本身是一个好用的工具,那线程池配置就是决定它会不会爆炸的开关。我在代码评审里经常看到有人二话不说就加 @Async,线程池是什么样、什么参数完全不关心。这种代码拿到测试环境没事,一上生产量稍微大点就各种问题。

3.1 默认线程池到底哪里不行

先打开 Spring 源码看默认情况。当你在方法上标注 @Async 后,Spring 在启动时会查找可用的 Executor Bean,它的查找顺序大概是:

  1. 唯一一个 Executor 类型的 Bean
  2. 名为 taskExecutor 的 Bean
  3. 名为 applicationTaskExecutor 的 Bean
  4. 前面都找不到,就默认使用 SimpleAsyncTaskExecutor

如果项目里没有定义任何 Executor 相关的 Bean,最终就会落到SimpleAsyncTaskExecutor 上。我前面已经提到,这个执行器的核心特征是“来一个任务就 new 一个线程”,上一次任务执行完了线程也不复用,直接丢弃。在高并发场景下,线程数量无上限地增长,最终把内存和文件描述符耗尽。

有朋友可能想说,Spring Boot 不是有默认的 applicationTaskExecutor 吗?确实,Spring Boot 在自动配置中定义了一个名为 applicationTaskExecutor 的线程池,它默认的配置参数是:核心线程数 8,最大线程数 Integer.MAX_VALUE(其实就是没有上限),队列使用的是无界队列 LinkedBlockingQueue。看到无界队列四个字就该警觉了——队列长度没有上限意味着任务会无限堆积,堆积到一定程度就是内存溢出。

3.2 自定义线程池的标准姿势

要避免这些默认问题,最好的做法是显式定义一个线程池 Bean,并明确告诉 Spring 使用它。

@Configuration @EnableAsync public class AsyncConfig implements AsyncConfigurer { @Bean(name = "businessExecutor") public ThreadPoolTaskExecutor businessExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // 核心线程数:正常情况下同时处理的任务数 executor.setCorePoolSize(10); // 最大线程数:高峰期最多创建的线程数 executor.setMaxPoolSize(20); // 队列容量:当核心线程都在忙碌时,新任务先进队列 executor.setQueueCapacity(200); // 线程名称前缀:方便排查问题 executor.setThreadNamePrefix("biz-exec-"); // 拒绝策略:队列和最大线程都满了以后怎么办 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } @Override public Executor getAsyncExecutor() { return businessExecutor(); } }

这里我把参数选择逻辑一个个拆开讲:

核心线程数(corePoolSize)到底设多少合适?没有放之四海而皆准的答案,但有一个经验公式可以参考:如果是 CPU 密集型任务,核心线程数可以设置为 CPU 核心数 + 1;如果是 IO 密集型任务(比如大量远程调用、数据库查询、文件读写),线程数可以适当加大,比如 CPU 核心数 * 2 或更多。关键还是要压测,用数据说话。

最大线程数(maxPoolSize)和队列容量(queueCapacity)是配套看的。很多人对这两个参数的配合有误解,以为任务提交时是“核心线程满了就创建新线程到最大线程数,然后再进队列”。实际上 ThreadPoolExecutor 的执行顺序是:

  1. 核心线程没满,创建核心线程执行任务
  2. 核心线程满了,先把任务放进队列
  3. 队列满了,才创建非核心线程
  4. 非核心线程也没有空闲了,触发拒绝策略

也就是说,在核心线程数 = 10、最大线程数 = 20、队列容量 = 200 的配置下,你的系统最多能承载 210 个同时提交的任务,超过 210 个以后才会触发拒绝策略。这样设计的好处是让大部分任务先排队,而不是一拥而上把服务器压垮。

拒绝策略(RejectedExecutionHandler)是最容易被忽略的。Spring 的 ThreadPoolTaskExecutor 默认使用的是 ThreadPoolExecutor.AbortPolicy,即直接抛出 RejectedExecutionException。这个异常如果没被捕获,可能会导致调用方的方法直接报错。我在生产环境更推荐 CallerRunsPolicy,它的意思是:线程池满了以后,新任务不丢弃,而是由提交任务的线程自己来执行。这样就天然实现了一种“限流”效果,提交方执行任务期间无法继续提交新任务,而且任务不会丢。不过 CallerRunsPolicy 有个副作用是阻塞调用方线程,如果你在接口请求里提交异步任务,那么接口响应时间可能会变长,这个得结合业务评估。

3.3 多个线程池场景下怎么精确指定

项目大了以后,往往会有多种异步任务,比如一类是发消息的,一类是做数据清洗的。这些任务对线程池的要求不一样,不能在同一个池子里“一锅炖”。Spring 提供了 @Async("executorName") 的方式,指定使用哪个线程池。

@Async("messageExecutor") public void sendMessage() { // 短信、推送等消息发送任务 } @Async("dataProcessExecutor") public void dataProcess() { // 批量数据清洗任务 }

分别定义两个线程池 Bean:

@Bean("messageExecutor") public ThreadPoolTaskExecutor messageExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(10); executor.setQueueCapacity(50); executor.setThreadNamePrefix("msg-"); return executor; } @Bean("dataProcessExecutor") public ThreadPoolTaskExecutor dataProcessExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(8); executor.setMaxPoolSize(15); executor.setQueueCapacity(100); executor.setThreadNamePrefix("data-"); return executor; }

这样做的好处很直观:消息发送任务即使高峰打满了,也不会挤占数据清洗任务的资源。很多时候系统故障就是从“线程池混用”开始的,一个任务慢了,把线程占满,其他不相关的任务也跟着全部卡死。

4. 五大高频问题,全部是真实踩坑记录

4.1 自调用导致 @Async 失效

这是所有 @Async 问题里出现频率最高的。看下面的代码:

@Service public class OrderService { public void createOrder() { // 业务逻辑 this.sendNotify(); } @Async public void sendNotify() { // 发送通知 } }

你会发现 sendNotify 并没有异步执行,而是同步阻塞的。原因就是我开头说的代理机制。外部调用 OrderService.createOrder() 时,实际上走的是代理对象。但 createOrder 方法内部通过 this 调用 sendNotify(),这个 this 指向的是当前对象自身,不是 Spring 生成的代理对象。所以 @Async 的拦截逻辑根本没有机会执行。

解法有三种:

第一种,把异步方法抽到另一个 Service 里,通过注入的代理对象调用。

@Service public class OrderService { private final NotificationService notificationService; public OrderService(NotificationService notificationService) { this.notificationService = notificationService; } public void createOrder() { notificationService.sendNotify(); } } @Service public class NotificationService { @Async public void sendNotify() { // 发送通知 } }

第二种,自己注入代理对象,用代理调用。

@Service public class OrderService { @Autowired private OrderService self; public void createOrder() { self.sendNotify(); } @Async public void sendNotify() { // 发送通知 } }

第三种,如果一定要在同一个类里解决,可以用 AopContext.currentProxy() 获取当前代理对象。注意要配置 exposeProxy = true,默认是 false。

@EnableAspectJAutoProxy(exposeProxy = true) public void createOrder() { OrderService proxy = (OrderService) AopContext.currentProxy(); proxy.sendNotify(); }

这三种方式优先推荐第一种,代码结构最清晰,也不容易出问题。

4.2 @Async 和事务注解一起用,事务经常不生效

有同事写过一个方法,同时加了 @Async 和 @Transactional:

@Async @Transactional public void asyncUpdateOrder(Order order) { orderRepository.save(order); }

结果发现数据经常没提交,或者抛出事务不生效的异常。这里牵扯到 Spring 代理对注解处理顺序的问题。@Async 和 @Transactional 都是通过 AOP 代理实现的,但它们的拦截器执行顺序由 Spring 内部决定。默认情况下,事务注解创建的是一个基于代理的事务边界,而 @Async 把方法调用丢进了线程池,事务管理器和这个线程池里的线程并不是同一个线程,事务上下文就无法正常传播。

更稳妥的做法是拆开:外层方法用 @Async,内层单独调用一个 @Transactional 方法。而且这个内层方法必须通过代理调用,不能用 this,否则事务同样白搭。

@Service public class OrderAsyncService { private final OrderService orderService; public OrderAsyncService(OrderService orderService) { this.orderService = orderService; } @Async public void asyncUpdateOrder(Order order) { orderService.updateOrderWithTx(order); } } @Service public class OrderService { @Transactional public void updateOrderWithTx(Order order) { orderRepository.save(order); } }

还有一个容易被忽略的点:事务的隔离级别和传播行为在线程池场景下要重新审视。如果异步方法里的数据库操作需要读取主事务尚未提交的数据,就会因为事务传播条件不同而读到旧数据,这类问题是排查起来最费神的一类。

4.3 异常被吞掉,问题日志一个都没有

@Async 标注的 void 方法,如果执行过程中抛出异常,默认情况下异常直接被吞掉,控制台和新日志文件里什么都看不到。因为任务已经丢给线程池了,这个异常抛回给调用方也没意义,调用方早就返回响应了。

解决这个问题有两个思路。

第一,方法内部自己捕获异常,处理好以后打日志:

@Async public void sendSms(String mobile, String content) { try { // 发送逻辑 } catch (Exception e) { log.error("短信发送失败 mobile={}, content={}", mobile, content, e); } }

这个方法简单直观,但缺点是每个异步方法都要写 try catch,比较啰嗦。

第二,实现 AsyncUncaughtExceptionHandler,统一处理所有由 @Async 方法抛出的未捕获异常。

@Configuration @EnableAsync public class AsyncConfig implements AsyncConfigurer { @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return (ex, method, params) -> { log.error("异步任务执行异常, method={}, params={}", method.getName(), Arrays.toString(params), ex); // 可以接入告警系统 }; } }

对于返回 Future/CompletableFuture 的 @Async 方法,异常不会走到 AsyncUncaughtExceptionHandler 里,而是包装在返回结果里,等你调用 future.get() 或 exceptionally() 的时候才能拿到。

CompletableFuture<OrderInfo> future = orderService.queryOrderInfo(orderId); future.exceptionally(throwable -> { log.error("查询订单失败", throwable); return new OrderInfo(); });

4.4 线程池满了以后的表现

前面提到 AbortPolicy 是默认拒绝策略,如果线程池和队列都满了,新任务会被直接丢弃,同时抛出 RejectedExecutionException。这个异常如果发生在 Controller 调用的那一刻,用户会直接看到 500 报错;如果发生在一个已经异步提交的任务里,异常也可能被吞掉。

我遇到过这样一个生产事故:某个接口突然大量请求进来,异步处理订单消息的线程池被打满,队列也堆满了,随后大量订单消息被 RejectedExecutionException 丢弃,消息丢失引发了一系列对账异常。后来排查发现线程池核心线程数配置只有 2,队列容量也只有 10,完全匹配不了业务峰值。

解决这类问题除了调大参数,更重要的是一开始就设计好拒绝策略和监控。我建议:

  • 核心线程、最大线程、队列容量都要结合峰值流量计算,不能拍脑袋
  • 拒绝策略优先选 CallerRunsPolicy,至少任务不会丢
  • 给线程池加上监控,比如使用 micrometer 上报活跃线程数、队列大小到 Prometheus/Grafana,发现线程池接近满载时提前预警

4.5 线程上下文传递问题(ThreadLocal 丢失)

异步任务执行的时候,提交任务的主线程和真正执行任务的工作线程不是同一个线程。如果代码里用了 ThreadLocal 传递用户信息、TraceId、事务上下文,这些数据在异步线程里默认是拿不到的。

常见的场景:在拦截器里用户信息放到 ThreadLocal,业务方法中通过 UserContext.get() 读取。一旦这个方法被 @Async 标记,异步线程里的 UserContext 是空的。

解决方式可以是使用 Spring 的 TaskDecorator 来包装任务,在工作线程执行任务前把主线程的 ThreadLocal 值复制过去,执行完再清理恢复。

@Bean("businessExecutor") public ThreadPoolTaskExecutor businessExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // 略去其他参数 executor.setTaskDecorator(runnable -> { // 保存主线程上下文 Map<String, String> contextMap = TransferContextHolder.get(); // 从主线程取出用户信息 Long userId = UserContext.getUserId(); return () -> { try { // 设置到工作线程 UserContext.setUserId(userId); TransferContextHolder.set(contextMap); runnable.run(); } finally { UserContext.clear(); TransferContextHolder.clear(); } }; }); executor.initialize(); return executor; }

如果你用的链路追踪框架是 Sleuth 或者 Micrometer Tracing,它们一般已经实现了 TaskDecorator,会把 TraceId 自动传递到异步线程里,这块可以直接用自带的能力。

5. 注意点汇总与排查工具

5.1 日常开发需要注意的清单

与其等出问题再来排查,不如在写代码时就做好这几点:

  1. 所有 @Async 方法都要显式指定一个自定义线程池,不要依赖默认
  2. 异步方法不要写在调用方同一个类里,抽到独立 Service,保证代理生效
  3. 返回值优先用 CompletableFuture,少用 Future,避免阻塞调用方线程
  4. void 方法内部要处理异常并打日志,或者配置全局 AsyncUncaughtExceptionHandler
  5. @Async 和 @Transactional 不要直接堆在同一个方法上,拆开发
  6. 涉及 ThreadLocal 的场景要关注上下文传递,必要时用 TaskDecorator
  7. 异步任务建议把 traceId、userId 这类上下文信息传进去,方便排障
  8. 线程池命名要有业务含义,比如 order-async-exec,别用默认名,否则线上看线程 dump 根本分不清是哪个业务

5.2 线上排查手段

如果线上异步任务没按预期执行,第一件事是看线程 dump。jstack 命令可以导出 Java 进程的线程快照:

jstack <pid> > thread_dump.txt

然后搜索线程池名称前缀,比如你配置的线程名是 order-async-exec-,就搜索这个关键字。看这个线程是不是存在、状态是什么(WAITING、RUNNABLE、BLOCKED)、有没有堆积很深的方法调用链。

其次看线程池监控指标。如果项目已经接了 Micrometer,ThreadPoolTaskExecutor 的相关指标一般是自动暴露的,重点看这几个值:

  • active 线程数
  • 队列大小
  • 完成任务总数
  • 拒绝任务数(如果配了计数器)

队列大小一直涨,说明消费速度跟不上生产速度;active 长期等于最大线程数,说明线程池饱和了;拒绝任务数 > 0 说明需要扩容或优化拒绝策略。

还有一个很实用的排查思路:给异步方法临时加日志,打印当前线程名。这个方法虽然土,但定位是否真的走异步非常有效。

@Async("orderExecutor") public void processOrder(Order order) { log.info("processOrder 开始执行, 当前线程: {}", Thread.currentThread().getName()); }

如果日志输出的线程名是 order-exec-xx 这样的业务线程,说明代理生效、线程池也生效了;如果输出的线程名还是 tomcat-xxx,那基本可以断定 @Async 没生效。

5.3 一个完整可落地的配置案例

最后贴一个我在项目里常用的异步配置模板,供参考。场景是处理订单消息,既有并发要求又不想让任务丢:

@Configuration @EnableAsync public class AsyncConfig implements AsyncConfigurer { @Bean("orderAsyncExecutor") public ThreadPoolTaskExecutor orderAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(8); executor.setMaxPoolSize(16); executor.setQueueCapacity(500); executor.setKeepAliveSeconds(60); executor.setThreadNamePrefix("order-async-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.setWaitForTasksToCompleteOnShutdown(true); executor.setAwaitTerminationSeconds(30); executor.initialize(); return executor; } @Override public Executor getAsyncExecutor() { return orderAsyncExecutor(); } @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return (ex, method, params) -> log.error("异步任务执行失败, method={}, params={}", method.getName(), Arrays.toString(params), ex); } }

waitForTasksToCompleteOnShutdown 和 awaitTerminationSeconds 这两个参数经常被忽略,但生产环境重启时很关键。前者让 Spring 容器关闭时等待已提交任务执行完,后者限定最大等待时间,避免一些卡死的任务拖住进程无法退出。

6. 从一个例子看 @Async 的正确姿势

把前面的知识点串起来,看一个相对完整的业务场景。

假设我们要做一个订单创建功能,下单成功以后需要做三件事:发送短信通知、发送 App 推送、记录操作日志。这三个操作都不需要等待结果,而且任何一个失败都不能影响主流程。

第一步,定义线程池。

@Bean("notifyExecutor") public ThreadPoolTaskExecutor notifyExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(10); executor.setQueueCapacity(100); executor.setThreadNamePrefix("notify-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; }

第二步,定义异步服务,方法之间互相独立。

@Service public class NotifyService { private static final Logger log = LoggerFactory.getLogger(NotifyService.class); @Async("notifyExecutor") public void sendSms(String mobile, String content) { try { Thread.sleep(300); log.info("短信已发送: {}", mobile); } catch (Exception e) { log.error("短信发送失败: {}", mobile, e); } } @Async("notifyExecutor") public void sendPush(Long userId, String content) { try { Thread.sleep(200); log.info("推送已发送: userId={}", userId); } catch (Exception e) { log.error("推送发送失败: userId={}", userId, e); } } @Async("notifyExecutor") public void writeLog(Order order) { log.info("订单日志: {}", order.getOrderNo()); } }

第三步,在订单服务里调用。

@Service public class OrderService { private final NotifyService notifyService; public OrderService(NotifyService notifyService) { this.notifyService = notifyService; } @Transactional public void createOrder(OrderCreateRequest request) { // 1. 保存订单 Order order = new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(request.getUserId()); order.setTotalAmount(request.getAmount()); orderRepository.save(order); // 2. 异步通知,不影响事务提交 notifyService.sendSms(request.getMobile(), "您的订单已创建,订单号:" + order.getOrderNo()); notifyService.sendPush(request.getUserId(), "您的订单已创建"); notifyService.writeLog(order); // 3. 返回,不用等通知执行完 } }

这样整个流程就是:主线程同步执行事务,事务提交后异步通知在线程池里并发执行,发送失败也不影响主流程,日志里还能看到错误堆栈。

我个人在实际项目中的体会是,@Async 的难点从来不是注解本身,而是它牵涉出来的线程池设计、代理机制和异常处理。写这篇文章前,我特意把这几年代码评审里碰到的问题过了一遍,大多数坑都集中在 4.1 到 4.5 这几个点上。如果你能用好线程池和异常处理这两个部分,80% 的异步问题都能避免。最后再分享一个小技巧:新写的 @Async 方法上线前,建议先单独写个测试接口调一次,看日志里线程名是否如预期,这一步能在早期发现很多容易在复杂调用链中被忽略的代理失效问题。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 22:47:57

VxWorks串口通信实战:从termios配置到Zynq平台部署

简介&#xff1a;VxWorks广泛部署于航空航天、通信、工业自动化等实时性要求极高的场景&#xff0c;串口通信则是设备交互与调试环节中最常用也最基础的手段之一。这份示例程序以TestUart项目为载体&#xff0c;面向嵌入式初学者与工程开发人员&#xff0c;重点演示VxWorks标准…

作者头像 李华
网站建设 2026/9/9 22:46:34

建议收藏|盘点2026年圈粉无数的的AI论文网站

一天写完毕业论文在2026年已不再是天方夜谭。2026年最炸裂、实测能大幅提速的AI论文网站&#xff0c;覆盖选题构思、文献整理、内容生成、降重润色、格式排版全流程&#xff0c;助你高效搞定论文&#xff0c;省时又省力。 一、全流程王者&#xff1a;一站式搞定论文全链路&…

作者头像 李华
网站建设 2026/9/9 22:45:34

当技术让一切趋同:AI时代如何守住判断力与独立思考

1. 当技术让一切趋同&#xff0c;我们最先失去的是什么 这期周刊的标题是《当技术让一切趋同&#xff0c;我们还剩什么&#xff1f;》&#xff0c;不少读者在后台留言说看到这个标题愣了一下。我说下我的理解&#xff1a;我聊的“趋同”&#xff0c;不是技术能力上的趋同&#…

作者头像 李华
网站建设 2026/9/9 22:43:55

数据资产入表与运营:第16期新闻深度拆解

做完第16期《全国数据资产新闻和报纸摘要联播》&#xff0c;我照例在后台把当天的信息流重新过了一遍。这期内容密度比前几期都要高&#xff0c;涉及数据资产入表、数据产品交易、可信数据空间、数据资产质押等好几个方向&#xff0c;每一块单独拎出来都够展开写一篇实操手册。…

作者头像 李华
网站建设 2026/9/9 22:42:17

TMS320F28335实现FOC控制:死区配置、电流采样与算法要点详解

简介&#xff1a;基于28335的电机FOC控制工程源码包&#xff0c;面向电机驱动、嵌入式控制和电力电子相关开发人员&#xff0c;定位为基于TI TMS320F28335实现FOCSVPWM控制策略的完整参考工程。压缩包共135个文件、1.25MB&#xff0c;主要包含DSP2833x标准库的C源文件与头文件&…

作者头像 李华
网站建设 2026/9/9 22:41:43

vm3dum_loader.dll丢失怎么办?VMware虚拟显卡驱动修复指南

如果你最近打开某些依赖 VMware 虚拟环境的软件&#xff0c;或者手动清理过显卡驱动残留&#xff0c;突然蹦出一个“找不到 vm3dum_loader.dll”或者“无法启动&#xff0c;因为计算机中丢失 vm3dum_loader.dll”的报错&#xff0c;大概率第一反应是去搜索引擎找这个 dll 的下载…

作者头像 李华