写点什么

【实战问题】-- 缓存穿透,缓存击穿和缓存雪崩的区别以及解决方案

发布于: 2021 年 03 月 22 日

平时我们使用缓存的方案,一般是在数据库中存储一份,在缓存中同步存储一份。当请求过来的视乎,可以先从缓存中取数据,如果有数据,直接返回缓存中的结果。如果缓存中没有数据,那么去数据库中取出数据,同时更新到缓存中,返回结果。如果数据库中也没有数据,可以直接返回空。


关于缓存,一般会有以下几个常见的问题


缓存穿透

缓存穿透是指,缓存和数据库都没有的数据,被大量请求,比如订单号不可能为-1,但是用户请求了大量订单号为-1的数据,由于数据不存在,缓存就也不会存在该数据,所有的请求都会直接穿透到数据库。

如果被恶意用户利用,疯狂请求不存在的数据,就会导致数据库压力过大,甚至垮掉。


注意:穿透的意思是,都没有,直接一路打到数据库。


那对于这种情况,我们该如何解决呢?


  1. 接口增加业务层级的Filter,进行合法校验,这可以有效拦截大部分不合法的请求。

  2. 作为第一点的补充,最常见的是使用布隆过滤器,针对一个或者多个维度,把可能存在的数据值 hash 到 bitmap 中,bitmap 证明该数据不存在则该数据一定不存在,但是 bitmap 证明该数据存在也只能是可能存在,因为不同的数值 hash 到的 bit 位很有可能是一样的,hash 冲突会导致误判,多个 hash 方法也只能是降低冲突的概率,无法做到避免。

  3. 另外一个常见的方法,则是针对数据库与缓存都没有的数据,对空的结果进行缓存,但是过期时间设置得较短,一般五分钟内。而这种数据,如果数据库有写入,或者更新,必须同时刷新缓存,否则会导致不一致的问题存在。


缓存击穿

缓存击穿是指数据库原本有得数据,但是缓存中没有,一般是缓存突然失效了,这时候如果有大量用户请求该数据,缓存没有则会去数据库请求,会引发数据库压力增大,可能会瞬间打垮。


针对这类问题,一般有以下做法:

  1. 如果是热点数据,那么可以考虑设置永远不过期。

  2. 如果数据一定会过期,那么就需要在数据为空的时候,设置一个互斥的锁,只让一个请求通过,只有一个请求去数据库拉取数据,取完数据,不管如何都需要释放锁,异常的时候也需要释放锁,要不其他线程会一直拿不到锁。


下面是缓存击穿的时候互斥锁的写法,注意:获取锁之后操作,不管成功或者失败,都应该释放锁,而其他的请求,如果没有获取到锁,应该等待,再重试。当然,如果是需要更加全面一点,应该加上一个等待次数,比如 1s 中,那么也就是睡眠五次,达到这个阈值,则直接返回空,不应该过度消耗机器,以免当个不可用的场景把整个应用的服务器带挂了。


    public static String getProductDescById(String id) {        String desc = redis.get(id);        // 缓存为空,过期了        if (desc == null) {            // 互斥锁,只有一个请求可以成功            if (redis.setnx(lock_id, 1, 60) == 1) {                try {                    // 从数据库取出数据                    desc = getFromDB(id);                    redis.set(id, desc, 60 * 60 * 24);                } catch (Exception ex) {                    LogHelper.error(ex);                } finally {                    // 确保最后删除,释放锁                    redis.del(lock_id);                    return desc;                }            } else {                // 否则睡眠200ms,接着获取锁                Thread.sleep(200);                return getProductDescById(id);            }        }    }
复制代码

缓存雪崩


缓存雪崩是指缓存中有大量的数据,在同一个时间点,或者较短的时间段内,全部过期了,这个时候请求过来,缓存没有数据,都会请求数据库,则数据库的压力就会突增,扛不住就会宕机。


针对这种情况,一般我们都是使用以下方案:

  1. 如果是热点数据,那么可以考虑设置永远不过期。

  2. 缓存的过期时间除非比较严格,要不考虑设置一个波动随机值,比如理论十分钟,那这类 key 的缓存时间都加上一个 1~3 分钟,过期时间在 7~13 分钟内波动,有效防止都在同一个时间点上大量过期。

  3. 方法 1 避免了有效过期的情况,但是要是所有的热点数据在一台 redis 服务器上,也是极其危险的,如果网络有问题,或者 redis 服务器挂了,那么所有的热点数据也会雪崩(查询不到),因此将热点数据打散分不到不同的机房中,也可以有效减少这种情况。

  4. 也可以考虑双缓存的方式,数据库数据同步到缓存 A 和 B,A 设置过期时间,B 不设置过期时间,如果 A 为空的时候去读 B,同时异步去更新缓存,但是更新的时候需要同时更新两个缓存。


比如设置产品的缓存时间:

redis.set(id,value,60*60 + Math.random()*1000);
复制代码


小结

缓存穿透是指数据库原本就没有的数据,请求如入无人之境,直奔数据库,而缓存击穿,则是指数据库有数据,缓存也本应该有数据,但是突然缓存过期了,这层保护屏障被击穿了,请求直奔数据库,缓存雪崩则是指很多缓存同一个时间失效了,流量全部涌入数据库,造成数据库极大的压力。


【刷题笔记】

Github 仓库地址:https://github.com/Damaer/codeSolution

笔记地址:https://damaer.github.io/codeSolution/


【作者简介】

秦怀,公众号【秦怀杂货店】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:Java 源码解析,JDBC,Mybatis,Spring,redis,分布式,剑指 Offer,LeetCode 等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。


2020年我写了什么?


开源刷题笔记


平日时间宝贵,只能使用晚上以及周末时间学习写作,关注我,我们一起成长吧~


发布于: 2021 年 03 月 22 日阅读数: 22
用户头像

纵使缓慢,驰而不息。 2018.05.17 加入

慢慢走,比较快。公众号:秦怀杂货店

评论

发布
暂无评论
【实战问题】-- 缓存穿透,缓存击穿和缓存雪崩的区别以及解决方案