缓存雪崩是大量key同时过期导致请求击穿缓存直压数据库,应加随机过期时间、用SetNX防并发回源、预热热key;穿透是查非法/不存在key,需空值缓存、布隆过滤器及参数校验。

Redis缓存雪崩:大量 key 同时过期怎么办
雪崩不是“Redis崩了”,是业务层在高并发下集体击穿缓存,把压力全甩给数据库。典型现象:DB CPU 突然拉满、Redis QPS 断崖下跌、慢查询日志里一堆 SELECT ... WHERE id = ?。
核心原因就一个:你设的 Expire 时间太整齐,比如所有商品详情都设了 30 * time.Minute,凌晨两点批量过期,三点流量高峰一来,全去查库。
- 给过期时间加随机扰动,比如
baseTTL + rand.Int63n(120)(加最多 2 分钟浮动) - 不依赖单一过期机制,对关键数据用
SetNX + 过期时间实现逻辑锁,避免重复回源 - 提前预热:服务启动或低峰期主动
GET并SET热 key,避开首请求高峰
注意:redis.Client.Set 的 expiration 参数必须是 time.Duration,传 int64 秒会直接 panic,别图省事写 30*60。
缓存穿透:查不到的 key 频繁打穿缓存
用户拿 id = -1、id = "abc" 或爆破式扫 /user/1 到 /user/9999999,Redis 里没这个 key,每次都要查 DB,还查不到——这就是穿透。
立即学习“go语言免费学习笔记(深入)”;
它和雪崩的区别在于:雪崩是“有 key 但过期了”,穿透是“压根不该存在的 key”。
- 对空结果也缓存,但设较短 TTL(如
2 * time.Second),避免脏数据长期滞留 - 使用布隆过滤器(
gobitset或redisbloom),在GET前先查BF.EXISTS;注意布隆有误判率,不能替代业务校验 - 接口层做基础参数校验:
if id ^\d+$).MatchString(idStr),拦住明显非法值
别用 nil 当空值塞进 Redis —— redis.Nil 是错误类型,client.Get(ctx, key).Result() 返回 "" 或 0 才是业务空值。
Go 里用 redigo / go-redis 怎么防并发回源
多个 goroutine 同时发现 key 缓存失效,一起查 DB 再写缓存,造成 DB 压力倍增,还可能写入不一致数据。
根本不是 Redis 客户端的问题,是业务没控制好竞态。
- 用
SET key value EX 30 NX做简易分布式锁:只有第一个成功 set 的 goroutine 去查 DB,其余等待几毫秒后重试GET - 更稳妥用
redis.Client.SetNX(go-redis)或conn.Do("SET", key, val, "EX", 30, "NX")(redigo) - 别在锁里做耗时操作,DB 查询超时必须设
context.WithTimeout,否则锁一直占着
示例片段(go-redis):
if ok, _ := rdb.SetNX(ctx, lockKey, "1", 5*time.Second).Result(); ok {
// 查 DB,写缓存,最后删 lockKey
} else {
time.Sleep(10 * time.Millisecond)
// 重试 GET
}为什么本地缓存(bigcache / freecache)不能替代 Redis 防穿透
本地缓存快,但它是 per-process 的。10 个 Go 实例,每个都得各自处理穿透——攻击者只要轮着打不同实例,照样压垮 DB。
- 本地缓存只适合读多写少、数据变更不敏感的场景,比如配置项、地区字典
- 穿透防护必须靠共享存储层(Redis)或前置网关(如 Nginx + lua_shared_dict)
- 如果真要用本地缓存兜底,至少配合
singleflight.Group拦同一时刻的重复请求,但仅限本进程内
singleflight.Do 的 key 必须包含业务上下文,比如 "user:" + userID,别图省事全用固定字符串,否则不同用户请求互相阻塞。
缓存设计最麻烦的从来不是“怎么存”,而是“什么时候不该存”和“谁来决定该不该查库”。这两个问题没想清楚,加再多层 cache 都是给故障埋更深的坑。










