Mata解决了缓存何时失效的世纪难题? - Lu


我们在 Meta/FB 的工程博客上写了一篇文章,介绍了我们如何管理缓存失效的复杂性,并使缓存在此过程中更加一致。我相信所描述的方法应该适用于大多数基于失效的缓存。缓存失效在计算机科学中可能不再是一件难事。

根据Hacker News上的讨论,人们对于 Phil Karlton 在他的名言中所指的“缓存失效”有不同的看法——计算机科学中只有两件困难的事情:缓存失效和命名事物。有些人认为缓存失效很难,因为很难确定何时失效,让我们来谈谈它。
(这里并没有将协调和一致性之间的权衡命名为缓存失效问题,见Marc文章

在其最通用的形式下,一个缓存可以存储来自任何数据源的任意物化视图。
这其中可物化依赖关系:例如,y = f(x);如果x改变了,缓存中的y也需要改变/验证。
然而,这个问题并不只存在于缓存中。
例如,你可以在一个数据库中存储一个 "朋友 "表(在我们的例子中为x),而在同一个数据库中存储另一个 "朋友的朋友 "表(在我们的例子中为y)。现在,为了保持两个表的一致性,基本上需要以事务方式更新它们(在一个ACID事务中是最简单的),这意味着两个模式都被与数据库交互的人所知道。
我想说的是,y=f(x)的问题并不是使缓存失效变得困难的原因。

如果你对何时以及如何可靠地使缓存失效特别感兴趣,这篇文章可能会有帮助。

与数据库不同,缓存的独特之处在于,数据可以在任何地方、任何地方被缓存。客户端可以来运行任意的查询,并将具体化的数据存储在客户端。关于缓存数据的模式不会被数据源知道。在这种情况下,这不是一个难点问题。在最通用的缓存和数据源的形式下,似乎很明显,缓存失效不可能在这种通用情况下工作,因为:

  • 客户端随意来来去去,没有确定性的集群成员资格(例如,应该失效的客户端的已知列表)
  • 存储在客户端中的数据没有被设计成数据表结构,因为它不能被数据源知道。没有通用的方法来确定状态改变是否会影响客户端上的某个缓存条目。当然,一个人可以使一切无效,但客户成员资格仍然是不确定的。

现在我们来谈谈这个问题的一个更具体的情况:当缓存数据的模式(或使用的查询)被告知数据源/验证管道。在这种情况下,当更新数据源时,为了保持缓存的一致性,你基本上需要对数据源和缓存进行事务(跨系统事务)。通常缓存有更多的副本,我不认为同步运行这种类型的事务在规模上是实用的。

如果我们不在两个系统(数据源和缓存)上同步进行事务会发生什么?那么,现在每当异步更新管道执行计算时,它是针对一个移动的数据源(而不是写入时的快照)进行的。现在我们假设数据源是Spanner,它提供了时间点的快照。在Spanner提交时,你可以得到一个提交时间(TrueTime)。现在利用这个提交时间,可以异步读取数据和计算缓存更新。因为缓存中的物化本身并不进行写入,所以对它们的更新基本上是盲目进行的,可以通过提交时间(TrueTime)来排序。

这还是相当有用的,因为我们增加的唯一要求是,缓存数据的数据表结构(使用的查询)要让数据源(如数据库)知道。这虽然很难建立,但可以做到。
如果这是你对 "缓存失效 "的定义,我同意这很难;而且Meta这篇博文也没有解决这个问题。困难是来自于你正在处理一个分布式系统的事实,从你开始使用cache的那一刻起。

如果说我从Hacker News上的这次公开的民间讨论中学到了什么,那就是”

  • 我们应该避免将缓存失效的权力/责任放在缓存用户的手中,而应该放在缓存服务所有者(和数据库所有者)的手中。
  • 我们应该通过更好的抽象、更简单的数据模型(例如图数据模型)来提供护栏。
  • 我们应该避免缓存关系(独立的物化),而更倾向于缓存索引,这样写/验证的放大作用就会受到限制。

banq评:该文作者,Meta的年轻工程师,可能没有意识到:当你将同一份数据复制两份,虽然只是两份,不是三份或无数份,一份在数据源,一份在缓存,你已经面临CAP定理  的三取二限制,而且缓存和数据源之间肯定存在网络连接,网络连接本身默认是不可能的,即使是在local host端口之间。
虽然,缓存和数据源之间理论是可以通过ACID事务来解决,但是这种ACID事务是类似2PC,还是一种一厢情愿。

参考该文作者另外一篇缓存失效的详细解释。他在文章中辩解重新定义“缓存失效”:

  • 我将在这里将“缓存”定义为本文中事实来源的任何物化视图,除非另有说明,否则在本文中假设此定义。
  • “失效”是指当真实来源发生变化时更新/删除相关缓存条目的操作,因此不会无限期地将陈旧数据存储在缓存中。

他的观点还是认为,当真实数据源发生变化时,更新删除缓存是一种可靠的操作,其实这个过程他忽视了网络连接的天生不可靠性,因此认为这个操作肯定会百分之百完成。