• 【通过案例看透Spring事务】


    请添加图片描述

    ✅ 本文案例源码,基于最新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 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,如果涉及嵌套事务,请务必确认事务传播类型对业务的影响,以确保业务正确性。

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

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

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

  • 相关阅读:
    在C#中使用NModbus4通信库执行【读】操作
    ArcGIS Pro 转换Smart3D生成的倾斜3D模型数据osgb——创建集成网格场景图层包
    使用docker-compose安装gitlab-ce 以及升级gitlab
    CSS基础篇---01选择器、字体与文本样式
    Grafana安装和配置Prometheus数据源教程
    视频点播平台EasyDSS自定义目录的存储路径写死,该如何更改?
    C语言实现希尔排序
    这MATLAB代码哪里有错啊
    Spring框架概述及核心设计思想
    [附源码]java毕业设计心理问题咨询预约系统
  • 原文地址:https://blog.csdn.net/rongtaoup/article/details/127688984