Spring 异步与事务混用的踩坑与原理分析

Spring 异步与事务混用的踩坑与原理分析 本文从一个实际业务场景出发记录了一次代码改造的完整思考过程从原有同步更新逻辑到引入异步执行再到解决事务问题最后总结出两种可行的方案及其背后的原理。希望对遇到类似问题的同学有所帮助。一、原始代码同步更新先看最原始的代码这是一个标准的批量更新 ServicejavaService RequiredArgsConstructor public class BatchService { private final JdbcTemplate jdbcTemplate; Transactional(rollbackFor Exception.class) public void batchUpdate(ListData dataList) { String sql UPDATE your_table SET status ? WHERE id ?; jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { Override public void setValues(PreparedStatement ps, int i) throws SQLException { Data data dataList.get(i); ps.setString(1, data.getStatus()); ps.setLong(2, data.getId()); } Override public int getBatchSize() { return dataList.size(); } }); } }调用方javaService RequiredArgsConstructor public class OrderService { private final BatchService batchService; Transactional(rollbackFor Exception.class) public void processOrders(ListData dataList) { // 前置业务逻辑... batchService.batchUpdate(dataList); // 同步执行 // 后置业务逻辑... } }现状所有数据更新都是同步执行且加入调用方的事务。新需求当数据量超过 10 条时改为异步执行且在独立事务中运行。二、第一步开启异步前提启用 Spring 异步功能javaConfiguration EnableAsync public class AsyncConfig { // 线程池配置稍后说明 }方案一新建一个 Service 类推荐这是最符合 Spring 设计理念的方式职责分离清晰。javaService RequiredArgsConstructor public class AsyncBatchService { private final BatchService batchService; Async public void asyncBatchUpdate(ListData dataList) { batchService.batchUpdate(dataList); } }调用方改造javaService RequiredArgsConstructor public class OrderService { private final BatchService batchService; private final AsyncBatchService asyncBatchService; Transactional(rollbackFor Exception.class) public void processOrders(ListData dataList) { if (dataList.size() 10) { asyncBatchService.asyncBatchUpdate(dataList); // 异步 } else { batchService.batchUpdate(dataList); // 同步 } } }优点职责清晰AsyncBatchService专门负责异步调度不需要任何特殊处理Async直接生效没有循环依赖风险缺点多了一个类有些人觉得过度设计方案二自己注入自己自注入如果不想新增类可以在原 Service 中注入自己。javaService RequiredArgsConstructor public class BatchService { private final JdbcTemplate jdbcTemplate; // 自注入获取代理对象 Autowired private BatchService self; Transactional(rollbackFor Exception.class) public void batchUpdate(ListData dataList) { jdbcTemplate.batchUpdate(...); } Async public void asyncBatchUpdate(ListData dataList) { // 必须通过 self 调用否则 Async 失效 self.batchUpdate(dataList); } }调用方javaService RequiredArgsConstructor public class OrderService { private final BatchService batchService; Transactional(rollbackFor Exception.class) public void processOrders(ListData dataList) { if (dataList.size() 10) { batchService.asyncBatchUpdate(dataList); // 通过代理调用Async 生效 } else { batchService.batchUpdate(dataList); } } }⚠️ 自注入的三个关键注意点1. 必须用Autowired字段注入不能用构造器注入java// ❌ 错误自注入不能用构造器 RequiredArgsConstructor Service public class BatchService { private final BatchService self; // 构造器注入会导致循环依赖启动失败 } // ✅ 正确使用字段注入 Service RequiredArgsConstructor public class BatchService { Autowired private BatchService self; // 字段注入Spring 允许 }原因自注入本质是自己依赖自己构造器注入会形成循环依赖Spring 无法解决。字段注入在对象创建完成后通过反射注入绕过了构造阶段。2. Spring Boot 2.6 可能需要LazySpring Boot 2.6 加强了循环依赖检测自注入可能触发警告textBeanCurrentlyInCreationException: Circular reference involving bean batchService解决方案javaService RequiredArgsConstructor public class BatchService { Lazy // 懒加载延迟到第一次使用时注入 Autowired private BatchService self; }3.Transactional的调用方式java// ❌ 错误内部调用事务失效 Async public void asyncBatchUpdate(ListData dataList) { this.batchUpdate(dataList); // this 是原始对象Transactional 失效 } // ✅ 正确通过 self 调用事务生效 Async public void asyncBatchUpdate(ListData dataList) { self.batchUpdate(dataList); // self 是代理对象Transactional 生效 }方案对比第一步维度新建 Service自注入代码量多一个类只加一个字段职责分离✅ 清晰❌ 耦合在一起Async 生效✅ 直接生效✅ 需通过 self 调用循环依赖风险❌ 无⚠️ 需注意注入方式推荐度⭐⭐⭐⭐⭐⭐⭐⭐三、第二步异步方法的事务问题上面的代码中batchUpdate上的Transactional在异步场景下能生效吗答案是视情况而定。调用方式事务是否生效原因新建 ServicebatchService.batchUpdate()✅ 生效调用的是batchService的代理对象自注入self.batchUpdate()✅ 生效self是代理对象内部调用this.batchUpdate()❌ 失效this是原始对象但是即使通过代理调用Transactional生效了它开启的事务是在异步线程中还是在主线程中关键问题事务是绑定在线程上的Spring 的事务管理基于ThreadLocaljava// TransactionSynchronizationManager 的核心实现 private static final ThreadLocalMapObject, Object resources new NamedThreadLocal(Transactional resources);结论主线程的事务异步线程无法继承异步线程必须重新开启自己的事务因此batchUpdate上的Transactional虽然在异步线程中生效了开启了一个新事务但这要求batchUpdate是通过代理对象调用的。3.1 事务问题的解决方案方案 A声明式事务 自注入适合自注入方案javaService RequiredArgsConstructor public class BatchService { private final JdbcTemplate jdbcTemplate; Autowired private BatchService self; // 对外入口根据数据量决定同步/异步 Transactional(rollbackFor Exception.class) public void executeBatch(ListData dataList) { if (dataList.size() 10) { self.asyncBatchUpdate(dataList); // 异步通过 self 调用 } else { doUpdate(dataList); // 同步直接调用 } } Async public void asyncBatchUpdate(ListData dataList) { // 通过 self 调用让 Transactional 在异步线程生效 self.doUpdate(dataList); } Transactional(rollbackFor Exception.class) public void doUpdate(ListData dataList) { jdbcTemplate.batchUpdate(...); } }执行流程text同步场景≤ 10 条 executeBatch() 开启事务 T1主线程 → doUpdate() 加入 T1 ✅ 异步场景 10 条 executeBatch() 开启事务 T1主线程 → self.asyncBatchUpdate() 提交到线程池 → 主线程返回T1 提交 → 异步线程执行 asyncBatchUpdate() → self.doUpdate() 开启新事务 T2异步线程✅优点代码集中在同一个类事务通过Transactional声明式控制缺点自注入再次被使用如果第一步选择了新建 Service这里再用自注入就矛盾了Async和Transactional仍然需要分离到不同方法方案 B编程式事务适合新建 Service 方案javaService RequiredArgsConstructor public class AsyncBatchService { private final BatchService batchService; private final TransactionTemplate transactionTemplate; Async public void asyncBatchUpdate(ListData dataList) { // 在异步线程中用编程式事务开启新事务 transactionTemplate.execute(status - { batchService.doUpdate(dataList); // 调用核心业务逻辑 return null; }); } }核心业务逻辑从BatchService中抽离javaService RequiredArgsConstructor public class BatchService { private final JdbcTemplate jdbcTemplate; // 纯业务逻辑不加事务注解事务由调用方控制 public void doUpdate(ListData dataList) { jdbcTemplate.batchUpdate(...); } }调用方javaService RequiredArgsConstructor public class OrderService { private final BatchService batchService; private final AsyncBatchService asyncBatchService; Transactional(rollbackFor Exception.class) public void processOrders(ListData dataList) { if (dataList.size() 10) { asyncBatchService.asyncBatchUpdate(dataList); // 异步 独立事务 } else { batchService.doUpdate(dataList); // 同步 加入当前事务 } } }执行流程text同步场景≤ 10 条 processOrders() 开启事务 T1主线程 → batchService.doUpdate() 加入 T1 ✅ 异步场景 10 条 processOrders() 开启事务 T1主线程 → asyncBatchService.asyncBatchUpdate() 提交到线程池 → 主线程返回T1 提交 → 异步线程执行 asyncBatchUpdate() → transactionTemplate.execute() 开启新事务 T2异步线程 → batchService.doUpdate() 在 T2 中执行 ✅优点职责分离AsyncBatchService负责异步 事务BatchService只负责业务完全避开Async和Transactional混用的问题不需要任何自注入缺点代码稍多但结构更清晰⚠️ 为什么不能异步方法直接加Transactional很多人会想当然地写成这样javaAsync Transactional // ❌ 这样写事务会失效 public void asyncBatchUpdate(ListData dataList) { // 业务逻辑 }原理分析Spring 同时支持Async和Transactional但两者都是通过代理实现的。当一个方法同时标注两个注解时Spring 会生成嵌套代理text外层代理事务代理 ↓ 内层代理异步代理 ↓ 原始方法执行流程text主线程 1. 事务代理拦截方法调用准备开启事务 2. 异步代理拦截将方法提交到线程池 3. 主线程直接返回事务代理的开启事务操作还没真正执行 异步线程 4. 执行原始方法体 5. 此时已经绕过了事务代理Transactional 完全失效 ❌结果异步线程中的数据库操作在无事务状态下执行每条 SQL 自动提交无法整体回滚。两种方案总结第二步方案第一步选择事务控制方式是否需要自注入推荐度声明式事务 自注入自注入Transactional✅ 需要⭐⭐⭐编程式事务新建 ServiceTransactionTemplate❌ 不需要⭐⭐⭐⭐⭐核心原则如果第一步选择了新建 Service来解决异步问题第二步就不要再使用自注入来解决事务问题否则新建 Service 就失去了意义——绕了一圈又回到自注入代码结构反而更混乱。四、完整方案对比方案一自注入 声明式事务一条道走到黑javaService RequiredArgsConstructor public class BatchService { private final JdbcTemplate jdbcTemplate; Lazy Autowired private BatchService self; Transactional(rollbackFor Exception.class) public void executeBatch(ListData dataList) { if (dataList.size() 10) { self.asyncBatchUpdate(dataList); } else { doUpdate(dataList); } } Async public void asyncBatchUpdate(ListData dataList) { self.doUpdate(dataList); } Transactional(rollbackFor Exception.class) public void doUpdate(ListData dataList) { jdbcTemplate.batchUpdate(...); } }特点所有逻辑都在一个类中需要处理自注入的循环依赖问题LazyAsync和Transactional分别在不同方法上同步走调用方事务异步走独立事务方案二新建 Service 编程式事务推荐java// 核心业务纯逻辑无事务注解 Service RequiredArgsConstructor public class BatchService { private final JdbcTemplate jdbcTemplate; public void doUpdate(ListData dataList) { jdbcTemplate.batchUpdate(...); } } // 异步包装负责异步 事务 Service RequiredArgsConstructor public class AsyncBatchService { private final BatchService batchService; private final TransactionTemplate transactionTemplate; Async public void asyncBatchUpdate(ListData dataList) { transactionTemplate.execute(status - { batchService.doUpdate(dataList); return null; }); } } // 调用方根据数据量决策 Service RequiredArgsConstructor public class OrderService { private final BatchService batchService; private final AsyncBatchService asyncBatchService; Transactional(rollbackFor Exception.class) public void processOrders(ListData dataList) { if (dataList.size() 10) { asyncBatchService.asyncBatchUpdate(dataList); } else { batchService.doUpdate(dataList); } } }特点职责清晰业务、异步调度、决策分离无自注入无循环依赖风险编程式事务完全可控单元测试更容易每个类职责单一五、线程池配置建议5.1 低频场景下的线程池配置如果数据 10是低频场景建议配置弹性线程池javaConfiguration EnableAsync public class AsyncConfig { Bean(batchExecutor) public Executor batchExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(0); // 核心线程为0不常驻 executor.setMaxPoolSize(10); // 峰值控制 executor.setQueueCapacity(100); executor.setKeepAliveSeconds(60); // 空闲60秒后销毁 executor.setAllowCoreThreadTimeOut(true); // 允许核心线程超时 executor.setThreadNamePrefix(batch-); executor.setRejectedExecutionHandler( new ThreadPoolExecutor.CallerRunsPolicy() ); executor.initialize(); return executor; } }5.2corePoolSize0与new Thread()的区别维度corePoolSize0线程池new Thread()线程创建按需创建每次新建线程复用60秒内复用不复用最大并发控制maxPoolSize限流无限创建风险高拒绝策略可配置降级保护无控制线程命名统一前缀随机 Thread-N优雅关闭shutdown()支持无法控制监控能力可查看队列、活跃数无结论即使低频也建议使用线程池而非new Thread()以获得并发控制和优雅关闭能力。六、架构决策树根据你的场景选择text你的第一步选择是什么 │ ├── 不想新增类 → 自注入 │ │ │ └── 第二步继续自注入 Transactional一条道走到黑 │ 特点代码集中需注意 Lazy 和注入方式 │ └── 愿意新增类 → 新建 Service │ └── 第二步编程式事务TransactionTemplate 特点职责清晰无自注入最推荐七、核心原理总结7.1 代理机制Spring 的Async和Transactional都基于动态代理调用方式是否走代理注解是否生效外部注入的对象调用✅ 是✅ 生效内部this调用❌ 否❌ 失效自注入self调用✅ 是✅ 生效7.2 事务与线程绑定Spring 事务基于ThreadLocal每个线程有自己的事务上下文主线程的事务无法传递给异步线程异步线程必须重新开启独立事务异步线程的事务和主线程事务完全隔离7.3 Async Transactional 不能同时标注在同一方法两个注解的代理会形成嵌套代理导致事务代理被异步代理架空最终事务失效。7.4 自注入的注意事项必须使用Autowired字段注入不能用构造器注入Spring Boot 2.6 可能需要加Lazy通过self调用才能走代理this调用无效八、注意1、手动使用事务transactionTemplate.execute(status - {batchService.doUpdate(dataList);return null;});这种会强制开启新事务不会采用默认的传播级别即继承之前的事务2、开启异步注解要在 Spring 中开启异步支持需要在配置类上添加EnableAsync注解。核心配置最基础的方式是在项目启动类或配置类上加上EnableAsyncjavaSpringBootApplication EnableAsync // 开启异步功能 public class YourApplication { public static void main(String[] args) { SpringApplication.run(YourApplication.class, args); } }线程池配置EnableAsync默认会使用一个简单的SimpleAsyncTaskExecutor这个执行器每次执行任务都会创建新线程在负载较高时不够高效。通常推荐进行自定义配置。这里是指spring的默认行为不是spring boot有两种常见的自定义方式方式一实现AsyncConfigurer接口javaConfiguration EnableAsync public class AsyncConfig implements AsyncConfigurer { Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); // 核心线程数 executor.setMaxPoolSize(10); // 最大线程数 executor.setQueueCapacity(100); // 队列容量 executor.setThreadNamePrefix(Async-); // 线程名前缀 executor.initialize(); return executor; } }方式二声明一个ExecutorBeanjavaConfiguration EnableAsync public class AsyncConfig { Bean(name taskExecutor) public Executor taskExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(10); executor.setQueueCapacity(100); executor.setThreadNamePrefix(Async-); executor.initialize(); return executor; } }自定义线程池后在Async注解中可以指定要使用的执行器名称javaAsync(taskExecutor) // 使用自定义线程池 public void asyncMethod() { // 异步任务逻辑 }Spring Boot 自动配置在 Spring Boot 中如果上下文中没有自定义的Executor它会自动配置一个ThreadPoolTaskExecutor默认核心线程数为 8可以在application.properties或application.yml中调优propertiesspring.task.execution.pool.max-size16 spring.task.execution.pool.queue-capacity100 spring.task.execution.pool.keep-alive10s总结配置 Spring 异步最核心的是EnableAsync注解它如同一个“总开关”。为了更好地控制性能和资源建议通过实现AsyncConfigurer或直接定义Bean的方式提供一个自定义的ThreadPoolTaskExecutor合理配置核心线程数、最大线程数和队列容量等关键参数。3、对于第二点的解释第二点是ai总结的我有个疑问对话如下我你说EnableAsync 默认会使用一个简单的 SimpleAsyncTaskExecutor然后又说如果上下文中没有自定义的 Executor它会自动配置一个 ThreadPoolTaskExecutor默认核心线程数为 8那么我使用EnableAsync 开启后默认到底用哪个线程池deepseek这个问题问得很好因为它触及了 Spring 和 Spring Boot 在异步处理上的一个关键区别。简单来说EnableAsync的默认行为与 Spring Boot 的自动配置是两个不同的逻辑。我们可以分两个层面来看第一层Spring 框架的默认行为仅EnableAsync如果你只是添加了EnableAsync注解而没有在 Spring Boot 环境下或者 Spring Boot 没来得及做自动配置那么它的默认行为是优先查找上下文中唯一的TaskExecutorbean或者名为taskExecutor的Executorbean。如果找不到最终会使用SimpleAsyncTaskExecutor。这个SimpleAsyncTaskExecutor会为每个任务创建一个新线程不进行复用在并发高时会有性能风险。第二层Spring Boot 的自动配置你实际遇到的情况但在 Spring Boot 项目中情况完全不同。Spring Boot 会介入并为你自动配置一个更合适的线程池。根据最新的 Spring Boot 官方文档如果项目中没有自定义的ExecutorBeanSpring Boot 会自动配置一个AsyncTaskExecutor。这个自动配置的执行器默认就是一个ThreadPoolTaskExecutor并且它拥有8 个核心线程。这些核心线程数可以根据负载动态调整。结论到底用的是哪一个在你的 Spring Boot 项目中因为没有任何自定义线程池所以最终生效的就是 Spring Boot 自动配置的那个拥有 8 个核心线程的ThreadPoolTaskExecutor。EnableAsync原本指向的SimpleAsyncTaskExecutor在这里被 Spring Boot 的更强大的自动配置覆盖了。你可以通过spring.task.execution开头的配置来微调这个线程池比如propertiesspring.task.execution.pool.max-size16 spring.task.execution.pool.queue-capacity100两个默认线程池的风险对比线程池核心行为主要风险SimpleAsyncTaskExecutor(Spring 框架默认)来一个任务创建一个新线程不复用。高并发下会无限制创建线程可能导致资源耗尽甚至 OOM。ThreadPoolTaskExecutor(Spring Boot 自动配置)使用 8 个核心线程线程可复用有任务队列。默认队列容量非常大Integer.MAX_VALUE如果任务积压可能消耗大量内存。所以即使 Spring Boot 的默认线程池比SimpleAsyncTaskExecutor要好但在生产环境中依然强烈建议你显式地自定义一个线程池。这样可以更精确地控制核心线程数、最大线程数和队列长度以匹配你业务的真实负载规避潜在的内存风险。九、最后总结方案异步实现事务控制自注入循环依赖推荐度方案一一条道走到黑自注入声明式Transactional✅ 需要⚠️ 需注意⭐⭐⭐方案二新建 Service新建类编程式TransactionTemplate❌ 不需要✅ 无⭐⭐⭐⭐⭐最终建议采用方案二新建 Service 编程式事务。虽然多了一个类但职责清晰、没有自注入的循环依赖风险、事务控制更灵活是生产环境最稳妥的选择。附录完整项目示例代码完整的可运行示例已包含在文中关键类为BatchService核心业务AsyncBatchService异步调度 事务OrderService调用方决策AsyncConfig线程池配置如有疑问欢迎交流讨论