Redis-11-缓存设计

缓存的收益和成本

收益:

  • 加速读写
  • 降低后端负载

成本:

  • 数据不一致性
  • 代码维护成本:加入缓存后,需要同时处理缓存层和存储层的逻辑,增大了开发者维护代码的成本
  • 运维成本

使用场景:

  • 开销大的复杂计算
  • 加速请求响应

缓存更新策略

  1. LRU/LFU/FIFO 算法剔除

    • 使用场景:通常用于缓存使用量超过了预设最大值的时候
    • 一致性:要清理哪些数据是由具体算法决定,开发人员只能决定使用哪种算法,所以数据的一致性是最差的。
    • 维护成本:算法不需要开发人员自己来实现,通常只需要配置最大 maxmemory 和对应的策略即可
  2. 超时剔除

    • 使用场景:通过给缓存数据设置过期时间,让其在过期时间后自动删除
    • 一致性:一段时间窗口内(取决于过期时间长短)存在一致性问题,即缓存数据和真实数据源的数据不一致
    • 维护成本:只需设置 expire 过期时间即可
  3. 主动更新

    • 使用场景:应用方对于数据的一致性要求高,需要在真实数据更新后,立即更新缓存数据
    • 一致性:一致性最高,但如果主动更新发生了问题,那么这条数据很可能很长时间不会更新,所以建议结合超时剔除一起使用效果会更好
    • 维护成本:维护成本会比较高,开发者需要自己来完成更新,并保证更新操作的正确性
  4. 实践建议

    • 低一致性业务建议配置最大内存和淘汰策略的方式使用
    • 高一致性业务可以结合使用超时剔除和主动更新,这样即使主动更新出了问题,也能保证数据过期时间后删除脏数据

穿透优化

缓存穿透是指查询一个根本不存在的数据,缓存层和存储层都不会命中。整个过程可分为如下三步:

  1. 缓存层不命中
  2. 存储层不命中,不将空结果写回缓存
  3. 返回空结果

缓存穿透将导致不存在的数据每次请求都要到存储层去查询,失去了缓存保护后端存储的意义。

解决办法:

  1. 缓存空对象

    缓存空对象会有两个问题:

    • 空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间(如果是攻击,问题更严重),比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除
    • 缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响
  2. 布隆过滤器拦截

    在访问缓存层和存储层之前,将存在的 key 用布隆过滤器提前保存起来,做第一层拦截。

    这种方法适用于数据命中不高、数据相对固定、实时性低(通常是数据集较大)的应用场景,代码维护较为复杂,但是缓存空间占用少。

无底洞优化

无底洞问题:分布式缓存中,有更多的机器不保证有更高的性能。

无底洞问题分析:

  • 客户端一次批量操作会涉及多次网络操作,也就意味着批量操作会随着节点的增多,耗时会不断增大
  • 网络连接数变多,对节点的性能也有一定影响

批量操作优化:

  1. 串行命令

    逐次执行 n 个命令(如 get)

    • 操作时间 = n 次网络时间 + n 次命令时间
    • 优点
      • 编程简单
      • 如果少量 keys,性能可以满足要求
    • 缺点:大量 keys 请求延迟严重
  2. 串行 IO

    将属于同一个节点的 key 进行归档,得到每个节点的 key 子列表,之后对每个节点执行 mget 或者 Pipeline 操作

    • 操作时间 = node 次网络时间 + n 次命令时间
    • 优点
      • 编程简单
      • 少量节点,性能满足要求
    • 缺点:大量 node 延迟严重
  3. 并行 IO

    串行 IO 中的最后一步改为多线程执行

    • 操作时间 = max_slow(node 网络时间) + n 次命令时间
    • 优点:利用并行特性,延迟取决于最慢的节点
    • 缺点
      • 编程复杂
      • 多线程,问题定位可能较难
  4. hash-tag 实现

    将多个 key 强制分配到一个节点上

    • 操作时间 = 1 次网络时间 + n 次命令时间
    • 优点:性能最高
    • 缺点
      • 业务维护成本较高
      • 容易出现数据倾斜

雪崩优化

缓存雪崩:由于缓存层承载着大量请求,有效地保护了存储层,但是如果缓存层由于某些原因不能提供服务,于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会级联宕机的情况。

解决方法:

  • 保证缓存层服务高可用性
  • 依赖隔离组件为后端限流并降级
  • 提前测试,在项目上线前,演练缓存层宕掉后,应用以及后端的负载情况以及可能出现的问题,在此基础上做一些预案设定

热点 key 重建优化

开发人员使用“缓存+过期时间”的策略既可以加速数据读写,又保证数据的定期更新,这种模式基本能够满足绝大部分需求。但是以下两个问题如果同时出现,可能就会对应用造成致命的危害:

  • 当前 key 是一个热点 key(例如一个热门的娱乐新闻),并发量非常大
  • 重建缓存不能在短时间完成
  1. 互斥锁(mutex key)

    此方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。

    优点

    • 思路简单
    • 保证一致性

    缺点

    • 代码复杂度增大
    • 存在死锁的风险
    • 存在线程池阻塞的风险
  2. 永远不过期

    “永远不过期” 包含两层意思:

    • 从缓存层面来看,确实没有设置过期时间,所以不会出现热点 key 过期后产生的问题,也就是“物理”不过期
    • 从功能层面来看,为每个 value 设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去构建缓存

    优点

    • 基本杜绝热点 key 问题

    缺点

    • 不保证一致性
    • 逻辑过期时间增加代码维护成本和内存成本