You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot中@Async+TransactionalEventListener首次请求未异步问题

问题分析:首次请求时@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,提问作者최지율

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 12:27:03