本文目录导读:

- 设置不同的过期时间(最基础、最常用)
- 使用互斥锁(Mutex Lock)—— 针对高并发读的“热点数据”
- 热点数据永不过期 + 逻辑过期(异步更新)
- 二级缓存(本地缓存 + 分布式缓存)
- 缓存预热 + 错峰更新
- 不同场景下的选择
PHP缓存雪崩是指大量缓存同时过期,导致所有请求瞬间穿透到数据库,造成数据库压力剧增甚至崩溃的现象。
要避免缓存雪崩,主要有以下几种策略,按推荐程度和适用场景排序:
设置不同的过期时间(最基础、最常用)
核心思想: 不让所有缓存key在同一时刻集体失效。
-
固定时间 + 随机值: 在基础过期时间上,为每个key添加一个随机的偏移量。
// 基础过期时间 3600 秒(1小时) $baseTtl = 3600; // 随机增加 0-600 秒(0-10分钟) $randomTtl = rand(0, 600); $cacheTtl = $baseTtl + $randomTtl; $cache->set($key, $data, $cacheTtl);
-
用业务时间作为缓存有效期: 如果数据要求整点更新,可以将过期时间设在整点后的随机秒数,而不是所有key都在整点过期。
使用互斥锁(Mutex Lock)—— 针对高并发读的“热点数据”
核心思想: 当缓存失效时,只允许一个请求去重建缓存,其他请求等待或获取旧数据。
适用场景: 某个key被大量请求同时访问(如首页、热门排行榜)。
实现步骤(以 Redis 为例):
- 尝试获取锁(如
SETNX key_lock)。 - 获取成功:去数据库查询数据,重建缓存,释放锁。
- 获取失败:其他请求等待一小段时间(如100ms)后,尝试再次从缓存读取(此时缓存可能已被重建),或者直接返回旧数据(如果实现了逻辑过期)。
function getDataWithLock($key) {
$cache = getRedis();
$data = $cache->get($key);
if ($data === false) {
// 尝试获取锁,锁的key与缓存key关联,过期时间短(如10秒)
$lockKey = $key . '_lock';
// setnx + expire 保证原子性,或用 set($lockKey, 1, ['nx', 'ex' => 10])
if ($cache->setnx($lockKey, 1)) {
$cache->expire($lockKey, 10);
// 模拟从数据库获取数据
$data = queryFromDatabase($key);
$cache->set($key, $data, 3600); // 重建缓存
$cache->del($lockKey); // 释放锁
} else {
// 获取锁失败,等待一小段时间后重试
usleep(100000); // 等待100毫秒
$data = $cache->get($key); // 通常此时缓存已经被重建
// 如果缓存依旧为空(极端情况),可以继续重试或返回默认值
}
}
return $data;
}
热点数据永不过期 + 逻辑过期(异步更新)
核心思想: 缓存数据不设置物理过期时间,但在数据中携带一个逻辑过期时间(或版本号),后台有一个定时任务或守护进程在数据“逻辑过期”前提前更新缓存。
适用场景: 更新频繁但有延迟容忍度的数据(如新闻列表、商品详情页)。
实现步骤:
- 缓存中存储:
['data' => ..., 'expire_time' => time() + 3600] - 读取时: 检查逻辑过期时间。
- 如果未过期:直接返回数据。
- 如果已过期:立即返回旧数据(关键!),然后触发一个异步任务(如放到消息队列、或
register_shutdown_function)去后台更新缓存。
- 消费者/进程: 从数据库获取最新数据,更新缓存中的
expire_time。
优点: 永远不会发生缓存同时失效导致的雪崩,响应速度极快。 缺点: 数据会有短暂的不一致,且需要额外的后台进程支持。
二级缓存(本地缓存 + 分布式缓存)
核心思想: 在应用服务器本地(如内存、文件)和分布式缓存(如Redis)之间再加一层。
流程:
- 请求来临时(第一层): 查本地缓存(如
apcu、array)。 - 未命中(第二层): 查分布式缓存(Redis)。
- 仍未命中(第三层): 查数据库,并同时回填本地缓存和分布式缓存。
雪崩保护: 即使Redis缓存全部失效,应用服务器的本地内存中可能仍有部分数据(虽然只能扛住第一波流量),并且查询会退化为查数据库,但不再是一瞬间百万请求打向数据库。
缓存预热 + 错峰更新
核心思想: 避免在系统启动或数据大版本更新时,缓存中无数据或数据同时过期。
- 预热: 在系统上线前,提前将热点数据加载到缓存中。
- 错峰: 后台定时任务(如Cron)更新缓存时,不要使用统一的
0点或整点更新,不同模块使用不同的更新周期,比如A类数据每10分钟刷新一次,B类数据每15分钟刷新一次。
不同场景下的选择
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 普通数据、非热点 | 方案1:随机TTL | 简单、无额外开销,能解决大部分问题。 |
| 热点数据、高并发读 | 方案2:互斥锁 | 防止大量请求重建缓存,但会小幅增加首次等待时间。 |
| 超高并发、对延迟敏感 | 方案3:逻辑过期 + 异步更新 | 读请求永远不阻塞,性能最优,需接受数据短暂不一致。 |
| 关键业务、容灾要求高 | 方案4:二级缓存 + 限流降级 | 架构最复杂,但能承受Redis故障级别的雪崩。 |
核心原则: 无论使用哪种方案,限流和降级都是最终防线,当缓存雪崩导致数据库确实扛不住时,应该直接返回“服务繁忙”或缓存中的旧数据,而不是让数据库挂掉。