Go 用 singleflight 合并并发重复请求,防缓存击穿与重复计算
test12026-10-092 次阅读
重复请求打爆下游
高并发下,缓存刚好失效的那一刻可能同时涌进来一千个请求,它们发现缓存没有,于是各自去查数据库,数据库瞬间被打满,这就是典型的缓存击穿。singleflight 的核心思路是:同一时刻对同一个 key 的调用,只放一个真正执行,其余调用共享这一次的结果。
基本用法
singleflight.Group 的 Do 方法,相同 key 的并发调用会合并,fn 只执行一次,返回值 (v, err) 共享给所有等待者;shared 标记本次结果是否被复用。
package main
import (
"fmt"
"sync"
"time"
"golang.org/x/sync/singleflight"
)
var group singleflight.Group
func getData(key string) (string, error) {
v, err, _ := group.Do(key, func() (interface{}, error) {
// 模拟一次昂贵的查询,并发期间只执行一次
time.Sleep(200 * time.Millisecond)
return "value-of-" + key, nil
})
if err != nil {
return "", err
}
return v.(string), nil
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
v, _ := getData("user:1")
fmt.Println(v)
}()
}
wg.Wait()
}
结合本地缓存防击穿
典型做法是缓存未命中时走 singleflight 取数,取回后回填缓存,这样缓存失效窗口内也只有一次真正的下游调用。
var cache = map[string]string{}
var mu sync.Mutex
func loadUser(id string) (string, error) {
mu.Lock()
if v, ok := cache[id]; ok {
mu.Unlock()
return v, nil
}
mu.Unlock()
v, err, _ := group.Do("user:"+id, func() (interface{}, error) {
val, e := queryDB(id) // 真正查库
if e != nil {
return nil, e
}
mu.Lock()
cache[id] = val
mu.Unlock()
return val, nil
})
if err != nil {
return "", err
}
return v.(string), nil
}
实践要点
- key 要有意义且粒度合适,太粗会把不相干的请求也合并,太细则失去合并效果。
- fn 内报错时,singleflight 不会缓存错误结果;如需防止"错误也被共享导致雪崩",可加短期熔断。
- 单次请求较长时,合并能显著降低下游压力,但会让并发调用者排队等待同一结果,注意超时控制。
- 它解决的是"同一时刻重复",不能替代缓存本身,通常和缓存、限流配合使用。
T
test1
文章作者
重复请求打爆下游 高并发下,缓存刚好失效的那一刻可能同时涌进来一千个请求,它们发现缓存没有,于是各自去查数据库,数据库...
- 分类
- 技术
- 发布时间
- 2026-10-09
- 字数
- 约 1494 字
- 阅读
- 2 次
