尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

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

Spring 异步与事务混用的踩坑与原理分析
📅 发布时间:2026/6/26 4:56:26

本文从一个实际业务场景出发,记录了一次代码改造的完整思考过程:从原有同步更新逻辑,到引入异步执行,再到解决事务问题,最后总结出两种可行的方案及其背后的原理。希望对遇到类似问题的同学有所帮助。


一、原始代码:同步更新

先看最原始的代码,这是一个标准的批量更新 Service:

java

@Service @RequiredArgsConstructor public class BatchService { private final JdbcTemplate jdbcTemplate; @Transactional(rollbackFor = Exception.class) public void batchUpdate(List<Data> 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(); } }); } }

调用方:

java

@Service @RequiredArgsConstructor public class OrderService { private final BatchService batchService; @Transactional(rollbackFor = Exception.class) public void processOrders(List<Data> dataList) { // 前置业务逻辑... batchService.batchUpdate(dataList); // 同步执行 // 后置业务逻辑... } }

现状:所有数据更新都是同步执行,且加入调用方的事务。

新需求:当数据量超过 10 条时,改为异步执行,且在独立事务中运行。


二、第一步:开启异步

前提:启用 Spring 异步功能

java

@Configuration @EnableAsync public class AsyncConfig { // 线程池配置稍后说明 }

方案一:新建一个 Service 类(推荐)

这是最符合 Spring 设计理念的方式,职责分离清晰。

java

@Service @RequiredArgsConstructor public class AsyncBatchService { private final BatchService batchService; @Async public void asyncBatchUpdate(List<Data> dataList) { batchService.batchUpdate(dataList); } }

调用方改造:

java

@Service @RequiredArgsConstructor public class OrderService { private final BatchService batchService; private final AsyncBatchService asyncBatchService; @Transactional(rollbackFor = Exception.class) public void processOrders(List<Data> dataList) { if (dataList.size() > 10) { asyncBatchService.asyncBatchUpdate(dataList); // 异步 } else { batchService.batchUpdate(dataList); // 同步 } } }

优点:

  • 职责清晰,AsyncBatchService专门负责异步调度

  • 不需要任何特殊处理,@Async直接生效

  • 没有循环依赖风险

缺点:

  • 多了一个类,有些人觉得"过度设计"


方案二:自己注入自己(自注入)

如果不想新增类,可以在原 Service 中注入自己。

java

@Service @RequiredArgsConstructor public class BatchService { private final JdbcTemplate jdbcTemplate; // 自注入:获取代理对象 @Autowired private BatchService self; @Transactional(rollbackFor = Exception.class) public void batchUpdate(List<Data> dataList) { jdbcTemplate.batchUpdate(...); } @Async public void asyncBatchUpdate(List<Data> dataList) { // 必须通过 self 调用,否则 @Async 失效 self.batchUpdate(dataList); } }

调用方:

java

@Service @RequiredArgsConstructor public class OrderService { private final BatchService batchService; @Transactional(rollbackFor = Exception.class) public void processOrders(List<Data> 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+ 可能需要@Lazy

Spring Boot 2.6 加强了循环依赖检测,自注入可能触发警告:

text

BeanCurrentlyInCreationException: Circular reference involving bean 'batchService'

解决方案:

java

@Service @RequiredArgsConstructor public class BatchService { @Lazy // 懒加载,延迟到第一次使用时注入 @Autowired private BatchService self; }
3.@Transactional的调用方式

java

// ❌ 错误:内部调用,事务失效 @Async public void asyncBatchUpdate(List<Data> dataList) { this.batchUpdate(dataList); // this 是原始对象,@Transactional 失效 } // ✅ 正确:通过 self 调用,事务生效 @Async public void asyncBatchUpdate(List<Data> dataList) { self.batchUpdate(dataList); // self 是代理对象,@Transactional 生效 }

方案对比(第一步)

维度新建 Service自注入
代码量多一个类只加一个字段
职责分离✅ 清晰❌ 耦合在一起
@Async 生效✅ 直接生效✅ 需通过 self 调用
循环依赖风险❌ 无⚠️ 需注意注入方式
推荐度⭐⭐⭐⭐⭐⭐⭐⭐

三、第二步:异步方法的事务问题

上面的代码中,batchUpdate上的@Transactional在异步场景下能生效吗?

答案是:视情况而定。

调用方式事务是否生效原因
新建 Service:batchService.batchUpdate()✅ 生效调用的是batchService的代理对象
自注入:self.batchUpdate()✅ 生效self是代理对象
内部调用:this.batchUpdate()❌ 失效this是原始对象

但是,即使通过代理调用,@Transactional生效了,它开启的事务是在异步线程中还是在主线程中?

关键问题:事务是绑定在线程上的

Spring 的事务管理基于ThreadLocal:

java

// TransactionSynchronizationManager 的核心实现 private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");

结论:

  • 主线程的事务,异步线程无法继承

  • 异步线程必须重新开启自己的事务

因此,batchUpdate上的@Transactional虽然在异步线程中生效了(开启了一个新事务),但这要求batchUpdate是通过代理对象调用的。


3.1 事务问题的解决方案

方案 A:声明式事务 + 自注入(适合自注入方案)

java

@Service @RequiredArgsConstructor public class BatchService { private final JdbcTemplate jdbcTemplate; @Autowired private BatchService self; // 对外入口:根据数据量决定同步/异步 @Transactional(rollbackFor = Exception.class) public void executeBatch(List<Data> dataList) { if (dataList.size() > 10) { self.asyncBatchUpdate(dataList); // 异步:通过 self 调用 } else { doUpdate(dataList); // 同步:直接调用 } } @Async public void asyncBatchUpdate(List<Data> dataList) { // 通过 self 调用,让 @Transactional 在异步线程生效 self.doUpdate(dataList); } @Transactional(rollbackFor = Exception.class) public void doUpdate(List<Data> 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 方案)

java

@Service @RequiredArgsConstructor public class AsyncBatchService { private final BatchService batchService; private final TransactionTemplate transactionTemplate; @Async public void asyncBatchUpdate(List<Data> dataList) { // 在异步线程中用编程式事务开启新事务 transactionTemplate.execute(status -> { batchService.doUpdate(dataList); // 调用核心业务逻辑 return null; }); } }

核心业务逻辑从BatchService中抽离:

java

@Service @RequiredArgsConstructor public class BatchService { private final JdbcTemplate jdbcTemplate; // 纯业务逻辑,不加事务注解(事务由调用方控制) public void doUpdate(List<Data> dataList) { jdbcTemplate.batchUpdate(...); } }

调用方:

java

@Service @RequiredArgsConstructor public class OrderService { private final BatchService batchService; private final AsyncBatchService asyncBatchService; @Transactional(rollbackFor = Exception.class) public void processOrders(List<Data> 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?

很多人会想当然地写成这样:

java

@Async @Transactional // ❌ 这样写事务会失效 public void asyncBatchUpdate(List<Data> dataList) { // 业务逻辑 }

原理分析:

Spring 同时支持@Async和@Transactional,但两者都是通过代理实现的。当一个方法同时标注两个注解时,Spring 会生成嵌套代理:

text

外层代理(事务代理) ↓ 内层代理(异步代理) ↓ 原始方法

执行流程:

text

主线程: 1. 事务代理拦截方法调用,准备开启事务 2. 异步代理拦截,将方法提交到线程池 3. 主线程直接返回(事务代理的"开启事务"操作还没真正执行) 异步线程: 4. 执行原始方法体 5. 此时已经绕过了事务代理,@Transactional 完全失效 ❌

结果:异步线程中的数据库操作在无事务状态下执行,每条 SQL 自动提交,无法整体回滚。


两种方案总结(第二步)

方案第一步选择事务控制方式是否需要自注入推荐度
声明式事务 + 自注入自注入@Transactional✅ 需要⭐⭐⭐
编程式事务新建 ServiceTransactionTemplate❌ 不需要⭐⭐⭐⭐⭐

核心原则:

如果第一步选择了"新建 Service"来解决异步问题,第二步就不要再使用自注入来解决事务问题,否则新建 Service 就失去了意义——绕了一圈又回到自注入,代码结构反而更混乱。


四、完整方案对比

方案一:自注入 + 声明式事务(一条道走到黑)

java

@Service @RequiredArgsConstructor public class BatchService { private final JdbcTemplate jdbcTemplate; @Lazy @Autowired private BatchService self; @Transactional(rollbackFor = Exception.class) public void executeBatch(List<Data> dataList) { if (dataList.size() > 10) { self.asyncBatchUpdate(dataList); } else { doUpdate(dataList); } } @Async public void asyncBatchUpdate(List<Data> dataList) { self.doUpdate(dataList); } @Transactional(rollbackFor = Exception.class) public void doUpdate(List<Data> dataList) { jdbcTemplate.batchUpdate(...); } }

特点:

  • 所有逻辑都在一个类中

  • 需要处理自注入的循环依赖问题(@Lazy)

  • @Async和@Transactional分别在不同方法上

  • 同步走调用方事务,异步走独立事务


方案二:新建 Service + 编程式事务(推荐)

java

// 核心业务:纯逻辑,无事务注解 @Service @RequiredArgsConstructor public class BatchService { private final JdbcTemplate jdbcTemplate; public void doUpdate(List<Data> dataList) { jdbcTemplate.batchUpdate(...); } } // 异步包装:负责异步 + 事务 @Service @RequiredArgsConstructor public class AsyncBatchService { private final BatchService batchService; private final TransactionTemplate transactionTemplate; @Async public void asyncBatchUpdate(List<Data> 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(List<Data> dataList) { if (dataList.size() > 10) { asyncBatchService.asyncBatchUpdate(dataList); } else { batchService.doUpdate(dataList); } } }

特点:

  • 职责清晰:业务、异步调度、决策分离

  • 无自注入,无循环依赖风险

  • 编程式事务完全可控

  • 单元测试更容易(每个类职责单一)


五、线程池配置建议

5.1 低频场景下的线程池配置

如果"数据 > 10"是低频场景,建议配置弹性线程池:

java

@Configuration @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.2corePoolSize=0与new Thread()的区别

维度corePoolSize=0线程池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注解。

核心配置

最基础的方式是在项目启动类或配置类上加上@EnableAsync:

java

@SpringBootApplication @EnableAsync // 开启异步功能 public class YourApplication { public static void main(String[] args) { SpringApplication.run(YourApplication.class, args); } }
线程池配置

@EnableAsync默认会使用一个简单的SimpleAsyncTaskExecutor,这个执行器每次执行任务都会创建新线程,在负载较高时不够高效。通常推荐进行自定义配置。(这里是指spring的默认行为,不是spring boot)

有两种常见的自定义方式:

方式一:实现AsyncConfigurer接口

java

@Configuration @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; } }
方式二:声明一个ExecutorBean

java

@Configuration @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注解中可以指定要使用的执行器名称:

java

@Async("taskExecutor") // 使用自定义线程池 public void asyncMethod() { // 异步任务逻辑 }
Spring Boot 自动配置

在 Spring Boot 中,如果上下文中没有自定义的Executor,它会自动配置一个ThreadPoolTaskExecutor,默认核心线程数为 8,可以在application.properties或application.yml中调优:

properties

spring.task.execution.pool.max-size=16 spring.task.execution.pool.queue-capacity=100 spring.task.execution.pool.keep-alive=10s
总结

配置 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 没来得及做自动配置,那么它的默认行为是:

  1. 优先查找上下文中唯一的TaskExecutorbean,或者名为"taskExecutor"的Executorbean。

  2. 如果找不到,最终会使用SimpleAsyncTaskExecutor。

这个SimpleAsyncTaskExecutor会为每个任务创建一个新线程,不进行复用,在并发高时会有性能风险。

第二层:Spring Boot 的自动配置(你实际遇到的情况)

但在 Spring Boot 项目中,情况完全不同。Spring Boot 会介入,并为你自动配置一个更合适的线程池。

根据最新的 Spring Boot 官方文档:

  • 如果项目中没有自定义的ExecutorBean,Spring Boot 会自动配置一个AsyncTaskExecutor。

  • 这个自动配置的执行器,默认就是一个ThreadPoolTaskExecutor,并且它拥有8 个核心线程。这些核心线程数可以根据负载动态调整。

结论:到底用的是哪一个?

在你的 Spring Boot 项目中,因为没有任何自定义线程池,所以最终生效的就是 Spring Boot 自动配置的那个拥有 8 个核心线程的ThreadPoolTaskExecutor。

@EnableAsync原本指向的SimpleAsyncTaskExecutor在这里被 Spring Boot 的更强大的自动配置覆盖了。你可以通过spring.task.execution开头的配置来微调这个线程池,比如:

properties

spring.task.execution.pool.max-size=16 spring.task.execution.pool.queue-capacity=100
两个默认线程池的风险对比
线程池核心行为主要风险
SimpleAsyncTaskExecutor(Spring 框架默认)来一个任务创建一个新线程,不复用。高并发下会无限制创建线程,可能导致资源耗尽甚至 OOM。
ThreadPoolTaskExecutor(Spring Boot 自动配置)使用 8 个核心线程,线程可复用,有任务队列。默认队列容量非常大(Integer.MAX_VALUE),如果任务积压,可能消耗大量内存。

所以,即使 Spring Boot 的默认线程池比SimpleAsyncTaskExecutor要好,但在生产环境中,依然强烈建议你显式地自定义一个线程池。这样可以更精确地控制核心线程数、最大线程数和队列长度,以匹配你业务的真实负载,规避潜在的内存风险。

九、最后总结

方案异步实现事务控制自注入循环依赖推荐度
方案一(一条道走到黑)自注入声明式(@Transactional)✅ 需要⚠️ 需注意⭐⭐⭐
方案二(新建 Service)新建类编程式(TransactionTemplate)❌ 不需要✅ 无⭐⭐⭐⭐⭐

最终建议:

采用方案二:新建 Service + 编程式事务。虽然多了一个类,但职责清晰、没有自注入的循环依赖风险、事务控制更灵活,是生产环境最稳妥的选择。


附录:完整项目示例代码

完整的可运行示例已包含在文中,关键类为:

  • BatchService:核心业务

  • AsyncBatchService:异步调度 + 事务

  • OrderService:调用方决策

  • AsyncConfig:线程池配置

如有疑问,欢迎交流讨论!😊

相关新闻

  • 记一次由浮点数精度引发的财务对账“惨案”
  • Koikatsu Sunshine HF Patch:终极游戏增强补丁完整指南
  • Kostka-Foulkes多项式与Chebyshev多项式的表示论桥梁

最新新闻

  • ABB工业机器人编程基础(十)搬运任务练习
  • 计算机毕业设计之基于微信小程序的代驾服务系统设计与实现
  • Java毕设项目:基于 SpringBoot 的在线出题、考试、阅卷一体化系统设计与实现 计算机基础学科线上自测与统考系统设计与实现 (源码+文档,讲解、调试运行,定制等)
  • 【6.18】混频器超通俗拆解,从零看懂!
  • 提升视野见识
  • 企业级电子屏信息发布系统:从内容管理到终端播放的完整实践

日新闻

  • Qwen2.5-Turbo百万上下文实战指南:百炼平台长文本处理全解析
  • 怎么监控对标账号更新,2026年作者监控工作流,5款深度对比
  • EdgeRemover:专业级Windows Edge浏览器管理工具,彻底解决顽固软件卸载难题

周新闻

  • Visual C++运行库修复终极指南:5分钟快速解决Windows软件启动错误
  • 手把手教你构建统计局地区经济数据爬虫:从环境搭建到数据持久化全指南
  • 2026多Agent深度解析:用AI团队替代单一模型,四种架构实战落地

月新闻

  • 【总结】入门篇:50句话让你记住架构核心概念
  • WeChatMsg技术方案解析:实现Mac微信数据自主管理的完整解决方案
  • WeChatMsg:革新性微信数据备份方案,打造你的专属数字记忆库

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号