本文目录导读:

在PHP项目中,缓存雪崩是一个非常典型的性能问题。缓存雪崩是指大量的缓存数据在同一时间段过期,导致所有请求直接打到数据库,从而压垮数据库。
实现防护的核心思想是避免集中过期和启用降级机制,以下是几种在PHP项目中可行的具体方案,按推荐程度排序:
设置不同的过期时间(防止集中失效)
这是最基础也最常用的方法,不要让所有缓存都在同一时刻过期。
-
原理: 在设置缓存的有效期时,加入一个随机值(通常是几秒到几分钟)。
-
PHP 示例(使用 Redis 或 Memcached):
<?php class CacheService { private $redis; private $baseTTL; // 基础过期时间,3600 秒 public function __construct($redis, $baseTTL = 3600) { $this->redis = $redis; $this->baseTTL = $baseTTL; } public function getWithRandomTLL($key, $callback) { $data = $this->redis->get($key); if ($data !== false) { return json_decode($data, true); } // 缓存不存在,执行回调获取数据 $data = $callback(); // 关键点:在基础时间上增加一个随机偏移(0-300 秒) $ttl = $this->baseTTL + rand(0, 300); $this->redis->setex($key, $ttl, json_encode($data)); return $data; } } // 使用示例 $cacheService = new CacheService($redis, 3600); $userList = $cacheService->getWithRandomTLL('user_list', function() { // 从数据库查询 return DB::table('users')->get()->toArray(); });
使用互斥锁(Mutex Lock)—— 单机防击穿
当缓存失效时,只允许一个请求去查询数据库并重建缓存,其他请求等待。
-
原理: 利用 Redis 的
set nx命令实现分布式锁。 -
注意: 这会降低并发时的响应速度,但保护了数据库。
<?php // 伪代码:基于 Redis 的互斥锁实现 function getDataWithMutex($key, $callback, $ttl = 3600) { $data = $redis->get($key); if ($data !== false) { return $data; } // 尝试获取锁(set nx ex) $lockKey = $key . ':lock'; $lockTTL = 10; // 锁的超时时间,防止死锁 if ($redis->setnx($lockKey, 1, ['ex' => $lockTTL, 'nx' => true])) { // 成功获取锁 try { // 再次检查缓存(双重检查),防止在获取锁的过程中被别人重建了 $data = $redis->get($key); if ($data !== false) { return $data; } // 从数据库加载数据 $data = $callback(); // 写入缓存 $redis->setex($key, $ttl, $data); return $data; } finally { // 释放锁(使用 Lua 脚本确保原子性删除) $redis->eval("if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end", [$lockKey, $lockValue], 1); } } else { // 没有获取到锁,说明另一个进程正在重建缓存 // 可以等待一小段时间后重试,或者直接返回一个“稍后再试”的降级结果 usleep(50000); // 等待 50 毫秒 return getDataWithMutex($key, $callback, $ttl); // 递归重试 } }
缓存二级策略(逻辑过期)
这是目前大中型项目中比较推荐的方案,它不设置物理过期时间,而是让缓存永远不过期,但在数据中存储一个逻辑过期时间。
-
原理: 优点是查询永远是直接命中缓存(除非被LRU淘汰),通过后台异步线程去更新缓存,避免雪崩。
-
PHP 实现思路:
<?php // 存储结构:['data' => ... , 'expire_at' => 时间戳] function getWithLogicExpire($key, $callback, $ttl = 3600) { $cache = $redis->get($key); if (!$cache) { // 缓存完全不存在(可能是第一次或者被淘汰了) // 需要同步重建(这里可以加互斥锁,或者直接重建) $data = $callback(); $cache = [ 'data' => $data, 'expire_at' => time() + $ttl ]; $redis->set($key, json_encode($cache)); // 注意:这里的 redis 不设 TTL return $data; } $cache = json_decode($cache, true); if (time() < $cache['expire_at']) { // 逻辑过期时间未到,直接返回 return $cache['data']; } // 逻辑过期时间已到,返回旧数据的同时,尝试异步更新 // PHP 实现异步更新常用手段: // 1. 使用消息队列(如 Redis Stream / RabbitMQ)投递一个更新任务 // 2. 使用 register_shutdown_function 在响应结束后发起请求 // 3. 简单的方案:加一个互斥锁,只让一个进程去更新 $lockKey = $key . ':renew_lock'; if ($redis->setnx($lockKey, 1, ['ex' => 10, 'nx' => true])) { // 当前进程负责更新缓存 try { $newData = $callback(); $redis->set($key, json_encode([ 'data' => $newData, 'expire_at' => time() + $ttl ])); } finally { $redis->del($lockKey); } } // 返回旧数据(即使过期了,也比让数据库死掉强) return $cache['data']; }
限流与降级(保底措施)
当缓存雪崩真的发生时(Redis 宕机),必须有保底策略。
-
降级:
- 如果查询缓存失败(如 Redis 连接超时),直接抛出一个友好的提示,或者返回静态的默认数据,而不是去查数据库。
- 可以设置一个“本地内存缓存”作为二级缓存(
apcu),当 Redis 不可用时,读取 APCu 中的过期数据。
<?php // 多级缓存示例 function getDataWithFallback($key, $callback) { // 1. 查 APCu (本地内存) $data = apcu_fetch($key); if ($data !== false) { return $data; } // 2. 查 Redis try { $data = $redis->get($key); if ($data !== false) { apcu_store($key, $data, 60); // 本地缓存 60 秒,作为 Redis 的备份 return $data; } } catch (Exception $e) { // Redis 挂了,记录日志,不要继续尝试 // 这里可以返回一个默认值或者 null } // 3. Redis 失效 + APCu 没有,降级:如果是非核心数据,直接返回空 // 如果是核心数据,尝试加锁查库(但风险较高) return $callback(); }
更彻底的方案:提前“预热”与“错峰”
- 预热: 项目上线前,写一个脚本将热点数据提前加载到缓存,而不是等用户请求来触发,这些缓存的有效期可以设成24小时 + 随机数。
- 错峰更新: 如果是定时任务刷新缓存(例如每天凌晨2点刷新1000个商品),可以在这个定时任务里,
sleep(rand(1, 300))秒再更新下一个,人工打散。
总结建议
- 首选方案 1(随机过期时间):代码改动最小,效果最明显,建议所有新项目都默认加上。
- 对于高并发热点数据:强烈推荐方案 3(逻辑过期),先用着老数据,后台慢慢更新。
- 对于关键业务(如支付、下单):必须加方案 4(降级),直接返回“服务器繁忙”也比让数据库雪崩要好。
- 尽量避免使用方案 2(互斥锁)作为唯一方案:因为它会降低吞吐量,更适合用于“缓存穿透”或辅助逻辑过期使用。
版权声明:足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播