singleflight.group是go官方专为防缓存击穿设计的去重工具,核心是将n个并发请求合并为1次真实加载并共享结果;必须全局复用、key稳定可比、完整加载逻辑(含db查询与缓存写入)须封装于do回调内,且需区分redis.nil与真实错误。

不能靠“语言学习技巧”解决缓存击穿——这是并发控制问题,不是语法或表达问题。 缓存击穿本质是多个 goroutine 同时发现缓存失效后,一窝蜂查数据库。Go 语言本身不提供“防击穿语法糖”,必须用明确的同步机制或去重逻辑来约束行为。
singleflight.Group 是最直接有效的方案
它不是“技巧”,而是 Go 官方 sync 扩展包中专为这类场景设计的工具。核心价值是把 N 个并发请求合并成 1 次真实加载,其余等待共享结果。
-
singleflight.Group必须全局复用(比如定义为包级变量),否则每次 new 都会失去去重能力 -
Do的 key 必须稳定可比:推荐用fmt.Sprintf("user:%d", id),别直接传int或未标准化的结构体 -
Do内部的函数必须包含完整加载逻辑:查 DB + 写缓存,不能只查 DB 然后在外面写缓存——否则其他 goroutine 可能读到空值 - 返回的
error要区分redis.Nil(缓存 miss)和真实错误(如超时、连接断),前者应正常返回,后者才该打日志或告警
分布式锁(如 SETNX)只在 Redis 场景下有效
本地 sync.Mutex 在单机进程内有用,但微服务多实例部署时完全无效——每个实例都有自己的锁,根本拦不住跨节点的并发请求。
- 用
SET item:123_lock "req-abc123" NX EX 5实现轻量锁,NX和EX必须原子执行 - 锁值必须带唯一标识(如 traceID 或随机字符串),避免 A 拿到锁后超时释放,B 拿到并写缓存,A 又误删 B 的锁
- 锁过期时间必须明显短于 DB 查询耗时(比如查询最长 800ms,锁设为 2s),否则可能刚写完缓存,锁就过期,导致重复加载
- 绝对不要用
GET + DEL + SET三步手写锁——中间存在竞态窗口,高并发下必然出错
本地缓存 + 分布式缓存组合时,击穿防护要分层做
比如用 go-zero 或自建 LRU 做本地缓存,Redis 做分布式缓存。此时击穿可能发生在任一层:本地 miss → 查 Redis;Redis miss → 查 DB。不能只防一层。
- 本地缓存未命中时,仍要走
singleflight.Group控制对 Redis 的并发访问,否则大量请求会同时穿透到 Redis - Redis 返回
redis.Nil后,singleflight的加载函数里再查 DB,并把结果同时写入 Redis 和本地缓存 - 如果本地缓存用了永不过期策略(如热点配置),必须配健康检查:后台刷新失败时,要 fallback 到带 TTL 的 Redis 缓存,否则单点刷新故障会导致全量请求击穿
最容易被忽略的是:所有防护逻辑都依赖「正确识别缓存 miss」。无论是 rdb.Get(ctx, key).Result() 还是 c.Get(key),返回 nil 不等于错误——它是业务预期路径的一部分。混为一谈会让整个防护链失效。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











