我是站长网,辽宁建设工程信息网联合体怎么报名,军队 网站备案,陕西网站开发公司地址前言在之前的文章中已经对Spring中的事务做了详细的分析了#xff0c;这篇文章我们来聊一聊平常工作时使用事务可能出现的一些问题(本文主要针对使用Transactional进行事务管理的方式进行讨论)以及对应的解决方案事务失效事务回滚相关问题读写分离跟事务结合使用时的问题事务失… 前言在之前的文章中已经对Spring中的事务做了详细的分析了这篇文章我们来聊一聊平常工作时使用事务可能出现的一些问题(本文主要针对使用Transactional进行事务管理的方式进行讨论)以及对应的解决方案事务失效事务回滚相关问题读写分离跟事务结合使用时的问题事务失效事务失效我们一般要从两个方面排查问题数据库层面数据库层面数据库使用的存储引擎是否支持事务默认情况下MySQL数据库使用的是Innodb存储引擎(5.5版本之后)它是支持事务的但是如果你的表特地修改了存储引擎例如你通过下面的语句修改了表使用的存储引擎为MyISAM而MyISAM又是不支持事务的alter table table_name enginemyisam;这样就会出现“事务失效”的问题了「解决方案」修改存储引擎为Innodb。业务代码层面业务层面的代码是否有问题这就有很多种可能了我们要使用Spring的声明式事务那么需要执行事务的Bean是否已经交由了Spring管理在代码中的体现就是类上是否有Service、Component等一系列注解「解决方案」将Bean交由Spring进行管理(添加Service注解)Transactional注解是否被放在了合适的位置。在上篇文章中我们对Spring中事务失效的原理做了详细的分析其中也分析了Spring内部是如何解析Transactional注解的我们稍微回顾下代码注解解析❝代码位于AbstractFallbackTransactionAttributeSource#computeTransactionAttribute中❞也就是说默认情况下你无法使用Transactional对一个非public的方法进行事务管理「解决方案」修改需要事务管理的方法为public。出现了自调用。什么是自调用呢我们看个例子Servicepublic class DmzService {public void saveAB(A a, B b) { saveA(a); saveB(b); }Transactionalpublic void saveA(A a) { dao.saveA(a); }Transactionalpublic void saveB(B b){ dao.saveB(a); }}上面三个方法都在同一个类DmzService中其中saveAB方法中调用了本类中的saveA跟saveB方法这就是自调用。在上面的例子中saveA跟saveB上的事务会失效那么自调用为什么会导致事务失效呢我们知道Spring中事务的实现是依赖于AOP的当容器在创建dmzService这个Bean时发现这个类中存在了被Transactional标注的方法(修饰符为public)那么就需要为这个类创建一个代理对象并放入到容器中创建的代理对象等价于下面这个类public class DmzServiceProxy {private DmzService dmzService;public DmzServiceProxy(DmzService dmzService) {this.dmzService dmzService; }public void saveAB(A a, B b) { dmzService.saveAB(a, b); }public void saveA(A a) {try {// 开启事务 startTransaction(); dmzService.saveA(a); } catch (Exception e) {// 出现异常回滚事务 rollbackTransaction(); }// 提交事务 commitTransaction(); }public void saveB(B b) {try {// 开启事务 startTransaction(); dmzService.saveB(b); } catch (Exception e) {// 出现异常回滚事务 rollbackTransaction(); }// 提交事务 commitTransaction(); }}上面是一段伪代码通过startTransaction、rollbackTransaction、commitTransaction这三个方法模拟代理类实现的逻辑。因为目标类DmzService中的saveA跟saveB方法上存在Transactional注解所以会对这两个方法进行拦截并嵌入事务管理的逻辑同时saveAB方法上没有Transactional相当于代理类直接调用了目标类中的方法。我们会发现当通过代理类调用saveAB时整个方法的调用链如下实际上我们在调用saveA跟saveB时调用的是目标类中的方法这种清空下事务当然会失效。常见的自调用导致的事务失效还有一个例子如下Servicepublic class DmzService {Transactionalpublic void save(A a, B b) { saveB(b); }Transactional(propagation Propagation.REQUIRES_NEW)public void saveB(B b){ dao.saveB(a); }}当我们调用save方法时我们预期的执行流程是这样的也就是说两个事务之间互不干扰每个事务都有自己的开启、回滚、提交操作。但根据之前的分析我们知道实际上在调用saveB方法时是直接调用的目标类中的saveB方法在saveB方法前后并不会有事务的开启或者提交、回滚等操作实际的流程是下面这样的由于saveB方法实际上是由dmzService也就是目标类自己调用的所以在saveB方法的前后并不会执行事务的相关操作。这也是自调用带来问题的根本原因「自调用时调用的是目标类中的方法而不是代理类中的方法」「解决方案」自己注入自己然后显示的调用例如Servicepublic class DmzService {// 自己注入自己Autowired DmzService dmzService;Transactionalpublic void save(A a, B b) { dmzService.saveB(b); }Transactional(propagation Propagation.REQUIRES_NEW)public void saveB(B b){ dao.saveB(a); }}这种方案看起来不是很优雅利用AopContext如下Servicepublic class DmzService {Transactionalpublic void save(A a, B b) { ((DmzService) AopContext.currentProxy()).saveB(b); }Transactional(propagation Propagation.REQUIRES_NEW)public void saveB(B b){ dao.saveB(a); }}❝使用上面这种解决方案需要注意的是需要在配置类上新增一个配置// exposeProxytrue代表将代理类放入到线程上下文中默认是falseEnableAspectJAutoProxy(exposeProxy true)❞个人比较喜欢的是第二种方式这里我们做个来做个小总结总结一图胜千言事务失效的原因事务回滚相关问题回滚相关的问题可以被总结为两句话想回滚的时候事务却提交了想提交的时候被标记成只能回滚了(rollback only)先看第一种情况「想回滚的时候事务却提交了」。这种情况往往是程序员对Spring中事务的rollbackFor属性不够了解导致的。❝Spring默认抛出了未检查unchecked异常(继承自 RuntimeException的异常)或者 Error才回滚事务其他异常不会触发回滚事务已经执行的SQL会提交掉。如果在事务中抛出其他类型的异常但却期望 Spring 能够回滚事务就需要指定rollbackFor属性。❞对应代码其实我们上篇文章也分析过了如下回滚代码❝以上代码位于TransactionAspectSupport#completeTransactionAfterThrowing方法中❞默认情况下只有出现RuntimeException或者Error才会回滚public boolean rollbackOn(Throwable ex) {return (ex instanceof RuntimeException || ex instanceof Error);}所以如果你想在出现了非RuntimeException或者Error时也回滚请指定回滚时的异常例如Transactional(rollbackFor Exception.class)第二种情况「想提交的时候被标记成只能回滚了(rollback only)」。对应的异常信息如下Transaction rolled back because it has been marked as rollback-only我们先来看个例子吧Servicepublic class DmzService {Autowired IndexService indexService;Transactionalpublic void testRollbackOnly() {try { indexService.a(); } catch (ClassNotFoundException e) { System.out.println(catch); } }}Servicepublic class IndexService {Transactional(rollbackFor Exception.class)public void a() throws ClassNotFoundException{// ......throw new ClassNotFoundException(); }}在上面这个例子中DmzService的testRollbackOnly方法跟IndexService的a方法都开启了事务并且事务的传播级别为required所以当我们在testRollbackOnly中调用IndexService的a方法时这两个方法应当是共用的一个事务。按照这种思路虽然IndexService的a方法抛出了异常但是我们在testRollbackOnly将异常捕获了那么这个事务应该是可以正常提交的为什么会抛出异常呢如果你看过我之前的源码分析的文章应该知道在处理回滚时有这么一段代码rollBackOnly设置在提交时又做了下面这个判断(这个方法我删掉了一些不重要的代码)commit_rollbackOnly可以看到当提交时发现事务已经被标记为rollbackOnly后会进入回滚处理中并且unexpected传入的为true。在处理回滚时又有下面这段代码抛出异常最后在这里抛出了这个异常。❝以上代码均位于AbstractPlatformTransactionManager中❞总结起来「主要的原因就是因为内部事务回滚时将整个大事务做了一个rollbackOnly的标记」所以即使我们在外部事务中catch了抛出的异常整个事务仍然无法正常提交并且如果你希望正常提交Spring还会抛出一个异常。「解决方案」:这个解决方案要依赖业务而定你要明确你想要的结果是什么内部事务发生异常外部事务catch异常后内部事务自行回滚不影响外部事务❝将内部事务的传播级别设置为nested/requires_new均可。在我们的例子中就是做如下修改// Transactional(rollbackFor Exception.class,propagation Propagation.REQUIRES_NEW)Transactional(rollbackFor Exception.class,propagation Propagation.NESTED)public void a() throws ClassNotFoundException{// ......throw new ClassNotFoundException();}❞虽然这两者都能得到上面的结果但是它们之间还是有不同的。当传播级别为requires_new时两个事务完全没有联系各自都有自己的事务管理机制(开启事务、关闭事务、回滚事务)。但是传播级别为nested时实际上只存在一个事务只是在调用a方法时设置了一个保存点当a方法回滚时实际上是回滚到保存点上并且当外部事务提交时内部事务才会提交外部事务如果回滚内部事务会跟着回滚。内部事务发生异常时外部事务catch异常后内外两个事务都回滚但是方法不抛出异常Transactionalpublic void testRollbackOnly() {try { indexService.a(); } catch (ClassNotFoundException e) {// 加上这句代码 TransactionInterceptor.currentTransactionStatus().setRollbackOnly(); }}❞通过显示的设置事务的状态为RollbackOnly。这样当提交事务时会进入下面这段代码显示回滚最大的区别在于处理回滚时第二个参数传入的是false,这意味着回滚是回滚是预期之中的所以在处理完回滚后并不会抛出异常。 读写分离跟事务结合使用时的问题读写分离一般有两种实现方式配置多数据源依赖中间件如MyCat如果是配置了多数据源的方式实现了读写分离那么需要注意的是「如果开启了一个读写事务那么必须使用写节点」「如果是一个只读事务那么可以使用读节点」如果是依赖于MyCat等中间件那么需要注意「只要开启了事务事务内的SQL都会使用写节点(依赖于具体中间件的实现也有可能会允许使用读节点具体策略需要自行跟DB团队确认)」基于上面的结论我们在使用事务时应该更加谨慎在没有必要开启事务时尽量不要开启。❝一般我们会在配置文件配置某些约定的方法名字前缀开启不同的事务(或者不开启)但现在随着注解事务的流行好多开发人员(或者架构师)搭建框架的时候在service类上加上了Transactional注解导致整个类都是开启事务的这样严重影响数据库执行的效率更重要的是开发人员不重视、或者不知道在查询类的方法上面自己加上Transactional(propagationPropagation.NOT_SUPPORTED)就会导致所有的查询方法实际并没有走从库导致主库压力过大。❞其次关于如果没有对只读事务做优化的话(优化意味着将只读事务路由到读节点)那么Transactional注解中的readOnly属性就应该要慎用。我们使用readOnly的原本目的是为了将事务标记为只读这样当MySQL服务端检测到是一个只读事务后就可以做优化少分配一些资源(例如只读事务不需要回滚所以不需要分配undo log段)。但是当配置了读写分离后可能会可能会导致只读事务内所有的SQL都被路由到了主库读写分离也就失去了意义。总结本文为事务专栏最后一篇啦这篇文章主要是总结了工作中事务相关的常见问题想让大家少走点弯路希望大家可以认真读完哦有什么问题可以直接在后台私信我或者加我微信这篇文章也是整个Spring系列的最后一篇文章之后可能会出一篇源码阅读心得跟大家聊聊如何学习源码。另外今年也给自己定了个小目标就是完成SSM框架源码的阅读。目前来说Spring是完成接下来就是SpringMVC跟MyBatis。在分析MyBatis前会从JDBC源码出发然后就是MyBatis对配置的解析、MyBatis执行流程、MyBatis的缓存、MyBatis的事务管理以及MyBatis的插件机制。在学习SpringMVC前会从TomCat出发先讲清楚TomCat的原理我们再来看SpringMVC。整个来说相比于Spring源码我觉得应该不算特别难。有道无术术可成有术无道止于术欢迎大家关注Java之道公众号好文章我在看❤️