Skip to content

Spring 事务传播机制(REQUIRED/REQUIRES_NEW/NESTED 等 7 种)

提出问题

事务传播机制是 Spring 面试中区分"会用"和"懂原理"的分水岭。很多开发者在单体 Service 里写 @Transactional(propagation=Propagation.REQUIRES_NEW) 时,遇到外层事务不回滚、内层事务不回滚、连接池爆满等诡异问题,往往只能靠"改配置试一下"来碰运气。面试官问这个问题,本质是想确认:你知不知道事务边界是怎么划分的?每个传播行为在底层是怎么操作数据库连接的?为什么 REQUIRES_NEW 要挂起当前事务而不是"新建一个子事务"?这些问题的答案,直接决定了你在生产环境里能不能写出正确的事务代码。

分析问题

7 种传播行为概览

Spring 通过 @Transactional(propagation=Propagation.XXX) 控制事务边界。7 种行为按是否依赖当前事务分为三组:

依赖当前事务的

  • REQUIRED(默认):支持当前事务,不存在则新建
  • SUPPORTS:支持当前事务,不存在则不开启
  • MANDATORY:必须存在当前事务,否则抛异常

独立于当前事务的

  • REQUIRES_NEW:总是新建事务,挂起当前事务
  • NOT_SUPPORTED:以非事务方式运行,挂起当前事务
  • NEVER:以非事务方式运行,存在事务则抛异常

嵌套事务的

  • NESTED:嵌套事务,在 Savepoint 基础上运行

实际项目中 90% 的场景只用得上 REQUIRED、REQUIRES_NEW 和 NESTED,但面试时会要求你全答出来。

7 种传播行为对比表

传播行为有无当前事务行为底层机制连接数典型场景
REQUIRED新建直接获取 conn1默认,90% 场景
REQUIRED加入复用 conn0同上
SUPPORTS非事务执行无事务0查询只读操作
SUPPORTS加入复用 conn0同上
MANDATORY抛 IllegalTransactionStateException0强制事务上下文
MANDATORY加入复用 conn0审计日志
REQUIRES_NEW新建获取新 conn1独立记录
REQUIRES_NEW挂起 + 新建doSuspend + 新 conn+1内层失败不影响外层
NOT_SUPPORTED非事务无事务0大文件写入
NOT_SUPPORTED挂起doSuspend,释放 conn-1避免长事务锁
NEVER非事务无事务0测试方法
NEVER抛异常0确保无事务执行
NESTED新建获取 conn1JDBC only
NESTED设置 Savepointconn.setSavepoint()0批量子操作回滚

REQUIRED vs REQUIRES_NEW 的核心差异

这是最高频的考点。看一个典型场景:

java
@Service
public class OrderService {
    @Autowired
    private PaymentService paymentService;

    @Transactional
    public void createOrder() {
        // 步骤 1:插入订单
        orderDao.insert(order);
        try {
            paymentService.deduct(); // 内层调用
        } catch (Exception e) {
            log.error("扣款失败,订单回滚");
        }
        // 步骤 3:...
    }
}

@Service
public class PaymentService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void deduct() {
        paymentDao.insert(payment);
        if (someCondition) {
            throw new RuntimeException("扣款失败");
        }
    }
}

REQUIRED 的行为:内层方法和外层方法共用同一个事务。内层抛异常 → 事务标记为 rollback-only → 外层即使 catch 了异常,最终提交时也会抛 UnexpectedRollbackException,整个事务回滚。外层 catch 并没有用

REQUIRES_NEW 的行为:内层方法挂起外层事务,新建一个独立的事务。内层抛异常 → 内层事务回滚 → 外层事务恢复并继续执行。外层 catch 了异常,外层事务可以正常提交。内外事务互不影响

底层源码差异:REQUIRED 通过 TransactionSynchronizationManager 的线程绑定资源实现——同一个线程共用一个数据库连接,内层方法直接复用 conn。REQUIRES_NEW 会调用 doSuspend() 断开当前 conn 与线程的绑定,再从 DataSource 获取一个新的连接,内层结束后调用 doResume() 恢复。

底层调用链(以 AbstractPlatformTransactionManager.handleExistingTransaction() 为例):

REQUIRED:   handleExistingTransaction() → 直接返回,复用已有 TransactionStatus
REQUIRES_NEW: handleExistingTransaction() → doSuspend(transaction) → startTransaction() → 新 conn → 新 TransactionStatus
NESTED:     handleExistingTransaction() → 判断事务管理器是否支持 Savepoint → createSavepoint()

NESTED 和 REQUIRES_NEW 有什么区别

NESTED 依赖 JDBC 3.0 的 Savepoint 机制(conn.setSavepoint()),内层方法回滚时只回滚到 Savepoint,外层事务不受影响。但有个关键区别:

java
@Transactional(propagation = Propagation.NESTED)
public void nestedMethod() {
    // 内层回滚只回滚到 Savepoint
}
  • NESTED 回滚:只回滚到 Savepoint,外层事务可以继续提交
  • NESTED 外层回滚:内层随外层一起回滚
  • REQUIRES_NEW 回滚:内外完全独立,互不感知

NESTED 只适用于 DataSourceTransactionManager(JDBC 事务管理器),JpaTransactionManagerHibernateTransactionManager 不支持。

加一个面试会追问的细节点:NESTED 在内层提交时,不会立即提交到数据库,而是等外层事务一起提交。因为 Savepoint 只是在同一个数据库连接上打的标记点,不涉及独立的事务提交。这意味着 NESTED 内层的结果在内存中对外层可见,但对其他事务不可见,直到外层事务提交。

REQUIRES_NEW 导致连接池耗尽问题

这是生产环境中最容易踩的坑:

java
@Transactional
public void batchProcess(List<Item> items) {
    for (Item item : items) {
        innerProcess(item); // REQUIRES_NEW
    }
}

假设 HikariCP 连接池最大连接数为 20,batchProcess 持有 1 个连接,内层每次 REQUIRES_NEW 从连接池拿一个新连接。如果 items 有 30 条,前 19 次内层调用正常,第 20 次时连接池已无可用连接,抛出 HikariPool-1 - Connection is not available, request timed out after 30000ms

更精确的数字:HikariCP 默认 maximumPoolSize=10,每轮 REQUIRES_NEW 需要 1 个新连接,外层事务占 1 个 + 内层占 1 个 = 2 个连接同时被占用。当内层执行完、释放连接后,外层事务还没结束,所以外层那个连接一直没释放。内层第 9 次调用时,连接池已分配 10 个连接(1 外层 + 9 内层),第 10 次 connectionTimeout 默认 30s 超时,应用直接报 Connection is not available

真实生产案例:某支付系统在结算对账时,循环调用 REQUIRES_NEW 的 logAudit() 方法,线上 10 个节点中的 4 个节点同时报连接池超时,导致大量请求被阻塞在 HikariPool.createTimeout 上,最终引发雪崩。排查后发现外层事务 @Transactional 在 Service 层,循环 50 次对账,连接池瞬间被打满。

解决方案:

  1. 把内层方法改为异步执行并限制并发数(@Async + ThreadPoolTaskExecutor 限制 corePoolSize)
  2. 拆成小批次逐批提交,外层不开启事务,内层各自独立事务
  3. TransactionTemplate 手动控制事务边界,不用 @Transactional 注解
java
// 方案 2 示例:外层不开启事务
public void batchProcess(List<Item> items) {
    // 外层没有 @Transactional
    for (Item item : items) {
        innerProcess(item); // REQUIRES_NEW,每次独立事务
    }
}
// 此时 REQUIRES_NEW 只需要 1 个连接,不会打满

事务传播中的自调用失效问题

在面试中,还有一个常见的坑:同一个类中 A 方法调用 B 方法,B 上的 @Transactional(propagation=REQUIRES_NEW) 不会生效

java
@Service
public class OrderService {
    @Transactional
    public void createOrder() {
        // 直接调用内部方法
        this.deduct(); // ❌ 不会走代理,事务传播不生效
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void deduct() {
        // 实际会使用 createOrder 的事务,而不是新建事务
    }
}

原理:Spring AOP 代理在 this.deduct() 调用时绕过了代理对象,TransactionInterceptor 不会被触发。解决方法:

  • 注入自身代理:@Autowired OrderService self,用 self.deduct() 调用
  • AopContext.currentProxy() 获取当前代理
  • 把 deduct 方法移到另一个 Service 中

面试高频追问

Q1:REQUIRES_NEW 挂起事务时,当前事务的数据库连接怎么处理?

A:doSuspend() 会从 TransactionSynchronizationManager 中移除当前事务的资源和同步器,但不会关闭连接。连接仍然存活,处于"休眠"状态,等待 doResume() 恢复时重新绑定到线程。这意味着挂起期间,这个连接在连接池中处于"被占用但空闲"的状态,不会归还给连接池

Q2:REQUIRED 内层方法抛异常,外层 catch 了为什么还是回滚了?

A:PlatformTransactionManager 在 commit 之前会检查 TransactionStatus.isRollbackOnly()。内层方法抛出异常后,Spring 事务拦截器会把当前事务标记为 rollbackOnly(通过 TransactionAspectSupportcompleteTransactionAfterThrowing)。外层即使 catch 住异常,commit 时发现 rollbackOnly=true,触发 UnexpectedRollbackException,最终执行 rollback()

Q3:NESTED 和 REQUIRES_NEW 的隔离级别谁更高?

A:REQUIRES_NEW 隔离级别更高:内层事务可以看到外层事务未提交的数据(因为共用一个连接?错!——REQUIRES_NEW 用的是新连接,所以看不到外层未提交的数据,隔离级别取决于数据库的默认隔离级别)。NESTED 共用一个连接,内层可以看到外层未提交的数据。这是面试中容易混淆的点。

总结

关键点清单:

  • REQUIRED:内外合并为同一事务,内层异常导致外层 rollback-only,外层 catch 无法阻止回滚
  • REQUIRES_NEW:挂起当前事务,新建独立事务,内外完全隔离,但会额外占用连接
  • NESTED:基于 Savepoint 的嵌套事务,JDBC only,内层回滚不影响外层
  • MANDATORY:没有当前事务直接抛异常,适合"必须在一个事务中执行"的方法
  • 循环中调用 REQUIRES_NEW 容易导致连接池耗尽,先用批量思维审视设计
  • 自调用导致传播失效,必须通过代理对象调用
  • 挂起事务期间连接不会被归还给连接池,属于"被占用但空闲"状态

面试话术示例:"如果业务要求内层失败不影响外层,我会首选 REQUIRES_NEW,但要注意连接池水位。如果只是想内层回滚但外层不想感知,且只用 JDBC,NESTED 更轻量。生产上我常用的是把事务边界拆开——让内层方法独立成一个 Service 调用,通过异步消息补偿来保证最终一致性,而不是靠事务传播机制硬撑。"

参考:Spring 源码 — AbstractPlatformTransactionManager.handleExistingTransaction();《Spring 事务管理:传播机制深度解析》

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。