Spring事务原理详解 三

通过《Spring事务原理 二》一文,我们知道了开启新事务的底层含义,就是从DataSource中获取一个connection,设置autocommit为false,并放到ThreadLocal中。

本文,我们继续来看事务的传播行为在源码中的实现:

  1. 方法的事务上下文创建和切换;
  2. 如何提交事务;
  3. 如何回滚事务。

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

一 切换事务上下文

TransactionStatus表示一个逻辑事务,保存着事务上下文信息。有个默认实现DefaultTransactionStatus。

执行每一个带事务的方法时,Spring都会为该方法创建一个TransactionStatus对象。方法间调用时,就涉及ThreadLocal中事务上下文切换。

1.1 两个事物

来看下面示例:保存订单时需要更新库存,两个方法在两个事物中执行。

java 复制代码
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);
	}
}
  1. 当执行saveOrder方法时,会创建TransactionStatus A对象,并将连接的ThreadLocal指向connection A。

  2. 当执行updateInventory方法时,需要开启一个新事物,就必须将saveOrder方法的事物挂起(将TransactionStatus A暂存到TransactionStatus B的suspendedResources属性),将连接ThreadLocal指向connection B。

  3. 当updateInventory方法执行完毕,继续执行saveOrder方法时,需要将ThreadLocal还原为TransactionStatus A。

1.2 一个事物

如果updateInventory方法传播行为是REQUIRED,那么将和saveOrder在一个事务中执行。updateInventory的TransactionStatus.newTransaction=false。

java 复制代码
	@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方法,大体逻辑如下:

  1. 如果transactionStatus.rollbackOnly=true,则回滚;

  2. connectionHolder.rollbackOnly为true,则回滚;

  3. 进入提交流程:

    • 执行TransactionSynchronization.beforeCommit()回调;

    • 执行TransactionSynchronization.beforeCompletion()回调;

    • 如果newTransaction为true,即则执行commit;否则不执行提交;

    • 执行TransactionSynchronization.afterCommit()回调;

    • 执行TransactionSynchronization.afterCompletion()回调;

    • 最后,执行cleanupAfterCompletion,恢复事务上下文,可能释放连接

可见,当前方法的transactionStatus的newTransaction为true时,才会执行connection.commit()。

比如,a方法调b方法时,两个方法在一个事务中;b方法正常结束时,不会真正commit;得等a方法执行完毕后,才能最终确定该提交还是该回滚。

java 复制代码
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void a() {
    // 用代理对象调用
    self.b();
}

@Transactional(propagation = Propagation.REQUIRED)
private void b() {
}

2.3 强制回滚

什么是强制回滚呢?当我们捕获了业务方法中异常时,又希望事务回滚时,就要用到rollbackOnly了。

来看一个示例:updateOrder方法捕获了异常,以便返回统一的Result对象,将导致Spring提交事务。

java 复制代码
	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回滚当前事务。

java 复制代码
@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。

java 复制代码
@Transactional(propagation = Propagation.REQUIRED, 
               rollbackFor = BusinessException.class,
               noRollbackFor = UnknownException.class)

3.2 执行回滚

回滚流程大体如下:

  1. 执行TransactionSynchronization.beforeCompletion()回调;
  2. 分三种情况执行:
  • 如果有Savepoint,则回滚到Savepoint;

  • 如果是一个新事务,则connection.rollback();

  • 如果不是新事务,设置了强制回滚,或允许局部失败全局回滚,则设置connectionHolder.rollbackOnly = true;

  1. 执行TransactionSynchronization.afterCompletion()回调;
  2. 最后,执行cleanupAfterCompletion,恢复事务上下文,可能释放连接。

可见,如果是当前方法开启的事务,异常时才直接回滚;否则,只是设置了rollbackOnly为true。

3.3 局部异常全局回滚

在AbstractPlatformTransactionManager中,globalRollbackOnParticipationFailure属性默认为true。

java 复制代码
private boolean globalRollbackOnParticipationFailure = true;

如何理解这个场景呢?如下,a、b方法在同一个事务中。即使a方法中,将b方法在try-catch中执行,但是b方法异常时,这个事务仍将回滚。

java 复制代码
@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,源码中将抛出如下异常。

老铁,对这个报错是不是很熟悉?

四 总结

  1. 每个带事务的方法执行时,Spring都会为它创建一个TransactionStatus对象,就是当前方法的事务上下文,保存到ThreadLocal中;
  2. 当方法间调用时,事务上下文会通过ThreadLocal来完成切换,在方法执行完毕后再恢复原状;
  3. 提交事务时,可能会因为强制回滚、全局回滚,从而转向执行回滚;
  4. 只有开启新事务的方法,正常结束时才会真正执行connection.commit;
  5. 只有开启新事务的方法,执行回滚时才真正执行connection.rollback;使用已有事务的方法,回滚时只是设置rollbackOnly=true;
  6. TransactionStatus有rollbackOnly属性,ConnectionHolder也有rollbackOnly属性。前者对应某个方法,称为LocalRollbackOnly;而后者在连接层面,称为GlobalRollbackOnly。
相关推荐
小猫猫猫◍˃ᵕ˂◍13 分钟前
备忘录模式:快速恢复原始数据
android·java·备忘录模式
liuyuzhongcc22 分钟前
List 接口中的 sort 和 forEach 方法
java·数据结构·python·list
小天努力学java23 分钟前
AI赋能传统系统:Spring AI Alibaba如何用大模型重构机票预订系统?
人工智能·spring
五月茶25 分钟前
Spring MVC
java·spring·mvc
sjsjsbbsbsn35 分钟前
Spring Boot定时任务原理
java·spring boot·后端
yqcoder36 分钟前
Express + MongoDB 实现在筛选时间段中用户名的模糊查询
java·前端·javascript
菜鸟蹦迪1 小时前
八股文实战之JUC:ArrayList不安全性
java
2501_903238651 小时前
Spring MVC配置与自定义的深度解析
java·spring·mvc·个人开发
逻各斯1 小时前
redis中的Lua脚本,redis的事务机制
java·redis·lua
计算机毕设指导61 小时前
基于Springboot学生宿舍水电信息管理系统【附源码】
java·spring boot·后端·mysql·spring·tomcat·maven