分布式事务
2PC
Two-Phase Commit缩写。二阶段提交。为了使基于分布式系统架构下的所有节点在进行事务处理过程中能够保持原子性、一致性而设计的一种算法。目前绝大部分的关系型数据库都是采用二阶段提交协议来完成分布式事务处理的。
[图片上传失败...(image-91bb3e-1653381325874)]
二阶段提交协议是将事务的提交过程分为两个阶段来进行处理:
阶段一:提交事务请求
1、事务询问。
协调者向所有的参与者发送事务内容,询问是否可以执行事务提交操作,并开始等待各参与者的响应。
2、执行事务。
各参与者节点执行事务操作,并将undo和redo信息记入事务日志中。
3、各参与者向协调者反馈事务询问的响应。
如果参与者成功执行了事务操作,那么反馈给协调者Yes响应,表示事务可以执行;
如果参与者没有成功执行事务,那么就反馈给协调者No响应,表示事务不可以执行。
上面内容在形式上近似是协调者组织各参与者对一次事务操作的投票表态过程。因此,二阶段提交协议的阶段一也被称为“投票阶段”。即各参与者投票表名是否要继续执行接下去的事务提交操作。
有点类似于,扣减库存业务之前的,预占逻辑。
阶段二:执行事务提交
协调者会根据各参与者的反馈情况来决定最终是否可以进行事务提交操作。正常情况下,包含以下两种可能:
1、执行事务提交
如果协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务提交。
1)发送提交请求
协调者向所有参与者节点发起Commit请求。
2)事务提交
参与者接受到Commit请求后,会正式执行事务提交操作,并在完成提交之后释放在整个事务执行期间占用的事务资源。
3)反馈事务提交结果
参与者在完成事务提交之后,向协调者发送Ack消息。
4)完成事务
协调者接受到所有参与者反馈的Ack消息后,完成事务。
2、中断事务
假如任何一个参与者向协调者反馈了No响应,或者在等待超时后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务。
1)发送回滚请求
协调者向所有参与者节点发出Rollback请求。
2)事务回滚
参与者接受到Rollback请求后,会利用其在阶段一中记录的Undo信息来执行事务回滚操作,并在完成回滚之后释放在整个事务执行期间占用的资源。
3)反馈事务回滚结果
参与者在完成事务回滚之后,向协调者发送Ack消息。
4)中断事务
协调者接受到所有参与者反馈的Ack消息后,完成事务中断。
优缺点
优点
原理简单、实现方便;
缺点
-
同步阻塞
最大、最明显的问题。极大限制了分布式系统的性能。所有参与该事务操作的逻辑都处于阻塞状态,也就是说,各个参与者在等待其他参与者响应的过程中,将无法进行其他任何操作。
-
单点问题
协调者的角色在整个二阶段提交协议中起到了非常重要的作用。如果协调者出现问题,那么整个二阶段提交流程将无法运转。更为严重的是,如果协调者是在阶段二中出现问题,那么其他参与者将会一直处于锁定事务资源的状态中,而无法继续完成事务操作。
-
数据不一致
在二阶段事务提交的时候,当协调者向所有的参与者发送或未发送完Commit请求而自身崩溃,导致只有部分参与者收到Commit请求。于是部分进行了事务提交,其他的参与者无法提交事务,造成数据不一致现象。
-
太过保守
如果参与者出现故障,导致协调者始终无法获取到参与者的响应信息,此时协调者只能依靠自身的超时机制来判断是否需要中断事务。二阶段提交协议没有设计较为完善的容错机制,任意一个节点的失败都会导致整个事务的失败。
有点类似于,真正去执行扣减逻辑(事务提交)或释放占用(事务回滚)
开启事务 -> 阶段一 -> 阶段二 -> 本地提交/回滚事务。
引用:
《从Paxos到Zookeeper分布式一致性原理与实践》
3PC
3PC,Three-Phase Commit,三阶段提交,是2PC的改进,将二阶段提交协议的“提交事务请求”过程一分为二,形成了由CanCommit、PreCommit和DoCommit三个阶段组成的事务处理协议。
[图片上传失败...(image-bf557a-1653381325874)]
阶段一:CanCommit
1、事务询问
协调者向所有的参与者发送一个包含事务内容的canCommit请求,询问是否可以执行事务提交操作,并开始等待各参与者的响应。
2、各参与者向协调者反馈事务询问的响应
参与者在接受到来自协调者的canCommit请求后,正常情况下,如果其自身认为可以顺利执行事务,那么会反馈Yes响应,并进入预备状态,否则反馈No响应。
阶段二:PreCommit
协调者会根据各参与者的反馈情况来决定是否可以进行事务的PreCommit操作。正常情况下,包含两种情况:
1、执行事务预提交
-
1)发送预提交请求
协调者向所有参与者节点发出preCommit请求,并进入Prepared阶段。
-
2)事务预提交
参与者接受到preCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。
-
3)各参与者向协调者反馈事务执行的响应
如果参与者成功执行了事务操作,那么就会反馈给协调者Ack响应,同时等待最终的指令:提交(commit)或中支(abort)。
2、中断事务
-
1)发送中断请求
协调者向所有参与者节点发起abort请求。
-
2)中断事务
无论是收到来自协调者的abort请求,或是在等待协调者请求过程中出现超时,参与者都会中断事务。
阶段三:doCommit
该阶段会进行真正的事务提交。
1、执行提交
-
1)发送提交请求
假设协调者处于正常工作状态,并且接收到了来自所有参与者的Ack响应,那么它将从预提交状态转换到提交状态,并向所有的参与者发送doCommit请求。
-
2)事务提交
参与者接受到doCommit请求后,会正式执行事务提交操作,并在完成提交之后释放整个事务执行期间占用的事务资源。
-
3)反馈事务提交结果
参与者在完成事务提交后,向协调者发送Ack消息。
-
4)完成事务
协调者接受到所有的参与者反馈的Ack消息,完成事务。
2、中断事务
参与者向协调者反馈No响应,或协调者无法收到所有参与者的反馈响应,就会中断事务。
-
1)发送中断请求
协调者向所有参与者节点发送abort请求。
-
2)事务回滚
参与者接受到abort请求后,会利用其在阶段二中记录的Undo信息来执行事务回滚操作,并在完成回滚之后释放在整个事务执行期间占用的资源。
-
3)反馈事务回滚结果
参与者在完成事务回滚之后,向协调者发送Ack消息。
-
4) 中断事务
协调者接受到所有参与者反馈的Ack消息后,中断事务。
优缺点
-
优点
降低了参与者的阻塞范围,能够在出现单点故障后继续达成一致
-
缺点
在去除阻塞的同时引入了新的问题。参与者接收到preCommit消息后,如果网络出现分区,此时协调者所在的节点和参与者无法进行正常的网络通信,参与者依然会进行事务的提交,这必然出现数据的不一致性。
3PC把2PC的第一阶段分拆成两个阶段,减少了阶段一对事务的阻塞时间。
阶段一can? -> 开启事务 -> 阶段2pre. -> 阶段3do! -> 提交事务;
整体感觉如:预占库存前,需要先校验单据状态、数据的有效性等。3PC像是把这一阶段的工作拆分成阶段一。后续阶段二理解为占用库存,阶段3为扣减或释放库存。
引用:
《从Paxos到Zookeeper分布式一致性原理与实践》
基于消息的分布式事务
ebay架构师发表的的文章:Base: An Acid Alternative。核心思想为:通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致性。实现手段为:将需要分布式系统执行的任务经过消息或日志的方式来异步执行。消息或日志能够存储到本地文件、数据库或消息队列,可以进行失败重试。
所以本方式的理论依据为:BASEBasically Available,Soft state,Eventually consistent
、WALWrite- Ahead Logging
。
基于消息的分布式事务模型主要有两种解决方案:
- 基于事务消息的解决方案;
- 基于本地消息的解决方案;
事务消息
[图片上传失败...(image-f3bde3-1653381325874)]
1)事务发起者预先发送事务消息。
2)MQ 收到事务性消息后,将消息持久化,将消息状态更新为“待发送”,并向发送方发送确认(ACK)消息。
3)如果事务发起者没有收到 ACK 消息,则取消本地事务的执行。如果事务发起者收到ACK消息,则执行本地事务,并发送另一条消息到MQ系统,通知本地事务的执行。
4)MQ 收到通知后,会根据本地事务的执行结果改变事务的消息状态。如果执行成功,MQ 将消息状态更改为“consumable”并将其交付给订阅者。如果执行失败,则删除该消息。
5)当执行本地事务时,发送到 MQ 的通知消息可能会丢失。因此,支持事务性消息的 MQ 具有定期的扫描逻辑。通过扫描,MQ 识别出处于“待发送”状态的消息,并向消息的发送者发起查询,以了解消息的最终状态。MQ 根据查询结果相应地更新消息状态。因此,事务的发起者需要为MQ系统提供查询事务消息状态的接口。
6)如果事务消息的状态是“准备发送”,MQ 将消息推送给下游参与者。如果推送失败,系统将继续重试。
7)收到消息后,下游参与者执行本地事务。如果本地事务执行成功,则向 MQ 系统发送 ACK 消息。如果执行失败,则不发送 ACK 消息。在这种情况下,MQ 会不断地向不返回 ACK 消息的下游参与者推送消息。
[图片上传失败...(image-5e53ee-1653381325874)]
客户端发送MQ事务消息,接受到ack后开始执行本地事务。客户端将本地事务执行结果通知MQ。
客户端要提供查询事务消息状态的接口,通知失败后MQ可以主动查询。
若成功,MQ将消息推送给下游,否则将消息丢弃。
如果推送失败,则不断重试,直到成功。
本地消息
事务发起者维护一个本地消息表。业务和本地消息表操作在同一个本地事务中执行。如果服务执行成功,本地消息表中也会记录一条状态为“待发送”的消息。系统启动定时任务,定期扫描本地消息表中处于“待发送”状态的消息发送给MQ。如果发送失败或超时,消息将被重新发送,直到发送成功。然后,该任务将从本地消息表中删除状态记录。后续的消费和订阅过程与事务性消息模式类似。
[图片上传失败...(image-b5e418-1653381325874)]
阶段一:消息的入队
开启事务写操作后,进行消息的发送或存储,全部成功后提交事务。
核心理念:让消息的入队操作与transaction的写操作通过一个本地事务处理。最简单的方式就是用一个消息表来存储这些消息。这个消息表和transaction在同一个数据库。
begin transaction
insert into transaction(xid, seller_id, buyer_id, amount);
//入队卖家数据更新消息,第一个字段是balance,第二个字段是id
queue message "update user("seller", seller_id, amount)";
//入队买家数据更新消息,第一个字段是balance,第二个字段是id
queue message "update user("buyer", buyer_id, amount)";
end transaction
阶段二:消息的出队
接收到消息后先进行唯一性/幂等校验。开启事务后存储消息并顺序执行事务写操作。
核心理念:
1、需要保证此阶段事务操作的幂等性;(引入update_applied表解决问题)
2、能够跟踪业务记录执行到哪个阶段。(引入update_applied表,幂等规避此问题。)
3、需要保证消息消费的顺序性(防止状态/数据回滚、乱序执行)。(update_applied引入版本号等)
for each message in queue
peek message
begin transaction
//如果当前消息在update_applied表中有记录,说明已经处理过了,否则没有处理过
select count(*) from update_applied as processed where trans_id = message.trans_id
and balance = message.balance and user_id = message.user_id;
if proccessed == 0
if message.balance == "seller"
//增加了last_purchase和trans_date的比较
update user set amt_sold = amt_sold + messgae.amount, last_purchase = message.trans_date
where id = message.seller_id and last_purchase < message.trans_date
else
//增加了last_sale和trans_date的比较
update user set amt_bought = amt_bought + message.amount, last_sale = message.trans_date
where id = message.buyer_id and last_sale < message.trans_date
end if
//插入记录,表示这条消息处理过了
insert into updates_applied(message.trans_id, message.balance, message.user_id);
end if
end transaction
if transaction successful
//如果上面的事务成功处理,则把消息从队列删除。如果这个操作失败也没关系,上面的事务已经变成幂等操作了
remove message from queue;
end if
end for
优缺点
-
优点
-
消息的出队阶段,引入了update_applied表,支持整个流程的重试/再次消费,保证幂等操作,保证了整个业务的最终一致性。
-
在update_applied表中,增加了last_purchase字段(版本号机制/乐观锁方案),防止消息乱序执行。
-
相对容易实施,因为它对业务的干扰较小,并且对 MQ 系统的要求不高。
-
-
缺点
-
适用于对最终一致性敏感度不高、业务路径短的场景,如系统间跨平台、跨企业的业务交互。
-
倾向于子事务最终会成功,通常不支持回滚。
-
引用:
XA规范
最早的分布式事务产品可能是 AT&T 在 1980 年代推出的 Tuxedo(Transactions for UNIX,Extended for Distributed Operations)。Tuxedo 最初是作为电信领域 OLTP 系统的分布式事务中间件开发的。后来标准化组织X/Open采用了Tuxedo的设计和一些接口,引入了分布式事务规范,XA规范。
四个核心角色
XA 规范定义了一个分布式事务处理模型,包括四个核心角色:
- RM(Resource Manager):提供数据操作和管理的接口,保证数据的一致性和完整性。数据库管理系统是最具代表性的 RM 实例。一些文件系统和消息队列 (MQ) 系统也可以被视为 RM 实例。
- TM(事务管理器):作为协调器,协调所有与跨数据库事务关联的 RM 的行为。
- 应用程序(AP):根据业务规则调用RM接口修改业务模型数据。如果一个数据的变化涉及到多个 RM 并且必须保护事务的一致性和完整性,AP 通过一个 TM 定义一个事务的边界,它负责协调事务中涉及的 RM 来完成一个全局事务。
- 通信资源管理器(CRM):负责跨服务传输事务。
下图显示了 XA 规范中定义的事务模型。发起分布式事务的TM实例称为根节点,其他TM实例统称为事务参与者。发起者启动一个全局事务,事务参与者运行自己的事务分支。如果一个 TM 实例向其他 TM 实例发起服务调用,则发起者为上级节点,而被调用的实例为下级节点。
[图片上传失败...(image-af89ea-1653381325874)]
事务处理过程
在 XA 规范中,分布式事务建立在 RM 的本地事务之上,然后被视为分支事务。TM 负责协调这些分支事务并确保它们全部成功提交或全部回滚。XA 规范将分布式事务处理过程分为以下两个阶段,因此也称为两阶段提交协议:
1)准备阶段
TM 记录事务启动并查询每个 RM 是否准备好执行准备操作。
RM 收到订单后,会评估自己的状态,并尝试为本地事务执行准备操作,例如预留资源、锁定资源和执行操作。然后,RM 等待来自 TM 的后续订单而不提交事务。如果前面的尝试失败,RM 会通知 TM 该阶段的执行失败并回滚已执行的操作。然后,RM 不再参与该交易。例如,MySQL 会在这个阶段锁定资源并写入 redo 和 undo 日志。
TM 收集 RM 的响应并将事务准备记录为已完成。
2) 提交或回滚阶段
本阶段根据前一阶段的协调结果发起事务提交或回滚操作。
如果所有 RM 在上一步中都响应成功,则执行以下操作:
- TM 将事务记录为已提交并向所有 RM 发出事务提交顺序。
- RM 收到订单后,提交事务,释放资源,并以“提交完成”响应 TM。
- 如果 TM 收到所有这些 RM 的响应,它会将事务记录为已完成。
如果任何 RM 在上一步中响应执行失败或没有及时响应,则 TM 将事务视为失败。然后,将执行以下操作:
- TM 将事务记录为中止,并向所有 RM 发出事务回滚命令。
- RM收到订单后,回滚事务,释放资源,回复TM“回滚完成”。
- 如果 TM 收到所有这些 RM 的响应,它会将事务记录为已完成。
[图片上传失败...(image-dc8241-1653381325874)]
XA 规范还定义了以下优化措施:
- 如果 TM 发现整个事务中只涉及到一个 RM,整个过程就会降级为一阶段提交。
- 如果 RM 接收到的来自 AP 的数据操作是只读操作,则 RM 可以在第一阶段完成事务,并通知 TM 不再需要第二阶段。如果是这种情况,可能会发生脏读。
- 如果RM在第一阶段完成后很久没有收到进入第二阶段的命令,它可以自行提交或回滚本地事务。这种情况称为启发式完成。请注意,此问题可能会破坏事务的一致性并导致异常。
XA 规范详细定义了核心组件之间的交互接口。以 TM 与 RM 的交互接口为例。下图是一个完整的全局事务,其中 TM 和 RM 的交互非常频繁。
[图片上传失败...(image-78b9a0-1653381325874)]
异常处理
交易执行期间可能会发生错误和网络超时。对于这些异常,不同的实现可能会有不同的异常处理方法,如下所述。
- 如果 TM 在第一阶段查询 RM 之前遇到停机,则从停机恢复后将不需要任何操作。
- 如果 TM 在第一阶段查询 RM 后遇到宕机,由于部分 RM 可能已经收到查询,因此需要向这些 RM 发送回滚请求。
- 如果 TM 在第一阶段查询 RM 之后但在将准备操作记录为已完成之前遇到停机,则需要在停机恢复后向 RM 发送回滚请求,因为 TM 不知道停机前的协商结果。
- 如果 TM 在将事务准备记录为在第一阶段完成后遇到停机时间,则可以在从停机时间恢复后根据生成的日志发出提交或回滚命令。
- 如果 TM 在第二阶段生成 commit 或 abort log 之前遇到 downtime,则可以根据 downtime 恢复后的 log 发出 commit 或 rollback 命令。
- 如果 TM 在将事务记录为在第二阶段完成之前遇到停机时间,则可以在从停机时间恢复后根据生成的日志发出提交或回滚命令。
- 如果 TM 在将事务记录为在第二阶段完成后遇到停机,则从停机恢复后将不需要任何操作。
- 当任何 RM 在第一阶段没有及时响应时,TM 会向所有 RM 发出回滚命令。
- 当任何 RM 在第二阶段没有及时响应时,TM 会不断向未响应的 RM 发出回滚命令。
这段有点像Saga Log的策略
优缺点
优点:
XA 规范是最早的分布式事务规范。Oracle、MySQL、SQL Server等主流数据库产品都支持XA规范。请注意,J2EE 中的 JTA 规范也是基于 XA 规范,因此与 XA 规范兼容。
XA 是在资源管理层实现的分布式事务模型。它的特点是对企业的侵入程度低。
缺点:
XA 的两阶段提交协议可以覆盖分布式事务的三种场景。但是,RM 在执行全局事务期间会一直锁定资源。如果事务涉及的 RM 过多,特别是在跨业务场景下,网络通信的数量和时间消耗会迅速增加。从而导致阻塞时间延长,系统吞吐量下降,事务死锁概率增加。因此,两阶段提交协议不适用于微服务场景下的跨服务分布式事务模式。
每个 TM 领域都会创建一个单点,这可能会导致单点故障。如果 TM 在第一阶段之后崩溃,参与的 RM 将不会收到第二阶段的订单,因此会长时间持有资源锁。因此,这会影响业务的吞吐量。另一方面,在一个完整的全局事务中,TM 与 RM 交互 8 次,导致复杂性和性能下降。
此外,两阶段协议可能会导致脑裂异常。如果 TM 在第二阶段指示 RM 提交事务后出现故障,并且只有部分 RM 收到提交顺序,那么当 TM 恢复时,它无法协调所有 RM 维护本地事务的一致性。
XA 必须处理许多异常情况,这对框架的实现具有挑战性。关于开源实现,可以参考 Atomikos 和 Bitronix。
针对两阶段提交协议中存在的问题,提出了一种改进的三阶段提交方案。这种新的解决方案消除了单点故障 (SPOF),并为 RM 添加了超时机制,以避免长期锁定资源。然而,三相解决方案无法解决裂脑问题,很少应用于实际案例。如果您对此解决方案感兴趣,可以阅读相关资料。
TCC
SAGA
https://zhuanlan.zhihu.com/p/95852045
https://baijiahao.baidu.com/s?id=1709259416203967205&wfr=spider&for=pc
https://opentalk.upyun.com/310.html
https://blog.csdn.net/qq_42046105/article/details/114985125
1、1987年发表的Sagas论文Hector & Kenneth - Sagas _ Department_of_Computer_Science。
2、2015年发布的Sagas论文Caitie,McCaffrey - Distributed Sagas
3、Chris Richardson发表的Sagas文章:Pattern:Saga
Saga为解决可能会长时间运行的分布式事务(Long-Lived Transaction长活事务)问题。将长活事务分解成可以交错运行的子事务集合。其中每个子事务都是一个保持数据库一致性的真实事务。属于是一种纯业务补偿模式。当一个服务失败的时候,其所有依赖的上游服务都进行业务补偿操作。
Saga中的事务相关联,作为非原子单位执行。任何未完成执行的Saga是不满足要求。如果发生,必须得到补偿,要修正未完成执行的部分。每个Saga子事务应提供对应的补偿事务。
Saga事务恢复策略
理论上,补偿事务永不失败。但是在复杂的真实生产环境中,服务器可能会宕机,网络会抖动,甚至数据中心会停电。这种情况下,最后的手段是提供回退措施,比如人工干预。
向前恢复 forward recovery
重试失败的事务,假设每个子事务最终都会成功。适用于必须要成功的场景。不需要提供补偿事务。在某种情况下更符合我们的需求。
T1,T2,...,Tj(失败),Tj(重试),...,Tn
向后恢复 backward recovery
如果任一子事务失败,补偿所有已完成的事务。这样最终撤销掉之前所有成功的sub-transaction,使得整个Saga的执行结果撤销。
T1, T2, ..., Tj, Cj,..., C2, C1,其中0 < j < n
Saga的使用条件
- Saga只允许两个层次的嵌套,顶级的Saga和简单子事务;
- 在外层,全原子性不能得到满足。sagas可能会看到其他sagas的部分结果。
- 每个子事务应该是独立的原子行为。
- 在我们的业务场景下,航班预订、租车、酒店预订等是自然独立的行为,而且每个事务都可以用对应服务的数据库保证原子操作。
补偿也有需要考虑的事项:
- 补偿事务从语义角度撤销了事务Ti的行为,但未必能将数据库返回到执行Ti时的状态(例如事务触发导弹发射,则可能无法撤销此操作)。
但是这对我们的业务来说不是问题。难以撤销的行为也可能被补偿。例如发送电邮的事务可以通过发送解释问题的另一封电邮来补偿。
Saga Log
Saga保证所有的子事务都得以完成或补偿,但Saga系统本身也会崩毁。其崩溃时可能处于下面几个状态:
-
尚未开始
Saga收到事务请求,但尚未开始。因子事务对应的微服务状态未被Saga修改,我们什么也不需要做。
-
部分子事务已完成
一些子事务已经完成。重启后,必须接着上次完成的事务恢复。
-
部分子事务已开始,但未完成
子事务已开始但未完成。此时不确定远程服务是否已完成事务。或许事务失败,或许服务超时。saga只能重新发起之前未确认完成子事务。这意味着子事务必须幂等。
-
子事务失败,补偿未开始
saga必须在重启后执行对应的补偿事务。
-
子事务失败,补偿未完成
解决方案与上面相同,表名补偿事务也必须是幂等。
-
所有的子事务或补偿均已完成
不需做任何动作。
为了恢复到上面状态,我们需要追踪子事务及补偿事务的每一步。可以通过事件的方式达到以上要求,并将事件保存在名为saga log的持久存储中:
- saga started event:保存整个saga请求,其中包括多个事务/补偿请求;
- transaction start event:保存对应事务请求;
- transaction ended event:保存对应事务请求及其回复;
- transaction aborted event:保存对应事务请求和失败的原因;
- transaction compensated event:保存对应补偿请求及回复;
- saga ended event:标志saga事务请求的结束,不需要保存任何内容。
[图片上传失败...(image-43e6d-1653381325874)]
通过将这些事件持久化到saga中,我们可以将saga恢复到上述任何状态。
Saga分布式事务协调
阿里巴巴的开源项目 Seata和华为的开源项目 ServiceComb 都支持 Saga。
参考
2、
网友评论