聊聊spring事务失效的12种场景,太坑了
前言
对于从事java开发工作的同学来说,spring的事务肯定再熟悉不过了。
在某些业务场景下,如果一个请求中,需要同时写入多张表的数据。为了保证操作的原子性(要么同时成功,要么同时失败),避免数据不一致的情况,我们一般都会用到spring事务。
确实,spring事务用起来贼爽,就用一个简单的注解: @Transactional ,就能轻松搞定事务。我猜大部分小伙伴也是这样用的,而且一直用一直爽。
但如果你使用不当,它也会坑你于无形。
今天我们就一起聊聊,事务失效的一些场景,说不定你已经中招了。不信,让我们一起看看。
一 事务不生效1.访问权限问题
众所周知,java的访问权限主要有四种:private、default、protected、public,它们的权限从左到右,依次变大。
但如果我们在开发过程中,把有某些事务方法,定义了错误的访问权限,就会导致事务功能出问题,例如: @Service public class UserService { @Transactional private void add(UserModel userModel) { saveData(userModel); updateData(userModel); } }
我们可以看到add方法的访问权限被定义成了 private ,这样会导致事务失效,spring要求被代理方法必须是public 的。
说白了,在 AbstractFallbackTransactionAttributeSource 类的computeTransactionAttribute 方法中有个判断,如果目标方法不是public,则TransactionAttribute 返回null,即不支持事务。 protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) { // Don"t allow no-public methods as required. if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) { return null; } // The method may be on an interface, but we need attributes from the target class. // If the target class is null, the method will be unchanged. Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); // First try is the method in the target class. TransactionAttribute txAttr = findTransactionAttribute(specificMethod); if (txAttr != null) { return txAttr; } // Second try is the transaction attribute on the target class. txAttr = findTransactionAttribute(specificMethod.getDeclaringClass()); if (txAttr != null && ClassUtils.isUserLevelMethod(method)) { return txAttr; } if (specificMethod != method) { // Fallback is to look at the original method. txAttr = findTransactionAttribute(method); if (txAttr != null) { return txAttr; } // Last fallback is the class of the original method. txAttr = findTransactionAttribute(method.getDeclaringClass()); if (txAttr != null && ClassUtils.isUserLevelMethod(method)) { return txAttr; } } return null; }
也就是说,如果我们自定义的事务方法(即目标方法),它的访问权限不是 public ,而是private、default或protected的话,spring则不会提供事务功能。 2. 方法用final修饰
有时候,某个方法不想被子类重新,这时可以将该方法定义成final的。普通方法这样定义是没问题的,但如果将事务方法定义成final,例如: @Service public class UserService { @Transactional public final void add(UserModel userModel){ saveData(userModel); updateData(userModel); } }
我们可以看到add方法被定义成了 final 的,这样会导致事务失效。
为什么?
如果你看过spring事务的源码,可能会知道spring事务底层使用了aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。
但如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能。
注意:如果某个方法是static的,同样无法通过动态代理,变成事务方法。 3.方法内部调用
有时候我们需要在某个Service类的某个方法中,调用另外一个事务方法,比如: @Service public class UserService { @Autowired private UserMapper userMapper; @Transactional public void add(UserModel userModel) { userMapper.insertUser(userModel); updateStatus(userModel); } @Transactional public void updateStatus(UserModel userModel) { doSameThing(); } }
我们看到在事务方法add中,直接调用事务方法updateStatus。从前面介绍的内容可以知道,updateStatus方法拥有事务的能力是因为spring aop生成代理了对象,但是这种方法直接调用了this对象的方法,所以updateStatus方法不会生成事务。
由此可见,在同一个类中的方法直接内部调用,会导致事务失效。
那么问题来了,如果有些场景,确实想在同一个类的某个方法中,调用它自己的另外一个方法,该怎么办呢? 3.1 新加一个Service方法
这个方法非常简单,只需要新加一个Service方法,把@Transactional注解加到新Service方法上,把需要事务执行的代码移到新方法中。具体代码如下: @Servcie public class ServiceA { @Autowired prvate ServiceB serviceB; public void save(User user) { queryData1(); queryData2(); serviceB.doSave(user); } } @Servcie public class ServiceB { @Transactional(rollbackFor=Exception.class) public void doSave(User user) { addData1(); updateData2(); } } 3.2 在该Service类中注入自己
如果不想再新加一个Service类,在该Service类中注入自己也是一种选择。具体代码如下: @Servcie public class ServiceA { @Autowired prvate ServiceA serviceA; public void save(User user) { queryData1(); queryData2(); serviceA.doSave(user); } @Transactional(rollbackFor=Exception.class) public void doSave(User user) { addData1(); updateData2(); } }
可能有些人可能会有这样的疑问:这种做法会不会出现循环依赖问题?
答案:不会。
其实spring ioc内部的三级缓存保证了它,不会出现循环依赖问题。但有些坑,如果你想进一步了解循环依赖问题,可以看看我之前文章《spring:我是如何解决循环依赖的?》。 3.3 通过AopContent类
在该Service类中使用AopContext.currentProxy()获取代理对象
上面的方法2确实可以解决问题,但是代码看起来并不直观,还可以通过在该Service类中使用AOPProxy获取代理对象,实现相同的功能。具体代码如下: @Servcie public class ServiceA { public void save(User user) { queryData1(); queryData2(); ((ServiceA)AopContext.currentProxy()).doSave(user); } @Transactional(rollbackFor=Exception.class) public void doSave(User user) { addData1(); updateData2(); } } 4.未被spring管理
在我们平时开发过程中,有个细节很容易被忽略。即使用spring事务的前提是:对象要被spring管理,需要创建bean实例。
通常情况下,我们通过@Controller、@Service、@Component、@Repository等注解,可以自动实现bean实例化和依赖注入的功能。
当然创建bean实例的方法还有很多,有兴趣的小伙伴可以看看我之前写的另一篇文章《@Autowired的这些骚操作,你都知道吗?》
如果有一天,你匆匆忙忙的开发了一个Service类,但忘了加@Service注解,比如: //@Service public class UserService { @Transactional public void add(UserModel userModel) { saveData(userModel); updateData(userModel); } }
从上面的例子,我们可以看到UserService类没有加 @Service 注解,那么该类不会交给spring管理,所以它的add方法也不会生成事务。 5.多线程调用
在实际项目开发中,多线程的使用场景还是挺多的。如果spring事务用在多线程场景中,会有问题吗? @Slf4j @Service public class UserService { @Autowired private UserMapper userMapper; @Autowired private RoleService roleService; @Transactional public void add(UserModel userModel) throws Exception { userMapper.insertUser(userModel); new Thread(() -> { roleService.doOtherThing(); }).start(); } } @Service public class RoleService { @Transactional public void doOtherThing() { System.out.println("保存role表数据"); } }
从上面的例子中,我们可以看到事务方法add中,调用了事务方法doOtherThing,但是事务方法doOtherThing是在另外一个线程中调用的。
这样会导致两个方法不在同一个线程中,获取到的数据库连接不一样,从而是两个不同的事务。如果想doOtherThing方法中抛了异常,add方法也回滚是不可能的。
如果看过spring事务源码的朋友,可能会知道spring的事务是通过数据库连接来实现的。当前线程中保存了一个map,key是数据源,value是数据库连接。 private static final ThreadLocal
热血传奇黑铁头盔是战士玩家首选的头盔,传奇游戏中的战士职业就以他的霸气和沉稳在众多职业当中也是格外的霸气,像之前的一些玩家基本上都分布在南方的那些地方,其他剩下的都是北方地区,不过要是说南方地区的网络发展还是不错的,北
暗黑破坏神系列中,暗黑破坏神不朽的水平如何?记得当年暗黑系列刚出的时候,就是普普通通没有什么特色的一般游戏,为什么你们要说他是非常优秀的游戏?这是我不解的,比较起同时期其他欧美ARPG,同样的职业套路,同样的游戏套路是怎么得
梦幻西游任务五开应该带哪个治疗门派?魔化生无底洞谁更好用?闲言三界故事,记录梦幻风云。大家好,我是阿盐。感谢大家长久以来的关注,阿盐的155五开渡劫之路,仍在继续。今天阿盐有两个牧场号来了守护兽,有熊猫守护兽鳄鱼守护兽和羊驼守护兽。两个号
服务器崩溃!完美世界幻塔公测人气高,剧情PV预告上线12月16日,由完美世界游戏旗下HottaStudio自主研发的轻科幻开放世界手游幻塔开启全平台公测,获得了巨大流量。一同释放的,还有全新剧情PV预告和公测主题曲Salvation
逗斗火柴人手机版(附攻略)逗斗火柴人手机版是一款格斗手游,凭借其魔性的武器地图设计,友尽式的格斗迅速征服玩家,让不少火柴人格斗玩家直呼精彩,再加上真实的物理反馈系统完美还原,火柴人的走位丝滑灵活又魔性,跳跃
传世手游经典传世等你来玩哈喽,大家好,我是春来。今日就来和大伙说一下传世怀旧版。众所周知,传世怀旧版本也是老版本了!也同样的有着着一大批骨灰级玩家!传世怀旧是正版授权手机游戏,游戏画面画面质量100复原经
轻科幻开放世界手游幻塔正式上线艾达星也太美了吧在无数玩家的期盼中,轻科幻开放世界手游幻塔终于在今天正式上线了。作为一款打磨N久的开放世界大作,幻塔中除了拥有丰富广袤的大世界,还拥有多种特色玩法。小编早就铆足了劲想要好好体验一番
继1。13c之后,暗黑破坏神2又将推出新内容?不知道你的回忆里,有没有一个暗黑破坏神2呢?或许在许多骨灰级玩家的心目中,暗黑破坏神2这个词已经从游戏名指代为某段岁月某个曾经可能你已经忘记了很多事,却对符文编号烂熟于心,合成精神
魔兽TBCT5团本一削到底,玩家可直面两大尾王,便当时代已到来经过例行维护之后,外服已经率先开启了魔兽世界TBC版本怀旧服P2。5阶段,也就是T5团本提前削弱的阶段。虽然暴雪放出T5团本削弱的消息已经有一段时间了,并且玩家们在PTR测试服也对
阿拉德之怒游戏已经开服,数百万玩家重出江湖阿拉德之怒游戏已经开服,数百万玩家重出江湖!三年前,一款格斗类手游火爆上线,其玩法与某企鹅的毒奶粉极为相似,所以许多喜欢横版格斗手游的玩家纷纷加入了游戏。后因某版权纠纷原因使得阿拉
热血传奇私服版本五花八门,漏洞千奇百怪,有的令人哭笑不得自从当年热血传奇的源码泄漏后,国内各地网吧里的传奇私服便开始像雨后春笋般多了起来。虽说搭设私服也是必须要有一定技术的,并不是人人都能够私下搭设私服的,要编写游戏代码,还需要了解相关