缓存击穿必须用分布式锁或请求合并,本地锁在多实例下无效;单个热点key过期瞬间并发打库是原子性缺失问题;推荐SETNX轻量锁或singleflight.Group去重;需正确识别redis.Nil、避免锁误删、监控驱动热点识别。

缓存击穿必须用分布式锁或请求合并,本地锁在多实例下完全无效。单个热点 key(如 item:123)过期瞬间涌入几十上百并发请求,全部打到数据库,是典型的原子性缺失问题——不是 Redis 不够快,是业务没控制住竞态。
用 SETNX 实现轻量分布式锁
Go 客户端(如 github.com/go-redis/redis/v9)不提供开箱即用的“防击穿”能力,得自己在业务层封装。核心是:只让一个 goroutine 回源,其余等待结果。
-
SET item:123_lock "req-abc123" NX EX 5是最简可行方案:抢锁成功才查 DB,失败则短暂休眠后重试GET - 锁值建议带唯一标识(如 trace ID 或随机字符串),避免误删别人持有的锁
- 别用
DEL + SET手写锁——存在竞态窗口;也别用GETSET,它无法判断旧值是否属于自己 - 锁超时必须严格短于业务查询耗时,否则可能刚查完 DB,锁就过期被别人续上,导致重复写缓存
用 singleflight.Group 合并重复请求
它不是替代缓存,而是和缓存配合使用的“去重器”。适用于所有需要抑制并发加载的场景,比如查 DB、调外部 API。
- key 必须是
string,推荐用fmt.Sprintf("user:%d", id)而非直接传id(int 不稳定) - 真实加载逻辑(含 DB 查询 + 写缓存)必须全部包进
Do的func() (interface{}, error)里,不能在外面做任何副作用操作 - 全局复用一个
singleflight.Group实例即可,不用每次 new - 注意:它不解决“锁释放”问题,也不处理超时传播,
ctx需显式传入 DB 查询等子操作
避免常见误判:别把 redis.Nil 当普通错误
击穿发生时,rdb.Get(ctx, "item:123").Result() 返回的是 redis.Nil 错误,不是网络超时或连接断开。混为一谈会导致降级策略失效。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 必须用
errors.Is(err, redis.Nil)判断空值,不能用err == redis.Nil(它是变量) - 别在
if err != nil分支统一兜底——redis.Nil是预期行为,其他错误(如 context deadline)才该打日志或告警 - 如果用了
singleflight,Do内部也要对redis.Nil做同样判断,否则会把空结果也当有效值缓存
慎选“永不过期 + 异步刷新”方案
对极少数超高频、低变更的 key(如系统配置、城市列表),可设 ttl = 0,靠后台 goroutine 定期刷新。但这不是银弹。
- 必须配健康检查:异步刷新失败时,要 fallback 到带 TTL 的兜底缓存,否则整个服务依赖单点定时任务
- 刷新时机不能只看时间,还得结合 DB 的 binlog 或 CDC 事件,否则更新延迟不可控
- 别在 HTTP handler 里启动 goroutine 刷新——容易泄漏;应使用独立的 worker pool 管理
- Redis 集群节点时间不同步时,
EXPIRE行为可能不一致,而永不过期能绕过这个问题
真正难的不是选哪个方案,而是识别出哪些 key 算“热点”:不能靠拍脑袋,得从慢查询日志、Redis MONITOR 或 APM 工具里捞出 GET 高频且 MISS 率突增的 key。没有监控的数据决策,和蒙眼开车没区别。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










