拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Spring声明式事务源码全解析:从@Transactional失效到代理机制

1. 从一次事务失效排查说起的整体脉络1.1 为什么用了 Transactional 还是没回滚前阵子帮同事排查一个线上问题场景非常简单一个Service类里的入口方法加了Transactional方法内做了三次数据库更新中间第八行抛了一个RuntimeException子类异常按道理整个方法应该回滚。但数据实际入库了且没有任何人手动commit过。排查到最后原因特别普通入口方法是被同类里的另一个方法this.xxx()调用的。也就是说事务注解加在了方法上但调用方并没有经过Spring生成的代理对象而是直接调用了原始Bean的方法事务拦截器根本没有机会介入。这个结论大家都听过无数次可真正让我下定决心把源码链路完整过一遍的是后面同事问我的那句那你说说Spring到底是在哪一步发现方法上有Transactional的事务是什么时候和数据库连接绑定的说实话如果只是停留在知道这一层遇到稍微变种的问题还是会懵。声明式事务之所以神是因为它把很多事藏在了框架内部注解解析、AOP代理、事务拦截、连接绑定、提交回滚决策每一步单独拿出来都不难但串在一起就很容易让人抓不住主线。这篇文章就是我通读Spring声明式事务源码后的完整复盘会把这条链路上每个关键节点的源码位置、执行顺序和设计动机都理一遍。1.2 声明式事务的全流程由三件事组成声明式事务这套机制本质上可以压缩成三件事容器启动阶段EnableTransactionManagement把事务相关的基础设施Bean注册进容器包括事务Advisor、注解解析器、事务拦截器。Bean创建阶段Spring在实例化Bean之后通过后置处理器发现某个Bean需要被事务增强于是生成代理对象并替换原有Bean。方法调用阶段代理对象被调用时事务拦截器开始工作负责开启事务、执行业务逻辑、提交或回滚事务同时保证整个事务期间所有DAO操作拿到的都是同一个数据库连接。这三件事层层递进任何一环出了问题表现都是事务没生效。比如第一环里EnableTransactionManagement漏了或位置不对容器里压根没有事务基础设施第二环里目标方法不是public后置处理器判断不需要增强代理上自然没有切面第三环里异常被try-catch吞掉拦截器看到的是正常返回自然走提交分支。可以用一个生活化的方式来理解代理对象是小区门卫Transactional注解是贴在业主家门上的访客登记须知事务拦截器就是严格执行登记流程的门检员。如果你从室内通过自家内部通道直接到另一个房间自调用门卫根本看不到你须知上的内容自然也就不会被执行。弄懂了这三层关系后面所有源码细节都只是往这三个框里填充具体实现。2. 注解如何变成事务切面从 EnableTransactionManagement 开始的装配链路2.1 ProxyTransactionManagementConfiguration 里注册的三个 BeanEnableTransactionManagement的实现并不复杂核心是下面两段Import(TransactionManagementConfigurationSelector.class) public interface EnableTransactionManagement { boolean proxyTargetClass() default false; AdviceMode mode() default AdviceMode.PROXY; int order() default Ordered.LOWEST_PRECEDENCE; }TransactionManagementConfigurationSelector在PROXY模式下会向容器中注册两个配置类一个是AutoProxyRegistrar负责注册AOP自动代理创建器另一个就是ProxyTransactionManagementConfiguration事务核心Bean全部在这里完成注册。打开ProxyTransactionManagementConfiguration你能看到三个Bean方法这三个Bean构成了事务能力的基础设施BeanFactoryTransactionAttributeSourceAdvisor事务Advisor它是Spring AOP中切面的载体内部同时持有Pointcut匹配哪些方法需要事务和Advice匹配上之后执行什么逻辑。AnnotationTransactionAttributeSource事务属性解析器负责从方法、类上读取Transactional注解并解析成Spring内部的事务属性对象TransactionAttribute。TransactionInterceptor事务拦截器真正干活的Advice所有的事务开启、提交、回滚逻辑最终都汇聚到它的invoke方法里。三个Bean的协作关系很清晰AnnotationTransactionAttributeSource告诉Advisor哪些方法带事务注解TransactionInterceptor则在方法被代理拦截后执行具体的事务处理。Advisor把两者拧在一起形成一个标准的Spring AOP切面单位交给自动代理创建器去织入目标Bean。提示很多人有疑问为什么AOP是针对Advisor而不是直接针对Advice原因在于Spring AOP的匹配单元是切面一个Advisor同时包含哪些方法需要增强Pointcut和增强是什么Advice这两部分信息。只注册Advice而不注册Pointcut框架不可能知道该给哪些Bean生成代理。2.2 InfrastructureAdvisorAutoProxyCreator 把 Advisor 织进目标 BeanAutoProxyRegistrar做的事情很关键但容易被忽略它向容器注册了一个内部Bean名字叫internalAutoProxyCreator实现类是InfrastructureAdvisorAutoProxyCreator。这个类继承了AbstractAdvisorAutoProxyCreator而后者又继承自AbstractAutoProxyCreator实现了SmartInstantiationAwareBeanPostProcessor接口。简单说它是一个后置处理器。Spring创建每个单例Bean时在Bean实例化和属性填充完成之后会调用postProcessAfterInitialization方法而这个方法内部会执行wrapIfNecessary也就是检查当前Bean是否匹配容器里已有的AdvisorpostProcessAfterInitialization - wrapIfNecessary - getAdvicesAndAdvisorsForBean - findEligibleAdvisors - AopUtils.findAdvisorsThatCanApply遍历所有Advisor用Pointcut匹配当前Bean的每个方法 - createProxy匹配成功则创建代理对象事务Advisor里的Pointcut是TransactionAttributeSourcePointcut它的matches方法会调用AnnotationTransactionAttributeSource.getTransactionAttribute(method, targetClass)去判断目标方法上是否存在事务注解。如果返回的事务属性不为空说明这个Class的方法需要事务增强findAdvisorsThatCanApply就会把事务Advisor加入候选列表。这里有个非常绕但很有意思的设计Pointcut的匹配本身依赖属性解析器去读注解而事务属性解析器又会在拦截器执行阶段再次被调用来获取事务配置。这样做的好处是Transactional的解析结果被复用了同一个方法在同一代理中只会解析一次避免重复扫描注解带来的性能损耗。2.3 JDK 代理还是 CGLIB代理方式由谁决定wrapIfNecessary判定Bean需要被增强之后会进入createProxy这一步需要决定代理方式。Spring默认行为是目标类实现了接口就用JDK动态代理没有实现接口则退化到CGLIB。从Spring Boot 2.x开始spring.aop.proxy-target-classtrue成为默认值所以现代Spring Boot项目基本都在用CGLIB。JDK动态代理基于java.lang.reflect.Proxy生成一个实现了目标类所有接口的代理类因此它只能拦截接口中声明的方法。CGLIB则通过生成目标类的子类来代理可以拦截所有非final、非private、非static的方法。这也解释了开发中遇到的两类事务失效现象目标类实现了接口但事务方法没写在接口里JDK代理模式下手上的对象实际上是一个接口实现类的代理方法如果不在接口签名中调用时根本走不到代理逻辑。事务方法被final或private修饰CGLIB无法重写final方法private方法只是类内部调用外部代理也看不到事务注解自然就失效了。一句话总结代理只对外部能按Spring方式触达的方法生效。方法声明得再好看如果代理机制本身够不着Transactional就是摆设。3. 事务属性解析Transactional 是怎么被读出来的3.1 TransactionAttributeSource 与 AnnotationParser 的分工AnnotationTransactionAttributeSource是TransactionAttributeSource接口最常用的实现。它的构造函数里维护了一个LinkedHashSetTransactionAnnotationParser默认注册三个解析器SpringTransactionAnnotationParser解析org.springframework.transaction.annotation.Transactional注解也就是我们最常用的那个。JtaTransactionAnnotationParser解析javax.transaction.Transactional。Ejb3TransactionAnnotationParser解析javax.ejb.TransactionAttribute。每个TransactionAnnotationParser负责把特定注解解析成统一的TransactionAttribute对象。这样Spring对外统一使用一套事务抽象底层却能支持多种注解体系典型的适配器模式用法。读取事务属性的入口在AbstractFallbackTransactionAttributeSource.getTransactionAttribute。伪代码逻辑如下getTransactionAttribute(method, targetClass) - 先查缓存命中直接返回 - computeTransactionAttribute(method, targetClass) - 方法上找注解 - 方法所在类上找注解 - 实现接口和default方法上找注解 - 都没有返回 null方法级注解优先于类级注解这个顺序对理解为什么类上写Transactional、方法里改传播行为会生效很有帮助。Spring会按方法属性覆盖类属性的规则处理方法上没写就继承类上的配置方法上写了就完全使用方法上的配置。3.2 TransactionAttribute 的完整属性模型解析完成后Transactional的各种配置项会被塞进一个TransactionAttribute实现类。接口定义包含传播行为、隔离级别、超时时间、只读标志、事务名称以及回滚规则列表。属性默认值说明propagationREQUIRED传播行为默认加入当前事务isolationDEFAULT隔离级别默认使用数据库默认级别timeout-1超时秒数-1表示使用事务管理器的默认超时readOnlyfalse是否只读事务rollbackFor空额外指定哪些异常需要回滚noRollbackFor空指定哪些异常不需要回滚这组默认值非常关键。REQUIRED意味着有事务就加入没有就新建绝大多数业务场景不需要改但timeout一旦忘了配长事务可能长时间占用数据库连接readOnly配了之后DataSourceTransactionManager会额外执行一条只读设置语句对MySQL而言还会触发set transaction read only语义普通写操作会被数据库层面拒绝。这些都不是Spring的语法糖而是实实在在的数据库行为。3.3 回滚规则 RollbackRule 的匹配逻辑回滚规则是源码中比较容易踩坑的地方。rollbackFor XxxException.class并不是简简单单存一个异常Class做equals比较而是构造了一个RollbackRuleAttribute对象放在RuleBasedTransactionAttribute.rollbackRules列表里。RuleBasedTransactionAttribute.rollbackOn(Throwable ex)的执行逻辑遍历 rollbackRules 列表 对每个 RollbackRuleAttribute 调用 getDepth(ex) getDepth 会沿着异常类继承体系向上走 统计 ex 到目标异常类之间的距离depth depth 0 表示命中规则 取命中规则中 depth 最小的那个作为 winner 如果没有命中任何规则则回退到 DefaultTransactionAttribute 的默认逻辑 最终 winner 是 NoRollbackRuleAttribute 则不回滚否则回滚DefaultTransactionAttribute.rollbackOn的默认逻辑很简单public boolean rollbackOn(Throwable ex) { return (ex instanceof RuntimeException || ex instanceof Error); }这意味着业务方法抛出受检异常比如IOException时Spring默认不会回滚而是走提交分支。我刚学Spring的时候一直觉得这个设计反直觉但站在Spring的角度想它的默认态度是我不确定你应该回滚所以我按最保守的方式做——COMMIT。所以涉及到受检异常必须显式配rollbackFor这不是可选项而是必要项。4. 事务拦截器执行链路invoke 与 invokeWithinTransaction 的全解析4.1 TransactionInterceptor 在整个拦截器链中的位置TransactionInterceptor继承了TransactionAspectSupport实现了Spring AOP联盟的MethodInterceptor接口。它本身是一个环绕通知如果目标方法同时被多个切面拦截它会按Order顺序和其他拦截器一起组成调用链各自在自己的invoke里做增强。public Object invoke(MethodInvocation invocation) throws Throwable { Class? targetClass AopUtils.getTargetClass(invocation.getThis()); return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed); }TransactionAspectSupport.invokeWithinTransaction是整条源码链的中枢。它做的事情可以概括为四步通过TransactionAttributeSource获取当前方法的事务属性TransactionAttribute。根据事务属性确定要使用的事务管理器PlatformTransactionManager。调用createTransactionIfNecessary创建或加入一个事务得到TransactionInfo对象。用invocation.proceed()执行真实业务方法然后在方法正常返回后提交事务在方法抛出异常后按规则回滚事务。TransactionInfo是一个很关键的内部类它把当前方法的事务属性、事务管理器和事务状态打包在一起同时维护了一个ThreadLocal栈用来保存调用前的旧事务信息保证方法退出后能把事务上下文恢复成调用前的状态。4.2 createTransactionIfNecessary获取事务管理器与创建事务createTransactionIfNecessary这个名字很直白如果需要才创建。方法内部先判断TransactionAttribute是不是null如果是null说明当前方法没有事务注解TransactionInfo只做一个空包装不会调用事务管理器。如果确实有事务属性接下来要做的就是找事务管理器PlatformTransactionManager。determineTransactionManager的决策逻辑是先看TransactionAttribute里有没有显式指定transactionManager名称指定了就直接从BeanFactory里拿。没有指定则从BeanFactory里按类型查找如果找到多个必须通过qualifier或名称消歧否则抛异常。拿到事务管理器后关键一步来了TransactionStatus status tm.getTransaction(txAttr);这一步会进入AbstractPlatformTransactionManager.getTransaction内部会判断当前线程是否已经存在事务然后根据传播行为决定是加入已有事务、挂起已有事务开新事务还是直接新建事务。返回的TransactionStatus记录了事务状态之后提交回滚都靠它。这里顺带解释了多数据源项目里一个经典报错No qualifying bean of type PlatformTransactionManager available: expected single matching bean but found 2。两个数据源意味着容器里有两个DataSourceTransactionManager按类型查找时无法确定该用哪个。解决办法是在Transactional注解或配置类上显式指定transactionManager名称。4.3 业务方法返回后的正常提交与异常回滚分流先看invokeWithinTransaction对业务方法调用和后续处理的骨架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;这个结构值得停下来细看。业务方法正常返回走commitTransactionAfterReturning业务方法抛异常才走completeTransactionAfterThrowing。也就是说Spring判断是否回滚的唯一依据是业务方法有没有把异常抛出来。如果异常被方法内部try-catch消化掉那么走的一定是提交分支根本没有回滚这回事。另一个值得注意的点是提交和回滚完成后finally里会执行cleanupTransactionInfo把当前线程保存的TransactionInfo弹栈还原。这里Spring用了类似递归调用的栈结构来保存事务上下文内层方法的事务处理完把外层方法的事务状态重新设回ThreadLocal这样外层才能继续使用原来的事务上下文。5. 数据源事务的核心连接绑定、事务状态与传播行为5.1 TransactionSynchronizationManager线程级资源中枢很多人读事务源码会被各种ThreadLocal搞晕其实只要抓住一个类就够了TransactionSynchronizationManager。它内部维护了一组ThreadLocal是当前线程所有事务状态的总账本resources保存当前线程绑定的数据库连接等资源key通常是DataSource。synchronizations保存事务同步回调列表。actualTransactionActive标记当前线程是否真的有活跃事务。currentTransactionReadOnly当前事务是否只读。currentTransactionIsolationLevel当前事务的隔离级别。currentTransactionName当前事务名称。DataAccessUtils和MyBatis等框架在获取数据库连接时都会先调用DataSourceUtils.getConnection内部逻辑是先查 TransactionSynchronizationManager.getResource(dataSource) 如果当前线程已经绑定过连接直接复用这个连接 否则从 DataSource 获取新连接这就是事务期间所有DAO操作天然使用同一个Connection的底层原因。不是Spring把你的Connection对象传递给了每个DAO而是DAO每次要连接时都去TransactionSynchronizationManager的资源表里查发现线程里已经挂着一个事务绑定的连接就直接拿过来用了。5.2 DataSourceTransactionManager.doBegin 打开一次真实事务DataSourceTransactionManager是处理JDBC数据源事务的标准实现。doGetTransaction方法返回的DataSourceTransactionObject会先检查当前线程是否已经绑定过连接如果绑定过事务对象持有的ConnectionHolder就不是空。接下来是doBegin这是连接真正被改造成事务连接的地方步骤大概如下如果事务对象里还没有ConnectionHolder从DataSource获取一个新连接。通过DataSourceUtils.prepareConnectionForTransaction处理隔离级别如果事务要求的隔离级别和连接当前不一致就用setTransactionIsolation修改并记录旧值以备恢复。如果是只读事务调用connection.setReadOnly(true)并且为了确保只读语义真正生效Spring还会执行一条语句强制把SQL发送到数据库MySQL等数据库在事务内设置只读后才生效。如果连接当前是自动提交模式记录mustRestoreAutoCommit true然后执行con.setAutoCommit(false)。这一步是把每次SQL自动提交变成由Spring统一控制提交/回滚的关键。把ConnectionHolder通过TransactionSynchronizationManager.bindResource绑定到当前线程的资源表里。注意第4步如果事务管理器只做commit/rollback而连接保持自动提交那业务方法里每条SQL执行完就落库了事务根本控制不住。所以setAutoCommit(false)这一步是JDBC事务的基石也是所有后续工作的前提。5.3 传播行为在 AbstractPlatformTransactionManager 中的落点AbstractPlatformTransactionManager.getTransaction是传播行为的决策中心。Object transaction doGetTransaction(); if (isExistingTransaction(transaction)) { return handleExistingTransaction(definition, transaction, debugEnabled); } // 当前没有事务时根据传播级别分支 if (MANDATORY) throw; // 没有事务但强制要求事务抛异常 if (REQUIRED || REQUIRES_NEW || NESTED) { doBegin(transaction, definition); // 开启新事务 prepareSynchronization(status, definition); } // SUPPORTS / NOT_SUPPORTED / NEVER 则不开启事务如果当前已有事务handleExistingTransaction里各传播行为的分支就是面试常考的点了。以两个常用传播级别为例PROPAGATION_REQUIRED什么都不做直接复用当前事务返回一个加入状态。PROPAGATION_REQUIRES_NEW先把当前事务的资源和同步状态挂起suspend然后开一个新事务执行新事务提交后再恢复外层事务。挂起操作表面上看只是把ThreadLocal里的连接信息暂存一下实际影响很大同一个线程在REQUIRES_NEW事务期间拿到的Connection和外层事务完全不是同一个。此时内层事务的提交、回滚对外层互不影响唯一的代价是数据库连接同时被占用了两份。PROPAGATION_NESTED在Spring源码里的处理也很有意思它利用了数据库的保存点savepoint机制在同一连接上创建一个嵌套保存点内层回滚只回滚到保存点位置不影响外层。MySQL支持保存点所以NESTED在某些场景下可以充当部分回滚的利器但需要连接的底层支持不是所有数据库都适用。6. 提交、回滚与事务同步回调的顺序问题6.1 processCommit 里提交前通知的重要意义业务方法正常返回后会走到AbstractPlatformTransactionManager.commit内部区分两种情况如果当前事务不是新事务说明只是参与者commit会直接把事务状态标记为可能让上级决定真正提交发生在最外层如果是新事务才执行processCommit。processCommit的执行顺序对理解Spring事务事件机制极其重要prepareForCommit(status) // 给事务管理器一个提交前准备的钩子 triggerBeforeCommit(status) // 触发 beforeCommit 同步回调 triggerBeforeCompletion(status) // 触发 beforeCompletion 同步回调 doCommit(status) // 真正执行 connection.commit() triggerAfterCommit(status) // 事务已提交触发 afterCommit triggerAfterCompletion(status) // 触发 afterCompletion状态为已提交beforeCommit和beforeCompletion的区别要分清beforeCommit执行时事务还没提交里面抛出的异常会导致回滚beforeCompletion同样在提交前执行但主要用于清理资源。真正提交完成后才执行afterCommit而在afterCommit阶段做的数据库操作已经不在原事务保护范围内了。TransactionSynchronization的afterCommit有个很典型的应用场景事务提交后发消息、发邮件、清理缓存。如果放在业务方法里直接做事务还没提交其他异步线程可能读不到数据如果放在afterCommit里做就能保证事务已持久化再对外通知。6.2 回滚规则判定与不用 rollbackFor 的代价异常路径进入completeTransactionAfterThrowing后第一件事是调用rollbackOn(ex)检查当前异常是否命中回滚规则命中回滚规则调用rollback(status)。未命中回滚规则源码里最反直觉的一步出现了它调用的是commit(status)。对你没有看错。业务方法抛出一个IOException却不满足rollbackFor配置时Spring不会帮你回滚反而会提交当前事务。我在排查生产问题时遇到过这种情况方法上只写了Transactional方法内部调用外部接口抛了IOException异常一路抛到入口但数据就是没有回滚因为Spring认为你没告诉我要对这个异常回滚那我就默认提交。正确的写法是在业务异常不需要被外部感知时使用自定义的运行时异常或者显式配置Transactional(rollbackFor {IOException.class, BusinessException.class})这样rollbackOn在遍历回滚规则时能命中IOException才会真正执行回滚。6.3 UnexpectedRollbackException 到底什么时候出现UnexpectedRollbackException是Spring事务里一个让人很困惑的异常它出现的典型场景是嵌套事务的只标记不回滚。伪代码如下外层方法ATransactional 调用内层方法BTransactional(propagation REQUIRED) 方法B内部 捕获了异常并自行处理同时把事务标记为 rollback-only比如B内部有另一个事务参与方失败 方法A正常返回没有抛异常当方法A在提交阶段执行processCommit时Spring发现当前事务已经被某个参与者标记为rollback-only但当前代码路径没有异常抛出。此时如果继续提交会违反参与者的意愿如果直接静默回滚调用方又完全不知情。Spring的选择是抛UnexpectedRollbackException明确告诉你事务被标记为只能回滚但我这里不是回滚入口所以给你一个意外回滚的异常。实际排查时看到这个异常不要只在当前方法里找问题。第一反应应该是去代码里搜索setRollbackOnly()的调用或者检查内层事务是否出现了部分失败但被吞掉的情况。事务的rollback-only标记是全局传递的和内层是否自己提交无关。7. 基于源码复盘常见的事务失效场景7.1 自调用失效代理拦不到 this 调用这是最经典的事务失效场景。理解了源码后原因非常清楚Spring容器给UserService生成了一个代理对象注入到OrderService里的引用实际上是这个代理。外部调用orderService.createOrder()时调用链进入代理对象的方法代理触发TransactionInterceptor.invoke事务开始。但OrderService内部如果写了this.saveOrderDetail()这里的this指向的是原始Bean不是代理对象。Spring AOP拦截器的逻辑根本不会执行saveOrderDetail上的Transactional被完全跳过。解决办法有几种从源码角度看把内部调用拆到另一个Spring管理的Bean里让调用经由代理链。注入self代理在当前类里注入Autowired private OrderService self;然后调用self.saveOrderDetail()。开启exposeProxy后使用AopContext.currentProxy()但这种方式侵入性稍强而且要确认代理是暴露在当前线程里的。从原理上讲事务不是看到注解就生效而是代理拦截到方法后去查注解。代理都没被触发后面对注解做的一切解析自然无从谈起。7.2 异常被 try-catch 吞掉后源码走的是哪条支路很多开发者在方法内部把异常捕获后记录日志然后继续往下走或者直接返回。这种情况下源码走的分支是invocation.proceed() 正常返回 - commitTransactionAfterReturning(txInfo) - processCommit业务方法里抛没抛异常Spring完全感知不到——因为拦截器看到的只有proceed()的返回结果。事务模块内部没有魔法雷达它不可能知道你哪条SQL出了问题它只认一件事方法调用是否抛出了异常。所以想找事务不生效的锅先找你代码里的try-catch。事务的边界是整个方法的调用边界异常必须跨越这个边界才能让Spring进入回滚分支。日志记录用catch可以但要么捕获后重新抛一个运行时异常要么等排查问题的时候你能接受这次操作被提交。7.3 多线程与跨数据源场景为什么会超出预期TransactionSynchronizationManager把连接绑在ThreadLocal上这就注定了事务的线程隔离特性。子线程里执行数据库操作时DataSourceUtils.getConnection在子线程自己的ThreadLocal里查不到父线程绑定的连接于是会拿一个全新的连接。新连接默认自动提交子线程里的SQL每执行一条就提交一条父事务的回滚根本覆盖不到它们。这也是为什么Async方法里的数据库操作经常不受外围事务控制的根源。要解决要么把子线程的业务逻辑独立设计为自含事务的方法要么在Spring 6.1之后使用TransationTemplate配合TransactionSynchronizationManager的资源传播机制……但更实际的建议是事务边界尽量收窄到单线程内不要幻想着让事务管理器跨线程指挥连接提交回滚。跨数据源时问题类似。一个DataSourceTransactionManager只能管理一个DataSource两个数据源意味着两套连接、两个事务管理器。一个方法上写Transactional即使指定了事务管理器也只能管它对应的那个数据源。多个数据源必须考虑JtaTransactionManager或分布式事务方案普通的声明式事务在这里天然失效。8. 排查问题时源码能帮你做什么8.1 推荐的调试断点位置我每次排查事务问题基本是按这套断点流程走效率很高排查目标断点位置观察变量Bean是否被代理AbstractAutoProxyCreator.wrapIfNecessary当前Bean是否返回代理对象事务拦截器是否进入TransactionInterceptor.invokeinvocation.getMethod()叫什么事务属性是否解析出来AbstractFallbackTransactionAttributeSource.getTransactionAttributetxAttr是不是null事务是否真正创建AbstractPlatformTransactionManager.getTransaction传播行为、当前有无事务连接是否绑定DataSourceTransactionManager.doBegincon的autoCommit状态提交还是回滚TransactionAspectSupport.completeTransactionAfterThrowingrollbackOn的结果用调试器跟一遍之后很多玄学问题都会变成可定位的流程问题。比如事务属性为null说明注解没被读到优先查方法是不是public、代理方式对不对doBegin没执行说明事务管理器或传播行为有问题。8.2 源码版本差异与阅读顺序建议不同Spring版本在源码细节上有差异。比如在Spring 5.3里TransactionAspectSupport的invokeWithinTransaction和Spring 6.x版本在内部方法拆分上有所不同TransactionSynchronizationManager虽然在6.x中核心保持稳定但事务资源管理的细节也做过调整。阅读源码时不必纠结某个方法的具体位置核心模型是稳定的属性解析、事务创建、资源绑定、提交回滚这四段链路在任何版本里都存在。如果刚接触这份源码我建议的阅读顺序是从ProxyTransactionManagementConfiguration开始建立三个Bean的整体印象。跟一遍TransactionInterceptor.invoke到invokeWithinTransaction理解拦截器骨架。进入AbstractPlatformTransactionManager.getTransaction理解传播行为在事务创建阶段的落点。再读DataSourceTransactionManager.doBegin把连接绑定自动提交关闭这些数据库层面的操作串起来。最后回看processCommit和completeTransactionAfterThrowing把提交回滚决策补全。按这个顺序读下来你脑子里会形成一条完整的调用链而不是零散记住一堆方法名。过程中遇到不认识的类优先看它实现了什么接口、被谁注册进容器比逐行读实现更有用。我在实际排查中还有一个习惯性操作排查任意事务相关的问题先看三个点——容器里事务基础设施有没有齐全、当前Bean有没有真的被代理、具体走的是哪个事务管理器。这三个点只要确认无误90%的问题都已经定位到业务代码层面了剩下10%再打开事务日志看spring-tx模块打印的Getting transaction for ...和Completing transaction ...基本能把整个生命周期看个清清楚楚。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门