Spring Boot中@Async+TransactionalEventListener首次请求未异步问题
环境信息
- Java 17
- Spring Boot 3.1.4
业务背景
父服务器(WCS)仅需确认是否接收到提交的数据,因此我们将耗时的数据转换、存储及转发至子系统的逻辑拆分出来,把父服务器的响应时间从8秒缩短至约1秒。但测试时遇到了异常情况:
问题描述
调试@TransactionalEventListener中的逻辑时:
- 首次请求
localhost:8080/save/1,必须等调试完成才会返回200响应 - 第二次请求时,即便在
@TransactionalEventListener中打断点,200响应仍会立即返回
为何首次请求会等待@TransactionalEventListener中的逻辑执行完毕?
测试场景临时使用@GetMapping,相关代码如下:
控制器代码
import com.example.demo.biz.OrderBizService; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; @RestController @RequiredArgsConstructor public class WcsWaveController { private final OrderBizService orderBizService; @GetMapping("/save/{id}") public ResponseEntity save(@PathVariable(name = "id") Long id) { orderBizService.save(id); return new ResponseEntity(HttpStatus.OK); } }
业务服务代码
@Service @RequiredArgsConstructor public class OrderBizService { private final WCSWaveService wcsWaveService; private final ApplicationEventPublisher publisher; @Transactional public void save(Long id) { System.out.println("OrderBizService Thread: "+Thread.currentThread().getName()); WCSWaveEntity wcsWaveEntity = new WCSWaveEntity(); wcsWaveEntity.setId(id); wcsWaveEntity.setName(Long.toString(id)); wcsWaveService.save(wcsWaveEntity); PickingSaveEvent pickingSaveEvent = new PickingSaveEvent(); pickingSaveEvent.setWcsWaveId(wcsWaveEntity.getId()); pickingSaveEvent.setName("test1"); publisher.publishEvent(pickingSaveEvent); } }
事件监听服务代码
@Service @RequiredArgsConstructor public class PickingWaveService { private final PickingWaveRepository pickingWaveRepository; @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) @Transactional(propagation = Propagation.REQUIRES_NEW) @Async public void save(PickingSaveEvent event) { System.out.println("TransactionalEventListener Thread: "+Thread.currentThread().getName()); PickingWaveEntity pickingWaveEntity = new PickingWaveEntity(); pickingWaveEntity.setId(1L); pickingWaveEntity.setWcsWaveId(event.getWcsWaveId()); pickingWaveEntity.setName(event.getName()); pickingWaveRepository.save(pickingWaveEntity); } }
启动类代码
@SpringBootApplication @EnableAsync public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }
事件实体类代码
@Getter @Setter public class PickingSaveEvent { private Long id; private Long WcsWaveId; private String name; }
异步线程池配置代码
@Configuration @EnableAsync public class AsyncThreadConfiguration { @Bean public Executor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(5); executor.setQueueCapacity(500); executor.setThreadNamePrefix("test"); executor.initialize(); return executor; } }
现象
- 首次请求未立即响应,不符合预期
- 第二次及之后的请求正常异步返回响应
原因分析
首次请求时,异步线程池和@Async方法的代理对象是懒加载初始化的。虽然你在配置类中调用了executor.initialize(),但Spring的异步代理机制会在首次执行@Async方法时,才完成代理对象的创建以及线程池的绑定工作。这个初始化过程是在当前请求线程中同步执行的,导致@TransactionalEventListener的逻辑被同步阻塞,直到初始化完成,请求才会返回响应。
第二次请求时,线程池和代理对象已经初始化完成,@Async方法可以直接提交到线程池的独立线程中执行,不会阻塞请求线程,因此响应会立即返回。
解决方案
1. 提前触发异步代理初始化
在应用启动时主动触发一次异步方法的执行,完成代理和线程池的初始化,避免首次请求阻塞。例如在启动类中实现CommandLineRunner:
@SpringBootApplication @EnableAsync public class DemoApplication implements CommandLineRunner { @Autowired private PickingWaveService pickingWaveService; public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } @Override public void run(String... args) throws Exception { // 触发异步代理初始化 pickingWaveService.initAsync(new AsyncTestEvent()); } // 定义测试用空事件 static class AsyncTestEvent {} }
然后在PickingWaveService中添加空的异步监听方法:
@Async @TransactionalEventListener public void initAsync(AsyncTestEvent event) { // 空方法,仅用于触发异步代理初始化 }
2. 优化线程池初始化配置
确保线程池完全初始化,虽然executor.initialize()已经调用,但结合Spring的生命周期,提前触发一次执行可以彻底解决懒加载带来的首次阻塞问题。
3. 调整注解顺序(可选优化)
@Async和@Transactional的代理顺序会影响执行逻辑,确保@Async代理优先于事务代理,这样异步逻辑能真正在独立线程中执行。可以通过调整注解顺序或使用@Order控制,但这不是首次阻塞的核心原因,仅作为优化点。
内容的提问来源于stack exchange,提问作者최지율

