基于PHP+Redis解决缓存击穿问题

10 / Jan / 2022 PM Note 122

PHP+Redis如何简单避免缓存击穿

基于PHP+Redis解决缓存击穿问题

解决方案:

/**
     * 防击穿缓存
     * @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;
}

代码解读:

  • 数据本体永不过期。
  • 设置锁的过期时间,锁过期则抢到锁的用户刷新数据。
  • 未抢到锁的用户如果有拿到数据就直接返回(此时数据已经是过期数据,如果对数据及时性要求高的需要自己改造代码)。
  • 未抢到锁的用户如果没有拿到数据则每秒判断抢到锁的用户是否执行完成。
  • 锁有极小概率变成死锁,最好有定时任务定期处理,比如每天业务低谷期清理死锁。

发布评论

© TIM All Rights Reserved sitemap

🌈Theme by TIM