哦,原来事务传播是这样

news2024/11/24 12:28:14

引言

​ 在介绍正文之前,让我们先一起来看下这段代码:

    @Transactional
    public void createProduct(Long skuId, Integer number, Long operatorUcid) {
        // 插入商品信息
        recordProduct(skuId, number);
        // 插入商品操作记录日志
        recordProductOperateLogClass.recordProductOperateLog(skuId, operatorUcid);
    }

    public void recordProduct(Long skuId, Integer number) {
        // 插入 商品的操作记录日志
        ProductPO productPO = new ProductPO();
        productPO.setSkuId(Math.toIntExact(skuId));
        productPO.setNumber(number);
        productMapper.insert(productPO);
    }

    @Service
    public class RecordProductOperateLogClass{
        @Resource
        ProductOperateLogMapper productOperateLogMapper;
        
        @Transactional
        public void recordProductOperateLog(Long skuId, Long operatorUcid) {
            // 插入 商品的操作记录日志
            ProductOperateLog productOperateLog = new ProductOperateLog();
            productOperateLog.setSkuId(Math.toIntExact(skuId));
            productOperateLog.setOperatorUcid(String.valueOf(operatorUcid));
            productOperateLogMapper.insert(productOperateLog);
			//故意抛错代码
            double b = 1 / 0;
        }
    }

​ 可以看到,这个是一个创建商品的流程时。在createProduct方法中,我们开启了第一个事务A。而在记录商品操作记录recordProductOperateLog时,我们又开启了一个新的事务B。 由此我们发现,我们创建了两个事务,而且外层的事务A包裹着内层的事务B,这就被称为事务嵌套

在这里插入图片描述

​ 那么问题来了,这两个事务相互间的关系是怎样的呢?是共用一个事务?还是两个完全不同的独立的事务呢?这就是我们本期文章要介绍的内容:事务传播机制

事务传播简介

事务传播分类

​ 结合上述的铺垫描述,事务传播机制可以定义如下:在事务嵌套的情况下,事务如何从调用者往被调用者传播的机制。

​ 首先,需要清楚的是,根据场景的不同,所需要使用的事务传播机制往往是不同的。比如对于数据强一致性的情况,我们希望这两个嵌套事务保持一致。但是对一些数据不需要保持强一致性的场景,我们可能希望两个事务间互不干扰。因此,根据使用场景的不同,事务传播被分成了七个不同的使用等级,如下所示:

事务传播级别代码中的传播级别级别描述
REQUIREDPROPAGATION_REQUIREDREQUIRED级别是Spring的默认事务级别,在Spring注释中对于该等级的描述是:会自动加入当前的事务,如果不存在则新建事务。
SUPPORTSPROPAGATION_SUPPORTSSUPPORT级别,跟REQUIRED类似,也是会自动加入当前的事务,但两者区别在于,若当前无事务,则按照无事务的方式执行。
MANDATORYPROPAGATION_MANDATORYMANDATORY级别,要求SQL操作强制在事务内部执行,如果当前不存在事务,则会抛出错误
REQUIRES_NEWPROPAGATION_REQUIRES_NEW该级别同REQUIRES_NEW相似,唯一不同的是,它不会加入已有的事务,而是每次都把事务挂起,新建一个事务,等到事务执行完成了在恢复原有事务的执行。
NOT_SUPPORTEDPROPAGATION_NOT_SUPPORTED该级别默认不支持事务,如果当前调用者已经有事务了,那么此时会将事务挂起,待SQL执行完后,重新返回事务节点。
NEVERPROPAGATION_NEVER该级别标志着SQL语句绝不在事务中操作,若当前在事务中,则直接抛出异常,刚好同MANDATORY级别相对。
NESTEDPROPAGATION_NESTED该级别同REQUIRES_NEW比较相似,都是会新起一个事务。然而不同点在于REQUIRES_NEW每次都创建新的独立的物理事务,而Nested只有一个物理事务;Nested嵌套事务回滚或提交不会导致外部事务回滚或提交,但外部事务回滚将导致嵌套事务回滚,而RequiresNew由于都是全新的事务,所以之间是无关联的。

​ 上述介绍的事务传播机制还是很容易看懂各自的作用的,唯一需要注意的是NESTED机制,其跟REQUIRES_NEW机制很像。在没有事务的时候,两者都会新创建一个事务。但是如果在存在事务的时候,两者就出现了差异:

在这里插入图片描述

​ 很容易看到,REQUIRES_NEW始终会新建事务,但是NESTED则可能通过在一个事务内新增加保存点的方式。因此也可能导致外层的保存点1出现问题的时候,会让所有的事务内容都进行回滚。

​ 总结下来,在存在不同的场景下,对应使用的事务传播类型各有偏差。具体使用的事务传播级别,需要根据各自使用场景方可决定。

事务传播实现原理

​ 要实现事务传播,很自然应该分为两步:

1、设置事务传播等级;

2、根据事务传播等级判断选择的事务。

​ 简单介绍一下上述两步的含义,由于事务传播机制是由用户自主选择的、根据场景区分的,因此自然需要提供给用户选择的余地。在用户选择的事务传播等级后,只需要根据事务传播等级选择不同的操作即可。下面我们就围绕这两步来介绍事务传播的实现原理。

设置事务传播等级

声明式事务

​ 对于声明式事务来说,设置事务传播等级的方式大家应该都不陌生。通过设置Transactional中的propagation变量就可以使得代码生效。

	@Transactional(propagation = Propagation.NOT_SUPPORTED)

​ 然而问题来了,在注解中设置的这个变量,是怎么被Spring识别到的呢?这就不得不提到声明式事务一个非常重要的类TransactionInterceptor了。

(不了解这个类的同学可以参考下我之前写的文章《浅析Spring事务实现原理》)

在这里插入图片描述

​ TransactionInterceptor有一个非常重要的点,就是继承了类TransactionAspectSupport。事务执行时会调用到TransactionInterceptor中的invoke方法,会紧接着调用父类TransactionAspectSupport中的invokeWithinTransaction方法,在该方法里有一个十分关键的点,就是会去查询当前的事务属性信息:

在这里插入图片描述

​ 紧跟着查询查找属性的代码内容,这里要追的链路很长,限于篇幅原因,我就把一些关键的节点标注成链路了,有兴趣的同学可以逐一展开查看。

在这里插入图片描述

​ 最终的话,咱们可以找到关键的类SpringTransactionAnnotationParser,其下有一个方法parseTransactionAnnotation,其具体代码如下所示:

protected TransactionAttribute parseTransactionAnnotation(AnnotationAttributes attributes) {
   RuleBasedTransactionAttribute rbta = new RuleBasedTransactionAttribute();

   Propagation propagation = attributes.getEnum("propagation");
   rbta.setPropagationBehavior(propagation.value());
   Isolation isolation = attributes.getEnum("isolation");
   rbta.setIsolationLevel(isolation.value());

   rbta.setTimeout(attributes.getNumber("timeout").intValue());
   String timeoutString = attributes.getString("timeoutString");
   Assert.isTrue(!StringUtils.hasText(timeoutString) || rbta.getTimeout() < 0,
         "Specify 'timeout' or 'timeoutString', not both");
   rbta.setTimeoutString(timeoutString);

   rbta.setReadOnly(attributes.getBoolean("readOnly"));
   rbta.setQualifier(attributes.getString("value"));
   rbta.setLabels(Arrays.asList(attributes.getStringArray("label")));

   List<RollbackRuleAttribute> rollbackRules = new ArrayList<>();
   for (Class<?> rbRule : attributes.getClassArray("rollbackFor")) {
      rollbackRules.add(new RollbackRuleAttribute(rbRule));
   }
   for (String rbRule : attributes.getStringArray("rollbackForClassName")) {
      rollbackRules.add(new RollbackRuleAttribute(rbRule));
   }
   for (Class<?> rbRule : attributes.getClassArray("noRollbackFor")) {
      rollbackRules.add(new NoRollbackRuleAttribute(rbRule));
   }
   for (String rbRule : attributes.getStringArray("noRollbackForClassName")) {
      rollbackRules.add(new NoRollbackRuleAttribute(rbRule));
   }
   rbta.setRollbackRules(rollbackRules);

   return rbta;
}

​ 很清晰可以看出来,就是在这里将我们所设置的每一个变量给获取出来的。咱们设置的propagation、value、rollbackFor,都是通过SpringTransactionAnnotationParser解析出来并保存下来的。

编程式事务

​ 在事务传播机制的设计上,编程事务则相对简单。通过setPropagationBehavior()方法,就可以设置事务传播的级别。

	transactionTemplate.setPropagationBehavior(PROPAGATION_REQUIRED);

​ 在后续也是直接通过getPropagationBehavior方法直接获取到对应的事务传播级别。

    default int getPropagationBehavior() {
       return PROPAGATION_REQUIRED;
    }

根据事务传播等级判断

​ 在实现了事务等级的设置之后,我们只需要根据设置好的事务等级选择事务传播方案即可,如:是否加入当前的事务?是否要挂起当前的事务?

​ 对于编程式和声明式事务来说,两者关于事务传播机制的实现,其实最终都统一由AbstractPlatformTransactionManager下的方法getTransaction上。其具体源代码如下所示:

@Override
public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
    throws TransactionException {
    ......
        Object transaction = doGetTransaction();
    //判断当前是否有事务
    if (isExistingTransaction(transaction)) {
        // 如果存在事务,则走不一样的判断逻辑
        return handleExistingTransaction(def, transaction, debugEnabled);
    }
    ......
        // 如果当前没有事务 and 传播级别是PROPAGATION_MANDATORY 则会直接报错
        if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
            throw new IllegalTransactionStateException(
                "No existing transaction found for transaction marked with propagation 'mandatory'");
        }else if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
                  def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
                  def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
            //判断当前如果是 REQUIRD/REQUIRD_NEW/NESTED 则走该逻辑新建事务
            SuspendedResourcesHolder suspendedResources = suspend(null);
            try {
                //开启新事务执行
                return startTransaction(def, transaction, debugEnabled, suspendedResources);
            }catch (RuntimeException | Error ex) {
                resume(null, suspendedResources);
                throw ex;
            }
        }else {
            // 剩余的 NEVER/NOT_SUPPORTED/SUPPORTS则走下面的逻辑,此时只开启empty事务(非真的事务),只是为了保持同步。
            ......
                boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
            return prepareTransactionStatus(def, null, true, newSynchronization, debugEnabled, null);

        }
}

​ 从getTransaction的源码中不难看到,在存在事务的时候其实是直接执行了handleExistingTransaction的方法。这里我们再引入方法handleExistingTransaction的源码:

	private TransactionStatus handleExistingTransaction(TransactionDefinition definition, 
                                  Object transaction, boolean debugEnabled) throws TransactionException {
		// 如果是NEVER,此时存在事务,直接报错!
		if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) {
			throw new IllegalTransactionStateException(
					"Existing transaction found for transaction marked with propagation 'never'");
		}
        // 如果是NOT_SUPPORTED,那么此时会挂起原事务,并保存挂起的holder。同时开启empty事务,保持同步
		if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) {
            ......
			Object suspendedResources = suspend(transaction);
			boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
			return prepareTransactionStatus(
					definition, null, false, newSynchronization, debugEnabled, suspendedResources);
		}
		// 如果是REQUIRES_NEW,那么此时悬挂旧事务,同时开启新事务
		if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) {
            ......
			SuspendedResourcesHolder suspendedResources = suspend(transaction);
			try {
                //依旧开启新事务
				return startTransaction(definition, transaction, debugEnabled, suspendedResources);
			}catch (RuntimeException | Error beginEx) {
				resumeAfterBeginException(transaction, suspendedResources, beginEx);
				throw beginEx;
			}
		}
		// 如果此时是 NESTED,
		if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
            // 如果不允许开启Nested事务,则报错。
			if (!isNestedTransactionAllowed()) {
				throw new NestedTransactionNotSupportedException(
						"Transaction manager does not allow nested transactions by default - " +
						"specify 'nestedTransactionAllowed' property with value 'true'");
			}
            ......
			if (useSavepointForNestedTransaction()) {
                //使用保存点 
				DefaultTransactionStatus status =
						prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null);
				status.createAndHoldSavepoint();
				return status;
			}else {
                // 否则开启事务
				return startTransaction(definition, transaction, debugEnabled, null);
			}
		}
		// 剩余的只有 SUPPORTS/REQUIRED/MANDATORY 传播级别 - 此时直接加入已有的事务执行即可
        ......
		return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null);
	}

​ 上述的代码片段比较长,我将上述代码的相应的执行流程整理成了如下流程图:

在这里插入图片描述

​ 从流程图中其实可以比较清晰的看到整个事务传播机制是如何判断出来的,以及是如何作用的。可以看到,主干主要分成了:当前存在事务和不存在事务两个分支。

不存在事务:

​ 对于不存在事务的情况,首先判断是否为MANDATORY级别,如果是,则直接报错。这就确保了MANDATORY一定是需要在有事务的时候执行的。

​ 紧接着判断是否为REQUIRED/REQUIRES_NEW/NESTED的级别,如果是这几个级别,则需要开启新事务执行。

​ 如果是NEVER/NOT_SUPPORTED/SUPPORTS三个级别,那么此时选择按照非事务的方式执行。

存在事务:

​ 存在事务的情况相较于不存在事务的情况相对更复杂一些,首先需要判断是否为NEVER级别,若是则直接报错。确保NEVER情况下不用事务进行。

​ 紧接着判断是否为NOT_SUPPORT级别,如果是那么此时要悬挂当前的事务,并按照非事务的方式执行。

​ 若上述两种都不满足,则再判断是否为REQUIRES_NEW级别,如果是,那么也是需要悬挂原有的事务,并新建事务执行的。

​ 然后如果上述依旧不满足,则再判断是否为NESTED级别,如果不是则直接按照当前事务执行(SUPPORT/REQUIRED/MANDATORY级别)的要求。

​ 如果是NESTED级别的话,还需要简单判断下是否支持保存点,若支持,则采用保存点保存,否则新建事务保存。

总结

​ 在本篇文章中,将事务传播机制的设计背景、等级分类及实现原理一一拆解开来。由此我们就可以在日后的实现过程中,能够更符合场景地选择我们所需的事务传播级别。

参考文献

事务的传播机制

Spring事务传播机制

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/114815.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

关于操作数组元素的实际应用

sort()升序、降序排序方法应用 sort()排序方式原理&#xff1a;当sort()传入函数中的第一个参数a位于第二个参数b之前&#xff0c;则返回一个负数&#xff0c;相等则返回0&#xff0c;a位于b之后则返回正数。 比如&#xff0c;当要做升序排序时&#xff0c;我们需要想到前面的…

【SpringMVC】常用注解

1.RequestParam 1.1 使用说明 作用: 把请求中指定名称的参数给控制器中的形参赋值 属性&#xff1a; ​ **value&#xff1a;**请求参数中的名称 ​ **required&#xff1a;**请求参数是否必须提供此参数。默认值&#xff1a;true&#xff0c;表示必须提供&#xff0c;如果…

Linux下tree命令C/C++实现(以树状格式列出目录的内容)

在UNIX/LINUX系统中&#xff0c;tree是一个递归目录列表程序&#xff0c;它生成文件的深度缩进列表。在没有参数的情况下&#xff0c;树将列出当前目录中的文件。当给定目录参数时&#xff0c;树依次列出在给定目录中找到的所有文件或目录。列出找到的所有文件和目录后&#xf…

机器学习从零到入门 逻辑回归详解

逻辑回归详解 从零开始 从理论到实践一、逻辑回归的理解1.1、字面含义1.2、引申1.2.1、阶跃函数的引入1.2.2、可导的阶跃函数 - Logistic函数1.2.3、Logistic回归1.2.4、回归系数的求解 - 极大似然估计二、sklearn的使用参考一、逻辑回归的理解 前面介绍了线性回归及其衍生回归…

目标检测之YOLOv4算法分析

基本原理 网络结构 CSPDarknet53 最后三个箭头指向输出即三种特征图 SPP 解决多尺度问题 对于同一个特征输出图&#xff0c;进行三种maxpool2d操作&#xff0c;然后将三种操作的输出进行叠加 PANet 融合上采样、下采样等特征&#xff0c;深度方向拼接 PANet由五个核心模…

Unity 3D工具栏与常用工具||Unity 3D 菜单栏与快捷键

Unity 3D 的工具栏&#xff08; Toolbar &#xff09;中&#xff0c;一共包含 13 种常用工具&#xff0c;如下所列。 一. 平移窗口工具&#xff1a;平移场景视图画面。 快捷键&#xff1a;鼠标中键 二. 位移工具&#xff1a;针对单个或两个轴向做位移。 快捷键&#xff1a;W…

stm32f407VET6 系统学习 day04 DHT11 温湿度传感器

1.温湿度的一次完整的数据包括&#xff1a; 一次完整的数据传输为40bit,高位先出。 数据格式: 8bit湿度整数数据 8bit湿度小数数据 8bi温度整数数据 8bit温度小数数据 8bit校验&#xff08;校验和的值是前四个字节数据的和) 用户MCU发送一次开始信号后,DHT11从低功耗模式转…

灰色关联分析(系统分析+综合评价)

系统分析&#xff1a;探究系统中哪个自变量对系统的影响最大 灰色关联分析的基本思想是根据序列曲线几何形状的相似程度来判断其联系是否紧密。曲线越接近&#xff0c;相应序列之间的关联度就越大&#xff0c;反之就越小 应用一、进行系统分析&#xff08;国内比赛合适&#xf…

idea创建spring项目

文章目录一、Maven 项目的创建1.1 创建一个空项目2.2 新建一个模块2.3 创建Maven模块2.4 添加resources目录2.5 选定maven版本二、添加spring2.1 添加依赖2.2 创建applicationContext文件2.3 添加bean一、Maven 项目的创建 1.1 创建一个空项目 新建一个空项目&#xff1b;随便…

十、Express 路由

路由是Express框架中最重要的功能之一&#xff0c;在Express中&#xff0c;路由指的是客户端的请求与服务器处理函数之间的映射关系&#xff0c;Express中的路由分别由请求的类型&#xff08;GET/POST等&#xff09;、请求的URL地址和处理函数三个部分组成的&#xff1b; APP级…

【C++】侯捷C++面向对象高级编程(下)

转换函数(conversion function) 可以把"这种"东西&#xff0c;转化为"别种"东西。 即Fraction ——> double class Fraction { public:Fraction(int num, int den 1) :m_numerator(num), m_denominator(den) {}operator double()const {return ((dou…

hashmap哈希map是什么?什么时候需要使用hashmap?C实现hashmap示例

背景 对于C程序员&#xff0c;尤其是嵌入式C程序员&#xff0c;hashmap使用的相对较少&#xff0c;所以会略显陌生&#xff0c;hashmap其实涉及到2个概念&#xff0c;分别是哈希(hash)、map。 哈希hash&#xff1a;是把任意长度输入通过蓝列算法变换成固定长度的输出&#xff…

CSS Flex 布局的 flex-direction 属性讲解

flex-direction 设置了主轴&#xff0c;从而定义了弹性项目放置在弹性容器中的方向。 Flexbox 是一种单向布局概念&#xff0c;可将弹性项目视为主要以水平行或垂直列布局。 .container {flex-direction: row | row-reverse | column | column-reverse; }几种支持的属性&#x…

devServer和VueCli | Webpack

文章目录devServer和VueCliwebpack-dev-servercontentBase模块热替换开启HMRhotOnly host配置port open compressproxyresolveextensions和alias配置如何区分开发环境devServer和VueCli webpack-dev-server contentBase 模块热替换 开启HMR hotOnly host配置 port open compres…

IMX6ULL学习笔记(15)——GPIO接口使用【官方SDK方式】

一、GPIO简介 i.MX6ULL 芯片的 GPIO 被分成 5 组,并且每组 GPIO 的数量不尽相同&#xff0c;例如 GPIO1 拥有 32 个引脚&#xff0c; GPIO2 拥有 22 个引脚&#xff0c; 其他 GPIO 分组的数量以及每个 GPIO 的功能请参考 《i.MX 6UltraLite Applications Processor Reference M…

有没有股票实时行情的同花顺l2接口?

对于个人投资者而言&#xff0c;一般看盘平台软件系统中&#xff0c;通过自定义公式接口&#xff0c;可以获取到股票实时行情的。例如同花顺l2接口系统会通过一些输入的公式就能查找指定的股票行情了&#xff0c;那么这个就相当于股票实时行情的API接口一样的道理&#xff0c;输…

加密解决HTTP协议带来的安全问题

HTTP协议默认是采取明文传输的&#xff0c;容易被中间人窃听、拦截、篡改&#xff0c;存在安全隐患。 常见提高安全性的方法是对通信内容进行加密&#xff0c;再进行传输&#xff0c;常见的加密方式有 不可逆加密&#xff1a;单向散列函数 可逆加密&#xff1a;对称加密、非对称…

从模型到服务——iDesktopX处理自动化工具实现BIM模型到三维服务发布

目录前言一、 处理自动化模型二、 算子参数设置1、 使用迭代数据集打开导出后的BIM模型2、 移除重复点、重复面和重复子对象3、 模型生成缓存4、 三维切片缓存发布5、 执行结果前言 BIM模型在SuperMap实际使用的业务流程中常常需要在桌面产品中生成缓存&#xff0c;然后通过iS…

安装和配置MySQL

首先前往官网下载mysql社区版&#xff08;不要钱&#xff09; MySQL Community Serverhttps://dev.mysql.com/downloads/windows/installer/8.0.html 甲骨文比较鸡贼&#xff0c;会要求你注册一个账号。但是下面有一行小字&#xff0c;直接点击下载就好了 双击后直接按…

Blender——苹果的纹理绘制

效果图 前言 在进行纹理绘制之前&#xff0c;首先要具有苹果三维模型。 关于苹果的建模请参考&#xff1a;Blender——“苹果”建模_行秋的博客 1.苹果UV的展开 1.1首先点击UV Eidting&#xff0c;滑动三维模型&#xff0c;使其大小适中。 1.2打开左上角的UV选区同步&#x…