Go 语言一直以简单高效著称,并发的支持更是 Go 语言的强项。除了 Goroutine 协程、Channel 通道、Atomic 原语等特性,还在扩展包 golang.org/x 中提供了 singleflight 这一工具。
singleflight 的导入路径为 golang.org/x/sync/singleflight,相关资料可以在 https://pkg.go.dev/golang.org/x/sync/singleflight 看到。singleflight 直译过来是“单飞”,它的主要作用就是将一组相同的请求合并成一个请求,实际上只会去请求一次,然后对所有的请求返回相同的结果。因为能确保同一操作不会被同时多次执行。这对于避免重复的工作和资源浪费非常有效,尤其是在并发环境中。
在实际生产中,singleflight 通常用来抑制请求避免缓存击穿。瞬间流量较大时,遇见缓存失效或者缓存未命中的情况,就需要请求数据库并将结果保存到 Redis 中,并且返回给请求。
实际运用中,有下面这两种方式。
首先看第一种方式,在请求 Redis 之后发现 cache miss,之后需要查询数据库。使用 singleflight 抑制了查询数据库的操作。访问 Redis 的过程并没有做限制,以 Redis 的性能来说是没有问题的。而且不需要在 singleflight 的逻辑中处理 Redis 访问错误。
再看第二种方式,在请求 Redis 之前即执行 singleflight,使用 singleflight 抑制了对于 Redis 的请求,也保证了同一时刻只有一个线程访问 DB。在请求数过多时,限制了访问 Redis 的线程数,可以有效减少 Redis 的请求以及网络开销。但是在访问 Redis 和读库这个过程中,需要处理好请求 Redis 失败时的逻辑,需要调用 Forget 方法,放开下一个线程的请求。
因为都抑制了请求,所以在访问 Redis 或者数据库时发生错误,需要调用 Forget 放下一个线程进入,避免这一批请求,因为第一个请求失败而导致全部失败。
1、导入 singleflight 相关的依赖。
go get golang.org/x/sync/singleflight
2、模拟一个昂贵的操作。
// expensiveOperation 模拟一个昂贵的操作
func expensiveOperation(key string) (string, error) {
fmt.Printf("Executing expensive operation for key: %s\n", key)
time.Sleep(2 * time.Second) // 模拟耗时操作
return fmt.Sprintf("Result for %s", key), nil
}
3、模拟使用 singleflight 进行请求抑制,减少实际请求的次数。
func main() {
// 创建一个 Group 实例
var g singleflight.Group
// 模拟并发请求
keys := []string{
"key1", "key2", "key1"}
for _, key := range keys {
go func(k string) {
// 使用 Group 的 Do 方法
result, err, _ := g.Do(k, func() (interface{
}, error) {
// 执行昂贵的操作
return expensiveOperation(k)
})
if err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Printf("Result for %s: %s\n", k, result.(string))
}
}(key)
}
// 等待所有 goroutines 完成
time.Sleep(3 * time.Second)
}
4、查看运行结果。
Executing expensive operation for key: key1
Executing expensive operation for key: key2
Result for key2: Result for key2
Result