分布式基础(二)分布式理论之分布式事务

目录

    • 什么是分布式事务
      • 单体应用
      • 分布式应用
    • 2PC
      • 投票阶段
      • 提交阶段
      • 两阶段提交协议成功场景示意图:
      • 优缺点
    • 3PC
      • 三阶段提交协议的成功场景:
      • 询问阶段(CanCommit)
      • 准备阶段(PreCommit,2pc中是叫投票阶段)
      • 提交阶段(DoCommit)
      • 优缺点
    • TCC
      • TCC的执行
        • Try
        • Confirm
        • Cancel
      • 总结
    • 可靠消息最终一致性方案
      • 本地消息表
        • 可靠消息服务
        • 生产者
        • 消费者
        • 总结
      • 分布式消息中间件
        • prepare阶段
        • 确认阶段
        • ACK机制

什么是分布式事务

单体应用

下图是一个单体应用的 3 个 模块,在同一个数据源上更新数据来完成一项业务,整个过程的数据一致性可以由数据库的本地事务来保证,如下图:
分布式基础(二)分布式理论之分布式事务_第1张图片

分布式应用

随着业务需求和架构的变化,单体应用进行了服务化拆分:原来的 3 个 模块被拆分为 3 个独立的服务,每个服务使用独立的数据源(Pattern: Database per service)。整个业务过程将由 3 个服务的调用来完成,如下图:
分布式基础(二)分布式理论之分布式事务_第2张图片
每个服务自身的数据一致性仍由本地事务来保证,但是整个业务层面的全局数据一致性要如何保障呢?比如订单服务和账户服务,都有各自的数据库,必须保证操作的一致性,不能出现下单成功但是没记账的情况。这就是分布式系统所面临的典型分布式事务需求:
分布式系统需要一个解决方案来保障对所有节点操作的数据一致性,这些操作组成一个分布式事务,要么全部执行,要么全部不执行。

2PC

二阶段提交协议,包含两类节点:

  • 一个中心化协调者节点(coordinator),一般也叫做事务协调者
  • 多个参与者节点(participant、cohort),一般也叫做事务参与者

2PC协议事务提交过程分为两个阶段:投票阶段和提交阶段

投票阶段

  • 事务协调者询问所有事务参与者进行投票:是否可以提交事务,然后等待所有参与者的投票结果;
  • 参与者如果投票表示可以提交事务,那么就必须预留本地资源(执行本地事务->写redo,undo日志,锁定资源,执行操作,但是不提交),然后响应YES,后续也不再允许放弃事务;如果不能,就返回NO响应;
  • 如果协调者接受某个参与者的响应超时,它会认为该参与者投票为NO,即预留资源失败。
    分布式基础(二)分布式理论之分布式事务_第3张图片

提交阶段

在该阶段,事务协调者将基于投票阶段的投票结果进行决策:提交或取消各参与者的本地事务

  • 仅当所有参与者都返回 YES 响应时,协调者才向所有参与者发出提交请求,此时所有参与者必须保证提交事务成功;
  • 如果投票阶段中任意一个参与者返回 No 响应,则协调者向所有参与者发出回滚请求,所有参与者进行回滚操作。

两阶段提交协议成功场景示意图:

分布式基础(二)分布式理论之分布式事务_第4张图片

优缺点

优点:

  • 强一致性,因为一阶段预留了资源,所有只要节点或者网络最终恢复正常,协议就能保证二阶段执行成功;
  • 业界标准支持,二阶段协议在业界有标准规范——XA 规范,许多数据库和框架都有针对XA规范的分布式事务实现。

缺点:

  • 在提交请求阶段,需要预留资源,在资源预留期间,其他人不能操作(比如,XA 在第一阶段会将相关资源锁定) ,会造成分布式系统吞吐量大幅下降;
  • 容错能力较差,比如在节点宕机或者超时的情况下,无法确定流程的状态,只能不断重试,同时这也会导致事务在访问共享资源时发生冲突和死锁的概率增高,随着数据库节点的增多,这种趋势会越来越严重,从而成为系统在数据库层面上水平伸缩的"枷锁";

3PC

在二阶段协议中,事务参与者在投票阶段,如果同意提交事务,则会锁定资源,此时任何其他访问该资源的请求将处于阻塞状态。
正因为这个原因,三阶段协议(Three-phase commit protocol, 3PC)对二阶段协议进行了改进:

  • 一方面引入超时机制,解决资源阻塞问题;
  • 另一方面新增一个询问阶段(CanCommit),提前确认下各个参与者的状态是否正常。

三阶段提交协议的成功场景:

其实就是在2pc的投票阶段前面,再加一个询问阶段
分布式基础(二)分布式理论之分布式事务_第5张图片

询问阶段(CanCommit)

  • 询问阶段,事务协调者向事务参与者发送 CanCommit 请求,参与者如果可以提交就返回 Yes 响应,否则返回 No 响应。
  • 询问阶段可以确保尽早的发现无法执行操作的参与者节点,这样的话参与者就不会取锁定资源。
  • 对于事务协调者,如果询问阶段有任一参与者返回NO或超时,则协调者向所有参与者发送abort指令。
  • 对于返回NO的参与者,如果在指定时间内无法收到协调者的abort指令,则自动中止事务。

准备阶段(PreCommit,2pc中是叫投票阶段)

事务协调者根据事务参与者在询问阶段的响应,判断是执行事务还是中断事务:

  • 如果询问阶段所有参与者都返回YES,则协调者向参与者们发送预执行指令(preCommit),参与者接受到preCommit指令后,写redoundo日志,执行事务操作,占用资源,但是不会提交事务;
  • 参与者响应事务操作结果,并等待最终指令:提交(doCommit)或中止(abort)。

提交阶段(DoCommit)

  • 如果每个参与者在准备阶段都返回ACK确认(即事务执行成功),则协调者向参与者发起提交指令(doCommit),参与者收到指令后提交事务,并释放锁定的资源,最后响应ACK

当参与者响应ACK后,即使在指定时间内没收到doCommit指令,也会进行事务的最终提交;
一旦进入提交阶段,即使因为网络原因导致参与者无法收到协调者的doCommit或Abort请求,超时时间一过,参与者也会自动完成事务的提交。

  • 如果任意一个参与者在准备阶段返回NO(即执行事务操作失败),或者协调者在指定时间没收到全部的ACK响应,就会发起中止(abort)指令,参与者取消已经变更的事务,执行undo日志,释放锁定的资源。

优缺点

优点:

  • 增加了一个询问阶段,询问阶段可以确保尽早的发现无法执行操作的参与者节点,提升效率;
  • 在准备阶段成功以后,协调者和参与者执行的任务中都增加了超时,一旦超时,参与者都会继续提交事务,默认为成功,降低了阻塞范围。

缺点:

  • 如果准备阶段执行事务后,某些参与者反馈执行事务失败,但是由于出现网络分区,导致这些参与者无法收到协调者的中止请求,那么由于超时机制,这些参与者仍会提交事务,导致出现不一致;
  • 性能瓶颈,不适合高并发场景。

所以无论是 2PC 还是 3PC,当出现网络分区且不能及时恢复时, 都不能保证分布式系统中的数据 100% 一致。

TCC

两阶段提交(2PC)和三阶段提交(3PC)并不适用于并发量大的业务场景。TCC事务机制相比于2PC、3PC,不会锁定整个资源,而是通过引入补偿机制,将资源转换为业务逻辑形式,锁的粒度变小。

TCC的核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作,分为三个阶段:

  • Try:这个阶段对各个服务的资源做检测以及对资源进行锁定或者预留;
  • Confirm:执行真正的业务操作,不作任何业务检查,只使用Try阶段预留的业务资源,Confirm操作要求具备幂等设计,Confirm失败后需要进行重试;
  • Cancel:如果任何一个服务的业务方法执行出错,那么这里就需要进行补偿,即执行回滚操作,释放Try阶段预留的业务资源,Cancel操作要求具备幂等设计,Cancel失败后需要进行重试。

TCC的执行

TCC将一次事务操作分为三个阶段:Try、Confirm、Cancel,我们通过一个订单/库存的示例来理解。假设我们的分布式系统一共包含4个服务:订单服务、库存服务、积分服务、仓储服务,每个服务有自己的数据库,如下图:
分布式基础(二)分布式理论之分布式事务_第6张图片

Try

Try阶段一般用于锁定某个资源,设置一个预备状态或冻结部分数据。
对于示例中的每一个服务,Try阶段所做的工作如下:
订单服务:先置一个中间状态“UPDATING”,而不是直接设置“支付成功”状态;
库存服务:先用一个冻结库存字段保存冻结库存数,而不是直接扣掉库存(可销售库存是另一个字段);
积分服务:预增加会员积分;
仓储服务:创建销售出库单,但状态是UNKONWN
分布式基础(二)分布式理论之分布式事务_第7张图片

Confirm

根据Try阶段的执行情况,Confirm分为两种情况:

  • 理想情况下,所有Try全部执行成功,则执行各个服务的Confirm逻辑;
  • 部分服务Try执行失败,则执行第三阶段——Cancel

Confirm阶段一般需要各个服务自己实现Confirm逻辑:
订单服务:confirm逻辑可以是将订单的中间状态变更为PAYED-支付成功;
库存服务:将冻结库存数清零,同时扣减掉真正的库存;
积分服务:将预增加积分清零,同时增加真实会员积分;
仓储服务:修改销售出库单的状态为已创建-CREATED
分布式基础(二)分布式理论之分布式事务_第8张图片
Confirm阶段的各个服务本身可能出现问题,这时候一般就需要TCC框架了(比如ByteTCC,tcc-transaction,himly),TCC事务框架一般会记录一些分布式事务的活动日志,保存事务运行的各个阶段和状态,从而保证整个分布式事务的最终一致性。

Cancel

如果Try阶段执行异常,就会执行Cancel阶段。比如:对于订单服务,可以实现的一种Cancel逻辑就是:将订单的状态设置为“CANCELED”;对于库存服务,Cancel逻辑就是:将冻结库存扣减掉,加回到可销售库存里去。
分布式基础(二)分布式理论之分布式事务_第9张图片

总结

从正常的流程上讲,TCC仍然是一个两阶段提交协议。但是,在执行出现问题的时候,有一定的自我修复能力,如果任何一个事务参与者出现了问题,协调者可以通过执行逆操作来取消之前的操作,达到最终的一致状态(比如冲正交易、查询交易)。

TCC的执行流程也可以看出,服务提供方需要提供额外的补偿逻辑,那么原来一个服务接口,引入TCC后可能要改造成3种逻辑:

  • Try:先是服务调用链路依次执行Try逻辑;
  • Confirm:如果都正常的话,TCC分布式事务框架推进执行Confirm逻辑,完成整个事务;
  • Cancel:如果某个服务的Try逻辑有问题,TCC分布式事务框架感知到之后就会推进执行各个服务的Cancel逻辑,撤销之前执行的各种操作。

目前相对比较成熟的是阿里开源的分布式事务框架seata

优点:
2PC比起来,实现以及流程相对简单了一些,但数据的一致性比2PC也要差一些,当然性能也可以得到提升。

缺点:
TCC模型对业务的侵入性太强,事务回滚实际上就是自己写业务代码来进行回滚和补偿,改造的难度大。一般来说支付、交易等核心业务场景,可能会用TCC来严格保证分布式事务的一致性,要么全部成功,要么全部自动回滚。这些业务场景都是整个公司的核心业务有,比如银行核心主机的账务系统,不容半点差池。

但是,在一般的业务场景下,尽量别没事就用TCC作为分布式事务的解决方案,因为自己手写回滚/补偿逻辑,会造成业务代码臃肿且很难维护。

可靠消息最终一致性方案

所谓可靠消息最终一致性方案,其实就是在分布式系统当中,把一个业务操作转换成一个消息,然后利用消息来实现事务的最终一致性。
可靠消息最终一致性方案一般有两种实现方式,原理其实是一样的:

  • 基于本地消息表
  • 基于支持分布式事务的消息中间件,如RocketMQ

本地消息表

核心思想是将分布式事务拆分成本地事务进行处理,这种思路是来源于eBay。
基于本地消息服务的分布式事务分为三大部分:

  • 可靠消息服务:存储消息,因为通常通过数据库存储,所以也叫本地消息表
  • 生产者(上游服务):生产者是接口的调用方,生产消息
  • 消费者(下游服务):消费者是接口的服务方,消费消息

分布式基础(二)分布式理论之分布式事务_第10张图片

可靠消息服务

可靠消息服务就是一个单独的服务,有自己的数据库,其主要作用就是存储消息(包含接口调用信息,全局唯一的消息编号),消息通常包含以下状态:

  • 待确认:上游服务发送待确认消息
  • 已发送:上游服务发送确认消息
  • 已取消(终态):上游服务发送取消消息
  • 已完成(终态):下游服务确认接口执行完成

生产者

服务调用方(消息生产者)需要调用下游接口时,不直接通过RPC之类的方式调用,而是先生成一条消息,其主要步骤如下:

  • 生产者调用接口前,先发送一条待确认消息(一般称为half-msg,包含接口调用信息)给可靠消息服务,可靠消息服务会将这条记录存储到自己的数据库(或本地磁盘),状态为【待确认】;
  • 生产者执行本地事务,本地事务执行成功并提交后,向可靠消息服务发送一条确认消息;如果本地执行失败,则向消息服务发送一条取消消息;
  • 可靠消息服务如果收到消息后,修改本地数据库中的那条消息记录的状态改为【已发送】或【已取消】。如果是确认消息,则将消息投递到MQ消息队列;(修改消息状态和投递MQ必须在一个事务里,保证要么都成功要么都失败)。

为了防止出现:生产者的本地事务执行成功,但是发送确认/取消消息超时的情况。可靠消息服务里一般会提供一个后台定时任务,不停的检查消息表中那些【待确认】的消息,然后回调生产者(上游服务)的一个接口,由生产者确认到底是取消这条消息,还是确认并发送这条消息。
分布式基础(二)分布式理论之分布式事务_第11张图片
通过上面这套机制,可以保证生产者对消息的100%可靠投递。

消费者

服务提供方(消息消费者),从MQ消费消息,然后执行本地事务。执行成功后,反过来通知可靠消息服务,说自己处理成功了,然后可靠消息服务就会把本地消息表中的消息状态置为最终状态【已完成】 。
这里要注意两种情况:

  • 消费者消费消息失败,或者消费成功但执行本地事务失败。
    针对这种情况,可靠消息服务可以提供一个后台定时任务,不停的检查消息表中那些【已发送】但始终没有变成【已完成】的消息,然后再次投递到MQ,让下游服务来再次处理。也可以引入zookeeper,由消费者通知zookeeper,生产者监听到zookeeper上节点变化后,进行消息的重新投递。
  • 如果消息重复投递,消息者的接口逻辑需要实现幂等性,保证多次处理一个消息不会插入重复数据或造成业务数据混乱。
    针对这种情况,消费者可以准备一张消息表,用于判重。消费者消费消息后,需要去本地消息表查看这条消息有没处理成功,如果处理成功直接返回成功。
    分布式基础(二)分布式理论之分布式事务_第12张图片

总结

这个方案的优点是简单,但最大的问题在于可靠消息服务是严重依赖于数据库的,即通过数据库的消息表来管理事务,不太适合并发量很高的场景。

分布式消息中间件

许多开源的消息中间件都支持分布式事务,比如RocketMQKafka。其思想几乎是和本地消息表/服务实一样的,只不过是将可靠消息服务和MQ功能封装在一起,屏蔽了底层细节,从而更方便用户的使用。这种方案有时也叫做可靠消息最终一致性方案。

RocketMQ为例,消息的发送分成2个阶段:Prepare阶段和确认阶段

prepare阶段

  • 生产者发送一个不完整的事务消息——HalfMsg到消息中间件,消息中间件会为这个HalfMsg生成一个全局唯一标识,生产者可以持有标识,以便下一阶段找到这个HalfMsg
  • 生产者执行本地事务。

注意:消费者无法立刻消费HalfMsg,生产者可以对HalfMsg进行Commit或者Rollback来终结事务。只有当CommitHalfMsg后,消费者才能消费到这条消息。

确认阶段

  • 如果生产者执行本地事务成功,就向消息中间件发送一个Commit消息(包含之前HalfMsg的唯一标识),中间件修改HalfMsg的状态为【已提交】,然后通知消费者执行事务;
  • 如果生产者执行本地事务失败,就向消息中间件发送一个Rollback消息(包含之前HalfMsg的唯一标识),中间件修改HalfMsg的状态为【已取消】。

消息中间件会定期去向生产者询问,是否可以Commit或者Rollback那些由于错误没有被终结的HalfMsg,以此来结束它们的生命周期,以达成事务最终的一致。之所以需要这个询问机制,是因为生产者可能提交完本地事务,还没来得及对HalfMsg进行Commit或者Rollback,就挂掉了,这样就会处于一种不一致状态。

ACK机制

消费者消费完消息后,可能因为自身异常,导致业务执行失败,此时就必须要能够重复消费消息。RocketMQ提供了ACK机制,即RocketMQ只有收到服务消费者的ack message后才认为消费成功。

所以,服务消费者可以在自身业务员逻辑执行成功后,向RocketMQ发送ack message,保证消费逻辑执行成功。

你可能感兴趣的:(分布式系统)