关于:缓存穿透、缓存击穿、缓存雪崩、热点数据失效
跳到导航
跳到搜索
关于
项目通常会引入NoSQL技术(即:基于内存的数据库,如redis),来应对大数据量的需求,以规避磁盘读/写速度比较慢的问题而存在的性能弊端;
但是,引入redis等又有可能出现缓存穿透,缓存击穿,缓存雪崩等问题,并需要开发对这些问题进行考量并作出应对。
缓存穿透
缓存穿透:请求一个不存在的数据,缓存和数据库都查不到这个数据,每次都会去数据库查询。
- 比如:用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。
解决方案
有很多种方法可以有效地解决缓存穿透问题:
- BloomFilter:(“布隆过滤器”)将所有可能存在的数据哈希到一个足够大的bitmap中:每次查询的时候都先去BloomFilter判断,如果没有就直接返回null;如果存在再进行查缓存、DB查询;
- 注意BloomFilter没有删除操作,对于删除的key,可以在缓存中缓存null;
- 缓存空值:如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),仍然为这些key在缓存中设置对应的值为null。
- 要对这些key设置过期时间,以防止真的有数据;
//伪代码 public object GetProductListNew() { int cacheTime = 30; String cacheKey = "product_list"; String cacheValue = CacheHelper.Get(cacheKey); if (cacheValue != null) { return cacheValue; } cacheValue = CacheHelper.Get(cacheKey); if (cacheValue != null) { return cacheValue; } else { //数据库查询不到,为空 cacheValue = GetProductListFromDB(); if (cacheValue == null) { //如果发现为空,设置个默认值,也缓存起来 cacheValue = string.Empty; } CacheHelper.Add(cacheKey, cacheValue, cacheTime); return cacheValue; } }
- BloomFilter可以结合缓存空值用;
- 针对于一些恶意攻击,攻击带过来的大量key是不存在的,可以考虑使用BloomFilter进行过滤。
缓存击穿
缓存击穿:大量的请求同时查询同一个key时,此时这个key正好失效了,就会导致同一时间,这些请求都会去查询数据库,这样的现象我们称为缓存击穿。
解决方案
- 采用分布式锁,只有拿到锁的第一个线程去请求数据库,然后插入缓存,当然每次拿到锁的时候都要去查询一下缓存有没有;
//伪代码 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; } }
缓存雪崩
缓存雪崩:当某一时刻发生大规模的缓存失效的情况,如缓存服务宕机。
解决方案
针对服务器宕机:
- 采用集群,降低服务宕机的概率;
- ehcache本地缓存 + Hystrix限流&降级;【???】
- ehcache 本地缓存:在 Redis Cluster 完全不可用的时候,ehcache 本地缓存做临时支持;
- Hystrix进行限流 & 降级 ,比如一秒来了5000个请求,我们可以设置假设只能有一秒 2000个请求能通过这个组件,那么其他剩余的 3000 请求就会走限流逻辑;
热点数据失效
热点数据失效:缓存的数据集体过期;
解决方案
针对缓存过期:
- 为缓存设置不同的失效时间(原有的失效时间基础上增加一个随机值),避免集体失效;
- 采用加锁或队列,来保证不会有大量的线程对数据库一次性进行读写;(缓存击穿的解决办法)
- 加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。甚至可能带来分布式锁的问题,因此,在真正的高并发场景下很少使用!
- 永不失效,就是采用定时任务对快要失效的缓存进行更新缓存和失效时间;
- (设置过期标志更新缓存:)
//伪代码 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过期后,实际缓存还能把旧数据返回给调用端,直到另外的线程在后台更新完成后,才会返回新缓存。