Go并發(fā)原語之SingleFlight請(qǐng)求合并方法實(shí)例
SingleFlight 的使用場(chǎng)景
在處理多個(gè) goroutine 同時(shí)調(diào)用同一個(gè)函數(shù)的時(shí)候,如何只用一個(gè) goroutine 去調(diào)用一次函數(shù),并將返回結(jié)果給到所有 goroutine,這是可以使用 SingleFlight,可以減少并發(fā)調(diào)用的數(shù)量。
在高并發(fā)請(qǐng)求場(chǎng)景中,例如秒殺場(chǎng)景:多個(gè)用戶在同一時(shí)間查詢庫存數(shù),這時(shí)候?qū)τ谒械挠脩舳裕粫r(shí)間查詢結(jié)果都是一樣的,如果后臺(tái)都去查緩存或者數(shù)據(jù)庫,那么性能壓力很大。如果相同時(shí)間只有一個(gè)查詢,那么性能將顯著提升。

一句話總結(jié):SingleFlight 主要作用是合并并發(fā)請(qǐng)求的場(chǎng)景,針對(duì)于相同的讀請(qǐng)求。
SingleFlight 的基本使用
下面先看看這段代碼,5個(gè)協(xié)程同時(shí)并發(fā)返回 getProductById ,看看輸出結(jié)果如何:
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
result := getProductById("商品A")
fmt.Printf("%v\n", result)
}()
}
wg.Wait()
}
func getProductById(name string) string {
fmt.Println("getProductById doing...")
time.Sleep(time.Millisecond * 10) // 模擬一下耗時(shí)
return name
}$ go run main.go getProductById doing... getProductById doing... getProductById doing... getProductById doing... getProductById doing... 商品A 商品A 商品A 商品A 商品A
可以看出 getProductById 方法被訪問了五次,那么如何通過 SingleFlight 進(jìn)行優(yōu)化呢?
定義一個(gè)全局變量 SingleFlight,在訪問 getProductById 方法時(shí)調(diào)用 Do 方法,即可實(shí)現(xiàn)同一時(shí)間只有一次方法,代碼如下:
import (
"fmt"
"golang.org/x/sync/singleflight"
"sync"
"time"
)
var g singleflight.Group
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
resp, _, _ := g.Do("商品A", func() (interface{}, error) {
result := getProductById("商品A")
return result, nil
})
fmt.Printf("%v\n", resp)
}()
}
wg.Wait()
}
func getProductById(name string) string {
fmt.Println("getProductById doing...")
time.Sleep(time.Millisecond * 10) // 模擬一下耗時(shí)
return name
}$ go run main.go getProductById doing... 商品A 商品A 商品A 商品A 商品A
你可能會(huì)想 SingleFlight 和 sync.Once 的區(qū)別,sync.Once 主要是用在單次初始化場(chǎng)景中,而 SingleFlight 主要用在合并請(qǐng)求中,針對(duì)于同一時(shí)間的并發(fā)場(chǎng)景。
SingleFlight 的實(shí)現(xiàn)原理
SingleFlight 的數(shù)據(jù)結(jié)構(gòu)是 Group ,結(jié)構(gòu)如下:
// call is an in-flight or completed singleflight.Do call
type call struct {
wg sync.WaitGroup
// These fields are written once before the WaitGroup is done
// and are only read after the WaitGroup is done.
val interface{}
err error
// These fields are read and written with the singleflight
// mutex held before the WaitGroup is done, and are read but
// not written after the WaitGroup is done.
dups int
chans []chan<- Result
}
// Group represents a class of work and forms a namespace in
// which units of work can be executed with duplicate suppression.
type Group struct {
mu sync.Mutex // protects m
m map[string]*call // lazily initialized
}
// Result holds the results of Do, so they can be passed
// on a channel.
type Result struct {
Val interface{}
Err error
Shared bool
}可以看出,SingleFlight 是使用互斥鎖 Mutex 和 Map 來實(shí)現(xiàn)的?;コ怄i Mutex 提供并發(fā)時(shí)的讀寫保護(hù),而 Map 用于保存同一個(gè) key 正在處理的請(qǐng)求。
其提供了3個(gè)方法:

Do 方法的實(shí)現(xiàn)邏輯
// Do executes and returns the results of the given function, making
// sure that only one execution is in-flight for a given key at a
// time. If a duplicate comes in, the duplicate caller waits for the
// original to complete and receives the same results.
// The return value shared indicates whether v was given to multiple callers.
func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) {
g.mu.Lock()
if g.m == nil {
g.m = make(map[string]*call)
}
if c, ok := g.m[key]; ok {
c.dups++
g.mu.Unlock()
c.wg.Wait()
if e, ok := c.err.(*panicError); ok {
panic(e)
} else if c.err == errGoexit {
runtime.Goexit()
}
return c.val, c.err, true
}
c := new(call)
c.wg.Add(1)
g.m[key] = c
g.mu.Unlock()
g.doCall(c, key, fn)
return c.val, c.err, c.dups > 0
}SingleFlight 定義了一個(gè)輔助對(duì)象 call,用于代表正在執(zhí)行 fn 函數(shù)的請(qǐng)求或者是否已經(jīng)執(zhí)行完請(qǐng)求。
- 如果存在相同的 key,其他請(qǐng)求將會(huì)等待這個(gè) key 執(zhí)行完成,并使用第一個(gè) key 獲取到的請(qǐng)求結(jié)果
- 如果不存在,創(chuàng)建一個(gè) call ,并將其加入到 map 中,執(zhí)行調(diào)用 fn 函數(shù)。
DoChan 方法的實(shí)現(xiàn)邏輯
而 DoChan 方法與 Do 方法類似:
// DoChan is like Do but returns a channel that will receive the
// results when they are ready.
//
// The returned channel will not be closed.
func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result {
ch := make(chan Result, 1)
g.mu.Lock()
if g.m == nil {
g.m = make(map[string]*call)
}
if c, ok := g.m[key]; ok {
c.dups++
c.chans = append(c.chans, ch)
g.mu.Unlock()
return ch
}
c := &call{chans: []chan<- Result{ch}}
c.wg.Add(1)
g.m[key] = c
g.mu.Unlock()
go g.doCall(c, key, fn)
return ch
}Forget 方法的實(shí)現(xiàn)邏輯
// Forget tells the singleflight to forget about a key. Future calls
// to Do for this key will call the function rather than waiting for
// an earlier call to complete.
func (g *Group) Forget(key string) {
g.mu.Lock()
delete(g.m, key)
g.mu.Unlock()
}將 key 從 map 中刪除。
總結(jié)
使用 SingleFlight 時(shí),通過將多個(gè)請(qǐng)求合并成一個(gè),降低并發(fā)訪問的壓力,極大地提升了系統(tǒng)性能,針對(duì)于多并發(fā)讀請(qǐng)求的場(chǎng)景,可以考慮是否滿足 SingleFlight 的使用情況。
而對(duì)于并發(fā)寫請(qǐng)求的場(chǎng)景,如果是多次寫只需要一次的情況,那么也是滿足的。例如:每個(gè) http 請(qǐng)求都會(huì)攜帶 token,每次請(qǐng)求都需要把 token 存入緩存或者寫入數(shù)據(jù)庫,如果多次并發(fā)請(qǐng)求同時(shí)來,只需要寫一次即可
以上就是Go并發(fā)原語之SingleFlight請(qǐng)求合并方法實(shí)例的詳細(xì)內(nèi)容,更多關(guān)于Go SingleFlight 請(qǐng)求合并的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- golang并發(fā)工具M(jìn)apReduce降低服務(wù)響應(yīng)時(shí)間
- Go中Goroutines輕量級(jí)并發(fā)的特性及效率探究
- 盤點(diǎn)總結(jié)2023年Go并發(fā)庫有哪些變化
- Go語言單線程運(yùn)行也會(huì)有的并發(fā)問題解析
- go并發(fā)數(shù)據(jù)一致性事務(wù)的保障面試應(yīng)答
- Go并發(fā)編程結(jié)構(gòu)體多字段原子操作示例詳解
- Go語言動(dòng)態(tài)并發(fā)控制sync.WaitGroup的靈活運(yùn)用示例詳解
- Go語言并發(fā)處理效率響應(yīng)能力及在現(xiàn)代軟件開發(fā)中的重要性
相關(guān)文章
Go切片導(dǎo)致rand.Shuffle產(chǎn)生重復(fù)數(shù)據(jù)的原因與解決方案
在 Go 語言的實(shí)際開發(fā)中,切片(slice)是一種非常靈活的數(shù)據(jù)結(jié)構(gòu),然而,由于其底層數(shù)據(jù)共享的特性,在某些情況下可能會(huì)導(dǎo)致意想不到的 Bug,本文將詳細(xì)分析 rand.Shuffle 之后,切片中的數(shù)據(jù)出現(xiàn)重復(fù)的問題,探討其根本原因,并給出最佳解決方案,需要的朋友可以參考下2025-02-02
GO語言實(shí)現(xiàn)TCP服務(wù)器的示例代碼
這篇文章主要為大家詳細(xì)介紹了如何通過GO語言實(shí)現(xiàn)TCP服務(wù)器,文中的示例代碼講解詳細(xì),對(duì)我們深入了解Go語言有一定的幫助,需要的可以參考一下2023-03-03
Go語言使用Timeout Context取消任務(wù)的實(shí)現(xiàn)
本文主要介紹了Go語言使用Timeout Context取消任務(wù)的實(shí)現(xiàn),包括基本的任務(wù)取消和控制HTTP客戶端請(qǐng)求的超時(shí),具有一定的參考價(jià)值,感興趣的可以了解一下2024-01-01
Go語言開發(fā)發(fā)送Get和Post請(qǐng)求的示例
這篇文章主要介紹了Go語言開發(fā)發(fā)送Get和Post請(qǐng)求的示例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-07-07

