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

61张图,剖析Spring事务,就是要水落石出!

时间:2023-03-12 06:56:08 科技观察

大家好,我是楼仔!下面我先简单介绍一下Spring事务的基础知识和使用方法,然后直接拆源码。不用BB,放在文章目录下。1.项目准备需要搭建环境的同学,详见代码:https://github.com/lml200701158/program_demo/tree/main/spring-transaction下面是DB数据和DB操作界面:uidunameusex1ZhangSannv2ChenHengnan3LouZinan//接口提供publicinterfaceUserDao{//select*fromuser_testwhereuid="#{uid}"publicMyUserselectUserById(Integeruid);//updateuser_testsetuname=#{uname},usex=#{usex}whereuid=#{uid}publicintupdateUser(MyUseruser);}基本测试代码,testSuccess()是事务生效时:@ServicepublicclassLouzai{@AutowiredprivateUserDaouserDao;publicvoidupdate(Integerid){MyUseruser=newMyUser();user.setUid(id);user.setUname("张三-测试");user.setUsex("女");userDao.updateUser(用户);}publicMyUserquery(Integerid){MyUseruser=userDao.selectUserById(id);返回用户;}//正常情况@Transactional(rollbackFor=Exception.class)publicvoidtestSuccess()throwsException{Integerid=1;我的用户user=query(id);System.out.println("原始记录:"+user);更新(编号);thrownewException("事务生成");}}执行入口:publicclassSpringMyBatisTest{publicstaticvoidmain(String[]args)throwsException{StringxmlPath="applicationContext.xml";ApplicationContextapplicationContext=newClassPathXmlApplicationContext(xmlPath);楼仔uc=(楼仔)applicationContext.getBean("楼仔");uc.testSuccess();}}输出:16:44:38.267[main]DEBUGorg.springframework.beans.factory.support.DefaultListableBeanFactory-创建单例bean的共享实例'org.springframework.transaction.interceptor.TransactionInterceptor#0'16:44:38.363[main]DEBUGorg.springframework.beans.factory.support.DefaultListableBeanFactory-创建单例bean'txManager'16:44:40.966的共享实例[main]DEBUGorg.springframework.jdbc.datasource.DataSourceTransactionManager-创建名称为[com.mybatis.controller.楼载.testSuccess]:PROPAGATION_REQUIRED,ISOLATION_DEFAULT,-java.lang.Exception16:44:40.968[main]DEBUGorg.springframework.jdbc.datasource.DriverManagerDataSource-创建新的JDBCDriverManager连接到[jdbc:mysql://127.0.0.1:3306/java_study?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimeznotallow=Asia/Shanghai]16:44:41.228[main]DEBUGorg.springframework.jdbc.datasource.DataSourceTransactionManager-获得连接[com.mysql.cj.jdbc.ConnectionImpl@5b5caf08]对于JDBCtransaction16:44:41.231[main]DEBUGorg.springframework.jdbc.datasource.DataSourceTransactionManager-切换JDBC连接[com.mysql.cj.jdbc.ConnectionImpl@5b5caf08]到手动提交记录:MyUser(uid=1,uname=张三,usex=女)16:42:59.345[main]DEBUGorg.springframework.jdbc.datasource.DataSourceTransactionManager-发起事务回滚16:42:59.346[main]DEBUGorg.springframework.jdbc.datasource.DataSourceTransactionManager-滚动b在连接上确认JDBC事务[com.mysql.cj.jdbc.ConnectionImpl@70807224]16:42:59.354[main]DEBUGorg.springframework.jdbc.datasource.DataSourceTransactionManager-释放JDBC连接[com.mysql.cj.jdbc.ConnectionImpl@70807224]aftertransactionExceptioninthread"main"java.lang.Exception:Transactiontakeseffectatcom.mybatis.controller.Louzai.testSuccess(Louzai.java:34)//异常日志省略...2.Spring事务工作流为了方便大家更好的理解后面的源码,我先整体介绍一下源码的执行过程,让大家有个整体的认识,不然很容易被绕过。整个Spring事务源码其实分为两部分,我们将结合上面的例子,来给大家讲解一下。第一部分是后处理。我们在创建楼仔Bean的post-processor中会做两件事:获取楼仔的切面方法:首先,我们会获取所有的切面信息,并将其与楼仔的所有方法进行匹配。然后找到楼仔的所有需要??进行事务处理的方法,如果匹配方法成功,则需要将事务属性保存在缓存attributeCache中。创建AOP代理对象:结合楼仔的AOP方法,选择Cglib或JDK,创建AOP代理对象。第二块是交易执行。整个逻辑比较复杂。我只选择了4个核心逻辑,分别是从缓存中获取事务属性、创建和开启事务、执行业务逻辑、提交或回滚事务。3.源码解读注意:Spring版本是5.2.15.RELEASE,否则和我的代码不一样!!!上面的知识点都不难,下面就是我们的重头戏,让你跟着楼子走一遍代码流程。3.1代码入口这里需要多跑几次,跳过前面的beanName,只看louzai。输入doGetBean()进入创建Bean的逻辑。进入createBean(),调用doCreateBean()。进入doCreateBean(),调用initializeBean()。如果你看过我之前系列的源码,你应该对这个入口非常熟悉,它实际上是用来创建代理对象的。3.2创建代理对象这里是重点!敲黑板!!!首先获取louzai类的所有aspect列表;创建一个AOP代理对象。3.2.1获取aspectlist这里有两个重要的方法。首先执行findCandidateAdvisors(),稍后我们会返回到findEligibleAdvisors()。依次返回并返回findEligibleAdvisors()。输入canApply()开始匹配楼仔的切面。这就是重点!敲黑板!!!这里只会匹配Louzai.testSuccess()方法,我们直接进入匹配逻辑。如果匹配成功,交易的属性配置信息也会被放入attributeCache缓存中。我们依次返回getTransactionAttribute(),查看放入缓存的数据。回到本节开头,我们获取楼仔的切面信息来创建AOP代理对象。3.2.2创建AOP代理对象创建AOP代理对象的逻辑在上一篇文章(SpringAOP)中有说明。我通过Cglib创建了它。有兴趣的同学可以关注公众号「楼子」,阅读楼子历史文章。3.3事务执行回到业务逻辑,开始通过louzai的AOP代理对象执行main方法。因为代理对象是由Cglib创建的,所以是由Cglib执行的。这就是重点!敲黑板!!!以下代码是事务执行invokeWithinTransaction()的核心逻辑。protectedObjectinvokeWithinTransaction(Methodmethod,@NullableClasstargetClass,finalInvocationCallbackinvocation)throwsThrowable{//获取我们的事务属性源对象TransactionAttributeSourcetas=getTransactionAttributeSource();//通过交易属性源对象获取我们交易的属性信息finalTransactionAttributetxAttr=(tas!=null?tas.getTransactionAttribute(method,targetClass):null);//获取我们配置的事务管理器对象finalPlatformTransactionManagertm=determineTransactionManager(txAttr);//从tx属性对象中获取@Transactionl标记的方法描述符finalStringjoinpointIdentification=methodIdentification(method,targetClass,txAttr);//处理声明式交易if(txAttr==null||!(tminstanceofCallbackPreferringPlatformTransactionManager)){//是否需要创建交易TransactionInfotxInfo=createTransactionIfNecessary(tm,txAttr,joinpointIdentification);对象返回值;try{//调用钩子函数回调目标方法retVal=invocation.proceedWithInvocation();}catch(Throwableex){//抛出异常进行回滚处理completeTransactionAfterThrowing(txInfo,ex);扔前;}finally{//清除我们线程变量cleanupTransactionInfo中transactionInfo的值(txInfo);}//提交事务commitTransactionAfterReturning(txInfo);返回值;}//程序化事务else{//这不是我们的重点,省略...}}3.3.1获取事务属性在invokeWithinTransaction()中,我们找到获取事务属性的入口,从attributeCache中获取事务缓存数据。缓存数据保存在“2.2.1获取AspectList”中。3.3.2创建交易通过doGetTransaction()获取交易。protectedObjectdoGetTransaction(){//创建数据源事务对象DataSourceTransactionObjecttxObject=newDataSourceTransactionObject();//是否允许当前事务设置保持点txObject.setSavepointAllowed(isNestedTransactionAllowed());/***TransactionSynchronizationManager事务同步管理器对象(该类中所有局部线程变量)*用于保存当前事务的信息。第一次,我们这里从线程变量中获取事务连接持有者对象,以数据源为key获取。*由于第一次进来事务没有存储在我们的事务同步管理器中。所以此时获取的conHolder为null*/ConnectionHolderconHolder=(ConnectionHolder)TransactionSynchronizationManager.getResource(obtainDataSource());txObject.setConnectionHolder(conHolder,false);//返回交易对象returntxObject;}通过startTransaction()启动事务。下面是开启交易的详细逻辑,看懂就好。protectedvoiddoBegin(Objecttransaction,TransactionDefinitiondefinition){//强制转换交易对象DataSourceTransactionObjecttxObject=(DataSourceTransactionObject)transaction;连接con=null;try{//判断事务对象没有数据库连接持有者if(!txObject.hasConnectionHolder()||txObject.getConnectionHolder().isSynchronizedWithTransaction()){//获取数据库连接对象ConnectionnewCon=obtainDataSource().getConnection();if(logger.isDebugEnabled()){logger.debug("AcquiredConnection["+newCon+"]forJDBCtransaction");}//将我们的数据库连接打包成一个ConnectionHolder对象,并设置为我们的txObject对象txObject.setConnectionHolder(newConnectionHolder(newCon),true);}//将当前连接标记为同步事务txObject.getConnectionHolder().setSynchronizedWithTransaction(true);con=txObject.getConconnectionHolder().getConnection();//设置当前事务的隔离级别IntegerpreviousIsolationLevel=DataSourceUtils.prepareConnectionForTransaction(con,definition);txObject.setPreviousIsolationLevel(previousIsolationLevel);//关闭自动提交if(con.getAutoCommit()){txObject.setMustRestoreAutoCommit(true);if(logger.isDebugEnabled()){logger.debug("将JDBC连接["+con+"]切换为手动提交");}con.setAutoCommit(false);}//判断事务PrepareTransactionalConnection(con,definition)为只读事务;//设置交易激活txObject.getConnectionHolder().setTransactionActive(true);//设置事务超时inttimeout=determineTimeout(definition);如果(超时!=TransactionDefinition.TIMEOUT_DEFAULT){txObject.getConnectionHolder()。setTimeoutInSeconds(超时);}//绑定我们的数据源并连接到我们的同步管理器。将数据源设置为键,将数据库连接设置为线程变量的值if(txObject.isNewConnectionHolder()){}}catch(Throwableex){if(txObject.isNewConnectionHolder()){//释放数据库连接DataSourceUtils.releaseConnection(con,obtainDataSource());txObject.setConnectionHolder(null,false);}thrownewCannotCreateTransactionException("无法打开事务的JDBC连接",ex);}}最后,返回invokeWithinTransaction()并获取txInfo对象。输入真正的业务逻辑。执行完成后抛出异常,依次返回,后续的回滚事务逻辑。3.3.4回滚事务还在invokeWithinTransaction()中,进入回滚事务的逻辑。.执行回滚逻辑很简单,我们只看如何判断是否回滚。如果抛出的异常类型与事务定义的异常类型匹配,则证明需要捕获该异常。之所以使用递归,不仅是为了判断抛出的异常本身,还要判断其继承的父异常,满足其中一个即可捕获。至此,所有流程结束。4.结论让我们来看看下一节。文章首先介绍了交易的使用示例和交易的执行过程。然后分析交易的源码,分为2部分:首先匹配louzai对象的所有关于交易的aspect列表,将匹配到的交易属性保存到缓存中;从缓存中取出事务属性,然后创建并启动事务,并执行业务逻辑,最后提交或回滚事务。