文章目录
-
- 一、开篇:从同步阻塞到异步非阻塞的性能跃迁
- 二、@Async异步调用完整实战
-
- [2.1 异步调用的三种实现方式](#2.1 异步调用的三种实现方式)
- [2.2 @Async注解实战](#2.2 @Async注解实战)
- [2.3 带返回值的异步方法](#2.3 带返回值的异步方法)
- [三、Spring Boot 4.0虚拟线程与线程池配置](#三、Spring Boot 4.0虚拟线程与线程池配置)
-
- [3.1 虚拟线程默认化:范式转变](#3.1 虚拟线程默认化:范式转变)
- [3.2 虚拟线程下@Async的自动配置](#3.2 虚拟线程下@Async的自动配置)
- [3.3 线程池精细化配置](#3.3 线程池精细化配置)
- [3.4 线程池参数调优指南](#3.4 线程池参数调优指南)
- [3.5 虚拟线程 vs 平台线程池的选择](#3.5 虚拟线程 vs 平台线程池的选择)
- 四、异步异常处理与日志上下文透传
-
- [4.1 异步异常处理的"黑洞"问题](#4.1 异步异常处理的“黑洞”问题)
- [4.2 MDC日志上下文透传:解决TraceID断链](#4.2 MDC日志上下文透传:解决TraceID断链)
- [4.3 异步调用的可观测性](#4.3 异步调用的可观测性)
- 五、Spring事件驱动开发
-
- [5.1 为什么要用事件驱动](#5.1 为什么要用事件驱动)
- [5.2 同步事件实战](#5.2 同步事件实战)
- [5.3 异步事件:@Async + @EventListener](#5.3 异步事件:@Async + @EventListener)
- [5.4 事务事件:@TransactionalEventListener](#5.4 事务事件:@TransactionalEventListener)
- [5.5 事件驱动 vs 消息队列的选型](#5.5 事件驱动 vs 消息队列的选型)
- 六、踩坑指南
- 七、课后作业
- 八、下节预告
- [🔗《最新版 SpringCloud 2025 从入门到实战》系列课程导航](#🔗《最新版 SpringCloud 2025 从入门到实战》系列课程导航)

适配版本 :Spring Boot 4.0.8、Spring Framework 7.0.x、Spring Cloud 2025.1.3(Oakwood)、JDK 21、Micrometer 2.x
课程定位:微服务高阶阶段开篇,从异步调用原理到线程池精细化调优,从日志上下文透传到事件驱动解耦,构建高性能微服务架构
一、开篇:从同步阻塞到异步非阻塞的性能跃迁
第25-28课我们完成了可观测性体系的全部建设。Prometheus告诉你系统状态如何,SkyWalking告诉你请求经过了哪些服务,ELK告诉你系统在某个时刻发生了什么。可观测性解决了"如何发现问题",但还有一个更根本的问题:如何从架构上避免问题?
考虑一个典型的下单场景:用户提交订单后,系统需要执行一系列操作------扣减库存、生成订单记录、发送短信通知、推送用户积分、记录操作日志。如果这些操作全部同步执行,用户需要等待所有步骤完成才能得到响应。假设扣减库存耗时200ms、生成订单100ms、发送短信300ms、推送积分150ms、记录日志50ms------总计800ms。其中,发送短信和推送积分与订单创建的核心流程无关,用户完全不需要等待它们完成。
这就是异步调用要解决的问题:将非核心的、耗时的操作从主流程中剥离,让主流程快速返回响应,后台线程异步完成剩余工作。同步调用模型下,整个请求的响应时间由最慢的环节决定;异步模型下,响应时间只取决于核心流程。
但异步不是"银弹"。引入异步后,你会遇到一系列新问题:线程池怎么配置才不会OOM?异步方法抛出的异常为什么消失了?日志中的TraceID为什么断了?多个业务模块之间的调用耦合越来越重,怎么解耦?
本课将逐一解决这些问题。Spring Boot 4.0带来了一个根本性的变化------虚拟线程默认化,这意味着@Async的底层执行策略发生了范式转变。原来为平台线程精心调优的线程池参数可能不再适用,需要重新审视。
二、@Async异步调用完整实战
2.1 异步调用的三种实现方式
Spring Boot中实现异步调用有三种方式,适用场景各不相同:
| 方式 | 实现 | 适用场景 | 返回值 |
|---|---|---|---|
| @Async注解 | 方法上添加@Async |
无返回值的后台任务 | void / Future |
| CompletableFuture | 手动编排异步任务 | 需要组合多个异步结果 | CompletableFuture |
| TaskExecutor | 手动提交任务 | 需要精细控制任务执行 | Future |
2.2 @Async注解实战
第一步:启用异步支持。 在启动类或配置类上添加@EnableAsync注解:
java
@SpringBootApplication
@EnableAsync
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
第二步:定义异步方法。 在Service中定义异步方法,方法上添加@Async注解:
java
@Service
@Slf4j
public class OrderAsyncService {
@Async
public void sendSmsNotification(Long orderId, String phone) {
log.info("发送短信通知: orderId={}, phone={}", orderId, phone);
// 调用短信服务,耗时约300ms
smsClient.send(phone, "您的订单已创建,订单号:" + orderId);
log.info("短信通知发送完成: orderId={}", orderId);
}
@Async
public void addUserPoints(Long userId, int points) {
log.info("增加用户积分: userId={}, points={}", userId, points);
// 调用积分服务,耗时约150ms
pointsClient.addPoints(userId, points);
}
}
第三步:调用异步方法。 在Controller或Service中注入并调用:
java
@RestController
@RequestMapping("/api/order")
public class OrderController {
@Autowired
private OrderAsyncService orderAsyncService;
@PostMapping("/create")
public Result<OrderVO> createOrder(@RequestBody OrderCreateRequest request) {
// 1. 同步执行:创建订单(核心流程)
OrderVO order = orderService.createOrder(request);
// 2. 异步执行:发送短信通知(非核心流程)
orderAsyncService.sendSmsNotification(order.getId(), request.getPhone());
// 3. 异步执行:推送用户积分(非核心流程)
orderAsyncService.addUserPoints(request.getUserId(), order.getTotalAmount().intValue());
return Result.success(order);
}
}
关键规则:
- 异步方法必须是public的
- 异步方法不能是static的
- 异步方法必须通过Spring代理 调用------在同一个类中直接调用
@Async方法不会生效 - 异步方法所在的Bean必须由Spring管理(
@Service、@Component等)
踩坑提示 :同一个类中的方法互相调用@Async方法不会生效 ,因为调用没有经过Spring代理。解决方案是注入自身(@Autowired private OrderAsyncService self;)或通过ApplicationContext.getBean()获取代理对象。
2.3 带返回值的异步方法
@Async方法可以返回CompletableFuture<T>,调用方可以通过get()或thenAccept()获取结果:
java
@Async
public CompletableFuture<UserDTO> getUserAsync(Long userId) {
log.info("异步查询用户: userId={}", userId);
UserDTO user = userMapper.selectById(userId);
return CompletableFuture.completedFuture(user);
}
调用方可以并行调用多个异步方法,最后汇总结果:
java
public OrderDetailVO getOrderDetail(Long orderId) {
CompletableFuture<UserDTO> userFuture = orderAsyncService.getUserAsync(userId);
CompletableFuture<ProductDTO> productFuture = orderAsyncService.getProductAsync(productId);
CompletableFuture<StockDTO> stockFuture = orderAsyncService.getStockAsync(productId);
// 等待所有异步任务完成
CompletableFuture.allOf(userFuture, productFuture, stockFuture).join();
OrderDetailVO detail = new OrderDetailVO();
detail.setUser(userFuture.join());
detail.setProduct(productFuture.join());
detail.setStock(stockFuture.join());
return detail;
}
执行效果:三个查询并行执行,总耗时 = max(200ms, 150ms, 100ms) = 200ms,而非串行的450ms。
三、Spring Boot 4.0虚拟线程与线程池配置
3.1 虚拟线程默认化:范式转变
Spring Boot 4.0最大的变化之一:虚拟线程从3.2的"opt-in实验特性"变成了"推荐默认值" 。在Java 21+环境中,Tomcat和Jetty的请求处理线程默认使用虚拟线程,@Async任务和定时任务也遵循同一模型。
这意味着原来精心配置的线程池参数可能不再适用。虚拟线程与平台线程的底层模型完全不同:
| 维度 | 平台线程 | 虚拟线程 |
|---|---|---|
| 内存占用 | ~1MB/线程 | ~几KB/线程 |
| 最大数量 | 数千 | 数百万 |
| 创建开销 | 高 | 极低 |
| 适用场景 | CPU密集型 | IO密集型 |
| ThreadLocal | 安全 | 需注意清理策略 |
迁移时需要检查的配置项:
spring.threads.virtual.enabled(4.0中默认为true)- 自定义的
ThreadPoolTaskExecutor配置是否与虚拟线程冲突 - ThreadLocal的使用模式------虚拟线程可以创建数百万实例,ThreadLocal的清理策略需要重新审视
- CPU密集型任务不受益于虚拟线程,需要单独配置平台线程池
3.2 虚拟线程下@Async的自动配置
在Spring Boot 4.0中,当虚拟线程启用时,自动配置的AsyncTaskExecutor是一个使用虚拟线程的SimpleAsyncTaskExecutor,而非传统的ThreadPoolTaskExecutor。
重要限制 :@Async异步任务默认仍使用平台线程 ,需要单独配置才能使用虚拟线程。如果你希望@Async任务也使用虚拟线程,需要显式配置:
java
@Configuration
@EnableAsync
public class AsyncVirtualThreadConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
// 使用虚拟线程的SimpleAsyncTaskExecutor
return new SimpleAsyncTaskExecutor(
Thread.ofVirtual().name("async-vt-", 0).factory()
);
}
}
关键区别:
- Tomcat请求处理线程:默认使用虚拟线程(Spring Boot 4.0自动配置)
@Async任务线程:默认使用平台线程池,需显式配置虚拟线程- CPU密集型任务:应使用平台线程池,虚拟线程的优势在于IO等待
3.3 线程池精细化配置
对于需要精细控制的场景(如CPU密集型任务、需要限制并发数的IO任务),仍需自定义ThreadPoolTaskExecutor。
通过YAML配置(推荐用于简单场景):
yaml
spring:
task:
execution:
pool:
core-size: 8 # 核心线程数
max-size: 16 # 最大线程数
queue-capacity: 100 # 队列容量
keep-alive: 10s # 空闲线程存活时间
thread-name-prefix: async- # 线程名前缀
当ThreadPoolTaskExecutor自动配置时,线程池默认使用8个核心线程,这些线程可以根据负载动态增长和收缩。可以通过spring.task.execution命名空间微调默认设置。配置为queue-capacity: 100后,当队列满(100个任务)时,线程池增加到最大16个线程;空闲10秒后,线程被回收。
通过Java配置(推荐用于生产环境):
java
@Configuration
@EnableAsync
public class AsyncThreadPoolConfig {
@Bean("orderTaskExecutor")
public ThreadPoolTaskExecutor orderTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(200);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("order-async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(30);
executor.initialize();
return executor;
}
}
使用指定线程池:
java
@Async("orderTaskExecutor")
public void sendSmsNotification(Long orderId, String phone) {
// 使用 orderTaskExecutor 线程池执行
}
3.4 线程池参数调优指南
参数对照表:
| 参数 | 作用 | 默认值 | 生产建议 |
|---|---|---|---|
corePoolSize |
核心线程数 | 8 | IO密集型:CPU核数×2;CPU密集型:CPU核数+1 |
maxPoolSize |
最大线程数 | Integer.MAX_VALUE | 根据系统承载能力设置,建议core×2~5 |
queueCapacity |
队列容量 | Integer.MAX_VALUE | 必须显式设置,否则队列无限大,OOM风险 |
keepAliveSeconds |
空闲线程存活时间 | 60s | 60~300s |
rejectedExecutionHandler |
拒绝策略 | AbortPolicy | 见下表 |
队列容量是最大的坑 :Spring默认队列容量是Integer.MAX_VALUE,意味着线程池永远不会触发maxPoolSize(因为队列永远塞不满),直到内存被撑爆(OOM)。生产环境必须显式配置queueCapacity。
拒绝策略选择:
| 策略 | 行为 | 适用场景 |
|---|---|---|
AbortPolicy |
抛出RejectedExecutionException |
默认策略,任务不可丢弃 |
CallerRunsPolicy |
由调用线程执行 | 任务不可丢弃且可以降级 |
DiscardPolicy |
静默丢弃 | 任务可丢弃(如日志上报) |
DiscardOldestPolicy |
丢弃最旧的任务 | 保留最新任务 |
IO密集型 vs CPU密集型:
| 任务类型 | 特征 | corePoolSize建议 | 说明 |
|---|---|---|---|
| IO密集型 | 等待外部IO(DB、HTTP、MQ) | CPU核数 × 2~4 | 线程大部分时间在等待 |
| CPU密集型 | 计算(压缩、加密、图像处理) | CPU核数 + 1 | 线程持续占用CPU |
3.5 虚拟线程 vs 平台线程池的选择
| 条件 | 推荐方案 |
|---|---|
| Java 21+ 且任务为IO密集型 | 虚拟线程 (spring.threads.virtual.enabled=true) |
| Java 21+ 但任务为CPU密集型 | 平台线程池 (显式配置ThreadPoolTaskExecutor) |
| Java 17 或更低版本 | 平台线程池 |
| 需要精细控制并发数 | 平台线程池(虚拟线程无法限制并发数) |
四、异步异常处理与日志上下文透传
4.1 异步异常处理的"黑洞"问题
@Async方法抛出的异常不会传播到调用方 。如果@Async方法返回void,异常只能被日志记录;如果返回CompletableFuture,异常会被包装在Future中,调用方需要显式调用get()才能看到异常。
解决方案:实现AsyncUncaughtExceptionHandler
java
@Configuration
@EnableAsync
public class AsyncExceptionConfig implements AsyncConfigurer {
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new CustomAsyncExceptionHandler();
}
}
@Slf4j
public class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
log.error("异步方法执行异常: method={}, params={}", method.getName(), Arrays.toString(params), ex);
// 可以在这里发送告警、记录审计日志等
alertService.sendAlert("异步任务异常: " + method.getName(), ex);
}
}
通过@EnableAsync配置:
java
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new CustomAsyncExceptionHandler();
}
}
4.2 MDC日志上下文透传:解决TraceID断链
@Async方法会将任务放到线程池中的另一个线程执行,而MDC(Mapped Diagnostic Context)是基于ThreadLocal的,新线程里没有父线程的TraceID,导致日志断链。
解决方案:自定义TaskDecorator
Spring提供了TaskDecorator接口,允许在任务提交到线程池时,包装任务执行逻辑,把父线程的MDC上下文传递过去:
java
import org.slf4j.MDC;
import org.springframework.core.task.TaskDecorator;
public class MdcTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
// 在父线程中获取 MDC 上下文(此时还在父线程中)
Map<String, String> contextMap = MDC.getCopyOfContextMap();
return () -> {
try {
// 在子线程中恢复 MDC 上下文
if (contextMap != null) {
MDC.setContextMap(contextMap);
}
runnable.run();
} finally {
// 子线程执行完成后清除 MDC,防止污染线程池中的其他任务
MDC.clear();
}
};
}
}
注册到线程池:
java
@Bean("orderTaskExecutor")
public ThreadPoolTaskExecutor orderTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("order-async-");
executor.setTaskDecorator(new MdcTaskDecorator()); // 关键:注入 MDC 装饰器
executor.initialize();
return executor;
}
为什么MDC.clear()必须在finally中执行? 线程池会复用线程。如果上一个任务的MDC没有被清理,下一个任务会"继承"上一个任务的TraceID,导致日志串号------这是生产环境中最隐蔽的排查陷阱之一。
4.3 异步调用的可观测性
Spring Boot 4.0的Actuator和Micrometer可以暴露线程池的指标(活跃线程数、队列深度、任务总数),帮助在生产环境中监控异步任务的健康状况:
yaml
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
关键指标:
| 指标 | 含义 | 告警阈值 |
|---|---|---|
executor.active |
活跃线程数 | 持续接近maxSize |
executor.queued |
队列中等待的任务数 | 持续>0 |
executor.completed |
已完成任务数 | 用于趋势分析 |
五、Spring事件驱动开发
5.1 为什么要用事件驱动
在微服务架构中,一个业务操作往往需要触发多个下游操作。以"用户注册"为例:注册成功后需要发送欢迎邮件、发放新人优惠券、初始化用户积分、记录注册日志。如果全部在UserService.register()中同步调用,会导致:
- 代码耦合严重 :
UserService依赖了邮件、优惠券、积分、日志四个服务 - 性能瓶颈:所有操作串行执行,注册接口响应时间 = 各操作耗时之和
- 扩展困难 :新增一个"注册后发送站内信"的功能,需要修改
UserService的代码
Spring事件驱动 基于发布-订阅模式(观察者模式),使得系统中不同模块可以通过事件进行松散耦合的通信,而不需要直接依赖彼此。UserService只需要发布一个"用户已注册"事件,邮件、优惠券、积分等模块各自监听该事件并执行自己的逻辑。
5.2 同步事件实战
定义事件:
java
public class UserRegisteredEvent extends ApplicationEvent {
private final Long userId;
private final String phone;
public UserRegisteredEvent(Object source, Long userId, String phone) {
super(source);
this.userId = userId;
this.phone = phone;
}
public Long getUserId() { return userId; }
public String getPhone() { return phone; }
}
发布事件:
java
@Service
public class UserService {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void register(UserCreateRequest request) {
// 1. 核心业务:创建用户
User user = new User();
user.setPhone(request.getPhone());
userMapper.insert(user);
// 2. 发布事件(同步)
eventPublisher.publishEvent(new UserRegisteredEvent(this, user.getId(), request.getPhone()));
}
}
监听事件:
java
@Component
@Slf4j
public class UserRegisteredEventListener {
@EventListener
public void handleWelcomeEmail(UserRegisteredEvent event) {
log.info("发送欢迎邮件: userId={}", event.getUserId());
emailService.sendWelcomeEmail(event.getUserId());
}
@EventListener
public void handleCoupon(UserRegisteredEvent event) {
log.info("发放新人优惠券: userId={}", event.getUserId());
couponService.grantNewUserCoupon(event.getUserId());
}
@EventListener
public void handlePoints(UserRegisteredEvent event) {
log.info("初始化用户积分: userId={}", event.getUserId());
pointsService.initPoints(event.getUserId());
}
}
5.3 异步事件:@Async + @EventListener
默认情况下,Spring事件是同步的,发布者线程会阻塞直到所有监听器完成处理。如果监听器的处理耗时较长(如发送邮件、调用外部服务),会导致发布者的响应时间增加。
解决方案:为监听器添加@Async注解:
java
@Component
@Slf4j
public class UserRegisteredEventListener {
@Async("orderTaskExecutor")
@EventListener
public void handleWelcomeEmail(UserRegisteredEvent event) {
log.info("异步发送欢迎邮件: userId={}", event.getUserId());
emailService.sendWelcomeEmail(event.getUserId());
}
@Async("orderTaskExecutor")
@EventListener
public void handleCoupon(UserRegisteredEvent event) {
log.info("异步发放新人优惠券: userId={}", event.getUserId());
couponService.grantNewUserCoupon(event.getUserId());
}
}
关键理解 :@EventListener和@Async配合使用时,事件发布者立即返回,监听器在异步线程中执行。这实现了"发布即返回"的效果。
踩坑提示 :@Async + @EventListener组合要求@EnableAsync已启用,且监听器必须由Spring管理。同时,异步监听器的异常处理同样需要AsyncUncaughtExceptionHandler。
5.4 事务事件:@TransactionalEventListener
Spring事件的一个经典陷阱:在事务提交前发布事件。如果事务回滚,事件已经发出去了,监听器可能已经执行了不可逆的操作(如发送了邮件)。
@TransactionalEventListener解决了这个问题------它允许将监听器绑定到事务的特定阶段:
java
@Component
@Slf4j
public class UserRegisteredTransactionListener {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Async("orderTaskExecutor")
public void handleAfterCommit(UserRegisteredEvent event) {
log.info("事务提交后处理: userId={}", event.getUserId());
// 只有事务提交成功后才执行
emailService.sendWelcomeEmail(event.getUserId());
couponService.grantNewUserCoupon(event.getUserId());
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void handleAfterRollback(UserRegisteredEvent event) {
log.warn("事务回滚,执行清理: userId={}", event.getUserId());
// 事务回滚后的清理逻辑
}
}
事务阶段对照表:
phase |
含义 | 适用场景 |
|---|---|---|
BEFORE_COMMIT |
事务提交前 | 需要在提交前做最后检查 |
AFTER_COMMIT |
事务成功提交后(默认) | 发送邮件、发放优惠券 |
AFTER_ROLLBACK |
事务回滚后 | 清理资源、记录回滚日志 |
AFTER_COMPLETION |
事务完成(提交或回滚) | 无论结果如何都要执行 |
关键规则 :@TransactionalEventListener默认在AFTER_COMMIT阶段执行。如果没有事务上下文,监听器默认不会执行 。可以通过fallbackExecution = true让监听器在没有事务时也执行:
java
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true)
5.5 事件驱动 vs 消息队列的选型
| 维度 | Spring事件 | 消息队列(RocketMQ/Kafka) |
|---|---|---|
| 作用范围 | 单个JVM内 | 跨服务、跨系统 |
| 可靠性 | 进程重启后事件丢失 | 持久化,支持重试 |
| 异步性 | 同步或异步 | 天然异步 |
| 适用场景 | 模块解耦、业务编排 | 服务解耦、流量削峰 |
| 复杂度 | 低(零依赖) | 高(需维护MQ集群) |
选型建议:
- 同一服务内的模块解耦 → Spring事件
- 跨服务的异步通信 → 消息队列(第31课详解)
- 需要事务保证的事件 → @TransactionalEventListener
- 需要可靠投递的事件 → 消息队列 + 本地消息表
六、踩坑指南
坑一:@Async方法在同一个类中调用不生效
现象 :在Service中定义了一个@Async方法,但在同一个类的另一个方法中直接调用,异步未生效。
原因 :Spring的@Async基于AOP代理实现。同一个类内部的方法调用不会经过代理,因此@Async注解被忽略。
解决 :注入自身(@Autowired private OrderAsyncService self;),通过self.asyncMethod()调用;或将异步方法抽取到独立的Service中。
坑二:线程池队列无限大导致OOM
现象:系统在高峰期出现OOM,线程池活跃线程数始终等于corePoolSize,从未增长到maxPoolSize。
原因 :queueCapacity使用了默认值Integer.MAX_VALUE,队列永远塞不满,maxPoolSize永远不会触发,任务无限堆积在队列中,最终OOM。
解决 :显式配置queueCapacity(建议200~1000),并设置合理的拒绝策略(CallerRunsPolicy或AbortPolicy)。
坑三:虚拟线程下ThreadLocal未清理导致内存泄漏
现象:启用虚拟线程后,内存使用量持续增长。
原因:虚拟线程可以创建数百万个,每个虚拟线程的ThreadLocal如果未清理,会累积占用大量内存。
解决 :在虚拟线程中,避免使用ThreadLocal ,改用ScopedValue(JDK 21预览特性);如果必须使用ThreadLocal,确保在finally中调用remove()。
坑四:异步方法抛出的异常"消失"了
现象 :@Async方法执行失败,但调用方没有任何感知,日志中也没有错误信息。
原因 :@Async void方法的异常不会被传播到调用方,默认只被日志记录(SimpleAsyncUncaughtExceptionHandler)。如果日志级别配置不当,异常可能被吞掉。
解决 :实现AsyncUncaughtExceptionHandler,在异步方法抛出未捕获异常时记录详细日志并发送告警。
坑五:MDC在异步线程中丢失导致TraceID断链
现象:异步方法中的日志没有TraceID,无法与主线程的日志关联。
原因 :MDC基于ThreadLocal,异步线程无法访问父线程的MDC。
解决 :自定义TaskDecorator,在任务提交时捕获MDC快照,在任务执行时恢复(详见第4.2节)。
坑六:@TransactionalEventListener在无事务时静默跳过
现象 :@TransactionalEventListener监听器没有执行,但没有任何错误日志。
原因 :@TransactionalEventListener默认只在事务上下文中执行。如果没有活动事务,监听器会被静默跳过。
解决 :设置fallbackExecution = true,或确保发布事件的方法上有@Transactional注解。
坑七:事件监听器中事务传播导致死锁
现象:同步事件监听器中执行了数据库操作,与主事务产生锁竞争,导致死锁。
原因 :同步@EventListener会加入当前事务,监听器中的数据库操作与主事务共享同一个数据库连接,可能导致锁竞争。
解决 :将监听器改为异步(@Async)或使用@TransactionalEventListener(phase = AFTER_COMMIT),让监听器在主事务提交后执行。
七、课后作业
作业一 :在service-order中创建OrderAsyncService,实现两个异步方法:sendSmsNotification(模拟耗时300ms)和addUserPoints(模拟耗时150ms)。在订单创建接口中调用这两个异步方法,验证接口响应时间不包含异步方法的耗时。
作业二 :配置自定义ThreadPoolTaskExecutor(核心线程10、最大线程50、队列容量200),并通过@Async("orderTaskExecutor")指定使用该线程池。在/actuator/metrics中查看线程池的活跃线程数和队列深度指标。
作业三 :实现AsyncUncaughtExceptionHandler,在异步方法抛出异常时记录详细日志并发送告警。验证异常处理方法被正确调用。
作业四 :实现MdcTaskDecorator,解决异步方法中TraceID断链问题。验证:在异步方法中输出的日志包含与主线程相同的TraceID。
作业五(进阶) :实现基于@TransactionalEventListener的用户注册事件。当用户注册事务提交成功后,异步执行"发送欢迎邮件"和"发放优惠券"两个监听器。验证:如果注册事务回滚,监听器不会执行。
八、下节预告
第30课将进入分布式事务解决方案 & Seata最终一致性实战 。内容包括分布式事务问题剖析、CAP/BASE理论、TCC/XA/AT三种模式的原理与对比、Seata架构详解、AT模式实战(含@GlobalTransactional注解)、事务回滚与超时处理,以及生产环境的事务适配方案。本课解决了单服务内的异步与解耦 问题,第30课将解决跨服务的分布式事务一致性问题------当一个业务操作需要跨多个服务、多个数据库时,如何保证数据的一致性。
🔗《最新版 SpringCloud 2025 从入门到实战》系列课程导航
第一部分:微服务前置基础 & 新版环境搭建(第1-5课)
第二部分:注册中心核心(Nacos 最新版)(第6-9课)
第三部分:配置中心核心(Nacos配置中心)(第10-12课)
第四部分:服务通信核心(OpenFeign + LoadBalancer)(第13-16课)
第五部分:网关核心(SpringCloud Gateway 新版)(第17-20课)
第六部分:熔断、限流、降级(Sentinel 新版)(第21-24课)
第七部分:微服务监控、链路追踪、日志体系(第25-28课)
第八部分:微服务高阶特性 & 分布式核心能力(第29-31课)
第九部分:企业级完整项目实战 & 架构复盘(第32-35课)