缓存击穿是单个热点key过期瞬间大量并发请求穿透至DB,核心方案是用Redis原子命令(如SETNX+EX)确保仅一个请求回源重建缓存,并配合singleflight合并重复请求、锁值带唯一标识、空值规范序列化等细节防控。

缓存击穿不是 Redis 慢,是业务层没控制住单个热点 key 失效瞬间的并发回源——必须用原子性写入或请求合并,本地锁在多实例下完全无效。
用 SET + NX + EX 原子抢写防击穿
击穿只发生在单个 key(如 "item:10086")过期那一刻,多个请求同时发现缓存 miss,全去查 DB。核心不是“加锁”,而是让“写缓存”这一步本身具备排他性。
-
rdb.Set(ctx, key, val, redis.SetArgs{Mode: redis.SetNX, Expire: 10 * time.Second})必须一气呵成:值、过期、仅当不存在才设置,三者由 Redis 原子保证 - 绝对不要先
rdb.Get()再判断是否为空,中间存在竞态窗口,高并发下照样打穿 - 锁过期时间得比 DB 查询耗时长(建议 ≥5s),但别超 30s,否则故障时残留锁拖垮后续请求
- 锁值建议带唯一标识(如
"req-" + uuid.NewString()),避免误删别人持有的锁
用 singleflight.Group 合并重复请求
singleflight 不是万能锁,它只负责合并请求;真正起作用的是「查缓存 → Do 回源 → 成功后写缓存」这个完整闭环。漏掉任一环,等于白加。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
-
var userGroup singleflight.Group必须声明为包级变量或注入 service 结构体,不能在 handler 里new(singleflight.Group)——否则每个请求都是新实例,Do完全无效 - 所有副作用操作(
db.QueryRowContext(ctx, ...)、rdb.Set(...)、日志、指标)必须全部塞进Do的回调函数里 - key 要稳定幂等,比如
"user:" + strconv.FormatInt(id, 10),不能含时间戳或随机数 - 回调内必须用带
ctx的 API,并自行处理超时与错误传播;失败时不写缓存,避免脏数据
别踩这些坑:空值、错误类型、锁生命周期
击穿防护失效,往往不是策略错,而是细节失控。
立即学习“go语言免费学习笔记(深入)”;
-
rdb.Get(ctx, key).Result()返回redis.Nil是预期行为(查不到),不是网络错误;混为一谈会导致降级逻辑崩溃 - 空值缓存必须序列化为可识别的占位符(如
"null"或{"exists":false}),不能传 Go 的nil,否则反序列化失败或误判 - 用
SETNX抢锁后,DB 查询失败必须主动DEL锁,否则残留锁会阻塞后续所有请求 - 别在
Do外部做rdb.Set()—— 若Do内部出错,外部写入就变成无保护的竞态写入
最易被忽略的一点:击穿只针对单 key,和雪崩(批量 key 同时过期)、穿透(查根本不存在的 key)触发条件完全不同。把布隆过滤器强加给用户 ID 类动态 key,重建成本高、误判率升;而给所有 key 都套 singleflight,又会拖慢非热点路径。策略必须按 key 类型和访问模式隔离使用。

















