
在Spring项目里Transactional大概是所有注解里最“看着简单、用起来坑最多”的一个。很多人习惯性地在Service方法上加一个Transactional以为事务就万无一失了结果线上数据对不上、钱多扣了、库存超卖了最后查出来是事务压根没生效。这个场景我遇到过太多次而且有个规律事务失效的问题往往不是报错而是“不报错但结果不对”所以特别隐蔽排查起来也很费劲。这篇文章我把平时帮同事和自己排查事务失效问题的经验整理成一套完整的内容包括Transactional的底层工作方式、常见的失效场景、传播机制的误用以及一套可以直接照着用的排查流程。不管你是刚接触Spring事务还是已经写了两三年业务代码这些内容都能帮你少踩几个坑。尤其是自调用、异常被吞、非public方法这几个经典场景几乎每个项目中都能碰到。1. 先搞懂Transactional是怎么工作的1.1 AOP代理事务注解背后的“代理机制”要理解事务为什么会失效首先得清楚Transactional不是“魔法”。Spring里事务功能的实现核心是AOP面向切面编程具体说就是Spring容器在启动时会为带有Transactional的Bean生成一个代理对象。当外部调用这个Bean的方法时实际上调用的是代理对象的方法代理对象在方法执行前开启事务在方法执行后根据结果决定提交还是回滚。这个过程就好比你请了一个前台助理。你对外公布的联系方式是助理的电话外人打电话进来助理先记录、再转接给你。正常通话没问题但如果绕过助理直接打你的私人电话助理就完全不知道这通电话的存在自然也没法帮你做记录。事务代理就是这个助理只有走代理的调用才被纳入事务管理。这里的关键点是外部调用才会走代理。如果是在同一个类内部一个方法直接调用另一个带有Transactional的方法实际上调用的是this对象的方法而不是代理对象的方法那事务配置就不会生效。很多人第一次遇到自调用失效的问题时都懵了原因就在这里。1.2 反向理解哪些场景代理根本不会生效理解代理机制之后很多失效场景就能反向推导出来。第一如果目标对象不是由Spring容器管理的Bean而是自己new出来的对象那Spring根本不会给它生成代理。比如在工具类里new一个Service或者在静态方法里手动new一个对象再调方法事务必然不生效。这相当于你根本没把助理的电话号码公布出去外人怎么可能通过助理找到你。第二如果目标方法不是public的事务也不会生效。Spring的Transactional底层依赖AOP而AOP对非public方法的支持是有限的。虽然有些高级配置看起来能处理非public方法但官方文档明确说Transactional应该放在public方法上。我建议你直接记住这个结论非public方法上的事务配置在大多数情况下都不会起作用而且不同版本之间行为还可能有差异属于典型的“看着没问题实际靠不住”的写法。第三如果类本身没有被Spring扫描到比如没有加Component、Service等注解或者包扫描路径没覆盖到那代理根本就不会生成。这一类属于配置层面的问题排查起来其实最简单但出问题的人也不少。2. 事务失效的几种常见原因与正确写法2.1 方法上的访问权限非public方法事务不生效这是一个非常经典的失效场景。先看一段错误示例Service public class OrderService { Transactional void createOrder(OrderDTO dto) { insertOrder(dto); updateStock(dto.getProductId(), dto.getQuantity()); } }这段代码里createOrder方法没有加public修饰符默认是包级可见。也就是说这个方法是“当前包内可以访问”但Spring的动态代理在默认配置下对非public方法的事务支持是打折扣的。实际效果就是方法正常执行但事务没有开启。如果执行到updateStock抛异常了前面的insertOrder也不会回滚。正确的写法是Service public class OrderService { Transactional public void createOrder(OrderDTO dto) { insertOrder(dto); updateStock(dto.getProductId(), dto.getQuantity()); } }这里多说一句不仅方法要是public被Transactional修饰的方法所在的类也必须是public的并且要能被Spring正常扫描到。类如果是package-private代理生成同样可能出问题。我见过有人在同一个包下测试没问题因为测试类和Service在同一个包调用方直接访问到了内部方法结果一到线上不同包就出问题这类问题特别容易让人误判。2.2 方法内部自调用最隐蔽的失效场景自调用是事务失效里最隐蔽、最容易踩坑的一类。很多人对代理机制有了解但写代码的时候一顺手就忘了。看下面这个例子Service public class OrderService { public void createOrderAndNotify(OrderDTO dto) { createOrder(dto); // 这里的调用不会走代理 notifyUser(dto.getUserId()); } Transactional public void createOrder(OrderDTO dto) { insertOrder(dto); updateStock(dto.getProductId(), dto.getQuantity()); } }createOrderAndNotify是一个普通方法内部直接调用了this.createOrder。虽然createOrder上有Transactional但因为这是同一个类内部的直接调用不会经过Spring的代理对象事务完全不会开启。调用方不会收到任何错误提示但数据库操作就是不在一个事务里一旦后面出了问题数据就会处于部分更新的状态。那问题来了如果一个方法既要对外提供事务能力又要在内部被复用该怎么处理有三种常见方案。第一种把事务方法拆到另一个Service里Service public class OrderService { private final OrderTransactionService orderTransactionService; public void createOrderAndNotify(OrderDTO dto) { orderTransactionService.createOrder(dto); notifyUser(dto.getUserId()); } } Service public class OrderTransactionService { Transactional public void createOrder(OrderDTO dto) { insertOrder(dto); updateStock(dto.getProductId(), dto.getQuantity()); } }第二种在当前类注入自身代理对象Spring循环依赖在Spring Boot 2.6以后默认关闭但这类写法仍然存在需要留意Service public class OrderService { // 注入自身代理 Autowired private OrderService self; public void createOrderAndNotify(OrderDTO dto) { self.createOrder(dto); notifyUser(dto.getUserId()); } Transactional public void createOrder(OrderDTO dto) { insertOrder(dto); updateStock(dto.getProductId(), dto.getQuantity()); } }第三种使用AopContext.currentProxy()获取当前代理对象但需要额外配置exposeProxy。我个人的习惯是第一优先拆Service因为这样职责更清晰代码看起来也更顺。自调用这个坑我建议让每个团队里的新人都知道它不像空指针那样立刻报错属于“安静地出错”危害更大。2.3 异常被吞掉try-catch之后事务静默失效这个场景在代码里特别常见尤其是接了第三方接口或者写复杂业务逻辑的时候很多人会在事务方法里做异常捕获结果把事务的回滚信号一起吞掉了。看例子Transactional public void payOrder(OrderDTO dto) { try { deductBalance(dto.getUserId(), dto.getAmount()); updateOrderStatus(dto.getOrderId(), PayStatus.PAID); } catch (Exception e) { log.error(支付失败, e); // 注意异常被捕获了没有重新抛出 } }这个写法的直接后果是如果deductBalance扣款成功但updateOrderStatus更新状态失败异常被catch住方法正常返回事务正常提交。用户的余额扣了订单还是待支付状态对账的时候就会差钱。解决思路有两条。第一条如果业务上确实需要捕获异常并做日志记录那么必须在catch块中重新抛出让事务感知到异常Transactional public void payOrder(OrderDTO dto) { try { deductBalance(dto.getUserId(), dto.getAmount()); updateOrderStatus(dto.getOrderId(), PayStatus.PAID); } catch (Exception e) { log.error(支付失败, e); throw new RuntimeException(支付失败, e); } }第二条把需要捕获异常的逻辑拆到事务方法之外事务方法内部只做会被事务保护的数据操作异常由外层的方法统一处理。比如Controller调用Service时在Controller层捕获异常做提示。我这里要说一个更细的点很多人以为只要抛异常就会回滚其实不然。Spring的事务回滚原则是默认只在运行时异常RuntimeException和错误Error时回滚受检异常Checked Exception默认不会触发回滚。所以如果你在catch块里throw了一个业务异常而那个业务异常继承的是Exception而不是RuntimeException那事务仍然不会回滚。这就是下面要说的另一个问题。2.4 异常类型不匹配checked exception默认不回滚看这个例子Transactional public void createOrder(OrderDTO dto) throws BusinessException { insertOrder(dto); updateStock(dto.getProductId(), dto.getQuantity()); if (stockNotEnough) { throw new BusinessException(库存不足); // 受检异常 } }假设BusinessException继承自Exception那么当它抛出时事务会正常提交。你没看错方法明明抛了异常但事务提交了。这在Spring的默认配置下就是这样的行为很多人在第一次遇到时都很难相信。解决方式是在Transactional中显式指定rollbackForTransactional(rollbackFor Exception.class) public void createOrder(OrderDTO dto) throws BusinessException { insertOrder(dto); updateStock(dto.getProductId(), dto.getQuantity()); if (stockNotEnough) { throw new BusinessException(库存不足); } }到这里我建议形成一个习惯所有Transactional注解都显式写上rollbackFor Exception.class。不要依赖默认行为因为默认行为太容易出边界问题。比如ArithmeticException是运行时异常默认会回滚但SQLException是受检异常默认不会回滚。你的方法里声明了throws SQLException数据库操作中途报错了自己处理的还行要是往上抛了事务却提交了那数据就全是脏的。下面这个表格把异常类型和默认回滚行为的对应关系列清楚方便对照异常类型例子默认是否回滚运行时异常RuntimeExceptionNullPointerException、IllegalArgumentException是错误ErrorOutOfMemoryError、StackOverflowError是受检异常Checked ExceptionIOException、SQLException、自定义BusinessException否受检异常且显式指定rollbackForTransactional(rollbackFor Exception.class)是2.5 类没有被Spring管理new出来的对象没有“魔法”很多人会在工具类或者工厂类里手动new一个Service然后调用它的方法期待事务生效。看下面这个典型错误public class OrderService { Transactional public void createOrder(OrderDTO dto) { insertOrder(dto); updateStock(dto.getProductId(), dto.getQuantity()); } } // 在使用的地方 OrderService orderService new OrderService(); orderService.createOrder(dto);OrderService类没有加Service注解Spring容器里根本不存在这个Bean的代理对象。代码里手动new出来的orderService就是普普通通的Java对象它的createOrder方法上的Transactional注解不会被任何机制解析事务自然不可能生效。虽然这个场景看起来太基础但我在真实项目里见过不止一次尤其是在老系统改造或者同事手写工具类时容易出现。还有一种情况是类本身被Spring管理但调用时通过静态方法获取实例比如自己写了一个静态工厂返回了一个非代理对象。这里面的坑在于Spring的依赖注入默认注入的就是代理对象但如果手动从ApplicationContext里拿Bean时做了类型转换或者绕过代理也可能导致事务失效。我的建议是代码中一律通过依赖注入来使用Service不要自己去new也不要在静态上下文里去“捞”实例。这样最稳。2.6 多线程调用事务传播不到子线程Spring事务默认是基于ThreadLocal实现的事务上下文绑定在当前线程上。如果你在事务方法里开了子线程在子线程里执行数据库操作那么子线程的执行不会加入到父线程的事务中。看这个例子Transactional public void batchProcess(ListOrderDTO orders) { orders.parallelStream().forEach(order - { updateOrder(order); // 这里在并行流子线程中执行 }); } private void updateOrder(OrderDTO order) { orderMapper.update(order); }这段代码存在两个层面的问题。一是parallelStream使用的是ForkJoinPool的公共线程池updateOrder的执行线程和batchProcess的主线程不是同一个事务上下文传递不过去。二是如果updateOrder内部还访问了ThreadLocal中的某些上下文参数子线程中也拿不到。所以并行流在处理事务方法时数据一致性是无法保证的。你需要自己去手动管理子线程的事务这也是为什么真正需要并发的场景里应该考虑用编程式事务而不是依赖Transactional的隐式事务。如果要让子线程参与事务正确的做法通常有两种。一种是子线程内部自己开启事务每个子线程处理自己的数据通过计数器保证整体进度失败时做补偿。另一种是不要让事务方法内部开线程而是先在一个线程里处理数据再通过消息队列异步处理后续流程这样每个流程都有自己独立的事务边界反而更容易保证数据一致。很多架构上追求异步化的团队其实都有意避免在事务方法里再开线程因为事务和异步本身是一对矛盾。2.7 数据库存储引擎层面的限制这个很多人会忽略属于“环境不给力”。Transactional要生效底层数据库必须支持事务。如果用的MySQL那表引擎必须是InnoDB而不能是MyISAM。MyISAM引擎不支持事务即使代码里加了TransactionalSQL执行出错后也没有回滚能力。诊断方法很简单SHOW TABLE STATUS LIKE your_table_name;看返回结果中的Engine字段如果是MyISAM就需要转换成InnoDB。转换命令ALTER TABLE your_table_name ENGINE InnoDB;还有一个常见问题是在同一个事务里访问了不支持事务的其他资源比如缓存、搜索引擎索引等。这类资源天然没有ACID的保证事务回滚不会把缓存数据也回滚掉。写代码时要把“数据库事务”和“分布式环境下多个数据源之间的一致性”分清楚前者可以靠Transactional解决后者需要引入分布式事务方案。很多人把一个普通事务方法里加了Redis操作就以为Redis的操作也能跟着一起回滚这是不对的。Redis的操作要么在事务提交后再执行要么通过其他机制做补偿。另外Spring的事务管理器和数据源要匹配。如果你配置了多数据源但Transactional没有指定对应的transactionManager事务可能管理的是另一个数据源看起来像是失效。这个问题在多数据源项目里比较典型后面会单独说。3. 事务传播机制与常见误用组合3.1 传播级别选了REQUIRES_NEW结果连不上Spring的Transactional有个propagation属性默认是Propagation.REQUIRED。REQUIRED的含义是如果当前没有事务就创建一个新事务如果当前已经有事务则加入当前事务。这个设置适合绝大多数场景。但有些人为了强制新开事务会设置Propagation.REQUIRES_NEW。它的行为是无论当前有没有事务都挂起当前事务新开一个独立事务。注意REQUIRES_NEW不会“加入”外部事务而是“另起炉灶”。外部事务发生异常回滚时REQUIRES_NEW里的操作已经独立提交了不会被回滚回调。换句话说REQUIRES_NEW内部执行成功的操作外部事务管不了。什么时候会用到REQUIRES_NEW典型场景是你希望某个操作不随着主事务的回滚而回滚。比如一个订单创建失败要回滚主数据但希望把失败原因记录到一张日志表里这时候写日志的操作可以用REQUIRES_NEW确保日志本身能成功写入。但如果不是这种明确的需求别随意使用REQUIRES_NEW因为它会打破“大事务包含小事务”的直观预期非常容易造成数据不一致。还需要注意一个点REQUIRES_NEW会独占数据库连接。如果一个方法里先开启了外部事务拿到了连接1又调用了REQUIRES_NEW方法Spring会再从连接池里拿连接2给新事务用。如果连接池最大连接数设置得不够大嵌套调用层级多了可能出现连接池耗尽的问题。现象就是系统运行一段时间后偶发超时查日志发现Waiting for connection timeout。3.2 noRollbackFor与rollbackFor的组合使用除了rollbackForSpring还提供了noRollbackFor参数用于指定哪些异常不触发回滚。这在一些特定的业务场景里是有用的比如你希望把某些异常视为“可接受的失败”不因为这类异常就把整个大事务回滚掉。举个例子Transactional(rollbackFor Exception.class, noRollbackFor BusinessWarnException.class) public void createOrder(OrderDTO dto) { insertOrder(dto); updateStock(dto.getProductId(), dto.getQuantity()); throw new BusinessWarnException(只是提示不滚); }这里BusinessWarnException虽然继承自RuntimeException但因为配置了noRollbackFor抛出后不会触发回滚。注意这里有个容易理解错的地方noRollbackFor不是“不抛异常”而是“抛异常但不回滚”。该方法最终还是会以异常结束事务提交调用方需要自行处理这个异常。我建议noRollbackFor的使用一定要克制它是典型的“知道的人多用得好的人少”的特性。如果一个团队里对这块没有明确约定尽量不要用。因为一旦外部事务嵌套调用了这个方法方法内部noRollbackFor不回滚但外部事务可能因为其他异常回滚两边行为不一致排查难度非常大。3.3 事务超时与锁等待不是失效但很像失效有时候你碰到的问题并不是事务完全没生效而是事务开启后因为超时或锁等待导致数据没有按预期回滚或提交。这类问题表现上和事务失效很像但根因完全不同。Spring的Transactional支持timeout属性单位是秒Transactional(timeout 5) public void heavyProcess() { // 执行超过5秒会抛出事务超时异常 }如果方法执行时间超过timeout设置Spring会让事务回滚并抛出异常。但在实际使用中timeout的语义在不同数据库和不同事务管理器下可能略有差异所以尽量不要依赖这个参数来兜底。它是给你主动设置一个保护上限的不是给你做精确控制的。锁等待的情况要更复杂一些。比如方法A更新了订单状态但事务迟迟没有提交持有行锁另一个线程的方法B想更新同一行就会一直等待。如果等待时间超过了数据库的innodb_lock_wait_timeout设置默认50秒B会抛出Lock wait timeout exceeded异常。这个异常会让B的事务回滚但A的事务可能还在继续。从B的视角看事务像是“失效”了明明加了Transactional异常也抛了但就是没成功。真实原因是锁竞争不是事务配置问题。遇到这类问题排查方向是先看数据库的锁等待情况而不是怀疑Spring配置。MySQL里可以用SELECT * FROM information_schema.innodb_trx; SELECT * FROM information_schema.innodb_lock_waits;查看当前事务和锁等待的具体情况。我见过一些团队为了排查事务问题把所有Transactional配置翻了个遍最后发现是慢SQL导致的长事务大量的锁等待把连接池拖垮了。所以排查事务问题时数据库层面的诊断和代码层面的检查要同步进行。事务的隔离级别也会影响锁的行为。MySQL默认的隔离级别是REPEATABLE READ事务内多次读取同一数据结果一致。如果某些业务场景希望读到其他事务已提交的数据可以配置Transactional(isolation Isolation.READ_COMMITTED)。但隔离级别的调整要谨慎它直接影响并发场景下的一致性和性能不建议在不熟悉的情况下频繁切换。4. 事务失效的排查思路与工具4.1 开启Spring事务日志遇到事务疑似失效时第一个反应不应该是猜而是看日志。Spring提供了事务管理器的日志输出可以在配置文件中开启logging: level: org.springframework.jdbc.datasource.DataSourceTransactionManager: DEBUG org.springframework.transaction: DEBUG开启之后运行时会在日志里看到这类信息Using transaction object [org.springframework.jdbc.datasource.ConnectionHolder] Creating new transaction with name [com.example.service.OrderService.createOrder]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT Initiating transaction commit Initiating transaction rollback如果方法上加了Transactional但日志里根本没有Creating new transaction说明代理压根没有生效优先检查这个类是否被Spring管理、方法是否是public。如果看到了Creating new transaction但没有看到transaction commit或rollback说明事务可能已经超时或者事务在另一个线程里执行日志没有打印完整。日志排查是第一步先确认事务到底有没有开启再去看为什么不回滚。很多人在代码层面反复修改结果问题根本不在那里日志一开就明白了。4.2 从日志里识别事务代理是否生效日志里出现的字面信息很有讲究。如果日志出现了Creating new transaction说明本次调用确实进入了代理的事务拦截逻辑事务创建成功。如果日志里没有这行信息但同时类上有Transactional注解那基本可以判断问题出在调用方式上比如自调用或new对象。如果创建了事务方法抛了异常但日志里没有Initiating transaction rollback那就要看抛的异常类型是否满足回滚条件。比如抛了一个受检异常且注解上没有配rollbackFor那么日志里就只会有Initiating transaction commit这就能解释为什么事务“失效”了。这种场景下日志是能直接给出答案的。这里给个实际建议排查事务问题时不要一上来就看业务代码先开日志跑一遍把“事务是否创建”和“事务是否回滚”这两个关键信息确认了再回头看代码。这样能省下一大半时间也避免在错误的方向上反复纠结。4.3 快速定位事务失效检查清单我把这些年遇到的事务失效场景整理成了一份检查清单按照优先级从高到低排列排查时按顺序过一遍序号检查项确认方法1方法是否为public看代码修饰符2类是否被Spring管理是否有Service/Component是否能被扫描到3是否通过代理调用不是自调用检查调用方是否通过注入的Bean调用4异常是否被try-catch吞掉看方法内是否有catch后未重新抛出5异常类型是否满足回滚条件检查rollbackFor配置异常是否RuntimeException6事务方法是否在子线程中执行看是否使用了线程池或并行流7数据库是否支持事务检查MySQL表引擎是否InnoDB8事务管理器是否匹配数据源多数据源场景下检查transactionManager9是否配置了传播级别导致每次新开事务检查propagation属性的实际配置10数据库有无锁等待或超时查询information_schema里的事务和锁等待这套清单在我自己排查问题时反复使用基本能覆盖九成以上的事务失效场景。如果你按顺序过一遍还没解决那大概率不是“失效”而是业务逻辑本身的问题比如补偿逻辑缺失、缓存不一致等建议往这个方向继续排查。5. 实测一个容易踩坑的完整案例5.1 场景复现模拟一个常见的电商场景用户下订单需要扣减库存、插入订单记录、给用户加积分。三个操作需要保证原子性任何一个失败都要全部回滚。最初的代码如下Service public class OrderServiceImpl implements OrderService { Autowired private OrderMapper orderMapper; Autowired private StockMapper stockMapper; Autowired private PointMapper pointMapper; Override Transactional public void createOrder(OrderDTO orderDTO) { orderMapper.insert(orderDTO); stockMapper.deduct(orderDTO.getProductId(), orderDTO.getQuantity()); pointMapper.add(orderDTO.getUserId(), orderDTO.getAmount()); } }如果Controller层直接注入OrderService接口走的也是代理对象这个代码看起来没有大问题。但如果把调用方式变成自调用或异常被吞问题就会出现。为了完整演示这个排查流程我在下面把几个典型问题放在同一个案例里模拟。5.2 逐步排查与解决第一轮日志排查。开启事务日志后发现日志中根本没有Creating new transaction说明事务没有创建。检查发现调用方在同一个Service内部调用了createOrder方法Service public class OrderServiceImpl implements OrderService { Override public void createOrderWithCheck(OrderDTO orderDTO) { createOrder(orderDTO); // 自调用 } Override Transactional public void createOrder(OrderDTO orderDTO) { orderMapper.insert(orderDTO); stockMapper.deduct(orderDTO.getProductId(), orderDTO.getQuantity()); pointMapper.add(orderDTO.getUserId(), orderDTO.getAmount()); } }createOrderWithCheck是一个普通方法它内部直接调用了this.createOrder所以Transactional没有生效。这个场景的修复方式前面已经提过最好把三个数据操作抽到一个独立的Bean里避免自调用。第二轮异常类型排查。假设自调用问题修复后日志中出现了Creating new transaction但抛异常后仍然只看到transaction commit。检查代码发现业务方法里抛的是自定义的BizException这个异常继承的是Exceptionpublic class BizException extends Exception { public BizException(String message) { super(message); } }由于默认不回滚受检异常事务就提交了。修复方式就是把Transactional改成Transactional(rollbackFor Exception.class)或者把BizException改成继承RuntimeException。我个人倾向于两个都做既让异常继承RuntimeException又显式配置rollbackFor双保险。第三轮并发排查。还有一个隐藏问题在createOrderWithCheck里如果对用户下单做了并发控制比如用分布式锁或者乐观锁但这些锁在事务提交前就被释放了可能导致另一个线程读到旧数据。这虽然不是事务失效但和事务的提交时机强相关容易被误判。这类问题的排查方向是确认锁的释放是在事务提交之后Transactional public void createOrder(OrderDTO orderDTO) { // 业务操作 orderMapper.insert(orderDTO); stockMapper.deduct(orderDTO.getProductId(), orderDTO.getQuantity()); pointMapper.add(orderDTO.getUserId(), orderDTO.getAmount()); // 不要把锁释放放在这里事务还没提交 }锁的释放应该由事务拦截器在事务提交后统一处理或者使用Spring的TransactionSynchronizationManager注册回调。5.3 最终代码综合上面的排查最终推荐的写法是Service public class OrderServiceImpl implements OrderService { Autowired private OrderMapper orderMapper; Autowired private StockMapper stockMapper; Autowired private PointMapper pointMapper; Override Transactional(rollbackFor Exception.class) public void createOrder(OrderDTO orderDTO) { orderMapper.insert(orderDTO); stockMapper.deduct(orderDTO.getProductId(), orderDTO.getQuantity()); pointMapper.add(orderDTO.getUserId(), orderDTO.getAmount()); } }外部如果有校验逻辑把校验逻辑放在独立方法里或者放在调用的Controller层不要放在同一个Bean里产生自调用。如果需要记录失败原因并且不希望被回滚影响再把记录操作拆到另一个独立Service里用REQUIRES_NEW或者干脆在事务提交后再处理。这套代码写完之后开启事务日志再跑一遍应该能看到完整的Creating new transaction、Initiating transaction commit/rollback这三类日志整个链路就稳定了。如果你也遇到了事务不生效的问题建议先把这份检查清单存下来遇到问题按顺序过基本上都能快速找到原因。踩过几次坑之后你会发现事务失效并不神秘它背后永远只有一个问题Spring的代理到底有没有起作用。