Spring事务原理详解 三
通过《Spring事务原理 二》一文,我们知道了开启新事务的底层含义,就是从DataSource中获取一个connection,设置autocommit为false,并放到ThreadLocal中。
本文,我们继续来看事务的传播行为在源码中的实现:
- 方法的事务上下文创建和切换;
- 如何提交事务;
- 如何回滚事务。
本文中源码来自Spring 5.3.x分支,github源码地址:GitHub - spring-projects/spring-framework: Spring Framework
一 切换事务上下文
TransactionStatus表示一个逻辑事务,保存着事务上下文信息。有个默认实现DefaultTransactionStatus。
执行每一个带事务的方法时,Spring都会为该方法创建一个TransactionStatus对象。方法间调用时,就涉及ThreadLocal中事务上下文切换。
1.1 两个事物
来看下面示例:保存订单时需要更新库存,两个方法在两个事物中执行。
import com.xiakexing.dao.InventoryDao;
import com.xiakexing.dao.OrderDao;
import com.xiakexing.entity.Order;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Component
public class OrderService {
@Autowired
private OrderService orderService;
private OrderDao orderDao;
private InventoryDao inventoryDao;
@Transactional(propagation = Propagation.REQUIRED)
public void saveOrder(Order order) {
orderDao.save(order);
orderService.updateInventory(order.getCode(), order.getCount());
// 其他逻辑...
}
// 更新库存,假设需要一个新事物
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateInventory(String code, int count) {
inventoryDao.update(code, count);
}
}
- 当执行saveOrder方法时,会创建TransactionStatus A对象,并将连接的ThreadLocal指向connection A。
- 当执行updateInventory方法时,需要开启一个新事物,就必须将saveOrder方法的事物挂起(将TransactionStatus A暂存到TransactionStatus B的suspendedResources属性),将连接ThreadLocal指向connection B。
- 当updateInventory方法执行完毕,继续执行saveOrder方法时,需要将ThreadLocal还原为TransactionStatus A。
1.2 一个事物
如果updateInventory方法传播行为是REQUIRED,那么将和saveOrder在一个事务中执行。updateInventory的TransactionStatus.newTransaction=false。
@Transactional(propagation = Propagation.REQUIRED)
public void saveOrder(Order order) {
orderDao.save(order);
orderService.updateInventory(order.getCode(), order.getCount());
// 其他逻辑...
}
// 更新库存,假设需要一个新事物
@Transactional(propagation = Propagation.REQUIRED)
public void updateInventory(String code, int count) {
inventoryDao.update(code, count);
}
尽管事务上下文会切换,但连接ThreadLocal指向同一个连接。
二 提交事务
2.1 TransactionInfo
该类是对TransactionStatus的一层封装,有以下属性。
代理对象执行业务方法前,会为该方法创建一个TransactionInfo对象。
并将TransactionInfo会设置到线程的ThreadLocal中。
业务方法执行完成后,提交或回滚事务,都要用到TransactionInfo对象。
2.2 提交事务
方法执行正常时将提交事务。会调用AbstractPlatformTransactionManager#commit方法,大体逻辑如下:
- 如果transactionStatus.rollbackOnly=true,则回滚;
- connectionHolder.rollbackOnly为true,则回滚;
- 进入提交流程:
-
- 执行TransactionSynchronization.beforeCommit()回调;
- 执行TransactionSynchronization.beforeCompletion()回调;
- 如果newTransaction为true,即则执行commit;否则不执行提交;
- 执行TransactionSynchronization.afterCommit()回调;
- 执行TransactionSynchronization.afterCompletion()回调;
- 最后,执行cleanupAfterCompletion,恢复事务上下文,可能释放连接
可见,当前方法的transactionStatus的newTransaction为true时,才会执行connection.commit()。
比如,a方法调b方法时,两个方法在一个事务中;b方法正常结束时,不会真正commit;得等a方法执行完毕后,才能最终确定该提交还是该回滚。
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void a() {
// 用代理对象调用
self.b();
}
@Transactional(propagation = Propagation.REQUIRED)
private void b() {
}
2.3 强制回滚
什么是强制回滚呢?当我们捕获了业务方法中异常时,又希望事务回滚时,就要用到rollbackOnly了。
来看一个示例:updateOrder方法捕获了异常,以便返回统一的Result对象,将导致Spring提交事务。
private OrderDao orderDao;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Result updateOrder(Order order) {
orderDao.save(order);
try {
doSomething();
} catch (Exception e) {
return Result.failure();
}
return Result.success();
}
private void doSomething() {
// 业务操作,可能抛异常
}
但从业务上讲,需要回滚事务。此时该怎么办呢?通过编程告诉Spring回滚当前事务。
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Result updateOrder(Order order) {
orderDao.save(order);
try {
doSomething();
} catch (Exception e) {
// 强制回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return Result.failure();
}
return Result.success();
}
三 回滚事务
当业务方法执行异常时,将回滚事务。
首先,判断异常是否满足rollbackOn指定的异常类型:
- 满足时进入回滚;
- 不满足则尝试继续提交;
3.1 检查rollbackOn
DefaultTransactionAttribute中,默认只在RuntimeException或Error类型异常时才回滚。
而使用@Transactional注解时,将调用RuleBasedTransactionAttribute.rollbackOn。
@Transactional(propagation = Propagation.REQUIRED,
rollbackFor = BusinessException.class,
noRollbackFor = UnknownException.class)
3.2 执行回滚
回滚流程大体如下:
- 执行TransactionSynchronization.beforeCompletion()回调;
- 分三种情况执行:
- 如果有Savepoint,则回滚到Savepoint;
- 如果是一个新事务,则connection.rollback();
- 如果不是新事务,设置了强制回滚,或允许局部失败全局回滚,则设置connectionHolder.rollbackOnly = true;
- 执行TransactionSynchronization.afterCompletion()回调;
- 最后,执行cleanupAfterCompletion,恢复事务上下文,可能释放连接。
可见,如果是当前方法开启的事务,异常时才直接回滚;否则,只是设置了rollbackOnly为true。
3.3 局部异常全局回滚
在AbstractPlatformTransactionManager中,globalRollbackOnParticipationFailure属性默认为true。
private boolean globalRollbackOnParticipationFailure = true;
如何理解这个场景呢?如下,a、b方法在同一个事务中。即使a方法中,将b方法在try-catch中执行,但是b方法异常时,这个事务仍将回滚。
@Transactional(propagation = Propagation.REQUIRED)
public void a() {
// 用代理对象调用
try {
self.b();
} catch (Exception e) {
log.error();
}
}
@Transactional(propagation = Propagation.REQUIRED)
private void b() throws Exception {
// 业务逻辑
}
一个事务中,不可能对部分SQL回滚,对部分SQL提交。得符合原子性要求。
因为b方法异常时,它使用一个已有事务,globalRollbackOnParticipationFailure默认为true,将设置connectionHolder.rollbackOnly = true。
当a方法正常结束去提交时,isGlobalRollbackOnly()返回true,从而转向回滚。
注意processRollback方法参数unexpected为true,源码中将抛出如下异常。
老铁,对这个报错是不是很熟悉?
四 总结
- 每个带事务的方法执行时,Spring都会为它创建一个TransactionStatus对象,就是当前方法的事务上下文,保存到ThreadLocal中;
- 当方法间调用时,事务上下文会通过ThreadLocal来完成切换,在方法执行完毕后再恢复原状;
- 提交事务时,可能会因为强制回滚、全局回滚,从而转向执行回滚;
- 只有开启新事务的方法,正常结束时才会真正执行connection.commit;
- 只有开启新事务的方法,执行回滚时才真正执行connection.rollback;使用已有事务的方法,回滚时只是设置rollbackOnly=true;
- TransactionStatus有rollbackOnly属性,ConnectionHolder也有rollbackOnly属性。前者对应某个方法,称为LocalRollbackOnly;而后者在连接层面,称为GlobalRollbackOnly。