Spring事务传播机制
什么是事务传播机制?
简单来说,事务传播机制定义了多个事务方法在相互调用时,事务应该如何传播。
比如,你有一个方法 methodA()
,它的事务属性是“开启一个新事务”。在 methodA()
中,它调用了 methodB()
。那么问题来了:
methodB()
是应该在methodA()
已开启的事务中运行呢?- 还是应该无视
methodA()
的事务,自己单独开启一个新事务? - 如果
methodA()
没有事务,methodB()
又该如何行为?
事务传播机制就是用来回答这些问题的一套规则。它规定了事务的边界如何在方法调用间传递。
核心:7种传播行为(Propagation Behavior)
Spring 定义了 7 种传播行为,全部封装在 Propagation
枚举中。它们是理解整个机制的关键。
传播行为类型 | 说明 |
---|---|
Propagation.REQUIRED |
(默认)【支持当前事务,如果不存在则创建一个新的事务】 - 如果当前存在事务,则加入该事务。 - 如果当前没有事务,则创建一个新事务。 |
Propagation.SUPPORTS |
【支持当前事务,如果不存在则以非事务方式执行】 - 如果当前存在事务,则加入该事务。 - 如果当前没有事务,则以非事务方式继续运行。 |
Propagation.MANDATORY |
【强制要求存在当前事务,否则抛出异常】 - 如果当前存在事务,则加入该事务。 - 如果当前没有事务,则抛出 IllegalTransactionStateException 异常。 |
Propagation.REQUIRES_NEW |
【创建一个新事务,如果当前存在事务则将其挂起】 - 无论如何都会创建一个新事务。 - 如果当前存在事务,则将当前事务挂起(Suspend),直到新事务结束。两个事务相互独立。 |
Propagation.NOT_SUPPORTED |
【以非事务方式运行,如果当前存在事务则将其挂起】 - 总是以非事务方式执行。 - 如果当前存在事务,则挂起当前事务,方法执行完毕后恢复。 |
Propagation.NEVER |
【以非事务方式运行,如果当前存在事务则抛出异常】 - 只能以非事务方式执行。 - 如果当前存在事务,则抛出 IllegalTransactionStateException 异常。 |
Propagation.NESTED |
【如果当前存在事务,则在嵌套事务内执行;否则行为同REQUIRED】 - 当前有事务时,会创建一个嵌套事务(一个保存点,Savepoint)。 - 嵌套事务是外部事务的一部分,只有外部事务提交了,它才会真正提交。 - 嵌套事务可以独立地回滚(回滚到保存点),而不影响外部事务。 - 如果外部事务回滚,嵌套事务一定会回滚。 - 关键点: 这种机制需要 JDBC 3.0+ 的支持,并且需要底层数据库支持保存点(如 MySQL、PostgreSQL)。 |
详细场景解析与举例
假设我们有两个方法:ServiceA.methodA()
和 ServiceB.methodB()
。我们来分析 methodA
调用 methodB
时,不同传播行为的组合会产生什么效果。
场景 1: REQUIRED
(最常用)
methodA
有事务,methodB
是REQUIRED
- 结果:
methodB
加入methodA
的事务。两者属于同一个事务。任何一个方法抛出异常,整个事务都会回滚。
- 结果:
methodA
无事务,methodB
是REQUIRED
- 结果:
methodB
自己创建一个新事务。
- 结果:
代码示例:
1 | // 默认就是REQUIRED,可省略 |
场景 2: REQUIRES_NEW
(常用于“记录日志”等独立操作)
methodA
有事务,methodB
是REQUIRES_NEW
- 结果:Spring 会挂起(Suspend)
methodA
的事务。然后为methodB
创建一个全新的、独立的事务。 methodB
的事务提交或回滚,不会影响methodA
的事务。methodA
的事务回滚,也不会影响methodB
已提交的事务。
- 结果:Spring 会挂起(Suspend)
代码示例:
1 |
|
场景 3: NESTED
(嵌套事务)
methodA
有事务,methodB
是NESTED
- 结果:
methodB
在一个嵌套事务(保存点)中执行。 - 如果
methodB
执行完成,事务状态会回到保存点。此时如果methodA
在后续操作中失败回滚,methodB
的操作也会被回滚。 - 如果
methodB
内部抛出了异常,并且被methodA
捕获并处理了,你可以选择只回滚嵌套事务(即methodB
的操作),而methodA
的事务可以继续。
- 结果:
代码示例:
1 |
|
REQUIRES_NEW
vs NESTED
:
REQUIRES_NEW
是完全独立的两个事务,互不影响。NESTED
是寄生在外部事务上的“子事务”,它的命运最终由外部事务决定(外部提交它才真提交,外部回滚它必回滚),但它自己可以提前“局部回滚”。
场景 4: 其他行为简要说明
SUPPORTS
: 常用于查询方法。如果调用方有事务,我就跟着事务走,保证查询的一致性;如果调用方没事务,我也没关系,直接读。MANDATORY
: 强制要求必须在事务中调用我,否则就报错。用于严格规定某些方法不能单独被调用。NOT_SUPPORTED
: 强制以非事务方式运行,会挂起当前事务。常用于不需要事务支持的只读操作,或者需要避免事务影响的方法。NEVER
: 我坚决不能在事务里被调用,如果你带事务来调用我,我就报错。是MANDATORY
的反向操作。
一个重要陷阱:自调用(Self-Invocation)
Spring 的事务管理是基于 AOP(动态代理) 实现的。这意味着只有通过代理对象调用方法时,事务注解才会生效。
如果你在同一个类中,一个非事务方法 method1
调用一个事务方法 method2
(@Transactional
),事务是不会生效的!
错误示例:
1 |
|
原因: this.method2()
是目标对象内部的调用,绕过了生成的代理对象,因此拦截器(负责开启/提交事务的代码)没有机会执行。
解决方案:
- 将方法放到另一个 Service 中(推荐)。
- 通过 ApplicationContext 获取自身的代理对象来调用(不优雅):(需要先开启
1
((MyService) AopContext.currentProxy()).method2();
@EnableAspectJAutoProxy(exposeProxy = true)
)
5. 总结与如何选择
传播行为 | 选择场景 |
---|---|
REQUIRED |
默认选择。适用于绝大多数增删改操作。 |
SUPPORTS |
适用于查询方法,可融入现有事务,也可无事务运行。 |
REQUIRES_NEW |
需要独立事务的操作,如日志记录、审计消息发送等,这些操作的成功不应影响主业务。 |
NESTED |
复杂业务中,存在可选的子操作。如果子操作失败,主操作可能仍要继续;但如果主操作失败,子操作必须回滚。不常用,且依赖数据库支持。 |
MANDATORY |
用于强制规定代码设计,确保某些方法必须被事务包裹。 |
NOT_SUPPORTED /NEVER |
需要明确排除事务影响的情况,例如执行一些特殊的非 SQL 操作。 |
核心建议: 除非有明确需求,否则一直使用默认的 REQUIRED
。需要绝对独立时用 REQUIRES_NEW
,需要复杂部分回滚控制时考虑 NESTED
(并确认数据库支持)。理解这些行为的关键在于多思考“这个子方法是否应该和主方法同生共死”。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 技术之路!
评论