缓存失效场景&优化
缓存穿透
场景
查询根本不存在的数据,使请求直达存储层。被恶意攻击者利用,导致负载过高宕机。
优化
缓存空对象
存储层查不到返回空值,将空值也存入缓存中,这样请求就可以打到缓存层。
缺点1:如果是攻击,将占用大量缓存空间。
解决:将空值键设置较短的过期时间。
缺点2:如果缓存层已缓存了空值,此时存储层对应的键已更新了值,会造成缓存层与存储层数据不一致。
解决:通过消息系统等方式清除掉缓存层的值。
布隆过滤器
将所有键存到缓存层之前的一个布隆过滤器中,请求打过来,如果布隆过滤其中没有对应key,就直接返回空,避免数据打到缓存层和存储层。
举例:比如一个推荐系统有1亿用户,每个小时后台根据用户的历史行为计算出推荐数据放入缓存层。
但新用户没有推荐数据,因此新用户的推荐数据请求会打到存储层。加入布隆过滤器,将1亿用户id放入,新用户di传过来,布隆过滤器中没有,就可以返回空值。
对比
优化方案 | 适用场景 | 维护成本 |
---|---|---|
缓存空值 | 数据命中不高/数据频繁变化实时性高 | 代码维护简单/需要过多缓存空间/数据不一致 |
布隆过滤器 | 数据命中不高/数据相对固定实时性低 | 代码维护复杂/缓存空间占用少 |
缓存击穿/热点key重建
场景
一份热点数据访问量非常大,在其失效瞬间,大量请求涌向存储层,导致服务崩溃。
优化
加互斥锁
对数据的访问加互斥锁,当一个线程访问该数据时,其他线程只能等待。
该线程访问过后,缓存中的数据将被重建,到时其他线程就可以直接从缓存取值。
永不过期
物理上不设置过期时间。
给value设置逻辑过期时间,发现逻辑时间过期时,单独线程重建缓存。
下次再访问时,value就是新值。
对比
优化方案 | 优点 | 缺点 |
---|---|---|
简单分布式锁 | 思路简单/保证一致性 | 代码复杂度大/存在死锁风险/存在线程池阻塞风险 |
永不过期 | 基本杜绝热点key问题 | 不保证一致性/逻辑过期时间增加代码维护和内存成本 |
缓存雪崩
场景
某种情况下整个缓存层突然失效,请求全部直达存储层。
优化
保证高可用,Redis Sentinel&Redis Cluster
避免键同时过期,键的过期时间设置为随机数
构建多级缓存,如增加本地缓存
限流&降级服务:一个服务无法提供后提供降级服务
p.s. Hystrix隔离工具
缓存无底洞
场景
缓存节点越扩越多,缓存性能反而下降。
原因:key通过hash算法分布在不同的节点上,如果一次性 mget
N个key,N个key会分布在M个节点上,节点越多,M可能越大,网络延时开销就越大。而只在1个节点上 mget
N个key,只有一次网络开销。
优化
前提:假设要一次性获取n个key的值。在集群上,直接mget keys
会报错,因为key分布在不同的slot上。
只有key都在同一个slot上才可以。
串行命令,执行n次get key
开销:n次网络时间 + n次命令时间
串行IO,Smart客户端会保存slot和节点的对应关系,这样可以得到每个节点key的字列表,再通过CRC16算法算出slot。
开销:node次网络时间 + n次命令时间
并行IO,因节点不同,将上面的串行改为并行。
开销:最慢的那次网络时间 + n次命令时间
hashtag,存的时候将所有key通过hashtag强制放到同一个槽上,获取的时候就方便获取
开销:1次网络时间 + n次命令时间
hashtag原理:键中内容含{},计算slot时是以slot中的内容为准
mget user:{10086}:info user:{10086}:video
对比
优化方案 | 优点 | 缺点 | 网络IO |
---|---|---|---|
串行命令 | 编程简单/key少可满足性能要求 | 大量key请求延迟严重 | O(keys) |
串行IO | 编程简单/节点少可满足性能要求 | 大量node请求延迟严重 | O(nodes) |
并行IO | 延迟取决于最慢节点 | 编程复杂/多线程定位问题较难 | O(max_slow(nodes)) |
hash_tag | 性能最高 | 业务维护成本高/易出现数据倾斜 | O(1) |