Yao
2023/01/11阅读:40主题:橙心
分布式事务解决方案
在《分布式事务理论基础》中介绍了分布式事务的理论基础,下面基于不同的一致性模型介绍分布式事务的常见解决方案。
XA --强一致性
由 Tuxedo 提出的 XA 是一个分布式事务协议,规定了事务管理器和资源管理器接口。XA 协议可以分为两部分,即事务管理器和本地资源管理器。
-
事务管理器作为 协调者
,负责各个本地资源的提交和回滚。 -
资源管理器就是分布式事务的 参与者
.其中资源管理通常是数据库。
基于 XA 协议的,发展出了二阶段提交协议(The two-phase commit protocol,2PC)和三阶段提交协议(Three-phase commit protocol,3PC)。
2PC
二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。
将事务的提交过程分为两个阶段来进行处理:准备阶段和提交阶段。
0x1 准备阶段
-
协调者向所有参与者发送 CanCommit 操作请求,并等待参与者的响应。 -
参与者接收到请求后,会执行请求中的事务操作,将 undo 和 redo 信息记入事务日志中,但是这时并不提交事务。 若不成功,则发送“No”消息,表示终止操作。当所有的参与者都返回了操作结果(Yes 或 No 消息)后,系统进入了提交阶段。
0x2 提交阶段
协调者会根据所有参与者返回的信息向参与者发送 DoCommit 或 DoAbort 指令
-
若协调者收到的都是“Yes”消息,则向参与者发送“DoCommit”消息,参与者会提交事务并释放资源,然后向协调者返回“Ack”消息。
-
如果协调者收到的消息中包含“No”消息,则向所有参与者发送“DoAbort”消息,此时发送“Yes”的参与者会根据之前执行操作时的回滚日志对操作进行回滚,然后所有参与者会向协调者发送“Ack”消息;
-
协调者接收到所有参与者的“Ack”消息,就意味着整个事务结束了。
2PC 实现起来比较简单,但是实际项目中使用比较少,主要因为以下问题:
性能问题:所有参与节点都是事务阻塞型的,占用系统资源,容易导致性能瓶颈。
可靠性问题:如果协调者出现故障,参与者将一直处于锁定状态。
数据一致性问题:在提交阶段,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收到提交消息,那么就导致了节点之间数据的不一致。
3PC
基于 2PC 基础上,3PC 对 2PC 进行了改进,引入了超时机制。同时将准备阶段拆分为 2 个阶段,多了一个 PreCommit 阶段。
3PC 可以划分为 CanCommit 阶段、PreCommit 阶段、DoCommit 阶段。
0x1 CanCommit 阶段
-
协调者向所有参与者发送 "CanCommit" 请求,询问是否可以提交事务,并等待所有参与者答复。 -
参与者收到 "CanCommit" 请求之后,回复 "Yes",表示可以顺利执行事务;否则回复 "No"。

0x2 PreCommit 阶段
协调者根据参与者的回复情况,来决定是否可以进行 PreCommit 操作或中断事务。
如果参与者返回的回复情况全部是 Yes
-
协调者向所有参与者发送 "PreCommit" 请求,参与者进入到预提交阶段。 -
参与者收到 "PreCommit" 请求后,执行事务操作,并将 undo 和 redo 信息记入事务日志中,但这时并不提交事务。 -
参与者向协调者反馈执行成功 "Yes" 或失败响应 "No"。

如果参与者返回的回复情况中包含 No,说明有一个事务执行失败。
-
协调者向所有参与者发送 “Abort”请求 -
参与者收到“Abort”消息之后,或超时后仍未收到协调者的消息,执行事务的中断操作。
0x3 DoCommit 阶段
协调者根据参与者的回复情况,来决定是否可以进行 DoCommit 操作 或 中断事务。
如果参与者返回的回复情况全部是 YES
-
协调者向所有参与者发送 "DoCommit" 消息。 -
参与者接收到 "DoCommit" 消息之后,正式提交事务。完成事务提交之后,释放所有锁住的资源。 -
参与者提交完事务之后,向协调者发送 "Ack" 响应 -
协调者接收到所有参与者的 "Ack" 响应之后,完成事务。

如果参与者返回的回复情况中包含 No,说明有一个事务执行失败。
-
协调者向所有参与者发送 "Abort" 请求。 -
参与者接收到 "Abort" 消息之后,利用其在 "PreCommit" 阶段记录的 undo 信息执行事务的回滚操作,并释放所有锁住的资源。 -
参与者完成事务回滚之后,向协调者发送 "Ack" 消息。 -
协调者接收到参与者反馈的 "Ack" 消息之后,执行事务的中断,并结束事务。
相比二阶段提交,三阶段降低了阻塞范围,在等待超时后协调者或参与者会中断事务,避免了协调者单点问题。DoCommit 阶段中协调者出现问题时,参与者会继续提交事务。
但是数据不一致问题依然存在,当在参与者收到 preCommit 请求后等待 DoCommit 指令时,此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。
TCC --最终一致性
TCC(Try-Confirm-Cancel)的概念,最早是由 Pat Helland 于 2007 年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。
TCC 是服务化的二阶段编程模型, 针对每个操作,都要实现对应的确认和补偿操作,也就是业务逻辑的每个服务都需要实现 Try、Confirm、Cancel 三个操作,第一阶段由业务代码编排来调用 Try 接口进行资源预留,当所有参与者的 Try 接口都成功了,事务协调者提交事务,并调用参与者的 Confirm 接口真正提交业务操作,否则调用每个参与者的 Cancel 接口回滚事务,并且由于 Confirm 或者 Cancel 有可能会重试,因此对应的部分需要支持幂等。
-
Try 阶段:尝试执行,完成所有业务检查(一致性), 预留必须业务资源(准隔离性) -
Confirm 阶段: 确认执行真正执行业务,不作任何业务检查,只使用 Try 阶段预留的业务资源,Confirm 操作满足幂等性 -
Cancel 阶段: 取消执行,释放 Try 阶段预留的业务资源 Cancel 操作满足幂等性 Cancel 阶段的异常和 Confirm 阶段异常处理方案基本上一致。

TCC 事务机制相比于上面介绍的 XA,解决了其几个缺点:
-
解决了协调者单点,由主业务方发起并完成这个业务活动。业务活动管理器也变成多点,引入集群。 -
同步阻塞:引入超时,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小。 -
数据一致性,有了补偿机制之后,由业务活动管理器控制一致性
但是TCC中Try、Confirm、Cancel 的操作需要业务来实现,耦合度过高。
本地消息表 --最终一致性
本地消息表这个方案最初是 ebay 架构师 Dan Pritchett 在 2008 年发表给 ACM 的文章。核心思路是将分布式事务拆分成本地事务进行处理。 本地事物表方案可以将事务分为事务主动方和事物被动方。
-
事务主动方: 分布式事务最先开始处理的事务方 -
事务被动方: 在事务主动方之后处理的业务内的其他事务
事务的主动方需要额外新建事务消息表
,用于记录分布式事务的消息的发生、处理状态。整个业务流程:
-
事务主动方在本地事务中处理业务更新操作和写消息表操作。 -
事务主动方通过消息中间件,通知事务被动方处理事务。 -
事务被动方通过消息中间件,通知事务主动方事务已处理的消息

本地消息表实现的条件:
-
消费者与生成者的接口都要支持幂等 -
生产者需要额外的创建消息表 -
需要提供补偿逻辑,如果消费者业务失败,需要生产者支持回滚操作
容错机制:
-
步骤 1 失败时,事务直接回滚 -
步骤 2、3 写 mq 与消费 mq 失败会进行重试 -
步骤 3 业务失败事务被动方向事务主动方发起事务回滚操作
MQ 事务 --最终一致性
有些 MQ 的实现支持事务,比如 RocketMQ ,基于 MQ 的分布式事务方案其实是对本地消息表的封装。以 RocketMQ 为例介绍 MQ 的分布式事务方案。
-
发送方向 MQ 服务端(MQ Server)发送 half 消息。这个 half 消息与普通消息的区别在于,在事物提交之前,这个消息对订阅方来说是不可见的,订阅方不会消费这个消息。 -
MQ Server 将消息持久化成功之后,向发送方 ACK 确认消息已经发送成功。 -
发送方开始执行本地事务逻辑。 -
如果事务提交成功,将会发送确认消息(commit 或是 rollback)至 MQ Server。 -
MQ Server 收到 commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;MQ Server 收到 rollback 状态则删除half消息,订阅方将不会接受该消息。

异常情况 1:如果发送方发送 commit 或 rollback 消息失败,未到达消息集群
-
MQ Server 会发起消息回查 -
发送方收到回查消息后,会检查本地事务的执行结果 -
根据本地事务的执行结果重新发送 commit 或 rollback 消息 -
MQ Server 根据接收到的消息(commit 或 rollback)判断消息是否可消费或直接删除
异常情况 2:接收方消费失败或消费超时
-
一直重试消费,直到事务订阅方消费消息成功,整个过程可能会导致重复消费问题,所以业务逻辑需要保证幂等性
异常情况 3:消息已消费,但接收方业务处理失败
-
通过 MQ Server 通知发送方进行补偿或事务回滚
Saga 事务 --最终一致性
Saga 事务源于 1987 年普林斯顿大学的 Hecto 和 Kenneth 发表的如何处理 long lived transaction(长活事务)论文,Saga 事务核心思想是将长事务拆分为多个本地短事务,由 Saga 事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。
Saga 事务基本协议如下:
每个 Saga 事务由一系列幂等的有序子事务(sub-transaction) Ti 组成。
每个 Ti 都有对应的幂等补偿动作 Ci,补偿动作用于撤销 Ti 造成的结果。
Saga 的执行顺序有两种:
-
T1, T2, T3, ..., Tn -
T1, T2, ..., Tj, Cj,..., C2, C1,其中 0 < j < n
Saga 定义了两种恢复策略:
-
向前恢复(forward recovery) 适用于必须要成功的场景,发生失败进行重试,执行顺序是类似于这样的:T1, T2, ..., Tj(失败), Tj(重试),..., Tn,其中 j 是发生错误的子事务(sub-transaction)。该情况下不需要 Ci。

-
向后恢复(backward recovery) 如果任一子事务失败,补偿所有已完成的事务。即上面提到的第二种执行顺序,其中 j 是发生错误的 sub-transaction,这种做法的效果是撤销掉之前所有成功的 sub-transation,使得整个 Saga 的执行结果撤销。

Saga 事务常见的有两种不同的实现方式:
-
命令协调(Order Orchestrator):中央协调器负责集中处理事件的决策和业务逻辑排序

-
事件编排 (Event Choreography):没有中央协调器(没有单点风险)时,每个服务产生并观察其他服务的事件,并决定是否应采取行动。 在事件编排方法中,第一个服务执行一个事务,然后发布一个事件。该事件被一个或多个服务进行监听,这些服务再执行本地事务并发布(或不发布)新的事件。 当最后一个服务执行本地事务并且不发布任何事件时,意味着分布式事务结束,或者它发布的事件没有被任何 Saga 参与者听到都意味着事务结束。

作者介绍