缓存设计中的常见问题
缓存更新策略
- LRU/LFU/FIFO算法剔除
- 适用场景:缓存使用量超过了最大值
- 一致性:清理具体哪些数据由具体算法决定,一致性较差
- 维护成本:几乎不需要
- 超时剔除
- 适用场景:业务可以容忍一段时间内,缓存层数据域存储层数据不一致
- 一致性:一段时间窗口内存在一致性问题
- 维护成本:不是很高
- 主动更新
- 适用场景:真实数据更新后,立即更新缓存数据。例如利用消息系统或其他方式通知缓存更新
- 一致性:高
- 维护成本:最高。如果主动更新发生了问题,那么这条数据很可能很长时间不会更新,所以建议结合超时剔除一起使用效果会更好
- 最佳实践
- 低一致性业务建议配置最大内存和淘汰策略的方式使用
- 高一致性业务结合使用超时剔除和主动更新,这样即使主动更新出了问题,也能保证数据过期时间后删除脏数据
缓存穿透
缓存穿透指查询一个根本不存在的数据,缓存层和存储层都不会命中,导致不存在的数据请求每次都要到存储层查询。
造成缓存穿透的基本原因有:
- 自身业务代码或数据出现问题
- 一些恶意攻击、爬虫等造成大量空命中
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 ,并发量大
- 重建缓存不能在短时间完成,可能是一个复杂计算
在缓存失效瞬间,有大量线程重建缓存,造成后端负载加大,甚至可能会让应用崩溃。
解决思路:
- 互斥锁:只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。
- 永远不过期:
两层意思:
- 缓存层面看:没有设置过期时间,“物理”不过期。存在一定隐患,可能会存在死锁和线程池阻塞的风险。
- 功能层面看:为每个 value 设置一个逻辑过期时间,当发现超过逻辑过期时间,会使用单独的线程来构建缓存。会存在数据不一致的情况,且代码复杂度会增大。