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 小时前
图解 MongoDB 21|选举与 failover:Primary 是怎么选出来的
后端·mongodb·agent
GetcharZp2 小时前
26k Star 开源内网穿透神器 NetBird,一分钟实现全球设备互联!
后端
考虑考虑2 小时前
Mybatis实现批量插入
java·后端·mybatis
咖啡八杯3 小时前
GoF设计模式——中介者模式
java·后端·spring·设计模式
lizhongxuan5 小时前
多Agent之间的区别
后端
青石路7 小时前
记一次多JDK版本问题的排查,一坑套一坑,差点没爬上来
java
杨充7 小时前
1.面向对象设计思想
后端
IT_陈寒7 小时前
Java的Date类又坑了我一次,改用时间戳真香
前端·人工智能·后端
systemPro8 小时前
2.6亿条设备数据,历史查询从超时到50ms,我做了什么
后端
要阿尔卑斯吗8 小时前
提示词优化启示:为什么“按顺序输出“比“关键度评分“更有效
后端