前言
最近在做项目的时候,又碰到了@Transactional事务失效的问题。说实话,这个注解看似简单,但用不好真的能把人坑惨。今天就把我踩过的几个坑整理出来,都是实战中实打实遇到的问题,希望能帮大家少走点弯路。
坑一:自己注入自己?小心循环依赖
先问大家一个问题:在 Service 里自己注入自己,会不会出现循环依赖?
@ServicepublicclassUserInfoServiceImplimplementsUserInfoService{@AutowiredprivateUserInfoServiceImpluserInfoService;// 自己注入自己// ...}很多人第一反应是:肯定会啊!但实际上,Spring 原生是支持解决循环依赖的,靠的就是那三级缓存。
但是!重点来了 ——Spring Boot 默认把循环依赖给关了。
对,你没听错。Spring Boot 2.6 之后,默认是不支持循环依赖的,启动直接给你报BeanCurrentlyInCreationException。
解决方案
如果你确实需要自己注入自己(后面会讲为什么需要这么做),可以在配置文件里把这个开关打开:
spring:main:allow-circular-references:true# 开启循环依赖支持加上这个配置,循环依赖的问题就解决了。
坑二:同类方法调用,事务直接失效!(重点)
这个是最常见的坑,没有之一。
问题场景
假设你有一个 Service 类,里面有两个方法:
@ServicepublicclassUserInfoServiceImplimplementsUserInfoService{@TransactionalpublicvoidsaveUser(UserInfouser){// 操作用户主表userInfoMapper.insert(user);// 调用同类的另一个方法saveUserStatus(user.getId());// 这里有坑!}@TransactionalpublicvoidsaveUserStatus(LonguserId){// 操作用户状态附表userStatusMapper.insert(userId);}}看起来没毛病对吧?两个方法都加了@Transactional,应该都在事务里啊?
大错特错!
UsersaveUserStatus()这个方法的事务根本不会生效!saveUserStatus()这个方法的事务根本不会生效!
为什么会失效?
原因很简单:Spring 的事务是基于 AOP 动态代理实现的。
外部调用
saveUser()时,实际上调用的是代理对象的方法,代理对象会帮你开启事务但在方法内部调用
saveUserStatus()时,用的是this(也就是原始对象),不是代理对象没有经过代理对象,AOP 就拦不住,事务自然就失效了
怎么判断事务有没有生效?
教大家一个简单的判断方法:只要是this.方法名()调用的,事务注解都不生效。
因为this代表的是当前对象本身,不是 Spring 生成的代理对象。
三种解决方案
方案一:抽到另一个 Service 里(最稳妥)
User把saveUserStatus()抽到一个新的 Service 中:把saveUserStatus()抽到一个新的 Service 中:
@ServicepublicclassUserOperateServiceImplimplementsUserOperateService{@AutowiredprivateUserStatusMapperuserStatusMapper;@Override@TransactionalpublicvoidsaveUserStatus(LonguserId){userStatusMapper.insert(userId);}}然后在原来的 Service 中注入这个新 Service:
@ServicepublicclassUserInfoServiceImplimplementsUserInfoService{@AutowiredprivateUserOperateServiceuserOperateService;@TransactionalpublicvoidsaveUser(UserInfouser){userInfoMapper.insert(user);userOperateService.saveUserStatus(user.getId());// 通过代理对象调用,事务生效}}优点:最规范,没有任何副作用
缺点:要多写一个类,有点麻烦
方案二:自己注入自己
@ServicepublicclassUserInfoServiceImplimplementsUserInfoService{@AutowiredprivateUserInfoServiceuserInfoService;// 注入自己(用接口类型)@TransactionalpublicvoidsaveUser(UserInfouser){userInfoMapper.insert(user);userInfoService.saveUserStatus(user.getId());// 通过注入的代理对象调用}@TransactionalpublicvoidsaveUserStatus(LonguserId){userStatusMapper.insert(userId);}}原理:注入的userInfoService是 Spring 生成的代理对象,通过它调用方法就能走 AOP。
注意:这种方式需要开启循环依赖支持,就是前面说的spring.main.allow-circular-references=true。
优点:不用新建类,代码改动小
缺点:需要开启循环依赖,有的人可能觉得不优雅
方案三:用 AopContext 获取代理对象(个人推荐)
这是我最喜欢的方式,代码最简洁。
第一步:在启动类或配置类上加注解,暴露代理对象:
@SpringBootApplication@EnableAspectJAutoProxy(exposeProxy=true)// 关键:暴露代理对象publicclassApplication{publicstaticvoidmain(String[]args){SpringApplication.run(Application.class,args);}}第二步:在方法中通过AopContext获取当前代理对象:
@ServicepublicclassUserInfoServiceImplimplementsUserInfoService{@TransactionalpublicvoidsaveUser(UserInfouser){userInfoMapper.insert(user);// 获取当前代理对象UserInfoServiceproxy=(UserInfoService)AopContext.currentProxy();proxy.saveUserStatus(user.getId());// 通过代理对象调用}@TransactionalpublicvoidsaveUserStatus(LonguserId){userStatusMapper.insert(userId);}}优点:不用新建类,不用自己注入自己,代码清晰
缺点:需要加一个启动类注解
💡个人建议:优先用方案三,最优雅也最方便。如果项目规范要求不能这么写,再考虑方案一。
坑三:抛了异常,事务居然不回滚?
这个坑也超级常见!
问题场景
@TransactionalpublicvoidsaveUser(UserInfouser)throwsSQLException{userInfoMapper.insert(user);// 模拟抛出数据库异常if(user.getId()==null){thrownewSQLException("数据库异常");}}你觉得上面的代码,抛了SQLException之后事务会回滚吗?
答案是:不会!
为什么不回滚?
因为 Spring 事务默认只对RuntimeException和Error进行回滚。
来看看异常的继承关系:
Throwable ├── Error(Spring会回滚) └── Exception ├── RuntimeException(Spring会回滚) └── 其他Exception(比如SQLException,Spring不回滚!)SQLException继承的是Exception,不是RuntimeException,所以 Spring 默认不回滚。
解决方案
加上rollbackFor属性,指定回滚的异常类型:
@Transactional(rollbackFor=Exception.class)// 所有Exception都回滚publicvoidsaveUser(UserInfouser)throwsSQLException{// ...}这样只要是Exception及其子类的异常,都会触发事务回滚。
💡最佳实践:建议大家写
@Transactional的时候,习惯性加上rollbackFor = Exception.class,避免踩坑。
补充:还有一个小细节
不知道大家注意到没有,IDEA 会在private方法上的@Transactional标红提醒。
为什么?因为事务注解必须加在public方法上。
私有方法外部访问不到,Spring 的代理也没法拦截,加了事务注解也没用。IDEA 很贴心地给你提示了。
总结
今天讲了@Transactional的三个大坑:
| 坑 | 原因 | 解决方案 |
|---|---|---|
| 循环依赖 | Spring Boot 默认关闭循环依赖 | 配置spring.main.allow-circular-references=true |
| 同类方法调用事务失效 | 用this调用,没走代理对象 | 1️⃣ 抽到另一个 Service 2️⃣ 自己注入自己 3️⃣ AopContext.currentProxy()(推荐) |
| 异常不回滚 | 默认只回滚RuntimeException | 加上rollbackFor = Exception.class |
希望这篇文章能帮大家避避坑。如果觉得有用,点个赞收藏一下,以后遇到事务问题翻出来看看就行~