面试知识库
极高 进阶

Spring事务传播行为#

一句话答案#

7 种传播行为核心三个:REQUIRED(有则加入无则新建)、REQUIRES_NEW(总是新建)、NESTED(嵌套事务/保存点)。

核心要点

7 种事务传播行为(propagation):

传播行为含义
REQUIRED(默认)有事务则加入,没有则新建(最常用)
REQUIRES_NEW无论如何都新建事务,挂起当前事务(独立事务,互不影响)
SUPPORTS有事务则加入,没有则以非事务方式执行
NOT_SUPPORTED以非事务方式执行,有事务则挂起
MANDATORY必须在事务内执行,没有事务则抛异常
NEVER不能在事务内执行,有事务则抛异常
NESTED有事务则在当前事务内创建嵌套事务(savepoint),没有则新建

实际常用场景:

// REQUIRED:默认,外层事务回滚,内层也回滚
@Transactional(propagation = Propagation.REQUIRED)

// REQUIRES_NEW:日志记录,即使主业务回滚,日志也要保存
@Transactional(propagation = Propagation.REQUIRES_NEW)

// NESTED:子事务失败只回滚到 savepoint,不影响外层
@Transactional(propagation = Propagation.NESTED)
java

事务失效的 8 种常见场景:

1. 方法不是 public 的

@Transactional
private void doSomething() { ... }  // ❌ 事务失效,只有 public 方法才能被代理拦截
java

2. 自调用(同类内方法调用)

@Service
class OrderService {
    public void placeOrder() {
        this.doCreate();  // ❌ this 调用的是原始对象,不是代理对象,事务不生效
    }
    
    @Transactional
    public void doCreate() { ... }
}
// 解决:注入自身代理,或拆分到不同类
java

3. 异常被吞掉(catch 了但没重新抛出)

@Transactional
public void doSomething() {
    try {
        // 业务逻辑
    } catch (Exception e) {
        log.error("error", e);  // ❌ 异常被捕获,Spring 不知道出了问题,不会回滚
    }
}
java

4. 抛出的异常类型不对

@Transactional  // 默认只回滚 RuntimeException 和 Error
public void doSomething() throws Exception {
    throw new Exception("checked");  // ❌ 受检异常不触发回滚
}
// 解决:@Transactional(rollbackFor = Exception.class)
java

5. 多线程调用(事务只在当前线程有效)

@Transactional
public void doSomething() {
    new Thread(() -> updateDB()).start();  // ❌ 新线程有新的数据库连接,不在同一事务中
}
java

6. 数据库或表不支持事务

  • MyISAM 引擎不支持事务,只有 InnoDB 支持

7. Bean 没有被 Spring 管理

class OrderService {  // ❌ 没有 @Service,不是 Spring Bean,AOP 代理不会生成
    @Transactional
    public void doSomething() { ... }
}
java

8. 传播行为配置错误

@Transactional(propagation = Propagation.NOT_SUPPORTED)
// 明确配置了以非事务方式执行,事务失效
java

深挖:事务在底层到底怎么生效的#

调用链(一条注解背后的真实路径):

代理对象调用方法
  → TransactionInterceptor(事务拦截器,本质是个 MethodInterceptor)
  → PlatformTransactionManager.getTransaction()
  → DataSourceTransactionManager
  → 拿到 Connection,setAutoCommit(false)
  → TransactionSynchronizationManager 把 Connection 绑到 ThreadLocal
  → 执行业务方法(期间 MyBatis/JDBC 都从 ThreadLocal 拿这条同一连接)
  → 正常 commit() / 异常 rollback() → 解绑、还连接
plaintext

两个关键动作:

  • 关 autocommitDataSourceTransactionManager 取出连接后调 setAutoCommit(false),从此这条连接上的所有 SQL 不再各自提交,攒成一个事务。
  • 连接绑 ThreadLocalTransactionSynchronizationManagerThreadLocal<Map<DataSource, ConnectionHolder>> 把连接挂到当前线程。这样同线程内任何地方(包括 MyBatis 的 SqlSessionUtils)取连接,拿到的都是同一条,SQL 才会落在同一事务里。

这正是两大失效场景的根因:

  • 自调用失效this.method() 不经过代理对象 → TransactionInterceptor 根本没被触发 → 连开事务的入口都没进,自然无事务。
  • 多线程失效:连接绑在发起线程的 ThreadLocal 上;new Thread() 是新线程,取不到那条连接,会自己新开一条连接(autocommit=true),与外层事务完全无关,外层回滚也带不动它。

REQUIRES_NEW 的「挂起」到底挂了什么?

当传播行为为 REQUIRES_NEWAbstractPlatformTransactionManager 会调 suspend():把当前线程 ThreadLocal 里绑的连接资源(ConnectionHolder 等)解绑并保存到一个 SuspendedResourcesHolder 局部变量里,然后新建一条连接、setAutoCommit(false)、绑到 ThreadLocal——这就是「新事务」。新事务 commit/rollback 完成后调 resume(),把之前保存的资源重新绑回 ThreadLocal,外层事务继续在它原来的连接上跑。所以内外是两条物理连接、两个事务,互不影响——这是 REQUIRES_NEW「独立提交」的物理实现。

面试回答(2分钟版)

Spring事务传播行为定义了多个事务方法嵌套调用时事务如何传递,一共7种,实际开发中核心掌握3种就够了。REQUIRED是默认行为,有事务就加入,没有就新建,适用于绑定在同一事务中的业务操作。REQUIRES_NEW无论外层有没有事务都会新建一个独立事务并挂起当前事务,典型场景是操作日志记录——即使主业务回滚,日志也必须保存。NESTED在当前事务内部通过savepoint创建嵌套事务,子事务失败只回滚到保存点不影响外层。实际项目中事务失效比传播行为本身更容易踩坑,常见8种失效场景包括:方法非public导致代理无法拦截、同类内this调用绕过代理对象、catch吞掉异常导致Spring感知不到错误、抛出checked异常但未配置rollbackFor=Exception.class、多线程场景下新线程使用独立连接不在同一事务中、MyISAM引擎不支持事务、Bean未被Spring管理以及传播行为配置错误。项目中我们通过统一配置rollbackFor=Exception.class和代码审查规避了大部分失效问题。

追问与易错

追问方向:

  • “REQUIRED 和 REQUIRES_NEW 区别?”→ REQUIRED 有事务就加入(共享同一事务,一起提交/回滚);REQUIRES_NEW 总是新建独立事务并挂起外层事务,内外事务互不影响
  • “NESTED 和 REQUIRES_NEW 区别?”→ NESTED 在外层事务内创建嵌套事务(基于 savepoint),子事务回滚只到保存点不影响外层,但外层回滚会连带子事务;REQUIRES_NEW 是完全独立的新事务
  • “什么场景用 REQUIRES_NEW?”→ 操作日志/审计记录(主业务回滚但日志必须保存)、发送通知/消息(不受主事务影响)、独立计数/统计等需要独立提交不受外层事务回滚影响的场景

易错点:

  • ❌ 事务传播就是事务嵌套——只有 NESTED 是嵌套
  • ❌ REQUIRES_NEW 子事务回滚影响外层——不会