046 弹力设计篇之“补偿事务”

前面,我们说过,分布式系统有一个比较明显的问题就是,一个业务流程需要组合一组服务。这样的事情在微服务下就更为明显了,因为这需要业务上的一致性的保证。也就是说,如果一个步骤失败了,那么要么回滚到以前的服务调用,要么不断重试保证所有的步骤都成功。

这里,如果需要强一性的需求,那么,在业务层上需要使用“两阶段提交”这样的方式。但是好在我们的很多情况下并不需要这么强的一致性,而且强一致性的最佳保证最好是在底层完成。或是像之前说的那样 Stateful 的 Sticky Session 那样在一台机器上完成。在我们接触到的大多数业务,其实只需要最终一致性就好。

ACID 和 BASE

谈到这里,有必要先说一下 ACID 和 BASE 的差别。传统关系型数据库系统的事务都有 ACID 属性,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation,又称独立性)、持久性(Durability)。

事务的 ACID 属性保证了数据库的一致性,比如银行系统中,转账就是一个事务,从原账户扣除金额,以及向目标账户添加金额,这两个数据库操作的总和构成一个完整的逻辑过程,是不可拆分的原子操作,从而保证了整个系统中的总金额没有变化。

然而,这对于我们的分布式系统来说,尤其是微服务来说,这样的方式是很难适合高性能的要求的。我们都很熟悉 CAP 理论——在分布式的服务架构中,一致性(Consistency)、可用性(Availability)、分区容忍性(Partition Tolerance),在现实中不能都满足,最多只能满足其中两个。

所以,为了提高性能,出现了 ACID 的一个变种 BASE。

可以看到,BASE 系统是允许或是容忍系统出现暂时性的问题的,这样一来,我们的系统就能更有弹力。因为我们知道,在分布式系统的世界里,故障是不可避免的,我们能做的就是把故障处理当成功能写入代码中,这就是 Design for Failure。

BASE 的系统倾向于设计出更加有弹力的的系统,这种系统的设计特点是,要保证在短时间内,就算是有数据不同步的风险,我们也应该允许新的交易可以发生,而后面我们在业务上将可能出现问题的事务给处理掉,以保证最终的一致性。

举个例子,网上卖书的场景。

ACID 的玩法就是,大家在买同一本书的过程中,每个用户的购买请求都需要把库存锁住,等减完库存后,把锁释放出来,后续的人才能进行购买。于是,在 ACID 的玩法下,我们在同一时间不可能有多个用户下单,我们的订单流程需要有排队的情况,这样一来,我们就不可能做出性能比较高的系统来。

BASE 的玩法是,大家都可以同时下单,这个时候不需要去真正地分配库存,然后系统异步地处理订单,而且是批量的处理。因为下单的时候没有真正去扣减库存,所以,有可能会有超卖的情况。而后台的系统会异步地处理订单时,发现库存没有了,于是才会告诉用户你没有购买成功。

BASE 这种玩法,其实就是亚马逊的玩法,因为要根据用户的地址去不同的仓库查看库存,这个操作非常耗时,所以,不想做成异步的都不行。

在亚马逊上买东西,你会收到一封邮件说,系统收到你的订单了,然后过一会儿你会收到你的订单被确认的邮件,这时候才是真正地分配了库存。所以,有某些时候,你会遇到你先收到了下单的邮件,过一会又收到了没有库存的致歉的邮件。

有趣的是,ACID 的意思是酸,而 BASE 却是碱的意思,因此这是一个对立的东西。其实,从本质上来讲,酸(ACID)强调的是一致性(CAP 中的 C),而碱(BASE)强调的是可用性(CAP 中的 A)。

业务补偿

有了上面对 ACID 和 BASE 的分析,我们知道,在很多情况下,我们是无法做到强一致的 ACID 的。特别是我们需要跨多个系统的时候,而且这些系统还不是由一个公司所提供的。比如,在我们的日常生活中,我们经常会遇到这样的情况,就是要找很多方协调很多事,而且要保证我们每一件事都成功,否则整件事就做不到。

比如,要出门旅游, 我们需要干这么几件事。第一,向公司请假,拿到相应的假期;第二,订飞机票或是火车票;第三,订酒店;第四,租车。这四件事中,前三件必需完全成功,我们才能出行,而第四件事只是一个锦上添花的事,但第四件事一旦确定,那么也会成为整个事务的一部分。这些事都是要向不同的组织或系统请求。我们可以并行地做这些事,而如果某个事有变化,其它的事都会跟着出现一些变化。

设想下面的几种情况。

  1. 我没有订到返程机票,那么我就去不了了。我需要把订到的去程机票,酒店、租到的车都给取消了,并且把请的假也取消了。
  2. 如果我假也请好了,机票,酒店也订好了,只是车没租到,那么并不影响我出行这个事,整个事还是可以继续的。
  3. 如果我的飞机因为天气原因取消或是晚点了,那么我被迫要去调整和修改我的酒店预订和租车的预订。

从人类的实际生活当中,我们可以看出,上述的这些情况都是天天在发生的事情。所以,我们的分布式系统也是一样的,也是需要处理这样的事情——就是当条件不满足,或是有变化的时候,需要从业务上做相应的整体事务的补偿。

一般来说,业务的事务补偿都是需要一个工作流引擎的。亚马逊是一个超级喜欢工作流引擎的公司,这个工作流引擎把各式各样的服务给串联在一起,并在工作流上做相应的业务补偿,整个过程设计成为最终一致性的。

对于业务补偿来说,首先需要将服务做成幂等性的,如果一个事务失败了或是超时了,我们需要不断地重试,努力地达到最终我们想要的状态。然后,如果我们不能达到这个我们想要的状态,我们需要把整个状态恢复到之前的状态。另外,如果有变化的请求,我们需要启动整个事务的业务更新机制。

所以,一个好的业务补偿机制需要做到下面这几点。

  1. 要能清楚地描述出要达到什么样的状态(比如:请假、机票、酒店这三个都必须成功,租车是可选的),以及如果其中的条件不满足,那么,我们要回退到哪一个状态。这就是所谓的整个业务的起始状态定义。
  2. 当整条业务跑起来的时候,我们可以串行或并行地做这些事。对于旅游订票是可以并行的,但是对于网购流程(下单、支付、送货)是不能并行的。总之,我们的系统需要努力地通过一系列的操作达到一个我们想要的状态。如果达不到,就需要通过补偿机制回滚到之前的状态。这就是所谓的状态拟合
  3. 对于已经完成的事务进行整体修改。其实可以考虑成一个修改事务。

其实,在纯技术的世界里也有这样的事。比如,线上运维系统需要发布一个新的服务或是对一个已有的服务进行水平扩展,我们需要先找到相应的机器,然后初始化环境,再部署上应用,再做相应的健康检查,最后接入流量。这一系列的动作都要完全成功,所以,我们的部署系统就需要管理好整个过程和相关的运行状态。

业务补偿的设计重点

业务补偿主要做两件事。

  1. 努力地把一个业务流程执行完成。
  2. 如果执行不下去,需要启动补偿机制,回滚业务流程。

所以,下面是几个重点。

小结

好了,我们来总结一下今天分享的主要内容。首先,我介绍了 ACID 和 BASE 两种不同级别的一致性。在分布式系统中,ACID 有更强的一致性,但可伸缩性非常差,仅在必要时使用;BASE 的一致性较弱,但有很好的可伸缩性,还可以异步批量处理;大多数分布式事务适合 BASE。

要实现 BASE 事务,需要实现补偿逻辑,因为事务可能失败,此时需要协调各方进行撤销。补偿的各个步骤可以根据具体业务来确定是串行还是并行。由于补偿事务是和业务强相关的,所以必须实现在业务逻辑里。下篇文章中,我们讲述重试设计。希望对你有帮助。

也欢迎你分享一下你的分布式服务用到了怎样的一致性?你是怎么实现补偿事务的?