当前位置: 首页 > article >正文

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 两个事物

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

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。

	@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方法执行完毕后,才能最终确定该提交还是该回滚。

@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 执行回滚

回滚流程大体如下:

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

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

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

  1. 执行TransactionSynchronization.afterCompletion()回调;
  2. 最后,执行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,源码中将抛出如下异常。

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

四 总结

  1. 每个带事务的方法执行时,Spring都会为它创建一个TransactionStatus对象,就是当前方法的事务上下文,保存到ThreadLocal中;
  2. 当方法间调用时,事务上下文会通过ThreadLocal来完成切换,在方法执行完毕后再恢复原状;
  3. 提交事务时,可能会因为强制回滚、全局回滚,从而转向执行回滚;
  4. 只有开启新事务的方法,正常结束时才会真正执行connection.commit;
  5. 只有开启新事务的方法,执行回滚时才真正执行connection.rollback;使用已有事务的方法,回滚时只是设置rollbackOnly=true;
  6. TransactionStatus有rollbackOnly属性,ConnectionHolder也有rollbackOnly属性。前者对应某个方法,称为LocalRollbackOnly;而后者在连接层面,称为GlobalRollbackOnly。


http://www.kler.cn/a/557231.html

相关文章:

  • Vue 2全屏滚动动画实战:结合fullpage-vue与animate.css打造炫酷H5页面
  • 使用 DistilBERT 进行资源高效的自然语言处理
  • 双非本南邮硕电子信息研一转码:优先掌握哪些编程语言?与学习路径推荐
  • JUC并发—10.锁优化与锁故障
  • Cursor提示词模板,开发GD32,C语言开发GD32 ARM单片机编程规范提示词 大厂风格代码规范
  • 嵌入式领域常用编译器深度解读
  • rkipc main.c 中 rk_param_init函数分析
  • C#-03-类继承
  • 【实战】ChatChat0.3.1+DeepSeek+本地知识库部署使用(上)
  • 10x Research:Secured Finance的稳定币如何推动Filecoin发展
  • 鸿蒙-如何发布一个三方库
  • 契约思维驱动开发:OpenAPI的最佳实践
  • MATLAB进阶之路:数据导入与处理
  • 【c语言】函数_作业详解
  • 一文读懂大模型文件后缀名,解锁 AI 世界的密码
  • 探索Android动态埋点的新视界:UprobeStats深度解析
  • 解决“error: Tried to call obs_frontend_start_virtualcam with no callbacks!”
  • 计算机视觉算法实战——智能零售货架监测(主页有源码)
  • 83_CentOS7通过yum无法安装软件问题解决方案
  • 基于springboot的攀枝花市鲜花销售系统