10 分钟搞定分布式应用缓存
本文深入探讨了分布式应用缓存的概念、实现方式、策略以及最佳实践,详细介绍了主要的缓存模式,并讨论了缓存驱逐策略及今后的发展。原文: Mastering Caching in Distributed Applications
缓存似乎是一种你觉得可以做对,但却永远做不对的东西。这是有原因的,毕竟缓存(或缓存失效)是计算机科学中最难解决的两个基础问题之一(另一个问题是变量的命名)。
不管是不是开玩笑,缓存确实很难做好,尤其是在大型分布式应用中。因此,团队通常会通过迭代和实验来调整缓存策略和实施,直到有希望在某个时候将其调整到某种合理的半优化状态。
本文将揭开缓存的神秘面纱,澄清其中一些经常被忽略或误解的问题。
希望读完这篇文章后,你能更清楚的了解什么是缓存、缓存的主要方法、需要注意的事项以及各种缓存技术在实际案例中的应用。
因此,不再赘述....
什么是缓存?
简而言之,缓存就是将数据存储在临时介质中,在这种介质中检索数据比从原始存储(记录系统)中检索数据更便宜、更快捷或更优化。
换句话说,想象一下下面的用例。
订单管理系统需要从库存系统中检索产品信息。假设库存系统性能不佳,每次收到请求时,都必须去中央数据库获取产品信息。该数据库速度很慢,无法支持过多的并行请求。
为了提高性能并减轻库存数据库的压力,我们引入缓存层,在其中存储相同的产品信息。现在,我们不再通过笨重的数据库来访问库存系统,而是先访问缓存,如果数据在缓存中,就从那里获取数据。
这里所做的是引入一种临时存储介质(缓存),以提高性能并优化原始数据库的资源使用。
什么是"缓存"?
人们开始感到困惑的一点是缓存的技术性质。
当我们听到"缓存"一词时,软件开发领域的大多数人都会产生非常特殊的联想。我们通常会把这个词与分布式缓存产品联系起来,比如 Redis、Memcached 或 EHCache。在其他时候,我们会想到浏览器缓存、数据库缓存、操作系统缓存,甚至硬件缓存。
这正是问题的关键所在。缓存的概念并不局限于计算机科学领域的某一特定产品或领域。从最广泛的意义上讲,"缓存"实际上是我们从某个记录系统中复制数据到任何类型的临时介质。之所以这样做,是因为将数据存储在临时介质中在某种程度上是有利的。
造成这种情况的典型原因是高速缓存比原始存储成本更低、性能更好或可扩展性更强。
看一下前面的订单管理和库存系统的例子,缓存层理论上可以是很多东西:
分布式缓存产品(例如 Redis)
另一个自带数据库的微服务
实际库存管理系统中的内存存储
尽管每个方案的实现方式各不相同,但上述所有方案都符合高速缓存的标准。
简单来说,上述所有东西都可以成为缓存。缓存作为一个概念,可以在(事实上也正在)计算机系统的各个层级实现,并横跨许多数字领域。
一些术语
在继续讨论之前,有必要了解一下围绕缓存这一主题的不同术语。
记录系统(System of Record):存储数据的永久存储器,最有可能是数据库,也称为真实源(source-of-truth)系统。
缓存缺失(Cache Miss):当应用程序在缓存中查询特定记录,但缓存中并不存在。
缓存命中(Cache Hit):记录确实存在于缓存中,并以此返回。
缓存污染(Cache Pollution):当缓存中充满未使用或未查询的值时。
缓存驱逐(Cache Eviction):从缓存中删除条目以释放内存的过程。
数据新鲜度(Data Freshness):缓存中的记录与底层记录系统的同步程度。
缓存过期(Cache Expiration):根据时间删除缓存记录,这是驱逐过程的一部分,也是缓存失效的一部分,将在下文讨论。
既然我们已经完全掌握了缓存术语,那么就让我们深入了解一下缓存的一些实施位置和层级。
缓存在哪里实施?
正如前面已经提到的,缓存的应用遍及整个技术领域--在各个层面和许多不同的技术栈中。
在硬件层面,缓存是 CPU 架构的一部分,例如,以 1-3 级(L1/L2/L3)缓存的形式出现。
在操作系统内核层面,有一种磁盘缓存形式被称为页面缓存。当然,还有其他形式。
对于基于网络的系统,当然有浏览器缓存和 CDN(内容分发网络)。它们分别在客户端或 CDN 端缓存常用的静态资源(图片、样式表等)。这样做的目的是减少带宽,并快速、高效、低成本的向用户提供这些资源。
不同类型的应用程序和中间件也有自己的缓存。例如,数据库使用缓存来保存经常使用的查询和经常返回的结果集。
当然,还有许多强大的软件缓存产品,如 Redis、EHCache、Memcached、Hazelcast、Infinispan 等,可以在分布式应用中实现可扩展的分布式缓存。
需要强调的一点是,"分布式"高速缓存的概念也可以与"本地"或"本地化"高速缓存相比较。分布式缓存是一种分布在网络上多个设备上的缓存形式,本地缓存只存在于一台设备上。
要理解这两个概念之间的区别,最好的办法是想象一个应用程序部署在一个服务器集群中。换句话说,应用程序有多个实例同时运行,这对于任何从事大型应用程序工作的人来说都不陌生。
如果在这样的系统中引入分布式高速缓存,那么任何一个应用程序实例都可以访问该高速缓存,并修改其中的记录。
另一方面,如果使用本地缓存,每个实例都将拥有自己的缓存--很可能就在该实例的内存中。不同实例无法访问另一个实例的缓存,而只能访问自己的缓存。
两种方法各有利弊。
一方面,如果有多个实例访问缓存,可能需要解决同步问题、竞争条件、数据损坏以及分布式应用带来的其他挑战。另一方面,共享缓存是一个强大的概念,可以帮助应用程序处理本地缓存(尽管更简单)无法处理的用例。
例如,你可能在多个可用区内的云环境中部署应用,每个可用区都可能有一个运行应用程序的虚拟机实例群集,每个集群很可能都有自己的分布式缓存。原因是分布式缓存的前提之一是能够快速高效的访问,这意味着高速缓存与所服务的实例之间的网络距离(不一定是物理距离,也可以是虚拟距离)要够近。
同时,分布式缓存和本地缓存都面临着一些共同的挑战。
主要挑战在于如何在保持数据新鲜度、在优化缓存失效和驱逐之间保持平衡,以及如何使缓存管理方式与特定用例相匹配。
接下来,我们将讨论管理缓存--缓存模式这一非常重要的概念。
与软件工程中的大多数决策一样,每种方法都有其利弊,下面我们将讨论每种方法的利弊。
本地和分布式缓存系统模式
主要有五种缓存模式,都与缓存的读取、写入以及与底层记录系统同步的方式有关。
旁路缓存(Cache-Aside)
旁路缓存策略可能是最流行的策略,也是大多数软件工程师所熟悉的策略,这种缓存方法完全由应用程序来控制缓存的写入和读取。应用既要控制何时从数据库或缓存读取数据,也要控制何时向数据库或缓存写入数据。
下面以一个例子来说明如何操作。
想象一下,应用接收到用户登录请求,并随之获取用户的邮寄地址。
应用首先会检查缓存中是否存在用户地址。
如果没有该用户的地址条目,应用将从数据库中获取数据。
但是,如果信息存在于缓存中,就会立即检索到该数据,从而节省了访问数据库的时间。
获取新信息后,应用也会将数据写入缓存。
在第 2 步中,如果缓存中没有该特定条目,通常被称为"缓存缺失"。
优点
实施简单
完全由应用控制
因为只有在需要时才会获取缓存项(懒加载),因此内存使用最小(至少理论上如此)
缺点
由于必须从速度较慢的存储区获取数据,缓存缺失的延迟会更高。缓存缺失次数过多,性能可能会受到影响
应用逻辑变得更加复杂(尽管总体构思很容易实现)
何时使用
当你想完全控制缓存的填充方式时
当你没有能够管理数据库读/写的缓存产品时
当高速缓存的访问模式不规则时
直写缓存(Write-Through Caching)
通过直写缓存可确保缓存与底层持久化数据存储之间的一致性。换句话说,当写入发生时,它会在同一个事务中同时传播到缓存和数据库中。
下面举例说明:
财务应用收到用新余额更新用户账户的请求。
数据库和缓存中都存在用户账户余额。
数据库和缓存都会在同一事务中更新新值。
又有一个请求出现了,这次是要读取用户余额。我们首先在缓存中查找并使用该值。由于缓存中有最新的值,因此不必担心该值与底层数据库不同步。
请注意,第 3 步可以通过应用逻辑完成。但通常情况下,实际的缓存产品会承担这一责任。例如,如果使用的是 EHCache 或 Infinispan,那么应用将更新 Redis 缓存,而 Redis 缓存又可配置为更新数据库。
优点
确保缓存与底层数据存储之间的一致性
缺点
事务复杂性,需要某种两阶段提交逻辑,以确保高速缓存和数据库的更新(如果不受高速缓存控制的话)
操作复杂性,如果其中一项出现故障,需要从容应对用户体验
写入速度会变慢,因为需要更新两个地方(缓存和数据存储),而不是一个地方(数据存储)。
何时使用
直写缓存非常适合那些要求数据一致性强、不能提供陈旧数据的应用程序。通常用于数据写入后必须立即准确更新的环境。
绕写缓存
这种策略会填充底层存储,但不会填充缓存本身。换句话说,写操作绕过缓存,只写入底层存储。这种技术与 Cache-Aside 有一些重叠。
不同之处在于,使用 Cache-Aside 时,重点是读取和懒加载--只有在首次从数据存储中读取数据时才将其填充到缓存中。而使用 Write-Around 缓存时,重点则是写入性能。当数据被频繁写入但不被频繁读取时,这种技术通常用于避免缓存污染。
优点
减少缓存污染,因为每次写入都不会填充缓存
缺点
如果某些记录经常被读取,性能就会受到影响,因此,如果能主动加载到缓存中,就能避免在首次读取时访问数据库。
何时使用
当写入量较大但读取量明显较小时,通常会使用这种方法。
回写(背写)缓存(Write-Back (Write-Behind) Caching)
写操作首先填充缓存,然后写入数据存储。这里的关键是,写入数据存储是异步进行的,因此不需要两阶段的事务提交。
Write-Behind 缓存策略通常由缓存产品处理。如果缓存产品有这种机制,应用将向缓存写入内容,然后缓存产品将负责向数据库发送更改内容。如果缓存产品不支持这种机制,应用程序本身将触发对数据库的异步更新。
优点
写入速度更快,因为系统只需在初始事务中写入缓存。数据库将在稍后时间更新。
如果流量由缓存产品处理,那么应用逻辑的复杂性就会降低。
缺点
在数据库接收到新更改之前,数据库和高速缓存可能会不同步,因此可能会出现不一致。
当缓存最终尝试更新数据库时,可能会出现错误。如果出现这种情况,就需要有更复杂的机制来确保数据库接收到最新的数据。
何时使用
当写入性能非常重要,而且数据库中的数据与缓存中的数据暂时稍有不同步是可以接受的时候,就可以使用回写缓存。适用于写入量大但一致性要求不那么严格的应用。例如,CDN(内容分发网络)可用于快速更新缓存内容,然后将其同步到记录系统。
直读(Read-Through)缓存
直读缓存在某种意义上类似于旁路缓存模式,因为在这两种模式中,缓存都是我们首先查找记录的地方。如果缓存未命中,就会在数据库中查找。不过,在旁路缓存模式中,查询缓存和数据库的责任都落在了应用程序身上,而在直读缓存中,这一责任则落在了缓存产品身上(如果它有这种机制的话)。
优点
简单--所有逻辑都封装在缓存应用程序中
缺点
缓存缺失时从数据库读取数据的潜在延迟。需要复杂的数据更新失效机制。
何时使用
当你想简化访问数据的代码时,就会使用直读缓存。此外,当你想确保缓存始终包含来自数据存储的最新数据时,也可以使用直读缓存。这对于读取数据比写入数据更频繁的应用程序非常有用。但这里的关键点是,缓存产品应该能够通过配置或本地方式从底层记录系统中执行读取操作。
缓存策略总结
以下是我们就五种缓存模式所做的总结。
旁路缓存
只有当应用程序提出请求,但在缓存中找不到数据时,才会按需将数据加载到缓存中。
真实场景:按需缓存产品详细信息的电子商务网站。
负责 DB 操作:应用程序
直写缓存
每次写操作都会同时写入缓存和底层数据存储,以保持一致性。
真实场景:可使交易账户余额保持一致的银行系统。
负责 DB 操作:缓存产品或应用程序
回写缓存
写入首先记录在高速缓存中,然后异步写入数据存储。
真实案例:CDN 先更新缓存中的内容,然后同步到存储系统。
负责 DB 操作:缓存产品或应用程序
绕写缓存
写操作会绕过缓存,直接更新数据存储,从而避免缓存不急需的数据。
真实案例:日志操作,日志直接写入存储,无需缓存。
负责 DB 操作:应用程序
直读缓存
缓存是读取数据的主要接口。如果缓存中缺少数据,则会从记录系统中获取并缓存。
真实案例:用户配置文件服务在缓存缺失时获取和缓存用户数据。
负责 DB 操作:缓存产品或应用程序
缓存失效
我们已经了解了填充缓存的不同方法,接下来还需要了解如何使缓存与底层记录系统保持同步。
说到缓存失效,主要有基于时间和基于事件的两种方法。基于时间的失效方法可以通过大多数缓存产品中的"存活时间"(TTL)设置来控制。基于事件的方法要求应用程序或其他设备将新记录发送到缓存。
缓存的问题在于,数据几乎总是至少与底层数据存储(记录系统)稍有不同步。换句话说,数据会变得陈旧。为了尽可能保持缓存与记录系统同步,需要实施某种缓存失效策略。
换句话说,我们需要确保缓存中数据的"新鲜度"。
缓存失效会导致新记录从记录系统中被检索到缓存中。因此,了解缓存失效与上面讨论的缓存策略之间的关系非常重要。
缓存策略与如何从缓存中加载和检索数据有关,而缓存失效则更多的与记录系统和缓存之间的数据一致性和新鲜度有关。
因此,这两个概念之间存在一些重叠,对于某些缓存策略来说,失效比其他策略更简单。例如,使用"通过缓存写入"方法时,缓存会在每次写入时更新,因此无需额外实现。但是,删除可能不会被反映,因此可能需要应用逻辑来显式处理。
有两种方法可以使缓存条目失效:
事件驱动
使用事件驱动方法,应用程序将在底层记录存储发生变化时通知缓存。无论同步还是异步,每次记录发生变化时,都会向缓存发出通知。
这可以通过应用程序完成,代码负责保持缓存的更新。或者,某些缓存产品可能具有发布/订阅功能,缓存产品可以订阅这些类型的通知。在这种情况下,应用程序的工作量可能会减少。不过,仍然需要一些东西来产生通知事件。
基于时间
在基于时间的方法中,所有缓存记录都有一个与之相关的 TTL(生存时间)。记录的 TTL 过期后,该缓存记录将被删除。这通常由缓存产品控制。
缓存驱逐策略
缓存驱逐与缓存失效类似,都是删除旧的缓存记录。但两者区别在于,当缓存已满,无法再容纳任何记录时,就需要进行缓存驱逐。
请记住,缓存的目的是存储最常访问记录的子集。而不是复制整个真实源系统。因此,缓存的大小通常要比数据库/真实源/记录系统中存储的数据小很多。
因此,需要一种可以"驱逐"或换句话说删除记录的机制。
与此同时,还需要确保从那些应用最不需要的记录开始,否则缓存的全部意义都将化为乌有。
为确保以最佳方式驱逐记录,可以利用多种驱逐策略:
最近最少使用 (LRU,Least Recently Used)
通过这种方法,可以删除很久没有使用的记录。
何时使用:适用于数据即将被访问的可能性随上次访问记录后时间的推移而降低的情况。这非常适合通用缓存,因为访问的周期是未来访问的重要指标。
何时不宜使用:不适合数据访问模式与周期无关的工作负载。
先进先出(FIFO,First In First Out)
将先于其他记录保存在缓存中的记录驱逐出去。
何时使用:适用于数据年龄比访问频率更重要的缓存。适用于缓存可预测使用时间的数据。
何时不宜使用:对于可能仍需频繁访问旧数据的工作负载而言,这不是最佳选择。
最不常用 (LFU,Least Frequently Used)
删除不经常使用/访问的记录。
何时使用:最适合长期频繁访问数据的情况。适用于具有稳定访问模式的应用程序。
何时不宜使用:在访问模式会发生显著变化的环境中,效果较差。不常访问的项目会污染缓存。
生存时间 (TTL)
根据预先确定的离开时间进行驱逐。
何时使用:适用于在一段时间后变得过时或陈旧的数据。
何时不宜使用:不适用于有效性不会随时间自然失效的数据,以及基于其他因素需要无限期保留在缓存中的数据。
随机替换
随机驱逐记录。
何时使用:用于复杂追踪机制得不偿失的情况,或者访问模式不可预测,因此其他驱逐策略不适合的情况。
何时不宜使用:在大多数实际情况下,当访问模式或多或少可预测时,其效率一般低于其他策略。
总结
我们已经讨论过缓存在分布式应用中的重要性以及选择正确缓存策略的关键性。目前有许多流行的策略:
旁路缓存(Cache-aside)
直写缓存(Write-through cache)
直读缓存(Read through cache)
回写缓存(Write behind cache)
绕写缓存(Write Around)
还讨论了使用基于时间或事件驱动的方法进行缓存失效的问题。
我们指出了缓存驱逐的重要性以及为此可采取的策略。这些策略是:
LRU
FIFO
LFU
TTL
随机
最后,缓存可以是本地缓存,也可以是分布式缓存。前者仅限于单台机器/应用实例,后者跨越多台机器,通常(但不一定)局限于一个实例集群。
希望这篇文章能让你清楚了解什么是缓存、为什么缓存很重要,以及在使用缓存技术时必须了解的所有不同术语和细微差别。
缓存:未来
与其他技术一样,市场上的缓存产品也出现了巨大创新。一些值得关注的亮点包括:
与边缘计算集成
随着边缘计算的不断发展,缓存策略变得更加分散,将数据转移到网络边缘需要的地方。接近用户降低了数据服务的延迟、带宽和成本,这对于物联网和移动应用等实时应用至关重要。
案例:自动驾驶汽车使用边缘计算在本地处理实时数据。在边缘节点缓存地图和交通状况等关键数据,有助于快速做出决策,而不会出现查询中央服务器的延迟。
人工智能和机器学习驱动的缓存
人工智能和机器学习可以预测数据使用模式,并根据预期需求预先缓存数据,从而增强缓存机制。这种积极主动的方法可以显著提高效率,尤其是在数据访问模式经常变化的动态环境中。
案例:亚马逊利用机器学习预测用户行为,并预先缓存用户在黑色星期五等高峰期可能购买的产品。这样可以缩短加载时间,从而提升用户体验。
内存数据网格(IMDG,In-Memory Data Grids)
IMDG 正在迅速发展成为一种强大的缓存解决方案,可提供跨分布式系统的低延迟复杂数据访问。IMDG 不仅能缓存数据,还能直接在缓存层中提供一系列数据处理、实时分析和决策功能。
案例:高频交易平台利用 IMDG 在内存中缓存市场数据和交易订单。这样可以快速访问和处理,这对于做出亚秒级的交易决策至关重要。
你好,我是俞凡,在 Motorola 做过研发,现在在 Mavenir 做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI 等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。为了方便大家以后能第一时间看到文章,请朋友们关注公众号"DeepNoMind",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!
版权声明: 本文为 InfoQ 作者【俞凡】的原创文章。
原文链接:【http://xie.infoq.cn/article/697716ba39ff006c5b66f6fc5】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论