news 2026/9/15 23:10:00

Spring事务失效的底层原理与排查实战:从代理机制到异常处理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring事务失效的底层原理与排查实战:从代理机制到异常处理

1. 从一次线上事故说起:事务到底什么时候会失效

如果你写过几年Java后端,大概率遇到过这样的场景:明明在Service方法上加了@Transactional注解,代码也层层检查了好几遍,结果数据该回滚的没回滚,一条错误记录就这么安安静静地落进了数据库。等你回头排查,发现不是注解没加,就是异常被吞了,再不然就是方法被this调用了——然后一拍大腿:原来是事务根本没生效。

我去年接手过一个对账系统,里面有一段批量更新账户余额的逻辑,外层方法标了@Transactional,结果某个批次中途抛了业务异常,前几百条却照样提交了。最后定位到原因:更新方法被同类内部调用,代理根本没能拦截住。那次排查花了我将近半天时间,也让我意识到,Spring事务失效这事儿,真不是简单背几条规则就能完全规避的,你得先把它的底层机制彻底吃透

这篇博文我打算从Spring事务的代理原理讲起,把这几种常见的失效场景逐一拆开揉碎,每一步都跟你说清楚“为什么失效”,而不是光扔给你一张“什么情况会失效”的表格。最后我还会分享一套我平时用来排查事务问题的实战经验,希望对你有实际帮助。

不管你是刚接触Spring的初级开发,还是已经踩过不少坑的资深工程师,这篇文章都值得你花二十分钟从头到尾读一遍。尤其是那些自认为“事务肯定没问题”的人,我敢说这里面至少有两条坑你多半也踩过。

2. 为什么事务会失效:先搞懂Spring事务的底层代理机制

2.1 @Transactional不是魔法,它靠的是AOP动态代理

很多人用@Transactional用了好几年,却未必真正理解它背后的运行原理。Spring声明式事务之所以能生效,靠的不是它在方法上加了几行“魔法”,而是AOP(面向切面编程)在运行时为你动态生成了一个代理对象。

你可以把代理对象理解成中间商:当你调用一个带有@Transactional注解的方法时,首先进入的是这个代理对象,代理对象负责在方法执行之前帮你去数据库连接池里开启事务(setAutoCommit(false)),在方法正常执行结束后提交事务,在方法抛出异常后执行回滚。而真正执行业务代码的,是被这个代理对象包装起来的原始Bean对象。

这个机制的关键点在于:只有当你调用的是代理对象上的方法时,事务拦截器才能插上手。如果你不小心绕过了代理对象、直接调用了原始Bean的方法,那@Transactional注解就形同虚设,拦截器根本不会执行,事务自然也就不会开启。

注意:默认情况下,Spring的事务管理是基于JDK动态代理或CGLIB代理实现的。JDK动态代理要求目标类必须实现接口,它代理的是接口;CGLIB通过生成目标类的子类来代理,不要求接口。无论哪种方式,代理对象都是Spring容器装配时替你创建好的,你在Controller里@Autowired注入的那个Bean,实际上注入的就是代理对象。

2.2 一个关键前提:事务管理器必须被正确配置

除了代理机制,事务失效还有一个经常被忽略的前提条件——Spring容器里必须有一个可用的PlatformTransactionManager(或TransactionManager)Bean。

如果你用的是Spring Boot,DataSourceTransactionManager会被自动装配,这通常不容易出问题。但如果你是传统SSM项目,或者自己手动配置Spring MVC,那么你必须在XML或者JavaConfig里显式声明事务管理器,并且要记得开启<tx:annotation-driven/>或者加@EnableTransactionManagement注解。少了这一步,你在方法上写多少个@Transactional都不会生效。

我见过一个很典型的案例:某个老项目是Dubbo + Spring的架构,所有Service接口和实现类都写好了,@Transactional也加得挺认真,但就是事务不回滚。排查到最后发现,Spring配置文件里只配了数据源和SqlSessionFactory,根本没有配置事务管理器,也没有开启注解驱动扫描。问题解决起来倒是很快,加一段XML配置就好了,但排查过程相当折磨人。

所以判断事务是否生效,第一步不是盯着业务代码看,而是确认这三样东西齐不齐:

  1. 数据源是否有事务管理器
  2. 注解驱动(或@EnableTransactionManagement)是否开启
  3. @Transactional所在的类是否被Spring容器扫描到

这三样缺一样,后面所有的失效场景分析都无从谈起。

2.3 iOS 测试事务是否生效的简易方法

在讲具体失效场景之前,先教大家一个最省的验证方法。当你怀疑某个@Transactional是否生效时,不用急着查各种配置,直接在方法里写一段代码:

@Transactional public void testTransaction() { System.out.println(TransactionSynchronizationManager.isActualTransactionActive()); // 如果这里打印 true,说明事务已经开启 // 如果打印 false,说明事务根本没生效 }

TransactionSynchronizationManager.isActualTransactionActive()是Spring提供的一个工具方法,用来判断当前线程是否绑定了一个真实开启的事务。如果输出true,说明代理生效、事务已开启;如果输出false,恭喜你,找到了问题的方向——事务压根就没启动。这个方法我几乎每次排查事务问题都会先用一遍,简单粗暴,却能省掉大量弯路的排查时间。

3. 失效场景一:方法自调用 —— 最经典的“事务没走代理”坑

3.1 同一个类的内部调用,代理对象直接被绕过了

假设你有一个UserService,里面有两个方法:

@Service public class UserService { @Transactional public void createUserWithOrders(User user) { userMapper.insert(user); this.createOrder(user.getId()); // 自己调自己 } @Transactional(propagation = Propagation.REQUIRES_NEW) public void createOrder(Long userId) { orderMapper.insert(userId); } }

上面这段代码看起来没什么毛病,但事务是失效的。原因在于this.createOrder(...)这里的this是原始Bean对象,不是Spring容器里那个代理对象。当你在外部通过@Autowired拿到UserService时,拿到的是代理对象;但一旦进入createUserWithOrders方法内部,this指向的就是目标类的真实实例,后续的调用链就彻底脱离了代理的控制。

事务拦截器连外层方法的拦截都还没完成呢——换句话说,外层方法的@Transactional也可能因为内部的调用方式而被影响,更深层的REQUIRES_NEW更是压根不会触发。

可以这么理解:代理对象相当于一个前台接待员,正常流程是你先跟前台打招呼,前台再帮你转接给后面的主管(原始Bean)。但自调用相当于你已经进了主管办公室,然后直接拿起电话打给隔壁同事,全程没人经过前台。前台当然不知道你打电话这回事,也就不会给你做事务增强。

3.2 解决自调用问题的三种思路

思路一:把被调用方法拆到另一个独立的Service里。这是最彻底的方案,也是Spring官方推荐的实践方向。你把createOrder拆到OrderService里,让UserService注入OrderService,调用时走的就是代理对象,事务拦截器自然能正常拦截。

思路二:自己注入自己。如果实在不想拆类,有一个偏门但有效的办法——在UserService里注入UserService自己:

@Service public class UserService { @Autowired private UserService self; @Transactional public void createUserWithOrders(User user) { userMapper.insert(user); self.createOrder(user.getId()); } }

这样self是代理对象,内部调用就经由代理了。不过这种写法容易让人困惑,而且不符合“依赖注入自引用”的常规设计,能不用尽量不用。

思路三:从ApplicationContext里拿代理对象。通过ApplicationContext.getBean()手动获取当前类的代理对象再调用,也能解决问题。但这种方式把容器对象硬编码到了业务逻辑中,耦合度很高,我个人不推荐生产环境用。

注意:自调用问题还有一个隐蔽的变种——子类调父类的@Transactional方法。如果你的父类方法标注了@Transactional,子类在内部通过super.method()调用该方法,同样不会走代理。道理是一样的:super也是原始对象引用,不是代理。

4. 失效场景二:方法修饰符限制 —— private、final、static 为什么不行

4.1 代理无法覆盖private方法

再来看一个很常见的坑:有些人习惯把@Transactional放在private方法上,觉得“反正这个细节方法只有自己内部用,加个注解应该也能用吧”。

答案是不能。

原因要从代理的实现方式说起。不管是JDK动态代理还是CGLIB,Spring创建代理对象时,都会对目标类的方法进行判断。private方法是不可被继承、不可被覆盖的。如果是JDK动态代理,代理类按接口实现,目标类的private方法根本不会被暴露在接口中;如果是CGLIB,它以生成子类的方式代理目标类,而private方法无法被子类重写。两种代理方式都拿private方法毫无办法,拦截器自然也无法植入事务逻辑。

顺带一提,Spring的@Transactional注解标注在private方法上时,Spring容器启动阶段一般都不会报错,只是静默忽略。这导致很多开发者根本意识不到事务已经失效,直到数据错乱后才开始排查,尤其是那些在方法内部做了大批量写操作的情况,后果往往会更严重。

4.2 final方法同样无法被代理覆盖

final修饰的方法也有类似问题。CGLIB通过生成目标类的子类来实现代理,它需要重写父类的方法来插入拦截逻辑。一个被final修饰的方法,子类是无法重写的,所以CGLIB只能放弃对这个方法的增强。

JDK动态代理针对接口,如果final方法是接口定义的方法,代理类一般仍然可以实现它;但实践里我们很少在接口里定义final方法(Java的接口方法默认public abstract,即使标识为default也不能加final),所以final和代理失效主要集中在CGLIB场景。另外,如果整个目标类都被final修饰,CGLIB直接无法生成子类,Spring启动时通常会抛出异常,这已经不只是事务失效的问题了。

Spring Boot 2.x 默认开启了proxyTargetClass=true,用的就是CGLIB代理(Spring 6 / Boot 3 的核心是CGLIB风格的代理)。在这样的默认配置下,public方法能正常走代理,final方法就会被跳过。我曾在一个项目中看到一个工具类里的方法被打上final,同时又标了@Transactional,启动时没有任何异常,但实际调用时事务完全没有开启,排查了很久才找到原因。

4.3 static方法是彻底的“无代理”区域

static方法不依赖于对象实例,它属于类本身。代理对象是基于实例创建的,拦截器也是在实例方法调用链上生效的。你调用静态方法根本不需要经过任何实例,代理自然不可能拦截到你头上。所以@Transactional标注在static方法上,基本等同于无效标注。

这里想多说一句:事务方法的设计,最佳实践永远是“public 方法 + 通过代理调用”。如果你确实需要在私有方法中执行数据库写操作,那么应该让一个public方法作为入口,私有方法只做逻辑拆分,@Transactional放在public入口上,而不是放在private方法上。设计良好的事务边界通常都是整个业务操作的入口方法,而不是内部细节方法。

5. 失效场景三:异常被吞了 —— 事务回滚的本质是“感知异常”

5.1 为什么catch异常会导致事务不回滚

这一节讲的内容可能是所有失效场景里最容易踩、也最隐蔽的。很多人总是疑惑:我的方法明明加了@Transactional,数据库操作也报错抛异常了,但数据还是提交了,为什么?

这就要回到Spring事务回滚的实现机制了。Spring的事务拦截器在方法执行后,会根据方法是否抛出异常来决定是commit还是rollback。它的逻辑大致是:

try { // 执行业务方法 Object result = invocation.proceed(); // 如果没有异常,提交事务 transactionManager.commit(); return result; } catch (RuntimeException | Error e) { // 如果抛出的是运行时异常或错误,回滚事务 transactionManager.rollback(); throw e; } catch (Exception e) { // 如果是受检异常,默认不回滚(除非配置了rollbackFor) transactionManager.commit(); throw e; }

关键来了:事务拦截器只能在方法抛出异常时感知到异常。如果你在业务方法内部自己try-catch把异常吞掉了,方法正常返回,拦截器一看“没有异常嘛”,然后就直接提交了——数据自然就落库了。

就像你给保安下了命令:“只要房间里有人呼救,你就冲进去叫医生。”但房间里面的人喊了两嗓子后,自己把嘴捂住了,深呼吸了两口气,然后若无其事地走出门说“我没事”。保安当然不会叫医生,因为他根本没有收到任何异常信号。

5.2 正确姿势:让异常继续往外抛

正确做法是,如果你在方法内部对某段代码做了try-catch处理,要么在catch块中抛出RuntimeExceptionError,要么使用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()手动标记事务回滚。

@Transactional public void createOrder(Order order) { try { orderMapper.insert(order); stockService.deductStock(order.getProductId()); } catch (StockNotEnoughException e) { log.error("库存不足", e); // 手动标记回滚 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); // 或者直接抛出运行时异常 // throw new RuntimeException("库存不足", e); } }

使用setRollbackOnly()可以让事务最终回滚,但方法会正常返回。这种方式适合那些“需要记录错误信息但是又必须回滚”的业务场景。不过这种方式也算一种“半隐式”回滚,维护起来容易被忽略,我通常更倾向于直接抛出统一封装的BusinessException并在全局异常处理器里捕获处理。

注意:有些框架工具在抛出受检异常时,事务默认不会回滚(后面会详细讲)。所以严谨起见,回滚条件最好通过rollbackFor显式指定,避免因为异常继承体系产生意外。

5.3 受检异常默认不回滚:Spring的“历史设计”与应对

Spring事务默认只对RuntimeExceptionError进行回滚,对于受检异常(Exception的子类,但不包含RuntimeException),默认不回滚。这就是为什么有些同学写的@Transactional方法,在业务层直接throw new Exception("xxx"),但数据照样提交的根本原因。

这一点源于Spring早期设计的理念:受检异常通常被认为是可以处理的业务异常,系统可以进行恢复,所以事务不应该盲目回滚。但放到现实的业务里,很多受检异常恰恰是不可恢复的,比如外部接口调用失败、消息发送失败等等,这个时候我们往往需要整条链路回滚。

解决方案很简单,给注解显式指定回滚异常类型:

@Transactional(rollbackFor = Exception.class)

或者更精确一些,只对某几种异常回滚:

@Transactional(rollbackFor = { BusinessException.class, RemoteCallException.class })

我个人强烈建议,在项目中凡是使用@Transactional的地方,直接在注解上写清楚rollbackFor。原因很实际:团队成员的编码水平参差不齐,有人喜欢抛自定义异常,有人习惯直接抛Exception,如果你依赖默认行为,很可能别人写了个受检异常就悄悄提交了数据。显式声明回滚范围,等于把事务边界的行为固定下来,能减少很多隐性风险。

6. 失效场景四:传播行为配置错误 —— REQUIRES_NEW 反而让你“丢了事务”

6.1 理解事务传播机制的核心概念

事务传播行为描述的是:当一个事务方法被另一个事务方法调用时,这个被调用方法的事务该如何响应。Spring定义了多个传播级别,我这里只重点讲两个在生产中最常用、也最容易出问题的:REQUIREDREQUIRES_NEW

REQUIRED是默认的传播行为:如果当前存在事务,则加入到当前事务中;如果当前没有事务,则新建一个事务。换句话说,多个方法在同一个事务上下文中执行,要么全成功,要么全回滚。

REQUIRES_NEW的字面意思是“总是开启一个新事务”:如果当前存在事务,则把当前事务挂起(suspend),创建一个全新的独立事务。新事务提交或回滚后,再恢复之前挂起的事务。——听起来很合理,但实际落地时,很多人对这个“独立事务”的理解有偏差,导致事务不如预期地生效。

拿前面那个自调用的例子来说,如果createOrder方法标了REQUIRES_NEW,而且它是被外部代理正常调用的,那么它确实会开启一个新事务,独立于外层createUserWithOrders事务。好处是:哪怕外层事务后来回滚了,createOrder已经提交的数据也不会跟着回滚。这可以用来做事务间的隔离,比如记录操作日志,不管主业务成功失败,日志都要写入。

坏处是:很多人以为REQUIRES_NEW会增强数据一致性,其实恰恰相反——它把原本一个整体的事务拆成了两个独立事务,部分失败时数据就可能出现中间状态。如果对这条理解不透彻,代码里到处是REQUIRES_NEW,最后的结果往往是莫名其妙丢了一部分数据写操作。

6.2 开发中最常见的传播行为误用

在同一个类内部使用propagation = Propagation.REQUIRES_NEW时,如果发生在自调用场景里,那新事务根本不会开启。外层调用链直接走进了原始Bean的方法,代理层的传播行为拦截器没机会执行。这就导致你费劲配置的“新事务”形同虚设,数据操作还是在原来的事务里执行。

还有一种很典型的误用场景,在事务方法中调用REQUIRES_NEW方法,但是外层事务在调用完新事务方法后抛出了异常。此时外层事务回滚,而REQUIRES_NEW事务已经独立提交——如果操作的是同步数据,就会留下“已提交新事务、外部主流程回滚”的不一致状态。

所以,当你决定使用REQUIRES_NEW时,先问自己几个问题:

  1. 这个被调用的方法是否必须独立提交,即使主流程失败?
  2. 新事务提交的数据是否会对外造成数据不一致?
  3. 是否还有更可靠的方案(比如独立的消息队列、事件表)?

如果答案不清晰,建议放弃REQUIRES_NEW,回到REQUIRED默认传播行为上,让多个写操作保持同一事务边界。

6.3 嵌套事务(REQUIRED + 自调用)同样可能失效

开头那个自调用的例子,即使两个方法都是默认的REQUIRED,自调用也会导致内层方法的事务注解形同虚设。很多人会误以为“外层方法有事务,内层方法就跟着一个事务了”,其实在自调用场景下,内层方法事务压根没有被代理拦截。

假如你在外层加了事务,内层方法里的数据库操作确实会落在同一个事务里——但这只是因为内层方法被外层代理方法顺带执行了而已,而不是因为内层方法自身的@Transactional生效了。如果你指望通过内层方法单独控制事务边界(比如某个内层方法需要REQUIRES_NEW),那它必然不会按你的预期工作。

7. 失效场景五:多线程、数据库引擎与手动提交 —— 那些容易被忽视的隐藏场景

7.1 @Transactional 和 Spring 事务的线程绑定机制

Spring事务的实现很大程度依赖ThreadLocal绑定资源。事务在某个线程上开启时,数据库连接会被绑定到当前线程上,后续同一个线程中的数据库操作会使用这个连接。如果事务方法内部又新开了子线程,子线程通过Spring管理的Mapper执行数据库操作,那它获取到的数据库连接就是另一条连接,和主线程的事务连接根本不沾边。

换句话说,@Transactional管理的是主线程上的事务,而子线程的数据库操作是独立连接上的操作,既不参与主线程事务的提交和回滚,也不会被主线程回滚掉。

我之前处理过一个并发导入的需求:主线程开启事务,分批提交给线程池处理数据导入,等所有批次完成后主线程再提交。结果运行中线程池里的某个批次出现异常,主线程虽然回滚了,但线程池里已经成功执行的那些批次数据全都悄悄提交到数据库了。这个坑踩得我印象深刻,从那以后我养成了一个基本准则:跨线程的操作坚决不用声明式事务去管理。要么把每个子线程当成独立事务处理,要么把所有数据汇总后由主线程串行写入。

7.2 数据库存储引擎不支持事务

这个场景在现在看起来有些“老古董”了,但依然值得提一句。MySQL的MyISAM存储引擎是不支持事务的,它只支持表级锁,没有redo/undo日志,自然也就没有回滚机制。如果你使用MyISAM表,即使用@Transactional把这方法包得再严实,数据操作也都是立即生效,无法回滚。

随着InnoDB成为MySQL默认存储引擎,遇到这个问题的概率大大降低了,但一些老系统、历史遗留表还是有可能存在MyISAM。排查事务失效时,如果你确认代码层面没问题,不妨顺手查一下表的存储引擎:

SHOW TABLE STATUS WHERE Name = 'your_table';

看一眼Engine字段,如果是MyISAM,那答案就明了了。另外,表结构中的ENGINE也可以在SHOW CREATE TABLE中看到。

7.3 手动提交事务与自动提交的混用

如果你使用Spring的TransactionTemplate或者DataSourceTransactionManager手动控制事务,同时又调用了某个被@Transactional标注的方法,两者混用很容易产生“外部事务已提交,内部方法却以为还处于事务中”的混乱局面。更常见的反模式是:在@Transactional方法中,自己从DataSource里拿Connection,然后调用connection.commit()connection.rollback(),这等于把Spring事务管理的连接给“劫持”了,回滚行为基本就失控了。

老实说,我在实际工作中几乎不使用手动提交,除非是做非常底层的批处理逻辑。Spring的声明式事务已经覆盖了绝大多数业务场景,手动介入事务控制只会增加理解和维护成本。

8. 失效场景六:注解标错位置、代理方式与自动配置条件

8.1 注解不能放在接口方法上(在JDK代理下会有歧义)

有些老项目习惯把@Transactional标在接口方法上,实现类里不标。在JDK动态代理模式下,Spring允许你通过接口方法上的注解来配置事务;但如果项目切换成了CGLIB代理(比如Spring Boot默认proxyTargetClass=true),注解标在接口上就可能不被代理目标类感知,事务又会悄悄失效。

更让新手困惑的是,Spring文档建议:注解应该放在实现类的方法上,而不是接口方法上。原因是实现类上的注解更直观、更不易丢失,而且无论代理方式如何变化,基于实现类的注解都能被正确定位。如果你追求严谨,不建议在接口和实现类上同时加注解,因为一旦两边配置不一致,会以哪个为准让人很头疼。

8.2 代理对象能否被创建:final类、非public类的问题

CGLIB代理需要生成目标类的子类。如果目标类是final的,CGLIB无法创建代理;如果目标类不是public的(比如包私有类),有时候也会受限于类加载器的模块访问规则,导致代理创建失败或者事务无法生效。Spring对这类问题往往在启动时就给出异常提示,这是好事——你能尽早发现问题。但我还是提醒一句:Service实现类尽量写成public的非final类,这是在各种代理方案下都不会出错的稳妥选择。

8.3 Spring Boot自动配置失效的隐晦坑

使用Spring Boot时,你可能会忘了@EnableTransactionManagement(Boot下默认开启,但如果你手动创建了多个PlatformTransactionManager,可能导致事务管理器选择错误)。如果配置中同时存在多个PlatformTransactionManager,又没有明确指定@Primary,Spring在事务匹配时可能取到错误的事务管理器,进而造成事务不生效。

这个问题比较隐蔽,因为它不报错——Spring按类型去找唯一的TransactionManager,找到多个时可能直接启动失败;但如果某些条件下的候选集不同,可能静默地选择了一个不匹配数据源的管理器。排查思路是:在配置里明确指定某个事务管理器为@Primary,或者在@Transactional注解中通过transactionManager属性指定准确的事务管理器Bean名称:

@Transactional(transactionManager = "orderTransactionManager", rollbackFor = Exception.class)

9. 事务失效排查手册:三步快速定位问题

9.1 第一步:验证事务是否真的开启了

不管怀疑哪一类原因,我建议都先跑一次最简单的验证,用TransactionSynchronizationManager.isActualTransactionActive()打印当前事务状态。如果打了false,就别再纠结异常、传播行为之类的了,先解决“事务压根没开”的问题,重点检查代理、事务管理器配置、类扫描范围。

如果打了true,但数据依然没回滚,那问题大概率在异常传播或事务边界上,继续看rollbackFor和异常是否被吞。

9.2 第二步:检查调用链是否经过代理

仔细检查@Transactional方法是被谁调用的:

  • 是从外部Bean(Controller/另一个Service)注入后调用的,还是同类内部this调用的?
  • 有没有把@Transactional方法设为private/final/static
  • 有没有用new直接创建对象而不是通过Spring容器获取?

一个很快速的自检办法:在@Transactional方法内打印当前对象的Class信息。如果被代理了,Class名称中通常会包含$$EnhancerBySpringCGLIB$$$Proxy之类的字样;如果打印出来是普通类名,说明你拿到的不是代理对象。

9.3 第三步:确认异常和事务边界

如果代理、配置都没问题,事务状态也打出了true,那重点检查异常链路:

  • 业务代码是否catch了异常并吞掉?
  • 异常类型是否是受检异常(默认不回滚)?
  • 有没有在catch块里又执行了其他数据库写操作?
  • 是否存在跨线程调用、外部接口调用后继续写库的情况?

按这三步排查,绝大多数事务失效问题都能快速找到根源。我自己在团队里带新人时,都是让他们先按这个流程走一遍,基本能避掉80%的坑。

10. 最后再总结几个事务使用的实战原则

写了这么多年代码,也踩过无数事务的坑,我总结了几条自认为很管用的实践原则,分享出来供你参考:

原则一:@Transactional只放在public方法上,且尽量放在实现类方法上。这是最稳的配置方式,能最大程度兼容不同代理方案。

原则二:永远显式声明rollbackFor = Exception.class除非你有特别明确的理由,否则不要依赖默认行为。防止受检异常悄悄提交数据。

原则三:不要在同一类内部调用带事务的方法。如果确实需要事务传递,把被调用方法拆分到独立的Service中,通过注入代理对象来调用。这是最直观也最容易被接受的方式。

原则四:不要在事务方法里长时间占用外部资源。比如事务方法内不要执行RPC调用、HTTP请求、文件上传下载等耗时操作,这些操作会持有数据库连接,容易导致连接池耗尽。尽量先拿到数据,提交事务后再调用外部服务。

原则五:多线程环境下,不要指望事务能跨线程传播。要么把每个线程的任务视为独立事务,要么重新设计数据写入策略,避免在一个事务里开子线程写数据。

原则六:排查事务问题,先验证是否真的开启了事务,再考虑异常和传播行为。别一上来就背“事务失效八大场景”,做无头苍蝇式排查。一个isActualTransactionActive()验证,能帮你快速排除掉一半的问题。

事务看似是一个很小的技术点,但它直接影响数据的一致性和系统的可靠性。很多人直到生产环境出了数据错乱,才回头痛苦地排查事务失效问题。希望这篇文章能帮你把这些坑提前都填平——至少下次再遇到“为什么我的数据没回滚”,你心里能马上浮现出好几个排查方向,而不是一脸懵地对着屏幕发呆。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 23:10:03

域名放别人网站风险大 选对服务商哪家好

域名放别人网站风险大 选对服务商哪家好 域名解析指向不明服务器,DNS劫持与挂马风险让你头疼?域名服务器搞不懂,怕被黑客利用打擦边球,选建站公司哪家好成了甲方最纠结的事。…

作者头像 李华
网站建设 2026/9/15 23:07:22

SpringBoot+Vue企业级家具商城系统全栈源码解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 23:05:41

本地大模型推理实战:消费级GPU部署与量化压缩全链路解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 23:04:44

域名放别人网站3招搞定,免费工具助你省钱

域名放别人网站3招搞定,免费工具助你省钱 找建站公司报价上万?别急,域名解析这步其实能自己搞定,还能省下大笔“技术溢价”。很多老板以为域名必须绑死在对方服务器,结果被绑住手脚,换供应商还得加钱。其实,通过 免费工具…

作者头像 李华
网站建设 2026/9/15 23:03:38

新能源车电耗变化分析:从数据记录到驾驶习惯优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华