MySQL事务传播机制七个类型一次搞懂事务嵌套的那些坑目录一个让人头疼的线上Bug事务传播是什么七个传播类型一览REQUIRED没有就新建有了就加入REQUIRES_NEW我必须开一个新的NESTED嵌套事务可以单独回滚其他四个传播类型了解即可REQUIRED vs REQUIRES_NEW vs NESTED三兄弟对比实际开发中可能导致事务失效的情况小结一个让人头疼的线上Bug假设你写了一个下单接口逻辑很简单扣库存 创建订单。为了代码复用你把扣库存抽到了一个 Service 方法里订单创建在另一个 Service 方法里下单接口依次调用它们。ServicepublicclassOrderService{AutowiredprivateStockServicestockService;TransactionalpublicvoidcreateOrder(OrderDTOdto){stockService.deduct(dto.getProductId(),dto.getCount());// 扣库存orderMapper.insert(dto);// 创建订单// 假设这里抛了个异常inti1/0;}}ServicepublicclassStockService{Transactionalpublicvoiddeduct(LongproductId,Integercount){stockMapper.deduct(productId,count);}}测试的时候你发现下单接口报错了订单确实没创建但库存被扣了。你一脸困惑——createOrder方法上明明标了Transactional异常回滚了怎么库存没跟着回滚我们来找寻一下问题的根源。这就是事务传播机制要解决的问题。两个方法都有Transactional当 A 方法调用 B 方法时B 的事务到底该不该加入 A 的事务Spring 用事务传播类型来回答这个问题。事务传播是什么简单说事务传播类型决定了当一个带有事务的方法被另一个带有事务的方法调用时事务该怎么办。Spring 定义了 7 种传播类型写在Propagation枚举里。我们逐个来看。七个传播类型一览传播类型含义没有外部事务时有外部事务时REQUIRED默认值新建事务加入外部事务REQUIRES_NEW必须新事务新建事务挂起外部事务新建事务NESTED嵌套事务新建事务创建保存点在保存点内操作SUPPORTS有就加入不开启事务加入外部事务NOT_SUPPORTED不支持事务不开启事务挂起外部事务不开启事务MANDATORY必须有事务抛异常加入外部事务NEVER不能有事务不开启事务抛异常看起来很多但实际开发中最常用的只有前三个REQUIRED、REQUIRES_NEW、NESTED。其他的了解一下就行面试一般不会问到。REQUIRED没有就新建有了就加入这是默认值。你不写Transactional(propagation ...)的话就是 REQUIRED。逻辑很简单如果当前没有事务就新建一个如果当前已经有事务了就加入这个事务回到开头的 bug。createOrder开启了一个事务deduct方法默认是 REQUIRED所以它加入了createOrder的事务。按理说createOrder异常回滚时deduct的操作也应该一起回滚才对。但问题出在哪deduct方法内部自己也标了Transactional而且 Spring AOP 的代理机制导致它走的是自己的事务代理。如果deduct内部抛出异常被createOrder捕获了或者事务配置不当就会出现库存没回滚的情况。更常见的正确用法是外层方法控制事务边界内层方法不要重复声明事务或者使用默认的 REQUIRED 让内层加入外层事务。ServicepublicclassOrderService{Transactional// 外层开启事务publicvoidcreateOrder(OrderDTOdto){stockService.deduct(dto.getCount());// 加入外层事务orderMapper.insert(dto);}}ServicepublicclassStockService{Transactional(propagationPropagation.REQUIRED)// 默认值加入外层事务publicvoiddeduct(Integercount){stockMapper.deduct(count);}}这样createOrder里不管哪一步出异常整个事务一起回滚库存和订单要么都成功要么都失败。REQUIRES_NEW我必须开一个新的REQUIRES_NEW 的意思是不管外面有没有事务我都要开一个全新的事务。如果外面有事务先把外面的挂起。外层事务 T1 │ ├── 执行操作 A │ ├── 调用内层方法REQUIRES_NEW │ → T1 挂起 │ → 开启新事务 T2 │ → 执行操作 B │ → T2 提交或回滚 │ → T1 恢复 │ └── 执行操作 C两个事务完全独立。内层事务的提交或回滚不影响外层事务外层事务的回滚也不影响已经提交的内层事务。这在哪些场景下有用呢场景下单时记录操作日志。不管下单成功还是失败操作日志都应该落库。如果日志和订单在同一个事务里订单回滚时日志也会被回滚掉那么你就不知道这次操作发生过了。ServicepublicclassOrderService{AutowiredprivateLogServicelogService;TransactionalpublicvoidcreateOrder(OrderDTOdto){orderMapper.insert(dto);logService.saveLog(创建订单: dto.getOrderNo());// 即使这里抛异常日志已经落库了}}ServicepublicclassLogService{Transactional(propagationPropagation.REQUIRES_NEW)// 独立事务publicvoidsaveLog(Stringcontent){logMapper.insert(content);}}saveLog使用 REQUIRES_NEW它会在一个独立的事务里执行。即使createOrder后面抛异常回滚了日志已经在一个独立事务里提交了不会丢失。NESTED嵌套事务可以单独回滚NESTED 是最容易和 REQUIRES_NEW 搞混的一个。它的意思是如果外面有事务就在外面的事务里创建一个保存点savepoint。内层操作可以单独回滚到这个保存点而不影响外层事务。外层事务 T1 │ ├── 执行操作 A │ ├── 调用内层方法NESTED │ → 创建保存点 SP1 │ → 执行操作 B │ → 如果 B 异常回滚到 SP1只撤销 B │ → 如果 B 成功继续 │ ├── 执行操作 C │ └── 外层提交或回滚NESTED 和 REQUIRES_NEW 的关键区别REQUIRES_NEW内层事务完全独立内层提交后不受外层回滚影响NESTED内层只是外层的一个保存点内层的提交并不是真正的提交要等外层提交才算真正提交举个例子批量导入用户数据每一条记录一个嵌套事务。某条记录格式不对只回滚这一条其他记录继续ServicepublicclassUserService{TransactionalpublicvoidbatchImport(ListUserDTOusers){for(UserDTOuser:users){try{importOne(user);}catch(Exceptione){log.warn(导入失败跳过: {},user.getName());}}}Transactional(propagationPropagation.NESTED)publicvoidimportOne(UserDTOuser){// 校验 入库if(user.getAge()null){thrownewIllegalArgumentException(年龄不能为空);}userMapper.insert(user);}}如果importOne抛异常它会回滚到保存点不影响其他记录。但注意这些记录的真正提交要等batchImport方法结束时才发生。如果batchImport最后抛异常了所有记录都会回滚包括那些成功的嵌套事务。注意NESTED 的底层依赖数据库的 savepoint 功能。MySQL InnoDB 支持 savepoint所以可以用。但有些数据库不支持需要注意兼容性。其他四个传播类型了解即可SUPPORTS有事务就加入没事务就以非事务方式运行。适合查询方法——你可能在一个事务里调用它加入事务也可能单独调用它不需要事务。Transactional(propagationPropagation.SUPPORTS)publicUserDOfindById(Longid){returnuserMapper.selectById(id);}NOT_SUPPORTED以非事务方式运行。如果外面有事务先挂起。适合不需要事务的操作比如调用外部 HTTP 接口、读取配置等。这些操作不应该被外层事务的回滚影响。Transactional(propagationPropagation.NOT_SUPPORTED)publicStringcallExternalApi(Stringurl){returnrestTemplate.getForObject(url,String.class);}MANDATORY必须在事务中运行。如果当前没有事务直接抛异常。适合必须被事务包裹的方法——如果你调用它的时候没开事务说明调用方式有问题应该早点暴露出来。Transactional(propagationPropagation.MANDATORY)publicvoidupdateBalance(LonguserId,BigDecimalamount){// 必须在事务中因为涉及资金操作accountMapper.updateBalance(userId,amount);}NEVER和 MANDATORY 相反。必须在非事务环境中运行如果有事务就抛异常。实际开发中很少用。REQUIRED vs REQUIRES_NEW vs NESTED三兄弟对比这三个是面试和实际开发中最常被问到的放在一起对比一下维度REQUIREDREQUIRES_NEWNESTED有外部事务时加入挂起新建创建保存点事务独立性不独立共享事务完全独立半独立保存点内层回滚外层也回滚外层不回滚只回滚内层到保存点内层提交等外层提交立即提交等外层提交外层回滚时内层一起回滚已提交的不回滚一起回滚底层机制共享连接新建连接savepoint用一张图来表示三者的事务边界关系REQUIRED ┌──────────────── 外层事务 ───────────────┐ │ 操作 A │ │ ┌──────── 内层事务同一事务────────┐ │ │ │ 操作 B │ │ │ └──────────────────────────────── ─┘ │ │ 操作 C │ └────────────────────────────────── ──────┘ → 一起提交一起回滚 REQUIRES_NEW ┌──────── 外层事务 T1 ────────┐ │ 操作 A │ │ │ T1 挂起 │ │ │ │ │ ┌── 内层事务 T2独立─┐ │ │ │ 操作 B │ │ │ └── T2 提交 ───────────┘ │ │ │ T1 恢复 │ │ 操作 C │ └───────────────────────────┘ → T2 独立提交T1 回滚不影响 T2 NESTED ┌──────────── 外层事务 ────────────┐ │ 操作 A │ │ ├── 保存点 SP1 │ │ │ 操作 B │ │ │ 异常时回滚到 SP1 │ │ 操作 C │ └─────────────────── ─────────────┘ → B 回滚不影响 A 和 C但最终提交由外层决定实际开发中可能导致事务失效的情况情况1同类方法调用事务不生效这是最经典的问题。在同一个类里A 方法调用 B 方法即使 B 方法上标了Transactional事务也不会生效。ServicepublicclassOrderService{publicvoidcreateOrder(OrderDTOdto){// 直接调用不走代理Transactional 失效this.saveOrder(dto);}TransactionalpublicvoidsaveOrder(OrderDTOdto){orderMapper.insert(dto);}}原因是 Spring AOP 基于代理。this.saveOrder()调用的是原始对象的方法没有经过代理所以注解不生效。解决办法注入自己通过代理调用。AutowiredprivateOrderServiceself;// 注入代理对象publicvoidcreateOrder(OrderDTOdto){self.saveOrder(dto);// 走代理事务生效}情况2异常被吞掉事务不回滚Spring 默认只在遇到RuntimeException和Error时回滚。如果你 catch 了异常但没有重新抛出Spring 认为一切正常提交事务。TransactionalpublicvoidcreateOrder(OrderDTOdto){try{orderMapper.insert(dto);inti1/0;}catch(Exceptione){log.error(出错了,e);// 异常被吞事务不会回滚}}解决办法要么不 catch要么 catch 之后重新抛出要么手动标记回滚。TransactionalpublicvoidcreateOrder(OrderDTOdto){try{orderMapper.insert(dto);inti1/0;}catch(Exceptione){log.error(出错了,e);TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();// 手动回滚}}情况3REQUIRES_NEW 导致死锁如果外层事务持有某行锁内层 REQUIRES_NEW 事务也要操作同一行就会死锁。因为外层事务在等内层事务完成内层事务在等外层事务释放锁。TransactionalpublicvoidupdateOrder(LongorderId){orderMapper.lockRow(orderId);// 外层持有行锁logService.saveLog(更新订单);// REQUIRES_NEW也想操作同一行 → 死锁}REQUIRES_NEW 的内层操作应该和外层操作的数据不重叠否则就有死锁风险。小结事务传播机制的本质是解决事务嵌套时多个事务之间如何协调的问题。回到开头的那个 bug——库存扣了但订单没创建根本原因是内层方法的事务传播类型配置不当导致内外层事务没有正确关联。实际开发中记住三个原则就够了默认用 REQUIRED让内层加入外层事务大多数场景够用需要独立提交的操作用 REQUIRES_NEW比如日志记录、消息发送需要部分回滚的批量操作用 NESTED比如批量导入、批量审批至于 SUPPORTS、NOT_SUPPORTED、MANDATORY、NEVER面试时能说清楚含义就行实际开发中很少用到。最后提醒一点事务传播机制是 Spring 框架的概念不是 MySQL 自身的功能。MySQL 只负责事务的开启、提交、回滚和 savepoint至于两个方法之间事务怎么协调那是 Spring 在上层做的编排。理解这个分层关系很多困惑就自然解开了。
MySQL事务传播机制
发布时间:2026/6/11 7:15:36
MySQL事务传播机制七个类型一次搞懂事务嵌套的那些坑目录一个让人头疼的线上Bug事务传播是什么七个传播类型一览REQUIRED没有就新建有了就加入REQUIRES_NEW我必须开一个新的NESTED嵌套事务可以单独回滚其他四个传播类型了解即可REQUIRED vs REQUIRES_NEW vs NESTED三兄弟对比实际开发中可能导致事务失效的情况小结一个让人头疼的线上Bug假设你写了一个下单接口逻辑很简单扣库存 创建订单。为了代码复用你把扣库存抽到了一个 Service 方法里订单创建在另一个 Service 方法里下单接口依次调用它们。ServicepublicclassOrderService{AutowiredprivateStockServicestockService;TransactionalpublicvoidcreateOrder(OrderDTOdto){stockService.deduct(dto.getProductId(),dto.getCount());// 扣库存orderMapper.insert(dto);// 创建订单// 假设这里抛了个异常inti1/0;}}ServicepublicclassStockService{Transactionalpublicvoiddeduct(LongproductId,Integercount){stockMapper.deduct(productId,count);}}测试的时候你发现下单接口报错了订单确实没创建但库存被扣了。你一脸困惑——createOrder方法上明明标了Transactional异常回滚了怎么库存没跟着回滚我们来找寻一下问题的根源。这就是事务传播机制要解决的问题。两个方法都有Transactional当 A 方法调用 B 方法时B 的事务到底该不该加入 A 的事务Spring 用事务传播类型来回答这个问题。事务传播是什么简单说事务传播类型决定了当一个带有事务的方法被另一个带有事务的方法调用时事务该怎么办。Spring 定义了 7 种传播类型写在Propagation枚举里。我们逐个来看。七个传播类型一览传播类型含义没有外部事务时有外部事务时REQUIRED默认值新建事务加入外部事务REQUIRES_NEW必须新事务新建事务挂起外部事务新建事务NESTED嵌套事务新建事务创建保存点在保存点内操作SUPPORTS有就加入不开启事务加入外部事务NOT_SUPPORTED不支持事务不开启事务挂起外部事务不开启事务MANDATORY必须有事务抛异常加入外部事务NEVER不能有事务不开启事务抛异常看起来很多但实际开发中最常用的只有前三个REQUIRED、REQUIRES_NEW、NESTED。其他的了解一下就行面试一般不会问到。REQUIRED没有就新建有了就加入这是默认值。你不写Transactional(propagation ...)的话就是 REQUIRED。逻辑很简单如果当前没有事务就新建一个如果当前已经有事务了就加入这个事务回到开头的 bug。createOrder开启了一个事务deduct方法默认是 REQUIRED所以它加入了createOrder的事务。按理说createOrder异常回滚时deduct的操作也应该一起回滚才对。但问题出在哪deduct方法内部自己也标了Transactional而且 Spring AOP 的代理机制导致它走的是自己的事务代理。如果deduct内部抛出异常被createOrder捕获了或者事务配置不当就会出现库存没回滚的情况。更常见的正确用法是外层方法控制事务边界内层方法不要重复声明事务或者使用默认的 REQUIRED 让内层加入外层事务。ServicepublicclassOrderService{Transactional// 外层开启事务publicvoidcreateOrder(OrderDTOdto){stockService.deduct(dto.getCount());// 加入外层事务orderMapper.insert(dto);}}ServicepublicclassStockService{Transactional(propagationPropagation.REQUIRED)// 默认值加入外层事务publicvoiddeduct(Integercount){stockMapper.deduct(count);}}这样createOrder里不管哪一步出异常整个事务一起回滚库存和订单要么都成功要么都失败。REQUIRES_NEW我必须开一个新的REQUIRES_NEW 的意思是不管外面有没有事务我都要开一个全新的事务。如果外面有事务先把外面的挂起。外层事务 T1 │ ├── 执行操作 A │ ├── 调用内层方法REQUIRES_NEW │ → T1 挂起 │ → 开启新事务 T2 │ → 执行操作 B │ → T2 提交或回滚 │ → T1 恢复 │ └── 执行操作 C两个事务完全独立。内层事务的提交或回滚不影响外层事务外层事务的回滚也不影响已经提交的内层事务。这在哪些场景下有用呢场景下单时记录操作日志。不管下单成功还是失败操作日志都应该落库。如果日志和订单在同一个事务里订单回滚时日志也会被回滚掉那么你就不知道这次操作发生过了。ServicepublicclassOrderService{AutowiredprivateLogServicelogService;TransactionalpublicvoidcreateOrder(OrderDTOdto){orderMapper.insert(dto);logService.saveLog(创建订单: dto.getOrderNo());// 即使这里抛异常日志已经落库了}}ServicepublicclassLogService{Transactional(propagationPropagation.REQUIRES_NEW)// 独立事务publicvoidsaveLog(Stringcontent){logMapper.insert(content);}}saveLog使用 REQUIRES_NEW它会在一个独立的事务里执行。即使createOrder后面抛异常回滚了日志已经在一个独立事务里提交了不会丢失。NESTED嵌套事务可以单独回滚NESTED 是最容易和 REQUIRES_NEW 搞混的一个。它的意思是如果外面有事务就在外面的事务里创建一个保存点savepoint。内层操作可以单独回滚到这个保存点而不影响外层事务。外层事务 T1 │ ├── 执行操作 A │ ├── 调用内层方法NESTED │ → 创建保存点 SP1 │ → 执行操作 B │ → 如果 B 异常回滚到 SP1只撤销 B │ → 如果 B 成功继续 │ ├── 执行操作 C │ └── 外层提交或回滚NESTED 和 REQUIRES_NEW 的关键区别REQUIRES_NEW内层事务完全独立内层提交后不受外层回滚影响NESTED内层只是外层的一个保存点内层的提交并不是真正的提交要等外层提交才算真正提交举个例子批量导入用户数据每一条记录一个嵌套事务。某条记录格式不对只回滚这一条其他记录继续ServicepublicclassUserService{TransactionalpublicvoidbatchImport(ListUserDTOusers){for(UserDTOuser:users){try{importOne(user);}catch(Exceptione){log.warn(导入失败跳过: {},user.getName());}}}Transactional(propagationPropagation.NESTED)publicvoidimportOne(UserDTOuser){// 校验 入库if(user.getAge()null){thrownewIllegalArgumentException(年龄不能为空);}userMapper.insert(user);}}如果importOne抛异常它会回滚到保存点不影响其他记录。但注意这些记录的真正提交要等batchImport方法结束时才发生。如果batchImport最后抛异常了所有记录都会回滚包括那些成功的嵌套事务。注意NESTED 的底层依赖数据库的 savepoint 功能。MySQL InnoDB 支持 savepoint所以可以用。但有些数据库不支持需要注意兼容性。其他四个传播类型了解即可SUPPORTS有事务就加入没事务就以非事务方式运行。适合查询方法——你可能在一个事务里调用它加入事务也可能单独调用它不需要事务。Transactional(propagationPropagation.SUPPORTS)publicUserDOfindById(Longid){returnuserMapper.selectById(id);}NOT_SUPPORTED以非事务方式运行。如果外面有事务先挂起。适合不需要事务的操作比如调用外部 HTTP 接口、读取配置等。这些操作不应该被外层事务的回滚影响。Transactional(propagationPropagation.NOT_SUPPORTED)publicStringcallExternalApi(Stringurl){returnrestTemplate.getForObject(url,String.class);}MANDATORY必须在事务中运行。如果当前没有事务直接抛异常。适合必须被事务包裹的方法——如果你调用它的时候没开事务说明调用方式有问题应该早点暴露出来。Transactional(propagationPropagation.MANDATORY)publicvoidupdateBalance(LonguserId,BigDecimalamount){// 必须在事务中因为涉及资金操作accountMapper.updateBalance(userId,amount);}NEVER和 MANDATORY 相反。必须在非事务环境中运行如果有事务就抛异常。实际开发中很少用。REQUIRED vs REQUIRES_NEW vs NESTED三兄弟对比这三个是面试和实际开发中最常被问到的放在一起对比一下维度REQUIREDREQUIRES_NEWNESTED有外部事务时加入挂起新建创建保存点事务独立性不独立共享事务完全独立半独立保存点内层回滚外层也回滚外层不回滚只回滚内层到保存点内层提交等外层提交立即提交等外层提交外层回滚时内层一起回滚已提交的不回滚一起回滚底层机制共享连接新建连接savepoint用一张图来表示三者的事务边界关系REQUIRED ┌──────────────── 外层事务 ───────────────┐ │ 操作 A │ │ ┌──────── 内层事务同一事务────────┐ │ │ │ 操作 B │ │ │ └──────────────────────────────── ─┘ │ │ 操作 C │ └────────────────────────────────── ──────┘ → 一起提交一起回滚 REQUIRES_NEW ┌──────── 外层事务 T1 ────────┐ │ 操作 A │ │ │ T1 挂起 │ │ │ │ │ ┌── 内层事务 T2独立─┐ │ │ │ 操作 B │ │ │ └── T2 提交 ───────────┘ │ │ │ T1 恢复 │ │ 操作 C │ └───────────────────────────┘ → T2 独立提交T1 回滚不影响 T2 NESTED ┌──────────── 外层事务 ────────────┐ │ 操作 A │ │ ├── 保存点 SP1 │ │ │ 操作 B │ │ │ 异常时回滚到 SP1 │ │ 操作 C │ └─────────────────── ─────────────┘ → B 回滚不影响 A 和 C但最终提交由外层决定实际开发中可能导致事务失效的情况情况1同类方法调用事务不生效这是最经典的问题。在同一个类里A 方法调用 B 方法即使 B 方法上标了Transactional事务也不会生效。ServicepublicclassOrderService{publicvoidcreateOrder(OrderDTOdto){// 直接调用不走代理Transactional 失效this.saveOrder(dto);}TransactionalpublicvoidsaveOrder(OrderDTOdto){orderMapper.insert(dto);}}原因是 Spring AOP 基于代理。this.saveOrder()调用的是原始对象的方法没有经过代理所以注解不生效。解决办法注入自己通过代理调用。AutowiredprivateOrderServiceself;// 注入代理对象publicvoidcreateOrder(OrderDTOdto){self.saveOrder(dto);// 走代理事务生效}情况2异常被吞掉事务不回滚Spring 默认只在遇到RuntimeException和Error时回滚。如果你 catch 了异常但没有重新抛出Spring 认为一切正常提交事务。TransactionalpublicvoidcreateOrder(OrderDTOdto){try{orderMapper.insert(dto);inti1/0;}catch(Exceptione){log.error(出错了,e);// 异常被吞事务不会回滚}}解决办法要么不 catch要么 catch 之后重新抛出要么手动标记回滚。TransactionalpublicvoidcreateOrder(OrderDTOdto){try{orderMapper.insert(dto);inti1/0;}catch(Exceptione){log.error(出错了,e);TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();// 手动回滚}}情况3REQUIRES_NEW 导致死锁如果外层事务持有某行锁内层 REQUIRES_NEW 事务也要操作同一行就会死锁。因为外层事务在等内层事务完成内层事务在等外层事务释放锁。TransactionalpublicvoidupdateOrder(LongorderId){orderMapper.lockRow(orderId);// 外层持有行锁logService.saveLog(更新订单);// REQUIRES_NEW也想操作同一行 → 死锁}REQUIRES_NEW 的内层操作应该和外层操作的数据不重叠否则就有死锁风险。小结事务传播机制的本质是解决事务嵌套时多个事务之间如何协调的问题。回到开头的那个 bug——库存扣了但订单没创建根本原因是内层方法的事务传播类型配置不当导致内外层事务没有正确关联。实际开发中记住三个原则就够了默认用 REQUIRED让内层加入外层事务大多数场景够用需要独立提交的操作用 REQUIRES_NEW比如日志记录、消息发送需要部分回滚的批量操作用 NESTED比如批量导入、批量审批至于 SUPPORTS、NOT_SUPPORTED、MANDATORY、NEVER面试时能说清楚含义就行实际开发中很少用到。最后提醒一点事务传播机制是 Spring 框架的概念不是 MySQL 自身的功能。MySQL 只负责事务的开启、提交、回滚和 savepoint至于两个方法之间事务怎么协调那是 Spring 在上层做的编排。理解这个分层关系很多困惑就自然解开了。