Redis缓存雪崩、击穿、穿透详解与解决方案

一、缓存雪崩

问题原理
大量缓存数据在同一时间过期或Redis集群宕机,导致所有请求直接穿透到数据库,造成数据库压力激增甚至崩溃。雪崩发生时,Redis中资源存在但已过期,数据库资源实际存在

PHP示例

// 危险做法:批量设置相同过期时间
$productIds = range(1, 10000);
foreach ($productIds as $id) {
    $key = "product_{$id}";
    $redis->setex($key, 3600, get_product_data($id)); // 全部1小时后同时过期
}

// 雪崩发生时的请求处理
function get_product(int $ id) {
    $key = "product_{$id}";
    if (!$data = $redis->get($key)) {
        // 所有请求同时进入这里查询数据库
        $data = query_database("SELECT * FROM products WHERE id = {$id}");
        $redis->setex($key, 3600, $data);
    }
    return $data;
}

解决方案

  1. 随机过期时间:
    $expire = 3600 + random_int(0, 600); // 基础1小时+随机10分钟
    $redis->setex($key, $expire, $data);
    
  2. 双层缓存策略
    // 主缓存
    $redis->setex($key, 3500, $data);
    // 备份缓存(永不过期)
    $redis->set("backup_{$key}", $data);
    
  3. 熔断降级机制
    use Swoole\Coroutine;
    if ($dbConcurrentRequests > 1000) { // 监控数据库压力
        Coroutine::sleep(0.1); // 延迟处理
        return cached_fallback_data(); // 返回降级数据
    }
    

二、缓存击穿

问题原理
热点Key突然失效时,大量并发请求同时穿透缓存访问数据库。击穿发生时,Redis中资源已过期,但数据库资源实际存在

PHP示例

// 热点商品缓存处理
function get_hot_product(int $id) {
    $key = "hot_product_{$id}";
    if (!$data = $redis->get($key)) { // 缓存失效瞬间
        // 瞬间大量并发请求到达此处
        $data = query_database("SELECT * FROM products WHERE id = {$id}");
        $redis->setex($key, 300, $data); // 重新设置缓存
    }
    return $data;
}

解决方案

  1. 分布式锁(互斥锁)
    $lockKey = "lock:{$key}";
    if (!$redis->exists($key)) {
        if ($redis->set($lockKey, 1, ['NX', 'EX' => 5])) { // 获取锁
            $data = get_from_db($id);
            $redis->setex($key, 300, $data);
            $redis->del($lockKey);
        } else {
            usleep(200000); // 等待200ms
            return $this->get_hot_product($id); // 重试
        }
    }
    
  2. 逻辑过期策略
    $data = [
        'expire' => time() + 300, // 逻辑过期时间
        'value' => $realData
    ];
    $redis->set($key, json_encode($data)); // 永不过期
    
    // 读取时检查逻辑过期
    $cached = json_decode($redis->get($key), true);
    if ($cached['expire'] < time()) {
        // 异步更新缓存
        start_async_update($key);
    }
    

三、缓存穿透

问题原理
恶意查询不存在的数据(如id=-1),绕过缓存直接访问数据库。穿透发生时,Redis和数据库中资源均不存在

PHP示例

// 未做防护的查询
function get_user(int $id) {
    $key = "user_{$id}";
    if (!$data = $redis->get($key)) {
        // 恶意请求反复查询不存在的ID
        $data = query_database("SELECT * FROM users WHERE id = {$id}");
        // 未缓存空结果,导致每次都会查库
    }
    return $data;
}

解决方案

  1. 布隆过滤器拦截
    // 初始化布隆过滤器
    $redis->executeRaw(['BF.RESERVE', 'user_filter', '0.001', '1000000']);
    
    // 查询前检查
    if (!$redis->executeRaw(['BF.EXISTS', 'user_filter', $id])) {
        return ['error' => 'Data not exists']; // 直接拦截
    }
    
    // 数据入库时添加标记
    $redis->executeRaw(['BF.ADD', 'user_filter', $newUserId]);
    
  2. 空值缓存策略
    if (!$data = $redis->get($key)) {
        $data = get_from_db($id);
        if (empty($data)) {
            $redis->setex($key, 300, 'NULL'); // 缓存空值
            return null;
        }
        $redis->setex($key, 3600, $data);
    }
    
  3. 请求参数校验
    // 拦截非法请求
    if ($id <= 0 || !is_numeric($id)) {
        http_response_code(400);
        exit('Invalid ID parameter');
    }
    

四、问题排查与诊断方法

当无法确定问题类型时,通过以下步骤排查:

  1. 监控指标分析

    // 获取Redis关键指标
    $info = $redis->info();
    $hitRate = $info['keyspace_hits'] / ($info['keyspace_hits'] + $info['keyspace_misses']);
    $expiredKeys = $info['expired_keys'];
    
    // 判断标准:
    if ($hitRate < 0.8) {
        if ($expiredKeys > 1000) return "可能雪崩";
        if (array_key_exists('malicious_ids', $_REQUEST)) return "可能穿透";
        return "可能击穿";
    }
    
  2. 日志追踪模式

    // 开启请求日志
    function cache_get($key) {
        $start = microtime(true);
        $data = $redis->get($key);
        
        if ($data === false) {
            // 记录缓存未命中
            log_message("CACHE_MISS:{$key}:" . (microtime(true)-$start));
            if (is_malicious_key($key)) {
                log_message("PENETRATION_SUSPECTED:{$key}");
            }
        }
        return $data;
    }
    
  3. 压力测试诊断

    # 模拟雪崩(批量过期)
    redis-cli keys "product_*" | xargs redis-cli expire 1
    
    # 模拟击穿(删除热点key)
    redis-cli del "hot_product_123"
    
    # 模拟穿透(请求不存在key)
    ab -n 10000 -c 100 "http://api.example.com/user/-1"
    

五、通用防御体系

  1. 多级缓存架构

    // L1: 本地缓存 (APCu)
    if ($data = apcu_fetch($key)) return $data;
    
    // L2: Redis集群
    if ($data = $redis->get($key)) {
        apcu_store($key, $data, 60); // 回写本地缓存
        return $data;
    }
    
    // L3: 数据库查询(带保护)
    $data = protected_db_query($key);
    
  2. 动态熔断机制

    class CircuitBreaker {
        private $failCount = 0;
        
        public function query($sql) {
            if ($this->failCount > 10) {
                throw new Exception("DB熔断开启");
            }
            
            try {
                $result = $db->query($sql);
                $this->failCount = max(0, $this->failCount-2);
                return $result;
            } catch (Exception $e) {
                $this->failCount++;
                throw $e;
            }
        }
    }
    
  3. 缓存预热策略

    // 定时任务预热热点数据
    $scheduler->addTask('0 3 * * *', function() {
        $hotProducts = get_hot_products_from_logs();
        foreach ($hotProducts as $id) {
            $key = "product_{$id}";
            $data = query_database(...);
            $redis->setex($key, 86400, $data); // 24小时缓存
        }
    });
    

总结对比表

问题类型触发条件数据库压力特征核心解决方案
雪崩批量缓存同时过期持续高压随机过期时间+双层缓存
击穿热点Key失效瞬时峰值分布式锁+逻辑过期
穿透查询不存在数据持续无效请求布隆过滤器+空值缓存

最佳实践建议

  1. 监控缓存命中率:$redis->info('stats')['keyspace_hits']

  2. 使用RedisBloom模块实现高效布隆过滤器

  3. 关键业务采用Redis Cluster+Sentinel高可用架构

  4. 结合Prometheus+Grafana建立监控看板

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

张杨不张扬

您的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值