分布式事务之TCC与SAGA

news2025/1/17 4:10:24

分布式事务之TCC与SAGA

在《关于分布式事务的理解》,介绍了可靠消息队列的实现原理,虽然它也能保证最终的结果是相对可靠的,过程也足够简单(相对于 TCC 来说),但现在你已经知道,可靠消息队列的整个实现过程完全没有任何隔离性可言。虽然在有些业务中,有没有隔离性不是很重要,比如说搜索系统。但在有些业务中,一旦缺乏了隔离性,就会带来许多麻烦。Fenix’s Bookstore 在线书店的场景事例中,如果缺乏了隔离性,就会带来一个显而易见的问题:超售。

事例场景:Fenix’s Bookstore 是一个在线书店。一份商品成功售出,需要确保以下三件事情被正确地处理:

1用户的账号扣减相应的商品款项;

2商品仓库中扣减库存,将商品标识为待配送状态;

3商家的账号增加相应的商品款项。

也就是说,在书店的业务场景下,很有可能会出现这样的情况:两个客户在短时间内都成功购买了同一件商品,而且他们各自购买的数量都不超过目前的库存,但他们购买的数量之和,却超过了库存。

如果这件事情是发生在刚性事务且隔离级别足够的情况下,其实是可以完全避免的。比如,我前面提到的“超售”场景,就需要“可重复读”(Repeatable Read)的隔离级别,以保证后面提交的事务会因为无法获得锁而导致失败。但用可靠消息队列就无法保证这一点了。之前《关于本地事务的理解》中已经给你介绍过数据库本地事务的相关知识,你可以再去回顾复习下。

TCC 事务

所以,如果业务需要隔离,我们通常就应该重点考虑 TCC 方案,它天生适合用于需要强隔离性的分布式事务中。在具体实现上,TCC 的操作其实有点儿麻烦和复杂,它是一种业务侵入性较强的事务方案,要求业务处理过程必须拆分为“预留业务资源”和“确认 / 释放消费资源”两个子过程。

另外,你看名字也能看出来,TCC 的实现过程分为了三个阶段:

Try:尝试执行阶段,完成所有业务可执行性的检查(保障一致性),并且预留好事务需要用到的所有业务资源(保障隔离性)。

Confirm:确认执行阶段,不进行任何业务检查,直接使用 Try 阶段准备的资源来完成业务处理。注意,Confirm 阶段可能会重复执行,因此需要满足幂等性。

Cancel:取消执行阶段,释放 Try 阶段预留的业务资源。注意,Cancel 阶段也可能会重复执行,因此也需要满足幂等性。

那么,根据 Fenix’s Bookstore 在线书店的场景事例,TCC 的执行过程应该是这样的:
在这里插入图片描述

第一步,最终用户向 Fenix’s Bookstore 发送交易请求:购买一本价值 100 元的《深入理解 Java 虚拟机》。

第二步,创建事务,生成事务 ID,记录在活动日志中,进入 Try 阶段:用户服务:检查业务可行性,可行的话,把该用户的 100 元设置为“冻结”状态,通知下一步进入 Confirm 阶段;

不可行的话,通知下一步进入 Cancel 阶段。

仓库服务:检查业务可行性,可行的话,将该仓库的 1 本《深入理解 Java 虚拟机》设置为“冻结”状态,通知下一步进入 Confirm 阶段;不可行的话,通知下一步进入 Cancel 阶段。商家服务:检查业务可行性,不需要冻结资源。

第三步,如果第二步中所有业务都反馈业务可行,就将活动日志中的状态记录为 Confirm,进入 Confirm 阶段:用户服务:完成业务操作(扣减被冻结的 100 元)。仓库服务:完成业务操作(标记那 1 本冻结的书为出库状态,扣减相应库存)。商家服务:完成业务操作(收款 100 元)。

第四步,如果第三步的操作全部完成了,事务就会宣告正常结束。而如果第三步中的任何一方出现了异常,不论是业务异常还是网络异常,都将会根据活动日志中的记录,来重复执行该服务的 Confirm 操作,即进行“最大努力交付”。

第五步,如果是在第二步,有任意一方反馈业务不可行,或是任意一方出现了超时,就将活动日志的状态记录为 Cancel,进入 Cancel 阶段:

  • 用户服务:取消业务操作(释放被冻结的 100 元)。

    仓库服务:取消业务操作(释放被冻结的 1 本书)。

    商家服务:取消业务操作(大哭一场后安慰商家谋生不易)

另外,TCC 在业务执行的时候,只操作预留资源,几乎不会涉及到锁和资源的争用,所以它具有很高的性能潜力。但是,由于 TCC 的业务侵入性比较高,需要开发编码配合,在一定程度上增加了不少工作量,也就给我们带来了一些使用上的弊端,那就是我们需要投入更高的开发成本和更换事务实现方案的替换成本。所以,通常我们并不会完全靠裸编码来实现 TCC,而是会基于某些分布式事务中间件(如阿里开源的Seata)来完成,以尽量减轻一些编码工作量。

好,现在你就已经知道了,TCC 事务具有较强的隔离性,能够有效避免“超售”的问题,而且它的性能可以说是包括可靠消息队列在内的几种柔性事务模式中最高的。但是,TCC 仍然不能满足所有的业务场景。

我在前面也提到了,TCC 最主要的限制是它的业务侵入性很强,但并不是指由此给开发编码带来的工作量,而是指它所要求的技术可控性上的约束。

比如说,我们把这个书店的场景事例修改一下:由于中国网络支付日益盛行,在书店系统中,现在用户和商家可以选择不再开设充值账号,至少不会强求一定要先从银行充值到系统中才能进行消费,而是允许在购物时,直接通过 U 盾或扫码支付,在银行账户中划转货款。

这个需求完全符合我们现在支付的习惯,但这也给系统的事务设计增加了额外的限制:如果用户、商家的账户余额由银行管理的话,其操作权限和数据结构就不可能再随心所欲地自行定义了,通常也就无法完成冻结款项、解冻、扣减这样的操作,因为银行一般不会配合你的操作。所以,在 TCC 的执行过程中,第一步 Try 阶段往往就已经无法施行了。那么,我们就只能考虑采用另外一种柔性事务方案:SAGA 事务。

SAGA 事务

原本提出 SAGA 的目的,是为了避免大事务长时间锁定数据库的资源,后来才逐渐发展成将一个分布式环境中的大事务,分解为一系列本地事务的设计模式。

SAGA 由两部分操作组成。

一部分是把大事务拆分成若干个小事务,将整个分布式事务 T 分解为 n 个子事务,我们命名为 T1,T2,…,Ti,…,Tn。每个子事务都应该、或者能被看作是原子行为。如果分布式事务 T 能够正常提交,那么它对数据的影响(最终一致性)就应该与连续按顺序成功提交子事务 Ti 等价。

另一部分是为每一个子事务设计对应的补偿动作,我们命名为 C1,C2,…,Ci,…,Cn。Ti 与 Ci 必须满足以下条件:

  • Ti 与 Ci 都具备幂等性;

    Ti 与 Ci 满足交换律(Commutative),即不管是先执行 Ti 还是先执行 Ci,效果都是一样的;

    Ci 必须能成功提交,即不考虑 Ci 本身提交失败被回滚的情况,如果出现就必须持续重试直至成功,或者要人工介入。

如果 T1 到 Tn 均成功提交,那么事务就可以顺利完成。否则,我们就要采取以下两种恢复策略之一:

  • 正向恢复(Forward Recovery):如果 Ti 事务提交失败,则一直对 Ti 进行重试,直至成功为止(最大努力交付)。这种恢复方式不需要补偿,适用于事务最终都要成功的场景,比如在别人的银行账号中扣了款,就一定要给别人发货。正向恢复的执行模式为:T1,T2,…,Ti(失败),Ti(重试)…,Ti+1,…,Tn。

    反向恢复(Backward Recovery):如果 Ti 事务提交失败,则一直执行 Ci 对 Ti 进行补偿,直至成功为止(最大努力交付)。这里要求 Ci 必须(在持续重试后)执行成功。反向恢复的执行模式为:T1,T2,…,Ti(失败),Ci(补偿),…,C2,C1。

    所以你能发现,与 TCC 相比,SAGA 不需要为资源设计冻结状态和撤销冻结的操作,补偿操作往往要比冻结操作容易实现得多。

    我给你举个例子。我在前面提到的账户余额直接在银行维护的场景,从银行划转货款到 Fenix’s Bookstore 系统中,这步是经由用户支付操作(扫码或 U 盾)来促使银行提供服务;如果后续业务操作失败,尽管我们无法要求银行撤销掉之前的用户转账操作,但是作为补偿措施,我们让 Fenix’s Bookstore 系统将货款转回到用户账上,却是完全可行的。SAGA 必须保证所有子事务都能够提交或者补偿,但 SAGA 系统本身也有可能会崩溃,所以它必须设计成与数据库类似的日志机制(被称为 SAGA Log),以保证系统恢复后可以追踪到子事务的执行情况,比如执行都到哪一步或者补偿到哪一步了。

    另外你还要注意,尽管补偿操作通常比冻结 / 撤销更容易实现,但要保证正向、反向恢复过程能严谨地进行,也需要你花费不少的工夫。比如,你可能需要通过服务编排、可靠事件队列等方式来完成。所以,SAGA 事务通常也不会直接靠裸编码来实现,一般也是在事务中间件的基础上完成。我前面提到的 Seata 就同样支持 SAGA 事务模式。

    另一种应用模式:AT 事务

    从整体上看,AT 事务是参照了 XA 两段提交协议来实现的,但针对 XA 2PC 的缺陷,即在准备阶段,必须等待所有数据源都返回成功后,协调者才能统一发出 Commit 命令而导致的木桶效应(所有涉及到的锁和资源,都需要等到最慢的事务完成后才能统一释放),AT 事务也设计了针对性的解决方案。

    它大致的做法是在业务数据提交时,自动拦截所有 SQL,分别保存 SQL 对数据修改前后结果的快照,生成行锁,通过本地事务一起提交到操作的数据源中,这就相当于自动记录了重做和回滚日志。

    如果分布式事务成功提交了,那么我们后续只需清理每个数据源中对应的日志数据即可;而如果分布式事务需要回滚,就要根据日志数据自动产生用于补偿的“逆向 SQL”。所以,基于这种补偿方式,分布式事务中所涉及的每一个数据源都可以单独提交,然后立刻释放锁和资源。

    AT 事务这种异步提交的模式,相比 2PC 极大地提升了系统的吞吐量水平。而使用的代价就是大幅度地牺牲了隔离性,甚至直接影响到了原子性。因为在缺乏隔离性的前提下,以补偿代替回滚不一定总能成功。

    比如,当在本地事务提交之后、分布式事务完成之前,该数据被补偿之前又被其他操作修改过,即出现了脏写(Dirty Wirte),而这个时候一旦出现分布式事务需要回滚,就不可能再通过自动的逆向 SQL 来实现补偿,只能由人工介入处理了。

    一般来说,对于脏写我们是一定要避免的,所有传统关系数据库在最低的隔离级别上,都仍然要加锁以避免脏写。因为脏写情况一旦发生,人工其实也很难进行有效处理。

    所以,GTS 增加了一个“全局锁”(Global Lock)的机制来实现写隔离,要求本地事务提交之前,一定要先拿到针对修改记录的全局锁后才允许提交,而在没有获得全局锁之前就必须一直等待。

    这种设计以牺牲一定性能为代价,避免了在两个分布式事务中,数据被同一个本地事务改写的情况,从而避免了脏写。

    另外,在读隔离方面,AT 事务默认的隔离级别是读未提交(Read Uncommitted),这意味着可能会产生脏读(Dirty Read)。读隔离也可以采用全局锁的方案来解决,但直接阻塞读取的话,我们要付出的代价就非常大了,一般并不会这样做。

    所以到这里,你其实能发现,分布式事务中并没有能一揽子包治百病的解决办法,你只有因地制宜地选用合适的事务处理方案,才是唯一有效的做法。

    小结

    通过上一讲和今天这节课的学习,我们已经知道,CAP 定理决定了 C 与 A 不可兼得,传统的 ACID 强一致性在分布式环境中,要想能保证一致性(C),就不得不牺牲可用性(A)。那么这个时候,随着分布式系统中节点数量的增加,整个系统发生服务中断的概率和时间都会随之增长。所以,我们只能退而求其次,把“最终一致性”作为分布式架构下事务处理的目标。在这两节课中,介绍的可靠事件队列、TCC 和 SAGA,都是实现最终一致性的三种主流模式。

补充:幂等接口

幂等接口其实就是为了最终一致性设计出来的。

就是避免反复提交导致数据不一致问题。

UUID就是为了rpc设计出来

了解了技术出现的背景,就能把层出不穷的技术名词穿起来。死记硬背只能是事倍功半。

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

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

相关文章

从零起步认识XAML

新建WPF项目 在Visual Studio 2022中,选择"创建新项目" 选择“WPF Application” 点击下一步 点击创建。 执行DebugStart Debugging菜单命令,或者快捷键为F5,或者工具栏上的图标。 在Solution Explorer窗口(ViewSoluti…

smardaten应用商超 | 邀您沉浸式体验企业级无代码开发

宝子们,还在发愁登录smardaten后缺少模板和数据吗?从0到1构建完整应用费时费力?别急,一大波模板资源已经装上车,这就给你安排上!【smardaten应用商超】基于数睿数据长期服务软件企业、大型客户的经验&#…

Redis实现服务注册与服务发现源码阅读(Go语言)

Redis实现服务注册与服务发现源码阅读 背景 近期在看开源项目CloudWeGo中看到目前GoLang微服务框架Hertz中支持通过Redis实现服务注册与服务发现功能。便想着阅读下源码 源码阅读 gut clone了hertz-contrib后看到在一级目录下有目前各种主流的服务注册与发现的实现方案。为…

【数据结构初阶】二叉树顺序结构:堆的实现

前言 前边077带着大家学习了树与二叉树的相关概念,这篇文章我们来实现一个二叉树的顺序结构。 二叉树的顺序结构 普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉…

SQL注入基础入门篇 注入思路及常见的SQL注入类型总结

SQL注入基础入门篇1. SQL注入的概念1.1 什么是SQL注入?1.2 注入过程1.3 SQL注入的分类2. 注入思路3. 第一次注入3.1 寻找注入点3.2 构造攻击语句3.2.1 数据出在哪里?3.2.2 怎么有序的获取核心数据?3.2.2.1 基础信息查询3.2.2.2 表名&#xff…

TCP三次握手与四次挥手(一次明白)

TCP基本信息 默认端口号:80 LINUX中TIME_WAIT的默认时间是30s TCP三次握手 三次握手过程:每行代表发起握手到另一方刚刚收到数据包时的状态 客户端服务端客户端状态服务端状态握手前CLOSELISTEN客户端发送带有SYN标志的数据包到服务端一次握手SYN_SENDLISTEN二次握手服务端发送…

Java项目(一些注解、依赖

文章目录常用的几个注解DataAllArgsConstructorNoArgsConstructorSetterGetterEqualsAndHashCodeLog4j/Slf4jMYBatis-Plus常用注解TableNameTableIdTableFieldTableLogicMapperMapperMapperScanpom.xml中加入依赖创建项目常用的几个注解 Data 注在类上,提供类的ge…

cv2.addWeighted 操作 np.array 踩坑记录

cv2.addWeighted函数是把两张图片img1, img2达到融合的效果, 看官网的解释,下图中f0和f1代表两张图片, 用法是这样的 import cv2alpha 0.6 beta (1.0 - alpha)src1 cv2.imread("img1.jpg") src2 cv2.imread("img2.jpg&q…

MQ-2烟雾传感器模块功能实现(STM32)

认识MQ-2模块与其工作原理 MQ-2型烟雾传感器属于二氧化锡半导体气敏材料,属于表面离子式N型半导体。当处于200~300摄氏度时,二氧化锡吸附空气中的氧,形成氧的负离子吸附,使半导体中的电子密度减少,从而使其电阻值增加。…

【C语言复习】C语言中的数组与指针

数组与指针复习写在前面数组和指针指针基础概念进阶知识指针的分类指针和数组笔试题写在前面 数组和指针小节,主要分为以下关键点: 常见指针分类,如指针数组、数组指针、函数指针等。什么是数组/ 指针有关数组和指针的题目数组传参 我们也…

写字楼/园区/购物中心空置率太高?快用快鲸智慧楼宇系统

客户租不租你的写字楼,事关区位、交通、环境、价格、面积、装修等诸多因素,但很多招商部对这些影响客户决策的数据并不重视,在客户初次上门看房时仅简单记录姓名、联系方式、需求面积,对其他核心数据熟视无睹,也为日后…

第十三届蓝桥杯

这里写目录标题一、刷题统计(ceil函数返回的是等值于某最小整数的浮点值,不强制转换回int就wa,没错就连和int整数相加都wa二、修剪灌木(主要应看清楚会调转方向三、统计子矩阵(前缀和滑动窗口⭐)四、[积木画…

【算法】笔记:LeetCode 206. 反转链表

文章目录前言思考问题:把分开的节点连在一起结合原题:使用[迭代]解决卡点引入新指针边界条件代码反转的逻辑代码(完整答案)结合原题:使用[递归]解决卡点完整代码问题的子问题当前层要干什么递归出口前言 这道题可以拆…

冰箱压缩机 方案

压缩机是制冷系统的心脏,它从吸气管吸入低温低压的制冷剂气体,通过电机运转带动活塞对其进行压缩后,向排气管排出高温高压的制冷剂气体,为制冷循环提供动力,从而实现压缩→冷凝→膨胀→蒸发 ( 吸热 ) 的制冷循环。压缩…

C#开发的OpenRA的游戏主界面怎么样创建

通过前面加载界面布局数据,可以把整个界面逻辑的数据加载到内存, 但是这些数据怎么显示出来,又是没有定义的。比如前面定义了多个界面的布局, 又是怎么样知道需要显示哪一个界面? 现在就来解决这个问题,其实整个游戏都是可以通过yaml文件进行配置的, 所以我们需要从yaml…

水果FLStudio21.0.0中文版全能数字音乐工作站DAW

FL Studio 21.0.0官方中文版重磅发布纯正简体中文支持,更快捷的音频剪辑及素材管理器,多样主题随心换!Mac版新增对苹果M2/1家族芯片原生支持。编曲、剪辑、录音、混音,20余年的技术积淀和实力研发,FL Studio 已经从电音…

【基础算法】单链表的OJ练习(3) # 移除链表元素 # 相交链表 #

文章目录前言移除链表元素相交链表写在最后前言 本章的OJ练习也是相对简单的,只要能够理解解题的思路,并且依照这个思路能够快速的写出代码,我相信,你的链表水平已经足够了。 对于OJ练习(2) : ->传送门…

不平凡的一天——

作者:指针不指南吗 专栏:个人日常记录 🐾或许会很慢,但是不可以停下来🐾 文章目录1.自我介绍2.上学期3.不凡的一天4.新学期写个博客,简单记录一下,新学期加油!!&#xff…

day7 同步互斥

作业 1.将一个文件中的数据打印到终端上类似cat一个文件,要求如下 (1)a线程读取文件中的数据 (2)B线程将A线程读取到的数据打印到终端上 (3)文件打印完毕后,结束进程 方法1&#…

CMMI流程规范—服务与维护

服务与维护(Service and Maintenance, SM)是指产品销售之后的客户服务和产品维护。客户服务和产品维护的宗旨就是提高客户对产品以及对开发方的满意度。服务与维护过程域是SPP模型的重要组成部分。本规范阐述了服务与维护过程域的两个主要规程&#xff1…