Spring事务源码原理详解(保姆级)

本文深入探讨Spring事务管理的核心机制,解析事务的执行流程、异常处理,并通过具体案例揭示常见问题及解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

请添加图片描述

✅ 本文案例源码,基于最新Spring Boot 版本2.7.5,Spring 版本是5.3.23

回顾 Spring AOP

Spring AOPSpring 中除了依赖注入外(DI)最为核心的功能,AOP 即 为面向切面编程

Spring AOP 通过 CGlibJDK 动态代理等方式来实现运行期动态方法增强,目的是将与业务无关的代码单独抽离出来,逻辑解耦降低系统的耦合性,提高程序的可重用性和开发效率。AOP 在日志记录、监控管理、性能统计、异常处理、权限管理、统一认证等各个方面被广泛使用。

🏷️JDK动态代理面向接口,通过反射生成目标代理接口的匿名实现类;
🏷️CGLIB动态代理则通过继承,使用字节码增强技术 为目标代理类生成代理子类。

Spring 默认对接口实现使用JDK动态代理,对具体类使用CGLIB,同时也支持配置全局使用CGLIB来生成代理对象。
在这里插入图片描述

Spring AOP Bean的增强有5种形式:

  • 前置增强(org.springframework.aop.BeforeAdvice):在目标方法执行之前进行增强;
  • 后置增强(org.springframework.aop.AfterReturningAdvice):在目标方法执行之后进行增强;
  • 👍环绕增强org.aopalliance.intercept.MethodInterceptor):在目标方法执行前后都执行增强
  • 异常抛出增强(org.springframework.aop.ThrowsAdvice):在目标方法抛出异常后执行增强;
  • 引介增强(org.springframework.aop.IntroductionInterceptor):为目标类添加新的方法和属性。

案例1:事务的执行流程

🏷️ 测试案例:向学生表中新增一条数据,走一遍Spring 事务的执行过程。

在这里插入图片描述

Spring声明式事务使用AOP 的环绕增强方式,在方法执行之前开启事务,在方法执行之后提交或回滚事务。对应的实现为 TransactionInterceptor ,其实现了 MethodInterceptor,即,通过AOP的环绕增强方式。
在这里插入图片描述

事务拦截器:TransactionInterceptor

在这里插入图片描述

TransactionInterceptor.invoke()主要有2步:

  1. 获取 TargetClass
  2. 调用父类 TransactionAspectSupport #invokeWithinTransaction处理事务

Spring事务管理的基类:TransactionAspectSupport

TransactionAspectSupport Spring 事务管理的基类,其支持声明式事务、编程式事务

  • 其通过模板方法模式规定了事务的执行流程。
  • 通过使用策略模式PlatformTransactionManagerReactiveTransactionManager实现将执行实际的事务管理
    • PlatformTransactionManager:声明式事务管理器
    • ReactiveTransactionManager:编程式事务管理器

TransactionAspectSupport #invokeWithinTransaction 事务处理方法主流程如下:
在这里插入图片描述

⭐主要源码及注释如下:
在这里插入图片描述

⭐1、 获取事务属性源 TransactionAttributeSource ,是一个策略接口,主要用来获取事务属性,根据不同类型的源,确定事务属性

⭐2、根据属性源获取事务属性TransactionAttribute,是直接从 事务属性缓存
Map<Object, TransactionAttribute> attributeCache 中获取的。

思考:事务属性缓存attributeCache是什么时候缓存的?

Spring 容器初始化时,Bean后置处理器 会识别到 @Transactional注解 的类和方法,将事务属性放入缓存attributeCache 中,并创建 AOP 代理对象。

TransactionAttribute继承TransactionDefinitio事务属性,包含:事务的隔离级别、事务的传播类型、事务超时时间、事务名称、事务是否只读等。
在这里插入图片描述

⭐3、确定事务管理器 PlatformTransactionManager 。如果没有指定事务管理器,会获取一个默认的事务管理器(本案例中是 DataSourceTransactionManager 并放到缓存中,之后直接从缓存获取。
在这里插入图片描述

PlatformTransactionManager 事务管理器,有3个方法:

  • getTransaction:获取当前激活的事务或者创建一个事务
  • commit:提交当前事务
  • rollback:回滚当前事务

PlatformTransactionManagerSpring 抽象的接口,其定义了事务的3个基本动作。具体实现是由相应的子类进行的。
在这里插入图片描述

  • DataSourceTransactionManager 使用Jdbc 和 ibatis 进行数据持久化的事务管理器
  • JtaTransactionManager 分布式事务的事务管理
  • CciLocalTransactionManager 主要面向JCA 进行 系统集成的 局部事务管理器

⭐4、获取 joinpoint 标识,用于确定事务名称,值是方法全路径。

⭐5、创建事务 createTransactionIfNecessary
在这里插入图片描述

  1. 根据 joinpoint 标识确定事务名称,主要用于对事务进行监控和记录。
  2. 获取事务状态 TransactionStatus # getTransaction 方法内部会调用相应的事务管理器 开启事务。
  3. 封装 TransactionInfo :根据事务状态和事务属性封装 TransactionInfo 事务信息对象,并将其加入到ThreadLocal中,交给事务管理器进行管理。

其中,b 步骤 getTransaction负责开启事务。见下图中的第4步操作 startTransaction
在这里插入图片描述

startTransaction 开启事务方法如下:
在这里插入图片描述

doBegin()是一个抽象方法,不同的事务管理器有对应的实现。
在这里插入图片描述

DataSourceTransactionManager # doBegin 开启事务代码如下,至此数据库事务已经开启了。

在这里插入图片描述

⭐6、回调执行目标方法:invocation.proceedWithInvocation(); 这里就是调用被 Spring AOP 增强的事务方法了。

⭐7、目标方法执行成功后,调用 DataSourceTransactionManager # doCommit 提交事务。

在这里插入图片描述


案例2:事务异常处理

🏷️我们向学生表中插入一条数据:“小荣同学”。紧接着我们抛Exception异常,试图将事务回滚。事务是否会回滚?

在这里插入图片描述

数据库插入学生方法打印了日志,并且事务方法抛出了 Exception.class,如果事务回滚了,那数据库中应该没有数据吧?

在这里插入图片描述

❓但发现,数据库中数据保存成功了,为什么事务没有回滚
在这里插入图片描述

⭐让我们回顾一下事务的处理流程。TransactionAspectSupport #invokeWithinTransaction
执行目标方法(事务方法),如果没有抛异常,则提交事务。如果抛异常,则执行回滚流程,问题就出现在回滚流程中。
在这里插入图片描述

异常处理流程,对应下图源码中的第6、7步,如果目标方法抛出异常,则捕获执行第7步回滚流程。
在这里插入图片描述

⭐Spring事务管理基类 TransactionAspectSupport 事务异常处理方法 completeTransactionAfterThrowing

🤔从Spring官方注释可以看到,这个方法不仅可以将事务回滚、也有可能提交事务,这取决于配置

Handle a throwable, completing the transaction. 处理异常,完成事务。
We may commit or roll back, depending on the configuration. 执行回滚、提交事务,取决于配置

在这里插入图片描述

这个方法逻辑很简单,只有2种情况:

  • 事务属性满足回滚条件,回滚事务。
  • 不满足条件,继续提交事务。

🤔 回滚条件,取决于什么配置

txInfo.transactionAttribute.rollbackOn(ex)这是事务的回滚的关键判断条件。当这个条件满足时,会触发 事务回滚。

然后调用RuleBasedTransactionAttribute #rollbackOn(ex) 判断是否满足回滚规则,对事务进行回滚。

rollbackOn(ex)有2个层级的判断逻辑
在这里插入图片描述

层级1:根据回滚规则和异常层级判断
如果在 @Transactional 中配置了 rollbackFor,这个方法就会用捕获到的异常和 rollbackFor 中配置的异常做比较。如果捕获到的异常是 rollbackFor 配置的异常或其子类,就会直接 rollback

层级2:如果没有配置 rollbackFor 或者 异常类型没有匹配,则使用父类的 super.rollbackOn(ex)条件进行判断
如果没有配置 rollbackFor 事务回滚条件是:只有抛出的异常类型是RuntimeException 或者Error时才回滚
在这里插入图片描述

如果配置了 rollbackFor 则只有捕获到 rollbackFor 配置的异常及其子类,才会回滚事务。
而我们的例子中,抛出的是 Excepton类型异常,并且没有指定rollbackFor属性 ,所以不会回滚事务,而是继续提交事务。提交事务的流程在 案例1 中我们已经分析过了。

🤔怎么改正问题?

  • 指定 rollbackFor @Transactional(rollbackFor = Exception.class)
  • RuntimeException throw new RuntimeException();

RuntimeException 是 Exception 的子类,如果用 rollbackFor=Exception.class,对 RuntimeException 也会生效。

🤔如果我们需要对 Exception 执行回滚操作,但对于 RuntimeException 不执行回滚操作,应该怎么做呢?

@Transactional(rollbackFor = Exception.class , noRollbackFor = RuntimeExcetion.class)


案例3:试图通过 this 调用事务方法

🏷️测试场景:通过 this 调用事务方法。方法向学生表中插入数据,之后抛出异常。事务是否回滚?

在这里插入图片描述

从日志看,向学生表插入数据,并且事务方法抛出了异常。
在这里插入图片描述

从数据库看,数据保存成功了,没有回滚
在这里插入图片描述

🤔通过 this 调用事务方法,为什么事务没有生效

通过断点调试,Controller 中 注入的 StudentService 是被Spring AOP 通过 CGLIB动态代理增强过的类。

在这里插入图片描述

StudentService 中 通过 this 调用事务方法,this就是一个普通的 对象,不是被Spring 代理过的类
在这里插入图片描述

只有通过Spring 动态代理过的类调用,(AOP)事务才会生效。使用 this 调用事务方法,是一个普通对象,不具有AOP增强功能,事务自然不会生效。

🤔怎么改正问题?

通过代理对象调用事务方法:使用 @Autowired 注入 StudentService

通过断点我们看到 现在是通过代理类调用事务方法了,此时(AOP)事务已经生效。

在这里插入图片描述


案例4:试图给 private 方法添加事务

🏷️测试场景:给 private 方法添加事务。向学生表中插入数据,之后抛出异常。事务是否回滚?

在这里插入图片描述

从日志看,向学生表插入数据,并且事务方法抛出了异常。
在这里插入图片描述

从数据库看,数据保存成功了,没有回滚
在这里插入图片描述

🤔通过 private 修饰的事务方法,为什么事务没有生效

这个问题,需要从Spring AOP动态代理的处理寻找答案。
在这里插入图片描述

Spring 代理操作的入口是 AbstractAutoProxyCreator # postProcessAfterInitialization

在这里插入图片描述

wrapIfNecessary 首先获取满足条件的切面数组,然后为其创建代理。
在这里插入图片描述

是否满足代理条件,最终会调用AopUtils canApply方法,看是否满足代理条件。
在这里插入图片描述

事务方法的匹配规则 会 调用事务属性源切点 TransactionAttributeSourcePointcut 的 matches 方法

这个方法的主要逻辑是:根据事务属性源获取事务属性,只要事务属性不为空即匹配成功。
在这里插入图片描述

AbstractFallbackTransactionAttributeSource # getTransactionAttribute,这个方法如果缓存中有值,则返回缓存中的值,否则 computeTransactionAttribute计算得到事务属性,并且加入到缓存中。

注意这里,我们代理对象还没有创建,此时缓存中是没有值的。
在这里插入图片描述

computeTransactionAttribute 其主要功能是根据方法和类的类型确定是否返回事务属性
在这里插入图片描述

注意这个判断条件
allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())
当这个判断结果为 true 的时候返回 nul也就意味着这个方法不会被代理,从而导致事务的注解不会生效

条件 1:allowPublicMethodsOnly(),这个方法意思是:是否只允许公共方法具有事务属性,默认是true的。
条件2:Modifier.isPublic(method.getModifiers()) ,这个方法是判断当前方法是不是public的。这个方法里通过位运算,只有public 方法才会返回true。
在这里插入图片描述

Spring 默认使用动态代理方式对目标方法执行AOP增强,使用private修饰事务方法,会导致类无法被Spring Cglib代理,无法进行AOP增强,也就是事务不会生效。

只有使用 public 修饰的事务方法,事务才会生效。

🤔 思考:使用final、static 修饰的类可以被代理吗


案例5:小心嵌套事务

🏷️测试场景。新增一个学生,然后记录日志。记录日志异常回滚了,但是不希望影响新增学生的流程,学生会保存成功吗?

保存学生时,我们catch住了日志的异常,希望它不会影响新增学生的结果。
在这里插入图片描述

记录日志方法中,我们模拟日志保存失败了,并抛出异常,让新增日志的事务回滚
在这里插入图片描述

注意这是一个嵌套事务场景saveStudent 方法 使用 @Transactional 注解开启了事务,saveStudent 方法中调用记录日志方法 logsService.saveLog(logs); 这个方法也开启了事务。

运行结果:
在这里插入图片描述

通过数据库记录看到,日志表回滚了,学生表也回滚了。为什么?
在这里插入图片描述

在这个例子中,我们在一个事务中开启了另一个事务,其实就是事务传播的问题

Spring 支持7种事务传播类型,具体的传播类型是通过 propagation 属性控制的。

  • REQUIRED(Spring 默认的事务传播类型):如果当前没有事务,则新建一个事务;如果当前已有事务,则加入这个事务。
  • SUPPORTS:如果当前存在事务,则加入该事务;如果没有事务,则以非事务的方式执行。
  • MANDATORY:如果当前存在事务,则加入该事务;如果没有事务,则抛异常;
  • REQUIRES_NEW:无论当前是否有事务,每次都新建一个事务。
  • NOT_SUPPORTED:无论当前是否存在事务,都以非事务的方式执行。
  • NEVER:不使用事务。如果当前存在事务,则抛异常;
  • NESTED:如果当前事务存在,则开启一个嵌套事务(子事务)。如果父事务回滚,子事务也会回滚;如果子事务回滚,不会影响父事务。

本例中,我们使用Spring 默认的事务传播类型,记录日志事务发现已经存在一个事务了,则加入该事务,也就是2个方法公用一个事务,这就好理解了,内层事务事务回滚,外层事务肯定也回滚

原理我们知道了,所以我们要解决这个问题,在内存事务中 指定事务传播类型属性为REQUIRES_NEWNESTED 即可。
在这里插入图片描述

🤔Spring 是如何处理嵌套事务的呢

⭐再回顾一下事务的执行流程。注意我们使用嵌套事务,所以通过AOP增强 会执行2次这个方法。
TransactionAspectSupport # invokeWithinTransaction 事务处理方法主流程如下:
在这里插入图片描述

主要源码及注释如下:
在这里插入图片描述

首先,内层事务由于抛出异常,会执行上图第7步:异常回滚流程。即 completeTransactionAfterThrowing方法

completeTransactionAfterThrowing 会检查事务配置是否满足回滚条件,内层事务我们手动指定回滚Exception 类型异常,是满足回滚条件的。@Transactional(rollbackFor = Exception.class)
(至于为什么满足回滚条件,我们在 案例2:事务异常处理 已经分析过了)
在这里插入图片描述

紧接着,上图 1.1 步骤 执行回滚。最终会调用
AbstractPlatformTransactionManager # processRollback事务的回滚流程。
在这里插入图片描述

processRollback 回滚流程主要有3种情况:

  • 是否有保存点:针对NESTED传播类型的子事务,只回滚到保存点,不会影响父事务。
  • 是否是新事务:如果是新事务,直接执行回滚操作。
  • 是否已有事务:如果已有事务了,满足回滚条件则设置回滚标记。

本案例中,事务传播使用Spring 默认的传播属性REQUIRED,属于第 3 种情况,执行第 3 种 情况的流程。先进行2个条件判断:

⭐条件1:status.isLocalRollbackOnly():默认值是false,只有 手动调用 TransactionStatus # setRollbackOnly 时,才会设置为true
在这里插入图片描述

⭐条件2:isGlobalRollbackOnParticipationFailure():这个的默认值是true;这个属性的意思是:是否回滚交由外层事务决定。
在这里插入图片描述

条件1、条件2 全部满足,则执行 3.2 步骤 回滚标记流程 DataSourceTransactionManager#doSetRollbackOnly。这个方法最终调用 setRollbackOnly()
在这里插入图片描述

至此,内层事务的异常回滚流程执行完毕。我们通过源码发现:内存事务没有立即回滚,而是设置了一个回滚标志 setRollbackOnly()

⭐继续回到外层事务,在外层事务中,我们catch住了内层事务抛出的异常,所以外层事务并不会执行异常回滚流程,而是走提交事务流程 即 commitTransactionAfterReturning

然后调用事务管理器的提交事务方法:AbstractPlatformTransactionManager#commit
注意,这个方法中如果满足回滚条件,则执行回滚事务流程;否则提交事务。
在这里插入图片描述

⭐回滚流程中有2个判断条件:

  • 条件1:shouldCommitOnGlobalRollbackOnly(),含义是回滚事务时,是否提交事务。这个默认值是 false。
  • 条件2:查看回滚标记 defStatus.isGlobalRollbackOnly() ,而这个标记正是内层事务设置的回滚标记。

至此,外层事务由于内层设置了回滚标记,也满足了回滚条件。调用AbstractPlatformTransactionManager#processRollback 回滚。
在这里插入图片描述

至此,外层事务也执行了数据库回滚操作。


总结

Spring事务使用的 注意事项如下

通过this调用事务方法时,this 对象是普通的对象,不是被代理过的类,所以事务不会生效

通过private修饰的事务方法,无法被Spring 动态代理,所以事务不会生效。事务方法必须使用public修饰

通过final、static修饰的事务方法,由于方法无法被重写,无法被Spring 动态代理,所以事务不会生效。

Spring 默认只会对抛出RuntimeExceptionError 的异常类型进行回滚,如果不指定rollbackFor并在程序中抛出Exception,事务是不会回滚的

阿里巴巴的开发者规范中,要求 rollbackFor 显示指定要回滚的异常类型 。一般建议设置成Exception,只要是指定的异常类型及其子类,才会回滚事务。

小心嵌套事务!Spring事务的默认传播类型是 REQUIRED,如果涉及嵌套事务,请务必确认事务传播类型对业务的影响,以确保业务正确性。

避免大事务!大事务在并发情况下有很多问题,包括:连接池打满、事务回滚时间长、锁等待、死锁等;所以我们要避免使用大事务!比如:

  • 将不必要的操作放在事务外,例如查询
  • 事务中避免远程调用,可以通过重试及补偿机制来达到数据最终一致性
  • 事务中避免一次性处理过多的数据

🎉 如果这篇文章对你有帮助,点赞👍 收藏⭐ 关注✅ 哦,创作不易,感谢!😀
请添加图片描述

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

涛声依旧叭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值