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

ops/2024/11/29 9:16:12/

第十九章 事务管理

背景

事务

  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
    java">public boolean rollbackOn(Throwable ex) {return (ex instanceof RuntimeException || ex instanceof Error);
    }
    
  • RuleBasedTransactionAttribute

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

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

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

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

    • TransactionAttributeSource

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

      • 该抽象类实现了TransactionAttributeSource接口,还是按照老套路定义了获取TransactionAttribute的模版,真正的获取交给子类去实现。
      java">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

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

    • 事务管理的抽象实现类。采用同样的套路定义了事务的操作流程,分别是获取事务,事务提交,事务回滚。这三个步骤在不同的数据源上操作又有区别,所以该抽象类同时定义了需要子类去实际执行的抽象方法。
    java">public abstract class AbstractPlatformTransactionManager implements PlatformTransactionManager, Serializable {@Overridepublic 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);}@Overridepublic 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);}@Overridepublic 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接口。

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

      java">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/beanshttp://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

java">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')");}
}

测试用例

java">public class ApiTest {private JdbcTemplate jdbcTemplate;private JdbcService jdbcService;private DataSource dataSource;@Beforepublic void init() {ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:spring.xml");jdbcTemplate = applicationContext.getBean(JdbcTemplate.class);dataSource = applicationContext.getBean(DataSource.class);jdbcService = applicationContext.getBean(JdbcService.class);}@Testpublic 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


http://www.ppmy.cn/ops/137613.html

相关文章

史陶比尔机器人维修-接口总结

史陶比尔机器人的通讯接口在机器人与外部设备的数据交互、远程监控与控制等方面发挥着举足轻重的作用。这一接口&#xff0c;作为机器人控制系统与外界沟通的桥梁&#xff0c;不仅承载着数据传输的重任&#xff0c;还涉及到程序的更新维护、错误诊断修复等一系列关键功能&#…

(长期更新)《零基础入门 ArcGIS(ArcMap) 》实验二----网络分析(超超超详细!!!)

相信实验一大家已经完成了&#xff0c;对Arcgis已进一步熟悉了&#xff0c;现在开启第二个实验 ArcMap实验--网络分析 目录 ArcMap实验--网络分析 1.1 网络分析介绍 1.2 实验内容及目的 1.2.1 实验内容 1.2.2 实验目的 2.2 实验方案 2.3 实验流程 2.3.1 实验准备 2.3.2 空间校正…

Redis的基础知识·

Redis是一个基于内存的key-value的结构数据库 基于内存存储 读写性能高适合存储热点数据&#xff08;热点商品 咨询 新闻&#xff09; 开启Redis 首先输入命令 redis-server.exe redis.windows.conf 然后重新打开命令行窗口 输入命令 redis-cli.exe 输入密码 …

联通云服务器部署老项目tomcat记录

1.先在服务器上安装mysql和tomcat 2.tomcat修改端口 3.在联通云运控平台配置tomcat访问端口&#xff08;相当于向外部提供可访问端口&#xff09; 4.将tomcat项目放在服务器tomcat的webapps里面 5.在mysql里创建项目数据库&#xff0c;运行sql创建表和导入数据 6.在配置文…

HTML飞舞的爱心

目录 系列文章 写在前面 完整代码 代码分析 写在后面 系列文章 序号目录1HTML满屏跳动的爱心&#xff08;可写字&#xff09;2HTML五彩缤纷的爱心3HTML满屏漂浮爱心4HTML情人节快乐5HTML蓝色爱心射线6HTML跳动的爱心&#xff08;简易版&#xff09;7HTML粒子爱心8HTML蓝色…

怎么样才算得上熟悉高并发编程?

提到并发编程很多人就会头疼了&#xff1b;首先就是一些基础概念&#xff1a;并发&#xff0c;并行&#xff0c;同步&#xff0c;异步&#xff0c;临界区&#xff0c;阻塞&#xff0c;非阻塞还有各种锁全都砸你脸上&#xff0c;随之而来的就是要保证程序运行时关键数据在多线程…

HarmonyOS:@Provide装饰器和@Consume装饰器:与后代组件双向同步

一、前言 Provide和Consume&#xff0c;应用于与后代组件的双向数据同步&#xff0c;应用于状态数据在多个层级之间传递的场景。不同于上文提到的父子组件之间通过命名参数机制传递&#xff0c;Provide和Consume摆脱参数传递机制的束缚&#xff0c;实现跨层级传递。 其中Provi…

【大数据学习 | Spark-SQL】SparkSQL读写数据

我们使用sparksql进行编程&#xff0c;编程的过程我们需要创建dataframe对象&#xff0c;这个对象的创建方式我们是先创建RDD然后再转换rdd变成为DataFrame对象。 但是sparksql给大家提供了多种便捷读取数据的方式。 //原始读取数据方式 sc.textFile().toRDD sqlSc.createDat…