什么是缓存击穿?
是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时查询一个key时,缓存没读到数据,因为缓存失效了,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。
如何解决?
- 对于频繁访问的热点key干脆不设置过期时间
- 互斥独占锁防止击穿
- 多级缓存,差异失效时间
phpdemo1
<?php /** * 防击穿缓存 * @param string $key 缓存key * @param int $expire 缓存过期时间(s) * @param bool $refresh 是否强制刷新数据 * @param callable $func 获取要缓存的数据的回调方法(仅支持返回类型:?string) * @return null|string 返回缓存的值,没有值时且没有抢到刷新权且缓存已过期时返回null */ function onceCache(string $key,int $expire,bool $refresh,callable $func): ?string { //内存,注:如果是cli模式不要使用 static $dataStatic=[]; //读取内存 if(isset(self::$dataStatic[$key])){ return self::$dataStatic[$key]; } //获取laravel的获取redis连接,其它框架需要修改 $redis=Redis::connection(); //原缓存key加后缀"lock"为锁的key $lockKey=$key.':lock'; //锁是否有效 $lock=false; //读取数据 $data=$redis->get($key); //如果 非强制刷新 且 缓存非空 ,获取锁 if(!$refresh && !is_null($data)){ $lock = $redis->get($lockKey); } if(!$lock){//如果锁过期或无数据 $lock=$redis->setnx($lockKey,1); //仅当锁不存在时设置锁,值1代表数据获取中 if($lock || $refresh){ //抢到新锁 或 强制刷新 try { $data=$func(); //从回调函数获取要缓存的数据 //有数据则写入缓存,没有则删除数据 if(!is_null($data)){ $redis->set($key,$data); }else{ $redis->del([$key]); } $redis->set($lockKey,2,'ex',$expire); //设置锁的过期时间,值2代表数据获取完成 }catch (Exception $exception){ $redis->del([$lockKey]); //发生异常,删除锁 } }else{ //如果没有数据,又没有抢到锁 //30秒内每秒判断抢到锁的用户是否执行完成,执行完成则从缓存得到数据并返回 $retry=0; do{ sleep(1); $retry++; $data = $redis->get($key); }while(is_null($data) && $redis->get($lockKey)==1 && $retry<30); } } //写入内存 if(!is_null($data)){ $dataStatic[$key]=$data; } //返回数据 return $data; }
代码解读:
- 数据本体永不过期
- 设置锁的过期时间,锁过期则抢到锁的用户刷新数据
- 未抢到锁的用户如果有拿到数据就直接返回(此时数据已经是过期数据,如果对数据及时性要求高的需要自己改造代码)
- 未抢到锁的用户如果没有拿到数据则每秒判断抢到锁的用户是否执行完成
- 锁有极小概率变成死锁,最好有定时任务定期处理,比如每天业务低谷期清理死锁
phpdemo2
//防止缓存击穿 function get_near_online_city($abbr = 'cn') { if (empty($abbr) || filter_abbr($abbr)) { return array(); } $ret = $this->redis->get('cm_near_cities:' . $abbr); if (!$ret) { $res = $this->redis->setnx('cm_near_cities:'.$abbr,'1'); if ($res) { $sql = "select cm_city_near.city2 from cm_city_near right join cm_cities on (cm_cities.abbr=cm_city_near.city2) where cm_city_near.manual=0 and cm_cities.online=1 and cm_city_near.city1='" . $abbr . "' ORDER BY cm_city_near.distance;"; $query = $this->db->query($sql); $ret = $query->result_array(); $ret = json_encode($ret); $this->redis->set('cm_near_cities:'.$abbr, $ret, 6400); $this->redis->set('cm_near_cities:b'.$abbr, $ret, 10000); } else { $ret = $this->redis->get('cm_near_cities:b'.$abbr); } } return json_decode($ret, true); }