SpringFramework — 事务

小龙 770 2022-06-21

回顾

先简单的回顾一下事务基础

简单来说事务是一组逻辑操作的组合,它们的执行结构要买全部成功,要么全部失败。

事务四大特性

  • 原子性:一个事务就是一个不可分割的整体,事务中的操作要么全部成功,要么全部失败;原子性强调的是事务的整体
  • 一致性:事务执行后,所有的数据都应该保持一致性;一致性强调的是数据的完整
  • 隔离性:多个数据操作并发执行时,一个请求的事务操作不能被其他操作干扰,多个并发事务执行之间要相互隔离;隔离性强调的是并发的隔离
  • 持久性:事务提交之后,对数据做出的修改会立刻持久化到磁盘中,永久存储;持久性强调的是操作的结果

事务并发操作汇总会出现三种问题:

  • 脏读:一个事务督导了另一个事务没有提交的数据
  • 不可重复读:一个事务读到了另一个事务已提交修改的数据;对同一个数据查询两次,结果不一致
  • 幻读:一个事务读到了另一个事务已提交新增的数据;对同一张表查询两次,第二次查询出现了第一次查询没有的数据,导致结果不一致。

针对上面的三个问题,数据库提供了事务的隔离级别:

  • read uncommitted:读未提交 —— 不解决任何问题
  • read committed:读已提交 —— 解决脏读
  • repeatable read: 可重复读 —— 解决脏读、不可重复读
  • serializable:可串行化 —— 解决脏读、幻读、不可重复读

这四种隔离级别,自上而下级别逐级降低。MySQL中默认的事务隔离级别是 repeatable read

Spring中的事务控制

编程式事务

SpringFramework 编程式事务中两个重要组件:DataSourceTransactionManagerTransactionTemplate

  • DataSourceTransactionManager:事务管理器,它负责控制事务
  • TransactionTemplate:事务模板,使用它可以完成编程式事务

注册方式

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>

<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
    <property name="transactionManager" ref="transactionManager"/>
</bean>

使用方式
在代码中引入 TransactionTemplate

@Autowired
TransactionTemplate transactionTemplate;

之后就是使用事务模板,调用方法 execute()

transactionTemplate.execute(???);

这个 execute() 方法中需要传入一个 TransactionCallback 类型的对象,这个 TransactionCallback 接口是一个函数式接口,可以直接使用 Lambda 表达式实现

@FunctionalInterface
public interface TransactionCallback<T> {
	T doInTransaction(TransactionStatus status);
}
transactionTemplate.execute(status -> {
        // 具体数据库操作
        return null;
    });

TransactionCallback的优化

TransactionCallback 需要有一个返回值,我们每次都要写一个 return null; 有点费劲,而且这个返回值根本用不到。所有 SpringFramework 针对 TransactionCallback 接口增加了一个抽象类 TransactionCallbackWithoutResult

public abstract class TransactionCallbackWithoutResult implements TransactionCallback<Object> {

	@Override
	public final Object doInTransaction(TransactionStatus status) {
		doInTransactionWithoutResult(status);
		return null;
	}

	protected abstract void doInTransactionWithoutResult(TransactionStatus status);
}

这个抽象类就是帮我返回那个 null 的,其他的内容和 TransactionCallback 完全一样。我们在开发中可以直接使用它,但是它无法使用 Lambda 表达式。

    transactionTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus status) {
            // 具体数据库操作
        }
    });

DataSourceTransactionManager

基于数据源的事务管理器。它实现了 PlatformTransactionManager 定义有 commit()rollback() 方法

public interface PlatformTransactionManager extends TransactionManager {
	TransactionStatus getTransaction(TransactionDefinition definition)
			throws TransactionException;
	void commit(TransactionStatus status) throws TransactionException;
	void rollback(TransactionStatus status) throws TransactionException;
}

这个 commit 和 rollback 方法要传入一个 TransactionStatus 的参数。

注意 PlatformTransactionManager 还有一个根接口 TransactionManager ,它是 SpringFramework 5.2 才新加的接口,由于响应式 jdbc 在 SpringFramework 中引入,在 SpringFramework 5.2 之后引入了响应式事务,由此产生了新的根接口。

TransactionTemplate

事务模板,它与 JdbcTemplate 在设计上类似的,提供一个简单的模板来完成平时比较复杂的工作,它的核心方法是来自 TransactionOperations 接口定义的 execute 方法:

@Override
@Nullable
public <T> T execute(TransactionCallback<T> action) throws TransactionException {
    Assert.state(this.transactionManager != null, "No PlatformTransactionManager set");

    if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {
        return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action);
    }
    else {
        TransactionStatus status = this.transactionManager.getTransaction(this);
        T result;
        try {
            result = action.doInTransaction(status);
        }
        catch (RuntimeException | Error ex) {
            // 业务代码出现异常,回滚事务
            rollbackOnException(status, ex);
            throw ex;
        }
        catch (Throwable ex) {
            // 业务代码出现异常,回滚事务
            rollbackOnException(status, ex);
            throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
        }
        // try块没有出现异常,业务代码执行成功,提交事务
        this.transactionManager.commit(status);
        return result;
    }
}

它的事务控制,与咱自己写的,在思路上是没有任何区别的。而且它提交和回滚事务的动作,就是拿的 TransactionManager 执行的 commit 和 rollback 方法。

声明式事务控制

前面的编程式事务,看上去已经挺简单了,但这种写法还是麻烦的,每个 Service 中都要注入 TransactionTemplate 不说,编写的所有方法都要先写一句 transactionTemplate.execute 方法,每个方法都要写,那还是很烦的。

为了解决这个问题,SpringFramework 提供了声明式事务的配置方案。使用它,可以更简单的配置和实现事务控制。

声明式事务控制,分为 XML 和 注解配置

基于 XML 配置文件实现的声明式事务控制

对于 xml 配置文件的声明式事务,需要引入新的命名空间:tx,它就是关于事务的配置部分。这里需要用到 AOP

xml 配置方式

<aop:config>
    <aop:advisor advice-ref="transactionAdvice"
                 pointcut="execution(* com.linkedbear.spring.transaction.c_declarativexml.service.*.*(..))"/>
</aop:config>

这个 <aop:advisor> 标签是 SpringFramework 原生 AOP 的东西,它相当于配置一个增强器 Advisor ,这个增强器的通知就是上面的事务通知,切入点表达式指定的就是要被控制事务的方法。很明显,小册这样配置,相当于让 c_declarativexml.service 包下面的所有类的所有方法,都织入一个事务通知。

<aop:aspect> 针对的是一个 Aspect 切面类的多个通知方法配置,而 <aop:advisor> 针对的是一个通知方法配置。这种配置是为了兼顾 SpringFramework 原生的 AOP 通知写法

完整 xml 配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	   xmlns:context="http://www.springframework.org/schema/context"
	   xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
	   xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">


	<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
		<property name="dataSource" ref="dataSource"/>
	</bean>

	<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
		<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
		<property name="url" value="jdbc:mysql://localhost:3306/spring-dao?characterEncoding=utf8"/>
		<property name="username" value="root"/>
		<property name="password" value="Rsthe_123456"/>
	</bean>

	<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource"/>
	</bean>
  
	<tx:advice id="transactionAdvice" transaction-manager="transactionManager">
		<tx:attributes>
			<tx:method name="add"/>
			<tx:method name="save"/>
			<tx:method name="transferAccounts"/>
		</tx:attributes>
	</tx:advice>

	<aop:config>
		<aop:advisor advice-ref="transactionAdvice" pointcut="execution(public * com.rsthe.JDBC.transaction.service..*.*(..))"/>
	</aop:config>

	<context:component-scan base-package="com.rsthe.JDBC.transaction"/>
	<context:annotation-config/>
</beans>

其中 <tx:advice id="transactionAdvice" transaction-manager="transactionManager"> 中的就是声明式事务的核心

上面 <tx:attributes> 中的 <tx:method name="add"/> 配置存在一个问题,如果我们的 service 中有100个方法,那么在此处我们也要定义100个 <tx:method />,这显然是不行的;SpringFramework 帮我们考虑到了,所以这里可以写通配符:

<tx:advice id="transactionAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="save*"/> <!-- 所有save开头的方法全部开启事务控制 -->
        <tx:method name="*"/> <!-- 任意方法均开启 -->
    </tx:attributes>
</tx:advice>

tx:method 的其它属性

处理 name 之外, <tx:method> 标签还有一些其它的属性:

  • isolation:事务隔离级别。默认是 DEFAULT,依据数据库默认的事务隔离级别来定义
  • timeout:事务超时时间,当事务执行超过指定时间后,事务会自动终止并回滚,单位 ;默认值 -1,表示永不超时
  • read-only:是否设置只读事务。默认 false,代表写型事务。当设置为 reue 时,当前事务为只读事务,通常用于查询操作(此时不会有 setAutoCommit(false)操作,可以加快查询速度)。
  • rollback-for:当方法触发指定异常时,事务回滚,需要传入异常类的全限定名。
    • 默认值为 null:表示捕获所有 RuntimeExceptionError 异常
    • 一般情况下,我们在日常开发中都设置为 Exception,目的是捕获非运行时异常
  • no-rollback-for:当方法触发指定异常时不回滚事务继续执行,需要传入异常类的全限定名。默认值为空,代表不忽略异常
  • propagetion事务传播行为

基于注解驱动的声明式事务

注解声明式事务的配置没有 xml 那么复杂;只需要在配置类上使用 @EnableTransactionManagement 注解,表示开启注解声明式事务控制

@Configuration
@ComponentScan("com.linkedbear.spring.transaction.d_declarativeanno")
@EnableTransactionManagement
public class DeclarativeTransactionConfiguration {}

不过只定义一个配置类,SpringFramework 也不知道那些方法需要事务,那些不需要,所以我们还要在需要事务的方法上使用 @Transactional 注解

事务的传播行为

试想一个场景:注册用户赠送积分。新用户在平台上注册成功后,通常会赠送一些积分;

用户注册和赠送积分肯定是分开的两个业务,这个业务都需要定义事务控制。看一下业务伪代码

@Service
public class PointService {
    @Transactional
    public void addPoint() {
        System.out.println("addPoint 添加积分 ......");
    }
}

@Service
public class UserService {
    
    @Autowired
    PointService pointService;
    
    @Transactional
    public void register() {
        // 持久化操作 ......
        System.out.println("register 注册用户 ......");
        pointService.addPoint();
    }
}

register 方法上有事务,addPoint 方法也有事务,这样就形成了事务的嵌套

如果 register 方法开启了事务,当执行 PointService 的 addPoint 方法时,是让它加入到当前事务呢?还是重新开一个事务?还是利用保存点的方案?等等等等,这些行为都是外层的事务传播到内层的事务后,内层的事务作出的行为(持有的态度),这就是事务传播行为。

事务的传播行为的 7 种策略

REQUIRE:必须的

这是 SpringFramework 中默认的事务传播行为,它的定义是:如果当前没有事务运行,则会开启一个新的事务;如果当前有事务运行,则方法会运行在当前事务中。简单概括就是:你没有,我创建;你有,我加入

REQUIRES_NEW:新事物

新事务,顾名思义,它必须开启一个全新的事务,那它的定义可以描述为:如果当前没有事务运行,则开启一个新的事务;如果当前已经存在事务,则将原事务挂起(暂停),开启一个新的事务运行。当前新开启的事务运行完毕后,再恢复原事务执行。简单概述:你没有,我开启;你有,你暂停,我开新的

SUPPORTS:支持

支持的定义是:如果当前有事务运行,则加入到事务中;如果当前没有事务运行,则以无事务方式运行。支持更倾向于一种无所谓的态度,简述就是:有我就加入,没有就拉倒

NOT_SUPPORTED:不支持

不支持与上面支持的定义相反:如果当前有事务运行,则将该事物挂起,以无事务方式运行。这种态度就是完全不接受事务,简单概述:有事务我不要,没有正好

MANDATORY:强制

强制的定义是:当前方法必须在事务中运行,如果没有事务,就直接抛出异常。它必须在事务中执行,没有事务就不干活了,简单概述:要干活就必须有事务,没有事务就不干

NEVER:不允许

与强制相反:当前方法不运行在事务中运行,如果存在事务就直接抛出异常。简述就是:要干活就不准有事务,有实物就不干活

NESTED:嵌套

嵌套比较特殊,它是基于保存点 SavePoint 的传播行为。它的定义是:如果当前没有事务运行,则开启一个新的事务;如果当前已经有事务运行,则会记录到一个保存点,并继续运行在当前事务中。如果子事务运行中抛出了异常,则不会全部回滚,而是回滚到上一个保存点。这个设计就是保存点设计,简单概述就是:你没有,我开启;你有,我记下;我走了,你再走;我挂了,就当无事发生

由于在 NESTED 的执行需要依赖关系型数据库的 SavePoint 机制,所以这种传播行为只适用于 DataSourceTransactionManage (即基于数据源的事务管理器)。

事务控制模型

SpringFramework 事务的三大核心

SpringFramework 的事务控制模型,实际上是三个最顶层的接口;

  • PlatformTransactionManager:平台事务管理器
  • TransactionDefinition:事务定义
  • TransactionStatus:事务状态

简单来说:SpringFramework 对于事务的控制,可以理解为事务管理器,可以根据事务的定义,获取 / 控制事务的状态

PlatformTransactionManager

平台事务管理器:SpringFramework 一开始对事务的控制就没有局限在单体应用上的数据源上;它有设计基于 Hibernate 的、JPA的,JTA(分布式事务) 的,这些不同的类型 SpringFramework 把它视为不同的 平台 ,所有才有了 平台事务管理器(Platform)

SpringFramework 做事务控制,必须依赖事务管理器,所以 PlatformTransactionManager 的地位至高无上;但是在 SpringFramework 5.2 之后,PlatformTransactionManager 不再是顶级接口了,它有一个父接口叫 TransactionManager :

public interface PlatformTransactionManager extends TransactionManager
public interface TransactionManager {}

PlatformTransactionManager的接口方法定义

public interface PlatformTransactionManager extends TransactionManager {
	TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
			throws TransactionException;
	void commit(TransactionStatus status) throws TransactionException;
	void rollback(TransactionStatus status) throws TransactionException;
}

TransactionManager 中没有任何常量和方法的定义, 它仅仅是传统平台事务管理器,或者响应式事务管理器的标识接口而已。

三个方法分别的含义:

  • getTransaction :传入 TransactionDefinition ,返回 TransactionStatus ,很明显它是根据一个事务的定义信息,查询事务当前的状态。
  • commit :提交事务,它需要传入事务当前的状态,来判断当前事务是否允许提交(如果当前事务已标记为需要回滚,则无法提交)
  • rollback :回滚事务,它也需要传入事务当前的状态,以此判断当前事务是否允许回滚(如果当前事务已经完成了,则无法回滚)

PlatformTransactionManager的层次体系

image-1655867423618

TransactionDefinition

事务定义,也叫做事务明细 / 事务属性。类比于 Bean 的 BeanDefinition,里面肯定存放了好多有关事务的属性:

  • 事务隔离级别
  • 事务传播行为
  • 是否为读写事务
  • 超市时间
  • … … … …

TransactionStatus

事务状态,这里面记录的是当前事务运行的状态,比方说:

  • 是否是一个全新的事务
  • 是否有保存点
  • 事务是否完成
  • … … … …

# AspectJ表达式 # 事务