第十九章 事务管理
背景
事务
- 事务(Transaction)是一个不可分割的工作单位,它由一组有限的数据库操作序列组成。在计算机术语中,事务是指访问并可能更新数据库中各种数据项的一个程序执行单元。
- 事务是为了保证数据库的一致性而提出的一种处理机制,它将一组操作视为一个整体进行处理,确保这些操作的原子性、一致性、隔离性和持久性。
- 事务具有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事务处理过程进行抽象化。整体设计结构如下图:
实现
代码结构
源码实现:https://github.com/swg209/spring-study/tree/main/step19-jdbc-transaction
类图
整个类图中,可以看到划分成了事务定义
,解析事务属性、获取事务
,事务切面
、开启事务
四个大块。
实现步骤
本章节主要关注事务管理主流程,配合扩展功能的springframework的core目录下的类,未做解析,后续有兴趣的伙伴可以自行阅读源码。
1、事务定义
- TransactionDefine
- 这是一个接口,定义了事务的一些属性。比如事务的传播性、事务隔离级别、超时时间等
- DefaultTransactionDefinition
- 这个类是TransactionDefine接口的实现类。用来设置事务的传播、隔离、超时等属性
-
TransactionAttribute
- 直接看这个接口的名字含义是属性的属性。确实如此,这个接口继承TransactionDefine,添加了一个
rollbackOn(Throwable ex)
方法。在进行事务回滚前用来判断对于当前发生的异常是否需要回滚。
- 直接看这个接口的名字含义是属性的属性。确实如此,这个接口继承TransactionDefine,添加了一个
-
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、事务状态
-
TransactionStatus
- 这个接口是对事务的状态进行描述,定义了
Savepoint
、是否是新事务等信息。通过TransactionDefinition
中的事务属性来创建一个TransactionStatus
。
- 这个接口是对事务的状态进行描述,定义了
-
AbstractTransactionStatus
- 事务状态描述的抽象类
-
DefaultTransactionStatus
- 默认的事务状态描述类
4、事务管理器
-
PlatformTransactionManager
- 这是一个比较重要的接口。定义了获取事务状态、事务提交、事务回滚等方法
java">TransactionStatus getTransaction(@Nullable TransactionDefinition definition)void commit(TransactionStatus status)void rollback(TransactionStatus status)
-
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
TransactionInfo
是TransactionAspectSupport
的内部类,将TransactionAttributeSource
、TransactionStatus
、PlatformTransactionManager
进行了组合。
-
TransactionAspectSupport
-
这是一个比较重要的类,实现了
BeanFactoreAware
、InitializingBean
接口。 -
另外定义了一个比较重要的方法
java">Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,final InvocationCallback invocation)
在这个方法里,分别获取
TransactionAttributeSource
、TransactionAttribute
、PlatformTransactionManager
。在这里获取到必要参数。开始执行主干流程。- 创建一个
TransactionInfo
此时将事务的内动都打包交给TransactionInfo
- 调用代理方法去执行业务逻辑。
- 如果出现异常进行执行异常
- 如果没有异常进行clean操作
- 最后进行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
-
配置数据库源连接信息,注册jdbcTemplate bean。
-
我本地的mysql版本是8.0.33,对应的mysql-connector-java也是 8.0.33,留意pom.xml文件,加上该依赖
-
& 是为了转义&符号,不加&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&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);}}
测试结果:
- 测试调用,有事务,不能同时提交2条有主键冲突的数据
-
测试调用,无事务,同时提交2条有主键冲突的数据,成功写入一条
总结
- 以注解方式(即使用
@Transactional
注解)的声明式事务管理为例,其流程如下:- 事务的开启:
- 当一个被
@Transactional
注解标记的方法被调用时,Spring框架会自动创建一个代理对象来拦截该方法的执行。 - 在方法执行前,代理对象会开启一个事务。这通常是通过调用事务管理器的
getTransaction
方法实现的,该方法会返回一个TransactionStatus
对象,用于记录当前事务的状态。
- 当一个被
- 事务的执行:
- 接下来,代理对象会调用目标方法的实际逻辑,执行数据库操作等。
- 在方法执行过程中,如果发生异常,代理对象会捕获这些异常,并根据事务的配置决定是否进行回滚。
- 事务的提交或回滚:
- 如果方法执行成功且没有抛出任何导致事务回滚的异常,那么代理对象会在方法执行完毕后提交事务。这通常是通过调用事务管理器的
commit
方法实现的。 - 如果方法执行过程中抛出了异常,且该异常符合事务回滚的条件(如被
@Transactional
注解的rollbackFor
属性指定),那么代理对象会回滚事务。这通常是通过调用事务管理器的rollback
方法实现的。
- 如果方法执行成功且没有抛出任何导致事务回滚的异常,那么代理对象会在方法执行完毕后提交事务。这通常是通过调用事务管理器的
- 事务的开启:
- 通过本章学习,我们深刻理解了Spring事务管理的核心原理和实现方式。声明式事务管理以其简洁、易用的特点成为最常用的事务管理方式。在未来的开发中,我们应充分利用Spring的事务管理功能,确保数据的一致性和完整性,提高开发效率和代码的可维护性。
- 扩展阅读: spring 事务注解失效的情况,https://blog.csdn.net/minghao0508/article/details/124374637
参考书籍:《手写Spring渐进式源码实践》
书籍源代码:https://github.com/fuzhengwei/small-spring