道家人 发表于 2024-5-13 02:15:33

分布式事件办理方案详解

1: 分布式事件简介

大多数场景下,我们的应用都只需要操纵单一的数据库,这种情况下的事件称之为本地事件(LocalTransaction)。本地事件的ACID特性是数据库直接提供支持。本地事件应用架构如下所示:
https://img2023.cnblogs.com/blog/1540879/202403/1540879-20240305223527536-316703542.png
但是在微服务架构中,完成某一个业务功能可能需要高出多个服务,操纵多个数据库。这就涉及到到了分布式事件,需要操纵的资源位于多个资源服务器上,而应用需要包管对于多个资源服务器的数据操纵,要么全部成功,要么全部失败。本质上来说,分布式事件就是为了包管不同资源服务器的数据一致性。
1.1: 跨库事件

跨库事件指的是,一个应用某个功能需要操纵多个库,不同的库中存储不同的业务数据。下图演示了一个服务同时操纵2个库的情况:
https://img2023.cnblogs.com/blog/1540879/202403/1540879-20240305223527159-1771197355.png
1.2: 分库分表

通常一个库数据量比较大或者预期未来的数据量比较大,都会进行分库分表。如下图,将数据库B拆分成了2个库:
https://img2023.cnblogs.com/blog/1540879/202403/1540879-20240305223526822-920676932.png
对于分库分表的情况,一般开辟人员都会使用一些数据库中间件来降低sql操纵的复杂性。如,对于sql:insert into user (id,name) values (1,"张三"),(2,"李四")。这条sql是操纵单库的语法,单库情况下,可以包管事件的一致性。但是由于现在进行了分库分表,开辟人员希望将1号记录插入分库1,2号记录插入分库2。所以数据库中间件要将其改写为2条sql,分别插入两个不同的分库,此时要包管两个库要不都成功,要不都失败,因此根本上全部的数据库中间件都面对着分布式事件的问题。
1.3: 微服务架构

下图演示了一个3个服务之间彼此调用的微服务架构:
https://img2023.cnblogs.com/blog/1540879/202403/1540879-20240305223526373-1324414868.png
ServiceA完成某个功能需要直接操纵数据库,同时需要调用ServiceB和ServiceC,而ServiceB又同时操纵了2个数据库,ServiceC也操纵了一个库。需要包管这些跨服务调用对多个数据库的操纵要么都成功,要么都失败,实际上这可能是最典型的分布式事件场景。
1.4: 小结

上述讨论的分布式事件场景中,无一例外的都直接或者间接的操纵了多个数据库。如何包管事件的ACID特性,对于分布式事件实现方案而言,是非常大的挑战。同时,分布式事件实现方案还必须要考虑性能的问题,如果为了严格包管ACID特性,导致性能严肃降落,那么对于一些要求快速响应的业务,是无法接受的。
2: 分布式事件办理方案种类

办理分布式问题是一个很复杂的问题,对于不同的业务场景,对业务的一致性要求和高并发要求的权衡, 都需要经心选用不同模式的实现方案.
分布式事件实现方案从类型上去分刚性事件、柔型事件:

[*]刚性事件满意CAP的CP理论
[*]柔性事件满意BASE理论(根本可用,最终一致,即AP)
其中刚性事件实现主要有2PC模式,XA协议, 3PC, Seata AT 模式
柔性事件主要有, TCC ,Saga, 可靠消息最终一致, 最大努力通知等,
放一张网络上的图
https://img2023.cnblogs.com/blog/1540879/202403/1540879-20240305223525751-783739931.png
3: 刚性事件

3.1: 2PC

两阶段提交(TwoPhaseCommit),就是将提交(commit)过程划分为2个阶段(Phase),但是在先容两个阶段之前,首先要知道,在2PC事件中的两个角色
分别为:

[*]协调者角色(事件管理器TM)
[*]到场者角色(资源管理器RM)
TM 负责收集各个到场者反馈的状态, 并统筹团体事件,向各个到场者发送指令,做出提交或者回滚决策
RM 接收协调者的指令实行事件操纵,向协调者反馈操纵结果,并继续实行协调者发送的最终指令
举例 :在学校中, 同学A和同学B一起到校门口集合,由于两同学间没有接洽, 所以只能靠老师依次接洽,要求其到校门口集合, 在这件事中需要两同学要不都来, 要不都不来
预备阶段 :同学A先到,在这里期待,其次同学B后带,人到齐
提交阶段 :老师宣布到齐,集体出发
例子中形成两一个事件,若同学A或同学B有一个因为有事没来, 这件事就办不成,只能让已经来的同学先回班级 。
整个事件过程由事件管理器和到场者构成,老师就是事件管理器,两同学就是事件到场者,事件管理器负责决策整个分布式事件的提交和回滚,事件到场者负责本身本地事件的提交和回滚。
下面再看一下两个阶段:
阶段1(预备阶段):
TM通知各个RM预备提交它们的事件分支。如果RM判断本身进行的工作可以被提交,那就对工作内容进行持久化,再给TM肯定答复;要是发生了其他情况,那给TM的都是否定答复。
以mysql数据库为例,在第一阶段,事件管理器向全部涉及到的数据库服务器发出prepare"预备提交"哀求,数据库收到哀求后实行数据修改和日志记录等处置惩罚,处置惩罚完成后只是把事件的状态改成"可以提交",然后把结果返回给事件管理器。
阶段2(提交阶段)
TM根据阶段1各个RM prepare的结果,决定是提交还是回滚事件。如果全部的RM都prepare成功,那么TM通知全部的RM进行提交;如果有RM prepare失败的话,则TM通知全部RM回滚本身的事件分支。
以mysql数据库为例,如果第一阶段中全部数据库都prepare成功,那么事件管理器向数据库服务器发出"确认提交"哀求,数据库服务器把事件的"可以提交"状态改为"提交完成"状态,然后返回应答。如果在第一阶段内有任何一个数据库的操纵发生了错误,或者事件管理器收不到某个数据库的回应,则认为事件失败,回撤全部数据库的事件。数据库服务器收不到第二阶段的确认提交哀求,也会把"可以提交"的事件回撤。
两阶段提交方案下全局事件的ACID特性,是依赖于RM的。一个全局事件内部包含了多个独立的事件分支,这一组事件分支要么都成功,要么都失败。各个事件分支的ACID特性共同构成了全局事件的ACID特性。也就是将单个事件分支支持的ACID特性提升一个层次到分布式事件的范畴。
都成功的示意图:
https://img2023.cnblogs.com/blog/1540879/202403/1540879-20240305223525304-1032615173.png
有失败时的示意图:
https://img2023.cnblogs.com/blog/1540879/202403/1540879-20240305223524738-1458164709.png
两阶段提交存在的问题:

[*]同步阻塞问题 2PC中的到场者是阻塞的。在第一阶段收到哀求后就会预先锁定资源,不绝到commit后才会开释。(如例子中,两同学在收到是集合出发,还是全部回家之前,都需要为去门口集合做预备,不能恣意乱跑,别人也让他两干不了其他事)
[*]单点故障 由于协调者的重要性,一旦协调者TM发生故障,到场者RM会不绝阻塞下去。尤其在第二阶段,协调者发生故障,那么全部的到场者还都处于锁定事件资源的状态中,而无法继续完成事件操纵。(例子中,如果老师没来,先来的同学会不绝在门口等着, 或者某一个同学迟迟没有消息,既没有跟老师说不来,也没有跟老师说来, 将也会导致先来的同学一致期待)
[*]数据不一致, 若协调者第二阶段发送提交哀求时崩溃,可能部分到场者收到commit哀求提交了事件,而另一部分到场者未收到commit哀求而放弃事件,从而造成数据不一致的问题。(如果老师宣布出发时,有一个同学没听见, 或者分神了,都将会导致没有集体出发)
3.2: XA 协议

2PC的传统方案是在数据库层面实现的,如Oracle、MySQL都支持2PC协议,为了统一标准减少行业内不必要的对接成本,需要订定标准化的处置惩罚模型及接口标准,国际开放标准组织Open Group定义分布式事件处置惩罚模型DTP(Distributed Transaction Processing Reference Model)。即 TM和RM之间通信的协议定义,即接口的定义, 若各开辟商需要实现XA协议的2PC模式, 需要对XA接口进行实现
一般我们常用的 数据库默认都 实现了 XA接口, 例如Mysql等等 . 例如Seata 分布式框架的XA模式,即是将默认支持XA规范的数据源做一层封装而已,使用起来更简单
3.3: 3PC

三阶段提交协议(3PC)主要是为了办理两阶段提交协议的阻塞问题,2pc存在的问题是当协作者崩溃时,到场者不能做出最后的选择。因此到场者可能在协作者恢复之前保持阻塞。三阶段提交(Three-phase commit),是二阶段提交(2PC)的改进版本。目标就是办理一阶段中的阻塞问题,或者说是部分阻塞
所谓的三个阶段分别是:询问,然后再锁资源,最后真正提交。
可以理解为 在二阶段之前 添加了 询问操纵,

[*]第一阶段:CanCommit
[*]第二阶段:PreCommit
[*]第三阶段:Do Commit
https://img2023.cnblogs.com/blog/1540879/202403/1540879-20240305223524287-1230580948.png
阶段一:CanCommit

[*]事件询问。协调者向全部到场者发送包含事件内容的canCommit的哀求,询问是否可以实行事件提交,并期待应答;
[*]各到场者反馈事件询问。正常情况下,如果到场者认为可以顺利实行事件,则返回Yes,否则返回No。
经过这一轮询问下来,包管了全部节点此时都是畅通的, 而且资源富足等等,并为后面做好了预备, 至少制止了99% 的事件异常的情况导致的阻塞,使异常提前发生,制止了在有些事件已经预提交后再发生问题,也使得后面的行为更加的大胆
阶段二:PreCommit
在本阶段,协调者会根据上一阶段的反馈情况来决定是否可以实行事件的PreCommit操纵。有以下两种可能

[*]实行事件预提交(CanCommit全部返回YES)
[*]中断事件 (恣意一个NO)
实行事件预提交

[*]发送预提交哀求。协调者向全部节点发出PreCommit哀求,并进入prepared阶段;
[*]事件预提交。到场者收到PreCommit哀求后,会开始事件操纵,并将Undo和Redo日志写入本机事件日志;
[*]各到场者成功实行事件操纵,同时将反馈以Ack响应形式发送给协调者,同事期待三阶段的最终的Commit或Abort指令。
中断事件
如果恣意一个到场者向协调者发送No响应,或者期待超时,协调者在没有得到全部到场者响应时,即可以中断事件。
中断事件的操纵为:

[*]发送中断哀求。 协调者向全部到场者发送Abort哀求;
[*]中断事件。无论是participant 收到协调者的Abort哀求,还是participant 期待协调者哀求过程中出现超时,到场者都会中断事件;
阶段三:doCommit
在这个阶段,会真正的进行事件提交,同样存在两种可能。

[*]实行提交
[*]回滚事件
实行提交

[*]coordinator发送提交哀求。假如coordinator协调者收到了全部到场者的Ack响应,那么将从预提交转换到提交状态,并向全部到场者,发送doCommit哀求;
[*]事件提交。到场者收到doCommit哀求后,会正式实行事件提交操纵,并在完成提交操纵后开释占用资源;
[*]反馈事件提交结果。到场者将在完成事件提交后,向协调者发送Ack消息;
[*]完成事件。协调者接收到全部到场者的Ack消息后,完成事件。
回滚事件
在该阶段,假设正常状态的协调者接收到任一个到场者发送的No响应,或在超时时间内,仍旧没收到反馈消息,就会回滚事件:

[*]发送中断哀求。协调者向全部的到场者发送rollback哀求;
[*]事件回滚。到场者收到rollback哀求后,会利用阶段二中的Undo消息实行事件回滚,并在完成回滚后开释占用资源;
[*]反馈事件回滚结果。到场者在完成回滚后向协调者发送Ack消息;
[*]回滚事件。协调者接收到全部到场者反馈的Ack消息后,完成事件回滚。
如何办理阻塞
在doCommit阶段,如果到场者无法及时接收到来自协调者的doCommit或者rollback哀求时,会在期待超时之后,继续进行事件的提交。因为如果能进入第三阶段,那么第一个阶段全部节点都返回了YES,换句话说就是 :
当进入第三阶段时,由于网络超时/网络分区等原因,虽然到场者没有收到commit或者abort响应,但是他有来由信任:成功提交的几率很大。
所以理论上就是办理了2PC中, 因为超时或者协调者宕机,导致所以资源不绝期待, 3PC则更加大胆, 超时直接提交
这个阻塞还是存在的,毕竟各个到场者还是会开启事件。那就存在一段时间,全部到场者都在事件中,还是会停止相应其他操纵。
但是阻塞不会不绝一连下去。在两阶段提交中,如果阻塞发生后协调者宕机,则阻塞会不绝存在,无法解开;而三阶段提交中,即使协调者宕机,到场者也会自动提交事件进而解开阻塞。
3.4: Seata AT 模式

3.4.1: 概述

AT 模式是 Seata 创新的一种非侵入式的分布式事件办理方案,Seata 在内部做了对数据库操纵的代理层,我们使用 Seata AT 模式时,实际上用的是 Seata 自带的数据源代理 DataSourceProxy,Seata 在这层代理中加入了很多逻辑,比如插入回滚 undo_log 日志,查抄全局锁等。Seata AT 模式是标准2PC的演变,在此基础上进行优化
使用前提:

[*]基于支持本地 ACID 事件的关系型数据库。
[*]Java 应用,通过 JDBC 访问数据库。
两阶段提交协议的演变:

[*]一阶段:业务数据和回滚日志记录在同一个本地事件中提交,开释本地锁和毗连资源。
[*]二阶段:

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

3.4.2:三个角色

在Seata AT的架构中,一共有三个角色:

[*]TC(TransactionCoordinator)-事件协调者
维护全局和分支事件的状态,驱动全局事件提交或回滚。
[*]TM(TransactionManager)-事件管理器
定义全局事件的范围:开始全局事件、提交或回滚全局事件。
[*]RM(ResourceManager)-资源管理器
管理分支事件处置惩罚的资源,与TC交谈以注册分支事件和报告分支事件的状态,并驱动分支事件提交或回滚。
其中,TC为单独摆设的Server服务端,TM和RM为嵌入到应用中的Client客户端。
在Seata中,一个分布式事件的生命周期如下:
1.客户端A作为入口, 远程调用了客户端B和C的资源,此时就在客户端A创建了一个TM,TM哀求TC开启一个全局事件。TC会生成一个XID作为该全局事件的编号。XID会在微服务的调用链路中传播(例如使用OpenFeign调用时,会拦截进行添加header),包管将多个微服务的子事件关联在一起。
2.被调用的链路中的RM会哀求TC将本地事件注册为全局事件的分支事件,通过全局事件的XID进行关联。
3.当客户端A调用完毕,而且B,C都没有报错,实行到客户端A方法的尾部, 那么处于客户端A中的TM会哀求TC告诉XID对应的全局事件是进行提交, 如果有报错则全部通知回滚。
4.TC驱动RM们将XID对应的本身的本地事件进行提交还是回滚。
如下图:
https://img2023.cnblogs.com/blog/1540879/202403/1540879-20240305223523861-1607218317.png
3.4.3: 两个阶段

以一个示例来说明整个 AT 分支的工作过程。
如下product表
FieldTypeKeyidbigint(20)PRInamevarchar(100)sincevarchar(100)某分支实行如下sql:
update product set name = 'GTS' where name = 'TXC';一阶段

[*]解析 SQL:得到 SQL 的类型(UPDATE),表(product),条件(where name = 'TXC')等相干的信息。
[*]查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据。(修改前的数据 )
[*]实行业务 SQL:更新这条记录的 name 为 'GTS'。
[*]查询后镜像:根据前镜像的结果,通过 主键 定位数据
[*]插入回滚日志:把前后镜像数据以及业务 SQL 相干的信息构成一条回滚日志记录,插入到 UNDO_LOG 表中。用作后续二阶段做预备
[*]提交前,向 TC 注册分支:申请 product 表中,主键值即是 1 的记录的 全局锁 。
[*]本地事件提交:业务数据的更新和前面步调中生成的 UNDO LOG 一并提交。
[*]将本地事件提交的结果上报给 TC。
如下示意图:
https://img2023.cnblogs.com/blog/1540879/202403/1540879-20240305223523395-1090494584.png
由此可以看出,Seata AT 模式和 传统2PC的根本区别在于, 一阶段中AT模式是将数据真正提交, 此时将会开释掉资源, 数据的回滚是靠记录的表数据进行, 而传统2PC 此处将不绝保持毗连, 直到全局事件的竣事
二阶段-回滚:

[*]收到 TC 的分支回滚哀求,开启一个本地事件,实行如下操纵。
[*]通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。
[*]数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事件之外的动作做了修改。这种情况,需要根据配置策略来做处置惩罚,具体的说明请自行查阅 Seata 官网。
[*]根据 UNDO LOG 中的前镜像和业务 SQL 的相干信息生成并实行回滚的语句
[*]提交本地事件。并把本地事件的实行结果(即分支事件回滚的结果)上报给 TC。
二阶段-提交:

[*]收到 TC 的分支提交哀求,把哀求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。
[*]异步任务阶段的分支提交哀求将异步和批量地删除相应 UNDO LOG 记录。
https://img2023.cnblogs.com/blog/1540879/202403/1540879-20240305223522858-881002826.png
3.4.5:Seata AT模式存在的问题,以及办理方案

问题一:
通过上面的学习,我们知道Seata AT的一阶段是真实将数据库提交的, 那么对于这条记录,其他业务此时是可以对这条记录进行修改的, 但是我们知道,在二阶段中, 我们有可能将此记录回滚, 这时就出现了脏写的问题
写隔离:Seata 使用了两把锁办理此问题

[*]一阶段本地事件提交前,需要确保先拿到 全局锁 。
[*]拿不到 全局锁 ,不能提交本地事件。
[*]拿 全局锁 的尝试被限制在肯定范围内,超出范围将放弃,并回滚本地事件,开释本地锁。
以一个示例来说明:
两个全局事件 tx1 和 tx2,分别对 a 表的 m 字段进行更新操纵,m 的初始值 1000。
tx1 先开始,开启本地事件,拿到本地锁,更新操纵 m = 1000 - 100 = 900。本地事件提交前,先拿到该记录的 全局锁 ,本地提交开释本地锁。 tx2 后开始,开启本地事件,拿到本地锁,更新操纵 m = 900 - 100 = 800。本地事件提交前,尝试拿该记录的 全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试期待 全局锁 。
https://img2023.cnblogs.com/blog/1540879/202403/1540879-20240305223522289-1676461838.png
tx1 二阶段全局提交,开释 全局锁 。tx2 拿到 全局锁 提交本地事件。
https://img2023.cnblogs.com/blog/1540879/202403/1540879-20240305223521873-558611025.png
如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操纵,实现分支的回滚。
此时,如果 tx2 仍在期待该数据的 全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会不绝重试,直到 tx2 的 全局锁 等锁超时,放弃 全局锁 并回滚本地事件开释本地锁,tx1 的分支回滚最终成功。(相当于人为的制造两锁之间的死锁, 然后是其中一方超时开释资源回滚, 另外一方不绝重试,即可拿到锁)
因为整个过程 全局锁 在 tx1 竣事前不绝是被 tx1 持有的,所以不会发生 脏写 的问题。
本地锁的目标是为了在后面的事件读到的是前事件提交后的数据, 例如在本例中, tx1在修改1000- 100 = 900 而且没有提交时, tx2开始实行,如果没有本地锁, 将读到 1000 ,而且也进行 1000 -100 = 900 ,那么当tx1顺利全局提交后,tx2也提交后,最终数据是 900, 与实际相悖, 而存在本地锁时, tx1,在读取开始到提交竣事时, 不绝都是持锁状态, tx2 需要等到 数据变成900 后才气进行操纵, 那么将进行 900 -100的操纵, 那么最终当 tx1 和 tx2提交后, 数据为800
全局锁的目标是为了在全局提交和全局回滚时防止数据出现异常, 例如上述tx1,tx2,是tx1先持有到全局锁,那么将先实行1000-100 = 900, tx2实行 900 -100 = 800, 如果没有全局锁, 可能会产生,tx2反而先提交, 先为800,后为900的情况, 同时全局锁和本地锁配合也能办理脏写问题
问题二:
在数据库本地事件隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) 。因为一阶段的提交,是将事件彻底提交,并记录undo_log日志表的方式, 所以在全局事件彻底提交之前, 后续事件会读取到该数据, 例如上面的问题一中, tx2 就可以读到 tx1修改后的 1000-100, 所以在全局事件的视角上, 该事件为读未提交
读隔离:
如果应用在特定场景下,必需要求全局的 读已提交 ,现在 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。比如上述的例子中,就逼迫要求tx2, 必须等到 tx1 真正全局提交后,再读取数据
https://img2023.cnblogs.com/blog/1540879/202403/1540879-20240305223521465-1323643365.png
SELECT FOR UPDATE 语句的实行会申请 全局锁 (此时全局锁在tx1手上,只有全局提交,或回滚后才开释),如果 全局锁 被其他事件持有,则开释本地锁(回滚 SELECT FOR UPDATE 语句的本地实行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相干数据是 已提交 的,才返回。
出于总体性能上的考虑,Seata 现在的方案并没有对全部 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。
4: 柔性事件

柔性事件主要分为补偿型和通知型
通知型:可靠消息最终一致、最大努力通知型
补偿型: TCC、Saga;
4.1: 可靠消息最终一致性(异步确保型事件)

需要办理下面两个问题:
发送方: 事件事件到场方接收消息的可靠性,即本地事件和消息发送成功的一致性
接收方: 消息重复消费的问题,要办理消息重复消费的问题就要实现事件到场方的方法幂等性
4.1.1: 本地消息表方案

本地消息表这个方案最初是 eBay 提出的,此方案的核心是通过本地事件包管数据业务操纵和消息的一致性,然后通过定时任务将消息发送至消息中间件,待确认消息发送给消费方成功再将消息删除。
https://img2023.cnblogs.com/blog/1540879/202403/1540879-20240305223521087-1963379793.png
​                如上图所示: 用户系统新增用户后,需要对此用户赠送积分,调用积分服务增加积分,此处要包管用户新增和新增
​                积分最终都要成功
        1.用户系统接受注册请求, 开启本地事务,并新增一条用户信息
        1.在本地事务中, 继续新增 积分新增 消息日志表记录, 由于是在同一个本地事务中,步骤一二保证一致性
        1.定时任务程序定期扫描积分消息表, 读取未发送状态的消息记录,进行发送消息,发送成功后(ACK机制)更新消息记录状态为已发送(这里可以主动扫描记录表,也可监听记录表的插入事件,例如使用canal监听binlog)
        1.MQ服务接受到消息,并将消息发送给积分服务
        1.积分服务消费消息,进行增加用户积分操作,这里需要保证消费接口的幂等性,保证消息重复消费不会重复增加积分,并且这里需要保证消息的4.1.2: 事件性消息

上面的本地消息方案中,确保事件成功发送,是由一个服务进行扫描消息表, 也就是MQ的客户端包管
事件性消息,即是通过消息发送方通知,或者靠自身定时回查发送方状态来决定是否将消息进行投递
例如自带事件性消息的Rocketmq
如下图所示:
https://img2023.cnblogs.com/blog/1540879/202403/1540879-20240305223520683-2070254747.png
以Rocketmq为例:

[*]生产者将消息发送至RocketMQ服务端
[*]RocketMQ服务端将消息持久化成功之后,向生产者返回Ack确认消息已经发送成功,此时消息被标记为"暂不能投递",这种状态下的消息即为半事件消息(预通知MQ,将需要发送的消息预先保存在MQ服务端)
[*]生产者开始实行本地事件逻辑
[*]生产者根据本地事件实行结果向服务端提交二次确认结果(Commit或是Rollback),服务端收到确认结果后处置惩罚逻辑如下:

[*]二次确认结果为Commit:服务端将半事件消息标记为可投递,并投递给消费者。
[*]二次确认结果为Rollback:服务端将回滚事件,不会将半事件消息投递给消费者。

[*]在断网或者是生产者应用重启的特殊情况下,若服务端未收到发送者提交的二次确认结果,或服务端收到的二次确认结果为Unknown未知状态,经过固定时间后,服务端将对消息生产者即生产者集群中任一生产者实例发起消息回查。
[*]生产者收到消息回查后,需要查抄对应消息的本地事件实行的最闭幕果。
[*]生产者根据查抄到的本地事件的最终状态再次提交二次确认,服务端仍按照步调4对半事件消息进行处置惩罚。
RocketMQ提供RocketMQLocalTransactionListener接口:
public interface RocketMQLocalTransactionListener {
    /**
   * 发送 prepare 消息成功此方法被回调,该方法用于执行本地事务
   *
   * @param msg 回传的消息,利用 transactionId 即可获取到该消息的唯一Id
   * @param arg 调用 send方法时传递的参数,当send时候若有额外的参数可以传递到send方法中,这里能获取到
   * @return 返回事务状态,COMMIT:提交ROLLBACK:回滚UNKNOW:回调
   */
    RocketMQLocalTransactionState executeLocalTransaction(final Message msg, final Object arg);

    /**
   * @param msg 通过获取 transactionId 来判断这条消息的本地事务执行状态
   * @return 返回事务状态, COMMIT:提交ROLLBACK:回滚UNKNOW:回调
   */
    RocketMQLocalTransactionState checkLocalTransaction(final Message msg);
}4.1.3: 二者区别

https://img2023.cnblogs.com/blog/1540879/202403/1540879-20240305223520117-1837358977.png
4.2: 最大努力通知

和可靠消息投递不同的是,可靠消息投递,是事件发起方尽可能的包管消息的投递,包管最终一致性, 其内部使用消息中间件作为通讯中介, 一般用在内部系统使用
最大努力通知事件,主要靠事件的被调用方发起通知,主要用于外部系统,因为外部的网络环境更加复杂和不可信,所以通知的手段也可依不同的场景进行选择,不能只依靠MQ, 要尽最大努力去通知实现数据最终一致性,,比如充值平台与运营商、支付对接、商户通知等等跨平台、跨企业的系统间业务交互场景;
如下图, 账户系统接受到充值哀求,调用充值系统进行支付(例如外部支付宝)
https://img2023.cnblogs.com/blog/1540879/202403/1540879-20240305223519666-51242653.png
其中需要主要的是两个点

[*]结果通知, 发起通知方需要尽可能的将处置惩罚结果通知到接受通知方, 通知手段可以使用MQ,如果使用MQ,也需要包管消息的可靠投递,也可以使用HTTP调用, 如果通知失败,需要在隔断时间内进行重试
[*]消息校对,接收通知方也可主动哀求查询结果, 可以作为通知不成功的兜底补偿方案
4.3: TCC

关于TCC(Try-Confirm-Cancel)的概念,最早是由Pat Helland于2007年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。在该论文中,TCC还是以Tentative-Confirmation-Cancellation作为名称;正式以Try-Confirm-Cancel作为名称的,可能是Atomikos(Gregor Hohpe所著册本《Enterprise Integration Patterns》中收录了关于TCC的先容,提到了Atomikos的Try-Confirm-Cancel,并认为二者是相似的概念)。
TCC事件机制相对于传统事件机制(X/Open XA Two-Phase-Commit),其特征在于它不依赖资源管理器(RM)对XA的支持,而是通过对(由业务系统提供的)业务逻辑的调度来实现分布式事件。
TCC 分为三个阶段,分别为 “预备”、“提交”和“回滚” ,三个阶段都需要本身业务逻辑实现, 所以理论上, TCC模式并不依赖于任何对于资源的限制, 而且,由于是 本身实现,如果考虑周全, 提交或者回滚完全可以隔断很长时间后实行, 包管最终一致就可
https://img2023.cnblogs.com/blog/1540879/202403/1540879-20240305223519276-1817580096.png
总体来说, TCC仍然是两阶段提交的模型, 如一个扣款例子

[*]一阶段(Try): 预留业务资源, 例如将张三的账户的余额扣除,并保留在冻结字段
[*]二阶段(Confirm/Cancel): 由全局事件通知提交或回滚, 实行自定义的 提交或者回滚方法,将张三的冻结金清除,或者加回余额
优点:

[*]相比较传统2PC的强一致性方案, TCC实现了最终一致性,在try阶段就将资源提交, 不会长时间的占用资源
[*]对比 Seata AT 模式, TCC和 他有些相似,都是先将资源提交,再用事先预备好的提交,回滚方案进行包管事件一致性, 只不过Seata AT方案是框架做好的,自动生成前置,后置镜像, 所以Seata AT依赖资源自身需要满意ACID的要求, 即是传统数据库, 而 TCC的预留资源, 提交资源, 回滚资源都是由业务本身实现, 所以可以是任何类型的资源, 而且提交回滚的异步操纵, 也使得其性能更高,对系统进行削峰填谷
缺点:

[*]TCC 是一种侵入式的分布式事件办理方案,以上三个操纵都需要业务系统自行实现,对业务系统有着非常大的入侵性
[*]设计相对复杂, 尤其是本身实现三个方法事, 需要考虑方方面面, 其中最为常见的有空回滚、幂等、悬挂(Seata TCC的实现,在新版本中已经自动给我们办理)等。
其中Seata 中 对TCC模式进行了实现, 可以参考如下文档:
https://seata.apache.org/zh-cn/docs/user/mode/tcc
4.3.1: 空回滚,幂等,悬挂问题

如何处置惩罚空回滚:
空回滚指的是在一个分布式事件中,在没有调用到场方的 Try 方法的情况下,TM 驱动二阶段回滚调用了到场方的 Cancel 方法。
例如在上面的转账案例中, 要扣款100 元, 那么在try方法中, 将对余额实行 余额-100 的操纵, cancel方法将实行 余额+100的操纵, 若此时,因为节点异常, 调用try失败,那么当全局任务回滚时,实行了该分支事件的 cancel方法, 如果没有控制, 那么将导致余额变动
要想防止空回滚,那么必须在 Cancel 方法中识别这是一个空回滚,
可以添加一张事件控制表,表中记录了每个分支事件当前实行的状态, 例如在实行try方法后, 将表中标识置为1, 那么当因为错误问题空回滚实行 cancel时,只需判断当前节点是否为1即可
如何处置惩罚幂等
幂等问题指的是 TC 重复进行二阶段提交,因此 Confirm/Cancel 接口需要支持幂等处置惩罚,即不会产生资源重复提交或者重复开释。
如何产生幂等问题:
https://img2023.cnblogs.com/blog/1540879/202403/1540879-20240305223518815-608152946.png
如上图所示,到场者 A 实行完二阶段之后,由于网络抖动或者宕机问题,会造成 TC 收不到到场者 A实行二阶段的返回结果,TC 会重复发起调用,直到二阶段实行结果成功。
同样,办理方法同样可以依赖控制表, 如果实行过 confirm方法, 则将标识置为 2, 如果发现标识已经是2,而且又调到 confirm方法,则直接跳过
如何处置惩罚悬挂
悬挂指的是二阶段 Cancel 方法比 一阶段 Try 方法优先实行,由于答应空回滚的原因,在实行完二阶段 Cancel 方法之后直接空回滚返回成功,此时全局事件已竣事,但是由于 Try 方法随后实行,这就会造成一阶段 Try 方法预留的资源永远无法提交和开释了。
如何产生:
https://img2023.cnblogs.com/blog/1540879/202403/1540879-20240305223518154-881670379.png
如上图所示,在实行到场者 A 的一阶段 Try 方法时,出现网路拥堵,由于 Seata 全局事件有超时限制,实行 Try 方法超时后,TM 决议全局回滚,回滚完成后如果此时 RPC 哀求才到到达场者 A,实行Try 方法进行资源预留,从而造成悬挂。
同样的控制表办理, 实行try方法时表中的字段应该为0, 如果事先先实行了confirm, 那么此时表中的字段为 2了, 那么直接报错即可
参考资料

https://www.bytesoft.org/tcc-intro/
https://seata.apache.org/zh-cn/docs/dev/mode/at-mode
https://rocketmq.apache.org/zh/docs/featureBehavior/04transactionmessage
https://www.cnblogs.com/crazymakercircle/p/13917517.html#autoid-h3-26-0-0
https://zhuanlan.zhihu.com/p/263555694

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 分布式事件办理方案详解