两个事务间的传播机制
- 两个事务间的传播机制
-
- [一、前置知识:Spring 事务原理](#一、前置知识:Spring 事务原理)
- 二、示例环境准备
- [示例1:直接调用 public 方法(无事务注解)](#示例1:直接调用 public 方法(无事务注解))
- [示例2:直接调用 private 方法](#示例2:直接调用 private 方法)
- 示例3:直接调用带事务注解的方法(注解失效)
- [示例4:通过代理调用 + REQUIRED(默认)](#示例4:通过代理调用 + REQUIRED(默认))
- [示例5:REQUIRES_NEW + 代理调用](#示例5:REQUIRES_NEW + 代理调用)
- [示例6:NESTED + 代理调用](#示例6:NESTED + 代理调用)
- [示例7:捕获异常 + 直接调用(无代理)](#示例7:捕获异常 + 直接调用(无代理))
- [示例8:REQUIRES_NEW + 直接调用(注解失效)](#示例8:REQUIRES_NEW + 直接调用(注解失效))
- 三、总结对比表
- 四、核心结论
- 五、最佳实践建议
两个事务间的传播机制
核心问题:在同一个类中,一个带事务的方法直接调用另一个方法(无论是否有
@Transactional),事务注解是否生效?答案:不生效,因为 Spring 事务基于代理,直接调用绕过了代理。
一、前置知识:Spring 事务原理
- Spring 通过 AOP 动态代理 实现事务管理。
- 只有通过 代理对象 调用方法,
@Transactional才会被拦截并生效。 this.method()或直接调用method()不会经过代理,因此事务注解失效。
二、示例环境准备
java
@RestController
public class TestController {
@Autowired
private UserService userService;
@Autowired
private PostService postService;
// 获取代理对象的方法(方式之一)
private TestController getProxy() {
return (TestController) AopContext.currentProxy();
}
}
示例1:直接调用 public 方法(无事务注解)
java
@GetMapping("/test1")
@Transactional
public String test1() {
userService.removeById(666L);
removePost();
return "ok";
}
public void removePost() {
postService.removeById(666L);
int a = 1 / 0; // 模拟异常
}
结果: ✅ 都回滚
原因: 异常传播到外层 test1(),外层事务回滚。
removePost()虽然无事务,但在外层事务上下文中执行,异常导致外层回滚。
示例2:直接调用 private 方法
java
@GetMapping("/test2")
@Transactional
public String test2() {
userService.removeById(666L);
removePost();
return "ok";
}
private void removePost() {
postService.removeById(666L);
int a = 1 / 0;
}
结果: ✅ 都回滚
原因: 同上,private 方法不影响事务传播。
示例3:直接调用带事务注解的方法(注解失效)
java
@GetMapping("/test3")
@Transactional
public String test3() {
userService.removeById(666L);
removePost(); // 直接调用
return "ok";
}
@Transactional
public void removePost() {
postService.removeById(666L);
int a = 1 / 0;
}
结果: ✅ 都回滚
原因:
removePost()的@Transactional失效(直接调用)- 代码仍在事务A中执行
- 异常导致事务A回滚
示例4:通过代理调用 + REQUIRED(默认)
java
@GetMapping("/test4")
@Transactional
public String test4() {
userService.removeById(666L);
getProxy().removePost();
return "ok";
}
@Transactional
public void removePost() {
postService.removeById(666L);
int a = 1 / 0;
}
结果: ✅ 都回滚
原因:
- 代理调用使
@Transactional生效 - 传播行为 =
REQUIRED,复用外层事务 - 异常导致唯一事务回滚
示例5:REQUIRES_NEW + 代理调用
java
@GetMapping("/test5")
@Transactional
public String test5() {
userService.removeById(666L);
try {
getProxy().removePostNew();
} catch (Exception e) {
System.out.println("捕获异常");
}
return "ok";
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void removePostNew() {
postService.removeById(666L);
int a = 1 / 0;
}
结果:
userService.removeById(666L)✅ 成功(外层事务提交)postService.removeById(666L)❌ 失败(独立事务回滚)
原因:REQUIRES_NEW挂起外层事务,创建独立事务- 独立事务回滚,不影响外层事务
示例6:NESTED + 代理调用
java
@GetMapping("/test6")
@Transactional
public String test6() {
userService.removeById(666L);
try {
getProxy().removePostNested();
} catch (Exception e) {
System.out.println("捕获异常");
}
return "ok";
}
@Transactional(propagation = Propagation.NESTED)
public void removePostNested() {
postService.removeById(666L);
int a = 1 / 0;
}
结果:
userService.removeById(666L)✅ 成功postService.removeById(666L)❌ 失败
原因:NESTED创建保存点,内层回滚到保存点- 外层事务可以继续提交
示例7:捕获异常 + 直接调用(无代理)
java
@GetMapping("/test7")
@Transactional
public String test7() {
userService.removeById(666L);
try {
removePost(); // 直接调用
} catch (Exception e) {
System.out.println("捕获异常");
}
return "ok";
}
@Transactional
public void removePost() {
postService.removeById(666L);
int a = 1 / 0;
}
结果:
userService.removeById(666L)✅ 成功postService.removeById(666L)❌ 失败
原因:- 虽然
removePost()有@Transactional,但直接调用导致注解失效 - 代码在外层事务中执行
- 异常被捕获,外层事务 不会回滚 (默认只对未捕获的
RuntimeException回滚)
⚠️ 注意:这是最容易被误解的场景!
很多人以为"有
@Transactional就会回滚",但直接调用时注解不生效。
示例8:REQUIRES_NEW + 直接调用(注解失效)
java
@GetMapping("/test8")
@Transactional
public String test8() {
userService.removeById(666L);
try {
removePostNew(); // 直接调用
} catch (Exception e) {
System.out.println("捕获异常");
}
return "ok";
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void removePostNew() {
postService.removeById(666L);
int a = 1 / 0;
}
结果:
userService.removeById(666L)✅ 成功postService.removeById(666L)❌ 失败
原因:- 直接调用 →
@Transactional失效 REQUIRES_NEW不生效,代码仍在原事务中- 异常被捕获 → 外层事务不回滚
三、总结对比表
| 示例 | 调用方式 | removePost有@Tx | 传播行为 | 是否代理 | 事务生效 | 异常处理 | user删除 | post删除 |
|---|---|---|---|---|---|---|---|---|
| 1 | 直接 | 无 | - | ❌ | - | 未捕获 | 回滚 | 回滚 |
| 2 | 直接(private) | 无 | - | ❌ | - | 未捕获 | 回滚 | 回滚 |
| 3 | 直接 | 有 | REQUIRED | ❌ | ❌ | 未捕获 | 回滚 | 回滚 |
| 4 | 代理 | 有 | REQUIRED | ✅ | ✅ | 未捕获 | 回滚 | 回滚 |
| 5 | 代理 | 有 | REQUIRES_NEW | ✅ | ✅ | 捕获 | 成功 | 回滚 |
| 6 | 代理 | 有 | NESTED | ✅ | ✅ | 捕获 | 成功 | 回滚 |
| 7 | 直接 | 有 | REQUIRED | ❌ | ❌ | 捕获 | 成功 | 回滚 |
| 8 | 直接 | 有 | REQUIRES_NEW | ❌ | ❌ | 捕获 | 成功 | 回滚 |
四、核心结论
-
直接调用(
this.method()):@Transactional完全失效- 方法在调用者的事务中执行(如果有)
- 异常是否回滚取决于外层事务配置
-
代理调用(
proxy.method()):@Transactional正常生效- 可按传播行为创建独立事务或嵌套事务
-
常见错误:
- 以为在同一个类中加了
@Transactional就会自动开启新事务 - 捕获异常后忘记重新抛出,导致事务不回滚
- 以为在同一个类中加了
-
解决方案:
- 将需要独立事务的方法放到另一个
@Service类中 - 使用
AopContext.currentProxy()获取代理对象 - 使用
@Autowired注入自身代理(需注意循环依赖)
- 将需要独立事务的方法放到另一个
五、最佳实践建议
- ✅ 避免同一个类中的内部事务调用
- ✅ 将事务方法拆分到不同的 Bean 中
- ✅ 明确设置
@Transactional(rollbackFor = Exception.class) - ✅ 捕获异常后,如需回滚请调用
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()