第5章第3节:最终一致性里的旧数据幻影

0 阅读6分钟

无论从哪个技术指标看,我们新的技术架构都表现非凡。系统运行流畅稳定且能够服务10万用户。以系统工程视角来说,这确实是赢得了一场架构升级之战。但如果站在用户角度,新架构却引入了一个十分奇怪,甚至有些魔幻的让人困扰的新问题。

设想一位名叫Priya的卖家。她经营着一家专卖定制珠宝的精品小店。她登录dukaan页面然后视线停留在一款店铺里最火的项链(售价1000卢比)。她决定搞一次闪促把价格降到800卢比(20%折扣)。于是她修改价格点击“保存”按钮。系统给出的响应非常及时:“商品信息修改成功!”

为了再次确认价格已修改,Priya尝试向顾客一样点击“查看店铺”按钮。很快就看到了那款项链,但是价格仍是1000卢比。

她心头一紧。是刚才没有保存成功嘛?她回到店铺商家后台页面,那里显示价格的确已经改成了800卢比。于是她回到店铺页面,但是价格却显示仍为1000卢比。她有点被搞晕了,甚至会有点恐慌。是她的店铺出问题了嘛?她的顾客会因为错误的价格而多付了钱?她只好一遍又一遍刷新页面。1000卢比。仍是1000卢比。在经历了5秒疯狂的页面刷新之后,价格终于变成了正确的800卢比。

Priya刚经历的这种情况可以被称作旧数据幻影。这让Priya成为同步延时的受害者。她的“保存”操作被立马送到了主库写入。但是她的“查看店铺”操作被送到了读库,而此时读库的数据与主库尚未完成同步。

正如前面所述,这并不是代码层面的bug。这是我们高性能的新架构内在的一个特性。我们牺牲了实时一致性以换取高可用性。欢迎来到最终一致性的世界。

深入技术细节:最终一致性

要明白这个概念,可以先跟大家普遍期待的情形做个对比。

  • 强一致性:这是大家生活中都习以为常的情形。当在银行转账时,余额应该马上在所有分行都得到更新。也就是说写入操作完成后,所有之后的读操作都保证能读到新的数据。默认情况,单数据库单服务器提供的就是强一致性。数据正确性的来源就是唯一的那台数据库。
  • 最终一致性:这是我们身处的分布式系统的情形。系统保证的是在所有写入操作停止之后,所有数据副本最终都会完成同步。但不保证完成同步的所需时间。它保证的是一致性一定会达成,但是不一定很快。

这的确是某种折中方案。我们牺牲了即刻的强一致性,换来的是可以同时服务上百万读请求的能力。对于我们99.9%的用户来说(浏览店铺页面的顾客群体),价格修改延时1秒完全可以接受,甚至大部分人都不会注意到这些延时。但对于剩下的0.1%的提供数据修改的用户(像Priya那样的卖家)来说,1秒的延时却是不爽或者完全无法接受的体验。

我们无法根除同步延时,因为它来自物理层面的限制。但我们必须得想个法子让用户免受其苦。

深入技术细节:数据过期策略

如何解决上述难题呢?既然无法让系统更快完成同步,那就只能让我们的应用变得更聪明些。

策略1:啥也不做(如果这能被接受)

对于大多数功能,短暂的延时完全不会有任何问题。举个例子,如果我们的管理员后台显示总店铺数,那这个数字比主库最新修改延时30秒也没啥关系。这种策略要求主动精确地识别出应用中哪些部分需要强一致性,哪些部分可以容忍最终一致性。

策略2:写后重读方案(VIP通道)

这正是我们针对Priya面临的困扰所采用的策略。逻辑很简单:对于某个具体的用户,在其完成某次写入操作之后,立刻把后续的读请求也一并发送到主库,虽然这违反了之前我们关于读请求送到从库的规则。

这就类似给了Priya一张VIP通行证:

  1. Priya点击保存项链的新价格:发送写入操作到主库。
  2. 我们的应用此时成功完成了这次价格修改的写入,然后会在Priya的会话中设置一个临时的标志位(类似浏览器的cookie)表示:“该用户在接下来的60秒窗口期内享受VIP待遇。”
  3. Priya改完价格之后马上刷新店铺页面:读请求发送至dukaan应用。
  4. 我们定制的数据库路由服务收到了来自Priya的读请求,并发现会话中包括存在关于“VIP”的标志位。
  5. 于是数据库路由会把这次请求直接发送到主库,而不是像对待其它用户那样发送到只读从库。
  6. 因为主库总是拥有最新的数据,这次读请求会返回正确的800卢比。Priya能够立马看到修改后的效果,这样她对dukaan的信心就不会因为最终一致性导致数据延时而削弱。
  7. 一分钟之后,VIP标志位在Priya的会话中失效。下一次读请求会如往常一样发送至从库,此刻数据同步应该早已完成。

这种方案让我们能够同时享受两种一致性模型的优势:提供给普通用户的始终可用,以及提供给关键用户的强一致性。

第5章关键知识点总结

  • 用只读从库拓展数据库是性能巨大提升,但不是没有代价:为了最终一致性(技术实现更复杂)牺牲了强一致性的简洁。
  • 同步延时是物理层面的现实,并不是bug。但主库和从库之间总会有延时。这个延时没法根除,只能从应用层面尽量降低影响。
  • 最终一致性可能会导致让人不爽且疑惑的体验。如果在用户修改之后刷新页面仍然看到修改前的旧数据,那这可能会严重损伤对产品的信任。
  • 实施一个兜底的“写后重读”策略。对于刚完成数据修改的用户,可以临时把相关读请求直接发送到主库。这可以让关注实时一致性的用户得到强一致性的体验,也不会牺牲只读从库的可用性。