分布式事务 -- seata框架AT模式实现原理

Seata AT 模式

  • 上一节中我们提到AT模式是基于XA事务模型演变过来的,所以他的整体机制也是一个改进版本的两阶段提交协议。
    • 第一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和链接资源
    • 第二阶段:提交异步化,非常快速完成。回滚通过第一阶段的回滚日志进行反向补偿
  • 接下来会解析整个执行流程中每一个阶段的具体实现 原来。同时为了更好的理解AT模式的工作机制,我们以产品表za_product来描述整个流程,表结构如下:
CREATE TABLE `za_product` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`product_code` varchar(100) NOT NULL,`name` varchar(100) NOT NULL,`count` varchar(128) NOT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

在这里插入图片描述

AT模式第一阶段
  • 业务流程中执行库存扣减操作的数据库操作时候,Seata会基于数据源代理对原执行的SQL进行解析,代理的配置代码如下。
@Beanpublic DataSourceProxy dataSourceProxy(DruidDataSource druidDataSource){return new DataSourceProxy(druidDataSource);}
  • 然后将业务数据在更新前后保存到undo_log 日志表中,利用本地事务的ACID特性,把业务数据的更新和回滚日志写入同一个本地事务中进行提交,完整的执行流程如图在这里插入图片描述

  • 日志表结构如下:

-- 注意此处0.7.0+ 增加字段 context
CREATE TABLE `undo_log` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`branch_id` bigint(20) NOT NULL,`xid` varchar(100) NOT NULL,`context` varchar(128) NOT NULL,`rollback_info` longblob NOT NULL,`log_status` int(11) NOT NULL,`log_created` datetime NOT NULL,`log_modified` datetime NOT NULL,PRIMARY KEY (`id`),UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
  • 假设AT分支事务的业务逻辑是如下SQl:
update za_product set count = count-1 where product_code = "GP202103221029"
  • 如上则第一阶段的执行逻辑如下
    • 通过DataSourceProxy对业务SQL进行解析,得到SQl的类型(UPDATE),表(za_product),条件(where product_code = “GP202103221029”)等相关信息
    • 查询修改之前的数据镜像,根据解析得到条件信息生成查询语句,定位数据
select id,product_code,count name from za_product where product_code = "GP202103221029"
  • 如上SQl得到产品的对应库存数据1000
  • 执行业务SQL:更新这条记录的count= count-1
  • 查询修改之后的数据镜像,根据镜像杰哥通过主键定位数据
select id,product_code,count name from za_product where id= 1
  • 得到修改之后的镜像数据,此时count= 999
  • 插入回滚日志:把前,后镜像数据以及业务SQl相关的信息组成一条回滚日志记录,插入undo_log表中,可以在对应数据库的undo_log表中获得数据,如下图:
    在这里插入图片描述
  • rollback_info字段是一个文件类型,包含了回滚的数据beforeImage和afterImage,使用官网数据,如下所示:
{"branchId": 2034562134,"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"
}
  • 提交前,向TC注册分支事务:申请za_product表中主机值等于1 的记录的全局锁
  • 本地事务提交:业务数据的更新和签名步骤中生成的undo_log一起提交
  • 将本地事务提交的结果上报给TC
  • 从AT的第一阶段实现原理看,分支的本地事务可以在第一阶段提交完成后马上释放本地事务所定的资源,这个是AT事务和XA最大的不同点,XA事务的两阶段提交,一般锁定资源后持续到第二阶段的提交或者回滚后才释放资源,所以实际上 AT模式降低了锁定的范围 ,从而大幅度提升了分布式事务处理的效率,之所以这样实现,是因为Seata记录了回滚日志,即使第二阶段发生异常,只需要更具undo_log记录的数据进行回滚即可。
AT模式第二阶段
  • TC接受到所有事务分支的事务状态汇报后,决定对全局事务进行提交或者回滚。
事务提交
  • 如果决定是全局提交,说明此事所有分支事务已经完成了提交,只需要清理undo_log日志即可,这也是和XA最大的不同点,因为在第一阶段各个分支的本地事务就已经提交了,所以这里并不需要TC来触发所有分支事务的提交,如下图:

在这里插入图片描述

  • 如上图中事务提交流程:
    • 分支事务收到TC的提交请求后吧请求放入一个异步队列,并马上返回提交成功的结果给TC
    • 从异步队列中执行分支,提交请求,批量删除对应的undo_log日志。
  • 第一个步骤中TC并不需要同步知道分支事务的处理结果,所以分支事务才会采用异步的方式来执行,因为对于提交操作来说,分支事务只需要清理undo_log中的日志即可,而即使清理失败,也不会对整个分布式事务产生任何影响。
事务回滚
  • 整个全局事务链中,任何一个事务分支执行失败,全局事务都会进入事务回滚流程。此处回滚根据undo_log中记录的镜像数据进行补偿,如,回滚成功,则数据的一致性得到保持。如下流程:

在这里插入图片描述

  • 所有分支事务接受到TC的事务回滚请求后,分支事务参与者开启一个本地事务,执行流程如下:
    • 通过XID和branch ID查找到相应的undo_log记录
    • 数据校验:拿undo_log中afterImage镜像数据与当前业务表中数据镜像比较(乐观锁),如果不同,说明数据被当前全局事务之外的动作修改了,那么事务不会回滚。
    • 如果afterImage中数据和当前业务表中对应的数据相同,则根据undo_log中的beforeImage镜像数据和业务SQL的相关信息生成回滚语句并执行:
update za_product set count = 1000 where id = 1
  • 提交本地事务,并把本地事务的执行结果(即分支事务回滚的结果)提交给TC。

关于事务隔离性保证

  • ACID在事务特种,有一个隔离性,指多个用户并发的访问数据库的时候,数据库为每一个用户开启的事务不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
  • 在AT模式中,当多个全局事务操作同一张表的时候,他的事务隔离的保证是基于全局锁来实现的,那么我们针对读隔离,写隔离进行说明
写隔离
  • 写隔离是为了在多个全局事务针对同一个表的同一个字段进行更新操作的时候,避免全局事务在没有被提交之前被其他全局事务修改。写隔离的主要实现是,在第一阶段本地事务提交之前,确保拿到了全局锁。如果拿不到全局锁,则不能提交本地事务。并且获取全局锁的尝试会有一个范围控制,如果超出范围将会放弃全局锁的获取,并且回滚事务,释放本地锁。

  • 我们用实际案例解析,假设两个全局事务tx1,tx2,分别对za_product表的count字段进行更新操作,count原始值100.

  • tx1先执行,开启本地事务,拿到本地锁(数据库级别的锁),更新count= count-1= 99。本地事务提交之前,需要拿到该记录的全局锁,然后提交本地事务并释放本地锁。

  • tx2后执行,同样先开启本地事务,拿到本地锁,更新count=count-1= 98。本地事务提交之前,尝试获取该记录的全局锁(全局锁由TC控制),由于该全局锁已经被tx1获取,所以tx2需要等待以重新获取全局锁。如果全局事务整体提交,那么提交时序图如下:

在这里插入图片描述

  • 如果tx1在第二阶段执行全局回滚,那么tx1需要重新获得该数据的本地锁,让后根据undo_log进行事务回滚。此时,如果tx2仍然在等待该激励的全局锁,同时持有本地锁,那么tx1 分支事务的回滚会失败。tx1分支事务回滚过程会一直重试,直到tx2的全局锁获取超时,放弃全局锁并回滚本地事务,释放本地锁,之后tx1的分支事务才会回滚成功。而在整个过程中,全局锁在tx1 结束之前一直被tx1 持有,所以不会发生脏写问题。全局事务回滚时序图如下:

在这里插入图片描述

读隔离
  • 我们在数据库学习中指定有4种隔离级别:
    • Read Uncommitted:读取为提交内容
    • Read Committed:读取已经提交内容
    • Repeatable read:可重复读
    • Serializable:可串行化
  • 在数据库本地事务隔离级别为Read Committed或者以上时候,Seata AT事务模式的默认全局隔离级别是Read Uncommitted,在该隔离级别,所有事务都可以看到其他未提交事务的执行结果,产生脏读。这在最终一致性事务模型中是允许存在的,并且在大部分分布式事务场景中可以接受脏读。
  • 在某些特定场景中要求事务隔离级别必须Read Committed,目前Seata是通过SelectForUpdateExecutor执行器对SELECT FOR UPDATE语句进行代理的,SELECT FOR UPDATE 语句在执行时候回申请全局锁。如果全局锁已经被其他分支事务持有,则释放本地锁(回滚SELECT FOR UPDATE 语句的本地执行)并重试。在这个过程中,查询请求会被“BLOCKING”,直到全局锁被拿到,也就是读取的相关数据已经提交时候才返回。

在这里插入图片描述

上一篇:分布式事务框架seata

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

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

相关文章

[Java基础]数据输入

Scanner使用的基本步骤: 1.导包: import java.util.Scanner;2.创建对象: Scanner sc new Scanner(System.in);3.接收数据: int i sc.nextInt();代码如下: import java.util.Scanner;public class OperatorTest {public static void main (String[] args) {//创建对象Scan…

k8s中流量分离以及资源隔离实战

源宝导读:明源云客的终端用户越来越多,也涌现出线上流量活动的场景,大量的访问和接口请求导致服务器出现较高负载。本文将介绍云客团队为了缓解服务器压力,通过K8S进行分流与资源隔离的实践过程。一、背景PaaS和B2C的主要客户云客…

怎样实现WPF Prism Module的国际化和本地化?

English | 简体中文上一篇有简单介绍主工程的国际化,使用的资源字典(XAML)实现的。这几天我添加了几个Prism模块(Module),发现子模块使用资源字典的方式实现国际化和本地化不好做,没有找到比较好的参考文章,所以换了一种方式&…

dotNET Core 3.X 使用 Jwt 实现接口认证

在前后端分离的架构中,前端需要通过 API 接口的方式获取数据,但 API 是无状态的,没有办法知道每次请求的身份,也就没有办法做权限的控制。如果不做控制,API 就对任何人敞开了大门,只要拿到了接口地址就可以…

数据结构与算法--代码鲁棒性案例分析

代码鲁棒性 鲁棒是robust的音译,就是健壮性。指程序能够判断输入是否符合规范,对不合要求的输入能够给出合理的结果。容错性是鲁棒的一个重要体现。不鲁棒的代码发生异常的时候,会出现不可预测的异常,或者程序奔溃。由于鲁棒性非…

【半译】两个gRPC的C#库:grpc-dotnet vs Grpc.Core

grpc-dotnet 是在2019年随着 .NET Core 3.0 一起发布的一个gPRC官方库。在ASP.NET Core 的 gRPC项目模板里面就使用了这个库。.NET Core 3.0之前难道不可以使用gRPC吗?目前,gRPC 在.NET上有两种官方实现:Grpc.Core:这个是原来的gR…

[Java基础]String对象的特点(易错点)

String对象的特点: 1.通过new创建的字符串对象,每一次new都会申请一个内存空间,虽然内容相同,但是地址值不同。 2.以""方式给出的字符串,只要字符串相同(顺序和大小写),无论在程序代码中出现几次&#xff0…

数据结构与算法--解决问题的方法- 二叉树的的镜像

解决问题的思路 工作中遇到的问题可能用到的数据结构由很多,并且各种数据结构都不简单,我们不可能光凭借想象就能得到问题的解法,因此画图是在家具问题过程中用来帮助自己分析,推理的常用手段。很多问题比较抽象,不容…

使用dnSpy调试asp.net core源码

环境:window 10vs2019 16.5.1dnspy v6.1.4.netcore3.1参考:.Net反编译技术详解及4个反编译工具介绍一、关于dnSpydnSpy是近几年的新秀,功能远比ILSpy强大,甩.net Reflector几条街,被汉化、破解、逆向方面的人才奉为神器…

数据结构与算法--解决问题的方法-顺时针打印矩阵

顺时针打印矩阵 题目输入一个矩阵,按照从外向里顺时针的顺序依次打印每一个数字。例如下案例: 如上图矩阵,顺时针打印:1,2,3,4,8,12,16,15,14,13,9,5,6,7,1,10 以上问题看起来比较复杂,但是又没有涉及到复杂的数据结…

.NET与鲲鹏共展翅,昇腾九万里(二)

在上一篇文章 .NET与鲲鹏共展翅,昇腾九万里(一)中,我们通过在鲲鹏架构的Euler系统上跑Docker的方式把dotnet core 跑起来了,有读者反馈说“还是走docker喽,你这个标题应该改成鲲鹏和docker两条鲸鱼的故事”…

[Java基础]final和static修饰符

final: final修饰局部变量时: static: static访问特点:

优化委托的 DynamicInvoke

优化委托的 DynamicInvokeIntro委托方法里有一个 DynamicInvoke 的方法,可以在不清楚委托实际类型的情况下执行委托方法,但是用 DynamicInvoke 去执行的话会比直接用 Invoke 的方法会慢上很多,差了两个数量级,所以在知道委托类型的…

数据结构与算法-- 广度优先打印二叉树

广度优先打印二叉树 题目:从上往下打印出二叉树的每一个节点,同一层节点按照从左到右顺序打印,例如下图中二叉树,依次打印出是8,6,10,5,7,9,11 如上题中二叉树的节点定义我们用之前文章 二叉树实现原理中定义的节点结构。此处提议…

实现一个基于动态代理的 AOP

实现一个基于动态代理的 AOPIntro上次看基于动态代理的 AOP 框架实现,立了一个 Flag, 自己写一个简单的 AOP 实现示例,今天过来填坑了目前的实现是基于 Emit 来做的,后面有时间再写一个基于 Roslyn 来实现的示例效果演示演示代码&…

数据结构与算法-- 二叉树后续遍历序列校验

二叉树后续遍历序列校验 题目:输入一个整数数组,判断改数组是否是某个二叉搜索树的后续遍历结果,如果是返回true否则false,假设输入数组的任意两个数字不相同。 例如输入{5,7,6,9,11,10,8}则返回true,因为这个整数序列…