缓存的设计以及相关问题
受益与成本
受益
- 加速读写
- 降低后端负载
成本
- 数据不一致:缓存层和数据层在时间窗口不一致
- 代码维护成本
- 运维成本:例如Redis cluster
缓存更新策略
各种更新策略对比
策略 | 一致性 | 维护成本 |
---|---|---|
LRU/LFU/FIFO算法剔除 | 最差 | 低 |
超时剔除(expire) | 较差 | 低 |
主动更新(开发控制缓存和数据一致性的业务) | 高 | 高 |
LRU和LFU的区别如下:
算法缩写 | 算法名称 | 算法目的 |
---|---|---|
LRU | 最近最少使用算法(Least Recently Used) | 淘汰最近最长时间未被使用的数据 |
LFU | 最近最不常用算法(Least Frequently Used) | 淘汰一定时期内被访问次数最少的数据 |
建议
- 低一致性:最大内存和淘汰策略
- 高一致性: 超时剔除和主动更新结合,最大内存和淘汰策略兜底。
缓存穿透问题
定义
缓存穿透是指查询时,这条数据在数据库和缓存都没有,但是还会一直查询数据库,对数据库的访问压力就会增加。
解决方案
缓存穿透的解决方案有以下两种:
- 缓存空对象:缓存空对象的实现代码很简单,但是缓存空对象会带来比较大的问题,就是缓存中会存在很多空对象,占用内存的空间(即使设置了过期时间),浪费资源。
- 布隆过滤器:布隆过滤器是一种基于概率的数据结构,主要用来判断某个元素是否在集合内,它具有运行速度快(时间效率),占用内存小的优点(空间效率),但是有一定的误识别率以及不支持删除的问题。它只能告诉你某个元素一定不在集合内或可能在集合内。
缓存击穿问题(热点key重建问题)
定义
缓存击穿是指一个key非常热点,在不停的扛着大并发,多个线程集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存直接请求数据库,瞬间对数据库的访问压力增加,甚至让应用崩溃。
原因
缓存击穿这里强调的是并发,造成缓存击穿的原因有以下两个:
- 该数据很少有人查询 ,突然大并发的访问(冷门数据)。
- 添加到了缓存且有设置数据失效的时间 ,在这条数据缓存刚好失效时大并发访问(热点数据)。
目标
- 减少重建缓存的次数 数据尽可能一致 较少的潜在危险
解决方案
目前有下面两种解决方案:
- 互斥锁:此方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。
- 永不过期:包含两层意思:从缓存层面来看,确实没有设置过期时间(没有用expire)。从功能层面来看,为每个value设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去构建缓存。
两种方案优缺点如下:
方案 | 优点 | 缺点 |
---|---|---|
互斥锁 | 思路简单、保证一致性 | 代码复杂度增加、存在死锁的风险 |
永远不过期 | 基本杜绝缓存击穿问题 | 不保证一致性、逻辑过期时间增加维护成本和内存成本 |
缓存雪崩问题
定义
缓存雪崩是指在某一个时间段内缓存集中过期失效,此刻无数的请求绕开缓存直接请求数据库,对数据库的访问压力增加,甚至压垮数据库。
原因
造成缓存雪崩的原因,有以下两种:
- reids宕机
- 大部分数据失效
解决方案
对于缓存雪崩的解决方案有以下两种:
- 搭建高可用的集群,防止单机的Redis宕机。
- 设置不同的过期时间,防止同一时间内大量的key失效。
无底洞问题
问题描述
Facebook的工作人员反应2010年已达到3000个memcached节点,储存数千G的缓存。他们发现一个问题–memcached的连接效率下降了,于是添加memcached节点,添加完之后,并没有好转。称为“无底洞”现象。
问题原因
键值数据库或者缓存系统,由于通常采用hash函数将key映射到对应的实例,造成key的分布与业务无关,但是由于数据量、访问量的需求,需要使用分布式后(无论是客户端一致性哈性、redis-cluster、codis),批量操作比如批量获取多个key(例如Redis的mget操作),通常需要从不同实例获取key值,相比于单机批量操作只涉及到一次网络操作,分布式批量操作会涉及到多次网络io。
问题关键点
- 更多的机器!=更高的性能
- 批量接口需求(mget,mset等)
- 数据增长与水平扩展需求
IO的优化思路
- 命令本身的效率:例如sql优化,命令优化
- 网络次数:减少通信次数
- 降低接入成本:长连/连接池,NIO等
- IO访问合并:O(n)到O(1)过程:批量接口(mget)