PHP缓存雪崩怎么避免

wen PHP项目 23

本文目录导读:

PHP缓存雪崩怎么避免

  1. 设置不同的过期时间(最基础、最常用)
  2. 使用互斥锁(Mutex Lock)—— 针对高并发读的“热点数据”
  3. 热点数据永不过期 + 逻辑过期(异步更新)
  4. 二级缓存(本地缓存 + 分布式缓存)
  5. 缓存预热 + 错峰更新
  6. 不同场景下的选择

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 为例):

  1. 尝试获取锁(如 SETNX key_lock)。
  2. 获取成功:去数据库查询数据,重建缓存,释放锁。
  3. 获取失败:其他请求等待一小段时间(如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;
}

热点数据永不过期 + 逻辑过期(异步更新)

核心思想: 缓存数据不设置物理过期时间,但在数据中携带一个逻辑过期时间(或版本号),后台有一个定时任务或守护进程在数据“逻辑过期”前提前更新缓存。

适用场景: 更新频繁但有延迟容忍度的数据(如新闻列表、商品详情页)。

实现步骤:

  1. 缓存中存储: ['data' => ..., 'expire_time' => time() + 3600]
  2. 读取时: 检查逻辑过期时间。
    • 如果未过期:直接返回数据。
    • 如果已过期:立即返回旧数据(关键!),然后触发一个异步任务(如放到消息队列、或register_shutdown_function)去后台更新缓存。
  3. 消费者/进程: 从数据库获取最新数据,更新缓存中的 expire_time

优点: 永远不会发生缓存同时失效导致的雪崩,响应速度极快。 缺点: 数据会有短暂的不一致,且需要额外的后台进程支持。

二级缓存(本地缓存 + 分布式缓存)

核心思想: 在应用服务器本地(如内存、文件)和分布式缓存(如Redis)之间再加一层。

流程:

  1. 请求来临时(第一层): 查本地缓存(如 apcuarray)。
  2. 未命中(第二层): 查分布式缓存(Redis)。
  3. 仍未命中(第三层): 查数据库,并同时回填本地缓存和分布式缓存。

雪崩保护: 即使Redis缓存全部失效,应用服务器的本地内存中可能仍有部分数据(虽然只能扛住第一波流量),并且查询会退化为查数据库,但不再是一瞬间百万请求打向数据库。

缓存预热 + 错峰更新

核心思想: 避免在系统启动或数据大版本更新时,缓存中无数据或数据同时过期。

  • 预热: 在系统上线前,提前将热点数据加载到缓存中。
  • 错峰: 后台定时任务(如Cron)更新缓存时,不要使用统一的 0点整点 更新,不同模块使用不同的更新周期,比如A类数据每10分钟刷新一次,B类数据每15分钟刷新一次。

不同场景下的选择

场景 推荐方案 原因
普通数据、非热点 方案1:随机TTL 简单、无额外开销,能解决大部分问题。
热点数据、高并发读 方案2:互斥锁 防止大量请求重建缓存,但会小幅增加首次等待时间。
超高并发、对延迟敏感 方案3:逻辑过期 + 异步更新 读请求永远不阻塞,性能最优,需接受数据短暂不一致。
关键业务、容灾要求高 方案4:二级缓存 + 限流降级 架构最复杂,但能承受Redis故障级别的雪崩。

核心原则: 无论使用哪种方案,限流和降级都是最终防线,当缓存雪崩导致数据库确实扛不住时,应该直接返回“服务繁忙”或缓存中的旧数据,而不是让数据库挂掉。

抱歉,评论功能暂时关闭!