本文从一个实际业务场景出发,记录了一次代码改造的完整思考过程:从原有同步更新逻辑,到引入异步执行,再到解决事务问题,最后总结出两种可行的方案及其背后的原理。希望对遇到类似问题的同学有所帮助。
一、原始代码:同步更新
先看最原始的代码,这是一个标准的批量更新 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 | ✅ 需要 | ⭐⭐⭐ |
| 编程式事务 | 新建 Service | TransactionTemplate | ❌ 不需要 | ⭐⭐⭐⭐⭐ |
核心原则:
如果第一步选择了"新建 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 没来得及做自动配置,那么它的默认行为是:
优先查找上下文中唯一的
TaskExecutorbean,或者名为"taskExecutor"的Executorbean。如果找不到,最终会使用
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:线程池配置
如有疑问,欢迎交流讨论!😊