Spring事务原理 二

在上一篇博文《Spring事务原理 一》中,我们熟悉了Spring声明式事务的AOP原理,以及事务执行的大体流程。

**本文中,介绍了Spring事务的核心组件、传播行为的源码实现。**下一篇中,我们将结合案例,来讲解实战中有关事务的易错点。

本文中源码来自Spring 5.3.x分支,github源码地址:GitHub - spring-projects/spring-framework: Spring Framework

一 Spring事务的核心组件

了解相关类和接口,看看Spring对概念、术语是如何封装的?

1.1 PlatformTransactionManager

事务管理器接口,负责获取数据库连接,事务的开启、提交和回滚。

有抽象实现AbstractPlatformTransactionManager,其中定义了doGetTransaction、doBegin、doCommit、doRollback等抽象方法,待子类实现。

AbstractPlatformTransactionManager中有几个值得关注的属性:

java 复制代码
// 是否允许嵌套事务
private boolean nestedTransactionAllowed = false;

// 局部失败时全局回滚,当为false时,部分失败则不回滚
private boolean globalRollbackOnParticipationFailure = true;

private boolean failEarlyOnGlobalRollbackOnly = false;

private boolean rollbackOnCommitFailure = false;

它有以下常见子类:

  • DataSourceTransactionManager:用于JDBC和MyBatis等基于数据源的事务管理。
  • HibernateTransactionManager:用于Hibernate框架的事务管理。
  • JpaTransactionManager:用于JPA(Java Persistence API)的事务管理。

DataSourceTransactionManager

该类中定义了两个属性,对doCommit等抽象方法提供实现。

  • doGetTransaction方法:创建一个DataSourceTransactionObject对象,设置connection。

  • doBegin方法:执行事务前的准备工作,如设置

    • 如果DataSourceTransactionObject没有连接,则获取一个连接

    • 根据TransactionDefinition,为connection设置属性,如isolationLevel、readOnly、timeout;

    • connection.setAutoCommit(false),关闭自动提交;

    • 将connection与当前线程绑定;

  • doCommit方法:从TransactionStatus中获取TransactionObject,拿到connection调用commit();

  • doRollback方法:与doCommit实现相似,只是调connection.rollback();

1.2 TransactionDefinition

定义事务的属性,如隔离级别、传播行为、超时时间等。

隔离级别(Isolation Level):定义了事务之间的隔离程度,常见的有:

  • DEFAULT:使用数据库默认的隔离级别。
  • READ_UNCOMMITTED:允许读取未提交的数据,可能导致脏读。
  • READ_COMMITTED:只能读取已提交的数据,避免脏读。
  • REPEATABLE_READ:确保在同一事务中多次读取同一数据时,结果一致。
  • SERIALIZABLE:最高的隔离级别,确保事务串行执行,避免脏读、不可重复读和幻读。

超时时间(Timeout):事务的超时时间,超过该时间未完成则自动回滚。

只读(Read-only):指定事务是否为只读事务,优化性能。

在子类DefaultTransactionDefinition中,可以看到默认值:PROPAGATION_REQUIRED、ISOLATION_DEFAULT、TIMEOUT_DEFAULT、非readOnly。

当使用@Transactional时,会将注解属性解析成一个TransactionDefinition对象。

1.3 TransactionStatus

表示事务的状态,提供了以下方法:

  • isNewTransaction():判断当前事务是否为新事务。
  • hasSavepoint():判断是否存在保存点(用于嵌套事务)。
  • setRollbackOnly():标记事务为回滚状态。
  • isRollbackOnly():判断事务是否被标记为回滚。

在子类DefaultTransactionStatus中,有这些属性

java 复制代码
private boolean rollbackOnly = false;

private boolean completed = false;

private final Object transaction;

private final boolean newTransaction;

private final boolean newSynchronization;

private final boolean readOnly;

1.4 TransactionSynchronizationManager

事务同步管理器,用于将事务相关信息与当前线程绑定,以支持各种事务传播行为。其中有多个ThreadLocal属性。

为什么保存连接的resources是Map类型?因为支持多数据源,当一个方法中操作多个数据库时,线程中就得保存多个connectionHolder对象,因此使用Map结构,key就是dataSource对象。

1.5 TransactionInterceptor

事务拦截器,就是AOP的代理逻辑,具体实现在TransactionAspectSupport#invokeWithinTransaction中。

大体流程为:

  1. 获取当前方法的@Transaction注解属性,创建TransactionDefinition对象;
  2. 获取TransactionManager对象;
  3. 根据方法名生成事务名;
  4. 如有必要则创建事务,并处理传播行为;
  5. 在try中执行下一个interceptor或被代理对象中方法;
  6. 异常时先回滚事务,正常时提交事务;
  7. 当前方法执行结束,还原TransactionInfo(恢复上层方法的事务信息)。

二 事务的传播机制

2.1 什么是事务传播

在日常开发中,业务代码中经常出现方法间调用,比如购物时下单减和库存:

java 复制代码
import com.xiakexing.dao.InventoryDao;
import com.xiakexing.dao.OrderDao;
import com.xiakexing.entity.Order;

public class OrderService {

    private OrderDao orderDao;
    private InventoryDao inventoryDao;

    public void saveOrder(Order order) {
        orderDao.save(order);
        updateInventory(order.getCode(), order.getCount());
    }

    public void updateInventory(String code, int count) {
        inventoryDao.update(code, count);
    }
}
  1. saveOrder()和updateInventory(),所有sql需要在同一个事务中;
  2. 单独调用updateInventory()时,如进货时不需要事务。

可见,updateInventory方法,在不同场景下对事务有不同要求。Spring中又如何实现呢?

Spring定义了传播行为(Propagation Behavior),定义了方法间调用时事务如何传递,类型有:

  • REQUIRED:如果当前线程存在事务,则加入该事务;如果不存在,则创建一个新事务。
  • REQUIRES_NEW:总是创建一个新事务,如果当前线程存在事务,则挂起当前事务。
  • SUPPORTS:如果当前线程存在事务,则加入该事务;如果不存在,则以非事务方式执行。
  • NOT_SUPPORTED:以非事务方式执行,如果当前线程存在事务,则挂起当前事务。
  • MANDATORY:如果当前线程存在事务,则加入该事务;如果不存在则抛出异常。
  • NEVER:以非事务方式执行,如果当前线程存在事务,则抛出异常。
  • NESTED:如果当前线程存在事务,则以嵌套事务中执行;如果不存在,则创建一个新事务。

这儿为什么强调线程呢?

方法间调用都在某个线程的方法栈中,按FILO顺序执行。如果两个方法的中sql使用两个不同的数据库连接执行,显然无法纳入一个事务中。

因为,数据库连接必须能够跨方法传递,Spring底层就是将connection放到ThreadLocal中。

2.2 传播机制的实现

2.2.1 线程绑定连接

在DataSourceTransactionManager#doBegin中:

  • 从DataSource获取数据连接connection,设置autocommit=false、隔离级别、超时时间等属性;

  • 将connection放入ThreadLocal<Map>,Map的key是DataSource对象,value是connectionHolder对象。

可见,方法间调用时可以从ThreadLocal中拿到同一个连接,去执行不同的SQL,进而一同提交或回滚。

2.2.2 处理传播机制

真正执行被代理对象方法前,会判断是否创建事务。

调用AbstractPlatformTransactionManager#getTransaction,逻辑如下:

  1. 创建DataSourceTransactionObject对象,从ThreadLocal中获取connectionHolder(可能为null);
  2. 当connectionHolder不为null且connectionHolder.transactionActive=ture时,说明已存在事务:

如果当前线程中存在事务:

  • 如果当前方法传播行为是PROPAGATION_NEVER,则抛异常

  • 如果是PROPAGATION_NOT_SUPPORTED,则挂起当前事务,用一个新连接的非事务方式执行当前方法;

  • 如果是PROPAGATION_REQUIRES_NEW,则挂起当前事务,开启一个新事务(获取新连接并带事务执行);

  • 如果是PROPAGATION_NESTED,则先设置savepoints(可以回滚到此处),然后使用同一个连接继续执行。

如果当前线程中不存在事务:

  • 如果传播行为是PROPAGATION_MANDATORY,则抛异常

  • 如果传播行为是PROPAGATION_REQUIRED、PROPAGATION_REQUIRES_NEW、PROPAGATION_NESTED,则开启事务

流程图如下:

三 总结

  1. Spring中,对于事务这一抽象概念,从多个方法进行了良好封装,如将隔离级别、超时时间等封装为TransactionDefinition,将事务状态、是否回滚等封装为TransactionStatus。
  2. 事务的传播行为,发生在方法间调用中。通过将connectionHolder放入ThreadLocal,实现了不同方法中使用同一数据库连接,从而支持多种传播方式。
  3. 事务底层,就是通过设置connection.autocommit为false,从而根据方法是否异常,选择commit还是rollback;
  4. 通过Savepoint实现嵌套事务(需要数据库支持)。
  5. 在执行某个方法时,判断当前是否已经存在事务,就是判断当前线程的ThreadLocal中是否存在一个数据库连接对象。
相关推荐
南宫生1 小时前
力扣每日一题【算法学习day.132】
java·学习·算法·leetcode
计算机毕设定制辅导-无忧学长2 小时前
Maven 基础环境搭建与配置(一)
java·maven
bing_1582 小时前
简单工厂模式 (Simple Factory Pattern) 在Spring Boot 中的应用
spring boot·后端·简单工厂模式
逸狼3 小时前
【JavaEE进阶】Spring MVC(3)
spring·java-ee·mvc
天上掉下来个程小白3 小时前
案例-14.文件上传-简介
数据库·spring boot·后端·mybatis·状态模式
风与沙的较量丶3 小时前
Java中的局部变量和成员变量在内存中的位置
java·开发语言
m0_748251723 小时前
SpringBoot3 升级介绍
java
Asthenia04123 小时前
基于Jackson注解的JSON工具封装与Redis集成实战
后端
编程星空4 小时前
css主题色修改后会多出一个css吗?css怎么定义变量?
开发语言·后端·rust
极客先躯4 小时前
说说高级java每日一道面试题-2025年2月13日-数据库篇-请说说 MySQL 数据库的锁 ?
java·数据库·mysql·数据库的锁·模式分·粒度分·属性分