(1)概念
雪崩就是指缓存中大批量热点数据过期后系统涌入大量查询请求,因为大部分数据在Redis层已经失效,请求渗透到数据库层,大批量请求犹如洪水一般涌入,引起数据库压力造成查询堵塞甚至宕机。
(2)多种解决方案 [重点***]
=======================================解决方案 1 ====================================== (1) 缓存雪崩的事前事中事后的解决方案1 // 【Redis集群高可用】 - 事前:redis 高可用,主从+哨兵,redis cluster,避免全盘崩溃。 即分布式集群,其中一台Redis服务器挂掉,其他Redis主机再服务,实现高可用; // 【多缓存、限流、熔断机制】 - 事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。 即先查询本地ehcache 缓存 ----->查询Redis -----> MySQL 其中再实现熔断器限流&降级 // 【缓存预热】 - 事后:Redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。 好处: - 数据库绝对不会死,限流组件确保了每秒只有多少个请求能通过。 - 只要数据库不死,就是说,对用户来说,2/5 的请求都是可以被处理的。 - 只要有 2/5 的请求可以被处理,就意味着你的系统没死,对用户来说,可能就是点击几次刷不出来页面,但是多点几次,就可以刷出来一次。 =======================================解决方案 2 ====================================== (2) 解决方案2 //【加锁】 用加锁或者队列的方式保证来不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层数据库。 =======================================解决方案 3 ====================================== (2) 解决方案3 // 【均匀分布】 - 将缓存失效时间分散开,比如每个key的过期时间是随机,防止同一时间大量数据过期现象发生,这样不会出现同一时间全部请求都落在数据库层,如果缓存数据库是分布式部署,将热点数据均匀分布在不同Redis和数据库中,有效分担压力,别让一个人扛。 =======================================解决方案 4 ====================================== (3)解决方案4// 【永不过期】 - 简单粗暴,让Redis数据永不过期(如果业务准许,比如不用更新的名单类)。当然,如果业务数据准许的情况下可以,比如中奖名单用户,每期用户开奖后,名单不可能会变了,无需更新。
(3)示例代码
<1> 加锁排队,伪代码如下:(解决方案 2 )
//伪代码 public object GetProductListNew() { int cacheTime = 30; String cacheKey = "product_list"; String lockKey = cacheKey; String cacheValue = CacheHelper.get(cacheKey); if (cacheValue != null) { return cacheValue; } else { synchronized(lockKey) { cacheValue = CacheHelper.get(cacheKey); if (cacheValue != null) { return cacheValue; } else { //这里一般是sql查询数据 cacheValue = GetProductListFromDB(); CacheHelper.Add(cacheKey, cacheValue, cacheTime); } } return cacheValue; } }
<2>缓存失效时间随机值的伪代码:(解决方案 3)
//伪代码 public object GetProductListNew() { int cacheTime = 30; String cacheKey = "product_list"; //缓存标记 String cacheSign = cacheKey + "_sign"; String sign = CacheHelper.Get(cacheSign); //获取缓存值 String cacheValue = CacheHelper.Get(cacheKey); if (sign != null) { return cacheValue; //未过期,直接返回 } else { CacheHelper.Add(cacheSign, "1", cacheTime); ThreadPool.QueueUserWorkItem((arg) -> { //这里一般是 sql查询数据 cacheValue = GetProductListFromDB(); //日期设缓存时间的2倍,用于脏读 CacheHelper.Add(cacheKey, cacheValue, cacheTime * 2); }); return cacheValue; } } /* 说明: - 缓存标记:记录缓存数据是否过期,如果过期会触发通知到另外的线程在后台去更新实际key的缓存; - 缓存数据:它的过期时间比缓存标记的时间延长1倍,例:标记缓存时间30分钟,数据缓存设置为60分钟。这样,当缓存标记key过期后,实际缓存还能把旧数据返回给调用端,直到另外的线程在后台更新完成后,才会返回新缓存。 /*
说明:
- 缓存标记:记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际key的缓存;
- 缓存数据:它的过期时间比缓存标记的时间延长1倍,例:标记缓存时间30分钟,数据缓存设置为60分钟。这样,当缓存标记key过期后,实际缓存还能把旧数据返回给调用端,直到另外的线程在后台更新完成后,才会返回新缓存。
(4) 雪崩示例
对于系统 A,假设每天高峰期每秒 5000 个请求,本来缓存在高峰期可以扛住每秒 4000 个请求,但是缓存机器意外发生了全盘宕机。缓存挂了,此时 1 秒 5000 个请求全部落数据库,数据库必然扛不住,它会报一下警,然后就挂了。此时,如果没有采用什么特别的方案来处理这个故障,DBA(数据库管理员) 很着急,重启数据库,但是数据库立马又被新的流量给打死了。 这就是缓存雪崩。
2. 缓存穿透
(1)概念
对于系统A,假设一秒 5000 个请求,结果其中 4000 个请求是黑客发出的恶意攻击。 那 4000 个请求数据,都不存在于缓存中,所以需要每次去数据库里查,可能也查不到。这样Redis就失效,不发挥作用了。也导致数据库直接面临大量请求。 如数据库 id 是从 1 开始的,结果黑客发过来的请求 id 全部都是负数。这样的话,缓存中不会有,请求每次都“视缓存于无物”,直接查询数据库。这种恶意攻击场景的缓存穿透就会直接把数据库给打死。
(2)解决方案
每次系统 A 从数据库中只要没查到,就写一个空值到缓存里去,比如 set -999 UNKNOWN。 然后设置一个过期时间,这样的话,下次有相同的 key 来访问的时候,在缓存失效之前,都可以直接从缓存中取数据。
3. 缓存击穿
(1)概念
缓存击穿:某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况, 当这个 key 在【失效的瞬间】,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。
(2)解决方案
可以将热点数据设置为永远不过期;或者基于 redis or zookeeper 实现互斥锁,等待第一个请求构建完缓存之后,再释放锁,进而其它请求才能通过该 key 访问数据。 使用互斥锁(mutex key) 简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。 SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。
public String get(key) { String value = redis.get(key); if (value == null) { //代表缓存值过期 //设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表设置成功 value = db.get(key); redis.set(key, value, expire_secs); redis.del(key_mutex); } else { //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可 sleep(50); get(key); //重试 } } else { return value; } }
总结:
雪崩的解决方案: 【大批量热点数据过期】
1. 数据的过期时间采用随机数,均匀分布 2. 数据的过期时间设置为永不过期 3. 搭建redis集群,实现高可用 4. 本地ehcache +redis +熔断&降级 5. 定期加载热数据到Redis缓存
穿透:【恶意请求,数据不存在于缓存、数据库中,如 id = -1,-2】
- 将不存在于缓存、数据库的数据,设置为null , 保存到redis 缓存中,并且设置过期时(redis:-1-null, -2- null )
- ip恶意频繁访问进行拦截
- 请求参数进行过滤
保证在短期内 用户恶意的请求 从redis 缓存中加载数据
击穿:【 单个热点数据 key 在 失效的瞬间 】
1.热点数据设置为永远不过期
2. 加互斥锁 --- mysql --- redis