Redis缓存策略与穿透防护实战:布隆过滤器加互斥重建

16 ℃ 0 条
内容摘要Redis 与数据库是两套独立存储,读写顺序或过期策略处理不当就会导致数据不一致甚至击穿数据库。本文从旁路缓存讲起,系统给出缓存穿透、缓存击穿与缓存雪崩三类风险的防护与落地写法,并附上布隆过滤器与互斥重建的可运行示例代码。
Redis缓存策略与穿透防护实战:布隆过滤器加互斥重建

Redis 把热点数据放进内存,能显著降低数据库压力,但它与数据库是两套独立存储,读写顺序或过期策略不当,就会出现数据不一致甚至击穿数据库。本文讲透穿透、击穿、雪崩三类风险的防护方案。

一、缓存读写策略与失效顺序

1. 旁路缓存是主流做法

旁路缓存(Cache Aside)由业务代码同时操作缓存与数据库:读时先查 Redis,未命中再查库并回写;写时先更新数据库,再删除缓存。选择删除而非更新,是并发下两个事务的更新顺序可能与写库顺序相反,反而写出更旧的值。

2. 先更新库还是先删缓存

若先删缓存再更库,两步之间若有读请求进来,会把旧值重新加载回缓存,造成长期不一致;先更库再删缓存,只在删缓存前出现短暂旧值,窗口更小。因此默认“先更库、后删缓存”。

二、穿透、击穿与雪崩的防护

1. 缓存穿透与布隆过滤器

穿透指查询的键在缓存和数据库都不存在,请求每次都落到库上。防护一是在入口用布隆过滤器拦截,它用位数组加多个哈希函数判断元素“可能存在”或“一定不存在”,判定不存在的键直接返回;二是给不存在的键写入短 TTL 的空值占位。

// 布隆过滤器判定不存在的键直接返回,不查库
if (!$redis->getBit('user:bf:' . ($uid % 8), $uid)) {
    return null;
}
$key  = 'user:' . $uid;
$data = $redis->get($key);
if ($data === false) {
    $data = $db->query('SELECT ... FROM users WHERE id = ?', [$uid]);
    $redis->setex($key, $data ? 600 : 60, json_encode($data));  // 空结果写短 TTL
}

2. 缓存击穿与互斥重建

击穿指某个热点键过期的瞬间,大量并发请求同时未命中,一起涌向数据库重建缓存。常用解法是加互斥锁,只放一个请求查库重建,其余等待后重试读缓存。

$key  = 'hot:goods:' . $gid;
$data = $redis->get($key);
if ($data === false) {
    // SET NX EX 抢锁,只让一个请求重建缓存
    if ($redis->set('lock:' . $key, 1, ['nx', 'ex' => 5])) {
        try {
            $data = $db->query('SELECT ... FROM goods WHERE id = ?', [$gid]);
            $redis->setex($key, 300 + rand(0, 120), json_encode($data));
        } finally {
            $redis->del('lock:' . $key);
        }
    } else {
        usleep(50000);   // 等待后重试读缓存
        $data = $redis->get($key);
    }
}

3. 缓存雪崩与过期打散

雪崩指大批键在同一时刻集中过期,或 Redis 实例宕机,流量瞬间全压到数据库。防护要点有三:给过期时间加随机抖动;核心数据做本地加 Redis 的多级缓存;Redis 用集群保证高可用。

三、落地实践与监控

1. 空值占位与热点预热

空值占位写入后,读取时要统一判断,不能把占位当有效数据返回。大促前对已知热点键做预热,避免洪峰时集中回源重建。日常还要盯住命中率与内存水位:命中率持续下滑通常意味着批量键过期,内存逼近 maxmemory 后会触发淘汰,需确认淘汰策略与预期一致。

关键配置提示

  1. 空值占位 TTL 取 30 到 60 秒,避免长期占内存。
  2. 互斥锁必须同时设置过期时间与 finally 释放,防止异常时死锁。
  3. 统一给过期时间加随机抖动,是成本最低的雪崩防护手段。

发表评论 (已有0条评论)

快来评论,快来抢沙发吧~