多级缓存设计—常见策略和实践模式

本地缓存

方法1: 堆上缓存

在进程中,使用堆内/堆外缓存。

优点:

  • 实现简单
  • 读取速度高

缺点:

  • 进程挂掉,缓存丢失
  • 进程重启,大流量可能冲垮应用
  • 堆上缓存可能造成语言 GC 效率降低
  • 无法持久化,无法分布式共享

方法2: local redis cache

使用本地 redis 代替堆上缓存。本地代表着没有网络 IO 消耗,读取速度高。并且解决了第一部分缺点的前 3 部分。

缺点如下:

  • 无法持久化,无法分布式共享
  • 单机数存储数据量小

多级缓存

为什么需要多级缓存?

在云开发网关中,缓存层的设计至关重要。

最初,缓存是放在堆上的本机缓存。基本策略是 LRU,过期支持异步续期。

假设集群中有 60 个 pod,采取普通轮询的方式分配流量,缓存有效期是 60s。用户每分钟请求少于 60 个,就会落到不同的 pod 上,不会命中缓存。对于用户,每次请求都很慢。随着集群上 workload 中 pod 的增多,缓存命中率只会越来越低,平均耗时越来越高。

除此之外,重启 pod 会造成缓存丢失,当然这个不如命中率低影响大。

多级缓存逻辑设计

缓存层是一个逻辑上的概念,由多个部分的缓存共同组成:

  • 进程(堆上)缓存
  • 分布式 redis 缓存中心

在读取缓存的时候,对于没有命中缓存的情况,会自动降级读取。具体如下:

  • 出于速度和一致性的考虑,首先读取进程缓存
  • 进程缓存没命中,则读取 redis 缓存中心的缓存数据
  • redis 缓存中心不命中,则异步请求数据,并将异步刷新缓存

相对地,缓存刷新策略如下:

  • 异步获取的数据先存储到进程缓存
  • 再存到 redis 缓存中心

其他还有一些小细节,比如 redis js 库的超时设置有时会“抽风”,更稳健的做法是在代码中使用setTimeout,超时则 Promise.reject,主动结束请求;比如可以加入默认值的设计等等。

常见缓存策略

  1. 基于空间:缓存占用的存储空间
  2. 基于容量:缓存总数
  3. 基于时间:过期时间、空闲期(多久没访问后,移除)
  4. 回收算法:
    • FIFO:先进先出
    • LRU:最近最少使用算法(最常用)
    • LFU:最不常用算法,一定时间内使用次数(频率)最少的那个被移除

常见多级存实践模式

  • Cache Aside:代码里面直接维护缓存的逻辑,比如缓存不命中,再调用后台接口请求缓存,再调用接口刷新缓存结果到本地和 redis。
  • Cache as SoR:抽象一层缓存层,开发者只需要通过缓存层接口读取/写数据即可,缓存层内部封装了默认值判断、缓存过期判断、异步请求等逻辑
    • Read Through:业务调用 cache 层,cache 层不命中,内部自动回源 Source
    • Write Through:业务调用 cache 层修改数据,cache 层内部去更新缓存和 Source
    • Write Aside:Write Through 的异步模式,降低耗时,快速返回结果

注:只是一些术语,本质上就是是否抽出一个缓存层,用来代理开发者操作,降低开发和维护成本。