Spring事务失效全解析:动态代理、自调用与异常回滚实践

Spring事务失效全解析:动态代理、自调用与异常回滚实践 Spring事务失效大概是被问烂了但每次排查线上问题还是会踩坑。前两天刚帮同事查一个数据不一致的问题查了半天发现又是事务没生效代码写得花团锦簇结果一个坑都没躲过去。回头想想很多事务失效的场景其实都是可以提前规避的关键是得知道它为什么会失效而不是死记硬背那些失效清单。这篇文章我就从自己实际踩坑和排查的经验出发把Spring事务失效这个话题拆开揉碎讲清楚。不管是刚接触Spring的初级开发还是做了几年需要系统梳理这块知识的老手相信都能从里面找到自己想要的东西。我会结合真实的业务场景、源码层面的分析和具体的排查工具尽量把这个问题讲透。1. 事务失效问题的本质先从动态代理说起不谈底层原理就聊事务失效基本等于耍流氓。要弄清楚事务为什么会失效必须先理解Spring事务机制的核心实现AOP动态代理。1.1 Spring事务的底层实现机制Spring声明式事务基于AOP实现说白了就是你写了一个加了Transactional注解的方法Spring在启动时会为这个Bean生成一个代理对象。调用代理对象的方法时代理会先开启事务、执行目标方法、然后提交或回滚事务。这个机制里最核心的一点是事务的控制权在代理对象手里而不是在目标对象手里。你通过Spring容器拿到的Bean本质上都是代理对象而不是原始对象。我举个例子帮大家理解。想象你去餐厅吃饭你打电话订餐调用方法接电话的是前台代理对象前台记录你的需求后通知后厨做菜目标方法。如果后厨做菜出了问题需要退单起作用的是前台因为你联系的是前台而不是后厨。同理事务的开启、提交、回滚都是在代理层完成的。所以只有当你调用的方法经过了代理对象事务才能生效。这就是理解所有事务失效场景的核心钥匙。1.2 理解JDK动态代理和CGLIB的区别Spring生成代理对象有两种方式这个选择会直接影响事务在特定场景下是否生效。JDK动态代理要求目标类必须实现接口它生成的代理类也实现同样的接口通过反射调用目标类的方法。CGLIB则是通过生成目标类的子类来创建代理不需要接口支持。这里藏着一个经典的事务失效场景如果目标类没有实现接口Spring默认使用CGLIB。如果类实现接口Spring会优先使用JDK动态代理除非配置了proxyTargetClass true。在Spring Boot 2.x及以上版本中默认使用CGLIB代理。但如果用的还是老项目或者老配置就要特别小心接口代理的场景。比如Transactional加在接口上JDK动态代理时是可以生效的但CGLIB代理下就有可能出现问题特别是配置了EnableTransactionManagement(proxyTargetClass false)时某些方法签名不被正确代理事务就悄悄失效了。不过说句实话日常开发中我建议直接把Transactional写在实现类的方法上别写在接口上这样最保险避免代理方式带来的各种隐藏问题。2. 事务失效的常见业务场景拆解这部分我结合自己实际开发中遇到的问题把事务失效的场景分为几大类每一类都有对应的代码示例和原理解析方便大家对照排查。2.1 方法自调用引发的事务失效这是面试最喜欢问、业务代码里也最常出现的问题。看一个典型的例子Service public class OrderService { public void createOrder(OrderDTO orderDTO) { // 做一些订单创建前的校验 validateOrder(orderDTO); // 保存订单 saveOrder(orderDTO); } Transactional public void saveOrder(OrderDTO orderDTO) { orderMapper.insert(orderDTO); throw new RuntimeException(模拟异常); } }这个场景中createOrder方法没有加事务但它内部调用了加事务的saveOrder方法。你觉得saveOrder的事务会生效吗答案是不会。原因很简单createOrder方法执行时this.saveOrder()中的this指向的是原始对象不是代理对象。事务拦截器根本没机会插入到这次调用中。我在真实的项目里踩过一次特别隐蔽的坑。当时有个定时任务调了一个handleExpiredOrders()方法这个方法内部通过this调用了几个加Transactional的私有方法。代码逻辑上看起来没问题结果统计数据经常对不上。排查到最后才发现整个事务压根就没生效每个私有的数据库操作都是独立提交的中间过程一旦报错前面已经提交的数据就回滚不了了。解决的方案有几种把事务方法拆到另一个Service类中通过注入的对象调用自己注入自己Autowired private OrderService orderService用注入的代理对象调用事务方法借助AopContext.currentProxy()拿到当前代理对象调用Service public class OrderService { Autowired private OrderService orderService; public void createOrder(OrderDTO orderDTO) { validateOrder(orderDTO); // 关键点通过代理对象调用事务才能生效 orderService.saveOrder(orderDTO); } Transactional public void saveOrder(OrderDTO orderDTO) { orderMapper.insert(orderDTO); throw new RuntimeException(模拟异常); } }需要额外注意Autowired注入自己这种方式在循环依赖检测上可能会误报最好配合Lazy注解一起使用。或者直接用Lazy Autowired注入自己实测最稳。还有一种更彻底的方式把事务边界上移直接在createOrder上加Transactional这样整个方法体都在一个事务里。但需要评估业务上是否合理因为事务范围太大会增加锁持有时间高并发场景下容易引发性能问题。2.2 final、private、static方法导致事务不生效这三个修饰符导致的事务失效原理上是一回事代理无法覆盖这些方法。先看private方法Transactional private void saveOrder(OrderDTO orderDTO) { orderMapper.insert(orderDTO); throw new RuntimeException(模拟异常); }Spring在生成CGLIB子类时private方法是不会被覆盖的因为子类根本无法继承private方法。JDK动态代理同样处理不了接口里的方法肯定都是public的。所以**Transactional放在private方法上等于白写**编译器不报错Spring也不报错但事务就是不生效。再看final方法Transactional public final void saveOrder(OrderDTO orderDTO) { orderMapper.insert(orderDTO); throw new RuntimeException(模拟异常); }如果Spring使用CGLIB代理本质上是通过继承目标类生成子类来增强方法。final方法不能被子类重写代理类就无法拦截这个方法事务同样失效。JDK动态代理不受final影响但如果是CGLIB场景就必须注意。static方法就不用多说了静态方法属于类本身而不是实例完全不在Spring的代理管辖范围内加上Transactional也毫无意义。这里我想多说一句这些场景中Spring启动时都不会报错导致很多开发者根本感觉不到问题直到线上出现数据不一致的bug才追悔莫及。所以大家写代码的时候要养成一个习惯——加了Transactional的方法检查一下修饰符是不是public非final非static。虽然Spring官方文档说Transactional可以用于protected或private方法用AspectJ模式时可以但默认的代理模式下这些修饰符都会让事务失效。2.3 异常被捕获后事务没有回滚这个场景可能是业务代码中最容易出问题的也是最难排查的。看下面这段代码Transactional public void createOrder(OrderDTO orderDTO) { try { orderMapper.insert(orderDTO); integrationService.callStockApi(orderDTO.getSkuId()); integrationService.callCouponApi(orderDTO.getCouponId()); } catch (Exception e) { log.error(创建订单失败, e); // 没有把异常继续往外抛 } }这段代码的事务会生效吗事务会开启但不会回滚。因为异常被捕获后Spring事务拦截器看到的执行结果就是“方法正常返回了”它就认为业务操作成功直接提交事务。这可以说是最常见的业务bug。经常出现在这种场景某个操作中需要调用多个外部系统开发人员想着自己捕获所有异常做好日志记录结果把事务回滚的机制也一起“捕获”掉了。我见过一个真实的线上事故一个批量导入功能循环处理几千条数据每条数据处理都有try-catch包裹说是要保证单条数据失败不影响整体导入。结果某次发布后用户反馈很多数据导入不完整数据库里出现了大量“孤儿数据”。原因就是每条数据的写操作和关联写操作没有在本地事务里前面的写成功了后面的写失败被吞掉了数据就断了。这类问题的解决思路有几种事务方法内不捕获异常让异常自然抛出由Spring事务拦截器统一处理回滚如果确实需要捕获异常做一些日志或通知操作捕获后重新抛出RuntimeException如果某些异常需要在方法内消化处理可以用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()手动标记回滚Transactional public void createOrder(OrderDTO orderDTO) { try { orderMapper.insert(orderDTO); integrationService.callStockApi(orderDTO.getSkuId()); } catch (BusinessException e) { log.error(创建订单失败, e); // 手动标记需要回滚 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); // 但要注意这里标记了回滚后续如果还有其他数据库操作都会跟着一起回滚 } }需要注意setRollbackOnly()这个方式比较暴力它会把当前事务标记为只可回滚即使后续代码继续执行了写操作最终这个事务也会回滚。所以用的时候要想清楚业务语义别把不想回滚的数据也一起回滚了。2.4 非RuntimeException异常导致事务不回滚Spring事务默认情况下只对RuntimeException和Error进行回滚对于受检异常checked exception默认是不回滚的因为Spring认为受检异常是业务可预期的比如库存不足、余额不够之类的业务规则异常不希望因为这类异常把前面所有的数据库操作都回滚。看这个例子Transactional public void createOrder(OrderDTO orderDTO) throws IOException { orderMapper.insert(orderDTO); // 调用外部接口失败抛出受检异常 integrationService.callCouponApi(orderDTO.getCouponId()); }假设callCouponApi抛出IOException这段代码里数据插入操作不会回滚订单数据已经提交到数据库了但优惠券并没有发放成功这就会导致严重的业务数据不一致。解决方案就是在Transactional注解上明确指定需要回滚的异常类型Transactional(rollbackFor Exception.class) public void createOrder(OrderDTO orderDTO) throws IOException { orderMapper.insert(orderDTO); integrationService.callCouponApi(orderDTO.getCouponId()); }这里还有个容易忽略的细节。有些开发人员习惯用Transactional(rollbackFor IOException.class)这种方式指定特定异常回滚但在业务代码里如果底层抛出的实际异常和配置的不完全一致比如被包装成RuntimeException的嵌套异常或者自定义异常继承了其他异常类型配置就不生效了。我之前遇到过一个案例同事写了一个Transactional(rollbackFor BizException.class)但代码里实际抛出的是BizException的父类ServiceException结果事务没有回滚。排查的时候翻了半天才在异常继承关系上发现问题。这里给一个经验值如果吃不准到底会抛什么异常直接写rollbackFor Exception.class最稳妥虽然粒度粗一点但至少不会漏掉该回滚的场景。2.5 事务传播行为配置错误导致失效或异常Spring事务的传播行为是事务管理的一个关键维度它定义了当一个事务方法被另一个事务方法调用时事务如何扩展。如果对传播行为理解不到位配置错误会引发各种奇怪的问题。默认的传播行为REQUIRED表示如果当前存在事务则加入当前事务如果当前不存在事务则新建一个事务。这个默认值在大多数场景下是没问题的。真正容易出问题的是REQUIRES_NEW的误用Service public class OrderService { Transactional public void createOrder(OrderDTO orderDTO) { orderMapper.insert(orderDTO); // 想开启一个新事务记录日志 logService.saveLog(orderDTO); throw new RuntimeException(订单创建失败); } } Service public class LogService { Transactional(propagation Propagation.REQUIRES_NEW) public void saveLog(OrderDTO orderDTO) { logMapper.insert(orderDTO); } }这段代码中createOrder方法最后抛了异常理论上订单数据应该回滚。问题是saveLog方法开启了REQUIRES_NEW传播行为它是一个独立的新事务不受外层事务影响。外层事务回滚了但日志记录已经提交了。这个场景本身不算“事务失效”因为日志这个独立事务确实是按照REQUIRES_NEW的语义执行了。但如果开发人员写这段代码的本意是希望日志跟着一起回滚那就变成了一种“逻辑上的失效”。更麻烦的是嵌套传播行为的组合错误。比如REQUIRES_NEW内层事务成功提交后外层事务回滚了内层已经提交的数据就变成了脏数据。这种问题的修复成本通常很高因为不光要改代码还要人工去处理那些脏数据。实际开发中对于需要独立提交的操作比如操作日志、审计日志、消息发送记录等使用REQUIRES_NEW确实是合理的。但一定要明确这个事务边界背后代表的业务语义这段数据不随主事务一起回滚。如果这个认知不清晰后面早晚踩坑。2.6 多线程环境下的事务失效这个场景在面试中经常出现实际项目中也越来越普遍。Transactional public void batchProcess(ListOrderDTO orderList) { orderList.parallelStream().forEach(order - { processOrder(order); }); } Transactional public void processOrder(OrderDTO order) { orderMapper.insert(order); }代码的逻辑很清晰批量订单处理并行处理每个订单。你以为每个processOrder方法都有自己的事务实际上完全不是这样。Spring事务和线程是绑定的或者说事务上下文是通过ThreadLocal传递的。parallelStream()底层使用ForkJoinPool中的线程来执行任务子线程中根本没有主线程的事务上下文。子线程中调用processOrder方法时整个事务机制等于失效了每个数据库操作都是自动提交的。类似的问题在Async注解异步方法中也经常出现Async Transactional public void asyncProcess(OrderDTO order) { orderMapper.insert(order); }异步线程池里的线程也没有主线程的事务上下文。如果异步方法内部有多个数据库操作其中一个失败其他的也不会回滚。多线程事务是一个很复杂的话题我建议如果在真实业务中遇到需要在多个线程中维护数据一致性的场景不要指望通过简单的Spring事务注解来解决。可以考虑将主线程的数据库操作改为同步执行异步只用于处理不涉及数据强一致性的逻辑使用编程式事务TransactionTemplate在子线程中手动管理事务引入分布式事务方案如Seata或在业务层面做补偿机制我在实际项目中遇到过最典型的是一个对账任务主线程遍历了所有的对账日期然后丢到线程池里并行核对。每个核对任务里都有好几个数据库更新操作里面加了Transactional。线上跑了好几个月都没发现问题直到某一天银行侧的接口超时有一张表的更新失败了结果前面已经更新的两张表数据没有回滚对账结果和银行侧差了三分钱。排查的过程非常痛苦最后定位到是多线程事务失效。2.7 数据库引擎与事务支持的限制框架层面已经没问题了但如果数据库本身不支持事务那上面所有的努力都是白费的。最常见的就是MySQL的MyISAM存储引擎。MyISAM是不支持事务的偏偏有些老项目还在用。Spring的Transactional注解在MyISAM表上执行时不会报任何错误因为MyISAM会“假装”接受这些事务控制指令但实际上根本没有回滚能力。怎么排查这类问题直接查询表的存储引擎SHOW TABLE STATUS LIKE order;看Engine字段如果是MyISAM那这个表上的事务就是无效的。解决方案就是修改存储引擎ALTER TABLE order ENGINE InnoDB;除了存储引擎数据库驱动不匹配也会导致事务失效。比如项目中配置了MySQL驱动但实际连接的数据库是PostgreSQL或Oracle多见于测试环境或自建数据库资源池。某些驱动版本对事务支持的默认行为可能不同。当然这只是理论上可能正常配置下不会出现这种情况。还有一个看起来不常见但确实存在的坑数据库连接关闭或初始化失败被静默吞掉。比如连接池配置的initialSize过大但数据库最大连接数限制了导致部分连接创建失败。Spring开启事务时如果拿到的连接是无效的就会产生诡异的现象——有些操作有事务有些操作没事务。这类问题排查时最直接的办法是看日志关注数据库连接池相关的错误信息。实在不行就把连接池参数调小再试看问题是否消失。2.8 Bean未被Spring容器管理这个场景说起来有点基础但在真实的代码中还是会遇到。public class OrderService { Transactional public void createOrder() { orderMapper.insert(order); } }上面这个类没有加Service、Component等注解也没有通过其他方式注册为Spring Bean。这种情况下Spring根本不知道这个类的存在自然也无法为它创建代理对象事务必然不生效。还有一种情况比较隐蔽——自己手动new出来的对象OrderService orderService new OrderService(); orderService.createOrder();即使OrderService被Spring管理了但这里你绕过Spring容器直接new实例拿到的是原始对象不是代理对象事务照样失效。我建议大家在日常开发中统一维护一个原则所有涉及数据库事务的操作类都交给Spring容器管理不要手动new。这个习惯能规避掉很大一部分低级的bug。3. 结合源码定位事务失效的根因前面罗列了这么多场景大家可能已经有点晕了。其实排查事务失效核心是靠对源码的理解去定位问题。我带大家简单过一遍Spring事务的核心源码路径。3.1 关键源码类与方法解析Spring事务的核心源码逻辑主要在TransactionInterceptor和AbstractPlatformTransactionManager中。TransactionInterceptor实现了MethodInterceptor接口它的invoke方法就是代理拦截的入口。核心代码如下public Object invoke(MethodInvocation invocation) throws Throwable { Class? targetClass AopUtils.getTargetClass(invocation.getThis()); // 拿到事务属性比如Transactional注解的配置 TransactionAttribute txAttr this.transactionAttributeSource.getTransactionAttribute(method, targetClass); // 拿到事务管理器 PlatformTransactionManager tm determineTransactionManager(txAttr); // 创建事务信息 TransactionInfo txInfo createTransactionIfNecessary(tm, txAttr, joinpointIdentification); Object retVal null; try { // 执行目标方法 retVal invocation.proceed(); } catch (Throwable ex) { // 异常时判断是否需要回滚如果需要则执行回滚 completeTransactionAfterThrowing(txInfo, ex); throw ex; } finally { cleanupTransactionInfo(txInfo); } // 正常返回提交事务 commitTransactionAfterReturning(txInfo); return retVal; }这段代码的精髓在于拦截器把业务方法夹在事务开启和事务提交/回滚之间。如果业务方法根本没有经过这个拦截器那事务自然不生效。继续看回滚的关键逻辑在AbstractPlatformTransactionManager的processRollback方法中private void processRollback(DefaultTransactionStatus status, boolean unexpected) { try { // 是否设置了只回滚标记 if (status.isLocalRollbackOnly()) { // 执行实际回滚 doRollback(status); } } catch (RuntimeException ex) { throw ex; } }这里值得一提的是TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()这个手动标记回滚的逻辑也是通过设置LocalRollbackOnly标志来实现的。还有一点容易被忽略的是TransactionAspectSupport中的completeTransactionAfterThrowing方法protected void completeTransactionAfterThrowing(TransactionInfo txInfo, Throwable ex) { if (txInfo ! null txInfo.getTransactionStatus() ! null) { // 关键逻辑在这里 if (txInfo.transactionAttribute.rollbackOn(ex)) { txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus()); } else { // 不需要回滚提交 txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); } } }看到没有默认情况下Spring要调用transactionAttribute.rollbackOn(ex)判断当前异常是否需要回滚。这个判断逻辑在DefaultTransactionAttribute中public boolean rollbackOn(Throwable ex) { return (ex instanceof RuntimeException || ex instanceof Error); }所以默认只对RuntimeException和Error回滚就一目了然了。3.2 通过断点走查事务失效过程如果代码已经跑起来了但发现事务没有生效最快的排查方式就是直接在TransactionInterceptor.invoke方法上打一个断点看你的业务方法有没有走到这里。以自调用场景为例在saveOrder方法内部调用另一个事务方法时你在断点处观察当前的this对象是什么类型。如果是OrderService$$EnhancerBySpringCGLIB$$xxxxxx这样的类是代理对象说明当前执行的是代理对象的方法事务有机会生效。如果显示的是OrderService原始类型那就要特别注意了这肯定不是通过代理调用的事务方法。再用异常吞掉的场景举例断点在completeTransactionAfterThrowing方法中如果异常从业务方法抛出但被异常处理器吞掉了你永远不会走到completeTransactionAfterThrowing方法会正常走commitTransactionAfterReturning事务就提交了。这种断点走查法虽然慢一点但确实是最直观、最容易定位问题的方式。我在平时帮团队成员排查问题时用得最多的就是这种方法。4. 事务失效问题排查手册与实战思路理论讲了这么多最后给大家整理一个实际排查事务失效问题的完整思路和步骤配合一些场景化的经验希望能作为一份可以直接用的排查手册。4.1 从线上问题定位到根因的五步排查法第一步确认问题表象。明确是数据不一致、部分成功部分失败还是应该回滚却没有回滚。把问题时序、操作步骤、异常日志等关键信息完整梳理出来。第二步确认Spring事务框架配置。检查EnableTransactionManagement是否存在Spring Boot中可以通过spring-boot-autoconfigure自动配置事务管理器是否正常Transactional注解写的位置是否正确。第三步检查目标方法和调用方式。方法修饰符是否public、是否被final/static修饰调用方是否通过注入的Bean调用是否存在自调用情况方法是否被同一个类中的其他方法调用。第四步检查异常处理路径。方法内部是否捕获了异常、异常类型是RuntimeException还是受检异常、rollbackFor配置是否正确。第五步检查数据库和连接层。确认数据库引擎支持事务检查数据源配置是否正确排除多数据源场景下事务管理器绑定错误。4.2 业务场景与失效模式的对照速查表业务场景失效模式排查要点一个方法调用同类的另一个事务方法事务完全不生效检查是否自调用方法内部try-catch吞掉异常事务不回滚检查异常是否被吞抛出的是受检异常如IOException事务不回滚检查rollbackFor配置方法被final/private/static修饰事务不生效检查方法修饰符多个线程并行处理数据每个线程里的操作独立提交检查是否跨线程传播事务方法所在的类没被Spring管理事务不生效检查类扫描路径或注解数据库表使用MyISAM引擎没有任何回滚能力检查表存储引擎方法直接new出来调用事务不生效检查是否通过容器获取Bean同一类中A事务方法调用B事务方法B方法事务可能不生效检查代理对象是否是当前对象事务传播行为设置为NOT_SUPPORTED外层事务被挂起操作独立提交检查传播行为配置4.3 自测清单避免事务失效问题流入生产环境结合我在团队里的经验每次涉及事务代码的Code Review我都要求开发人员对照下面这个清单自查一遍Transactional注解是否写在public方法上这个方法是否是final/private修饰rollbackFor是否指定了正确的异常类型不确定时是否写了Exception.class方法内部是否有try-catch吞异常的情况捕获后的处理方式是什么是否通过this调用了同类中的事务方法事务方法所在的类是否被Spring容器管理涉及多线程的操作是否确认了子线程中事务不生效的问题方法中是否有远程调用、外部接口调用这些调用失败会导致本地事务回滚吗是否有跨数据源操作如果有确认了事务管理器绑定的是哪个数据源吗方法的传播行为配置是否符合业务预期这个清单看起来简单但每一项背后都对应着一个或多个真实踩过的坑。每次Review都过一遍能有效拦截掉绝大多数事务失效的代码。5. 六个容易被忽略的事务进阶注意点最后再补充几个我在实战中总结的进阶注意点这些不属于严格意义上的“事务失效”但如果不注意很容易造成类似事务失效的错觉。5.1 大事务与长事务的性能隐患事务范围太大数据库连接和锁的持有时间过长在高并发场景下会导致大量线程阻塞甚至连接池被打满。一个小经验值单个事务中尽量不要包含超过几百毫秒的外部调用尤其是远程接口、消息发送、文件读写这类操作。如果你发现事务方法里有外部接口调用最好先想想业务上是否可以把外部调用移到事务外面。如果确实需要在事务内调用要评估好调用超时和降级方案。5.2 分布式事务与本地事务的边界微服务架构下分布式事务是绕不开的话题。有些人把Transactional直接加到了涉及多个微服务调用的方法上以为这样能保证跨服务的数据一致性这是典型的误区。Transactional只能保证本地事务的一致性也就是一个事务管理器管理的一个数据源。跨服务的调用要通过分布式事务方案如Seata的AT模式、TCC模式或者最终一致性方案如本地消息表消息队列来保证。我在实际项目中见过的最典型的错误是一个下单方法先本地插入订单再调用库存服务的接口扣减库存最后调用用户服务的接口扣减余额。开发人员在方法上加了Transactional以为这样整个下单流程就能保持一致。结果库存接口超时导致异常本地订单回滚了但库存服务的扣减是独立事务根本不会跟着回滚。最终用户订单没有创建成功库存却少了。这个问题的根源就是把分布式事务的边界错误地理解成了本地事务的范畴。5.3 事务与锁的配合使用有些人觉得有了事务就万事大吉忽略了并发场景下的锁问题。看这个场景Transactional public synchronized void deductStock(Long skuId, Integer count) { Stock stock stockMapper.selectBySkuId(skuId); if (stock.getCount() count) { throw new RuntimeException(库存不足); } stockMapper.deduct(skuId, count); }方法加了synchronized事务加了Transactional感觉万无一失。但synchronized锁的对象是当前实例而分布式部署下多个实例各有各的锁根本锁不住。另外synchronized释放锁是在方法退出时而事务提交发生在方法退出之前还是之后Spring事务拦截器是在方法执行完成后提交事务的如果你在synchronized同步块中操作数据事务提交时锁已经释放了另一个线程仍然可能读到未提交的数据。更稳妥的方式是用数据库层面的乐观锁或悲观锁来保证并发安全而不是依赖synchronized。5.4 事务方法中调用自身与AOP增强的边界之前我们讲过自调用会让事务失效但这里补充一个进阶场景如果事务方法内部调用了另一个被AOP增强的方法但使用的是原始对象调用那另一个方法的所有AOP增强都不会生效不只是事务。比如某个方法上加了自定义的性能监控AOP逻辑但它在同类中的其他方法里通过this被调用那性能监控就不会记录。这个问题的排查思路和事务失效是一样的确保所有需要AOP增强的方法调用都通过代理对象。5.5Transactional注解加在接口上的隐患对于老项目或者使用了JDK动态代理的项目有人习惯把Transactional注解写在接口方法上。这个习惯在新项目中不太建议。原因有两点第一如果代理方式切换为CGLIBSpring Boot 2.x默认事务可以拿到实现类上的注解但如果实现类没有覆盖接口方法上的注解某些场景下会读取不到第二接口上的注解语义容易被实现类覆盖造成理解偏差。统一建议把Transactional写在实现类的方法上保证代理能准确读取到事务配置。5.6 事务超时设置的必要性默认情况下Spring事务没有默认超时时间或者根据数据库驱动配置来决定不同驱动默认值不同。如果某个事务方法执行时间过长可能会把持数据库连接很久引发连接池耗尽的问题。建议对长事务方法显式配置超时时间Transactional(timeout 5) public void batchProcess(ListOrderDTO orderList) { // 超过5秒自动抛出异常并回滚 }这个参数的单位是秒根据自己的业务响应时间要求来合理设置。设置太短会导致正常业务被误杀设置太长又起不到保护作用。我个人在实际操作中的体会是事务问题大部分不是“会不会”的问题而是“踩没踩过”的问题。很多失效场景在你没遇到过之前你根本不知道它藏在哪个角落。也正因如此把这些常见失效模式整理出来作为日常开发中的自检清单比临时抱佛脚去排查更有效。最后再分享一个小技巧如果怀疑线上某个方法的事务有问题又不好直接打断点可以在方法入口打一条日志调用TransactionSynchronizationManager.isActualTransactionActive()把当前是否在事务中打出来。有了这个明确的状态输出你就能从各种代码逻辑的迷雾里脱身出来快速定位问题到底出在哪一层。