recover不能防范缓存击穿,它仅捕获singleflight回调内panic并转为error;击穿需靠缓存策略、singleflight请求合并与context超时控制三者协同解决。

recover 不能防范缓存击穿,它只能捕获 singleflight 回调函数里 panic 导致的 goroutine 崩溃;击穿本身是业务逻辑问题,得靠缓存策略+singleflight+超时控制共同解决。
singleflight.Do 回调里 panic 会怎样
如果你在 Do 的回调函数里写了可能 panic 的代码(比如 nil 指针解引用、强制类型断言失败、并发写 map),singleflight.Group 会 recover 它,并把 panic 转成 error 返回给所有等待者——但这个 error 是你无法区分“业务错误”和“崩溃错误”的泛化错误。
- 所有并发请求都会拿到同一个
error,比如panic: runtime error: invalid memory address被转成error后只剩字符串,没堆栈 - 该 key 的 pending 状态不会被清理,后续请求仍会排队等一个永远不会再返回的结果(除非超时)
- recover 发生在
singleflight内部,你无法在外部加 defer/recover 拦截它
真正该 recover 的位置:回源函数内部
你要在自己写的回源逻辑里主动加 defer/recover,把 panic 控制在最小范围,并返回有意义的 error。别指望 singleflight 替你兜底。
- 回源函数必须用
defer func() { if r := recover(); r != nil { ... } }()包一层 - recover 后建议记录带堆栈的 warn 日志(用
debug.PrintStack()或第三方库),再返回具体 error,比如fmt.Errorf("fetchFromDB panic: %v", r) - 不要在 recover 里重试或调用其他可能 panic 的函数,避免嵌套崩溃
- 示例片段:
func fetchFromDB(id string) (Product, error) {
defer func() {
if r := recover(); r != nil {
log.Warnw("fetchFromDB panic", "id", id, "panic", r)
debug.PrintStack()
}
}()
// 正常查 DB 逻辑...
}
为什么不能只靠 recover 解决击穿问题
缓存击穿的本质是:大量并发请求同时发现缓存 miss,又没做请求合并,直接打到下游。recover 只管“崩了怎么办”,不管“要不要崩”。
-
recover不解决高并发穿透,singleflight才负责合并请求 -
recover不解决超时卡死,你得在回源函数里用context.WithTimeout主动控制 -
recover不解决 key 泄露,恶意构造不同 id 会让singleflight.Group的 map 持续增长,最终 OOM - 如果回源函数里没写 recover,且发生了 panic,
singleflight虽然能转 error,但你失去了定位能力——日志里只有 “panic captured”,没有原始上下文
最容易被忽略的点:recover 后别忘清理状态
有些回源逻辑会操作共享资源(比如初始化一个全局连接池、写本地缓存标记),如果 panic 发生在中间,recover 后若不手动回滚或重置,后续请求可能拿到脏状态。
- 例如:先
mu.Lock(),再操作 map,中间 panic → recover 后必须mu.Unlock() - 例如:往 sync.Map 写了一个临时 key 表示“正在加载”,panic 后没删 → 这个 key 会一直残留,影响后续判断
- 这不是
singleflight的责任,是你回源函数的契约义务
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











