@Transactional 传播机制

@Transactional 传播机制 Transactional是 Spring Boot 中处理事务最核心的注解。它的作用是保证一系列数据库操作要么全部成功要么全部失败回滚。在实际复杂的业务中比如 Service A 调用 Service B我们需要控制这两个 Service 的事务是“合二为一”还是“各自独立”这就是事务传播机制 (Propagation) 解决的问题。一、Transactional的基础用法1、基础语法通常加在 Service 层的方法上也可以加在类 上表示该类所有 public 方法都开启事务。import org.springframework.transaction.annotation.Transactional; Service public class OrderService { // 推荐写法明确指定回滚异常类型 Transactional(rollbackFor Exception.class) public void createOrder(OrderDTO order) { // 1. 扣减库存 inventoryService.deduct(order.getProductId()); // 2. 保存订单 orderRepository.save(order); // 3. 扣减余额 accountService.pay(order.getUserId(), order.getAmount()); } }2、需要注意的坑规则1必须是 public 方法。Spring AOP 默认无法拦截 private/protected 方法规则2加上 rollbackFor Exception.class默认情况Spring 只会在遇到 RuntimeException (运行时异常) 或 Error 时回滚。如果你的代码抛出了 Checked Exception (比如 IOException 或自定义的非运行时异常)事务不会回滚最佳实践养成习惯永远写成 Transactional(rollbackFor Exception.class)。二、 事务传播机制 (Propagation) 详解当 Service A外层调用 Service B内层时Service B 是加入 A 的事务还是自己新开一个这由 propagation 属性决定。虽然 Spring 有 7 种传播行为但日常开发中 95% 只会用到以下三种场景设定假设我们有两个服务MainService (外层)主业务。SubService (内层)子业务被外层调用// 外层调用伪代码 Transactional public void mainMethod() { // 做一些事... subService.subMethod(); // 调用内层 // 做另一些事... }1. REQUIRED (默认值) —— “同生共死”如果外层有事务内层就加入如果外层没事务内层就自己开一个。这是最常用的。逻辑外层和内层属于同一个物理事务。结果任何一方报错全部回滚。代码示例// 外层处理订单 Transactional(propagation Propagation.REQUIRED) public void createOrder() { orderMapper.insertOrder(); // 步骤1 // 调用内层扣减库存 stockService.deductStock(); // 假设这里报错int i 1/0; - 步骤1和内层的扣减库存都会回滚 } // 内层扣减库存 Transactional(propagation Propagation.REQUIRED) public void deductStock() { stockMapper.updateStock(); // 假设这里报错 - 外层的 createOrder 也会感知到异常全部回滚 }2. REQUIRES_NEW —— “各自为政”不管外层有没有事务内层都开启一个全新的事务。如果外层有事务会把外层事务挂起等内层搞定提交了再恢复外层。逻辑两个独立的物理事务两个数据库连接。核心用途记录日志、发送记录。不管主业务是成功还是失败日志必须保存下来不能因为主业务回滚把日志也回滚了。代码示例// 外层主业务 Transactional(propagation Propagation.REQUIRED) public void buyItem() { // 1. 买东西 productMapper.reduceStock(); // 2. 记录日志 (不管买没买成我都要记录这一次操作) try { logService.saveLog(用户尝试购买); } catch (Exception e) { // 如果日志记失败了捕获异常不影响买东西的主流程 } // 3. 模拟异常 throw new RuntimeException(购买失败余额不足); } // 内层日志服务 Transactional(propagation Propagation.REQUIRES_NEW) // --- 关键点 public void saveLog(String msg) { logMapper.insert(msg); }结果分析buyItem 抛出异常reduceStock 会回滚没买成。但是saveLog 因为是 REQUIRES_NEW它已经独立提交了。所以日志会保留在数据库中3. NESTED —— “留有后路” (嵌套事务)如果外层有事务内层就在这个事务里做一个Savepoint (保存点)。如果内层报错只回滚到保存点内层刚才做的操作没了外层可以选择捕获异常继续执行其他逻辑外层不一定回滚。如果外层报错内层和外层统统回滚。逻辑同一个物理事务但利用了 JDBC 的 Savepoint 功能。核心用途重试机制、备选方案。比如尝试用积分支付如果失败了回滚积分扣减catch 住异常自动改用余额支付。代码示例// 外层支付流程 Transactional(propagation Propagation.REQUIRED) public void pay() { orderMapper.updateStatus(PAYING); try { // 尝试用积分支付 (内层) pointService.payWithPoints(); } catch (Exception e) { // 捕获了内层的异常 // 因为是 NESTED外层不会被标记为 rollback-only我们可以走 Plan B System.out.println(积分支付失败转用余额支付); balanceService.payWithBalance(); } } // 内层积分服务 Transactional(propagation Propagation.NESTED) // --- 关键点 public void payWithPoints() { pointMapper.deduct(); throw new RuntimeException(积分不够); // 抛出异常 }结果分析payWithPoints 失败回滚但只回滚了“扣积分”这一步。pay 方法捕获了异常updateStatus(PAYING) 不会回滚且 payWithBalance 会继续执行并提交。注意点如果内层NESTED抛出异常且外层没有捕获try-catch这个异常那么外层和内层都会回滚。数据就像什么都没发生过一样全军覆没。1、底层逻辑虽然 NESTED 是通过数据库的 Savepoint保存点实现的但异常的 传递遵循 Java 的基本规则。流程如下内层报错内层方法执行失败数据库操作回滚到“保存点”也就是说内层做的修改先撤销了。异常冒泡因为外层没有写 try-catch这个异常会继续向上抛出传给外层方法。外层崩溃外层方法接收到了异常也随之执行中断报错。最终判定Spring 的事务管理器监控到外层事务也抛出了异常于是触发外层事务的默认回滚机制——把整个事务全部回滚。​ 所以最终结果看起来和 REQUIRED 是一模一样的所有数据都回滚了。2、代码示例Service public class MainService { Autowired private SubService subService; Autowired private UserMapper userMapper; // 外层事务 Transactional(propagation Propagation.REQUIRED) public void mainMethod() { // 1. 外层做了一些操作 userMapper.updateName(外层张三); // 2. 调用内层 NESTED 方法 // 【关键点】这里没有 try-catch subService.nestedMethod(); // 3. 后面的代码不会执行 System.out.println(这就话永远打印不出来); } } Service public class SubService { Autowired private UserMapper userMapper; // 内层事务 (NESTED) Transactional(propagation Propagation.NESTED) public void nestedMethod() { // 4. 内层做了一些操作 userMapper.updateAge(18); // 5. 抛出异常 throw new RuntimeException(内层炸了); } }结果分析内层updateAge(18) 会回滚因为它回滚到了 Savepoint。外层由于异常冒泡到了 mainMethod 导致 mainMethod 也是异常结束所以 updateName(外层张三) 也会回滚。数据库名字没变年龄也没变。3. 既然不捕获就是全部回滚那 NESTED 和 REQUIRED 有什么区别如果在“不捕获异常”的情况下它们确实表现一致。NESTED 存在的唯一意义就在于外层拥有“选择权”。REQUIRED不管外层捕不捕获只要内层炸了外层必须陪葬因为 Spring 标记了 rollback-only。即使外层 try-catch 了提交时也会报错 Transaction rolled back because it has been marked as rollback-only。NESTED内层炸了外层可以选择捕获异常并继续执行其他逻辑比如走备用方案。总结一句话 用 NESTED 时如果你不写 try-catch那你就纯属“浪费感情”效果和默认的 REQUIRED 一模一样还多了一个创建 Savepoint 的性能开销。所以用 NESTED 必须配合 try-catch 使用。三、 总结对比表传播属性 行为描述 谁报错回滚谁 常用场景REQUIRED (默认) 加入当前事务没有则新建 只要有一处报错所有操作全回滚 普通的业务逻辑调用REQUIRES_NEW 挂起当前事务新建独立事务 内层报错回滚内层外层报错回滚外层。互不干扰 记录操作日志、流水记录NESTED 在当前事务中嵌套 (Savepoint) 内层报错只回滚内层(若被捕获)外层报错回滚全部 复杂的子业务尝试、备选方案四、不常用四种坦白说非常不常用。在 99% 的实际开发工作中你只需要掌握前三种REQUIRED, REQUIRES_NEW, NESTED。剩下的四种属于**“特定场景下的特殊手段”**。第一类随波逐流型 (可有可无)4. SUPPORTS (佛系)含义如果外层有事务我就加入如果外层没事务我就以非事务方式运行。常用度⭐ (极低)场景通常用于只读查询。你希望如果在一个大事务里调用它它能读到事务内修改的数据保持一致性但如果只是单独调用它没必要为了查个数据专门开个事务为了性能。例子JavaTransactional(propagation Propagation.SUPPORTS)public User findUserById(Long id) {// 如果外层有事务这里就在事务里查能查到外层刚修改还没提交的数据。// 如果外层没事务这里就普通查询。return userMapper.selectById(id);}5. NOT_SUPPORTED (独善其身)含义我不支持事务。如果外层有事务把它挂起暂停我以非事务方式运行完再恢复外层事务。常用度⭐⭐ (偶尔用到主要为了性能)场景执行耗时操作如发邮件、发短信、复杂的图像处理。原因数据库连接是非常宝贵的资源。如果在事务里发邮件耗时 5 秒这意味着数据库连接被占用了 5 秒不释放且可能持有锁。用 NOT_SUPPORTED 可以先把数据库事务挂起发完邮件再回来极大释放数据库压力。例子Servicepublic class OrderService {Transactionalpublic void createOrder() {orderMapper.insert(); // 占用数据库连接// 发送邮件耗时操作不应该占用数据库事务资源emailService.sendEmail();otherMapper.update(); // 恢复事务继续做}}// 在 EmailService 中Transactional(propagation Propagation.NOT_SUPPORTED)public void sendEmail() {// 这里没有事务运行慢一点也不会锁住数据库表Thread.sleep(5000);}第二类严格管控型 (强制约束)这两类更多是为了代码规范和防止误用。6. MANDATORY (强依赖)含义我必须在事务中运行。如果外层没事务我就直接抛异常。常用度⭐ (极低)场景用于约束某些方法绝对不能独立调用必须作为某个大业务流程的一部分。比如“扣减库存”这个动作你规定它必须依赖于“订单创建”或“支付”的事务不允许任何人单独写个测试代码去裸调它。例子JavaTransactional(propagation Propagation.MANDATORY)public void deductStock() {// 如果直接在 Controller 里调用这个方法且没开事务直接报错// IllegalTransactionStateException: No existing transaction found for transaction marked with propagation mandatorystockMapper.update();}7. NEVER (绝缘体)含义我绝不能在事务中运行。如果外层有事务我就抛异常。常用度⭐ (几乎不用)场景极少见。可能用于某些非线程安全的、或者涉及多线程并发操作的代码为了防止混淆上下文强制要求不能在 Spring 管理的事务上下文中执行。例子JavaTransactional(propagation Propagation.NEVER)public void selfInit() {// 如果你在一个 Transactional 方法里调用我我就报错。}