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 次