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

server/2024/12/3 1:44:25/

第十九章 事务管理

背景

事务

  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/server/146893.html

相关文章

针对Qwen-Agent框架的Function Call及ReAct的源码阅读与解析:Agent基类篇

文章目录 Agent继承链Agent类总体架构初始化方法`__init__` 方法:`_init_tool` 方法:对话生成方法`_call_llm` 方法:工具调用方法`_call_tool` 方法:`_detect_tool` 方法:整体执行方法`run` 方法:`_run` 方法:`run_nonstream` 方法总结回顾本文在 基于Qwen-Agent框架的Functio…

IT人日常健康工作生活方案

1. 早餐(7:00-8:00) 早餐是一天中最重要的一餐,提供充足的能量来启动新的一天。根据亚洲饮食的特点,我们加入了米饭、豆腐、蔬菜等传统食材,同时保持高蛋白、低糖的原则。 糙米粥或小米粥(1碗):低GI碳水化合物,有助于稳定血糖,提供持久能量。可加入少量的红枣、枸杞…

node-koa

文章目录 koa 基础属性koa-jwtkoa 路由koa - swaggerkoa - loggerkoa 之 mysql1. 建立数据库连接2. 插入用户信息的路由及逻辑3. 查询用户信息的路由及逻辑4. 启动Koa应用5. 代码解释6. 注意事项 koa 特点及优势 koa 基础属性 Koa是一个基于Node.js的下一代Web框架&#xff0c…

【拥抱AI】如何查看Milvus的使用情况?

查看Milvus的使用情况和性能指标可以帮助你了解数据库的健康状况、性能指标和资源使用情况。以下是一些常用的方法和工具&#xff0c;帮助你全面监控和查看Milvus的使用情况和性能指标。 1. 查看日志 Milvus的日志文件记录了运行时的各种信息&#xff0c;包括错误、警告和调…

第4章-运行时数据区-虚拟机栈

虚拟机栈的出现背景 由于跨平台性的设计&#xff0c;Java的指令都是根据栈来设计的。不同平台CPU架构不同&#xff0c;所以不能设计为基于寄存器的【如果设计成基于寄存器的&#xff0c;耦合度高&#xff0c;性能会有所提升&#xff0c;因为可以对具体的CPU架构进行优化&#…

PHP 常量

PHP 常量 引言 在 PHP 编程语言中&#xff0c;常量是一种用于存储固定值的标识符。与变量不同&#xff0c;常量一旦被定义&#xff0c;其值就不能被改变。常量在程序中广泛用于存储那些不会改变的值&#xff0c;如配置设置、系统参数等。本文将详细介绍 PHP 常量的概念、用法…

学习嵩山版《Java 开发手册》:编程规约 - 命名风格(P15 ~ P16)

概述 《Java 开发手册》是阿里巴巴集团技术团队的集体智慧结晶和经验总结&#xff0c;他旨在提升开发效率和代码质量 《Java 开发手册》是一本极具价值的 Java 开发规范指南&#xff0c;对于提升开发者的综合素质和代码质量具有重要意义 学习《Java 开发手册》是一个提升 Jav…

Vue进阶之单组件开发与组件通信

书接上篇&#xff0c;我们了解了如何快速创建一个脚手架&#xff0c;现在我们来学习如何基于vite创建属于自己的脚手架。在创建一个新的组件时&#xff0c;要在新建文件夹中打开终端创建一个基本的脚手架&#xff0c;可在脚手架中原有的文件中修改或在相应路径重新创建&#xf…