分布式事务一致性的几种解决方案(图)

173 阅读8分钟

背景说明

现在有以下服务器:

  • game服务器,角色是BFF,没有业务逻辑负责其他服务器的调度工作.
  • club服务器,工会业务服务器,负责club的具体业务.
  • leaderboard服务器,排行榜服务器,负责排行榜的具体业务.
  • chat服务器,聊天室服务器,负责聊天室相关的具体业务.

服务器的架构如下: image.png

现在有一个业务是这样的,有人申请加入工会,对应club服务器来说需要执行添加成员的业务逻辑,leaderboard服务器需要更新这个工会的总分(是通过总分进行排行的),chat服务器需要把玩家加入到聊天室中.

下面来分别说下几种解决事务一致性的方案:

两阶段提交(2PC)

依赖数据库的事务方式,将完整的业务拆分成两个阶段 阶段1是准备阶段,协调者询问每一个事务的参与者是否准备好了,这时候参与者需要开启事务并执行相关的业务逻辑但是并不提交事务,当参与者执行好了这些事情后返回给协调者表示准备好了. 接下来协调者就继续执行阶段2提交事务或是回滚事务,如果在阶段1中全部的参与者都回复准备好了,那么阶段2就是提交事务,如果阶段1中有事务回复没准备好或是回复超时了,则阶段2执行的就是回滚事务

阶段1: 准备阶段 image.png

阶段2: 提交事务 image.png

如果阶段1出现了未准备好的情况(这里也包括了超时的情况): image.png

阶段2就变成了回滚阶段: image.png

2PC存在的问题
  • 单点故障: 协调者的崩溃会导致整个事务无法继续进行,这使得协调者成为一个单点故障。除非有额外的机制来恢复或选举新的协调者,否则整个系统的可靠性会受影响
  • 如果有参与者不能使用,另外的参与者做的就是无用功
  • 性能和开销: 2PC是一种同步的一致性方式,需要多个参与者同时在线并且强依赖数据库的事务,可以返现我们在阶段1开启了事务但没有提价或是回滚事务,而是到了阶段2才提交或回滚的事务.因为有锁的存在这会影响数据库的执行效率和数据库的负担 下面我将使用mongodb为例子来说下在执行2PC的时候Mongodb还有哪些锁(开启事务的时间越长性能就越来越差)
    • 集合级别的锁: MongoDB在事务期间会对更新的集合加锁。不同于某些数据库的行级锁,MongoDB使用的是更粗粒度的锁,比如集合锁和数据库锁。在事务期间,对集合进行的写操作会获取写锁,这意味着在事务提交之前,其他事务不能对同一集合进行并发的写操作
    • 文档级别的锁: 虽然 MongoDB 在更新和插入操作时会使用文档级别的锁定来修改单个文档,但在事务中,跨多个文档的操作仍然会因为事务的隔离性而受到集合级别锁的影响。因此,虽然单个文档的更新是通过文档级锁来控制的,但事务整体的并发控制会涉及更高层次的锁
    • 读写意图锁: 这些锁用于指示数据库范围内的更高粒度锁需要聚合列表来计划操作。意图锁并不阻止读取,但确保在写入锁存在时,写操作可以安全地进行。

三阶段提交 3PC

为了解决2PC中"如果有参与者不可用,另外的参与者做的都是无用功"这样的问题于是有了3PC. 那么这个3PC在2PC的基础上添加了一个阶段,这个阶段叫can-Commit通过这个阶段协调者来获取各个服务器的基本信息,是否具备了执行分布式事务的能力 各个阶段的名字也改了改 现在的三个阶段是 Can-Commit Pre-Commit Do-Commit三个阶段

Can-Commit阶段,通过这个阶段协调者获得参与者是否有执行事务的能力 如果在这阶段有参与者出现了响应失败或是超时的问题,那么整个分布式事务将终止 image.png

Pre-Commit: 预提交阶段 image.png

Do-Commit: 提交事务 image.png

如果Pre-Commit出现了未准备好的情况(这里也包括了超时的情况): image.png

Do-Commit就变成了回滚阶段: image.png

3PC缺点

3PC是在2PC的基础上实现的,解决了有参与者不能执行事务时另外参与者做"无用功"的问题,本质上来说2PC有的问题3PC都有

TCC

类似于2PC,但是2PC或是3PC都是强依赖数据库的事务来进行提交或是回滚操作,而TCC是通过业务代码逻辑的方式实现分布式事务 TCC具体是 Try Commit Cancel 三个单词的首字母 执行的流程如下

Try阶段: 尝试去实现业务逻辑,但我们添加了一个新的业务状态,从业务的方式把这个数据进行隔离,让这个数据在业务中暂时是无效数据(这里leaderboard并没有做业务是因为对于排行榜业务来说只是添加一个总分,如果想要教条的实现业务逻辑并暂时无效的话需要再引入冻结分数,这样会让业务逻辑变的过于复杂了) image.png

Confirm阶段: 如果Try阶段各个参与者都执行成功,则这个阶段协调者告诉参与者去执行Confrim操作,具体就是将步骤1中生成的数据变成业务上的有效数据 image.png

Cancel阶段: 如果Try阶段出现了任何一个参与者执行失败或是执行超时的情况,就会执行Cancel,具体的逻辑就是将Try阶段产生的新数据进行反向操作 image.png

TCC的存在的问题:

TCC解决了2PC和3PC强依赖数据库的事务的问题,性能也会高于2PC和3PC(开启事务的时间变少了,性能肯定会更好一些),但TCC也有它的问题:

  • 相对2PC和3PC操作更加复杂了,不依赖数据库的事务就需要自己写补偿和回滚逻辑
  • 数据的临时不一致性,数据会最终一致
  • 事务的隔离性比较差

Saga

流水线事务,Saga的逻辑和TCC其实是差不多的,只是Saga更擅长处理流程比较长的事务 在Saga中,一共分为两个执行阶段,阶段1执行阶段,就当分布式事务不存在一样的去执行各自的业务逻辑,阶段2补偿阶段,如果阶段1执行过程中出现了异常,就需要执行阶段2,在这个阶段里需要对阶段1做的事务做出补偿,比如club服务器添加了工会,在补偿阶段就需要删除这个工会

执行阶段:就当没有分布式事务一样去执行 image.png

补偿阶段: 如果执行阶段出现了异常,则会执行补偿阶段,通过业务逻辑的方式将数据做出补偿 image.png

Saga的问题:

  • 事务的一致性很差,比TCC还要差,在TCC的Confirm阶段前虽然数据已经有了,但是有新的状态字段(enable/disabled)所以从业务的角度去看数据依然是无效的,但Saga就不同了,没有这个状态,数据的临时一致性的时间会比较长
  • 同TCC一样,不依赖数据库的事务需要自己实现业务逻辑的回滚

MQ消息最终一致性

协调者发送事件给MQ,不同的参与者订阅MQ感兴趣的事件,通过MQ的ack和重试机制实现消息的最终一致性,这种方式是最解耦的,参与者不需要关心协调者,协调者也不用对业务进行编排

image.png

利用MQ的重试机制实现最终消息的一致性 但其实也会存在异常,比如类似库存如果出现了库存不足怎么办? 这种虽然不是业务上的bug但也会导致业务无法进行下去

场景

适合2PC/3PC的场景:

  • 适用于事务粒度较小、参与者较少的场景
  • 数据一致性要求较高,但可以接受一定程度的阻塞
  • 适合需要严格ACID特性的金融系统或银行交易

不适合2PC/3PC的场景:

  • 高并发、大规模分布式系统,因为阻塞问题和单点故障可能导致性能瓶颈

适合TCC/Saga的场景:

  • 可以使用业务逻辑来补偿或回滚业务
  • 可以出现临时的数据不一致问题
  • 有明确的区分阶段

不适合TCC/Saga的场景:

  • 实现补偿逻辑困难或不可能完全补偿的场景
  • 数据一致性要求很高

适合MQ消息一致性的场景:

  • 异步处理流程和事件驱动架构下的消息一致性需求

不适合MQ消息一致性的场景:

  • 严格同步的操作,这类场景可能需要ACID事务特性