Skip to content

缓存

负载均衡能够帮助系统水平扩展到越来越多的服务器,而缓存则能大幅提高现有资源的利用率,并使一些原本难以实现的产品需求变得可行。缓存利用局部性原理(locality of reference):最近请求的数据很可能会再次被请求。几乎所有计算系统的层级都会使用缓存,包括硬件、操作系统、网页浏览器、Web 应用等。

缓存类似于短期记忆:它的存储空间有限,但通常比原始数据源访问速度更快,并且存储的是最近访问的数据。在系统架构的各个层级都可能存在缓存,但最常见的是在靠近前端的层级,这样可以在不消耗下游资源的情况下快速返回数据。

应用服务器缓存

将缓存直接放置在请求层的节点上,可以本地存储响应数据。每当有请求到达服务时,如果缓存中已有相应数据,则节点可以快速返回本地缓存的内容。如果数据不在缓存中,节点会从磁盘中查询数据。

缓存存储位置:

  • 内存缓存(Memory Cache):访问速度最快。
  • 本地磁盘缓存(Local Disk Cache):比访问网络存储快。

当请求层扩展到多个节点时,每个节点都可以维护自己的缓存。但是,如果负载均衡器随机分发请求,同一个请求可能会被不同的节点处理,导致缓存未命中率(cache miss)增加。为了解决这一问题,可以采用全局缓存(global cache)分布式缓存(distributed cache)

内容分发网络

CDN(内容分发网络)是一种专门用于提供大量静态资源的缓存。典型的 CDN 机制如下:

  1. 用户请求某个静态资源时,CDN 服务器会首先检查本地是否已有缓存
  2. 如果缓存命中,CDN 直接返回资源。
  3. 如果缓存未命中,CDN 会请求后端服务器获取该文件,并将其存储在本地缓存中,以便后续请求时直接提供。

如果当前系统的规模尚未达到需要自建 CDN 的程度,可以采取分阶段优化策略

  • 先通过单独的子域名(如 static.yourservice.com)提供静态资源,并使用 轻量级 HTTP 服务器(如 Nginx)进行托管。
  • 未来如果需要迁移到 CDN,只需修改 DNS 解析,将流量切换到 CDN 即可。

缓存失效

虽然缓存非常有效,但它确实需要一些维护,以确保缓存与真实数据源(例如数据库)保持一致。如果数据库中的数据被修改,则应该使缓存中的数据失效;否则,这可能导致应用程序行为不一致。
解决这个问题被称为缓存失效。常用的三种方案如下:

  1. 写直达缓存(Write-through cache):
    在此方案下,数据同时写入缓存和相应的数据库。缓存数据允许快速检索,并且由于相同的数据被写入永久存储,因此缓存与存储之间将保持完全的数据一致性。此外,该方案确保在崩溃、电源故障或其他系统中断的情况下不会丢失数据。
    尽管写直达缓存最小化了数据丢失的风险,但由于每个写操作必须在返回成功给客户端之前执行两次,因此该方案的缺点是写操作的延迟较高。

  2. 绕过缓存(Write-around cache):
    这种技术类似于写直达缓存,但数据直接写入永久存储,跳过了缓存。这样可以减少缓存被不会被重新读取的写操作填满,但其缺点是,对于最近写入的数据的读取请求会产生“缓存未命中”,并且必须从较慢的后端存储读取,导致更高的延迟。

  3. 写回缓存(Write-back cache):
    在此方案下,数据只写入缓存,并立即确认操作完成给客户端。写入永久存储的操作在指定的间隔或特定条件下完成。这对于写密集型应用来说具有低延迟和高吞吐量,但这种速度带来了数据丢失的风险,因为写入数据的唯一副本存在于缓存中,若发生崩溃或其他不良事件,数据可能会丢失。

缓存驱逐策略

以下是一些常见的缓存驱逐策略:

  1. 先进先出(FIFO):缓存首先驱逐最早访问的块,而不考虑该块以前被访问的频率或次数。
  2. 后进先出(LIFO):缓存首先驱逐最近访问的块,而不考虑该块以前被访问的频率或次数。
  3. 最少最近使用(LRU):首先丢弃最少最近使用的项目。
  4. 最近最少使用(MRU):与LRU相反,首先丢弃最近使用的项目。
  5. 最少频繁使用(LFU):计算每个项目被访问的次数,最少使用的项目首先被丢弃。
  6. 随机替换(RR):随机选择一个候选项并丢弃它,以便在需要时腾出空间。

以下链接包含了一些关于缓存的精彩讨论:
[1] 缓存 - https://en.wikipedia.org/wiki/Cache_(computing)
[2] 系统架构入门 - https://lethain.com/introduction-to-architectingsystems-for-scale/