凤凰架构:构建可靠的大型分布式系统
上QQ阅读APP看书,第一时间看更新

3.4.2 可靠事件队列

最终一致性的概念是由eBay的系统架构师Dan Pritchett在2008年在ACM发表的论文“Base:An Acid Alternative”[1]中提出的,该论文总结了一种独立于ACID获得的强一致性之外的、使用BASE来达成一致性目的的途径。BASE分别是基本可用性(Basically Available)、柔性事务(Soft State)和最终一致性(Eventually Consistent)的缩写。BASE这种提法简直是把数据库科学家酷爱凑缩写的恶趣味发挥到淋漓尽致,不过有ACID vs BASE(酸vs碱)这个朗朗上口的梗,该论文的影响力的确传播得足够快。在这里笔者就不多谈BASE中的概念问题了,虽然调侃它是恶趣味,但这篇论文本身作为最终一致性的概念起源,并系统性地总结了一种针对分布式事务的技术手段,是非常有价值的。

我们继续以本章的场景事例来解释Dan Pritchett提出的“可靠事件队列”的具体做法,目标仍然是交易过程中正确修改账号、仓库和商家服务中的数据,图3-7列出了修改过程的时序图。

图3-7 修改过程时序图

1)最终用户向Fenix’s Bookstore发送交易请求:购买一本价值100元的《深入理解Java虚拟机》。

2)Fenix’s Bookstore首先应对用户账号扣款、商家账号收款、库存商品出库这三个操作有一个出错概率的先验评估,根据出错概率的大小来安排它们的操作顺序,这种评估一般直接体现在程序代码中,一些大型系统也可能会实现动态排序。譬如,根据统计,最有可能出现的交易异常是用户购买了商品,但是不同意扣款,或者账号余额不足;其次是仓库发现商品库存不够,无法发货;风险最低的是收款,如果到了商家收款环节,一般就不会出什么意外了。那最容易出错的就应该最先进行,即:账号扣款→仓库出库→商家收款。

3)账号服务进行扣款业务,如扣款成功,则在自己的数据库建立一张消息表,里面存入一条消息:“事务ID:某UUID,扣款:100元(状态:已完成),仓库出库《深入理解Java虚拟机》:1本(状态:进行中),某商家收款:100元(状态:进行中)”。注意,这个步骤中“扣款业务”和“写入消息”是使用同一个本地事务写入账号服务自己的数据库的。

4)在系统中建立一个消息服务,定时轮询消息表,将状态是“进行中”的消息同时发送到库存和商家服务节点中去(也可以串行地发,即一个成功后再发送另一个,但在我们讨论的场景中没必要)。这时候可能产生以下几种情况。

·商家和仓库服务都成功完成了收款和出库工作,向用户账号服务器返回执行结果,用户账号服务把消息状态从“进行中”更新为“已完成”。整个事务顺利结束,达到最终一致性的状态。

·商家或仓库服务中至少有一个因网络原因,未能收到来自用户账号服务的消息。此时,由于用户账号服务器中存储的消息状态一直处于“进行中”,所以消息服务器将在每次轮询的时候持续地向未响应的服务重复发送消息。这个步骤的可重复性决定了所有被消息服务器发送的消息都必须具备幂等性,通常的设计是让消息带上一个唯一的事务ID,以保证一个事务中的出库、收款动作会且只会被处理一次。

·商家或仓库服务有某个或全部无法完成工作,譬如仓库发现《深入理解Java虚拟机》没有库存了,此时,仍然是持续自动重发消息,直至操作成功(譬如补充了新库存),或者被人工介入为止。由此可见,可靠事件队列只要第一步业务完成了,后续就没有失败回滚的概念,只许成功,不许失败。

·商家和仓库服务成功完成了收款和出库工作,但回复的应答消息因网络原因丢失,此时,用户账号服务仍会重新发出下一条消息,但因操作具备幂等性,所以不会导致重复出库和收款,只会导致商家、仓库服务器重新发送一条应答消息,此过程持续自动重复直至双方网络通信恢复正常。

·也有一些支持分布式事务的消息框架,如RocketMQ,原生就支持分布式事务操作,这时候上述第二、四种情况也可以交由消息框架来保障。

以上这种依靠持续重试来保证可靠性的解决方案谈不上是Dan Pritchett的首创或者独创,它在计算机的其他领域中已被频繁使用,也有了专门的名字——“最大努力交付”(Best-Effort Delivery),譬如TCP协议中未收到ACK应答自动重新发包的可靠性保障就属于最大努力交付。而可靠事件队列还有一种更普通的形式,被称为“最大努力一次提交”(Best-Effort 1PC),指的是将最有可能出错的业务以本地事务的方式完成后,采用不断重试的方式(不限于消息系统)来促使同一个分布式事务中的其他关联业务全部完成。

[1] 文章地址:https://queue.acm.org/detail.cfm?id=1394128。