ToB企服应用市场:ToB评测及商务社交产业平台

标题: 分布式事件办理方案详解 [打印本页]

作者: 道家人    时间: 2024-5-13 02:15
标题: 分布式事件办理方案详解
1: 分布式事件简介

大多数场景下,我们的应用都只需要操纵单一的数据库,这种情况下的事件称之为本地事件(LocalTransaction)。本地事件的ACID特性是数据库直接提供支持。本地事件应用架构如下所示:

但是在微服务架构中,完成某一个业务功能可能需要高出多个服务,操纵多个数据库。这就涉及到到了分布式事件,需要操纵的资源位于多个资源服务器上,而应用需要包管对于多个资源服务器的数据操纵,要么全部成功,要么全部失败。本质上来说,分布式事件就是为了包管不同资源服务器的数据一致性。
1.1: 跨库事件

跨库事件指的是,一个应用某个功能需要操纵多个库,不同的库中存储不同的业务数据。下图演示了一个服务同时操纵2个库的情况:

1.2: 分库分表

通常一个库数据量比较大或者预期未来的数据量比较大,都会进行分库分表。如下图,将数据库B拆分成了2个库:

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

下图演示了一个3个服务之间彼此调用的微服务架构:

ServiceA完成某个功能需要直接操纵数据库,同时需要调用ServiceB和ServiceC,而ServiceB又同时操纵了2个数据库,ServiceC也操纵了一个库。需要包管这些跨服务调用对多个数据库的操纵要么都成功,要么都失败,实际上这可能是最典型的分布式事件场景。
1.4: 小结

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

办理分布式问题是一个很复杂的问题,对于不同的业务场景,对业务的一致性要求和高并发要求的权衡, 都需要经心选用不同模式的实现方案.
分布式事件实现方案从类型上去分刚性事件、柔型事件:
其中刚性事件实现主要有  2PC模式,XA协议, 3PC, Seata AT 模式
柔性事件主要有, TCC ,Saga, 可靠消息最终一致, 最大努力通知等,
放一张网络上的图

3: 刚性事件

3.1: 2PC

两阶段提交(TwoPhaseCommit),就是将提交(commit)过程划分为2个阶段(Phase),但是在先容两个阶段之前,首先要知道,在2PC事件中的两个角色
分别为:
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特性提升一个层次到分布式事件的范畴。
都成功的示意图:

有失败时的示意图:

两阶段提交存在的问题:
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
经过这一轮询问下来,包管了全部节点此时都是畅通的, 而且资源富足等等,并为后面做好了预备, 至少制止了99% 的事件异常的情况导致的阻塞,使异常提前发生,制止了在有些事件已经预提交后再发生问题,  也使得后面的行为更加的大胆
阶段二:PreCommit
在本阶段,协调者会根据上一阶段的反馈情况来决定是否可以实行事件的PreCommit操纵。有以下两种可能
实行事件预提交
中断事件
如果恣意一个到场者向协调者发送No响应,或者期待超时,协调者在没有得到全部到场者响应时,即可以中断事件。
中断事件的操纵为:
阶段三:doCommit
在这个阶段,会真正的进行事件提交,同样存在两种可能。
实行提交
回滚事件
在该阶段,假设正常状态的协调者接收到任一个到场者发送的No响应,或在超时时间内,仍旧没收到反馈消息,就会回滚事件:
如何办理阻塞
在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的演变,在此基础上进行优化
使用前提:
两阶段提交协议的演变:
3.4.2:  三个角色

在Seata AT的架构中,一共有三个角色:
其中,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对应的本身的本地事件进行提交还是回滚。
如下图:

3.4.3: 两个阶段

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

由此可以看出,Seata AT 模式和 传统2PC的根本区别在于, 一阶段中AT模式是将数据真正提交, 此时将会开释掉资源, 数据的回滚是靠记录的表数据进行, 而传统2PC 此处将不绝保持毗连, 直到全局事件的竣事
二阶段-回滚:
二阶段-提交:

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 需要重试期待 全局锁

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

如果 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 真正全局提交后,再读取数据

SELECT FOR UPDATE 语句的实行会申请 全局锁 (此时全局锁在tx1手上,只有全局提交,或回滚后才开释),如果 全局锁 被其他事件持有,则开释本地锁(回滚 SELECT FOR UPDATE 语句的本地实行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相干数据是 已提交 的,才返回。
出于总体性能上的考虑,Seata 现在的方案并没有对全部 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。
4: 柔性事件

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

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

本地消息表这个方案最初是 eBay 提出的,此方案的核心是通过本地事件包管数据业务操纵和消息的一致性,然后通过定时任务将消息发送至消息中间件,待确认消息发送给消费方成功再将消息删除。

​                如上图所示: 用户系统新增用户后,需要对此用户赠送积分,调用积分服务增加积分,此处要包管用户新增和新增
​                积分最终都要成功
  1.         1.  用户系统接受注册请求, 开启本地事务,并新增一条用户信息
  2.         1.  在本地事务中, 继续新增 积分新增 消息日志表记录, 由于是在同一个本地事务中,步骤一二保证一致性
  3.         1.  定时任务程序定期扫描积分消息表, 读取未发送状态的消息记录,进行发送消息,发送成功后(ACK机制)更新消息记录状态为已发送(这里可以主动扫描记录表,也可监听记录表的插入事件,例如使用canal监听binlog)
  4.         1.  MQ服务接受到消息,并将消息发送给积分服务
  5.         1.  积分服务消费消息,进行增加用户积分操作,这里需要保证消费接口的幂等性,保证消息重复消费不会重复增加积分,并且这里需要保证消息的
复制代码
4.1.2: 事件性消息

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

以Rocketmq为例:
RocketMQ提供RocketMQLocalTransactionListener接口:
  1. public interface RocketMQLocalTransactionListener {
  2.     /**
  3.      * 发送 prepare 消息成功此方法被回调,该方法用于执行本地事务
  4.      *
  5.      * @param msg 回传的消息,利用 transactionId 即可获取到该消息的唯一Id
  6.      * @param arg 调用 send方法时传递的参数,当send时候若有额外的参数可以传递到send方法中,这里能获取到
  7.      * @return 返回事务状态,COMMIT:提交ROLLBACK:回滚UNKNOW:回调
  8.      */
  9.     RocketMQLocalTransactionState executeLocalTransaction(final Message msg, final Object arg);
  10.     /**
  11.      * @param msg 通过获取 transactionId 来判断这条消息的本地事务执行状态
  12.      * @return 返回事务状态, COMMIT:提交ROLLBACK:回滚UNKNOW:回调
  13.      */
  14.     RocketMQLocalTransactionState checkLocalTransaction(final Message msg);
  15. }
复制代码
4.1.3: 二者区别


4.2: 最大努力通知

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

其中需要主要的是两个点
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模式并不依赖于任何对于资源的限制, 而且,由于是 本身实现,如果考虑周全, 提交或者回滚完全可以隔断很长时间后实行, 包管最终一致就可

总体来说, 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 接口需要支持幂等处置惩罚,即不会产生资源重复提交或者重复开释。
如何产生幂等问题:

如上图所示,到场者 A 实行完二阶段之后,由于网络抖动或者宕机问题,会造成 TC 收不到到场者 A实行二阶段的返回结果,TC 会重复发起调用,直到二阶段实行结果成功。
同样,办理方法同样可以依赖控制表, 如果实行过 confirm方法, 则将标识置为 2, 如果发现标识已经是2,而且又调到 confirm方法,则直接跳过
如何处置惩罚悬挂
悬挂指的是二阶段 Cancel 方法比 一阶段 Try 方法优先实行,由于答应空回滚的原因,在实行完二阶段 Cancel 方法之后直接空回滚返回成功,此时全局事件已竣事,但是由于 Try 方法随后实行,这就会造成一阶段 Try 方法预留的资源永远无法提交和开释了。
如何产生:

如上图所示,在实行到场者 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企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4