踩坑总结:Spring @Transactional 事务注解的这几个坑,你踩过几个?
前言最近在做项目的时候又碰到了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;OverrideTransactionalpublicvoidsaveUserStatus(LonguserId){userStatusMapper.insert(userId);}}然后在原来的 Service 中注入这个新 ServiceServicepublicclassUserInfoServiceImplimplementsUserInfoService{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-referencestrue。优点不用新建类代码改动小缺点需要开启循环依赖有的人可能觉得不优雅方案三用 AopContext 获取代理对象个人推荐这是我最喜欢的方式代码最简洁。第一步在启动类或配置类上加注解暴露代理对象SpringBootApplicationEnableAspectJAutoProxy(exposeProxytrue)// 关键暴露代理对象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 ├── ErrorSpring会回滚 └── Exception ├── RuntimeExceptionSpring会回滚 └── 其他Exception比如SQLExceptionSpring不回滚SQLException继承的是Exception不是RuntimeException所以 Spring 默认不回滚。解决方案加上rollbackFor属性指定回滚的异常类型Transactional(rollbackForException.class)// 所有Exception都回滚publicvoidsaveUser(UserInfouser)throwsSQLException{// ...}这样只要是Exception及其子类的异常都会触发事务回滚。最佳实践建议大家写Transactional的时候习惯性加上rollbackFor Exception.class避免踩坑。补充还有一个小细节不知道大家注意到没有IDEA 会在private方法上的Transactional标红提醒。为什么因为事务注解必须加在public方法上。私有方法外部访问不到Spring 的代理也没法拦截加了事务注解也没用。IDEA 很贴心地给你提示了。总结今天讲了Transactional的三个大坑坑原因解决方案循环依赖Spring Boot 默认关闭循环依赖配置spring.main.allow-circular-referencestrue同类方法调用事务失效用this调用没走代理对象1️⃣ 抽到另一个 Service2️⃣ 自己注入自己3️⃣AopContext.currentProxy()推荐异常不回滚默认只回滚RuntimeException加上rollbackFor Exception.class希望这篇文章能帮大家避避坑。如果觉得有用点个赞收藏一下以后遇到事务问题翻出来看看就行