Redis 缓存穿透解决

Redis 缓存穿透解决

缓存穿透是指查询不存在的数据,请求都打到数据库,可能被攻击。

1. 什么是缓存穿透

  • 缓存里没有这个数据
  • 数据库里也没有
  • 每次请求都查数据库
  • 有人恶意攻击数据库就挂了

2. 和其他问题的区别

  • 穿透: 查不存在的数据
  • 击穿: 热点key过期
  • 雪崩: 大量key同时过期

3. 解决方案

方案一:缓存空值

function getUser($id) {
    global $redis, $db;

    $key = "user:".$id;
    $user = $redis->get($key);

    if ($user !== false) {
        return json_decode($user, true);
    }

    // 查数据库
    $user = $db->query("SELECT * FROM users WHERE id=$id");

    if ($user) {
        // 存正常数据
        $redis->setex($key, 3600, json_encode($user));
    } else {
        // 存空值,短时间过期
        $redis->setex($key, 60, "null");
    }

    return $user;
}

方案二:布隆过滤器

  • 所有存在的数据存在布隆过滤器里
  • 查询前先过布隆过滤器
  • 不存在直接返回,不查数据库

方案三:接口校验

  • 前端做参数校验
  • 比如id必须大于0
  • 不合法的直接拒绝

4. 代码示例

// 简单防穿透
if ($id <= 0) {
    return null;
}

// 先查缓存
$user = $redis->get("user:".$id);

if ($user === "null") {
    return null;
}

if ($user) {
    return json_decode($user, true);
}

// 查数据库
$user = $db->query("SELECT * FROM users WHERE id=$id");

// 存缓存
if ($user) {
    $redis->setex("user:".$id, 3600, json_encode($user));
} else {
    $redis->setex("user:".$id, 60, "null");
}

return $user;

5. 最佳实践

  • 空值缓存,短时间过期
  • 重要接口加布隆过滤器
  • 前端做参数校验
  • 数据库做限流保护

总结

缓存穿透解决要点:

  • 查不到也存空值
  • 空值过期时间短
  • 用布隆过滤器
  • 做好接口校验

高并发系统,缓存穿透一定要防。


emer 发布于  2026-10-8 07:23 

Redis 缓存策略:穿透、击穿、雪崩的完整解决方案

前言

Redis 缓存是高并发系统的标配,但用不好反而会出大问题。缓存穿透、击穿、雪崩这三个经典问题,几乎每个后端开发都踩过坑。本文用实战的方式,带你彻底搞懂这三个问题的原理和解决方案。

一、缓存穿透

什么是穿透

查询一个数据库里根本不存在的数据,缓存里也没有,每次请求都打到数据库上。这就是缓存穿透。

比如有人恶意请求 id = -1 的用户数据,数据库里永远查不到,缓存里也永远没有,每次请求都要查一次数据库。

解决方案

1. 缓存空值

function getUserById($id) {
    $user = $redis->get("user:{$id}");

    if ($user === false) {
        // 数据库查询
        $user = $db->query("SELECT * FROM users WHERE id = ?", [$id])->fetch();

        if ($user) {
            $redis->setex("user:{$id}", 3600, json_encode($user));
        } else {
            // 空值也缓存,短时间过期
            $redis->setex("user:{$id}", 60, 'NULL');
        }
    }

    return $user === 'NULL' ? null : $user;
}

优点:简单直接
缺点:如果恶意请求的 id 非常多,会占用大量缓存空间

2. 布隆过滤器

在缓存前面加一层布隆过滤器,先判断这个 id 是否存在,不存在直接返回。

// 初始化布隆过滤器,把所有存在的 id 放进去
$bloomFilter = new RedisBloomFilter($redis);
$bloomFilter->add($allUserIds);

function getUserById($id) {
    // 先查布隆过滤器,不存在直接返回
    if (!$bloomFilter->exists($id)) {
        return null;
    }

    // 再走缓存查询...
}

优点:占用空间小,效率高
缺点:有一定误判率,实现复杂

二、缓存击穿

什么是击穿

某个热点 key 突然过期了,这一瞬间大量请求同时打到数据库上,就像被击穿了一样。

和穿透的区别:穿透是数据不存在,击穿是数据存在但缓存过期了。

解决方案

1. 互斥锁

function getHotProduct($id) {
    $product = $redis->get("product:{$id}");

    if ($product === false) {
        // 尝试获取锁
        $lockKey = "lock:product:{$id}";
        $isLocked = $redis->set($lockKey, 1, ['NX', 'EX' => 10]);

        if ($isLocked) {
            // 拿到锁,查数据库
            $product = $db->query("SELECT * FROM products WHERE id = ?", [$id])->fetch();
            $redis->setex("product:{$id}", 3600, json_encode($product));
            $redis->del($lockKey);
        } else {
            // 没拿到锁,等一会再查缓存
            usleep(10000);
            return getHotProduct($id);
        }
    }

    return $product;
}

优点:保证只有一个请求查数据库
缺点:实现复杂,有死锁风险

2. 热点数据永不过期

对真正的热点数据,直接设置永不过期,由后台异步更新。

// 热点数据不设过期时间,后台定时更新
$redis->set("hot_product:{$id}", json_encode($product));

三、缓存雪崩

什么是雪崩

大量缓存 key 在同一时间集体过期,或者 Redis 整个宕机了,所有请求瞬间全部打到数据库上,数据库直接被压垮。

和击穿的区别:击穿是一个热点 key,雪崩是大量 key 同时失效。

解决方案

1. 过期时间加随机值

// 不要所有 key 都设成 3600 秒过期
$ttl = 3600 + rand(0, 600);  // 在 1 小时基础上加 0-10 分钟随机值
$redis->setex("product:{$id}", $ttl, json_encode($product));

这样就不会出现大量 key 同一时间过期的情况。

2. Redis 集群高可用

部署 Redis 主从 + 哨兵,或者 Redis Cluster,确保 Redis 本身不会单点故障。

3. 服务降级限流

当数据库压力过大时,主动限流,返回兜底数据。

// 简单的降级逻辑
if ($redis->ping() === false) {
    // Redis 挂了,直接返回默认数据
    return getDefaultProduct();
}

四、三种问题对比

问题 原因 特点 解决方案
穿透 数据不存在 查不到的数据一直被请求 缓存空值 / 布隆过滤器
击穿 热点 key 过期 单个热点 key 瞬间失效 互斥锁 / 永不过期
雪崩 大量 key 同时过期 大面积缓存失效 随机过期时间 / 集群 / 限流

总结

缓存三件套的核心思路:

  1. 穿透:不让不存在的数据反复打数据库
  2. 击穿:热点数据过期时只让一个请求去查数据库
  3. 雪崩:让缓存失效时间错开,同时保证 Redis 高可用

搞懂这三个问题,你的缓存方案才算真正入门了。


emer 发布于  2026-10-4 09:58 
'); })();