栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

【SpringCloud】Seata AT模式之分布式事务原理探究

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

【SpringCloud】Seata AT模式之分布式事务原理探究

文章目录
      • 一、前言
      • 二、Seata原理
        • 2.1 Seata中重要角色及工作流程
        • 2.2 两阶段提交
        • 2.3 AT模式
        • 2.4 Seata AT模型实现的2PC与传统2PC的差别:
        • 2.5 读写隔离
      • 三、总结
      • 四、参考

一、前言

微服务下每个模块都可能连接不同数据库,或者一个模块连接不同数据库,一次业务流程往往需要调用多个不同的服务,由此产生分布式事务,分布式事务顾名思义就是要在分布式系统中实现事务,它其实是由多个本地事务组合而成。对于单机(本地)事务,大部分情况下我们使用@Transaction注解或者手动控制事务提交回滚保证事务的ACID,而分布式情况下,一次业务流程包含多个本地事务,每个服务内的数据一致性自然由本地事务保证,但是整个业务逻辑范围就需要分布式事务了。

Seata是一款Alibaba开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。官网地址:Seata,本文主要根据官网内容完善介绍一下Seata的原理,在下篇文章中,再利用SpringCloud进行整合,将Seata注册进Nacos中,并利用Seata实操分布式事务案例。

二、Seata原理

Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。通常默认使用AT模式,Seata中文文档地址:Seata中文文档

首先,如何定义分布式事务?

我们说,分布式事务是由一批分支事务组成的全局事务,通常分支事务只是本地事务。

2.1 Seata中重要角色及工作流程

Seata 框架中的三个角色:

在说三个组件之前先说Seata中的一个重要角色Transaction ID (XID) 全局唯一的事务ID。

  • 事务协调器(Transaction Coordinator,TC):维护全局和分支事务的状态,驱动全局提交或回滚。
  • 事务管理器(Transaction Manager,TM):定义全局事务的范围,负责开始一个全局事务,最终发起全局提交或回滚一个全局事务。
  • 资源管理器(Resource Manager,RM):管理分支事务处理的资源,与TC通信注册分支事务和报告分支事务状态,并驱动分支事务提交或回滚。

Seata托管分布式事务的工作流程:

  • TM 要求 TC 开始新的全局事务。TC 生成代表全局事务的 XID。
  • XID 通过微服务的调用链传播。
  • RM将本地事务作为XID对应的全局事务的一个分支注册到TC。
  • TM 请求 TC 提交或回滚 XID 对应的全局事务。
  • TC 驱动 XID 对应全局事务下的所有分支事务完成分支提交或回滚。

在介绍Seata的AT整体模式机制之前我们先了解一下2PC:

2.2 两阶段提交

2PC(Two-phase commit protocol),叫做两阶段提交。 两阶段提交是一种强一致性设计,2PC 引入一个事务管理者(TM)的角色来协调管理各参与者(RM)(也可称之为各本地资源)的提交和回滚,两阶段分别指的是准备(投票)和提交两个阶段。

具体流程如下:

  • 准备阶段(prepare phase):所有的RM锁住需要的资源,事务管理器给每个参与者发送Prepare消息,在本地执行这个事务(执行sql,写redo/undo log等),但不提交,然后向TM报告已准备就绪。协调者会给各参与者发送准备命令,可以把准备命令理解成除了提交事务之外啥事都做完了。
  • 提交阶段(commit phase):同步等待所有资源的响应之后就进入第二阶段即提交阶段(注意提交阶段不一定是提交事务,也可能是回滚事务)。如果事务管理器收到其中一个参与者的执行失败或者超时消息时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据事务管理器的指令执行提交或者回滚操作,并释放事务处理过程中使用的锁资源。

假如在第一阶段所有参与者都返回准备成功,那么协调者则向所有参与者发送提交事务命令,然后等待所有事务都提交成功之后,返回事务执行成功。假如在第一阶段有一个参与者返回失败,那么协调者就会向所有参与者发送回滚事务的请求,即分布式事务执行失败。

如果第二阶段提交失败呢?

该阶段提交失败分为两类,一种是指定回滚操作失败,另一种是指定提交事务操作失败,下面分别介绍。

第一种是第二阶段执行的是回滚事务操作,那么答案是不断重试,直到所有参与者都回滚了,不然那些在第一阶段准备成功的参与者会一直阻塞着。

第二种是第二阶段执行的是提交事务操作,那么答案也是不断重试,因为有可能一些参与者的事务已经提交成功了,这个时候只有不断的重试,直到提交成功,到最后真的不行只能人工介入处理。

总结一下2PC, 2PC 是一个同步阻塞协议,像第一阶段协调者会等待所有参与者响应才会进行下一步操作,当然第一阶段的协调者有超时机制,假设因为网络原因没有收到某参与者的响应或某参与者挂了,那么超时后就会判断事务失败,向所有参与者发送回滚命令。在第二阶段协调者的没法超时,因为按照我们上面分析只能不断重试!

2PC的传统方案是在数据库层面实现的,保证了分布式事务的强⼀致性,但是性能较差。Oracle、MySQL都支持2PC协议。

2.3 AT模式

Seata中两阶段提交协议的演变:

一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。

二阶段:

  • 提交异步化,非常快速地完成。
  • 回滚通过一阶段的回滚日志进行反向补偿。

可以看到,Seata中两阶段提交协议演变主要表现为第一阶段执行完就会释放本地锁,第二阶段中的提交异步化,以及回滚时候的反向补偿机制。

AT模式实现分布式事务,设计了一个关键表 undo_log (回滚日志记录表),我们在每个应用分布式事务的业务库中创建这张表,这个表的核心作用就是,将业务数据在更新前后的数据镜像组织成回滚日志,备份在 undo_log表中,以便业务异常能随时回滚。

AT模式分布式事务工作过程:


上图大概能理解Seata中分布式事务如何工作的,具体到以用户下订单为例:

订单服务:

1、订单服务的 TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。
2、订单服务的 RM 向 TC 注册分支事务,该分支事务在订单服务执行业务逻辑,并将其纳入 XID 对应全局事务的管辖。
3、订单服务中会把订单业务数据在更新前后的数据镜像组织成回滚日志,将业务数据的更新和回滚日志在同一个本地事务中提交,分别插入到业务表和 undo_log表中。订单服务执行提交分支事务。

库存服务:

  • 当业务逻辑执行到远程调用库存服务时(XID 在微服务调用链路的上下文中传播)。订单服务的 RM 向 TC 注册分支事务,该分支事务执行业务逻辑,并将其纳入 XID 对应全局事务的管辖。
  • 库存服务会把库存数据在更新前后的数据镜像组织成回滚日志,将业务数据的更新和回滚日志在同一个本地事务中提交,分别插入到业务表和 undo_log表中。执行提交分支事务。

最后,订单服务下的各分支事务执行完毕,此时:

  • TM 向 TC 发起针对 XID 的全局提交决议(全局提交)。
  • TC 调度 XID 下管辖的全部分支事务完成提交请求。

如果各分支事务已经在第一阶段提交并成功,这时全局事务协调者(TC) 会向分支发送第二阶段的请求。收到 TC 的分支提交请求,该请求会被放入一个异步任务队列中,并马上返回提交成功结果给 TC。异步队列中会异步和批量地根据分支事务ID 查找并删除相应 undo_log 回滚记录。

假设库存服务执行分支事务失败则会进行库存本地事务回滚,而订单服务收到远程服务调用失败,此时:

  • TM 向 TC 发起针对 XID 的全局回滚决议(全局回滚)。
  • TC 调度 XID 下管辖的全部分支事务完成回滚请求。

RM 服务方收到 TC 全局协调者发来的回滚请求,通过XID和分支事务ID找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚,并删掉undo_log表中记录。

下面具体化AT机制中分支的工作过程:

业务表:product

FieldTypeKey
idbigint(20)PRI
namevarchar(100)
sincevarchar(100)

AT 分支事务的业务逻辑:

update product set name = 'GTS' where name = 'TXC';

一阶段

  • (1)、解析 SQL:得到 SQL 的类型(UPDATE),表(product),条件(where name = ‘TXC’)等相关的信息。
  • (2)、查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据。
select id, name, since from product where name = 'TXC';

得到前镜像:

  • (3)、执行业务 SQL:更新这条记录的 name 为 ‘GTS’。
  • (4)、查询后镜像:根据前镜像的结果,通过 主键 定位数据。
select id, name, since from product where id = 1;

得到后镜像:

  • (5)、插入回滚日志:把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录,插入到 undo_log 表中。
{
	"branchId": 641789253,
	"undoItems": [{
		"afterImage": {
			"rows": [{
				"fields": [{
					"name": "id",
					"type": 4,
					"value": 1
				}, {
					"name": "name",
					"type": 12,
					"value": "GTS"
				}, {
					"name": "since",
					"type": 12,
					"value": "2014"
				}]
			}],
			"tableName": "product"
		},
		"beforeImage": {
			"rows": [{
				"fields": [{
					"name": "id",
					"type": 4,
					"value": 1
				}, {
					"name": "name",
					"type": 12,
					"value": "TXC"
				}, {
					"name": "since",
					"type": 12,
					"value": "2014"
				}]
			}],
			"tableName": "product"
		},
		"sqlType": "UPDATE"
	}],
	"xid": "xid:xxx"
}
  • (6)、提交前,向 TC 注册分支:申请 product 表中,主键值等于 1 的记录的 全局锁 。
  • (7)、本地事务提交:业务数据的更新和前面步骤中生成的 undo_log一并提交。
  • (8)、将本地事务提交的结果上报给 TC。

二阶段回滚

  • (1)、收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作。
  • (2)、通过 XID 和 Branch ID 查找到相应的 undo_log记录。
  • (3)、数据校验:拿 undo_log中的后镜像与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理。
  • (4)、根据undo_log 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句:
update product set name = 'TXC' where id = 1;
  • (5)、提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。

二阶段提交

  • (1)、收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。
  • (2)、异步任务阶段的分支提交请求将异步和批量地删除相应 undo_log记录。

在mysql中的本地回滚日志表通常如下,主要包含分支id和全局的xid:

2.4 Seata AT模型实现的2PC与传统2PC的差别:
  • 架构层次方面:传统 2PC 方案的RM 实际上是在数据库层,RM 本质上就是数据库自身,通过 XA 协议实现,而 Seata 的RM是以 jar 包的形式作为中间件层部署在应用程序这一侧的。

  • 两阶段提交方面:传统 2PC无论第二阶段的决议是 commit 还是 rollback ,事务性资源的锁都要保持到 第二阶段完成才释放。而 Seata 的做法是在第一阶段就将本地事务提交,这样就可以省去第二阶段持锁的时间,整体提高效率。

  • 高可用方面:Seata将分布式事务中的协调者独立部署,可以实现高可用(可以部署多个seata服务)。

  • 写隔离:一阶段本地事务提交前,需要确保先拿到全局锁 。拿不到全局锁 ,不能提交本地事务。
    拿全局锁的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。因为整个过程全局锁在某个全局事务结束前一直是被某个全局事务持有的,所以不会发生脏写的问题。

  • 读隔离:在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) ,如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。

2.5 读写隔离

上面和传统2PC对比时提到了Seata读写隔离问题,下面将对Seata AT模式中的读写隔离分别进行介绍。

写隔离
写隔离主要分为三个步骤实现:

  • 一阶段本地事务提交前,需要确保先拿到全局锁 。
  • 拿不到 全局锁 ,不能提交本地事务。
  • 拿全局锁的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。

例子:以两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。

首先tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的全局锁 ,本地提交释放本地锁。 tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待全局锁 。

tx1 二阶段全局提交,释放全局锁 。tx2 拿到全局锁提交本地事务。

如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。

此时,如果 tx2 仍在等待该数据的全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的 全局锁 等锁超时,放弃 全局锁 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。

因为整个过程全局锁在 tx1 结束前一直是被 tx1 持有的,所以不会发生脏写的问题。

读隔离

在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) 。

如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE语句的代理。

SELECT FOR UPDATE语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回。

出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。

三、总结

本文首先对分布式事务进行简单介绍,然后对Seata AT模式下分布式事务的原理进行探究,当然,主要内容是参看官方文档,然后根据自己理解整理了一下,在对Seata AT模式原理进行介绍时,先介绍了2PC的原理,对比2PC突出Seata AT模式下两阶段提交的改进及优点,总体上来说,AT 模式基于支持本地 ACID 事务的关系型数据库,对Java应用,通过 JDBC 访问数据库为前提,其流程如下:

  • 一阶段 prepare:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
  • 二阶段 commit:马上成功结束,自动 异步批量清理回滚日志。
  • 二阶段 rollback:通过回滚日志,自动 生成补偿操作,完成数据回滚。
四、参考

1、Seata官方文档
2、分布式事务方案(XA 2PC TCC Seata)
3、分布式事务之解决方案(XA和2PC)

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/678051.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号