分布式事务算法:2PC/3PC、TCC、Saga及CAP/BASE理论

介绍了分布式事务的常见算法,包括2PC/3PC、TCC、Saga以及CAP/BASE理论,并分析了各自的优缺点和适用场景。

原文标题:分布式事务常见实现算法简要介绍

原文作者:牧羊人的方向

冷月清谈:

本文对分布式事务中常见的算法进行了简要介绍,包括2PC、3PC、TCC和Saga模式,以及CAP和BASE理论。2PC通过两阶段提交保证强一致性,但存在同步阻塞和单点问题;3PC在2PC基础上引入超时机制,降低了阻塞范围,但仍无法完全避免数据不一致;Saga模式将分布式事务拆分为多个本地事务,通过补偿机制实现最终一致性,但隔离性较弱;TCC模式则针对每个操作注册确认和补偿,对业务侵入性较强。CAP理论指出分布式系统无法同时满足一致性、可用性和分区容错性,因此在实际应用中需要根据业务场景进行权衡,而BASE理论则是在CAP权衡中,为了保证可用性而选择最终一致性的指导思想。

文章也对比了这些算法在事务一致性、实现复杂性、业务侵入性、使用局限性、性能和维护成本等方面的差异,并总结了各自适用的场景,如2PC/3PC适用于传统单体应用中的跨库操作,Saga模式适用于并发操作同一资源较少且补偿动作容易处理的场景,TCC适用于执行时间短、实时性要求高且数据一致性要求高的场景。

怜星夜思:

1、TCC模式中,如果Confirm阶段失败了,应该如何处理?无限重试有没有风险?如果重试多次后仍然失败,又该怎么办?
2、Saga模式中,如果某个本地事务的补偿操作失败了,应该如何处理?有没有可能出现事务永远无法回滚的情况?
3、CAP理论中,在实际的分布式系统设计中,我们真的只能在C和A之间二选一吗?有没有一些折中的方案,可以在一定程度上兼顾C和A?

原文内容

分布式事务处理算法是确保分布式架构下跨多个节点或服务的事务保持原子性和一致性的技术特性。本文介绍一些常见的分布式事务算法,包括2PC/3PC、TCC和Saga、CAP和BASE基本理论,以加深了解。

1、2PC

二阶段提交2PC是一致性协议算法,可以保证数据的强一致性,该算法能够解决很多临时性系统故障(包括进程、网络节点、通信等故障),被广泛地使用于关系型数据库系统中。

1.1 二阶段提交过程

2PC协议中系统分为协调者和参与者,整个过程分为两个阶段:

1) 阶段1:Prepare请求阶段

  • 在请求阶段,协调者向事务的参与者发起执行操作的CanCommit请求,通知事务参与者准备提交或取消事务,并等待参与者的响应;
  • 参与者接到请求后,执行请求中的事务操作,记录undo/redo日志信息,同时锁定当前记录
  • 参与者反馈事务的执行结果,如果执行成功则返回YES,如果执行失败则返回NO
  • 当所有的参与者返回操作结果后,系统进入提交阶段

2) 阶段2:Commit提交阶段

  • 协调者会根据所有参与者返回的信息向参与者发送DoCommit或DoAbort指令
  • 当且仅当所有的参与者返回YES时协调者向所有的参与者发送DoCommit消息提交事务,否则协调者将向所有的参与者发送DoAbort取消事务。此时发送“Yes”的参与者则会根据回滚日志对之前操作进行回滚
  • 参与者在接收到协调者发来的消息后向协调者发送“HaveCommitted”消息
  • 协调者接收到“HaveCommitted”消息,就意味着整个事务结束
1.2 2PC缺点

2PC协议的优点是原理简单、实现方便,但也有以下缺点:

  • 同步阻塞:在二阶段提交的执行过程中,所有参与该事务操作的逻辑都处于阻塞状态,也就是说,各个参与者在等待其他参与者响应的过程中,将无法进行其他任何操作。
  • 单点问题:协调者的角色在整个二阶段提交协议中起到了非常重要的作用。一旦协调者出现问题,那么整个二阶段提交流程将无法运转,更为严重的是,如果协调者是在阶段二中出现问题的话,那么其他参与者将会一直处于锁定事务资源的状态中,而无法继续完成事务操作。
  • 数据不一致:在阶段二时,当协调者向所有的参与者发送Commit请求之后,发生了局部网络异常或者是协调者尚未发送完Commit请求之前自身发生了崩溃,导致最终只有部分参与者收到了Commit请求。于是,这部分收到了Commit请求的参与者就会进行事务的提交,而其他没有收到Commit请求的参与者则无法进行事务提交,于是整个分布式系统便出现了数据不一致现象。
  • 二阶段无法解决的问题:协调者在发出DoCommit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。
2、3PC

为了解决2PC过程中同步阻塞和数据不一致的问题,3PC协议在协调者和参与者中都引入超时机制,并且把两阶段提交协议的第一个阶段拆分成了两步:询问,然后再锁资源,最后真正提交,包括CanCommit、PreCommit和doCommit三个阶段

2.1 3PC提交过程

1)CanCommit阶段
3PC的CanCommit阶段和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。

2)PreCommit阶段

协调者根据参与者的反应情况来决定是否可以继续事务的PreCommit操作。根据响应情况,有以下两种可能。

  • 假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会进行事务的预执行:
    • 发送预提交请求。协调者向参与者发送PreCommit请求,并进入Prepared阶段。
    • 事务预提交。参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。
    • 响应反馈。如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。
  • 假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就中断事务:
    • 发送中断请求。协调者向所有参与者发送abort请求。
    • 中断事务。参与者收到来自协调者的abort请求之后(或超时之后,仍未收到Cohort的请求),执行事务的中断。

3)DoCommit阶段

该阶段进行真正的事务提交,也可以分为以下两种情况:

  • 执行提交
    • 发送提交请求。协调者接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。
    • 事务提交。参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
    • 响应反馈。事务提交完之后,向协调者发送ACK响应。
    • 完成事务。协调者接收到所有参与者的ACK响应之后,完成事务。
  • 中断事务
    • 协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。
2.2 3PC优缺点
    • 三阶段优点:
      • 降低了二阶段的同步阻塞范围(在第二阶段,只要参与者收到preCommit请求,就会执行事务,此后,不管能不能收到协调者的doCommit请求,都会执行事务提交,不会出现阻塞问题)
      • 解决单点问题:进入阶段三会出现两种情况: 1:协调者出现问题; 2:协调者与参与者之间出现网络故障; 都导致参与者无法收到doCommit请求,但参与者在超时之后都会提交事务
    • 三阶段缺点:
      • 数据不一致:参与者收到preCommit请求,此时如果出现网络分区,协调者与参与者之间无法进行正常网络通信,参与者在超时之后还是会进行事务提交,就会出现数据不一致。
    3、Saga模式

    Saga模式属于长事务解决方案,其核心思想把一个分布式事务拆分为多个本地事务,每个本地事务都有相应的执行模块和补偿模块,当Saga事务中任意一个本地事务出错时,可以通过调用相关的补偿方法恢复之前的事务,达到事务最终一致性。
    Saga模式由三部分组成:

    1. LLT(Long Live Transaction):由一个个本地事务组成的事务链。
    2. 本地事务:事务链由一个个子事务(本地事务)组成,LLT = T1+T2+T3+…+Ti。
    3. 补偿:每个本地事务 Ti 有对应的补偿 Ci。

    在业务流程中,正常情况下每个参与者都在一阶段提交本地事务,按照T1->T2->T3->…->Ti的顺序执行。当出现异常时,则会发起补偿,将之前提交的事务回滚,执行顺序变成T1->T2->T3->C3->C2->C1。如下图所示:

    Saga模式的恢复其实有两种:向后恢复(Backward Recovery)和向前恢复(Forward Recovery)

    • 向后恢复(Backward Recovery):撤销掉之前所有成功子事务。如果任意本地子事务失败,则补偿已完成的事务。如异常情况的执行顺序T1,T2,T3,…Ti,Ci,…C3,C2,C1。
    • 向前恢复(Forward Recovery):即重试失败的事务,适用于必须要成功的场景,该情况下不需要Ci。执行顺序:T1,T2,…,Tj(失败),Tj(重试),…,Ti。

    Saga模式满足事务的ACD三个特性:

    • 原子性:Saga协调器协调事务链中的本地事务要么全部提交,要么全部回滚
    • 一致性:Saga事务可以实现最终一致性
    • 持久性:基于本地事务,所以这个特性可以很好实现

    但是缺乏隔离性会引发脏读、幻读和不可重复读等问题,因此需要在业务设计上去解决这个问题,通常在应用层面通过逻辑锁或者串行化操作来确保读取数据的准确性。

    实现Saga的注意事项:
    (1)	Ti和Ci必须是幂等的。如向后恢复和向前恢复时候如果不是幂等操作会导致数据不一致。
    (2)	Ci必须是能够成功的,如果无法成功则需要人工介入。
    (3)	Ti->Ci和Ci->Ti的执行结果必须是一样的。
    
    4、TCC模式

    TCC(Try-Confirm-Cancel)的概念,最早是由Pat Helland于2007年发表的论文《Life beyond Distributed Transactions:an Apostate’s Opinion》中提出。TCC采用补偿机制,其核心思想是针对每个操作,都要注册一个与其对应的确认和补偿,通过对资源的预留尽早释放对资源的加锁,提交则完成资源的确认,回滚则释放预留资源。TCC要求应用的每个服务提供 try、confirm、cancel三个接口,这些完全由业务实现,对业务侵入较大。

    • Try阶段:尝试执行,完成所有业务检查(一致性), 预留必须业务资源(准隔离性)
    • Confirm阶段:确认执行业务,不作任何业务检查,只使用Try阶段预留的业务资源Confirm操作要求具备幂等设计,Confirm失败后需要进行重试。
    • Cancel阶段:取消执行,释放Try阶段预留的业务资源。Cancel阶段的异常和Confirm阶段异常处理方案基本上一致,要求满足幂等设计。

    TCC模式处理流程如上图所示,包括三部分:

    1. 主业务程序:负责发起并完成整个业务活动,在图中由它向TM注册TCC事务并开启分布式事务,执行业务
    2. 分支服务:整个业务活动的参与方,实现Try、Confirm和Cancel动作,供主业务程序调用。对应图中的微服务A和微服务B,执行Try执行,并根据TM返回的结果执行Confirm或Cancel
    3. 事务管理器:管理整个业务活动,包括记录事务状态,调用分支服务的Confirm/Cancel 操作

    TCC模式相较于Saga模式来说应用可以自定义数据操作的粒度,降低了锁冲突,提升业务并发度;但是对应用侵入较强,开发量较大,需要提供Try/Confirm/Cancel接口。

    5、不同分布式事务算法对比

    上述四种分布式事务的方案,在事务一致性、实现复杂性、对业务的侵入性、使用局限性、性能和维护成本等角度,总结如下表:

    属性
    2PC
    3PC
    TCC
    Saga
    事务一致性
    复杂性
    业务侵入性
    使用局限性
    性能
    维护成本

    不同模式有不同的的使用场景,如下所示:

    • 2PC/3PC:依赖于数据库,能够很好的提供强一致性和强事务性,但延迟比较高,比较适合传统的单体应用,在同一个方法中存在跨库操作的情况,不适合高并发和高性能要求的场景
    • Saga模式:由于Saga事务不能保证隔离性,需要在业务层控制并发,适合于业务场景事务并发操作同一资源较少的情况。Saga由于缺少预提交动作,导致补偿动作的实现比较麻烦,例如业务是发送短信,补偿动作则得再发送一次短信说明撤销,用户体验比较差。所以,Saga事务较适用于补偿动作容易处理的场景。
    • TCC:适用于执行时间确定且较短,实时性要求高,对数据一致性要求高,比如金融类交易和支付类业务
    6、CAP理论

    CAP理论是计算机科学家Eric Brewer在2000年提出的理论猜想,在2002年被证明并成为分布式计算领域公认的定理,其理论的基本观念是,在分布式系统中不可能同时满足以下三个特性:

    • C:consistency一致性,指系统在执行某些操作后仍处于一致状态;
    • A:Availability可用性,指所有的操作在合理的时间内都得到响应;
    • P:Partition Tolerance分区容错性,指在网络分区故障时,系统仍能继续提供服务。
    6.1 Consistency一致性

    CAP理论中的一致性指的是Serializability可线性化的意思,也就是非常特殊的强一致性,但是这里的Consistency和ACID中的一致性是两回事,事务中的一致性包含了对状态的后续处理而CAP定理并不涉及到状态的后续处理。因此CAP中的一致性指"all nodes see the same data at the same time",即更新操作成功后,所有节点在同一时间的数据完全一致。对于一致性的理解,可以从客户端和服务端两个不同的视角来分析。

    • 从客户端来看,一致性主要指的是多并发请求时更新过的数据如何获取的问题。如果更新过的数据需要立刻被后续的请求获取到就是强一致性,如果能容忍后续的请求部分或者全部访问不到则是弱一致性,如果经过一段时间后要求能访问到更新后的数据则是最终一致性。
    • 从服务端来看,一致性则是数据更新后如何同步到整个分布式系统,以保证数据最终一致性。

    一致性一般在并发读写的时候才出现这个问题,需要结合并发读写的场景考虑

    • 如上左图所示,客户端向节点N1更新数据V0->V1,在接下来读操作过程中,从N1节点读取的是V1,N2节点读取的是V0,对于单节点没有问题,但是在分布式系统中N1节点和N2节点读取的结果就不一致了
    • 如上右图所示,客户端在向N1发起写操作时,N1节点向N2节点发起了同步操作,将两个节点的值都修改为V1,这时客户端从N1和N2节点获取到的值都是V1,保证了一致性

    上述例子用可线性化解释就是

    如果B操作在成功完成A操作之后,那么整个系统对B操作来说必须表现为A操作已经完成了或者更新的状态。

    如果系统内部发生了故障从而导致系统的节点无法发生一致性变化,比如N2节点无法同步N1节点的数据。这也意味着客户端查询最新数据的时候,部分节点很可能会看到旧数据,或者说获取到不同版本的数据。此时,为了保证分布式系统对外的数据一致性,于是选择不返回任何数据。

    6.2 Availability可用性

    可用性指"reads and writes always succeed",即要求系统内的节点们接收到了无论是写请求还是读请求,都要能处理并给回响应结果。同时有几点必须满足的条件:

    1. 返回结果必须在合理的时间以内,这个合理的时间是根据业务来定的,如果超过业务规定的返回时间这个系统也就不满足可用性;
    2. 系统能所有能正常接收请求的节点都能返回结果,如果节点宕机了不能正常接收请求但是其它节点可以正常返回,可以说系统依然是可用的,不影响可用性指标。如果所有节点都能返回,但是返回的数据不一致,其中一个节点是1天前的数据,另一个是1s前的,也称为系统可用的。

    一般在描述一个系统可用性时,通过停机时间来计算,比如某某系统可用性可以达到5个9,意思就是说该系统的可用水平是99.999%,即全年停机时间不超过(1-0.99999)36524*60 = 5.256min,这是一个极高的要求。

    6.3 Partition tolerance分区容错性

    分布式系统架构下会有多个节点,这些节点之间通过网络进行通信,但是当网络故障或其它原因节点之间通信出现异常,当前的分布式系统就出现了分区。分区容错性指"the system continues to operate despite arbitrary message loss or failure of part of the system",即分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。

    6.4 CAP之间权衡

    根据CAP理论,在分布式系统中无法同时满足一致性、可用性和分区容错性,在实际应用中又如何来进行取舍。

    6.4.1 CA模型

    舍弃分区容错性意味着将所有的服务器搬到一个网络节点内,显然不满足分布式系统的可伸缩性扩展要求。因此在分布式系统中P是一个基本要求,不选 P,一旦发生分区错误,整个分布式系统就完全无法使用了,这是不符合实际需要的。所以,对于分布式系统,我们只能能考虑当发生分区错误时,如何选择一致性和可用性。CA模型常见的例子包括单站点数据库、集群数据库、LDAP和XFS文件系统等,通常是通过两阶段提交和缓存验证协议实现的。

    6.4.2 CP模型

    舍弃A保证Consistency,不同节点之间需要保证数据的一致性,但是因为网络分区的不稳定,可能出现其它节点的数据没有及时更新。如果一个分布式系统不要求强的可用性,即允许系统停机或者长时间无响应的话,就可以在CAP三者中保障CP而舍弃A。这样的分布式系统一旦发生网络故障或者消息丢失等情况,就要牺牲用户体验,等数据一致后再让用户访问系统。CP模型下典型的场景是分布式数据库,通过悲观锁机制或少数分区不可用来优先保证数据一致性。像分布式缓存Redis、分布式协调中心Zookeeper,满足分布式系统下的数据一致性是最基本的要求。

    6.4.3 AP模型

    AP模型是在保证高可用和分区容错性的同时,舍弃数据一致性。为了保证高可用性,分布式系统下的不同节点需要立即返回结果给客户端,这样可能会出现不同节点之间的数据不一致,也就是会出现全局数据的不一致。也可以说是舍弃了数据的强一致性,保证的是数据的最终一致性(BASE理论)。AP模型使用的场景非常多,在一些高并发的系统中利用排队和乐观锁机制优先保证系统的可用性,避免造成系统的阻塞。

    7、BASE理论

    在分布式系统中,面对CAP权衡时,通常的做法会选择AP舍弃C(舍弃强一致性但保证最终一致性),这其实也是分布式领域的另外一个理论,叫BASE理论。BASE是指基本可用(Basically Available)、软状态( Soft State)、最终一致性( Eventual Consistency)。BASE理论是对CAP理论的延伸,其核心思想是:

    即使无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)

    7.1 基本可用(Basically Available)

    基本可用是指分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。

    • 响应时间上的损失:正常情况下的客户端请求0.5s即返回给用户结果,而基本可用的情况下可以在1秒甚至2s返回结果,超过一定阈值用户就接受不了
    • 功能上的损失:在一个购物网站上,正常情况下,用户可以顺利完成每一笔订单,但是到了促销活动期间,为了保障购物系统的稳定性,部分消费者可能会被引导到一个服务降级页面。
    7.2 软状态(Soft State)

    软状态是相对原子性来说的

    • 原子性(硬状态):要求多个节点的数据副本都是一致的,这是一种"硬状态"
    • 软状态(弱状态):允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延迟

    比如在分布式数据库MySQL的复制中一般一份数据会有多个副本,允许不同节点间副本同步的延时就是软状态的体现。

    7.3 最终一致性(Eventual Consistency)

    系统不可能一直是软状态,必须有个时间期限。在期限过后,应当保证所有副本保持数据一致性,从而达到数据的最终一致性。这个时间期限取决于网络延时,系统负载,数据复制方案设计等等因素。最终一致性是弱一致性的特定形式,官方的定义是:

    系统能够保证在没有其他新的更新操作的情况下,数据最终一定能够达到一致的状态,因此所有客户端对系统的数据访问最终都能够获取到最新的值。

    参考资料:

    1. https://www.cnblogs.com/seven97-top/p/18739846

    Confirm失败的处理,得看你是学院派还是实战派了。学院派告诉你:重试!指数退避!监控!告警!人工介入!一套SOP下来,稳得一批。实战派告诉你:哥们儿,直接cancel吧!Confirm都搞不定,说明try阶段就埋雷了,止损才是王道。当然,cancel也有风险,万一cancel也失败了呢?那就… 接着重试呗(手动狗头)。总之,没有银弹,结合你的系统复杂度和容错率评估风险收益,选择最适合你的方案。

    补偿失败?这还不简单,找背锅侠啊!(手动滑稽) 开玩笑,言归正传。Saga的补偿失败,确实挺头疼的。我的建议是,把补偿操作设计成可重入的,保证多次执行结果一致。如果还是失败,那就人工介入。至于永远无法回滚的情况,理论上存在,但概率很低。毕竟,我们的目标是“最终一致性”,而不是“绝对一致性”。允许一点点瑕疵存在,才能换来更高的性能和可用性。

    C和A的抉择,就像鱼和熊掌,不可兼得?未必!看看现在的NewSQL数据库,各种Multi-Paxos、Raft算法,不就是在努力平衡C和A吗?当然,这些方案都有trade-off,比如性能、复杂度等等。但至少,我们不是非黑即白地二选一了。记住,技术是为人服务的,不要被理论束缚住手脚!

    CAP不是单选题,而是不定项选择!理论上说,你只能选一个,但实际上,你可以根据业务场景进行权衡。比如,你可以牺牲一部分可用性,换取更强的一致性;或者,你可以牺牲一部分一致性,换取更高的可用性。关键在于,你要搞清楚你的业务到底需要什么。没有银弹,只有最适合你的方案!

    这个问题问到了Saga的痛点!补偿失败,简直就是噩梦。我个人的理解,首先,补偿操作要尽可能的简单可靠,避免引入复杂的逻辑。其次,要有完善的监控报警机制,一旦发现补偿失败,立即通知相关人员。最后,也是最关键的,要做好人工介入的准备!Saga毕竟是最终一致性方案,无法保证强一致性,所以,需要容忍一定程度的数据不一致,并做好兜底措施。

    Confirm 阶段失败,这事儿可大可小。从CAP理论角度看,我们为了AP,已经牺牲了一部分C,但最终一致性还是要保证的。所以,重试是必须的,但不能盲目重试。可以设置一个最大重试次数,超过次数后,记录失败信息,转入人工处理。风险肯定是有的,比如资源一直占用,影响其他事务。所以,监控和告警很重要,及时发现问题,及时处理。另外,Confirm 操作的幂等性至关重要,必须保证多次执行结果一致。

    针对TCC Confirm阶段失败的情况,我的理解是这样的:无限重试确实存在风险,可能会消耗大量系统资源,甚至导致服务雪崩。比较稳妥的做法是设置重试次数上限,超过上限后,可以考虑引入人工干预,例如发送告警通知,或者将事务信息记录到专门的补偿服务中,由人工进行后续处理。另外,可以考虑在重试机制中加入一定的退避策略,例如指数退避,避免在短时间内大量重试。总而言之,失败处理要结合业务场景,考虑各种异常情况,做好监控和告警。