两个事务间的传播机制

两个事务间的传播机制

  • 两个事务间的传播机制
    • [一、前置知识: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 ❌ ❌ 捕获 成功 回滚

四、核心结论

  1. 直接调用(this.method()):

    • @Transactional 完全失效
    • 方法在调用者的事务中执行(如果有)
    • 异常是否回滚取决于外层事务配置
  2. 代理调用(proxy.method()):

    • @Transactional 正常生效
    • 可按传播行为创建独立事务或嵌套事务
  3. 常见错误:

    • 以为在同一个类中加了 @Transactional 就会自动开启新事务
    • 捕获异常后忘记重新抛出,导致事务不回滚
  4. 解决方案:

    • 将需要独立事务的方法放到另一个 @Service 类中
    • 使用 AopContext.currentProxy() 获取代理对象
    • 使用 @Autowired 注入自身代理(需注意循环依赖)

五、最佳实践建议

  • ✅ 避免同一个类中的内部事务调用
  • ✅ 将事务方法拆分到不同的 Bean 中
  • ✅ 明确设置 @Transactional(rollbackFor = Exception.class)
  • ✅ 捕获异常后,如需回滚请调用 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()
相关推荐
天河归来10 小时前
第三方接口不稳定,如何让核心任务继续完成
java·大数据
Knight_AL10 小时前
Java 文件下载时文件名中的 `#` 被转换成 `%23`?一次 URL 编码与解码问题排查
java·开发语言
砚底藏山河12 小时前
python量化自定义周期 K 线合成
java·开发语言·数据库·python·金融
猿究院_夏燊12 小时前
MySQL 的四种隔离级别
java·数据库·mysql
码流子13 小时前
移动收费怎么做?手持终端+App+收费中心的架构实践
java·开发语言·架构
霸道流氓气质13 小时前
AI 流式对话与实时聊天平台:SSE 解码竞态、有状态会话隔离与背压取消的 Vue3 + Spring AI 生产级实战
java·人工智能·spring
云创智城-yuncitys13 小时前
YunParking 4.0:JDK 17 与 Spring Boot 3.2 的停车云升级
java·开发语言·spring boot
文丑颜不良啊13 小时前
LeetCode1661~1680题目解析
java·leetcode
子非鱼@Itfuture13 小时前
关于前后端联调中登录状态/登录信息失效?
java·前端
郭小李13 小时前
openFeign入门
java·开发语言·前端