分布式事务 - 两阶段提交和三阶段提交

分布式事务

想聊聊分布式事务。

看了网上的一些说法,仔细思考之后感觉都不大统一,有些就是不对。本文加入了一些自己的思考,讨论了一些实现中的细节,如果不对欢迎指正。

先说说两阶段和三阶段提交吧。

两阶段提交

我这里说的两阶段提交,区别于网络上某些文章里提到的显然不实用的两阶段实现,是考虑到超时、异常恢复的两阶段提交。

前提

各系统的所有操作应当保证幂等。

流程

具体的流程图不再画(网上随便搜搜就有),简单描述一下就是两步:

  1. 发起事务:事务的发起者提出一个请求(比如用户下单购买某个商品),要求其依赖服务(也就是事务的执行者)响应请求(比如通知优惠券业务锁定使用的优惠券、通知支付业务冻结付款金额、通知仓储服务冻结库存等等)
    当所有依赖方都回复确认之后,事务的准备阶段完毕。

  2. 确认/取消事务:当请求得到所有依赖服务的成功确认后,事务的发起者通知所有执行者确认(confirm)事务;如果第一步中只要有一个执行者返回失败,则取消(cancel)事务。

对于第二步,有些文章中的简单的二阶段提交是不需要执行者回复的,个人认为这意味着发起者无法确认第二步事务(无论是 confirm 还是 cancel 操作)有没有成功执行,所以个人认为需要确认。下文按有确认进行讨论。

更进一步,对于执行者而言,因为第一步的锁定返回了成功,所以第二步的确认只能是成功,不允许失败。执行者应想办法重试并保证成功。如果失败则意味着出现了系统的数据不一致。

超时处理

以上的流程在是理想情况下。

当考虑到网络异常等情况,会存在三个问题:

  1. 对执行者而言:如果没有收到第二步事务,该如何处理?此时的执行者会一直锁定资源等待第二步事务。

  2. 对发起者而言:如果第一步中没有收到回复,该如果处理?此时的发起者无法得知是否所有执行者都成功锁定了资源。

  3. 对发起者而言:如果第二步中没有收到回复,该如果处理?此时的发起者无法得知是否所有执行者都成功确认了事务。

执行者没有收到第一步事务,对执行者而言是无感知的。所以没有这个问题。

依次回答这三个问题:

  1. 执行者没有收到第二步事务,有三种处理方案:第一种是一直锁定资源等待,第二种是超时 confirm 事务,第三种是超时 cancel 事务。对于两阶段提交而言,其他执行者在第一步是有可能返回失败的,所以显然强行 confirm 会有风险,第三种更为合理。此处有“应当 confirm 但因为网络或其他问题而没有收到,最终执行了超时 cancel”的风险,会导致数据不一致。

  2. 发起者第一步中没有收到回复,也存在两种策略:要么超时重试(再次提出事务),要么超时后当做返回失败处理。这两种可以组合使用,即多次超时重试后仍无回复则当做返回失败处理。

  3. 发起者第二步中没有收到回复,和问题 2 的处理策略类似,多次超时重试后仍无返回说明出现了异常,但不同的是这个异常是一个无法回滚的异常,意味着系统中可能出现了数据不一致,可能需要其他(很可能是人工)方式修复数据。

对于问题 3 ,因为在问题 1 的回答中我们默认执行者超时会 cancel 事务,所以当发起者第二步提出的是 cancel 时不会有什么问题。换句话说,当发起者在第二步提出 confirm 而没有收到回复时可能会出现数据不一致。

异常重试

当执行过程中发生异常(比如宕机),事务应当可以重试。

  1. 对发起者而言:如果在第一步发生异常:部分执行者锁定了资源,而另一部分从未收到过事务请求。由于执行者会默认超时 cancel,所以发起者发起 cancel 后(或不处理,直接等待超时)重新发起新事务即可。

  2. 对发起者而言:如果在第二步发生异常:如果执行的是 cancel,则无需重试,当做成功即可(当然也可以重试)。如果执行的是 confirm,则可能发生部分机器成功 confirm,部分机器由于没有收到 confirm,默认超时 cancel 请求,从而数据不一致的风险。

  3. 对执行者而言:如果在第一步发生异常:尽量返回失败即可,超时发起者会重试/cancel 请求。不会有什么风险。

  4. 对执行者而言:如果在第二步发生异常:尽量重试并保证成功。如果执行的是 confirm,说明第一步的锁定返回了成功,所以第二步的确认只能是成功。如果是 cancel,则更应当自行重试保证资源释放。

问题

在思考前两章问题的过程中,我们意识到了这个流程所存在的问题:

  1. 最严重的风险,如果发起者在第二步 confirm 的过程中出现了异常、或由于网络问题部分执行者没有收到 confirm,那么会出现数据不一致的问题。

  2. 第一步操作会锁定资源,然而有可能操作不成功,需要释放资源。这种反复的“锁定-释放”降低了并发。

三阶段提交

相对两阶段提交的改进

名词上的修改没太多意义(cancel -> rollback,confirm -> doCommit)

三阶段提交在两阶段提交上的改进就是在之前多了一步:

在锁定资源之前先进行查询,确认是否可提交。我们姑且将其称之为第 0 步。

好处是什么?

还是以之前“用户下单购买某个商品”为例。对于这个场景,第 0 步会检查向优惠券服务检查优惠券是否可用、向支付业务检查账户余额是否足够、向仓储服务检查库存是否足够等。

只有当第 0 步全部返回成功时,才会执行第一步的锁定资源。这时的第一步也几乎可以全部返回成功(只有并发情况下会失败)。

因此,对于执行者而言,如果一直没有收到第二步(实际上的第三步)的事务,超时可以默认执行 confirm 操作。大多数情况下都会成功避免数据不一致。(只有并发竞争情况下有可能失败)

简而言之,三阶段提交相比两阶段提交多了第 0 步检查是否可提交。执行者的默认超时行为从 cancel 改为 confirm。

总结三阶段提交

我们可以看到,三阶段提交的确成功提高了并发,降低了反复的“锁定-释放”的可能。

然而他并没有完全解决二阶段提交数据不一致的问题,只是极大概率避免了数据不一致的可能性。在极端情况下:由于高并发,多个请求同时通过了第 0 步检查,部分却在第 1 步锁定失败,本应 cancel 却因为网络或其他问题导致部分(或全部)执行者没有收到 cancel 命令默认 confirm 了事务,导致了数据不一致。

下一章聊聊 TCC。