阿里面试Redis最常问的三个问题:缓存雪崩、击穿、穿透(带答案)
一、缓存雪崩
缓存雪崩表示在某一时间段,缓存集中失效,导致请求全部走数据库,有可能搞垮数据库,使整个服务瘫痪。
使缓存集中失效的原因:
1、redis服务器挂掉了。
2、对缓存数据设置了相同的过期时间,导致某时间段内缓存集中失效。
如何解决缓存集中失效:
1、针对原因1,可以实现redis的高可用,Redis Cluster 或者 Redis Sentinel(哨兵) 等方案。
2、针对原因2,设置缓存过期时间时加上一个随机值,避免缓存在同一时间过期。
<?php $redis = new Redis(); $redis->connect('127.0.0.1', 6379, 60); $redis->auth(''); //设置过期时间加上一个随机值 $redis->set('article_content_1', '文章内容', 60 + mt_rand(1, 60)); $redis->set('article_content_2', '文章内容', 60 + mt_rand(1, 60));
3、使用双缓存策略,设置两个缓存,原始缓存和备用缓存,原始缓存失效时,访问备用缓存,备用缓存失效时间设置长点。
//原始缓存 $redis->set('article_content_2', '文章内容', 60); //设置备用缓存,失效时间设置长点 $redis->set('article_content_backup_2', '文章内容', 1800);
二、缓存穿透
缓存穿透表示查询一个一定不存在的数据,由于没有获取到缓存,所以没写入缓存,导致这个不存在的数据每次都需要去数据库查询,失去了缓存的意义。
请求的数据大量的没有获取到缓存,导致走数据库,有可能搞垮数据库,使整个服务瘫痪。
比如文章表,一般我们的主键ID都是无符号的自增类型,有些人想要搞垮你的数据库,每次请求都用负数ID,而ID为负数的记录在数据库根本就没有。
解决方案:
1、对于像ID为负数的非法请求直接过滤掉,采用布隆过滤器(Bloom Filter)。
2、针对在数据库中找不到记录的,我们仍然将该空数据存入缓存中,当然一般会设置一个较短的过期时间。
//设置文章ID为-10000的缓存为空 $id = -10000; $redis->set('article_content_' . $id, '', 60); var_dump($redis->get('article_content_' . $id));
三、缓存击穿
缓存击穿表示某个key的缓存非常热门,有很高的并发一直在访问,如果该缓存失效,那同时会走数据库,压垮数据库。
缓存击穿与缓存雪崩的区别是这里针对的是某一热门key缓存,而雪崩针对的是大量缓存的集中失效。
解决方案:
1、让该热门key的缓存永不过期。
2、使用互斥锁,通过redis的setnx实现互斥锁。
查询逻辑
如果缓存命中直接返回数据集
如果缓存没有,则尝试获取分布式锁(有超时设置)
如果没有拿到锁,则阻塞当前线程,n秒,之后再次尝试获取分布式锁(自旋,轮询,浪费CPU)
拿到锁之后检查数据是否已经被其他线程放到redis缓存中,如果redis缓存已有,直接返回redis中的数据,释放分布式锁
如果缓存没有被刷新,则查数据库
将数据库查询的结果保存到redis缓存中
返回查询结果
优点
1、数据的实时性较高,不需要其他外部系统依赖,利用了redis自己的特性,实现分布式锁,保证了同样的数据库查询同时只会查询1次,对数据库的压力较小
2、不会侵入业务代码,spring的aop就能很好的实现
不足
1、由于阻塞等待分布式锁是个自旋阻塞操作,所以其实对应用服务器来说非常浪费cpu的分片时间,如果这时候大量请求打过来, 应用服务器反而会先扛不住,因为这里会有大量的线程在自旋占用CPU,如果用户的查询是由多个系统的结果构成,每个系统的查询依赖上一个系统查询的结果,各个查询是串行的,那么自旋的睡眠时间可能会成为拖慢请求的罪魁祸首,多个系统都这么设计都在自旋睡眠,明显效率很低
适用情况
如果要求保证数据库的压力特别小,同样的请求只能查询一次数据库,
而且服务器较多,足以将多个请求分散到不同服务器,不至于造成太多线程自旋,那么可以使用这样的设计,但不推荐,因为这种自旋操作真的不是个好设计
<?php function getRedis() { $redis = new Redis(); $redis->connect('127.0.0.1', 6379, 60); return $redis; } //加锁 function lock($key, $random) { $redis = getRedis(); //设置锁的超时时间,避免释放锁失败,del()操作失败,产生死锁。 $ret = $redis->set($key, $random, ['nx', 'ex' => 3 * 60]); return $ret; } //解锁 function unLock($key, $random) { $redis = getRedis(); //这里的随机数作用是,防止更新缓存操作时间过长,超过了锁的有效时间,导致其他请求拿到了锁。 //但上一个请求更新缓存完毕后,如果不加判断直接删除锁,就会误删其他请求创建的锁。 if ($redis->get($key) == $random) { $redis->del($key); } } //从缓存中获取文章数据 function getArticleInCache($id) { $redis = getRedis(); $key = 'article_content_' . $id; $ret = $redis->get($key); if ($ret === false) { //生成锁的key $lockKey = $key . '_lock'; //生成随机数,用于设置锁的值,后面释放锁时会用到 $random = mt_rand(); //拿到互斥锁 if (lock($lockKey, $random)) { //这里是伪代码,表示从数据库中获取文章数据 $value = $db->getArticle($id); //更新缓存,过期时间可以根据情况自已调整 $redis->set($key, $value, 2 * 60); //释放锁 unLock($lockKey, $random); } else { //等待200毫秒,然后重新获取缓存值,让其他获取到锁的进程取得数据并设置缓存 usleep(200); getArticleInCache($id); } } else { return $ret; } }