Spring Transaction 之 不离不弃生死相依 事务是什么?小A排了很长很长一条多米诺骨牌序列,小C拿着相机表示:“只要你排的没有岔子,这一定是一条超酷的素材”。诶,这就是事务,不可分割,结果只有成功或者失败,没有成功了一部分之说,如果小A的骨牌序列是完美的,那小C就能得到他想要的素材,但是只要中间除了定点岔子,小A小C就啥也没有了。
前言 引子的举例可能不是那么的准确,所以还是来看看Wikipedia 对事务的定义:
Transaction processing is information processing in computer science that is divided into individual, indivisible operations called transactions . Each transaction must succeed or fail as a complete unit;
事务处理是计算机科学中的信息处理,原子的、不可分割的操作被称作事务,每个完整单元有且只有成功或失败两个状态。
事务的具体操作是数据库层面的内容,本篇内容不会有过多提及,而Spring Transaction
更像是一个DB上层的管家,这次主要关注的是基于SpringAOP
的Spring Transaction
如何使我们无感知使用事务的同时肩负起事务的开启、回滚、提交操作。上次已经对Spring AOP
的实现有过相关分析,所以也请各位对AOP
门道还不是太清晰的小朋友移步**SpringAOP篇 **先看看,以获得本篇更愉快的阅读体验。
Ps: 本篇内容所有逻辑分析是基于Spring-Boot (v2.2.9.RELEASE) 的somke-test-data-jdbc项目。Git地址: https://github.com/spring-projects/spring-boot.git 。
北京猿人辈儿的事务实现 在正式开始接触Spring Transaction
之前,我们还是得按老规矩,先来看看北京猿人辈儿,纯JDBC时代,是怎么样开启、回滚、提交一个事务的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public void updateSomething (Connection con, Coffee coffee, MarketGoods goods) { String updateCoffeeSql = "UPDATE coffee SET price = ? WHERE type = ?" ; String updateMarketSql = "UPDATE market SET sale = sale + ? WHERE coffee_type = ?" ; try ( PreparedStatement updateCoffee = new PreparedStatement (updateCoffeeSql); PreparedStatement updateMarket = new PreparedStatement (updateCoffeeSql) ) { con.setAutoCommit(false ); updateCoffee.setBigDecimal(1 , coffee.getPrice()); updateCoffee.setString(2 , coffee.getString()); updateCoffee.executeUpdate(); ...doSomethingOthers(); updateMarket.setInt(1 , goods.getSale()); updateMarket.setString(2 , goods.getCoffeeType()); updateMarket.executeUpdate(); con.commit(); } catch (SQLException e) { logger.warn("execute transaction wrong" , e); if (con != null ) { try { con.rollback(); } catch (SQLException excep) { logger.warn("rollback failed" , e); } } } }
Spring Transaction的具体实现 在上边儿咱们过了一下基于原生JDBC的事务实现逻辑,整体来看还是挺冗长的,从局部的逻辑来看设置为手动提交,以及在执行完成或执行失败时提交和回滚操作都是通用的,我们只需要关系具体的数据操作实现。那么作为事务管家的Spring Transaction
是如何完成这部分的工作的呢?
准备阶段:管家报道 咱在前言里稍稍提到了一嘴,说Spring Transaction
是基于AOP
进行实现的,那么肯定有一个Advice
作为事务逻辑的具体执行者。先给大家透个底,具体执行者是一个叫TransactionInterceptor
的MethodInterceptor
类,它是哪来的呢?别急,我们慢慢看。
在“万物”溯源皆可溯到的spring-boot-autoconfigure
的项目中,spring.factories
文件里我们可以看到两个类配置,TransactionAutoConfiguration
和DataSourceTransactionManagerAutoConfiguration
,在DataSourceTransactionManagerAutoConfiguration
自动配置类里,会根据我们应用的数据源,向IoC
容器中注册事务管理器DateSourceTransactionManager
,而TransactionAutoConfiguration
则会在事务管理器完成注册后执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Configuration(proxyBeanMethods = false) @ConditionalOnBean(TransactionManager.class) @ConditionalOnMissingBean(AbstractTransactionManagementConfiguration.class) public static class EnableTransactionManagementConfiguration { ... @Configuration(proxyBeanMethods = false) @EnableTransactionManagement(proxyTargetClass = true) @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true", matchIfMissing = true) public static class CglibAutoProxyConfiguration { } }
根据Spring自动装配的执行逻辑,CglibAutoProxyConfiguration
会被认为是符合配置规则的配置类,撇开配置类的定义注解和配置规则的定义注解,那么现在我们能关注点就是在@EnableTransactionManagement
这个注解上。嗯?你问我Spring自动装配是什么?去看看这个 ,虽然挺长的,但或许能解开你那一星半点儿的疑惑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Import(TransactionManagementConfigurationSelector.class) public @interface EnableTransactionManagement { boolean proxyTargetClass () default false ; AdviceMode mode () default AdviceMode.PROXY; int order () default Ordered.LOWEST_PRECEDENCE; } protected String[] selectImports(AdviceMode adviceMode) { switch (adviceMode) { case PROXY: return new String [] {AutoProxyRegistrar.class.getName(), ProxyTransactionManagementConfiguration.class.getName()}; case ASPECTJ: return new String [] {determineTransactionAspectClass()}; default : return null ; } }
这个注解里引入了TransactionManagementConfigurationSelector
配置选择类,默认的切面模式是PROXY
,在配置选择类中根据切面模式,会加载一个叫ProxyTransactionManagementConfiguration
配置类。当然你也可以不使用Spring默认的PROXY
模式,转而使用ASPECTJ
模式,那么Spring Transaction
的切入方式就会由AspectJ
进行实现,本篇主要关注的是SpringAOP
实现的事务关理,所有关于AspectJ
的事务实现就不过多分析了。
在ProxyTransactionManagementConfiguration
的配置里,我们终于找到了最初的那个目标TransactionInterceptor
,还有另一个小伙伴AnnotationTransactionAttributeSource
,它俩相辅相成,但是我们可以晚点再说它。
认领服务对象 找到了事务逻辑的执行者后,我们得知道它是怎么和我们应用内需要事务执行的类做绑定的。上回书说道:
Spring在从缓存里捞出所有的Advice后,会将Advice列表和Bean元信息交由AopUtils#findAdvisorsThatCanApply
进行查找筛选并返回可匹配的Advice子集,其内部匹配逻辑由org.aspectj.weaver.patterns.Pointcut#match
负责。Pointcut#match
方法会判断目标类中的每个方法是否适配切面的要求,只要有一个方法符合要求,则会对目标类进行增强操作。
找到包裹了我们TransactionInterceptor
的Advice
类BeanFactoryTransactionAttributeSourceAdvisor
其成员属性pointcut的match()
方法。
1 2 3 4 5 @Override public boolean matches (Method method, Class<?> targetClass) { TransactionAttributeSource tas = getTransactionAttributeSource(); return (tas == null || tas.getTransactionAttribute(method, targetClass) != null ); }
在getTransactionAttribute
中,会对检查过的方法进行缓存,在后期具体执行事务逻辑时,若缓存中方法被标记为NULL_TRANSACTION_ATTRIBUTE
,则会跳过事务开启逻辑。由于篇幅限制,整个getTransactionAttribute
方法中,我们主要关注其中的一小段,感兴趣的小朋友可以前往阅读剩余逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 protected TransactionAttribute computeTransactionAttribute (Method method, @Nullable Class<?> targetClass) { if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) { return null ; } Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); TransactionAttribute txAttr = findTransactionAttribute(specificMethod); if (txAttr != null ) { return txAttr; } txAttr = findTransactionAttribute(specificMethod.getDeclaringClass()); if (txAttr != null && ClassUtils.isUserLevelMethod(method)) { return txAttr; } if (specificMethod != method) { txAttr = findTransactionAttribute(method); if (txAttr != null ) { return txAttr; } txAttr = findTransactionAttribute(method.getDeclaringClass()); if (txAttr != null && ClassUtils.isUserLevelMethod(method)) { return txAttr; } } return null ; }
在computeTransactionAttribute
方法中,我们可以看到Spring会在目标类上搜索@Transactional
注解,若注解存在,则会将配置信息在内部解析器中构建为RuleBasedTransactionAttribute
对象并缓存进attributeCache
对象中。而底层使用的则是java.lang.reflect
反射获取类注解信息,当Spring查找到类/方法上标记了@Transactional
注解时,就会将事务拦截器添加进目标类的切面链中,以进行后续的事务包装操作。
一点点小尾巴ノ)゚Д゚( 在SpringFramwork2.0.x版本的官方文档 中有过这么一段描述:"The Spring team's recommendation is that you only annotate concrete classes with the @Transactional
annotation, as opposed to annotating interfaces. You certainly can place the @Transactional
annotation on an interface (or an interface method), but this will only work as you would expect it to if you are using interface-based proxies . The fact that annotations are not inherited means that if you are using class-based proxies then the transaction settings will not be recognised by the class-based proxying infrastructure and the object will not be wrapped in a transactional proxy (which would be decidedly bad )",简单翻译一下就是:“Spring团队建议将@Transactional
注解标记在具体的实现类而非接口上,虽然注解可以被标记在接口或接口方法上,但只在其为基于接口代理 时才生效(即JDKProxy),由于注解的不可继承性,在使用基于类代理 时(即CglibProxy)无法识别事务设置,以至于对象不会被事务管理器所管理”。这也是现在网络上现存的大部分博客对@Transactional
注解进行提要时,会有“不建议在接口上进行标记” 的原因所在。
但是依据阿消(指自己)的测试和源码逻辑阅读,在SpringBoot2.2.9.RELEASE(SpringFramework5.2.8.RELEASE)
版本中,Spring的事务托管并不受代理实现是Cglib或是JDKProxy的影响,之和能否通过getTransactionAttribute()
在目标方法上获取到@Transactional注解元信息有关。由于目前Github上SpringFramework的源码版本最早只能追溯到3.0.x版本,所以依然无法再了解当时SpringTransaction的实现,有了解的朋友也欢迎分享一下。虽然确实依旧还是不建议把@Transactional注解标记在接口上(因为接口只是抽象,而是否需要事务是实现应该关心的),但是原因确实和是基于类的代理实现还是基于接口的代理实现没关系了。(在SpringFramwork5的文档上已经找不到相关描述了,当然如果是我的测试步骤有问题也希望大家指正)
事务执行:管家干活 在上一小节,我们知道了SpringTransaction
的事务管理器是怎么来的,以及它是怎么找到它需要服务的对象的。对象都找到了,那么作为资本家(bushi)就需要唆使打工人TransactionManager
开始干活了。由于前文关于SpringAOP
的流程分析是基于Cglib
增强的,所以这部分也会按照Cglib
的流程走,本篇可能会有较多的源码内容,也请大家耐心阅读。JDKProxy
的流程在大体上是相似的,所以也不再赘述了。
在Spring应用启动完成之后,IoC容器中存放着我们被增强后的类实例,而我们需要以事务方式执行的类或者方法,则会执行时被拦截器中的TransactionInterceptor
事务拦截器截获,进行Spring给我们抽象好的事务包装操作。让我来看看TransactionInterceptor
方法。
1 2 3 4 5 6 7 8 9 10 11 public Object invoke (MethodInvocation invocation) throws Throwable { Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null ); return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed); }
TransactionInterceptor
中的逻辑很简单,主要的核心方法就是TransactionAspectSupport#invokeWithinTransaction
,该方法内容比较多,整体挪过来也比较占篇幅,所以我会把部分我觉得重要的点提溜出来,剩余一些辅助性质的逻辑感兴趣的小朋友也可以自己进到方法里去看一看。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 protected Object invokeWithinTransaction (Method method, @Nullable Class<?> targetClass, final InvocationCallback invocation) throws Throwable { TransactionAttributeSource tas = getTransactionAttributeSource(); final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null ); final TransactionManager tm = determineTransactionManager(txAttr); PlatformTransactionManager ptm = asPlatformTransactionManager(tm); final String joinpointIdentification = methodIdentification(method, targetClass, txAttr); if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) { TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification); Object retVal; try { retVal = invocation.proceedWithInvocation(); } catch (Throwable ex) { completeTransactionAfterThrowing(txInfo, ex); throw ex; } finally { cleanupTransactionInfo(txInfo); } commitTransactionAfterReturning(txInfo); return retVal; } else { } }
整个事务的执行流程我们大致的先过了一遍,流程不是很复杂,相关的方法我都补充了注释,从注释当中我们能拎出几个核心方法:
TransactionAspectSupport#createTransactionIfNecessary
创建事务逻辑TransactionAspectSupport#completeTransactionAfterThrowing
事务异常流程处理逻辑TransactionAspectSupport#commitTransactionAfterReturning
事务正常流程处理逻辑 我们就按顺序一个一个往下看。
创建事务 - 起个好头 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 protected TransactionInfo createTransactionIfNecessary (@Nullable PlatformTransactionManager tm, @Nullable TransactionAttribute txAttr, final String joinpointIdentification) { if (txAttr != null && txAttr.getName() == null ) { txAttr = new DelegatingTransactionAttribute (txAttr) { @Override public String getName () { return joinpointIdentification; } }; } TransactionStatus status = null ; if (txAttr != null ) { if (tm != null ) { status = tm.getTransaction(txAttr); } else { if (logger.isDebugEnabled()) { logger.debug("Skipping transactional joinpoint [" + joinpointIdentification + "] because no transaction manager has been configured" ); } } } return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status); }
当方法不存在事务属性配置时,根据运行逻辑,不会进行相关的事务创建,而事务的创建过程getTransaction
,则被抽象到了AbstractPlatformTransactionManager
类上,该方法中会获取方法事务配置的事务传播等级、隔离等级,是否只读、过期时间等配置,而后会根据事务的传播等级进行相关的逻辑操作,譬如PROPAGATION_MANDATORY
等级会在没有事务存在时抛出异常。具体每个传播等级所对应执行的逻辑操作这次就不细说了,我们主要来看看其中的startTranaction
以及其中的doBegin
逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 private TransactionStatus startTransaction (TransactionDefinition definition, Object transaction, boolean debugEnabled, @Nullable SuspendedResourcesHolder suspendedResources) { boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); DefaultTransactionStatus status = newTransactionStatus( definition, transaction, true , newSynchronization, debugEnabled, suspendedResources); doBegin(transaction, definition); prepareSynchronization(status, definition); return status; } protected void doBegin (Object transaction, TransactionDefinition definition) { DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; Connection con = null ; try { if (!txObject.hasConnectionHolder() || txObject.getConnectionHolder().isSynchronizedWithTransaction()) { Connection newCon = obtainDataSource().getConnection(); if (logger.isDebugEnabled()) { logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction" ); } txObject.setConnectionHolder(new ConnectionHolder (newCon), true ); } txObject.getConnectionHolder().setSynchronizedWithTransaction(true ); con = txObject.getConnectionHolder().getConnection(); Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition); txObject.setPreviousIsolationLevel(previousIsolationLevel); txObject.setReadOnly(definition.isReadOnly()); if (con.getAutoCommit()) { txObject.setMustRestoreAutoCommit(true ); if (logger.isDebugEnabled()) { logger.debug("Switching JDBC Connection [" + con + "] to manual commit" ); } con.setAutoCommit(false ); } prepareTransactionalConnection(con, definition); txObject.getConnectionHolder().setTransactionActive(true ); int timeout = determineTimeout(definition); if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) { txObject.getConnectionHolder().setTimeoutInSeconds(timeout); } if (txObject.isNewConnectionHolder()) { TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder()); } } catch (Throwable ex) { if (txObject.isNewConnectionHolder()) { DataSourceUtils.releaseConnection(con, obtainDataSource()); txObject.setConnectionHolder(null , false ); } throw new CannotCreateTransactionException ("Could not open JDBC Connection for transaction" , ex); } }
在doBegin
方法中,我们最初想要找的,在内容开头写的那个“北京猿人辈儿”的事务开启逻辑,已经出现了。还有一个小重点,SpringTransaction
中对数据源、连接、事务信息、方法切入点等信息绑定到线程的操作,调用的TransactionSynchronizationManager
逻辑,都是通过ThreadLocal
对象进行实现的,这也就是在标记为事务的方法上通过异步逻辑调用其它事务方法,会导致事务失效的原因。ThreadLocal
对象的分析我们之后有机会再说,看到这个点的时候,或许…可以考虑一下在多线程执行事务方法时,如何保证事务不失效的逻辑。
回滚事务 - 诶呀,出错了 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 protected void completeTransactionAfterThrowing (@Nullable TransactionInfo txInfo, Throwable ex) { if (txInfo != null && txInfo.getTransactionStatus() != null ) { if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) { try { txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus()); } catch (Exception ex2) { } } else { try { txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); } catch (Exception ex2) { } } } } protected void doRollback (DefaultTransactionStatus status) { DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); Connection con = txObject.getConnectionHolder().getConnection(); if (status.isDebug()) { logger.debug("Rolling back JDBC transaction on Connection [" + con + "]" ); } try { con.rollback(); } catch (SQLException ex) { throw new TransactionSystemException ("Could not roll back JDBC transaction" , ex); } }
在事务的回滚逻辑中还有一个点需要注意,即TransactionAttribute#rollbackOn
方法的判定是否需要回滚,由于SpringTransactionAnnotationParser
默认构建的是RuleBasedTransactionAttribute
,在解析时会将rollbackFor
、rollbackForClassName
封装成RollbackRuleAttribute
对象,将noRollbackFor
、noRollbackForClassName
封装成NoRollbackRuleAttribute
对象并塞进rules列表中,进行后续的回滚判断操作依据,所以我们就来看看它的回滚逻辑是怎样的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public boolean rollbackOn (Throwable ex) { RollbackRuleAttribute winner = null ; int deepest = Integer.MAX_VALUE; if (this .rollbackRules != null ) { for (RollbackRuleAttribute rule : this .rollbackRules) { int depth = rule.getDepth(ex); if (depth >= 0 && depth < deepest) { deepest = depth; winner = rule; } } } if (winner == null ) { return super .rollbackOn(ex); } return !(winner instanceof NoRollbackRuleAttribute); }
从以上逻辑能获取几个信息:
所有的RuntimeException
和Error都会进行回滚,除非RuntimeException
被配置进了noRollbackFor
列表中 回采取优胜者回滚模式进行回滚,譬如子类异常配置为不回滚,父类异常配置为回滚,因为子类异常深度更小,所以会采取子类异常的不回滚操作 提交事务 - 事情做完惹 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 protected void commitTransactionAfterReturning (@Nullable TransactionInfo txInfo) { if (txInfo != null && txInfo.getTransactionStatus() != null ) { txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); } } protected void doCommit (DefaultTransactionStatus status) { DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); Connection con = txObject.getConnectionHolder().getConnection(); try { con.commit(); } catch (SQLException ex) { throw new TransactionSystemException ("Could not commit JDBC transaction" , ex); } }
尾巴 至此,我大概是把SpringTransaction
讲清楚了吧,从头看到现在的你辛苦了,也希望你通过这个流程,知道了Spring事务在执行过程中为什么会失效,譬如没有作为SpringBean
被IoC
容器所管理的Bean不会被进行增强;在本类中进行this.xxx
方法调用也不会触发事务逻辑,因为使用的是本类对象而不是增强对象;@Transactional
注解被打在了非Public
方法上,因为Cglib
不会对非Public
方法进行增强操作;异常被你用try/catch
捕捉了导致没有触发回滚,因为TransactionInterceptor
是在最外层进行的catch
中进行的回滚逻辑判断。
总之,Spring是个怪兽,我不期望我有一天会把它理解的透彻,只希望在哪天碰到了什么问题,我会知道问题出在哪里,我该怎么解决。
Ps: 最近挺忙的,但是总算还是把这篇内容写完了,很长,我也知道能真真儿的读完实属不易,这篇内容写完我就要准备回家过年的事情了,也不知道有多少人会留在工作地过年…无论如何,新年快乐,还有啊…祝你在新的一年里,有人能与你共悲欢。我是阿消,新年快乐。