
1. 从一个转账失败的深夜说起上周五晚上十一点我正打算关电脑突然收到测试同事发来的消息“哥那个批量转账的接口又出问题了A账户扣了钱B账户没收到但日志里显示两个操作都成功了。” 我心里咯噔一下这已经是本月第三次了。打开代码一看果然又是一个典型的事务传播问题在一个标记了Transactional的“主”方法里调用了另一个也标记了Transactional的“子”方法当子方法内部发生异常时预期是整个操作回滚但实际只有子方法自己的数据库操作回滚了主方法里之前的扣款操作却被提交了。钱就这么凭空消失了。这个场景我相信很多后端开发朋友都遇到过或者即将遇到。Spring 事务传播机制这个听起来有点学术的名词实际上是我们日常开发中处理数据库一致性的“交通规则”。它定义了当一个事务方法被另一个事务方法调用时事务应该如何传播。规则没搞懂代码就会像没有交通灯的十字路口事故频发。网上很多文章一上来就罗列七种传播行为配上官方文档的翻译看完依然云里雾里。今天我就结合自己踩过的坑和修复过的线上问题用最直白的语言和场景帮你彻底搞懂 Spring 事务传播机制让你写的代码不再“丢钱”。2. 事务传播到底在“传播”什么在深入七种行为之前我们必须先统一认知Spring 事务传播机制传播的不是事务对象本身而是“对事务的参与方式”。想象一下你主方法要去银行办业务你朋友子方法也正好要办。事务传播机制就是你们俩商量好的“排队策略”策略一REQUIRED你看大厅里已经有一个队伍存在事务你就直接排到那个队伍后面和你朋友共用同一个窗口同一个物理事务。如果大厅里没队伍没有事务你就新开一个窗口新启事务然后让你朋友排到你后面。策略二REQUIRES_NEW不管大厅里有没有队伍你都必须为你朋友单独开一个新窗口新启一个独立的事务。两个窗口业务完全独立一个窗口业务失败关门回滚不影响另一个窗口。所以核心在于当方法B被方法A调用时B是加入A的事务成为其一部分还是自立门户开启新事务或是干脆不参与事务以非事务方式运行。这个“加入/不加入/新开”的规则就是传播行为。这里有一个至关重要的底层知识Spring 的事务管理是基于ThreadLocal的。它会把当前线程关联的数据库连接Connection和事务状态信息绑定到当前线程上。当你在方法上标注Transactional时Spring 会检查当前线程是否已经绑定了一个事务即ThreadLocal里有没有。根据传播行为的不同它决定是复用这个已有的连接和事务状态还是从连接池拿一个新的连接绑定新的状态到线程上。理解这一点就能明白为什么事务能“传播”——因为大家都在同一个线程里操作可以共享线程上下文中的资源。3. 七种传播行为逐个数用场景代替定义Spring 定义了七种传播行为定义很枯燥我们直接看它们各自最适合的“战场”。3.1 REQUIRED默认最常用的“随大流”策略官方解释如果当前存在事务则加入该事务如果当前没有事务则创建一个新的事务。大白话有福同享有难同当。有现成的事务就跟着一起干没有就自己当组长。典型场景绝大多数业务方法。比如用户下单需要扣库存、生成订单、扣减优惠券。这三个子操作对应三个Service方法都应该使用REQUIRED。它们被一个上游的placeOrder方法调用共同组成一个事务单元。任何一个子操作失败整个订单操作全部回滚。代码与数据库视角Service public class OrderService { Transactional(propagation Propagation.REQUIRED) // 主事务 public void placeOrder(OrderDTO dto) { inventoryService.deductStock(dto.getSkuId(), dto.getQuantity()); // 子方法 REQUIRED orderMapper.insert(dto); // 本方法操作 couponService.useCoupon(dto.getUserId(), dto.getCouponId()); // 子方法 REQUIRED // 如果这里抛出异常所有三个数据库操作都会回滚 } } Service public class InventoryService { Transactional(propagation Propagation.REQUIRED) // 默认就是REQUIRED public void deductStock(Long skuId, Integer quantity) { // 操作库存表 } }在数据库层面placeOrder方法开始时Spring 会从连接池获取一个Connection设置autoCommitfalse并绑定到当前线程。deductStock方法被调用时发现当前线程已绑定事务就不再获取新连接而是直接使用这个连接执行SQL。所以所有SQL都在同一个数据库会话中受同一个事务控制。3.2 REQUIRES_NEW必须开新店的“独立承包商”官方解释创建一个新的事务如果当前存在事务则把当前事务挂起。大白话别管你现在在干嘛我必须要一个新的事务你的事先放一边。典型场景日志记录、异步消息预处理、需要绝对独立成功的操作。比如无论用户支付成功与否都需要记录一条不可丢失的操作日志到数据库。如果日志记录和支付在同一个事务里支付失败回滚日志也会被回滚这就丢失了重要的审计信息。Transactional(propagation Propagation.REQUIRED) public void processPayment(PaymentInfo info) { // 1. 核心支付逻辑可能失败 paymentCoreService.process(info); // 2. 记录日志必须成功即使支付回滚 logService.saveOperationLog(buildLog(info)); } Service public class LogService { Transactional(propagation Propagation.REQUIRES_NEW) // 独立事务 public void saveOperationLog(OperationLog log) { logMapper.insert(log); // 这个插入会立即提交不受外部事务影响 } }关键点与坑REQUIRES_NEW会挂起外部事务。这意味着内部方法saveOperationLog会从连接池获取一个全新的Connection开启独立事务并执行提交。外部事务的Connection会被暂时搁置等内部事务完成后再恢复。数据库连接翻倍这需要数据库连接池有足够的连接支撑在高并发下可能成为瓶颈。死锁风险如果内外事务操作同一条数据且加锁顺序不当极易引发死锁。3.3 NESTED基于保存点的“后悔药”官方解释如果当前存在事务则在嵌套事务内执行。嵌套事务可以独立于外部事务进行提交或回滚。如果当前没有事务则行为同REQUIRED。大白话在现有事务里划出一个“子任务区”。这个子任务可以单独回滚而不影响主任务但主任务回滚子任务一定回滚。实现原理它依赖于数据库的保存点Savepoint功能。外部事务开始时内部方法 (NESTED) 被调用时会在当前事务中创建一个保存点。如果内部方法失败事务只会回滚到该保存点外部方法之前的操作依然有效。如果外部方法失败则整个事务包括所有保存点回滚。典型场景可部分回滚的复杂业务。例如一个批量导入用户的任务每处理100条记录作为一个批次。我们希望单个批次失败时只回滚该批次的记录而不影响已成功的前序批次。Transactional public void batchImportUsers(ListUser users) { for (int i 0; i users.size(); i 100) { ListUser batch users.subList(i, Math.min(i 100, users.size())); try { importSingleBatch(batch); // 嵌套事务 } catch (BatchImportException e) { // 仅该批次回滚记录日志继续下一批 log.error(批次 {} 导入失败, i/100, e); } } // 所有批次完成最终提交 } Transactional(propagation Propagation.NESTED) public void importSingleBatch(ListUser batch) { for (User user : batch) { userMapper.insert(user); // 可能触发业务校验异常 } }重要限制NESTED必须在一个已存在的事务中才有效且并非所有数据库都支持保存点。MySQL 的 InnoDB 引擎支持但某些数据库可能不支持。使用时需确认数据库兼容性。3.4 SUPPORTS佛系的“跟随者”官方解释如果当前存在事务则加入该事务如果当前没有事务则以非事务方式执行。大白话你们有组织我就跟着组织你们是散兵游勇我也单干。典型场景查询方法或对事务无强制要求的辅助方法。比如一个查询用户详情的方法如果它在某个更新事务中被调用就跟着一起在事务里读保证读到最新数据如果它被单独调用就直接执行避免开启不必要的事务开销。Transactional(propagation Propagation.SUPPORTS) public User getUserDetail(Long userId) { // 纯查询不修改数据 return userMapper.selectById(userId); }注意对于纯查询现在更推荐使用Transactional(readOnly true)这能给数据库和框架更明确的优化提示。3.5 NOT_SUPPORTED坚定的“非事务执行者”官方解释以非事务方式执行操作。如果当前存在事务则将该事务挂起。大白话我不管你们在不在搞事务反正我不用事务。你们的事先停一下。典型场景需要避免事务影响的操作。例如执行一个非常耗时的统计计算或者调用一个不支持事务的第三方存储如某些 NoSQL 的写操作。你不希望这个长耗时操作占用着数据库连接导致整个事务锁持有时间过长。public void generateReport() { // 一些非DB操作... heavyCalculationService.calculate(); // 这个方法不希望受事务管理 // ... } Service public class HeavyCalculationService { Transactional(propagation Propagation.NOT_SUPPORTED) public void calculate() { // 复杂的、与事务无关的计算或非事务性存储操作 // 即使外部有事务这里也会被挂起此方法无事务 } }与REQUIRES_NEW的区别NOT_SUPPORTED是完全不开启事务而REQUIRES_NEW是开启一个独立的新事务。前者不涉及事务提交回滚后者涉及。3.6 MANDATORY强制的“事务依赖者”官方解释如果当前存在事务则加入该事务如果当前没有事务则抛出异常。大白话我必须在一个事务里干活没有事务就别叫我典型场景用于强制某些方法必须在事务上下文中被调用是一种设计上的约束和防御性编程。可以避免在非事务环境下调用本应在事务中执行的方法导致数据不一致。Service public class CriticalService { Transactional(propagation Propagation.MANDATORY) public void updateCriticalData(Data data) { // 这个操作非常重要必须在事务保护下进行 criticalDataMapper.update(data); } } Service public class OuterService { Transactional // 有这个调用 updateCriticalData 正常 public void businessProcess() { // ... 其他操作 criticalService.updateCriticalData(data); // OK } public void anotherProcess() { // 没有 Transactional criticalService.updateCriticalData(data); // 抛出 IllegalTransactionStateException! } }3.7 NEVER决绝的“事务排斥者”官方解释以非事务方式执行。如果当前存在事务则抛出异常。大白话我坚决不在事务里干活你们要是正在搞事务就别来烦我典型场景明确要求不能在事务中执行的方法。比如发送消息到消息队列。消息队列的生产者调用通常不希望被包裹在数据库事务里因为事务提交可能很慢或者我们希望消息发送和数据库操作解耦。使用NEVER可以确保调用方不会错误地在一个事务方法里调用它。Service public class MessageService { Transactional(propagation Propagation.NEVER) public void sendOrderCreatedMessage(Order order) { // 发送消息到MQ rocketMQTemplate.send(order); } }注意在现代架构中更常见的做法是使用事务消息如 RocketMQ 的事务消息或本地消息表来保证数据库操作与消息发送的最终一致性而不是简单用NEVER。NEVER更多是一种严格的约束。4. 核心原理与底层机制拆解理解了“是什么”和“怎么用”我们再来深挖一层“为什么”。Spring 是如何实现这套传播机制的呢关键在于两个核心接口PlatformTransactionManager和TransactionStatus以及背后的线程绑定机制。1.PlatformTransactionManager(平台事务管理器)这是 Spring 事务抽象的核心。无论是 JDBC、JPA、Hibernate 还是 JTASpring 都通过这个接口的统一方法来管理事务getTransaction,commit,rollback。我们配置的DataSourceTransactionManager或JpaTransactionManager就是它的实现。2.TransactionStatus(事务状态)它代表一个事务的当前状态包含了诸如“是否是新事务”、“是否有保存点”、“是否已完成”等信息。传播行为的决策就体现在getTransaction(TransactionDefinition definition)这个方法里根据传入的定义包含传播行为和当前线程状态决定是返回一个已有的TransactionStatus对应加入事务还是创建一个新的。3. 线程绑定与TransactionSynchronizationManager这是实现传播的“粘合剂”。它是一个基于ThreadLocal的工具类关键属性有resources: 一个 Map用来将数据源DataSource映射到实际的数据库连接Connection或其他资源。synchronizations: 一个TransactionSynchronization回调列表用于在事务完成前后执行自定义逻辑如清理缓存、发送事件。currentTransactionName: 当前事务的名称。currentTransactionReadOnly: 当前事务是否只读。currentTransactionIsolationLevel: 当前事务的隔离级别。actualTransactionActive: 当前是否真的有事务活动。传播决策流程以REQUIRED和REQUIRES_NEW为例假设方法A (REQUIRED) 调用方法B (REQUIRES_NEW)。进入方法ASpring 拦截器调用TransactionManager.getTransaction()传入Propagation.REQUIRED。TransactionManager通过TransactionSynchronizationManager检查当前线程是否已绑定资源连接和事务状态。发现没有于是从数据源获取一个新的Connection。设置autoCommitfalse。将这个Connection绑定到当前线程的resources中。创建一个新的TransactionStatus对象标记为新事务并返回。方法A执行调用方法B。进入方法BSpring 拦截器再次调用TransactionManager.getTransaction()但这次传入Propagation.REQUIRES_NEW。TransactionManager检查当前线程发现已绑定了一个活跃的事务来自方法A。由于传播行为是REQUIRES_NEW它需要挂起当前事务挂起将当前线程绑定的所有事务资源主要是那个Connection和TransactionStatus解绑并保存在一个SuspendedResourcesHolder对象中。新建从数据源再获取一个新的Connection绑定到线程开启新事务创建新的TransactionStatus。方法B在这个全新的连接和事务中执行。提交或回滚只影响这个新连接上的操作。方法B执行完毕Spring 会提交/回滚方法B的事务。将方法B的连接归还连接池或关闭。恢复将之前挂起的SuspendedResourcesHolder中的资源方法A的连接和事务状态重新绑定回当前线程。控制权回到方法A它继续使用自己原来的连接执行。最终方法A提交时提交的是它自己连接上的所有操作。这个过程清晰地展示了“挂起”和“独立连接”的含义。NESTED的实现则不同它不会获取新连接而是在当前连接上执行SAVEPOINT savepoint_name的SQL语句来创建保存点。5. 实战中的高频“深坑”与避坑指南理论懂了代码还是会写错。下面是我总结的几个最容易出问题的地方。坑一REQUIRES_NEW不生效内外事务一起回滚现象在方法A中调用方法BB配置了Transactional(propagation Propagation.REQUIRES_NEW)期望B独立提交。但当B执行完A随后抛出异常时B的操作也被回滚了。根因异常被吃掉了或传播错了。Spring 事务的回滚规则是默认只对RuntimeException和Error回滚。如果方法B内部抛出的异常被自己catch了但没有重新抛出或者抛出的是受检异常如Exception且A方法没有配置Transactional(rollbackFor Exception.class)那么B方法的事务可能不会回滚但A方法抛出的运行时异常会导致A的事务回滚。关键在于如果B方法的事务已经提交了那么A的事务回滚是无法影响B的。出现一起回滚说明B的事务根本没提交成功。排查检查B方法内部是否捕获了异常。检查B方法抛出的异常类型。确保是RuntimeException或已在Transactional中声明回滚的异常。最关键的检查B方法是否被正确代理。如果B方法是在同一个类内部被调用的即this.methodB()那么Transactional注解会失效因为Spring基于AOP的代理无法拦截内部调用。此时B方法根本没有独立事务它的操作属于A事务的一部分。解决方案确保异常正确抛出。解决自调用问题将B方法抽取到另一个ServiceBean中通过注入的Bean来调用或者使用AopContext.currentProxy()不推荐侵入性强。坑二NESTED报错No existing transaction found for transaction marked with propagation nested现象配置了NESTED的方法在调用时抛出上述异常。根因NESTED行为要求当前必须存在一个事务。如果调用它的方法没有Transactional或者事务传播行为是NOT_SUPPORTED、NEVER、SUPPORTS且当前无事务就会触发此错误。解决方案确保NESTED方法被一个具有有效事务REQUIRED,REQUIRES_NEW,MANDATORY等的上下文所调用。坑三事务与异步方法Async的冲突现象在事务方法中调用一个异步方法去执行数据库更新发现更新不生效或报错。根因Spring 的Async默认使用不同的线程池执行任务。事务资源Connection是绑定在调用线程的ThreadLocal上的。当任务切换到另一个线程时原线程的事务上下文无法传递过去导致异步方法内要么没有事务要么开启的是另一个完全独立的事务。解决方案方案A简单但需注意在异步方法内部自己管理事务即在其方法上添加Transactional。但这意味着它是一个独立事务与调用方事务无关联。方案B复杂但严谨使用编程式事务管理TransactionTemplate在异步任务的Runnable或Callable内部显式控制事务边界。方案C架构层面重新考虑业务设计避免在事务边界内进行真正的异步数据库写操作。通常的做法是在主事务中记录一个“待处理”状态到数据库然后提交。再由异步任务去轮询或监听消息处理这个“待处理”的记录。这保证了主事务的快速提交和最终一致性。坑四事务传播与锁的协同问题场景方法A (REQUIRED) 先查询并锁定某行数据SELECT ... FOR UPDATE然后调用方法B (REQUIRES_NEW)。方法B也需要更新这行数据。问题在MySQL中行锁是基于连接的。方法A和方法B使用了不同的数据库连接因为REQUIRES_NEW。方法B的连接无法获取被方法A的连接持有的行锁会导致锁等待超时。如果设计不当甚至可能形成跨连接死锁。避坑指南在设计使用REQUIRES_NEW或NOT_SUPPORTED的方法时要高度警惕它们是否可能与外部事务操作相同的数据库资源。如果可能需要仔细评估锁的粒度和持有时间或者考虑改变设计避免跨事务的锁竞争。6. 如何为你的方法选择正确的传播行为面对七种选择不必慌张。遵循以下决策路径可以帮你做出90%以上的正确选择第一步这个方法要不要事务纯查询且不需要“可重复读”等事务隔离级别保证- 考虑SUPPORTS或readOnlytrue。甚至可以不加Transactional让数据库autoCommit。需要原子性的增删改- 进入第二步。第二步它通常被谁调用几乎总是作为其他业务方法的一部分被调用如扣库存、生成订单项 -首选REQUIRED默认值。这是最安全、最常用的选择保证大家在同一艘船上。需要强制在事务中被调用如关键状态更新 - 使用MANDATORY。这是一种防御性编程在编译期就能暴露设计缺陷。需要强制不在事务中被调用如发送消息 - 使用NEVER。第三步它是否需要独立于调用方事务需要独立且失败不影响调用方成功也必须持久化如审计日志 - 使用REQUIRES_NEW。务必评估连接池压力和死锁风险。需要部分独立即自己失败不影响调用方但调用方失败自己也要回滚如批量处理中的子批次 - 使用NESTED。务必确认数据库支持保存点。希望不参与事务避免长事务持有锁如耗时计算 - 使用NOT_SUPPORTED。一个简单的选择矩阵方法特性推荐传播行为原因标准的增删改业务单元REQUIRED保证原子性与调用方同进退必须记录成功的日志REQUIRES_NEW独立提交防止被主业务回滚批量处理中的可失败子任务NESTED支持部分回滚依赖主事务查询方法SUPPORTS或readOnlytrue灵活避免不必要的开启事务必须在事务中调用的关键方法MANDATORY强制约束提前暴露错误调用绝对不能在有事务时调用的方法NEVER强制约束防止错误嵌套避免受事务影响的耗时操作NOT_SUPPORTED挂起当前事务非事务运行最后记住两个黄金实践保持简单除非有明确且强烈的理由否则坚持使用默认的REQUIRED。过度设计传播行为会增加系统的复杂性和理解成本。充分测试尤其是使用了REQUIRES_NEW、NESTED等行为时必须编写集成测试模拟正常、异常情况验证事务的提交和回滚是否符合预期。可以结合SpringBootTest和内存数据库如 H2进行快速验证。事务传播不是魔法它是一套清晰的契约。理解每个行为背后的“合约条款”就能在复杂的业务流中精准地控制好每一笔数据操作的生死与共。