10 Redis雪崩,穿透,击穿三连问
能够生存下来的物种,并不是那些最强壮的,也不是那些最聪明的,而是那些对变化作出快速反应的。
——达尔文
引言
关于Redis雪崩,穿透,击穿的问题,第一次接触名字有点陌生,听上去还比较相似,难以理解,过去做的很多项目中也都是用过Redis,但是第一次听到这几个关于Redis的坑还是一脸懵逼,直到这些坑真正显灵的时候才彻底意识到要搞明白。
第一次线上遇到 Redis 雪崩的时候我是维护某电影线上平台(某首富家的)国内某电影平台,因为系统架构老旧,每年大年初一贺岁档电影上映,购票人达到高峰,系统就会发生因缓存失效问题导致的出票故障,当时底层是用的oracle小型机,依旧扛不住。大年初一,所有同事打开电脑被迫营业。关于缓存失效引起的类似问题,坚决要扼杀,对用户友好,对程序员友好,对老板也友好。
1.面试官:关于Redis雪崩,穿透,击穿你是怎么理解的?
Redis 雪崩:
雪崩就是指缓存中大批量热点数据过期后系统涌入大量查询请求,因为大部分数据在Redis层已经失效,请求渗透到数据库层,大批量请求犹如洪水一般涌入,引起数据库压力造成查询堵塞甚至宕机。
解决办法:
- 将缓存失效时间分散开,比如每个key的过期时间是随机,防止同一时间大量数据过期现象发生,这样不会出现同一时间全部请求都落在数据库层,如果缓存数据库是分布式部署,将热点数据均匀分布在不同Redis和数据库中,有效分担压力,别一个人扛。
- 简单粗暴,让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 雪崩,穿透问题,只要发生一定是一个大麻烦,作为一个有经验的高级开发人员,一定要掌握,这种任务的开发工作往往交给项目组里资深的开发人员,如果经验不足的你掌握了这些坑点,一定会助你爬得更快。