缓存设计中的常见问题

缓存更新策略

  1. LRU/LFU/FIFO算法剔除

    • 适用场景:缓存使用量超过了最大值
    • 一致性:清理具体哪些数据由具体算法决定,一致性较差
    • 维护成本:几乎不需要
  2. 超时剔除

    • 适用场景:业务可以容忍一段时间内,缓存层数据域存储层数据不一致
    • 一致性:一段时间窗口内存在一致性问题
    • 维护成本:不是很高
  3. 主动更新

    • 适用场景:真实数据更新后,立即更新缓存数据。例如利用消息系统或其他方式通知缓存更新
    • 一致性:高
    • 维护成本:最高。如果主动更新发生了问题,那么这条数据很可能很长时间不会更新,所以建议结合超时剔除一起使用效果会更好
  4. 最佳实践

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

缓存穿透

缓存穿透指查询一个根本不存在的数据,缓存层和存储层都不会命中,导致不存在的数据请求每次都要到存储层查询。

造成缓存穿透的基本原因有:

  • 自身业务代码或数据出现问题
  • 一些恶意攻击、爬虫等造成大量空命中

1.缓存空对象

当存储层不命中后,仍然将空对象保存到缓存层中,之后再访问这个数据将会从缓存中获取。

可能会引入的问题: - 空值缓存,需要更多的内存空间:针对这类数据设置一个较短的过期时间,让其自动剔除 - 不一致问题:存储层和数据层存在不一致。可用消息系统或其他方式清掉缓存中的空对象

2.布隆滤波器过滤

将存在的 key 用布隆滤波器提前保存起来,做第一层拦截。

无底洞优化

“无底洞”现象:为了满足业务要求添加了大量新的节点,但是性能不但没有好转反而下降了。

原因:键值数据库通常采用哈希函数将 key 映射到各个节点上,造成key 的分布与业务无关,由于数据量和访问量的持续增长,造成需要添加大量节点做水平扩容,导致键值分布到更多节点上。批量操作通常需要从不同节点上获取,相当于单机批量操作只涉及一次网络操作,分布式批量操作会涉及多次网络时间。

假设批量获取n个字符串为例:

1.串行命令

逐次执行n个get命令,操作时间=n次网络时间+n次命令时间

2.串行IO

Smart客户端会保存 slot 和节点的对应关系,有了这两个数据可将属于同一个节点的 key 进行归档,得到每个节点的key子列表,之后对每个节点执行 mget  Pipeline 操作。操作时间=node次网络事件+n次命令时间。

3.并行IO

将上面串行IO的最后一步改为多线程执行,使用多线程网络事件变为O(1),操作时间变为:

max_slow(node网络事件) + n次命令时间

4.hash_tag实现

将多个 key 强制分配到一个节点上,操作时间=1次网络事件+n次命令时间

雪崩

缓存雪崩:缓存层由于某些原因不能提供服务,于是所有请求都会达到存储层,存储层调用量暴增,造成存储层级联宕机的情况。

解决思路:

  • 保证缓存层服务高可用性:
  • 依赖隔离组件为后端限流并降级。Hystrix依赖隔离
  • 提前演练:演练缓存层宕掉后,可能出现的问题,做一些预案设定。

热点key重建优化

如果有两个问题同时出现:

  • key 是一个热点 key ,并发量大
  • 重建缓存不能在短时间完成,可能是一个复杂计算

在缓存失效瞬间,有大量线程重建缓存,造成后端负载加大,甚至可能会让应用崩溃。

解决思路:

  1. 互斥锁:只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。
  2. 永远不过期:

两层意思:

  • 缓存层面看:没有设置过期时间,“物理”不过期。存在一定隐患,可能会存在死锁和线程池阻塞的风险。
  • 功能层面看:为每个 value 设置一个逻辑过期时间,当发现超过逻辑过期时间,会使用单独的线程来构建缓存。会存在数据不一致的情况,且代码复杂度会增大。