Redis缓存雪崩
什么是缓存雪崩?
雪崩,雪一样的大面积崩。一些缓存设定用的是定时更新,这样的数据缓存是同时刷新,同时失效。比如失效时刚好跨零点跨天,商品开始抢购,大量请求进来,Redis缓存失效,只能全部打到DB去了,如果是小量请求,没慢日志,分库分表的数据库倒还扛得住,这时如果大量的话,1秒几千个请求进来,数据库必定挂了,再重启,再挂,这就是缓存雪崩
雪崩解决方法?
- 在批量设置Redis缓存时候,把每个Key的失效时间加上个随机值,time() + rand(1, 1000),保证不会都在同一时间失效
- 热点数据直接设置为不过期,数据有更新时直接刷新下缓存
- 用Redis集群部署,把数据分布在不同的Redis分片中,也可避免全部失效直打DB问题
Redis缓存穿透
什么是缓存穿透?
穿透,穿透过已有的缓存层,直接到DB。访问的数据在Redis缓存中没有,就去DB,DB也没有,没有设值到缓存,相当于每个请求都是透明的直接请求DB。比如请求id为负数,而用户不断发起请求,如果是黑客攻击,数据库压力就会很大,严重时数据库就会跨掉
穿透解决方法?
-
请求参数做基准校验,只要是请求的就要验证过滤,不信任任何方的请求参数,不符合就return
-
在接入层,通过nginx用limit_req_zone和limit_req_conn限制单个ip的每秒访问速率和并发数
-
读取DB中没有,也要设置key缓存为空值(Null),再加上一个短时的过期时间,比如30秒,让其可自动剔除。加短时间超时,一方面是为了防止请求大量不存在的key,内存会占用越来越大,消耗内存;一方面是为了防止正常情况下空值导致无法使用
-
用布隆过滤器(BloomFilter),简单介绍就是把有的key都放到一个大的集合里,它是一个二进制向量,存放的不是0就是1,当key检测不存在集合中,则是非法参数,直接return
(1) 加入key到集合时,需要把key通过多个不同的算法计算对应位数,比如 JSHash算法,算出对应位数X,则设X为1
(2) 检测是否在集合中,则通过算法获取对应的位数下值是否都为1 ,只要有一个为 0 则表示不存在集合中。所以,缺点就是集合越大,有可能出现误判,存在判为不存在
(3) 例子:
比如新建一个长度为8的布隆过滤器,一开始集合为空的时候,默认值都是00 0 0 0 0 0 0 0 加入一个key,通过Hash1算法 Hash1(key)= 2,Hash2(key) = 4,Hash3(key) = 7,把下标2,下标4,下标7标 注为1
0 0 1 0 1 0 0 1 验证时,则把key通过三个算法获取对应获取下标2,4,7,判断是否都为1,则key存在集合中
(4) 主要直观代码:
$keyBloom = "bloom:filter"; //集合名,总的key //加入布隆集合 function add($item) { $hash1Pos = Hash1($item); $hash2Pos = Hash2($item); $hash3Pos = Hash3($item); //用管道,合并一次IO $pipe = $redis->pipeline(); $pipe->setbit($keyBloom, $hash1Pos, 1); $pipe->setbit($keyBloom, $hash2Pos, 1); $pipe->setbit($keyBloom, $hash3Pos, 1); $pipe->execute(); } //判断是否存在集合中 //return true | false public function has($item) { $hash1Pos = Hash1($item); $hash2Pos = Hash2($item); $hash3Pos = Hash3($item); //用管道,合并一次IO $pipe = $redis->pipeline(); $pipe->getbit($keyBloom, $hash1Pos); $pipe->getbit($keyBloom, $hash2Pos); $pipe->getbit($keyBloom, $hash3Pos); $result = $pipe->execute(); return !in_array(0, $result); }
什么是Redis缓存击穿?
击穿则是跟缓存雪崩有点类似,只不过雪崩是大面积,击穿是单个key非常热点,一直扛着高并发请求,当key在失效瞬间,持续的高并发请求直打DB,DB压力急剧上升
击穿解决方法?
-
既然是这么高热点数据,设置为不过期,有更新刷新下缓存
-
延时双重检测,首次获取缓存为空,先睡一小会,再获取缓存检测,还是为空再读DB设值到缓存,主要代码如下
$result = $redis->get("testKey"); //首次检测 if(!isset($result)){ //睡0.3秒,再检测 usleep(3000); $result = $redis->get("testKey"); if(!isset($result)){ //还是没有,读db,设值到缓存 $result = GetDataByDB("testKey"); $redis->set("testKey", $result); } } return $result;
-
互斥锁,获取缓存没有,先获取操作锁,取得锁之后,读取DB设值到缓存
互斥锁操作主要代码如下
$now = time(); //判断和设置时候要有原子性,用eval+lua实现,防止并发,脏读情况 //设置锁为10秒自动失效 $lua = <<<eof local key = KEYS[1] local val = KEYS[2] local lock = redis.call('get', key) if lock then if (tonumber(lock) > tonumber(val)) then return false end redis.call('set', key, val + 10) return true else redis.call('set', key, val + 10) return true end eof; $arr = ["lock", $now]; //获取操作锁 $result = $redis->eval($lua, $arr, count($arr)); if ($result) { // //查询数据库然后把值同步到缓存逻辑~~~~~~ // //移除锁 $redis->del("lock"); echo "读db完成"; } else{ echo "互斥中"; }
简单总结:
-
Qps高的,可用Redis集群,主从, 哨兵实现高可用,避免全盘崩溃
-
请求接入层限流,比如nginx的ip,连接数限制,超出阀值拉黑
-
采用AOF做热备,RDB做冷备,持久化保存,实现服务器稳健