redis 本身不提供防击穿能力,需在 go 中用 singleflight.group 合并请求或 set+nx+ex 原子锁实现串行回源;空值必须序列化为"null"并设短 ttl,热点 key 的 ttl 需加随机扰动。

直接上结论:Redis 本身不提供“防击穿”能力,所谓“部署 Redis 防击穿”,本质是在 Go 组件里用 Redis 做分布式锁或请求合并的基础设施——不是配个 Redis 就自动防击穿,得写代码。
为什么不能只靠 Redis 客户端默认行为防击穿
go-redis/v9 的 Get、Set 都是单次原子操作,但“缓存未命中 → 查 DB → 写缓存”这个三步流程在并发下天然竞态。100 个请求同时发现 item:123 不存在,全都会去查 DB,这就是击穿。Redis 不会自动帮你串行化这 100 次回源。
- 本地
sync.Mutex在多实例部署下完全无效——每个进程锁自己的内存,互不感知 -
SET key val EX 300 NX只能抢锁,不能等结果;没抢到锁的请求必须自己决定是重试还是返回 - 如果用
DEL+SET手写锁,存在“删掉别人锁”的竞态窗口,极难兜住
用 singleflight.Group 合并重复请求(推荐首选)
它不依赖 Redis,纯内存级去重,轻量、无网络开销,适合大多数场景。关键是它和缓存逻辑解耦,只管“同一 key 的并发加载”这件事。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
-
key必须是稳定字符串,比如fmt.Sprintf("product:%d", id),别直接传id(int 类型哈希不稳定) - 真实加载逻辑(含 DB 查询 +
redisClient.Set)必须整个包进Do的闭包里,不能在外面先查 DB 再塞结果 - 全局复用一个
singleflight.Group实例,别每次 new,否则失去去重意义 - 注意:
Do返回的 error 是你闭包里抛的,不是 Redis 错误;空结果仍需按业务判断是否缓存
用 SETNX 实现轻量分布式锁(需 Redis 协作)
当 singleflight 无法覆盖(比如跨进程共享锁),就得靠 Redis。核心是让只有一个 goroutine 回源,其余等待锁释放后读缓存。
- 锁命令必须是原子的:
SET item:123_lock "req-abc123" NX EX 5,其中"req-abc123"是唯一 trace ID 或随机字符串,避免误删 - 锁超时时间(EX)必须明显短于 DB 查询耗时,否则可能刚写完缓存,锁就过期,被下一个请求续上,导致重复写
- 释放锁不能用
DEL,要用 Lua 脚本比对 value 再删,否则可能删错别人的锁 - 一定要用
errors.Is(err, redis.Nil)判断缓存 miss,别用err == redis.Nil——后者永远为 false
缓存空值和随机 TTL 是配套底线
击穿常和穿透、雪崩共存。光防击穿不够,漏掉空值或固定过期时间,照样压垮 DB。
- DB 查无结果时,必须调
redisClient.Set(ctx, key, "null", 2*time.Minute),值不能是 Go 的nil - 所有热点 key 的 TTL 要加扰动,比如
time.Duration(baseTTL + rand.Int63n(300)) * time.Second - 别在 HTTP handler 里同步执行预热或锁逻辑——启动时用独立
go func() { ... }(),或扔进 asynq 任务队列 - 连接池必须配:
MinIdleConns=5、MaxConnAge=30*time.Minute、PoolSize按 QPS 设(1k QPS 建议 20–30)
最易被忽略的点:singleflight 的 key 和缓存 key 要对齐,且空值判断必须在反序列化后立刻做——否则 “null” 字符串可能被当成有效数据返回给前端。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










