Bootstrap

《手写Spring渐进式源码实践》实践笔记(第十九章 实现事务管理@Transactional注解)

第十九章 事务管理

背景

事务

  1. 事务(Transaction)是一个不可分割的工作单位,它由一组有限的数据库操作序列组成。在计算机术语中,事务是指访问并可能更新数据库中各种数据项的一个程序执行单元。
  2. 事务是为了保证数据库的一致性而提出的一种处理机制,它将一组操作视为一个整体进行处理,确保这些操作的原子性、一致性、隔离性和持久性。
  3. 事务具有ACID四个基本属性,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。
  • 原子性:事务中的所有操作要么全部成功执行,要么全部不执行。只要其中一个指令执行失败,所有的指令都执行失败,数据会进行回滚,回到执行指令前的数据状态。

  • 一致性:事务执行前后,数据库的状态保持一致。即事务在完成时,所有的数据都必须处于一致的状态。

  • 隔离性:事务的执行过程对其他事务是隔离的,一个事务的执行不能被其他事务所影响。

  • 持久性:事务一旦正确完成后,它对数据库中数据的改变就是永久性的。

Spring支持两种主要的事务管理方式:编程式事务管理声明式事务管理

一、编程式事务管理
  • 定义:编程式事务管理是通过编写代码来手动管理事务,需要在代码中显式地开启、提交或回滚事务。

  • 实现方式

    • TransactionTemplate:Spring提供了一个TransactionTemplate类,它可以简化事务管理的代码。开发者只需在需要事务支持的方法中注入TransactionTemplate,并通过其execute方法执行事务操作。
    • PlatformTransactionManager:这是一个事务管理器的接口,开发者可以实现此接口或使用Spring提供的实现类(如DataSourceTransactionManager、JpaTransactionManager等)来手动管理事务。通过调用getTransaction方法获取事务状态,然后在业务逻辑执行完毕后根据需要调用commit或rollback方法。
  • 优缺点:编程式事务管理提供了更细粒度的事务控制,但增加了代码的复杂度,且容易出错。此外,它与业务代码的耦合度较高,不利于代码的维护和扩展。

二、声明式事务管理
  • 定义:声明式事务管理是通过配置的方式来管理事务,通常使用注解或XML配置。它将事务管理的代码从业务代码中解耦,简化了事务管理。

  • 实现方式

    • 注解方式:使用@Transactional注解来声明事务的边界。该注解可以应用于接口定义、接口方法、类定义、类的公有方法上。当Spring应用启动时,会查找带有@Transactional注解的类和方法,并为其创建代理。当调用代理对象的方法时,如果这个方法被@Transactional注解标记,Spring会自动应用事务管理逻辑。
    • XML配置方式:在早期的Spring版本中,也支持通过XML配置文件来声明事务管理。这种方式相对繁琐,需要编写大量的XML配置,但随着注解的普及,这种方式已逐渐被淘汰。
  • 优缺点:声明式事务管理简化了事务管理的代码,降低了与业务代码的耦合度,提高了代码的可读性和可维护性。它是最常用的事务管理方式,因为它易于使用且几乎与业务代码无耦合。然而,它的灵活性相对较低,无法在运行时动态地改变事务管理策略。

业务背景

Spring实现事务管理,可以确保数据的一致性和完整性,通过Spring的事务管理功能,开发者可以快速地配置和管理事务,无需编写大量重复的事务处理代码。这不仅可以提高开发效率,还可以降低出错的风险。本章节通过实现Spring声明式事务的两种方式,来完成事务管理功能。

目标

基于当前实现的 Spring 框架,完成声明式事务管理,实现@Transactional注解的方式。

设计

为了实现Spring框架进行数据库事务管理的交互,我们需要将JDBC事务处理过程进行抽象化。整体设计结构如下图:
在这里插入图片描述

实现

代码结构

image-20241128180245118

源码实现:https://github.com/swg209/spring-study/tree/main/step19-jdbc-transaction

类图

在这里插入图片描述

整个类图中,可以看到划分成了事务定义解析事务属性、获取事务事务切面开启事务四个大块。

实现步骤

本章节主要关注事务管理主流程,配合扩展功能的springframework的core目录下的类,未做解析,后续有兴趣的伙伴可以自行阅读源码。

1、事务定义
  • TransactionDefine
    • 这是一个接口,定义了事务的一些属性。比如事务的传播性、事务隔离级别、超时时间等
  • DefaultTransactionDefinition
    • 这个类是TransactionDefine接口的实现类。用来设置事务的传播、隔离、超时等属性

  • TransactionAttribute

    • 直接看这个接口的名字含义是属性的属性。确实如此,这个接口继承TransactionDefine,添加了一个rollbackOn(Throwable ex)方法。在进行事务回滚前用来判断对于当前发生的异常是否需要回滚。
  • DefaultTransactionAttribute

    • 这个类并没做特殊的事情,就是常规属性的设置
    • 对于rollbackOn(Throwable ex)这个方法在这里进行了实现。如果当前异常是运行时异常或者error,那么返回的是ture
    public boolean rollbackOn(Throwable ex) {
       return (ex instanceof RuntimeException || ex instanceof Error);
    }
    
  • RuleBasedTransactionAttribute

    • 这个类比较重要的方法是rollbackOn(Throwable ex)。对于指定的异常是否应该进行事务回滚,特别是这个规则的判断逻辑可以单独描述。
2、解析事务属性,获取事务
  • @Transactional

    • 这是作用于方法和类上的标识注解。标识该方法或者该类中的方法应用事务。
  • TransactionAnnotationParser

    • 该接口就一个解析方法。用于解析方法或者类上的注解得到事务的属性。
    TransactionAttribute parseTransactionAnnotation(AnnotatedElement element);
    
  • SpringTransactionAnnotationParse

    • 这个类用于实现解析Transactional注解,获取业务中设置的相关属性。

    • TransactionAttributeSource

      • 该接口可以认为是TransactionAttribute的包装接口,该接口中就一个获取TransactionAttribute的方法。
      TransactionAttribute getTransactionAttribute(Method method, @Nullable Class<?> targetClass);
      
    • AbstractFallbackTransactionAttributeSource

      • 该抽象类实现了TransactionAttributeSource接口,还是按照老套路定义了获取TransactionAttribute的模版,真正的获取交给子类去实现。
      protected abstract TransactionAttribute findTransactionAttribute(Class<?> clazz);
      
      protected abstract TransactionAttribute findTransactionAttribute(Class<?> clazz);
      
    • AnnotationTransactionAttributeSource

      • 这个就是用来实际工作的类。读取Transactional注解返回一个TransactionAttribute。同时这个类也支持JTA和EJB。
      • 在这个类中比较重要的方法就是determineTransactionAttribute(AnnotatedElement element) 。在该方法中调用解析事务的类SpringTransactionAnnotationParse去完成解析工作。
3、事务状态
  1. TransactionStatus

    • 这个接口是对事务的状态进行描述,定义了Savepoint、是否是新事务等信息。通过TransactionDefinition中的事务属性来创建一个TransactionStatus
  2. AbstractTransactionStatus

    • 事务状态描述的抽象类
  3. DefaultTransactionStatus

    • 默认的事务状态描述类
4、事务管理器
  1. PlatformTransactionManager

    • 这是一个比较重要的接口。定义了获取事务状态、事务提交、事务回滚等方法
    TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
    
    void commit(TransactionStatus status)
    
    void rollback(TransactionStatus status)
    
  2. AbstractPlatformTransactionManager

    • 事务管理的抽象实现类。采用同样的套路定义了事务的操作流程,分别是获取事务,事务提交,事务回滚。这三个步骤在不同的数据源上操作又有区别,所以该抽象类同时定义了需要子类去实际执行的抽象方法。
    public abstract class AbstractPlatformTransactionManager implements PlatformTransactionManager, Serializable {
        @Override
        public final TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {
            Object transaction = doGetTransaction();
            if (null == definition) {
                definition = new DefaultTransactionDefinition();
            }
            if (definition.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) {
                throw new TransactionException("Invalid transaction timeout " + definition.getTimeout());
            }
            // 暂定事务传播为默认的行为
            DefaultTransactionStatus status = newTransactionStatus(definition, transaction, true);
    
            // 开始事务
            doBegin(transaction, definition);
            return status;
        }
    
        protected DefaultTransactionStatus newTransactionStatus(TransactionDefinition definition, Object transaction, boolean newTransaction) {
            return new DefaultTransactionStatus(transaction, newTransaction);
        }
    
        @Override
        public void commit(TransactionStatus status) throws TransactionException {
            if (status.isCompleted()) {
                throw new IllegalArgumentException(
                        "Transaction is already completed - do not call or rollback more than once per transaction");
            }
            DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
            processCommit(defStatus);
        }
    
        private void processCommit(DefaultTransactionStatus status) throws TransactionException {
            doCommit(status);
        }
    
        @Override
        public void rollback(TransactionStatus status) throws TransactionException {
            if (status.isCompleted()) {
                throw new IllegalArgumentException(
                        "Transaction is already completed - do not call commit or rollback more than once per transaction");
            }
            DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
            processRollback(defStatus, false);
        }
    
        private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
            doRollback(status);
        }
    
    
        /**
         * 获取事务
         */
        protected abstract Object doGetTransaction() throws TransactionException;
    
        /**
         * 提交事务
         */
        protected abstract void doCommit(DefaultTransactionStatus status) throws TransactionException;
    
        /**
         * 事务回滚
         */
        protected abstract void doRollback(DefaultTransactionStatus status) throws TransactionException;
    
        /**
         * 开始事务
         */
        protected abstract void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException;
    }
    
    
5、事务切面
  • TransactionInfo

    • TransactionInfoTransactionAspectSupport的内部类,将TransactionAttributeSourceTransactionStatusPlatformTransactionManager进行了组合。
  • TransactionAspectSupport

    • 这是一个比较重要的类,实现了BeanFactoreAwareInitializingBean接口。

    • 另外定义了一个比较重要的方法

      Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
            final InvocationCallback invocation)
      

      在这个方法里,分别获取TransactionAttributeSourceTransactionAttributePlatformTransactionManager。在这里获取到必要参数。开始执行主干流程。

      1. 创建一个TransactionInfo此时将事务的内动都打包交给TransactionInfo
      2. 调用代理方法去执行业务逻辑。
      3. 如果出现异常进行执行异常
      4. 如果没有异常进行clean操作
      5. 最后进行commit

测试

事先准备

mysql数据库,配置好连接信息, 建表语句。(也可以后续执行ApiTest#executeSqlTest 完成建表 )

#创建数据库
CREATE DATABASE mybatis;

#创建用户表
USE mybatis;
 
CREATE TABLE user (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `username` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '用户名',
  PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

属性配置文件

spring.xml

  1. 配置数据库源连接信息,注册jdbcTemplate bean。

  2. 我本地的mysql版本是8.0.33,对应的mysql-connector-java也是 8.0.33,留意pom.xml文件,加上该依赖

  3. &amp; 是为了转义&符号,不加&amp;allowPublicKeyRetrieval=true,会报java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed
    错误,这个异常通常出现在尝试使用JDBC连接到MySQL数据库时,特别是当使用SSL连接到MySQL 8.0或更高版本时。这个异常的原因是JDBC驱动程序默认不允许从服务器检索公钥,这是出于安全考虑,以防止中间人攻击(MITM)。
    
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
	         http://www.springframework.org/schema/beans/spring-beans.xsd">


    <bean id="dataSource"
          class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClass" value="com.mysql.cj.jdbc.Driver"/>
        <property name="jdbcUrl"
                  value="jdbc:mysql://localhost:3306/mybatis?useSSL=false&amp;allowPublicKeyRetrieval=true"/>
        <property name="username" value=""/>
        <property name="password" value=""/>
    </bean>

    <bean id="jdbcTemplate"
          class="cn.suwg.springframework.jdbc.support.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <bean id="jdbcService" class="cn.suwg.springframework.test.bean.JdbcService"/>

</beans>

JdbcService

public class JdbcService {


    /**
     * 使用注解事务.
     */
    @Transactional(rollbackFor = Exception.class)
    public void saveData(JdbcTemplate jdbcTemplate) {
        System.out.println("保存数据,带事务处理");
        jdbcTemplate.execute("insert into user (id, username) values (4, '小苏1')");
        jdbcTemplate.execute("insert into user (id, userna me) values (4, '小苏2')");
    }

    public void saveDataWithoutTx(JdbcTemplate jdbcTemplate) {
        System.out.println("保存数据,不带事务");
        jdbcTemplate.execute("insert into user (id, username) values (4, '小苏1')");
        jdbcTemplate.execute("insert into user (id, userna me) values (4, '小苏2')");
    }
}

测试用例

public class ApiTest {
    private JdbcTemplate jdbcTemplate;

    private JdbcService jdbcService;

    private DataSource dataSource;

    @Before
    public void init() {
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:spring.xml");
        jdbcTemplate = applicationContext.getBean(JdbcTemplate.class);
        dataSource = applicationContext.getBean(DataSource.class);
        jdbcService = applicationContext.getBean(JdbcService.class);
    }


    @Test
    public void testTransaction() throws SQLException {
        // 事务属性源
        AnnotationTransactionAttributeSource transactionAttributeSource = new AnnotationTransactionAttributeSource();
        transactionAttributeSource.findTransactionAttribute(jdbcService.getClass());

        // 事务管理器
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
        TransactionInterceptor interceptor = new TransactionInterceptor(transactionManager, transactionAttributeSource);

        // 组装代理信息
        AdvisedSupport advisedSupport = new AdvisedSupport();
        advisedSupport.setTargetSource(new TargetSource(jdbcService));
        advisedSupport.setMethodInterceptor(interceptor);
        advisedSupport.setMethodMatcher(new AspectJExpressionPointcut("execution(* cn.suwg.springframework.test.bean.JdbcService.*(..))"));

        // 代理对象(Cglib2AopProxy)
        JdbcService proxy_cglib = (JdbcService) new Cglib2AopProxy(advisedSupport).getProxy();

        // 测试调用,有事务【不能同时提交2条有主键冲突的数据】
        proxy_cglib.saveData(jdbcTemplate);

        // 测试调用,无事务【提交2条有主键冲突的数据成功一条】
        //proxy_cglib.saveDataWithoutTx(jdbcTemplate);

    }

}

测试结果:

  1. 测试调用,有事务,不能同时提交2条有主键冲突的数据

在这里插入图片描述

  1. 测试调用,无事务,同时提交2条有主键冲突的数据,成功写入一条

    在这里插入图片描述

总结

  • 以注解方式(即使用@Transactional注解)的声明式事务管理为例,其流程如下:
    1. 事务的开启
      • 当一个被@Transactional注解标记的方法被调用时,Spring框架会自动创建一个代理对象来拦截该方法的执行。
      • 在方法执行前,代理对象会开启一个事务。这通常是通过调用事务管理器的getTransaction方法实现的,该方法会返回一个TransactionStatus对象,用于记录当前事务的状态。
    2. 事务的执行
      • 接下来,代理对象会调用目标方法的实际逻辑,执行数据库操作等。
      • 在方法执行过程中,如果发生异常,代理对象会捕获这些异常,并根据事务的配置决定是否进行回滚。
    3. 事务的提交或回滚
      • 如果方法执行成功且没有抛出任何导致事务回滚的异常,那么代理对象会在方法执行完毕后提交事务。这通常是通过调用事务管理器的commit方法实现的。
      • 如果方法执行过程中抛出了异常,且该异常符合事务回滚的条件(如被@Transactional注解的rollbackFor属性指定),那么代理对象会回滚事务。这通常是通过调用事务管理器的rollback方法实现的。
  • 通过本章学习,我们深刻理解了Spring事务管理的核心原理和实现方式。声明式事务管理以其简洁、易用的特点成为最常用的事务管理方式。在未来的开发中,我们应充分利用Spring的事务管理功能,确保数据的一致性和完整性,提高开发效率和代码的可维护性。
  • 扩展阅读: spring 事务注解失效的情况,https://blog.csdn.net/minghao0508/article/details/124374637

参考书籍:《手写Spring渐进式源码实践》

书籍源代码:https://github.com/fuzhengwei/small-spring

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;