Redis缓存雪崩,击穿,穿透

redis

Posted by MistRay on June 17, 2020

前言

写这篇文章的契机是同事最近面试遇到的一道神奇面试题。 面试官先让同事解释了下Redis缓存雪崩,击穿,穿透的含义和解决方案。 然后刁钻的问题来了,缓存雪崩后如何恢复。下面会对上述问题逐一分析。

缓存雪崩

服务中的热点数据会做缓存,一般就是两种逻辑,第一种就是通过定时任务去刷新,第二种就是查缓存查不到后更新数据。

缓存雪崩指的是缓存同时大面积失效的情况,一般存在于上面说的第一种缓存逻辑中。如果能够通过定时任务刷新缓存, 刷新后缓存数据的失效时间会集中在一个很小的时间区间,如果这个时候同时有大流量打进来(比如秒杀场景),后果不堪设想。

轻则数据库cpu飙升,服务报警的轰炸会让半夜正在睡觉的你不得安宁,重则数据库被打挂,缓存这块的责任人当月绩效直接拿C。 用户点了半天没反应,睡觉去了,并且卸载了你们的app。

但其实解决这种情况的方法很简单。既然不可以同时大面积失效,那我把缓存失效的时间段拉长不就好了。每个Key的失效时间都加个随机值, 这样就可以让缓存不会大面积同时失效,数据库的压力也会小很多。

上面的解决方案已经很简单了,但还有个更简单的方法,就是热点数据永不过期,有更新操作就更新缓存。 这块可以抽取出来一个服务,专门做缓存的更新,传输使用MQ,用多Topic,多partition分片路由的方式保证顺序。

缓存穿透

缓存穿透指的是缓存和数据库中都没有数据,而用户/黑客不断发起查询请求去查询不存在数据,导致数据库压力过大,最终把数据库打卦的情况。

缓存穿透的影响比缓存雪崩的影响要小一些,现在云服务器都自带防御,花点钱还能买高防, 如果没有比较大的ip肉鸡资源池,想打透服务器的防御,其实并不简单。 但我们还可以使黑客的攻击成本变得更高,就是添加各种校验,业务相关的,有的没的都校验上,还可以增加用户鉴权。

还有一个方案就是取不到就写null或对应的错误信息到redis,缓存有效时间设置的短一些,防止影响到正常的业务。

如果你还嫌不够安全,Redis自己实现了一个布隆过滤器(Bloom Filter),原理就是利用它特有的数据结构和算法快速判断出key 在库中是否存在,如果不存在直接return。这么做如果库中的数据量特别大的时候,其实成本是相对高的, 虽然布隆过滤器的内存占用相较HashMap要低很多,但一般情况下,我们并不太需要这种级别的防御。

传统的布隆过滤器并不支持删除操作。但是名为 Counting Bloom filter 的变种可以用来测试元素计数个数是否绝对小于某个阈值,它支持元素删除。 可以参考文章 Counting Bloom Filter 的原理和实现

这个世界上其实没有绝对安全的系统,黑客的玩法五花八门,你在软件系统上防住了,人家还可以玩社会工程学。 这块就算做了这么多其实就是提升黑客的攻击成本,假设黑客要花100W RMB的资源攻击才能给你攻破,但只能赚10W RMB, 冒着入狱的风险亏90W,是个正常人都不会去做的。只要攻击者获取的收益远低于攻击成本,你的系统就是相对安全的。

缓存击穿

缓存击穿指的是一个非常热点的Key失效了,巨大的并发量一下都打到了数据库,这种情况其实不是很容易给数据库打挂, 但会把数据库的连接占满,造成服务假死,等缓存的数据进了Redis,就会恢复。但这样造成的用户体验也是极差的。

这种情况的解决方案也是很简单的,就是热点数据永不过期,然后通过分布式锁的方式查库更新数据。 下面是一段伪代码。


public String getMessage(String key){
    // 在Redis里查
    String result = RedisUtil.getData(key);
    // 如果没查到
    if(StringUtils.isBlank(result)){
        // 获取分布式锁
        RLock lock = RedissionUtil.getLock(key);
        try{
            // 加锁
            if(lock.tryLock()){
                // 到数据库中查
                result = messageMapper.selectByKey(key);
                if(StringUtils.isNotBlank(result)){
                    // 存数据到Redis
                    RedisUtil.setData(result);
                }
            }else{
                // 睡一下,递归获取
                Thread.sleep(300);
                result = this.getMessage(key);
            }
        }catch(Exception ex){
            log.error(ex.getMessage(),ex);
        }finally{
            // 解锁
            lock.unlock();
        }
    }
    // 返回内容
    return result;
}

缓存雪崩后如何恢复

上面的问题都不是很难,也都有很明确的解决方案,但是这个问题有所不同,雪崩如果提前准备比较充分的情况下, 基本是不可能发生的,但如果真的发生了,数据库已经被冲垮的情况下要如何让服务恢复,如何不让用户的大并发给数据库一次又一次的打死, 都是比较有意思的问题。

虽然我没有真正遇到过缓存雪崩的情况(生产中,这个级别的生产事故真的很难碰到),但是基本是执行这几个步骤。

第一步,修改入口网关的限流策略,给个错误页面反馈给用户(比如系统繁忙,请稍后再试什么的),虽然体验很差,但当务之急是把系统恢复过来。

第二步,把挂掉的数据库启动起来,通过脚本或者什么方式,把热点数据写到缓存中,即缓存快速预热 (缓存都雪崩了,说明预防不到位,那写热点数据的脚本或工具是否存在,这里画个问号)

接着就分两种情况,缓存已预热的情况,和缓存未预热的情况。如果是已预热的情况, 那么限流策略可以放开的稍微激进一点,并且时刻关注数据库的各项指标,让用户进来刷缓存,如果这波抗住了,基本系统就恢复正常了, 趁着这段时间赶紧亡羊补牢,找到雪崩的原因,补救好后紧急上线,这次危机就算彻底渡过了。

如果未预热的情况,那就只能用保守一点的限流策略,一点点的通过用户访问的方式把数据写到缓存。 阶梯式的放开限流后,找原因,fix bug,系统最终也会恢复正常,但是系统不可用的时间相对第一种情况要多一些。

总之,缓存雪崩重在预防,在已经雪崩的情况下,补救的方案基本都大同小异,用户体验必然是不好的。

总结

缓存雪崩指的是缓存同时大面积失效的情况,通过过期时间加随机数的方式解决。

缓存穿透指的是缓存和数据库中都没有数据,而用户/黑客不断发起查询请求去查询不存在数据,导致数据库压力过大,最终把数据库打卦的情况。 通过服务器自身防御,写null,布隆过滤器的方式解决。

缓存击穿指的是一个非常热点的Key失效了,巨大的并发量一下都打到了数据库的情况。 通过加分布式锁的方式解决。

缓存雪崩后恢复,即限流,缓存预热,阶梯式放开限流,修复bug,紧急上线,系统恢复。

Reference

缓存雪崩、击穿、穿透
Counting Bloom Filter 的原理和实现
详解布隆过滤器的原理、使用场景和注意事项
缓存雪崩问题及处理方案

转载

本文遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。