微服务分布式事务-补偿模式

为了描述方便,这里先定义两个概念:

  • 业务异常:业务逻辑产生错误的情况,比如账户余额不足、商品库存不足等。

  • 技术异常:非业务逻辑产生的异常,如网络连接异常、网络超时等。

补偿模式使用一个额外的协调服务来协调各个需要保证一致性的微服务,协调服务按顺序调用各个微服务,如果某个微服务调用异常(包括业务异常和技术异常)就取消之前所有已经调用成功的微服务。

补偿模式建议仅用于不能避免出现业务异常的情况,如果有可能应该优化业务模式,以避免要求补偿事务。如账户余额不足的业务异常可通过预先冻结金额的方式避免,商品库存不足可要求商家准备额外的库存等。

我们通过一个实例来说明补偿模式,一家旅行公司提供预订行程的业务,可以通过公司的网站提前预订飞机票、火车票、酒店等。

假设一位客户规划的行程是:

  • 上海-北京6月19日9点的某某航班。

  • 某某酒店住宿3晚。

  • 北京-上海6月22日17点火车。

在客户提交行程后,旅行公司的预订行程业务按顺序串行的调用航班预订服务、酒店预订服务、火车预订服务。最后的火车预订服务成功后整个预订业务才算完成。

640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1

如果火车票预订服务没有调用成功,那么之前预订的航班、酒店都得取消。取消之前预订的酒店、航班即为补偿过程。

640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1

为了降低开发的复杂性和提高效率,协调服务实现为一个通用的补偿框架。补偿框架提供服务编排和自动完成补偿的能力。

要实现补偿过程,我们需要做到两点:

首先要确定失败的步骤和状态,从而确定需要补偿的范围。

640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1

在上面的例子中我们不仅要知道第 3 个步骤(预订火车)失败,还要知道失败的原因。如果是因为预订火车服务返回无票,那么补偿过程只需要取消前两个步骤就可以了;但是如果失败的原因是因为网络超时,那么补偿过程除前两个步骤之外还需要包括第 3 个步骤。

其次要能提供补偿操作使用到的业务数据。


比如一个支付微服务的补偿操作要求参数包括支付时的业务流水 id、账号和金额。理论上说实际完成补偿操作可以根据唯一的业务流水 id 就可以,但是提供更多的要素有益于微服务的健壮性,微服务在收到补偿操作的时候可以做业务的检查,比如检查账户是否相等,金额是否一致等等。

640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1

做到上面两点的办法是记录完整的业务流水,可以通过业务流水的状态来确定需要补偿的步骤,同时业务流水为补偿操作提供需要的业务数据。

640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1

当客户的一个预订请求达到时,协调服务(补偿框架)为请求生成一个全局唯一的业务流水号,并在调用各个工作服务的同时记录完整的状态。

  1. 记录调用 bookFlight 的业务流水,调用 bookFlight 服务,更新业务流水状态。

  2. 记录调用 bookHotel 的业务流水,调用 bookHotel 服务,更新业务流水状态。

  3. 记录调用 bookTrain 的业务流水,调用 bookTrain 服务,更新业务流水状态。

当调用某个服务出现异常时,比如第 3 步骤(预订火车)异常。

640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1

协调服务(补偿框架)同样会记录第 3 步的状态,同时会另外记录一条事件,说明业务出现了异常。然后就是执行补偿过程了,可以从业务流水的状态中知道补偿的范围,补偿过程中需要的业务数据从记录的业务流水中获取。

对于一个通用的补偿框架来说,预先知道微服务需要记录的业务要素是不可能的。那么就需要一种方法来保证业务流水的可扩展性,这里介绍两种方法:大表和关联表。

640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1

大表顾明思议就是设计时除必须的字段外,还需要预留大量的备用字段,框架可以提供辅助工具来帮助将业务数据映射到备用字段中。

关联表,分为框架表和业务表,技术表中保存为实现补偿操作所需要的技术数据,业务表保存业务数据,通过在技术表中增加业务表名和业务表主键来建立和业务数据的关联。

大表对于框架层实现起来简单,但是也有一些难点,比如预留多少字段合适,每个字段又需要预留多少长度。另外一个难点是如果向从数据层面来查询数据,很难看出备用字段的业务含义,维护过程不友好。

关联表在业务要素上更灵活,能支持不同的业务类型记录不同的业务要素;但是对于框架实现上难度更高,另外每次查询都需要复杂的关联动作,性能方面会受影响。

有了上面的完整的流水记录,协调服务就可以根据工作服务的状态在异常时完成补偿过程。但是补偿由于网络等原因,补偿操作并不一定能保证 100%成功,这时候我们还要做更多一点。

640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1

补偿过程作为一个服务调用过程同样存在调用不成功的情况,这个时候需要通过重试的机制来保证补偿的成功率。当然这也就要求补偿操作本身具备幂等性。

关于幂等性的实现在前面做过讨论。

如果只是一味的失败就立即重试会给工作服务造成不必要的压力,我们要根据服务执行失败的原因来选择不同的重试策略。

640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1

1) 如果失败的原因不是暂时性的,由于业务因素导致(如业务要素检查失败)的业务错误,这类错误是不会重发就能自动恢复的,那么应该立即终止重试。

2) 如果错误的原因是一些罕见的异常,比如因为网络传输过程出现数据丢失或者错误,应该立即再次重试,因为类似的错误一般很少会再次发生。

3) 如果错误的原因是系统繁忙(比如 HTTP 协议返回的 500 或者另外约定的返回码)或者超时,这个时候需要等待一些时间再重试。

重试操作一般会指定重试次数上线,如果重试次数达到了上限就不再进行重试了。这个时候应该通过一种手段通知相关人员进行处理。

对于等待重试的策略如果重试时仍然错误,可逐渐增加等待的时间,直到达到一个上限后,以上限作为等待时间。

如果某个时刻聚集了大量需要重试的操作,补偿框架需要控制请求的流量,以防止对工作服务造成过大的压力。

另外关于补偿模式还有几点补充说明。

  1. 微服务实现补偿操作不是简单的回退到业务发生时的状态,因为可能还有其他的并发的请求同时更改了状态。一般都使用逆操作的方式完成补偿。

  2. 补偿过程不需要严格按照与业务发生的相反顺序执行,可以依据工作服务的重用程度优先执行,甚至是可以并发的执行。

  3. 有些服务的补偿过程是有依赖关系的,被依赖服务的补偿操作没有成功就要及时终止补偿过程。

  4. 如果在一个业务中包含的工作服务不是都提供了补偿操作,那我们编排服务时应该把提供补偿操作的服务放在前面,这样当后面的工作服务错误时还有机会补偿。

  5. 设计工作服务的补偿接口时应该以协调服务请求的业务要素作为条件,不要以工作服务的应答要素作为条件。因为还存在超时需要补偿的情况,这时补偿框架就没法提供补偿需要的业务要素。