在 Spring Boot 开发中,事务是一个至关重要的概念,尤其是在涉及多层业务逻辑或者多个数据库操作时。Spring 提供了强大的事务管理功能,使得开发者可以方便地控制事务的行为。事务传播机制作为 Spring 事务管理的一部分,是 Spring 事务管理中一个非常重要的概念。
本文将介绍 Spring Boot 中事务传播机制的原理及其常用配置,以帮助开发者更好地理解事务传播的工作方式。
一、什么是事务传播机制?
事务传播机制定义了在多个方法中调用事务时,事务的行为是如何传播的。换句话说,它决定了一个事务方法在被另一个方法调用时应该如何处理事务的开启、提交、回滚等操作。
事务传播机制通过 @Transactional 注解的 propagation 属性来配置,它有多个传播行为,开发者可以根据具体的需求来选择合适的传播方式。常见的传播行为包括:
- REQUIRED
- REQUIRES_NEW
- SUPPORTS
- MANDATORY
- NOT_SUPPORTED
- NEVER
- NESTED
二、Spring 事务传播机制的传播行为
1. REQUIRED(默认传播行为)
传播行为: 如果当前没有事务,则创建一个新的事务;如果当前已经存在事务,则加入到现有事务中。
应用场景: 这是最常用的传播行为,通常在业务方法调用中使用,确保调用方法的一致性。
1
2
3
4
5
6
7
8
9
|
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
// 业务逻辑
}
@Transactional(propagation = Propagation.REQUIRED)
public void methodB() {
// 业务逻辑
}
|
- 如果 methodA() 没有事务,它会新建一个事务;
- 如果 methodA() 已经在一个事务中,那么 methodB() 会加入到这个事务中。
2. REQUIRES_NEW
传播行为: 总是新建一个事务。如果当前有事务存在,则将当前事务挂起,等新事务提交或回滚后再恢复当前事务。
应用场景: 当我们希望某个方法独立于当前事务进行处理,通常用于一些不希望受到外部事务影响的操作,例如日志记录、通知等。
1
2
3
4
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodC() {
// 业务逻辑
}
|
- 无论 methodC() 调用时是否有事务,它都会开启一个新的事务;
- 如果 methodC() 调用时已经有事务存在,它会将当前事务挂起,开启一个新的事务;
- 当 methodC() 的事务结束后,原本挂起的事务会恢复继续执行。
3. SUPPORTS
传播行为: 如果当前有事务,则加入到现有事务中;如果当前没有事务,则以非事务方式执行。
应用场景: 当方法支持事务,但不强制要求事务存在时,可以使用 SUPPORTS。例如,一些方法可能不需要事务,但如果存在事务,它们会加入其中。
1
2
3
4
|
@Transactional(propagation = Propagation.SUPPORTS)
public void methodD() {
// 业务逻辑
}
|
- 如果 methodD() 调用时已有事务,它将加入该事务;
- 如果没有事务,methodD() 以非事务方式执行。
4. MANDATORY
传播行为: 如果当前有事务,则加入到现有事务中;如果没有事务,则抛出异常。
应用场景: 如果方法依赖事务执行,但又不希望自行创建事务,则可以使用 MANDATORY。如果没有现有事务,将抛出 TransactionRequiredException 异常。
1
2
3
4
|
@Transactional(propagation = Propagation.MANDATORY)
public void methodE() {
// 业务逻辑
}
|
- 如果当前没有事务,methodE() 会抛出异常;
- 如果当前有事务,methodE() 会加入到该事务中。
5. NOT_SUPPORTED
传播行为: 如果当前有事务,则将当前事务挂起,并以非事务方式执行方法。
应用场景: 当某个方法不希望参与事务操作时,可以使用 NOT_SUPPORTED,例如一些查询操作,它们无需事务支持。
1
2
3
4
|
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void methodF() {
// 业务逻辑
}
|
- 如果当前有事务,methodF() 会挂起当前事务,执行时不支持事务;
- 如果没有事务,methodF() 以非事务方式执行。
6. NEVER
传播行为: 如果当前有事务,则抛出异常;如果没有事务,则以非事务方式执行。
应用场景: 当一个方法不允许在事务中运行时使用。例如,一些特定的检查方法,它们要求事务完全不存在。
1
2
3
4
|
@Transactional(propagation = Propagation.NEVER)
public void methodG() {
// 业务逻辑
}
|
- 如果当前有事务,methodG() 会抛出 IllegalTransactionStateException 异常;
- 如果没有事务,methodG() 以非事务方式执行。
7. NESTED
传播行为: 如果当前没有事务,则新建一个事务;如果当前已有事务,则在当前事务中嵌套一个事务。嵌套事务可以独立提交或回滚。
应用场景: 如果你希望事务能够嵌套,并且在嵌套事务回滚时不会影响外部事务的提交,可以使用 NESTED。
1
2
3
4
|
@Transactional(propagation = Propagation.NESTED)
public void methodH() {
// 业务逻辑
}
|
- 如果 methodH() 内部抛出异常并回滚,则不会影响外部事务;
- 如果 methodH() 成功提交,外部事务也会提交。
三、事务传播机制原理分析
1. 事务的传播原理
Spring 的事务传播机制实际上是通过 AOP(面向切面编程)来实现的。Spring 在运行时会生成一个代理对象(通常是 JDK 动态代理或 CGLIB 代理),在事务方法执行时,代理会负责判断事务的传播行为并根据行为决定是否开启新的事务或加入到现有事务中。
- 事务开始:当方法执行时,代理会检查是否已有事务存在。如果没有,则会根据传播行为决定是否需要创建新的事务。
- 事务嵌套:对于 REQUIRES_NEW 或 NESTED 传播行为,Spring 会创建新的事务,这些事务与外部事务相互独立。
- 事务回滚:如果方法发生异常且指定了回滚规则,则代理会回滚事务。
- 事务提交:当方法执行成功,Spring 会提交事务。
2. 事务传播机制的执行顺序
假设方法 A 调用方法 B,方法 B 使用 REQUIRES_NEW 传播行为:
- 方法 A 开始执行时,判断是否有事务,如果没有事务,则开启事务。
- 方法 A 调用方法 B,方法 B 会暂停方法 A 的事务,并开启自己的事务。
- 方法 B 执行完成后,提交自己的事务,并恢复方法 A 的事务。
这就是事务传播机制在嵌套调用中的行为。
在 Spring 中,事务传播机制的实现依赖于 AOP(面向切面编程),而 AOP 只会应用于通过 Spring 管理的 bean。如果我们直接调用同一个类中的方法(即同一个实例的方法),则事务传播机制可能会失效,因为 Spring 的代理对象并未被应用到这些内部方法调用中。以下是关于事务传播机制的一些代码示例,并且会展示事务传播机制失效的场景。
四、代码测试示例
1. REQUIRED 传播行为示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
@Service
public class UserService {
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
System.out.println("methodA: 开始事务");
// 模拟数据库操作
try {
Thread.sleep(1000); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("methodA: 完成事务");
}
@Transactional(propagation = Propagation.REQUIRED)
public void methodB() {
System.out.println("methodB: 开始事务");
// 模拟数据库操作
try {
Thread.sleep(1000); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("methodB: 完成事务");
}
public void testRequiredPropagation() {
methodA(); // 这会触发事务的传播机制
methodB(); // 也会加入到当前事务中
}
}
|
预期输出:
methodA: 开始事务
methodA: 完成事务
methodB: 开始事务
methodB: 完成事务
2. REQUIRES_NEW 传播行为示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@Service
public class UserService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodC() {
System.out.println("methodC: 开始新事务");
// 模拟数据库操作
try {
Thread.sleep(1000); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("methodC: 完成新事务");
}
public void testRequiresNewPropagation() {
methodC(); // 新事务会独立执行
}
}
|
预期输出:
methodC: 开始新事务
methodC: 完成新事务
3. SUPPORTS 传播行为示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@Service
public class UserService {
@Transactional(propagation = Propagation.SUPPORTS)
public void methodD() {
System.out.println("methodD: 支持事务(如果有)");
// 模拟数据库操作
try {
Thread.sleep(1000); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("methodD: 完成");
}
public void testSupportsPropagation() {
methodD(); // 如果存在事务,方法会加入到当前事务中
}
}
|
预期输出:
如果没有事务:
methodD: 支持事务(如果有)
methodD: 完成
如果有事务:
methodD: 支持事务(如果有)
methodD: 完成
4. MANDATORY 传播行为示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@Service
public class UserService {
@Transactional(propagation = Propagation.MANDATORY)
public void methodE() {
System.out.println("methodE: 必须加入事务");
// 模拟数据库操作
try {
Thread.sleep(1000); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("methodE: 完成事务");
}
public void testMandatoryPropagation() {
methodE(); // 调用时必须有事务存在,否则会抛出异常
}
}
|
预期输出:
- 如果没有事务,抛出 TransactionRequiredException 异常;
- 如果有事务,方法会加入现有事务。
5. NOT_SUPPORTED 传播行为示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@Service
public class UserService {
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void methodF() {
System.out.println("methodF: 当前事务被挂起");
// 模拟数据库操作
try {
Thread.sleep(1000); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("methodF: 完成");
}
public void testNotSupportedPropagation() {
methodF(); // 如果有事务,会被挂起,执行非事务操作
}
}
|
预期输出:
如果方法在事务中调用,则事务会被挂起,并执行非事务操作:
methodF: 当前事务被挂起
methodF: 完成
6. NESTED 传播行为示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@Service
public class UserService {
@Transactional(propagation = Propagation.NESTED)
public void methodG() {
System.out.println("methodG: 开始嵌套事务");
// 模拟数据库操作
try {
Thread.sleep(1000); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("methodG: 完成嵌套事务");
}
public void testNestedPropagation() {
methodG(); // 方法会启动一个嵌套事务
}
}
|
预期输出:
methodG: 开始嵌套事务
methodG: 完成嵌套事务
五、事务传播机制失效的场景
场景 1:同一个类中的方法直接调用
如果我们在一个类的实例中直接调用另一个被 @Transactional 注解的方法,事务传播机制可能会失效,因为事务代理是基于 Spring AOP 的,而 AOP 仅对外部方法调用起作用。
示例:事务失效
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
@Service
public class UserService {
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
System.out.println("methodA: 开始事务");
// 模拟数据库操作
try {
Thread.sleep(1000); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("methodA: 完成事务");
}
public void methodB() {
System.out.println("methodB: 事务不生效(直接调用)");
methodA(); // 直接调用 methodA(),事务不会传播
}
}
|
问题:
methodB() 会直接调用 methodA(),但是因为事务注解依赖 AOP 代理,而 methodB() 没有通过 Spring 代理调用 methodA(),因此事务不会生效。
预期输出:
methodB: 事务不生效(直接调用)
methodA: 开始事务
methodA: 完成事务
事务应该在 methodA() 中生效,但因为是直接调用,所以没有生效。
解决方案
为了让事务传播机制生效,方法应该通过 Spring 容器中的代理对象进行调用,可以通过 @Autowired 注入当前类实例并调用其方法,或者通过使用外部类实例来间接调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
@Service
public class UserService {
@Autowired
private UserService self; // 注入当前类的代理实例
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
System.out.println("methodA: 开始事务");
// 模拟数据库操作
try {
Thread.sleep(1000); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("methodA: 完成事务");
}
public void methodB() {
System.out.println("methodB: 事务会生效(通过代理调用)");
self.methodA(); // 通过代理调用 methodA()
}
}
|
六、总结
Spring Boot 的事务传播机制为开发者提供了灵活的事务管理方式,确保在复杂的业务逻辑中能够精准地控制事务的行为。通过合理选择事务传播行为,我们可以在多层业务逻辑中实现事务的一致性和隔离性。
- REQUIRED:大多数情况下使用此传播行为,保证事务一致性。
- REQUIRES_NEW:当需要独立事务时使用。
- SUPPORTS 和 NOT_SUPPORTED:当方法支持或不支持事务时使用。
- MANDATORY 和 NEVER:严格控制事务的参与。
- NESTED:支持嵌套事务,可以独立提交和回滚。