什么是事务传播机制?

简单来说,事务传播机制定义了多个事务方法在相互调用时,事务应该如何传播

比如,你有一个方法 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 有事务,methodBREQUIRED

    • 结果:methodB 加入 methodA 的事务。两者属于同一个事务。任何一个方法抛出异常,整个事务都会回滚。
  • methodA 无事务,methodBREQUIRED

    • 结果:methodB 自己创建一个新事务。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
@Transactional(propagation = Propagation.REQUIRED) // 默认就是REQUIRED,可省略
public void methodA() {
// 一些数据库操作
serviceB.methodB(); // methodB 也是 REQUIRED
// 更多数据库操作
// 如果这里抛出异常,methodB的操作也会回滚
}

@Transactional(propagation = Propagation.REQUIRED)
public void methodB() {
// 数据库操作
}

场景 2: REQUIRES_NEW (常用于“记录日志”等独立操作)

  • methodA 有事务,methodBREQUIRES_NEW
    • 结果:Spring 会挂起(Suspend) methodA 的事务。然后为 methodB 创建一个全新的、独立的事务
    • methodB 的事务提交或回滚,不会影响 methodA 的事务。
    • methodA 的事务回滚,也不会影响 methodB 已提交的事务。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Transactional
public void placeOrder(methodA) { // 下单主业务
try {
orderDao.insert(order); // 订单表操作,属于methodA的事务

// 记录日志,希望即使下单失败,日志也要记录下来
logService.insertLog(methodB); // REQUIRES_NEW

} catch (Exception e) {
// 即使这里导致methodA的事务回滚,订单插入被撤销,
// insertLog方法因为有自己的新事务,它的记录依然会成功提交。
}
}

@Service
public class LogService {
@Transactional(propagation = Propagation.REQUIRES_NEW) // 总是开新事务
public void insertLog() {
logDao.insert(log); // 日志表操作
}
}

场景 3: NESTED (嵌套事务)

  • methodA 有事务,methodBNESTED
    • 结果:methodB 在一个嵌套事务(保存点)中执行。
    • 如果 methodB 执行完成,事务状态会回到保存点。此时如果 methodA 在后续操作中失败回滚,methodB 的操作也会被回滚。
    • 如果 methodB 内部抛出了异常,并且被 methodA 捕获并处理了,你可以选择只回滚嵌套事务(即 methodB 的操作),而 methodA 的事务可以继续。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Transactional
public void methodA() {
userDao.updateUser(); // 操作1

try {
serviceB.methodB(); // NESTED
} catch (Exception e) {
// 只回滚methodB的操作,不影响其他操作
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); // 手动标记回滚
// 但更常见的做法是让异常抛出,让外部事务整体回滚
}

orderDao.updateOrder(); // 操作2
// 如果这里失败,操作1、操作2以及methodB的操作都会回滚
}

@Transactional(propagation = Propagation.NESTED)
public void methodB() {
// ... 数据库操作
// 如果这里抛出异常,且methodA不捕获,则整个事务回滚。
// 如果methodA捕获了,可以决定只回滚这个嵌套部分。
}

REQUIRES_NEW vs NESTED

  • REQUIRES_NEW 是完全独立的两个事务,互不影响。
  • NESTED 是寄生在外部事务上的“子事务”,它的命运最终由外部事务决定(外部提交它才真提交,外部回滚它必回滚),但它自己可以提前“局部回滚”。

场景 4: 其他行为简要说明

  • SUPPORTS: 常用于查询方法。如果调用方有事务,我就跟着事务走,保证查询的一致性;如果调用方没事务,我也没关系,直接读。
  • MANDATORY: 强制要求必须在事务中调用我,否则就报错。用于严格规定某些方法不能单独被调用。
  • NOT_SUPPORTED: 强制以非事务方式运行,会挂起当前事务。常用于不需要事务支持的只读操作,或者需要避免事务影响的方法。
  • NEVER: 我坚决不能在事务里被调用,如果你带事务来调用我,我就报错。是 MANDATORY 的反向操作。

一个重要陷阱:自调用(Self-Invocation)

Spring 的事务管理是基于 AOP(动态代理) 实现的。这意味着只有通过代理对象调用方法时,事务注解才会生效

如果你在同一个类中,一个非事务方法 method1 调用一个事务方法 method2 (@Transactional),事务是不会生效的!

错误示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service
public class MyService {

public void method1() {
// ...
this.method2(); // 自调用!事务失效!
// ...
}

@Transactional
public void method2() {
// 数据库操作,这里不会有事务!
}
}

原因: this.method2() 是目标对象内部的调用,绕过了生成的代理对象,因此拦截器(负责开启/提交事务的代码)没有机会执行。

解决方案:

  1. 将方法放到另一个 Service 中(推荐)。
  2. 通过 ApplicationContext 获取自身的代理对象来调用(不优雅):
    1
    ((MyService) AopContext.currentProxy()).method2();
    (需要先开启 @EnableAspectJAutoProxy(exposeProxy = true)

5. 总结与如何选择

传播行为 选择场景
REQUIRED 默认选择。适用于绝大多数增删改操作。
SUPPORTS 适用于查询方法,可融入现有事务,也可无事务运行。
REQUIRES_NEW 需要独立事务的操作,如日志记录、审计消息发送等,这些操作的成功不应影响主业务。
NESTED 复杂业务中,存在可选的子操作。如果子操作失败,主操作可能仍要继续;但如果主操作失败,子操作必须回滚。不常用,且依赖数据库支持。
MANDATORY 用于强制规定代码设计,确保某些方法必须被事务包裹。
NOT_SUPPORTED/NEVER 需要明确排除事务影响的情况,例如执行一些特殊的非 SQL 操作。

核心建议: 除非有明确需求,否则一直使用默认的 REQUIRED。需要绝对独立时用 REQUIRES_NEW,需要复杂部分回滚控制时考虑 NESTED(并确认数据库支持)。理解这些行为的关键在于多思考“这个子方法是否应该和主方法同生共死”。