分布式 - Redis雪崩,穿透,击穿三连问

不啰嗦,我们直接开始!

引言

关于Redis雪崩,穿透,击穿的问题,第一次接触名字有点陌生,听上去还比较相似,难以理解,过去做的很多项目中也都是用过Redis,但是第一次听到这几个关于Redis的坑还是一脸懵逼,直到这些坑真正显灵的时候才彻底意识到要搞明白。

1、面试官:关于Redis雪崩,穿透,击穿你是怎么理解的?

Redis 雪崩:

雪崩就是指缓存中大批量热点数据过期后系统涌入大量查询请求,因为大部分数据在Redis层已经失效,请求渗透到数据库层,大批量请求犹如洪水一般涌入,引起数据库压力造成查询堵塞甚至宕机。

解决办法:

  1. 将缓存失效时间分散开,比如每个key的过期时间是随机,防止同一时间大量数据过期现象发生,这样不会出现同一时间全部请求都落在数据库层,如果缓存数据库是分布式部署,将热点数据均匀分布在不同Redis和数据库中,有效分担压力,别一个人扛。
  2. 简单粗暴,让Redis数据永不过期(如果业务准许,比如不用更新的名单类)。当然,如果业务数据准许的情况下可以,比如中奖名单用户,每期用户开奖后,名单不可能会变了,无需更新。

2、面试官:嗯呢,我听懂了。那击穿和穿透呢?两种场景有啥区别又如何解决。

Redis 穿透:

穿透是指绕过Reids,调用者发起的请求参数(key)在缓存和数据库中都不存在,通过不存在的key,成功穿透到系统底层,大规模不断发起不存在的key检索请求导致系统压力过大最后故障。

造成穿透的伪代码多为这样:

if(redis.get(key) == null){
    // redis数据不存在或已经过期 查询数据库
    Object value = dao.query(key);
    // 重新将value刷入缓存。
    redis.set(key);
} else {
    return reids.get(key);
}

解决办法:

  1. 分布式布隆过滤器:布隆是BloomFilter音译过来的,Redis 自身支持BloomFilter。
  2. 返回空值:遇到数据库和Redis都查询不到的值,在Redis里set一个null value,过期时间很短,目的在于同一个key再次请求时直接返回null,避免穿透。

伪代码:

try {
    Object value = dao.query(key); // 从数据库中查询数据
    if (value == null) {
        redis.set(key, null, 20);   // set null值,10ms过期时间
    } else {
        redis.set(key, value, 1000);
    }
} catch(Exception e) {
    redis.set(key, null, 20);
}

击穿:

击穿和穿透概念类似,一般是指一个key被穿透,这个key是热点key,同一个key会被有成千上万次请求,比如微博热点排行榜,key是小时时间戳,value是个list的榜单。每个小时产生一个key,这个key会有百万QPS,如果这个key失效了,就像保险丝熔断,百万QPS直接压垮数据库。

解决办法:

业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。类似下面的代码:

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;      
        }
}

3、面试官:这些东西你确实掌握了,非常棒,那你工作中有没有真实遇到过redis雪崩类的问题,能举个具体的例子吗?

问题分析:根据墨菲定律,这种事情迟早会发生,如果真的没遇到过,那是足够幸运,但是这种case,即便是没有亲自经历过,也要看一个别人的case变成自己的,应付面试官。没任何挑战怎么见彩虹呢?

答:公司 app 首页右上角有一个【未读消息】功能。(出于公司规定,我不能直接把公司的东西搬上来,用JD代替)

APP用户打开页面都要轮询一遍未读消息。比如对于京东 APP 首页,每个用户每次进入都会查询有没有未读消息,公司用户总数是数亿级别,但是实际上并不是真的每个用户都有未读消息,对于没有未读消息的用户,也需要查询未读消息,甚至需要查询DB,实际上是比较浪费的,当类似场景越来越多,DB负载越来越大,服务后台经常发出高压报警,项目初期没有考虑到这个问题,导致部分服务发出告警。

接口初期的设计是这样的,在Redis记录了有消息的用户userId,用户打开app,查询一次Redis,返回true代表有消息,然后查询真正的Redis消息列表或者DB等其它后续操作。如果返回,当用户量逐渐变大,内存或Redis占用的空间也越来越多,有几千万、甚至几亿用户,那么内存空间会增加很多。如果是存储URL之类的字符串,消耗的内存简直是不能忍受。某些场景对所有用户轮询DB也是不可取的。

基于以上考虑,最好采用一种新的方案过滤,因为之前看过业内其它公司采用BloomFilter来实现海量数据去重过滤,各方面性能表现优异,因此考虑基于BloomFilter来实现过滤无效user,这里我使用了布隆过滤器,对于没有未读消息的用户,免去后续操作。大大减少了无用功。

面试官:嗯,我懂你的意思了,实战经历非常强(时间有限,那布隆过滤器原理就不深挖了,会用就好。)

深入分析 

布隆过滤器原理

BloomFilter:即布隆过滤器。可以用于检索一个元素是否在一个集合中。

特点:

BloomFilter检索一个元素是否在一个集合中有一定的错误率(很低),但不会漏判。

  • 如果判断一个key不在集合中,那一定不在。
  • 如果判断一个key存在,那不一定真的在。

本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”

相比于传统的 List、Set、Map 等数据结构,它更高效、占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的。

实现原理

基本思想是利用一个足够好的Hash函数将一个字符串映射到二进制位数组中的某一位,这样不管字符串如何长,都只有一位,因此存储空间就极大的提升了。但是不管hash函数如何高效,总是会存在Hash冲突,尤其是数据量变大的时候,而BloomFilter是利用多个不同的Hash函数来解决“冲突”,即一次采用多个Hash函数把数据映射到不同的位上,降低了冲突概率。如下图所示:

x1和x2可能都会映射到同一位。

图片描述

如何根据输入元素个数n,确定位数组m的大小及Hash函数个数?已知文献证明,当Hash函数个数k=(ln2)*(m/n)时错误率最小。

BloomFilter有个缺点是不能删除数据,因为删除数据可能会影响到其它数据,有一些增强算法可以实现改功能,但代价比较大,不建议使用。

BloomFilter十分适合海量数据去重、过滤,尤其是当检测的字符串比较大时,极大地节省内存和存储空间,同时查询效率也十分高效。如果只是在内存使用,直接使用guava包的api即可;如果要做到分布式,结合Redis可以高效实现分布式的过滤效果。

 总结

Redis 雪崩,穿透问题,只要发生一定是一个大麻烦,作为一个有经验的高级开发人员,一定要掌握,这种任务的开发工作往往交给项目组里资深的开发人员,如果经验不足的你掌握了这些坑点,一定会助你爬得更快。

推荐阅读

美团技术博客:常见性能优化策略的总结

不啰嗦,文章结束,期待三连!

你可能感兴趣的