最终一致性分布式事务解决方案

一、概述

  • 强一致性分布式事务解决方案要求参与事务的各个节点的数据时刻保持一致,查询任意节点的数据都能得到最新的数据结果。这就导致在分布式场景,尤其是高并发场景下,系统的性能受到影响
  • 而最终一致性分布式事务解决方案并不要求参与事务的各节点数据时刻保持一致,允许其存在中间状态,只要一段时间后,能够达到数据的最终一致状态即可

1. 简介

  • 在最终一致性分布式事务解决方案中,每个服务都存在中间状态,服务与服务之间不必保持强一致性,允许在某个时刻查询出来的数据存在短暂的不一致性,经过一段时间后,各个服务之间的数据能够达到最终一致性,这样极大地提高了系统的整体性能并降低了分布式事务执行过程中出错的概率
  • 适用场景:比如在电商支付场景中,会涉及订单服务、支付服务、库存服务、积分服务、仓储服务等环节,每个服务都是单独部署的。订单服务会调用支付服务生成交易流水,订单服务会调用库存服务扣减商品库存,订单服务会调用积分服务为用户的账户增加积分,订单服务会调用仓储服务生成出库单。如果这一系列的服务调用操作使用强一致性分布式事务,很容易造成系统性能低下,导致系统卡顿,并且服务与服务之间的交互是通过网络进行的,由于网络的不稳定性,就会导致服务之间的调用出现各种各样的问题,难以完成强一致性分布式事务的提交操作
  • 优点:
    • 性能高,不会因长时间持有事务占用的资源而消耗过多的性能
    • 具备可用性
    • 适合高并发场景
  • 缺点:
    • 在某个时刻查询出的数据状态可能会不一致
    • 不适用对于事务一致性要求特别高的场景

2. 典型方案

  • TCC 解决方案
  • 可靠消息最终一致性解决方案
  • 最大努力通知型解决方案

二、服务模式

  • 最终一致性分布式事务解决方案存在 4 种典型的服务模式,分别为可查询操作、幂等操作、TCC 操作和可补偿操作

1. 可查询操作

  • 可查询操作服务模式需要服务的操作具有可标识性,主要体现在服务的操作具有全局唯一的标识,如订单号、交易流水号等,同时也要有完整的操作时间信息
  • 在可查询操作中,业务服务既要提供操作业务的接口,也要提供查询某条业务数据的接口和批量查询业务数据的接口
  • 比如在如下的订单操作中,除了更新订单状态的操作为本地操作外,其他操作都需要调用 RPC 接口来执行,在这种情况下就需要引入分布式事务。在分布式事务的执行过程中,如果出现了错误,需要明确知道其他操作的处理情况,此时就需要其他服务提供可查询的接口,以保证通过可查询的接口获取其他服务的处理情况
1
2
3
4
5
6
7
8
9
10
11
12
public void handleOrder() {
// 订单服务本地更新订单状态
orderDao.update();
// 调用资金账户服务给资金账户扣款
accountService.update();
// 调用积分服务给积分账户增加积分
pointService.update();
// 调用会计服务向会计系统写入会计原始凭证
accountingService.insert();
// 调用物流服务生成物流信息
logisticsService.save();
}

2. 幂等操作

  • 幂等操作服务模式要求操作具有幂等性。幂等性指的是对于同一个方法来说,只要参数相同,无论执行多少次都与第一次执行时产生的影响相同
  • 业务服务对外提供操作业务数据的接口,并且需要在接口的实现中保证对数据处理的幂等性
  • 在分布式环境中,难免会出现数据不一致的情况。比如为了保证数据的最终一致性,系统会提供很多重试操作。如果这些重试操作涉及的方法中,某些方法的实现不具有幂等性,则即使重试操作成功了,也无法保证数据的最终一致性
  • 实现幂等性的方式通常有两种:一种是通过业务操作本身实现幂等性;另一种是通过系统缓存所有的请求与处理结果,当再次检测到相同的请求时,直接返回之前缓存的处理结果

3. TCC 操作

  • TCC 操作服务模式主要包括 3 个阶段,分别为 Try 阶段(尝试业务执行)、Confirm 阶段(确定业务执行)和 Cancel 阶段(取消业务执行)
  • Try 阶段:
    • 完成所有业务的一致性检查
    • 预留必要的业务资源,并需要与其他操作隔离
  • Confirm 阶段:
    • 真正执行业务操作
    • 因为在 Try 阶段完成了业务的一致性检查,所以此阶段不会做任何业务检查
    • 只用 Try 阶段预留的业务资源进行操作
    • 此阶段的操作需要满足幂等性
  • Cancel 阶段:
    • 释放 Try 阶段预留的业务资源
    • 此阶段的操作需要满足幂等性

4. 可补偿操作

  • 在分布式系统中,如果某些数据处于不正常的状态,需要通过某种方式进行业务补偿,使数据能够达到最终一致性
  • 业务服务对外提供操作数据的接口时,也需要对外提供补偿业务的接口,当其他服务调用业务服务操作数据的接口出现异常时,能够通过补偿接口进行业务补偿操作
    • 在执行业务操作时,完成业务操作并返回业务操作结果,这些操作结果对外部都是可见的
    • 在进行业务补偿时,能够补偿或者抵消正向业务操作的结果,并且业务补偿操作需要满足幂等性

三、TCC 解决方案

1. 适用场景

  • TCC 解决方案适用于具有强隔离性、严格一致性要求的业务场景,也适用于执行时间比较短的业务
  • 比如对于电商业务场景中的下单减库存等业务,如果使用 TCC 分布式事务,则会经过 Try、Confirm、Cancel 三个阶段
    • Try 阶段:提交订单并将订单的状态设置为待提交,调用库存服务预扣减库存。具体操作为在库存数据表中将商品库存字段的数据减去提交订单时传递的商品数量,同时在预扣减库存字段中增加提交订单时传递的商品数量
    • Confirm 阶段:如果 Try 阶段的操作全部执行成功,则执行 Confirm 阶段。订单服务将订单数据的状态标记为已提交,库存服务则将库存数据表中预扣减库存字段的数据减去提交订单时传递的商品数量,实现真正扣减库存
    • Cancel 阶段:如果 Try 阶段执行失败或者抛出异常,则执行 Cancel 阶段。订单服务将订单数据的状态标记为已取消,库存服务将库存数据表中商品库存字段的数据增加提交订单时传递的商品数量,同时对预扣减库存字段的数据减去提交订单时传递的商品数量,实现事务回滚

2. 需要实现的服务模式

  • TCC 操作:需要实现 Try、Confirm 和 Cancel 三个阶段的业务逻辑
  • 幂等操作:TCC 操作中每个阶段的方法都需要实现幂等性
  • 可补偿操作:如果在执行分布式事务的过程中,业务服务或者网络出现了异常情况,则需要支持重试操作,以达到事务补偿的目的
  • 可查询操作:业务服务需要提供可以查询自身内部事务状态的接口,以供其他服务调用

3. 方案的执行流程

  • Try 阶段:不会执行任何业务逻辑,仅做业务的一致性检查和预留相应的资源,这些资源能够和其他操作保持隔离
  • Confirm 阶段:当 Try 阶段所有分支事务执行成功后开始执行 Confirm 阶段。通常情况下会认为 Confirm 阶段是不会出错的,即只要 Try 阶段的操作执行成功了,Confirm 阶段就一定会执行成功。如果 Confirm 阶段出错了,就需要引入重试机制或人工处理,对出错的事务进行干预
  • Cancel 阶段:在业务执行异常或出现错误的情况下,需要回滚事务的操作,执行分支事务的取消操作,并且释放 Try 阶段预留的资源。通常情况下会认为 Cancel 阶段也是一定会执行成功的。如果 Cancel 阶段出错了,也需要引入重试机制或人工处理,对出错的事务进行干预

4. 优缺点

  • 优点:
    • 在应用层实现具体逻辑,锁定资源的粒度变小,提升了系统的性能
    • Confirm 阶段和 Cancel 阶段的方法具备幂等性,能够保证分布式事务执行完毕后数据的一致性
    • TCC 分布式事务解决方案由主业务发起整个事务,无论是主业务还是分支事务所在的业务,都能部署为集群模式,从而解决了 XA 规范的单点故障问题
  • 缺点:
    • 代码需要耦合到具体业务中,每个参与分布式事务的业务方法都要拆分成 Try、Confirm 和 Cancel 三个阶段的方法,提高了开发成本

5. 需要注意的问题

(1) 空回滚问题

  • 原因:一个分支事务所在的服务器宕机或者网络发生异常,此分支事务调用失败,此时并未执行此分支事务 Try 阶段的方法。当服务器或者网络恢复后,TCC 分布式事务执行回滚操作,会调用分支事务 Cancel 阶段的方法,如果 Cancel 阶段的方法不能处理这种情况,就会出现空回滚问题
  • 解决方案:判断是否执行了 Try 阶段的方法,如果执行了 Try 阶段的方法,就没有空回滚,否则就出现了空回滚。具体解决方案是在主业务发起全局事务时,生成全局事务记录,并为全局事务记录生成一个唯一的全局事务 ID,这个全局事务 ID 会贯穿整个分布式事务的执行流程。再创建一张分支事务记录表,用于记录分支事务,将全局事务 ID 和分支事务 ID 保存到分支事务表中。执行 Try 阶段的方法时,会向分支事务记录表中插入一条记录,表示执行了 Try 阶段。当事务回滚执行 Cancel 阶段的方法时,首先读取分支事务表中的数据,如果存在 Try 阶段插入的数据,则执行正常操作回滚事务,否则为空回滚,不做任何操作

(2) 幂等问题

  • 原因:由于服务器宕机、应用崩溃或者网络异常等原因,可能会出现方法调用超时的情况,为了保证方法的正常执行,往往会在 TCC 方案中加入超时重试机制。因为超时重试有可能导致数据不一致的问题,所以需要保证分支事务的执行以及 TCC 方案的 Confirm 阶段和 Cancel 阶段具备幂等性
  • 解决方案:在分支事务记录表中增加事务的执行状态,每次执行分支事务以及 Confirm 阶段和 Cancel 阶段的方法时,都查询此事务的执行状态,以此判断事务的幂等性

(3) 悬挂问题

  • 原因:通过 RPC 调用分支事务 Try 阶段的方法时,会先注册分支事务,再执行 RPC 调用。如果此时发生服务器宕机、应用崩溃或者网络异常等情况,RPC 调用就会超时,事务管理器会通知对应的资源管理器回滚事务。可能资源管理器回滚完事务后,RPC 请求到达了参与分支事务所在的业务方法,因为此时事务已经回滚,所以在 Try 阶段预留的资源就无法释放了,这种情况就称为悬挂。总之,悬挂问题就是预留业务资源后,无法继续往下处理
  • 解决方案:如果执行了 Confirm 阶段或者 Cancel 阶段的方法,则 Try 阶段的方法就不能再执行。具体方案是在执行 Try 阶段的方法时,判断分支记录表中是否已经存在同一全局事务下 Confirm 阶段或者 Cancel 阶段的事务记录,如果存在,则不再执行 Try 阶段的方法

四、可靠消息最终一致性解决方案

  • 可靠消息最终一致性分布式事务解决方案指的是事务的发起方执行完本地事务之后,发出一条消息,事务的参与方,也就是消息的消费者一定能够接收到这条消息并处理成功。这个方案强调的是只要事务发起方将消息发送给事务参与方,事务参与方就一定能够执行成功,事务最终达到一致的状态

1. 适用场景

  • 适用于消息数据能够独立存储,能够降低系统之间耦合度,并且业务对数据一致性的时间敏感度高的场景
  • 以电商支付场景,向用户发放优惠券为例,具体流程为订单服务向 RocketMQ 发送 Half 消息,发送成功后,RocketMQ 会向订单服务响应 Half 消息发送成功的状态。接下来,订单服务执行本地事务,修改订单数据的状态,并向 RocketMQ 发送提交事务或者回滚事务的消息。如果是提交事务的消息,则 RocketMQ 会向优惠券服务投递事务消息,优惠券服务收到消息后,会执行为用户发放优惠券的逻辑。如果是回滚消息,则 RocketMQ 会删除相应的消息,不再向优惠券服务投递对应的事务消息

2. 需要实现的服务模式

  • 可查询操作:在具体实现的过程中,需要参与分布式事务的业务服务提供可查询自身事务状态的接口,在发生异常时,能够让其他服务通过查询接口查询具体的事务状态
  • 幂等操作:参与分布式事务的各个业务接口需要保证数据操作的幂等性

3. 方案的执行流程

  • 首先,事务发起方将消息发送给可靠消息服务,这里的可靠消息服务可以基于本地数据表实现,也可以基于消息队列中间件实现。然后,事务参与方从可靠消息服务中接收消息
  • 事务发起方和可靠消息服务之间、可靠消息服务和事务参与方之间都是通过网络进行通信的,由于网络本身的不稳定性,可能会造成分布式事务问题,因此在实现上,需要引入消息确认服务和消息恢复服务
    • 消息确认服务:定期检测事务发起方业务的执行状态和消息库中的数据,如果发现事务发起方业务的执行状态与消息库中的数据不一致,消息确认服务就会同步事务发起方的业务数据和消息库中的数据,保证数据一致性,确保事务发起方业务完成本地事务后消息一定会发送成功
    • 消息恢复服务:定期检测事务参与方业务的执行状态和消息库中的数据,如果发现事务参与方业务的执行状态与消息库中的数据不一致(这里的不一致,通常指的是事务参与方消费消息后,执行本地事务操作失败,导致事务参与方本地事务的执行状态与消息库中的数据不一致),消息恢复服务就会恢复消息库中消息的状态,使消息的状态回滚为事务发起方发送消息成功,但未被事务参与方消费的状态

4. 优缺点

  • 基于本地消息表实现的最终消息一致性方案
    • 优点:
      • 在业务应用中实现了消息的可靠性,减少了对消息中间件的依赖
    • 缺点:
      • 绑定了具体的业务场景,耦合性太高,不可公用和扩展
      • 消息数据与业务数据在同一个数据库,占用了业务系统的资源
      • 消息数据可能会受到数据库并发性的影响
  • 基于消息队列中间件实现的最终消息一致性方案
    • 优点:
      • 消息数据能够独立存储,与具体的业务数据库解耦
      • 消息的并发性和吞吐量优于本地消息表方案
    • 缺点:
      • 发送一次消息需要完成两次网络交互,一次是消息的发送,另一次是消息的提交或回滚
      • 需要实现消息的回查接口,增加了开发成本

5. 需要注意的问题

(1) 事务发送方本地事务与消息发送的原子性问题

  • 原因:可靠消息最终一致性要求事务发起方的本地事务与消息发送的操作具有原子性,也就是事务发起方执行本地事务成功后,一定要将消息发送出去,执行本地事务失败后,一定要丢弃消息
  • 解决方案:可以通过消息确认服务解决本地事务与消息发送的原子性问题

(2) 事务参与方接收消息的可靠性问题

  • 原因:由于服务器宕机、服务崩溃或网络异常等原因,导致事务参与方不能正常接收消息,或者接收消息后处理事务的过程中发生异常,无法将结果正确回传到消息库中。此时就会产生可靠性问题
  • 解决方案:可以通过消息恢复服务保证事务参与方的可靠性

(3) 事务参与方接收消息的幂等性问题

  • 原因:在实际场景中,由于某种原因,可靠消息服务可能会多次向事务参与方发送消息,如果事务参与方的方法不具有幂等性,就会造成消息重复消费的问题
  • 解决方案:事务参与方的方法实现要具有幂等性,只要参数相同,无论调用多少次接口或方法,得出的结果都与第一次调用接口或方法得出的结果相同

五、最大努力通知型解决方案

  • 当分布式事务跨越多个不同的系统,尤其是不同企业之间的系统时,解决分布式事务问题就需要用到最大努力通知型方案

1. 适用场景

  • 适用于最终一致性时间敏感度低的场景,并且事务被动方的处理结果不会影响主动方的处理结果
  • 比如支付成功后,支付平台异步通知商户支付结果

2. 需要实现的服务模式

  • 幂等操作:例如在充值业务场景中,用户调用支付服务充值成功后,支付服务会按照一定的阶梯型通知规则调用账户服务的接口,向账户服务发送支付数据。此时,账户服务的接口需要满足幂等性
  • 可查询操作:如果支付服务调用账户服务的接口超过了设置的最大次数,仍然没有调用成功,则支付服务需要提供查询支付结果的接口,以便账户服务调用并恢复丢失的业务

3. 方案的执行流程

  • 业务主动方在完成业务处理后,会向业务被动方发送消息通知。发送消息通知时,允许消息丢失
  • 在实现上,业务主动方可以设置时间阶梯型通知规则,在消息通知失败后,可以按照规则再次通知,直到到达最大通知次数为止
  • 业务主动方需要提供查询接口供业务被动方按照需要查询,用于恢复丢失的消息

4. 优缺点

  • 优点:
    • 能够实现跨企业的数据一致性
    • 业务被动方的处理结果不会影响业务主动方的处理结果
    • 能够快速接入其他业务系统,达到业务数据一致性
  • 缺点:
    • 只适用于时间敏感度低的场景
    • 业务主动方发送的消息可能丢失,造成业务被动方收不到消息
    • 需要业务主动方提供查询消息的接口,业务被动方需要按照主动方的接口要求查询数据,增加了开发成本

5. 需要注意的问题

(1) 消息重复通知的问题

  • 原因:由于业务主动方发送消息通知后,业务被动方不一定能够接收到消息,因此需要按照一定的阶梯型通知规则重复向业务被动方发送消息通知。此时,就出现了消息重复通知的情况,因为业务被动方的方法被执行了多次,所以有可能造成数据不一致的问题
  • 解决方案:保证业务被动方接收消息通知的方法具备幂等性,则在业务上就能够解决消息重复通知的问题

(2) 消息通知丢失的问题

  • 原因:如果业务主动方尽最大努力都没有将消息通知给业务被动方,或者业务被动方接收到消息并执行完毕后,需要再次获取消息。此时,业务主动方已经删除对应的通知消息,不再向业务被动方发送消息通知
  • 解决方案:业务主动方需要提供查询消息的接口来满足业务被动方主动查询消息的需求,以恢复丢失的业务。另外,业务主动方在设计消息回查接口时,一定要注意接口的安全性和并发性

6. 与可靠消息最终一致性的区别

  • 设计不同:
    • 可靠消息最终一致性方案需要事务发起方一定要将消息发送成功
    • 最大努力通知型方案中,业务主动方尽最大努力将消息通知给业务被动方,但消息可能会丢失,业务被动方不一定能够接收到消息
  • 业务场景不同:
    • 可靠消息最终一致性方案适用于时间敏感度高的场景,以异步的方式达到事务的最终一致
    • 最大努力通知型方案适用于时间敏感度低的场景,业务主动方只需要将处理结果通知出去
  • 解决的问题不同:
    • 可靠消息最终一致性方案解决的是消息从事务发起方发出,到事务参与方接收的一致性,并且事务参与方接收到消息后,能够正确地执行事务操作,达到事务最终一致
    • 最大努力通知型方案虽然无法保证消息从业务主动方发出到业务被动方接收的一致性,但是能够提供消息接收的可靠性。这里的可靠性包括业务被动方能够接收到业务主动方通知的消息,以及业务被动方能够主动查询业务主动方提供的消息回查接口来恢复丢失的业务

参考

  • 《深入理解分布式事务:原理与实战》