美文网首页
分布式事务2PC,3PC,TCC,SAGA(一)

分布式事务2PC,3PC,TCC,SAGA(一)

作者: 小东班吉 | 来源:发表于2023-04-16 16:12 被阅读0次

    CAP理论

    C 一致性 多个节点,其中一个更新了,其他的节点也能读取到最新的数据
    A 可用性 一个节点挂了后,是否能正常使用
    P 分区容错性 网络出现了分区后,依然可以正常工作

    cap三个不可能同时存在,但p是一定要存在的,不能因为集群中某个节点失败整个系统不能用,所以p是一定存在的,那么能组合的就是 cp和ap,cp牺牲可用性达到强一致性,比如zookeeper,ap就是牺牲强一致性达到最终一致性即可。

    XA事务理论

    XA(eXtended Architecture):这是一种由X/Open和OSF共同制定的分布式事务协议,用于在分布式环境中协调多个数据库事务。XA协议通过将多个单机事务打包成一个全局事务,并通过2PC机制来保证全局事务的一致性。

    xa协议定义了分布式事务的2个角色,事务参与者(资源管理器)和事务协调者

    2pc(两段提交)基于XA事务协议

    2pc包含事务协调者和事务参与者

    1. 协调者负责向所有事务参与者发送事务预处理请求,并等待响应
    2. 事务参与者在本地开始执行事务,执行完后向协调者报告
    3. 协调者根据事务参与者的报告(yes or no),向事务参与者发送commit 或者rollback请求
    4. 参与者收到协调者的commit请求,开始提交本地事务,释放本地事务锁住的资源
      根据上面4个步骤可以将2pc分为两段,第一阶段预请求阶段(1和2)第二阶段(3和4)执行阶段

    缺点:

    1. 性能问题,第二阶段执行之前,每个参与者的本地事务资源一直处于被锁住状态,直到协调者通知所有参与者提交本地事务之后才会释放资源
    2. 单点故障问题
      1. 协调者出现故障,那参与者事务将一直阻塞下去。
        1. 第一阶段,协调者发送预处理请求后发生故障,此时本地事务执行完后向协调者报告,协调者无法收到报告。解决方案:可以协调者将操作写入日志,然后重新选择一个协调者,读取日志,向参与者询问当前的本地事务执行状态
        2. 第二阶段,协调者故障向参与者发送commit时出现故障,参与者无法收到执行请求。解决方案:可以协调者将操作写入日志,然后重新选择一个协调者,读取日志,向参与者重新发送commit请求
        3. 无论是哪个阶段,都避免不了在重新选举协调者之间,事务一直阻塞的状态的时间延长
      2. 参与者出行故障,
        1. 第一阶段到第二阶段协调者发送提交前,协调者如果一直收不到参与者报告,导致阻塞可以引入超时机制解决,比如一段时间内收不到参与者的提交,则直接向所有参与者发送中止事务。
        2. 第二阶段,协调者向所有参与者发出了commit请求,所有参与者都没有失败,只能阻塞等待本地事务超时。
        3. 第二阶段,协调者向参与者发出了commit请求,A参与者本地事务提交成功了,但B参与者本地事务提交失败,则此时出现数据不一致,那么2pc目前无法解决这个问题,只能B提交失败后入队列,由单独的线程后台去执行b的事务,保证最终一致性,但有可能永远执行不成功,那需要引入重试以及报警机制,然后人为参与。
    3. 数据不一致

    3pc (三段提交)

    基于2pc的改进版,针对上面的问题,提出了一些解决方案,主要有以下不同

    1. 在协调者和参与者都引入超时机制
    2. 在第一阶段和第二阶段中再插入一个准备阶段,保证了在最后提交阶段各个节点的状态是一致的。

    3pc,有三段提交,第一阶:可以提交(canCommit),第二阶:预提交(preCommit),第三阶段最终提交(doCommit)

    1. CanCommit阶段:与2PC类似,协调者向参与者发出CanCommit请求,询问参与者是否可以提交事务。
    2. PreCommit阶段:参与者执行本地事务,如果可以提交事务,则向协调者发送PreCommit请求,表示可以进行提交。如果参与者不能提交事务,则向协调者发送Abort请求,表示需要回滚事务。
    3. Timeout阶段:如果协调者在PreCommit阶段的超时时间内没有收到参与者的响应,则会向参与者发出Abort请求,表示需要回滚事务。
    4. DoCommit阶段:如果协调者在PreCommit阶段接收到所有参与者的PreCommit请求,并且没有超时,则向所有参与者发出DoCommit请求,要求提交事务。如果任意一个参与者不能提交,则向所有参与者发送Abort请求,表示需要回滚事务。
      相比较2pc可以看出3pc解决了在2pc中的一些问题
    5. 单点故障中协调者挂了,由于引入了超时机制
      1. PreCommit阶段 和 DoCommit阶段协调者挂了,都可以在超时后向其他协调者请求给所有参与者发送提交或回滚请求
      2. CanCommit阶段协调者挂了,则事务直接终止
    6. 单点故障中参与者挂了
      1. CanCommit阶段参与者挂了,则协调者超时收不到参与者响应后终止本次事务
      2. PreCommit阶段参与者挂了,则本地事务不能执行成功,无法向协调者报告,则协调者在超时后终止本次事务
      3. DoCommit阶段参与者挂了,协调者不能向参与者发送commit请求,此时超时后终止本次事务

    那么总结下:
    3PC引入了超时机制,首先解决了单点故障的问题导致的资源一直得不到释放,但性能(由于引入了一个新的阶段,导致系统复杂且多了交互,自然性能下降)和数据不一致的问题依然没有得到解决。其次引入了一个新的阶段保证了在最终提交的阶段之前各个节点的状态是一致的。

    TCC

    (Try-Confirm-Cancel)基于补偿的分布式事务解决方案,将事务拆分成Try、Confirm和Cancel三个阶段,它采用的是预留资源的方式,将整个分布式事务拆分成多个本地事务,并在每个本地事务执行之前预留好资源。在整个分布式事务执行完成之前,每个参与者都保持着其预留资源的状态,如果整个分布式事务执行成功,则所有参与者提交本地事务并释放预留资源;如果执行失败,则所有参与者撤销本地事务并释放预留资源。

    TCC事务的执行过程可以分为三个阶段:

    1. Try阶段:首先,TCC事务协调器协调者向所有参与者发出Try请求,要求参与者预留必要的资源,并执行本地事务的“尝试”(Try)操作。如果所有参与者的Try操作都执行成功,则进入Confirm阶段;否则进入Cancel阶段。

    2. Confirm阶段:在Confirm阶段中,TCC事务协调器向所有参与者发出Confirm请求,要求参与者确认执行本地事务。如果所有参与者的Confirm操作都执行成功,则整个分布式事务执行成功,所有参与者提交本地事务并释放预留资源;否则进入Cancel阶段。

    3. Cancel阶段:在Cancel阶段中,TCC事务协调器向所有参与者发出Cancel请求,要求参与者撤销本地事务,并释放预留资源。如果所有参与者的Cancel操作都执行成功,则整个分布式事务执行失败,所有参与者释放预留资源;否则需要手动处理。

    和2PC,3PC不同的是,它是一个业务层面上的事务,每个业务服务都要实tcc的三个阶段接口,2pc的问题是单点故障,tcc通过重试机制解决了单点故障,重试次数过多报警则引入人为干预。Confirm和Cancel操作都满足米等性,如果操作执行失败则会有补偿机制,一直不断重试,直到成功为止。

    问题:

    空回滚

    如果协调者向参与者发送try请求时失败,则直接进入cancel阶段,协调者向参与者发送cancel,但此时参与者并没有执行try阶段操作,也没有预料资源,而直接回滚,这个时候回滚的就是一个空事务。
    如果在confirm阶段协调者发出Confirm请求失败,那此时参与者会一直等待执行本地事务,参与者在超时后则直接回滚,此时因为事务没有开始,但需要清楚try阶段预料的资源,这也是一个空的回滚事务。

    当在"Cancel"阶段发生异常或者无法正常执行时,TCC框架会在分布式事务管理器中创建一个空的分布式事务,并将其标记为已回滚。这样一来,预留资源就被清除掉了,而且不会影响已提交事务的状态。空回滚操作可以保证分布式事务的完整性和一致性,避免了因为预留资源和已提交事务不一致而导致的数据错误或者死锁等问题。

    相比2PC和3PC,TCC事务解决了如下问题:

    1. 分布式事务中的数据一致性问题:2PC和3PC协议的特点是将分布式事务中所有的资源一起提交或回滚,因此在分布式事务的执行过程中,存在一段时间的不确定状态,即协议的执行过程中无法确定一个事务的最终结果。而TCC事务通过在try阶段预留资源、confirm阶段提交资源、cancel阶段释放资源,保证了不同事务之间资源的隔离性和一致性。

    2. 可扩展性问题:2PC和3PC的强一致性和串行化执行的特点,导致在高并发场景下,容易出现性能瓶颈。而TCC事务在分布式系统中的可扩展性更好,可以通过横向扩展或分片来增加并发处理能力。

    3. 单点故障

      1. 在2PC中,协调者是整个分布式事务的中心节点,当协调者出现故障时,整个事务就会陷入僵局。而TCC模型将事务分解为try、confirm和cancel三个步骤,每个步骤都由各个服务端自己实现,没有中心节点,因此不存在单点故障的问题。
      2. 具体来说,在TCC中,每个服务端都是事务的一部分,而不是像2PC中的协调者和参与者那样区分角色。try阶段每个服务端都会尝试执行本地事务,如果所有服务端都执行成功,那么就进入confirm阶段,否则进入cancel阶段。在confirm阶段和cancel阶段,每个服务端同样都会尝试执行本地事务,保证数据的一致性。因此,即使某个服务端出现故障,也不会影响整个事务的执行。

    TCC事务的缺点:

    1. 代码实现复杂:TCC需要在代码中显式地编写try、confirm、cancel三个阶段的业务逻辑,实现代码的复杂度相对于传统的事务方式要高。

    2. 系统的额外负载:TCC需要在每个分布式事务中进行三个步骤的处理,其中try阶段需要对资源进行锁定和预留,增加了系统的额外负载。

    3. 风险管理难度:TCC的事务模型可能导致一些难以处理的风险,例如,当confirm或cancel阶段执行失败时,必须考虑如何进行补偿或回滚。

    4. 长事务:TCC模式将一个大的事务拆分成了多个阶段,每个阶段都需要占用资源,如果其中一个阶段耗时很长,可能会导致其他事务的阻塞或超时,从而影响系统的并发性能。而且在TCC模式中,每个阶段的处理需要编写额外的代码,增加了系统的复杂度。因此,TCC模式适用于事务时间较短的业务场景。

      1. 一种可能的解决方案是,增加一个全局锁,在执行补偿操作时进行加锁,以避免并发问题。比如,在下面的例子中,当库存扣减失败后,触发了回滚,此时后台线程对账户余额进行增加,可以先对一个全局的锁进行加锁,然后再对账户余额进行增加。这样可以保证在进行账户余额增加操作时,其他请求无法修改账户余额,从而避免并发问题的发生。当然,这种方案也可能会带来一些问题,比如锁的竞争可能会导致性能下降,或者锁的粒度过大导致系统吞吐量下降等
      2. 最好的办法就是尽量控制tcc事务的时间,长时间事务采用其他方案。

    举个例子:
    用户下单后,订单要入库,库存服务扣减,金额扣减,订单入库是主业务,子业务是库存扣减和金额扣减。

    1. 主业务生成订单信息保存设置状态为一个中间状态待确认,库存服务,账户金额服务分别进入try阶段,并返回是否可以执行业务
      1. 同时库存服务和账户服务都检查下库存和金额是否足够扣减,如果有冻结的,要减去冻结的,虽然冻结的有可能回滚,账户金额同样。
      2. 在这个阶段,预留业务资源,假设账户和库存数量都够扣减,但不直接扣减,用一个冻结字段冻结订单库存,账户金额也是预留一个冻结的金额。
    2. 主业务收到2个服务try阶段都返回成功时,则进入Confirm阶段,通知所有参与者提交事务
      1. 主业务更改订单状态为已支付
      2. 所有参与者库存扣减以及账户金额的扣减
    3. 如果有一个执行失败了则进入Cancel阶段,回滚整个事务

    下面是一份chatgpt写的tcc实现的伪代码:

    # 定义 TCC 事务状态
    class TccStatus(Enum):
        PENDING = 0
        TRYING = 1
        CONFIRMING = 2
        CANCELING = 3
        CONFIRMED = 4
        CANCELED = 5
    
    # 定义 TCC 事务
    class TccTransaction:
        def __init__(self):
            self.id = generate_transaction_id()  # 生成全局唯一的事务 ID
            self.status = TccStatus.PENDING
            self.branches = []
    
        # 定义 TCC 事务的 Try 阶段
        def try_transaction(self):
            self.status = TccStatus.TRYING
            try:
                for branch in self.branches:
                    # 调用分支事务的 try 方法
                    branch.try_transaction()
            except Exception as e:
                self.status = TccStatus.CANCELING
                raise e
    
        # 定义 TCC 事务的 Confirm 阶段
        def confirm_transaction(self):
            self.status = TccStatus.CONFIRMING
            try:
                for branch in self.branches:
                    # 调用分支事务的 confirm 方法
                    branch.confirm_transaction()
            except Exception as e:
                self.status = TccStatus.CANCELED
                raise e
            else:
                self.status = TccStatus.CONFIRMED
    
        # 定义 TCC 事务的 Cancel 阶段
        def cancel_transaction(self):
            self.status = TccStatus.CANCELING
            try:
                for branch in self.branches:
                    # 调用分支事务的 cancel 方法
                    branch.cancel_transaction()
            except Exception as e:
                # 可以记录日志等待人工干预
                log_error(e)
            finally:
                self.status = TccStatus.CANCELED
    
    # 定义 TCC 分支事务
    class TccBranch:
        def __init__(self, tcc_tx_id):
            self.id = generate_branch_id()  # 生成分支事务 ID
            self.tcc_tx_id = tcc_tx_id  # 关联所属 TCC 事务 ID
            self.status = TccStatus.PENDING
    
        # 定义 TCC 分支事务的 try 方法
        def try_transaction(self):
            self.status = TccStatus.TRYING
            # 执行具体的业务逻辑
    
        # 定义 TCC 分支事务的 confirm 方法
        def confirm_transaction(self):
            if self.status == TccStatus.TRYING:
                # 执行 confirm 操作
                self.status = TccStatus.CONFIRMED
    
        # 定义 TCC 分支事务的 cancel 方法
        def cancel_transaction(self):
            if self.status in [TccStatus.TRYING, TccStatus.CONFIRMING]:
                # 执行 cancel 操作
                self.status = TccStatus.CANCELED
    
    

    本地消息表

    也是一种补偿型事务,在这种模式下,使用本地消息表来缓存需要执行的操作,消息发送成功后,将在本地消息表中创建一条记录,并在事务提交后立即执行相关操作,同时标记该条记录为已执行,如果在执行过程中发生异常,则回滚该条记录。在定时任务的支持下,未被成功执行的消息会被重试,直到成功或者超过重试次数后标记为执行失败。

    实现步骤:

    1. 在发送消息的时候,先在本地数据库中创建一个消息表,并将消息数据插入到该表中,同时设置消息的状态为“待发送”或“发送中”。

    2. 在本地数据库中再创建一个业务表,并在其中插入需要处理的业务数据。

    3. 通过消息队列将消息发送到远程节点,让远程节点消费并处理该消息。

    4. 远程节点在处理完消息后,会将处理结果通过消息队列返回给本地节点。

    5. 本地节点接收到处理结果后,根据处理结果更新消息表和业务表中的数据。

    6. 如果处理成功,将消息表中的状态更新为“已发送”,并将业务表中的状态更新为“已处理”。

    7. 如果处理失败,则将消息表中的状态更新为“发送失败”,并将业务表中的状态更新为“未处理”。

    8. 在后台定时扫描本地消息表,将状态为“发送失败”的消息重新发送,直到消息发送成功。

    需要注意的是,本地消息表的实现需要考虑以下几点:

    1. 数据库事务的一致性:本地消息表需要和业务表在同一个事务中提交,保证消息和业务数据的一致性。

    2. 消息的幂等性:在消息的处理过程中需要保证幂等性,避免消息的重复处理。

    3. 处理结果的可靠性:需要保证消息队列的可靠性,避免消息的丢失或重复消费。

    4. 扫描任务的定时性和效率:需要定时扫描本地消息表,将发送失败的消息重新发送,但是扫描任务的定时间隔不能太短,否则会对数据库的性能产生影响,也不能太长,否则会影响消息的发送效率。

    这种模式的优势在于,不需要全局协调者进行事务管理,而是通过本地消息表来管理每个事务,可以支持较高的并发性和可用性,并且可以比较容易地实现幂等性。同时,由于本地消息表本身就是一个持久化存储,可以很好地保证消息的可靠性。
    MQ的事务实际上就是本地消息表的封装,如图(图是网上抄的):


    1682222196975.jpg

    不过,需要注意的是,本地消息表模式需要开发人员自己维护消息表的状态,包括消息的生命周期、重试机制、并发性等,相对于TCC模式需要更多的开发工作。同时,本地消息表模式对于一些对数据一致性要求较高的业务场景可能不太适合,例如对于一些财务结算等需要实时保证数据一致性的场景。

    举个例子:
    下单需要扣减库存,扣减账户余额,数据库中用一张表存储消息。

    1. 下单后将扣减账户余额,扣减库存的消息存到消息表中
    2. 创建订单(消息表和创建订单表在一个库中,这里依靠数据库本地事务可以保证一致性,失败就回滚,成功则提交)
    3. 后台单独的线程去轮询消息表,没处理的消息(设置一个未处理的状态)异步去处理,在这里就是通知账户,库存服务去扣减余额,扣减库存,执行成功后更新消息表状态
    4. 失败则重试,设置重试次数,如果过多则发警报,由人为去干预,接口需要保证幂等性,保持数据的最终一致性,也就是说在下单后,扣减余额和口额库存这两个认为是一定要成功的,在最终扣减成功这段时间里可能数据会出入,所以适合对实时性要求不高的场景。

    SAGA模式

    SAGA模式是微服务架构下实现分布式事务的一种模式。它的基本思想是:将一个大的事务划分成多个小的事务,每个小事务对应一个服务。这些小事务之间采用补偿事务来保证数据一致性

    SAGA模式的实现通常包含以下几个步骤:

    1. 开始一个事务:客户端向第一个服务发起请求,开始一个事务
    2. 服务1执行业务逻辑:第一个服务执行业务逻辑,并发起请求调用下一个服务
    3. 服务2执行业务逻辑:第二个服务执行业务逻辑,并发起请求调用下一个服务......以此类推
    4. 最后一个服务提交事务:执行完业务逻辑后,最后一个服务提交事务
    5. 中间服务失败:如果中间任何一个服务执行失败,它会发送补偿事件给前面已经执行成功的服务执行补偿操作,回滚事务
    6. 发起服务接收到失败通知:第一个服务接收到失败通知,发起整个事务的回滚

    优点:

    1. 比全局事务更加可靠。因为不依赖于一个巨大的全局事务,所以单点故障的概率更小
    2. 每个服务可以独立去实现自己的事务机制,更容易在微服务架构中实现
    3. 即使某个服务不支持事务,也不会影响其他服务
    4. 可以最大限度的利用每个服务的并发性能
      缺点:
    5. 实现起来比较复杂,需要维护大量的补偿逻辑,这会增加系统的复杂度
    6. 只能保证最终一致性,中间状态难以控制,全局事务可以确保所有服务的状态变更都是原子的,SAGA模式无法做到这一点
    7. 一旦后续服务失败,前面服务已经执行的操作需要全部回滚,这会带来较大的性能损耗
    8. 需要服务之间相互协调依赖较重,整个系统的健壮性较差。一旦协调服务出现问题,整个SAGA事务都会受影响
    9. 难以在业务逻辑复杂的情况下进行边界的确定,这会导致补偿逻辑变得非常复杂

    所以SAGA模式适用于微服务架构,并且业务逻辑比较简单,数据一致性要求不高的场景。它牺牲了一定的一致性,换来了可靠性,那么对于复杂业务,基于XA协议的全局式分布式事务会比较可靠一点

    它和事件事务也有一定的关系,事件事务是通过发布订阅事件驱动业务流程,saga模式的事务也是通过发布订阅来做补偿机制的

    举个例子:
    还是以用户下单,扣减库存和支付来做例子

    1. 用户下单,订单服务创建订单后发布"订单创建成功”事件,此时订单的状态为一个中间状态,比如待确认或者下单中
    2. 商品服务订阅到订单成功的事件,检查商品库存,如果库存足够则扣减库存,并发布“商品库存扣减成功”事件
    3. 支付服务订阅到“商品库存扣减成功”事件,调用支付接口进行支付,支付成功后发布“支付成功”事件。
    4. 订单服务订阅到“支付成功”事件,则将订单状态更新为“已完成”。

    如果任意的事件处理失败,则会进行补偿:

    1. 商品库存扣减失败:订单服务发布“订单取消”事件,触发订单取消流程。
    2. 支付失败:支付服务发布“支付失败”事件,订单服务发布“订单取消”事件,触发订单取消流程。
    3. 订单取消流程:订单服务更新状态为“已取消”,发布“库存回滚”事件,商品服务订阅事件并回滚库存变更。

    这种协同式的有一个好处就是简单,耦合度低,但缺点也很明显,代码复杂,服务之间的依赖关系会形成循环依赖,还有一种基于控制器类处理的编排式,控制类发出命令和消息给每个参与方,指导参与方完成具体的操作。

    相关文章

      网友评论

          本文标题:分布式事务2PC,3PC,TCC,SAGA(一)

          本文链接:https://www.haomeiwen.com/subject/enafjdtx.html