缓存问题隐患
- 缓存无底洞
- 缓存穿透
- 缓存雪崩
- 缓存失效
- 热点key倾斜
- 热点key重建
- 缓存数据库双写不一致
缓存淘汰算法
- FIFO:先进先出,在这种淘汰算法中,先进入缓存的会先被淘汰,会导致命中率很低。
- LRU:最近最少使用算法,每次访问数据都会将其放在我们的队尾,如果需要淘汰数据,就只需要淘汰队首即可。仍然有个问题,如果有个数据在 1 分钟访问了 1000次,再后 1 分钟没有访问这个数据,但是有其他的数据访问,就导致了我们这个热点数据被淘汰。
- LFU:最近最少频率使用,利用额外的空间记录每个数据的使用频率,然后选出频率最低进行淘汰。这样就避免了 LRU 不能处理时间段的问题。
上面三种策略各有利弊,实现的成本也是一个比一个高,同时命中率也是一个比一个好。Guava Cache虽然有这么多的功能,但是本质上还是对LRU的封装,如果有更优良的算法,并且也能提供这么多功能,相比之下就相形见绌了。
LFU的局限性 :在 LFU 中只要数据访问模式的概率分布随时间保持不变时,其命中率就能变得非常高。比如有部新剧出来了,我们使用 LFU 给他缓存下来,这部新剧在这几天大概访问了几亿次,这个访问频率也在我们的 LFU 中记录了几亿次。但是新剧总会过气的,比如一个月之后这个新剧的前几集其实已经过气了,但是他的访问量的确是太高了,其他的电视剧根本无法淘汰这个新剧,所以在这种模式下是有局限性。
LRU的优点和局限性 :LRU可以很好的应对突发流量的情况,因为他不需要累计数据频率。但LRU通过历史数据来预测未来是局限的,它会认为最后到来的数据是最可能被再次访问的,从而给与它最高的优先级。
缓存一致性问题:
参考: https://mp.weixin.qq.com/s?__biz=MzAwNTQ4MTQ4NQ==&mid=2453582112&idx=1&sn=005ffdf453d025a04b417179a2599f76&chksm=8cd1e442bba66d54263d5e78a10fa454e5511649d5c4bdf146dce377114cf90c8fc33fc87b00&scene=21#wechat_redirect
先更新数据库,后删除缓存
分布式缓存的25个优秀实践与线上案例
1、缓存雪崩
问题现象:大量的缓存同一时刻失效;
典型问题:所有首页的Key失效时间都是12小时,秒杀活动大量用户涌入,假设当时每秒 6000 个请求,本来缓存在可以扛住每秒 5000 个请求,但是缓存当时所有的Key都失效了。此时 1 秒 6000 个请求全部落数据库。
解决方案:
把每个Key的失效时间都加个随机值,这样可以保证数据不会在同一时间大面积失效
如果Redis是集群部署,将热点数据均匀分布在不同的Redis库中也能避免全部失效的问题;
或者设置热点数据永远不过期,有更新操作就更新缓存就好了(比如运维更新了首页商品,那你刷下缓存就完事了,不要设置过期时间)
2、缓存穿透
问题现象:缓存和数据库中都没有的数据,而用户不断发起请求,我们数据库的 id 都是1开始自增上去的,如发起为id值为 -1 的数据或 id 为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大,严重会击垮数据库。
解决方案:
接口层增加校验,比如用户鉴权校验,参数做校验,不合法的参数直接代码Return,比如:id 做基础校验,id <=0的直接拦截等。
将对应Key的Value对写为null,过期时间设置很短
高级用法布隆过滤器(Bloom Filter)这个也能很好的防止缓存穿透的发生,他的原理也很简单就是利用高效的数据结构和算法快速判断出你这个Key是否在数据库中存在,不存在你return就好了,存在你就去查了DB刷新KV再return。
利用nginx对单个IP的访问频次做阈值控制
3、缓存击穿
指一个Key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个Key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库。
解决方案:
设置热点数据永远不过期。或者加上互斥锁就能搞定了
一般避免以上情况发生我们从三个时间段去分析下:
- 事前:Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃。
- 事中:本地 ehcache Hystrix 限流+降级,避免MySQL被打死。
- 事后:Redis RDB+AOF,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
4、数据库与缓存双写不一致问题(秒杀场景)
读多写少: 引入分布式锁的读写锁如Redisson.getReadWriteLock(lockKey)