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三个核心传播行为的回滚边界:
| 场景 | REQUIRED | REQUIRES_NEW | NESTED |
|---|---|---|---|
| 内外是否同一物理事务 | 是 | 否(两条连接、两个事务) | 是(同一连接,内层是 savepoint) |
| 内层抛异常、外层 catch 住 | 整个事务被标记 rollback-only,外层提交时抛 UnexpectedRollbackException 并回滚 | 只回滚内层,外层可正常提交 | 回滚到 savepoint,外层可正常提交 |
| 内层抛异常、外层没 catch | 一起回滚 | 内层回滚;异常继续上抛,外层也回滚 | 一起回滚 |
| 内层成功、外层后来回滚 | 一起回滚 | 内层已提交,不受影响 | 一起回滚(savepoint 随外层回滚) |
使用注意:
- 传播行为只在经过代理的调用之间生效;同类
this.xxx()自调用不走代理,配什么传播行为都没用(失效场景详见 Spring事务失效场景)。 NESTED依赖 JDBC savepoint,DataSourceTransactionManager支持;JtaTransactionManager等不一定支持,JPA 下使用受限。REQUIRES_NEW会同时占用两条连接,外层持有连接时内层再取一条,连接池小、并发高时可能把连接池耗尽甚至互相等待。
深挖:事务在底层到底怎么生效的#
调用链(一条注解背后的真实路径):
代理对象调用方法
→ TransactionInterceptor(事务拦截器,本质是个 MethodInterceptor)
→ PlatformTransactionManager.getTransaction()
→ DataSourceTransactionManager
→ 拿到 Connection,setAutoCommit(false)
→ TransactionSynchronizationManager 把 Connection 绑到 ThreadLocal
→ 执行业务方法(期间 MyBatis/JDBC 都从 ThreadLocal 拿这条同一连接)
→ 正常 commit() / 异常 rollback() → 解绑、还连接plaintext两个关键动作:
- 关 autocommit:
DataSourceTransactionManager取出连接后调setAutoCommit(false),从此这条连接上的所有 SQL 不再各自提交,攒成一个事务。 - 连接绑 ThreadLocal:
TransactionSynchronizationManager用ThreadLocal<Map<DataSource, ConnectionHolder>>把连接挂到当前线程。这样同线程内任何地方(包括 MyBatis 的SqlSessionUtils)取连接,拿到的都是同一条,SQL 才会落在同一事务里。
这正是两大失效场景的根因:
- 自调用失效:
this.method()不经过代理对象 →TransactionInterceptor根本没被触发 → 连开事务的入口都没进,自然无事务。 - 多线程失效:连接绑在发起线程的 ThreadLocal 上;
new Thread()是新线程,取不到那条连接,会自己新开一条连接(autocommit=true),与外层事务完全无关,外层回滚也带不动它。
REQUIRES_NEW 的「挂起」到底挂了什么?
当传播行为为
REQUIRES_NEW,AbstractPlatformTransactionManager会调suspend():把当前线程 ThreadLocal 里绑的连接资源(ConnectionHolder等)解绑并封装成SuspendedResourcesHolder,挂在新事务的TransactionStatus上,然后新建一条连接、setAutoCommit(false)、绑到 ThreadLocal——这就是「新事务」。新事务 commit/rollback 完成后调resume(),把之前保存的资源重新绑回 ThreadLocal,外层事务继续在它原来的连接上跑。所以内外是两条物理连接、两个事务,互不影响——这是 REQUIRES_NEW「独立提交」的物理实现。
面试回答(2分钟版)
Spring事务传播行为定义了多个事务方法嵌套调用时事务如何传递,一共7种,实际开发中核心掌握3种就够了。REQUIRED是默认行为,有事务就加入,没有就新建,适用于绑定在同一事务中的业务操作。REQUIRES_NEW无论外层有没有事务都会新建一个独立事务并挂起当前事务,典型场景是操作日志记录——即使主业务回滚,日志也必须保存。NESTED在当前事务内部通过savepoint创建嵌套事务,子事务失败只回滚到保存点不影响外层。底层上,REQUIRED是内外共用同一条绑定在ThreadLocal上的连接;REQUIRES_NEW会先suspend把外层连接从ThreadLocal解绑保存,再取一条新连接开新事务,完成后resume绑回去;NESTED则是在同一条连接上打savepoint。有个常见坑:REQUIRED下内层方法抛异常、外层catch住继续执行,因为共用一个事务,内层已经把事务标记为rollback-only,外层提交时会抛UnexpectedRollbackException。另外传播行为只在经过代理的调用之间生效,同类自调用配什么都没用。
追问与易错
追问方向:
- “REQUIRED 和 REQUIRES_NEW 区别?”→ REQUIRED 有事务就加入(共享同一事务,一起提交/回滚);REQUIRES_NEW 总是新建独立事务并挂起外层事务,内外事务互不影响
- “NESTED 和 REQUIRES_NEW 区别?”→ NESTED 在外层事务内创建嵌套事务(基于 savepoint),子事务回滚只到保存点不影响外层,但外层回滚会连带子事务;REQUIRES_NEW 是完全独立的新事务
- “什么场景用 REQUIRES_NEW?”→ 操作日志/审计记录(主业务回滚但日志必须保存)、发送通知/消息(不受主事务影响)、独立计数/统计等需要独立提交不受外层事务回滚影响的场景
易错点:
- ❌ 事务传播就是事务嵌套——只有 NESTED 是嵌套
- ❌ REQUIRES_NEW 子事务回滚影响外层——事务本身不会;但如果内层异常没被外层 catch,异常上抛同样会让外层回滚
- ❌ REQUIRED 内层异常被外层 catch 住就能正常提交——不能,事务已被标记 rollback-only,提交时抛 UnexpectedRollbackException