Spring 事务失效的六种情况

网站建设3年前发布
50 0 0

20230306122421867aefc8192fc614ae2151f88130404983b576772,最近有小伙伴告诉松哥说面试中被问到这个问题了,不知道该怎么回答,这能忍?捋一篇文章和小伙伴们分享下吧。,既然捋成文章,就连同 Spring 事务一起梳理下吧。,数据库事务是指作为单个逻辑工作单元执行的一系列操作,这些操作要么一起成功,要么一起失败,是一个不可分割的工作单元。,在我们日常工作中,涉及到事务的场景非常多,一个 service 中往往需要调用不同的 dao 层方法,这些方法要么同时成功要么同时失败,我们需要在 service 层确保这一点。,说到事务最典型的案例就是转账了:,张三要给李四转账 500 块钱,这里涉及到两个操作,从张三的账户上减去 500 块钱,给李四的账户添加 500 块钱,这两个操作要么同时成功要么同时失败,如何确保他们同时成功或者同时失败呢?答案就是事务。,事务有四大特性(ACID):,202303061224221709d621133c18aed81930aaaf6772802f1625419,这就是事务的四大特性。,Spring 作为 Java 开发中的基础设施,对于事务也提供了很好的支持,总体上来说,Spring 支持两种类型的事务,声明式事务和编程式事务。,编程式事务类似于 Jdbc 事务的写法,需要将事务的代码嵌入到业务逻辑中,这样代码的耦合度较高,而声明式事务通过 AOP 的思想能够有效的将事务和业务逻辑代码解耦,因此在实际开发中,声明式事务得到了广泛的应用,而编程式事务则较少使用,考虑到文章内容的完整,本文对两种事务方式都会介绍。,Spring 中对事务的支持提供了三大基础设施,我们先来了解下。,这三个核心类是 Spring 处理事务的核心类。,PlatformTransactionManager 是事务处理的核心,它有诸多的实现类,如下:,20230306122423f3fb45f84d60a690404101886b743025ae9176729,PlatformTransactionManager 的定义如下:,可以看到 PlatformTransactionManager 中定义了基本的事务操作方法,这些事务操作方法都是平台无关的,具体的实现都是由不同的子类来实现的。,这就像 JDBC 一样,SUN 公司制定标准,其他数据库厂商提供具体的实现。这么做的好处就是我们 Java 程序员只需要掌握好这套标准即可,不用去管接口的具体实现。以 PlatformTransactionManager​ 为例,它有众多实现,如果你使用的是 JDBC 那么可以将 DataSourceTransactionManager​ 作为事务管理器;如果你使用的是 Hibernate,那么可以将 HibernateTransactionManager​ 作为事务管理器;如果你使用的是 JPA,那么可以将 JpaTransactionManager​ 作为事务管理器。DataSourceTransactionManager、HibernateTransactionManager​ 以及 JpaTransactionManager​ 都是 PlatformTransactionManager​ 的具体实现,但是我们并不需要掌握这些具体实现类的用法,我们只需要掌握好 PlatformTransactionManager 的用法即可。,PlatformTransactionManager 中主要有如下三个方法:,1.getTransaction(),getTransaction() 是根据传入的 TransactionDefinition 获取一个事务对象,TransactionDefinition 中定义了一些事务的基本规则,例如传播性、隔离级别等。,2.commit(),commit() 方法用来提交事务。,3.rollback(),rollback() 方法用来回滚事务。,TransactionDefinition 用来描述事务的具体规则,也称作事务的属性。事务有哪些属性呢?看下图:,20230306122424356271813e8aa9cc535085803e82bc24cd934b871,可以看到,主要是五种属性:,这五种属性接下来松哥会和大家详细介绍。,TransactionDefinition 类中的方法如下:,2023030612242528320ba14a28c83f00c269a0fb381c54de8ee6909,可以看到一共有五个方法:,TransactionDefinition 也有诸多的实现类,如下:,20230306122426e26560d148953be5b56378cca84b8c252c3956673,如果开发者使用了编程式事务的话,直接使用 DefaultTransactionDefinition 即可。,TransactionStatus 可以直接理解为事务本身,该接口源码如下:,这就是 Spring 中支持事务的三大基础设施。,我们先来看看编程式事务怎么玩。,通过 PlatformTransactionManager 或者 TransactionTemplate 可以实现编程式事务。如果是在 Spring Boot 项目中,这两个对象 Spring Boot 会自动提供,我们直接使用即可。但是如果是在传统的 SSM 项目中,则需要我们通过配置来提供这两个对象,松哥给一个简单的配置参考,如下(简单起见,数据库操作我们使用 JdbcTemplate):,有了这两个对象,接下来的代码就简单了:,这段代码很简单,没啥好解释的,在 try...catch... 中进行业务操作,没问题就 commit,有问题就 rollback。如果我们需要配置事务的隔离性、传播性等,可以在 DefaultTransactionDefinition 对象中进行配置。,上面的代码是通过 PlatformTransactionManager 实现的编程式事务,我们也可以通过 TransactionTemplate 来实现编程式事务,如下:,直接注入 TransactionTemplate,然后在 execute 方法中添加回调写核心的业务即可,当抛出异常时,将当前事务标注为只能回滚即可。注意,execute 方法中,如果不需要获取事务执行的结果,则直接使用 TransactionCallbackWithoutResult 类即可,如果要获取事务执行结果,则使用 TransactionCallback 即可。,这就是两种编程式事务的玩法。,编程式事务由于代码入侵太严重了,因为在实际开发中使用的很少,我们在项目中更多的是使用声明式事务。,声明式事务如果使用 XML​ 配置,可以做到无侵入;如果使用 Java​ 配置,也只有一个 @Transactional 注解侵入而已,相对来说非常容易。,以下配置针对传统 SSM 项目(因为在 Spring Boot 项目中,事务相关的组件已经配置好了):,XML 配置声明式事务大致上可以分为三个步骤,如下:,配置事务管理器,配置事务通知,配置 AOP,第二步和第三步中定义出来的方法交集,就是我们要添加事务的方法。,配置完成后,如下一些方法就自动具备事务了:,我们也可以使用 Java 配置来实现声明式事务:,这里要配置的东西其实和 XML 中配置的都差不多,最最关键的就两个:,事务管理器 PlatformTransactionManager。,@EnableTransactionManagement 注解开启事务支持。,配置完成后,接下来,哪个方法需要事务就在哪个方法上添加 @Transactional 注解即可,向下面这样:,当然这个稍微有点代码入侵,不过问题不大,日常开发中这种方式使用较多。当@Transactional 注解加在类上面的时候,表示该类的所有方法都有事务,该注解加在方法上面的时候,表示该方法有事务。,也可以 Java 代码和 XML 混合配置来实现声明式事务,就是一部分配置用 XML 来实现,一部分配置用 Java 代码来实现:,假设 XML 配置如下:,那么 Java 代码中的配置如下:,Java 配置中通过 @ImportResource 注解导入了 XML 配置,XML 配置中的内容就是开启 @Transactional 注解的支持,所以 Java 配置中省略了 @EnableTransactionManagement 注解。,这就是声明式事务的几种配置方式。好玩吧!,在前面的配置中,我们只是简单说了事务的用法,并没有和大家详细聊一聊事务的一些属性细节,那么接下来我们就来仔细捋一捋事务中的五大属性。,首先就是事务的隔离性,也就是事务的隔离级别。,MySQL 中有四种不同的隔离级别,这四种不同的隔离级别在 Spring 中都得到了很好的支持。Spring 中默认的事务隔离级别是 default,即数据库本身的隔离级别是啥就是啥,default 就能满足我们日常开发中的大部分场景。,不过如果项目有需要,我们也可以调整事务的隔离级别。,调整方式如下:,如果是编程式事务,通过如下方式修改事务的隔离级别:,TransactionTemplate,TransactionDefinition 中定义了各种隔离级别。,PlatformTransactionManager,这里是在 DefaultTransactionDefinition 对象中设置事务的隔离级别。,如果是声明式事务通过如下方式修改隔离级别:,XML:,Java:,先来说说何谓事务的传播性:,事务传播行为是为了解决业务层方法之间互相调用的事务问题,当一个事务方法被另一个事务方法调用时,事务该以何种状态存在?例如新方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行,等等,这些规则就涉及到事务的传播性。,关于事务的传播性,Spring 主要定义了如下几种:,具体含义如下:,一共是七种传播性,具体配置也简单:,TransactionTemplate中的配置,PlatformTransactionManager中的配置,声明式事务的配置(XML),声明式事务的配置(Java),用就是这么来用,至于七种传播的具体含义,松哥来和大家一个一个说。,REQUIRED 表示如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。,例如我有如下一段代码:,我在 handle2 方法中调用 handle1。,那么:,如果 handle2 方法本身是有事务的,则 handle1 方法就会加入到 handle2 方法所在的事务中,这样两个方法将处于同一个事务中,一起成功或者一起失败(不管是 handle2 还是 handle1 谁抛异常,都会导致整体回滚)。,如果 handle2 方法本身是没有事务的,则 handle1 方法就会自己开启一个新的事务,自己玩。,举一个简单的例子:handle2 方法有事务,handle1 方法也有事务(小伙伴们根据前面的讲解自行配置事务),项目打印出来的事务日志如下:,从日志中可以看到,前前后后一共就开启了一个事务,日志中有这么一句:,这个就说明 handle1 方法没有自己开启事务,而是加入到 handle2 方法的事务中了。,REQUIRES_NEW 表示创建一个新的事务,如果当前存在事务,则把当前事务挂起。换言之,不管外部方法是否有事务,REQUIRES_NEW 都会开启自己的事务。,这块松哥要多说两句,有的小伙伴可能觉得 REQUIRES_NEW 和 REQUIRED 太像了,似乎没啥区别。其实你要是单纯看最终回滚效果,可能确实看不到啥区别。但是,大家注意松哥上面的加粗,在 REQUIRES_NEW 中可能会同时存在两个事务,外部方法的事务被挂起,内部方法的事务独自运行,而在 REQUIRED 中则不会出现这种情况,如果内外部方法传播性都是 REQUIRED,那么最终也只是一个事务。,还是上面那个例子,假设 handle1 和 handle2 方法都有事务,handle2 方法的事务传播性是 REQUIRED,而 handle1 方法的事务传播性是 REQUIRES_NEW,那么最终打印出来的事务日志如下:,分析这段日志我们可以看到:,从这段日志中大家可以非常明确的看到 REQUIRES_NEW 和 REQUIRED 的区别。,松哥再来简单总结下(假设 handle1 方法的事务传播性是 REQUIRES_NEW):,如果 handle2 方法没有事务,handle1 方法自己开启一个事务自己玩。,如果 handle2 方法有事务,handle1 方法还是会开启一个事务。此时,如果 handle2 发生了异常进行回滚,并不会导致 handle1 方法回滚,因为 handle1 方法是独立的事务;如果 handle1 方法发生了异常导致回滚,并且 handle1 方法的异常没有被捕获处理传到了 handle2 方法中,那么也会导致 handle2 方法回滚。,这个地方小伙伴们要稍微注意一下,我们测试的时候,由于是两个更新 SQL,如果更新的查询字段不是索引字段,那么 InnoDB 将使用表锁,这样就会发生死锁(handle2 方法执行时开启表锁,导致 handle1 方法陷入等待中,而必须 handle1 方法执行完,handle2 才能释放锁)。所以,在上面的测试中,我们要将 username 字段设置为索引字段,这样默认就使用行锁了。,NESTED 表示如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 TransactionDefinition.PROPAGATION_REQUIRED。,假设 handle2 方法有事务,handle1 方法也有事务且传播性为 NESTED,那么最终执行的事务日志如下:,关键一句在 Creating nested transaction。,此时,NESTED 修饰的内部方法(handle1)属于外部事务的子事务,外部主事务回滚的话,子事务也会回滚,而内部子事务可以单独回滚而不影响外部主事务和其他子事务(需要处理掉内部子事务的异常)。,MANDATORY 表示如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。,这个好理解,我举两个例子:,假设 handle2 方法有事务,handle1 方法也有事务且传播性为 MANDATORY,那么最终执行的事务日志如下:,从这段日志可以看出:,假设 handle2 方法无事务,handle1 方法有事务且传播性为 MANDATORY,那么最终执行时会抛出如下异常:,由于没有已经存在的事务,所以出错了。,SUPPORTS 表示如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。,这个也简单,举两个例子大家就明白了。,假设 handle2 方法有事务,handle1 方法也有事务且传播性为 SUPPORTS,那么最终事务执行日志如下:,这段日志很简单,没啥好说的,认准 Participating in existing transaction 表示加入到已经存在的事务中即可。,假设 handle2 方法无事务,handle1 方法有事务且传播性为 SUPPORTS,这个最终就不会开启事务了,也没有相关日志。,NOT_SUPPORTED 表示以非事务方式运行,如果当前存在事务,则把当前事务挂起。,假设 handle2 方法有事务,handle1 方法也有事务且传播性为 NOT_SUPPORTED,那么最终事务执行日志如下:,这段日志大家认准这两句就行了 : Suspending current transaction​ 表示挂起当前事务;Resuming suspended transaction 表示恢复挂起的事务。,NEVER 表示以非事务方式运行,如果当前存在事务,则抛出异常。,假设 handle2 方法有事务,handle1 方法也有事务且传播性为 NEVER,那么最终会抛出如下异常:,默认情况下,事务只有遇到运行期异常(RuntimeException 的子类)以及 Error 时才会回滚,在遇到检查型(Checked Exception)异常时不会回滚。,像 1/0,空指针这些是 RuntimeException,而 IOException 则算是 Checked Exception,换言之,默认情况下,如果发生 IOException 并不会导致事务回滚。,如果我们希望发生 IOException 时也能触发事务回滚,那么可以按照如下方式配置:,Java 配置:,XML 配置:,另外,我们也可以指定在发生某些异常时不回滚,例如当系统抛出 ArithmeticException 异常并不要触发事务回滚,配置方式如下:,Java 配置:,XML 配置:,只读事务一般设置在查询方法上,但不是所有的查询方法都需要只读事务,要看具体情况。,一般来说,如果这个业务方法只有一个查询 SQL,那么就没必要添加事务,强行添加最终效果适得其反。,但是如果一个业务方法中有多个查询 SQL,情况就不一样了:多个查询 SQL,默认情况下,每个查询 SQL 都会开启一个独立的事务,这样,如果有并发操作修改了数据,那么多个查询 SQL 就会查到不一样的数据。此时,如果我们开启事务,并设置为只读事务,那么多个查询 SQL 将被置于同一个事务中,多条相同的 SQL 在该事务中执行将会获取到相同的查询结果。,设置事务只读的方式如下:,Java 配置:,XML 配置:,超时时间是说一个事务允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。,事务超时时间配置方式如下(单位为秒):,Java 配置:,XML 配置:,在 TransactionDefinition 中以 int 的值来表示超时时间,其单位是秒,默认值为-1。,那么什么情况下事务会失效呢?,这个主要是针对声明式事务的,经过前面的介绍,小伙伴们其实也能够看出来,声明式事务底层其实就是 AOP,所以在声明式事务中,我们我们拿到的服务类并不是服务类本身,而是一个代理对象,在这个代理对象中的代理方法中,自动添加了事务的逻辑,所以如果我们直接方法自调用,没有经过这个代理对象,事务就会失效。,我写一段伪代码小伙伴们一起来看下:,此时,如果我们在 UserController 中注入 UserService,那么拿到的并不是 UserService 对象本身,而是通过动态代理为 UserService 生成的一个动态代理类,这个动态代理就类似下面这样(伪代码):,所以你最终调用的并不是 UserService 本身的方法,而是动态代理对象中的方法。,因此,如果存在这样的代码:,在 useSayHello 中调用 sayHello 方法,sayHello 方法上虽然有事务注解,但是这里的事务不生效(因为调用的不是的动态代理对象中的 sayHello 方法,而是当前对象 this 的 sayHello 方法)。,搞明白了 6.1,再来看 6.2 小节就很容易懂了。,如果我们在 sayHello 方法中将异常捕获了,那么动态代理类中的方法,就感知不知道目标方法发生异常了,自然也就不会自动处理事务回滚了。还是以前面的 UserServiceProxy 为例:,如果调用 调用父类 sayHello 的时候,sayHello 方法自动将异常捕获了,那么很明显,这里就不会进行异常回滚了。,这个算是 Spring 官方的一个强制要求了,声明式事务方法只能是 public,对于非 public 的方法如果想用声明式事务,那得上 AspectJ。,这个前面 5.3 小节介绍过了,默认情况下,只会捕获 RuntimeException,如果想扩大捕获范围,可以自行配置。,基于 6.1 小节的理解,来看这个应该也很好懂。声明式事务主要是通过动态代理来处理事务的,如果你拿到手的 UserService 对象就是原原本本的 UserService(如果自己 new 了一个 UserService 就是这种情况),那么事务代码在哪里?没有事务处理的代码,事务自然不会生效。,声明式事务的核心,就是动态代理生成的那个对象,没有用到那个对象,事务就没戏。,这个没啥好说,数据库不支持,Spring 咋配都没用。,好啦,这就是松哥和大家分享的 Spring 事务的玩法,不知道小伙伴们搞明白没有?

© 版权声明

相关文章