在上一篇博文《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中。
大体流程为:
- 获取当前方法的@Transaction注解属性,创建TransactionDefinition对象;
- 获取TransactionManager对象;
- 根据方法名生成事务名;
- 如有必要则创建事务,并处理传播行为;
- 在try中执行下一个interceptor或被代理对象中方法;
- 异常时先回滚事务,正常时提交事务;
- 当前方法执行结束,还原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);
}
}
- saveOrder()和updateInventory(),所有sql需要在同一个事务中;
- 单独调用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,逻辑如下:
- 创建DataSourceTransactionObject对象,从ThreadLocal中获取connectionHolder(可能为null);
- 当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,则开启事务
-
流程图如下:
三 总结
- Spring中,对于事务这一抽象概念,从多个方法进行了良好封装,如将隔离级别、超时时间等封装为TransactionDefinition,将事务状态、是否回滚等封装为TransactionStatus。
- 事务的传播行为,发生在方法间调用中。通过将connectionHolder放入ThreadLocal,实现了不同方法中使用同一数据库连接,从而支持多种传播方式。
- 事务底层,就是通过设置connection.autocommit为false,从而根据方法是否异常,选择commit还是rollback;
- 通过Savepoint实现嵌套事务(需要数据库支持)。
- 在执行某个方法时,判断当前是否已经存在事务,就是判断当前线程的ThreadLocal中是否存在一个数据库连接对象。