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

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


一、原始代码:同步更新

先看最原始的代码,这是一个标准的批量更新 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=0new 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.propertiesapplication.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. 优先查找上下文中唯一的 TaskExecutor bean,或者名为 "taskExecutor"Executor bean。

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

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

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

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

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

  • 如果项目中没有自定义的 Executor Bean ,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:线程池配置

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