设计模式
设计模式在实际代码中的应用示例
以下是本系列文章的参考资料
仅仅三集之后,我的写作进度就变得不稳定了。这应该算是破纪录了。为了保持动力,我决定回意大利待一整季,这样我就迫切需要练习英语了。
其实也不完全是这样:我在这里过节是因为食物。和往常一样,这引出了我这篇文章的主题:食物缓存。
现在读者大概分为两类:一类知道那个关于缓存的著名笑话,另一类则不知道。为了照顾到这两类读者,这里精心挑选了一些极其悲惨的变体版本。
毋庸置疑,我觉得它们都非常搞笑。
总之,这篇文章将作为圣诞系列文章的一部分,主题是缓存技术。我即将介绍主动缓存(即,如何在不造成太大损失的情况下进行缓存)和被动缓存(即,如何依赖浏览器缓存以及类似方法)。
本文是“主动缓存”部分的第一篇文章。
你还在疑惑食物和藏匿食物有什么关系吗?你最好搞清楚,不然我得好好提升一下我的悬念制造技巧了。
让我们从一个简单的非IT问题开始。今天是圣诞前夜,你正计划为你的朋友和家人准备一顿令人垂涎的晚餐。为了便于讨论,我们将使用一道传统的意大利圣诞食谱:“il capitone” 1。
咱们开始做饭吧。食材清单上的第一项是鳗鱼。你打电话给你最喜欢的鱼店,让他们把鱼送过来。第二项是特级初榨橄榄油。你打电话给你最喜欢的农场,订一瓶橄榄油,让他们送过来。第三项是柠檬……
你想想,这有多不方便,对吧?所以,你就会提前买好东西,然后把它们存放在更方便取用的地方,离你实际使用的地方更近,这样就能更高效地拿到这些食材。我们姑且把这个地方叫做橱柜吧。
一旦你意识到可以把东西储存在家里,你可能就会忍不住只叫一次送货员,把圣诞节和新年晚餐的食材都买齐。所以当你去鱼店的时候,你会买鳗鱼和大虾,而实际上你打算一周后再做。
几天后,一股令人作呕的气味熏死了周围所有生物,你这才意识到虾可能已经过期了,你应该用新鲜的虾来烹饪。
缓存也有着同样的问题和好处:我们通常缓存项目是为了节省一些计算、时间或避免无谓地调用外部数据源,但我们应该格外注意条目的过期问题,因为它们最终可能会变得不一致(而且非常糟糕)。
照例,在深入探讨模式(也许用“策略”这个词更合适)之前,让我先介绍一些术语,这将有助于我们进行沟通。
以下是参与者名单:
在之前的例子中,这些角色是这样映射的:
缓存既包括读取(使用原料),也包括写入(存储原料),因此分类也相应地遵循这一原则。本文将探讨读取技巧。
阅读策略:
写作策略:
警告:
遗憾的是,这些模式的命名规则并不统一,因此您可能会发现它们有不同的名称。
为了了解其工作原理以及我们为什么要使用它们,我们将分析上述所有模式的以下场景:
免责声明:
为了简化说明,我们照例将这些策略单独进行探讨。在实际应用中,这些技巧通常会结合使用才能发挥最佳效果。
之所以这样命名,是因为在这种模式下,客户端永远不会直接负责调用数据访问组件,而是将判断缓存条目是否足够或是否需要新条目的责任委托给资源管理器。
资源管理器则位于客户端和数据访问组件之间。
跟着箭头上的数字走,你应该很容易就能明白这里发生了什么:
1) 客户端向资源管理器请求数据;
2) 资源管理器从缓存中没有获取到缓存条目,因此调用数据访问组件;
3) 资源管理器获取数据,存储数据,然后将其返回给客户端。
如您所见,在这里使用缓存减少了步骤数,因此该策略实际上是有效的!
从缓存的角度来看,这种方法确保我们只缓存实际使用的数据。这通常被称为惰性缓存。这种方法还有助于将职责拆分到不同的组件中,它怎么会有缺点呢?!
唉,很遗憾,情况就是这样 :(
第一个问题当然是,当出现缓存未命中情况时,请求必须经过更长的路径才能到达客户端,这使得第一个请求实际上比完全没有缓存时更慢。
解决这个问题的一种方法是进行缓存初始化:系统启动时预先填充缓存层,这样就能始终保证缓存命中。显然,这会降低缓存机制的惰性。一如既往,最佳方案取决于实际情况。
第二个缺点是,由于数据只缓存一次(缓存未命中时),数据可能会很快过时。
再说一遍,这并非世界末日:就像食物一样,你可以为条目设置过期时间。它通常被称为TTL(即生存时间)。当条目过期时,资源管理器可以再次调用数据访问组件并刷新缓存³。
与 Cache Inline 不同,Cache Aside 将由客户端负责与缓存层通信,以了解是否需要缓存条目。
实现这种行为的伪代码可以非常简单:
class Client {
CacheLayerManager cacheLayerManager;
DataAccessComponent dataAccessComponent;
getResource() : Resource {
const resource = this.cacheLayerManager.getResource()
return !resource
? this.dataAccessComponent.getResource()
: resource
}
}
你可以通过查看上面的伪代码来了解这里发生了什么。正如你所看到的,调用数据访问组件的责任现在由客户端承担,而缓存实际上被……搁置了。
这次行程更短,所以这种模式实际上是有效的。
除非我们想了解缓存入门知识,否则这种技术(与 Cache Aside 类似)是一种惰性缓存技术。此外,与 Cache Aside 一样,也存在数据过期的问题,但这个问题同样可以通过TTL(生存时间)来解决。
那么,为什么有人会选择 Cache Aside 而不是 Cache Inline 呢?
由于客户端现在负责直接与缓存层通信,当资源管理器发生故障时,我们只需在第一次请求(即通过缓存未命中路径)时支付惩罚,从而使我们的系统整体上更加健壮。
此外,由于消除了缓存内容与从数据访问组件获取的内容之间的依赖关系,我们可能会有两种不同的模型:一种Model表示从数据访问组件获取的内容,另一种CachedModel表示我们缓存的内容。
这确实会拓宽缓存的应用范围:例如,您可以对缓存数据进行水合或转换,从而仅使用一个缓存条目即可在多个操作中提高性能。
我们举个例子来说明这一点。
假设你正在提供一份从此处获取的银行交易列表AwesomeBankAPI。你的应用程序应该公开两个不同的端点:` getAllTransactionsa` 和 `b` getPayments。当然,`a`AwesomeBankAPI本身不提供任何过滤功能。你可以做的是在首次调用任一端点时,存储所有交易的列表。
从现在开始,如果调用指向 `<object>` getAllTransactions,则直接返回列表。如果调用指向 `<object>` getPayments,则会从缓存中获取整个列表(而不是AwesomeBankAPI再次调用),您只需在本地进行过滤即可。
您可以在这里找到这些示例的更详细版本。
我在这里展示的示例是用Node编写的。这是一个简单的应用程序,旨在与XKCD通信以获取最新漫画。
CacheLayer在这个例子中,它用一个简单的表示Map。我使用一个CacheManager来处理它,这样,如果你想尝试真正的缓存引擎(比如redis或memcached),就可以轻松地做到这一点。
它DataAccessComponent由一个简单的XKCDClient元素表示,该元素(以原生 JavaScript 的方式……)仅公开一个getLastComics方法。
另一个组件确实ResourceManager只在内联缓存示例中使用。
由于所有这些组件最终都是相同的,所以我创建了两个不同的客户端,根据我们想要遵循的策略,以不同的方式共享和使用这些组件。
缓存内联示例演示了如何两次请求相同的资源(即最近的三幅 XKCD 漫画),但第二次请求速度要快得多。这是因为我们没有进行任何缓存预加载,所以第一次实际上是直接调用 XKCD API,而第二次则是从缓存中检索信息。
相反,“ Cache Aside”示例展示了缓存的强大之处,尤其是在我们需要根据已有信息计算资源时。在这个例子中,我们先从 XKCD 获取了最近的五幅漫画,然后又只获取了最近的两幅。当然,第二次调用并没有直接调用 API。
这里的主要区别在于,我们使用缓存来获取之前没有的资源,而不是使用缓存CacheLayer来获取我们已经获取过的东西。
再次强调,这两种策略可以(而且通常也确实)共存。如果您想尝试一下这些示例,可以尝试让ResourceManager第一个示例中的算法更智能一些,使其能够直接使用现有条目(即仓库中已有的内容),或者尝试从中提取所需信息CacheLayer,并决定是否调用 API。
至此,本圣诞特辑(是的,作为电视节目)的第一集就结束了。
你可能已经注意到,我尽量让这篇文章比平时更简短易懂,这样即使你因为圣诞节期间吃太多东西而产生幻觉,也可以不用笔记本电脑轻松阅读。
和往常一样,如果你有任何反馈(比如内容太简单、缺少我的梗、我起名字太烂等等),请留言,我们一起改进吧 :D
下次再见!
1.在意大利其他地方,人们圣诞节几乎都吃肉。我来自一个很奇葩的地方,在那里吃一条巨大的鳗鱼象征着正义战胜了蛇形的邪恶……
2.很遗憾,这里没有标准术语,所以这些名称都是我自创的。如果您有任何改进建议,请告诉我哦 (:
3.为每条记录确定合适的有效期,这既需要智慧,也需要技巧。很可能需要经历大量的错误和尝试(或者说经验,如果你愿意的话),才能为你的情况选择最佳的有效期。
文章来源:https://dev.to/shikaan/-design-patterns-in-web-development----active-caching-1-23e2