常见缓存策略设计


本文将介绍缓存方面的一些挑战、使用的典型解决方案以及使用命令查询职责分离 (CQRS) 作为更好策略的概念。

缓存都是关于延迟的
低延迟请求是标准的非功能性要求,尤其是对于电子商务应用程序,因为人们普遍认为,您的应用程序每 X 毫秒为您的客户提供数据,企业就会失去潜在的销售。
解决高延迟的途径,特别是信息检索,很可能会以添加缓存的形式出现——你的数据的副本——与我们尝试从源头做同样的事情相比,它的检索速度要快得多。图 1 说明了这个过程。

图 1. 缓存保存数据的副本,访问速度比原始数据快得多。

缓存被广泛使用,从微处理器内部到用于为您看到的网页和我们运行的应用程序提供服务的网络基础设施。虽然缓存有时具有其他优势和目标,例如提供可靠性和冗余,但在本文中,我将主要关注延迟方面。
在我们进一步研究之前,让我们讨论一下我们是否真的需要缓存。

缓存什么时候有用?
在一篇关于缓存的文章中提出可能听起来很奇怪,但在决定添加缓存之前,请确保实际考虑您的应用程序是否需要缓存。面向客户的应用程序通常比服务器到服务器的需求更敏感。与在没有用户交互的情况下发生的服务器到服务器调用相比,面向客户的应用程序中的给定结果具有更大的影响。
传统上,缓存的需求源于您的响应是在后台执行多个连接的查询的结果。这些连接是昂贵的,利用了不能用于​​其他请求的资源,在将响应返回给客户端之前需要一些时间,并且有效地限制了您可以同时服务的请求数量。在您的系统处于高负载的情况下,拥有一个快速访问的预连接结果变得很有吸引力,甚至是必不可少的。
随着CosmosDBDynamoDB等 NoSQL 解决方案的发展,可以大规模获得低延迟响应,这可能会否定增加更多复杂性来创建和维护缓存基础设施的必要性。这些技术如何实现低延迟超出了本文的范围,但可以说它并非没有权衡取舍,因此请务必在进行切换之前阅读细则。
在您的应用程序开发中实现缓存之前,我建议评估非功能性需求并访问您将使用的模式。您的分析可能表明您根本不需要缓存。例如,如果您的应用程序没有将信息发回给正在浏览您的电子商务网站的人,那么处理您的请求需要 10 毫秒还是 100 毫秒可能并不重要。
如果您决定使用缓存,并且预期的最终结果是相同的,那么创建缓存有不同的策略,每种策略都有其优缺点。让我们更深入地了解主流。

常见的缓存策略
直读缓存

第一个也是最简单的策略称为通读。在这种方法中,您的应用程序将首先尝试从您的缓存中读取。如果未找到请求的信息,它将从原始来源获取,将该信息添加到缓存中,然后返回给客户端。

图 2. 通读缓存。

一个通读缓存实现的伪代码如下所示:

在最佳情况下,信息驻留在缓存中,其内容按原样返回。最坏的情况是,缓存中不存在信息,必须访问源以检索内容并将它们添加到缓存中,然后再返回客户端。

由于有一个内置的回退机制,因此不需要所有数据都适合缓存,并且您可能有一个驱逐策略来删除太旧且可能过时的条目,或者访问频率不高且可以删除的条目为了节省空间。

直写缓存
第二种策略,直写,在每次写入源时自动填充缓存。


图 3. 写入过程持续存在于源站和缓存中。

读取进程只从缓存中读取。

直写式缓存实现的伪代码如下所示:


这种策略将使缓存与源保持同步,并且还具有消除陈旧数据的好处(下一节将解释这个问题),但会带来两个额外的成本:您的缓存需要适合整个数据集,以及您的写入将更慢且更复杂,因为它们需要同时写入持久性解决方案和其中之一的潜在故障,即。写入源成功,但写入缓存失败。

后写缓存
我将回顾的最后一个策略是 write-behind,它将真相源 (SOT) 翻转到缓存。


图 4. 在 write-behind 中,项目在单独的过程中被复制到源。

实现的伪代码如下所示:

与直写类似,缓存必须能够保存整个数据集,但 SOT 是暂时的缓存,最终会到达原点。因为信息首先写入缓存,所以它始终是最新的,检索将始终以较低(更好)的延迟返回信息。

不幸的是,这个解决方案有两个复杂性:缓存必须具有弹性,以确保它在到达源之前不会丢失任何信息,并且还有一个额外的同步过程需要开发和维护。

为什么添加缓存是一项重要的任务
表面上添加缓存很简单,以通读策略为例。您已经能够从源中检索数据,然后使用将这些数据保存在缓存中的调用来包装它。
实际上,您会遇到一些细节,即使是最直接的方法也容易出错。让我们看看经常被忽视的陷阱。

过时的数据
对于使用缓存的应用程序,它必须已经接受它将提供可能过时的信息。但是我们如何确定数据的新鲜度何时足够好呢?
最简单的方法是建立一些时间,也称为生存时间 (TTL),在此时间过后,缓存中的数据将被忽略。这很好,但你应该把它放在什么价值上?5秒?5分钟?
如果您将值设置得太大,则源中的信息可能会发生太大变化,您将向客户发送大量过时的信息。如果你把它放得太小,你可能会完全否定缓存的好处,因为对特定条目的请求频率低于 TTL。最后,您的应用程序上下文应该决定您选择的 TTL。

缓存踩踏

正如我们在查看不同的缓存策略时看到的那样,我们会遇到我们尝试接收的数据尚未在缓存中或被认为不适合使用的情况。在这些情况下,需要对数据来源​​提出请求。

没有什么不寻常的,但是想象一下,如果您在缓存中没有的数据很受欢迎,那么对于刚刚发布且需求量很大的“热门”产品可能就是这种情况。您可能有数百个几乎同时到达的条目请求,所有这些请求都没有在缓存中找到条目并触发对源的昂贵请求。

根据这些请求的数量,它们可能会在尝试一遍又一遍地为相同的操作提供服务时对源造成开销。


图 5. 对缓存中相同(缺失)条目的大量请求可能会使持久性过载。

有一些既定的方法来处理缓存踩踏事件,从锁定访问以便只有一个请求会真正触发对源的访问,到在启用任何流量之前先发制人地确保在缓存中找到一个项目。

所有这些方法都增加了解决方案的复杂性,而这很少考虑到估计或维护成本中。

为什么 CQRS 可以成为您的缓存
CQRS 命令查询职责分离是一种已经存在一段时间的模式。简而言之,它承认在请求某些信息的人和改变给定系统状态的人之间对交互的需求是不同的。
查询是在命令发生时不会改变状态的操作。此外,虽然不是强制性的,但 CQRS 实现在查询和命令端之间利用了不同的持久性解决方案(或使用)。


图 6. 具有用于写入和读取操作的两种不同持久性的 CQRS 的简单示例。

一个原因是编写方面是复杂性所在,以确保遵守业务规则。另一方面,读取端可能只需要实体的(子集)并且不需要逻辑来保护更改。
写入端将发出消息——事件——代表命令导致的状态变化。您将使用这些消息来创建和维护读取端。


图 7. 利用 AWS 服务流式传输事件并构建一个或多个读取模型的示例实施。

由于读取端没有义务在使用的技术上与写入端匹配,因此您可以选择一种满足延迟要求的技术。图 6 说明了一个示例,其中使用相同的持久性技术可能无法满足所有访问模式。在这种情况下,您可以使用相同的事件来构建不同的模型。

因此,让我们回顾一下缓存中常见的属性:

  • 缓存数据最终与原点一致
  • 提供预先计算的结果
  • 满足具有低(er)延迟要求的只读模式

使用 CQRS,我们最终得到了一个与上述所有内容相匹配的解决方案,具有以下几个优点:

  • 与大多数缓存实现相反,CQRS 不是事后的想法,而是从一开始就计划好的
  • 读取端可以很简单,因为不需要存储库或操作实体。简单数据传输对象 ( DTO ) 可用于表示数据。

结论
缓存是应用程序开发中普遍使用的模式。典型的周期是开发你的应用程序,部署它,一段时间后发现你必须添加缓存,因为你的应用程序无法满足需求。
从表面上看,这似乎是一项微不足道的任务,但管理 TTL 和处理踩踏事件是您在决定添加缓存时应该考虑的两个最容易被忽视的复杂性。
或者,如果您已经在开发事件驱动应用程序 (EDA),一个潜在的解决方案是利用 CQRS 模式,而不是添加缓存,因为它将利用现有的事件方法和基础架构从开始吧。
最后,请记住挑战您的应用程序是否真的从特定的低延迟操作中受益。更快通常更好,但实现它的成本可能很高。无论您选择哪种解决方案,请确保区分需要缓存的愿望和真正有利的用例,其中实现缓存所需的工作对您的应用程序有显着改进。