当前位置: 首页 > 科技观察

本文将带你深入了解Spring事务的原理

时间:2023-03-20 17:17:23 科技观察

Spring事务的基本原理Spring事务的本质其实就是数据库对事务的支持。没有数据库的事务支持,spring无法提供事务功能。对于纯JDBC操作的数据库,如果要使用事务,可以按照以下步骤进行:获取连接Connectioncon=DriverManager.getConnection()启动事务con.setAutoCommit(true/false);执行CRUD提交事务/回滚事务con.commit()/con.rollback();关闭连接conn.close();使用Spring的事务管理功能后,我们可以不再编写步骤2和4的代码,而是由Spirng自动完成。那么Spring是如何在我们写的CRUD前后开启和关闭事务的呢?解决这个问题,我们可以从整体上理解Spring的事务管理的实现原理。下面简单介绍一下。注解方法就是一个例子。在配置文件中开启注解驱动,使用注解@Transactional来标记相关的类和方法。spring启动时,会解析并生成相关的bean。这时候它会检查有相关注解的类和方法,并为这些类和方法生成代理,根据@Transaction的相关参数进行相关的配置注入,这样就可以在代理中使用了,我们已经处理了相关的事务(打开正常的提交事务,异常回滚事务)。3、真实数据库层的事务提交和回滚是通过binlog或者redolog来实现的。Spring的事务机制所有的数据访问技术都有事务处理机制。这些技术提供API来开启事务,提交事务完成数据操作,或者发生错误时回滚数据。Spring的事务机制采用统一的机制来处理不同数据访问技术的事务处理。Spring的事务机制提供了一个PlatformTransactionManager接口。不同数据访问技术的事务使用不同的接口实现,如表所示。数据访问技术及实现程序中定义事务管理器的代码如下:@BeanpublicPlatformTransactionManagertransactionManager(){JpaTransactionManagertransactionManager=newJpaTransactionManager();transactionManager.setDataSource(dataSource());returntransactionManager;}声明式事务Spring支持声明式事务,即就是,使用注解来选择需要使用事务的方法,它在方法上使用@Transactional注解来表示该方法需要事务支持。这是一个基于AOP的操作实现。@TransactionalpublicvoidsaveSomething(Longid,Stringname){//数据库操作}这里需要注意的是,这个@Transactional注解来自于org.springframework.transaction.annotation包,而不是javax.transaction。AOP代理的两种实现:jdk是代理接口,接口中不能存在私有方法,这样就不会被拦截;cglib是一个子类,私有方法仍然不会出现在子类中,也不能被拦截intercept。Java动态代理。具体分为以下四个步骤:通过实现InvocationHandler接口创建自己的调用处理器;通过为Proxy类指定一个ClassLoader对象和一组接口来创建一个动态代理类;通过反射机制获取动态代理类的构造函数,其唯一参数类型为调用处理器接口类型;通过构造函数创建一个动态代理类实例,调用处理器对象在构造时作为参数传入。GCLIB代理cglib(CodeGenerationLibrary)是一个功能强大、高性能、高质量的代码生成库。它可以在运行时扩展Java类并实现Java接口。cglib封装了asm,可以在运行时动态生成新的类(子类)。cglib用于AOP,jdk中的代理必须基于接口,而cglib没有这个限制。原理区别:Java动态代理使用反射机制生成一个实现代理接口的匿名类,在调用具体方法之前调用InvokeHandler进行处理。cglib动态代理使用asm开源包加载代理对象类的class文件,通过修改其字节码生成子类进行处理。如果目标对象实现了接口,默认情况下会使用JDK的动态代理来实现AOP。如果目标对象实现了接口,可以强制使用CGLIB实现AOP。如果目标对象没有实现接口,则必须使用CGLIB库,spring会在JDK动态自动实现如果代理和CGLIB之间的转换是类的内部方法,而不是直接使用代理,你可以在这个时候维护一个它自己的实例的代理。@ServicepublicclassPersonServiceImplimplementsPersonService{@AutowiredPersonRepositorypersonRepository;//注入自己的代理对象,该类内部的方法调用事务的传递性会生效@AutowiredPersonServiceselfProxyPersonService;/***测试事务的传递性**@paramperson*@return*/@TransactionalpublicPersonsave(Personperson){Personp=personRepository.save(person);try{//新事务独立回滚selfProxyPersonService.delete();}catch(Exceptione){e.printStackTrace();}try{//使用当前事务返回所有rollselfProxyPersonService.save2(person);}catch(Exceptione){e.printStackTrace();}personRepository.save(person);returnp;}@Transactionalpublicvoidsave2(Personperson){personRepository.save(person);thrownewRuntimeException();}@Transactional(propagation=Propagation.REQUIRES_NEW)publicvoiddelete(){personRepository.delete(1L);thrownewRuntimeException();}}Spring事务的Propagation属性所谓spring事务的ed传播属性定义了当多个事务同时存在时,spring这些事务应该如何表现。这些属性定义在TransactionDefinition中,具体常量的解释如下表所示:数据库隔离级别脏读:一个事务对数据进行增删改,但没有提交,另一个事务可以读取未提交的数据。如果此时回滚第一个事务,第二个事务就会读到脏数据。不可重复读:一次事务中发生两次读操作。在第一个读取操作和第二个操作之间,另一个事务修改了数据。这时候两次读取的数据是不一致的。幻读:第一个事务批量修改了某个范围内的数据,第二个事务在这个范围内添加了一条数据。这时候第一个事务就会丢失对新增数据的修改。总结:隔离级别越高,越能保证数据的完整性和一致性,但对并发性能的影响也越大。大多数数据库默认的隔离级别是ReadCommited,比如SqlServer和Oracle,少数数据库默认的隔离级别是:RepeatableRead例如:MySQLInnoDBSpring中隔离级别事务的嵌套基于以上理论知识,我们大致知道了数据库事务和spring事务的一些属性和特点,接下来我们分析一些嵌套的事务场景,深入理解spring事务传播的机制。假设外层ServiceA的MethodA()调用了内层ServiceB的MethodB()PROPAGATION_REQUIRED(spring默认)。如果ServiceB.methodB()的事务级别定义为PROPAGATION_REQUIRED,那么在ServiceA.methodA()执行时,sp??ring已经启动了一个事务,此时调用ServiceB.methodB(),ServiceB.methodB()看到它已经在ServiceA.methodA()的事务内部运行,并且不会启动新的事务。如果ServiceB.methodB()运行时发现自己不在事务中,就会给自己分配一个事务。这样,如果在ServiceA.methodA()或ServiceB.methodB()中的任何地方发生异常,事务将被回滚。PROPAGATION_REQUIRES_NEW比如我们设计ServiceA.methodA()的事务级别为PROPAGATION_REQUIRED,ServiceB.methodB()的事务级别为PROPAGATION_REQUIRES_NEW。那么当ServiceB.methodB()执行时,ServiceA.methodA()所在的事务会被挂起,ServiceB.methodB()会启动一个新的事务,等待ServiceB.methodB()的事务完成,然后它会继续执行。他和PROPAGATION_REQUIRED的区别在于事务的回滚程度。因为ServiceB.methodB()是一个新事务,所以有两个不同的事务。如果ServiceB.methodB()已经提交,那么ServiceA.methodA()回滚失败,ServiceB.methodB()也不会回滚。如果ServiceB.methodB()回滚失败,如果他抛出的异常被ServiceA.methodA()捕获到,ServiceA.methodA()事务可能还是会提交(主要看B抛出的异常是不是异常A将回滚)。PROPAGATION_SUPPORTS假设ServiceB.methodB()的事务级别为PROPAGATION_SUPPORTS,那么在执行ServiceB.methodB()时,如果发现ServiceA.methodA()开启了事务,则加入当前事务,如果ServiceA.methodA()找到了如果你不开启事务,你自己就没有开启事务。这时,内层方法的事务性完全依赖于最外层的事务。PROPAGATION_NESTED现在情况变得更复杂了。ServiceB.methodB()的事务属性配置为PROPAGATION_NESTED。此时两人将如何合作?如果ServiceB#methodB回滚,则内部事务(即ServiceB#methodB)会回滚到执行前的SavePoint,而外部事务(即ServiceA#methodA)可以有以下两种处理方式:捕获异常,执行异常分支逻辑voidmethodA(){try{ServiceB.methodB();}catch(SomeException){//执行其他业务,比如ServiceC.methodC();}}这个方法也是最有价值的嵌套事务的地方,有分支执行的作用,如果ServiceB.methodB失败,则执行ServiceC。methodC(),而ServiceB.methodB在执行之前已经回滚到SavePoint,所以不会产生脏数据(相当于这个方法从来没有执行过),这个特性可以用在一些特殊的业务中,而PROPAGATION_REQUIRED既不是PROPAGATION_REQUIRES_NEW有办法做到这一点。b.外部事务回滚/提交代码不做任何改变,那么如果内部事务(ServiceB#methodB)回滚,那么首先ServiceB.methodB回滚到执行前的SavePoint(无论如何),外部事务(即就是,ServiceA#methodA)会根据具体的配置来决定是commit还是rollback。其他三个事务传播属性基本不用,这里不再分析。总结对于项目中需要使用事务的地方,我建议开发者还是使用spring的TransactionCallback接口来实现事务。不要盲目使用spring事务注解。如果必须使用注解,则必须了解spring事务传播机制和隔离级别。详细了解,否则很可能会出现意想不到的效果。SpringBoot通过org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration类支持事务。我们可以看到SpringBoot自动开启了对注解事务的支持。Spring只读事务的一些概念(@Transactional(readOnly=true)):从此时设置的时间点(时间点a)到事务结束的过程中,其他事务提交的数据不会被本次交易可见!(其他人在时间点a之后提交的数据不会出现在query中)。@Transcational(readOnly=true)这个注解一般写在业务类或其方法上,为其添加事务控制。当括号中加上readOnly=true时,会告诉底层数据源这是一个只读事务。对于JDBC,只读事务会有一定的速度优化。这样,事务控制的其他配置都采用默认值,事务的隔离级别为DEFAULT,也就是遵循底层数据源的隔离级别,事务的传播行为是REQUIRED,所以还是会有be事务存在,代码中抛出RuntimeException,仍然会导致事务回滚。应用:如果一次只执行一条查询语句,则不需要启用事务支持。数据库默认支持SQL执行期间的读一致性;如果一次执行多条查询语句,比如统计查询、报表查询等,这种场景下,多条查询SQL必须保证整体读的一致性,否则在上一次SQL查询之后,第二次SQL查询之前,数据发生变化被其他用户调用,那么本次的整体统计查询就会出现读取数据不一致的情况,应该启用交易支持。