本文从一个实际业务场景出发,记录了一次代码改造的完整思考过程:从原有同步更新逻辑,到引入异步执行,再到解决事务问题,最后总结出两种可行的方案及其背后的原理。希望对遇到类似问题的同学有所帮助。
一、原始代码:同步更新
先看最原始的代码,这是一个标准的批量更新 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.2 corePoolSize=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;
}
}
方式二:声明一个 Executor Bean
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:线程池配置
如有疑问,欢迎交流讨论!😊