Redis 缓存穿透、击穿与雪崩:策略与命令详解

缓存穿透、缓存击穿和缓存雪崩是缓存灾难的三驾马车。它们听起来相似,但成因和解决方案截然不同。本指南用具体的 Redis 命令拆解每个问题。

缓存穿透

问题描述

缓存穿透发生在请求查询的数据既不在缓存中也不在数据库中。每个请求都绕过缓存直接打到数据库。

例子:攻击者发送数百万个查询 user:99999999 的请求,而这个用户根本不存在。每个请求都缓存未命中,直接查库。

方案一:缓存空值

最简单的修复——用短 TTL 缓存"未找到"的结果:

SET cache:user:99999999 "NULL" EX 60
GET cache:user:99999999

应用逻辑:如果缓存值是 "NULL",直接返回 404,不查库。

方案二:布隆过滤器

布隆过滤器是一种概率数据结构,能告诉你"一定不在集合中"或"可能在集合中"。

在查询 Redis 或数据库之前,先检查布隆过滤器:

BF.ADD users:filter "user:1001"
BF.ADD users:filter "user:1002"
BF.EXISTS users:filter "user:99999999"
BF.EXISTS users:filter "user:1001"

注意:BF.* 命令需要 RedisBloom 模块。没有的话,可以用 SETBITGETBIT 实现简易布隆过滤器:

SETBIT bloom:users 42 1
SETBIT bloom:users 87 1
SETBIT bloom:users 156 1
GETBIT bloom:users 42
GETBIT bloom:users 99

坑点

空值的 TTL 不要设太长。如果数据后来被创建了,用户会一直看到过期的"未找到"响应,直到 TTL 过期。

缓存击穿

问题描述

缓存击穿发生在单个热点 Key 过期时,数百个并发请求同时打到数据库去重建缓存。数据库被"惊群效应"击垮。

方案一:互斥锁 SET NX

用分布式锁确保只有一个请求重建缓存:

SET lock:product:2001 "rebuilding" NX EX 10

应用逻辑:

  1. 尝试用 SET lock:key NX EX 10 获取锁。
  2. 如果获取成功(返回 OK),查库、重建缓存、释放锁。
  3. 如果获取失败(返回 nil),短暂等待后重试读缓存。
SET lock:product:2001 "rebuilding" NX EX 10
SET cache:product:2001 '{"name":"Widget","price":9.99}' EX 600
DEL lock:product:2001

方案二:逻辑过期

不依赖 Redis TTL,而是在值内部存储过期时间戳:

SET cache:product:2001 '{"data":{"name":"Widget"},"expire":1739700000}'
GET cache:product:2001

应用检查 expire 字段。如果过期,触发异步重建,同时继续返回旧数据。确保零停机。

方案三:热点 Key 永不过期

对于真正关键的热点 Key,不设 TTL。通过后台任务主动更新:

SET hotkey:homepage:banner '{"title":"大促!","img":"banner.jpg"}'

定时任务或消息消费者在源数据变化时更新这个 Key。

坑点

互斥锁方案在极端负载下可能导致请求排队。设置合理的锁 TTL(如 10 秒),并实现带退避的重试。

缓存雪崩

问题描述

缓存雪崩发生在大量 Key 同时过期,导致洪水般的请求同时打到数据库。

常见原因:启动时加载所有缓存数据,使用相同的 TTL。

SET cache:product:1 "data1" EX 3600
SET cache:product:2 "data2" EX 3600
SET cache:product:3 "data3" EX 3600

三个 Key 在完全相同的时刻过期。数据库被打爆。

方案一:TTL 加随机抖动

通过添加随机偏移量分散过期时间:

SET cache:product:1 "data1" EX 3600
SET cache:product:2 "data2" EX 3720
SET cache:product:3 "data3" EX 3540

应用代码中:TTL = 基础TTL + random(0, 300)

方案二:多级缓存

在 Redis(L2)前面加一层本地内存缓存(L1):

请求 → L1(进程内缓存)→ L2(Redis)→ 数据库

即使 Redis 缓存过期,L1 缓存也能吸收流量尖峰。L1 通常有更短的 TTL(如 30 秒)。

方案三:熔断器

当数据库错误率飙升时,触发熔断器,返回降级响应(旧缓存、默认值或错误页面),而不是放所有请求通过。

方案四:预热缓存

在已知的流量高峰前(如促销活动),预加载关键数据:

SET cache:product:1 '{"name":"Widget","price":4.99}' EX 7200
SET cache:product:2 '{"name":"Gadget","price":14.99}' EX 7320
SET cache:product:3 '{"name":"Doohickey","price":2.99}' EX 7140

坑点

仅靠随机抖动是不够的,如果基础 TTL 太短的话。如果所有 Key 的 TTL 在 50-60 秒之间,它们仍然会在一个很窄的时间窗口内过期。使用有意义的基础 TTL(如 1 小时),抖动范围 5-10 分钟。

快速对比

问题原因关键症状主要修复方案
穿透查询不存在的数据每个请求都打到数据库布隆过滤器 / 缓存空值
击穿热点 Key 过期单个 Key 的惊群效应互斥锁 / 逻辑过期
雪崩大量 Key 同时过期数据库负载飙升TTL 抖动 / 多级缓存

在线编辑器试一试

前往 Redis 在线编辑器 模拟这些场景:

SET cache:user:99999999 "NULL" EX 60
GET cache:user:99999999
TTL cache:user:99999999

SET lock:product:2001 "rebuilding" NX EX 10
SET lock:product:2001 "rebuilding" NX EX 10

SET cache:product:1 "data1" EX 3600
SET cache:product:2 "data2" EX 3720
SET cache:product:3 "data3" EX 3540
TTL cache:product:1
TTL cache:product:2
TTL cache:product:3

试试第二个 SET NX——它返回 nil,因为锁已经被持有了。这就是互斥锁模式的实际效果。