場景:
有N 個併發請求來訪問Api1時 ,如果數據庫或者web服務器沒有對請求做限制,那麼所有請求都會訪問一次數據庫,很可能造成數據庫壓力比較大,而且 HTTP訪問也比較耗時。
實現:
有N 個併發請求來訪問Api1時, 只有一個請求可以訪問到數據庫,其他請求共享一個請求的結果。
安排:
1. 定義一個請求組,來存儲所有的請求
type RequestGroup struct {
mu sync.Mutex
m map[string]*Result // 請求類型=>請求結果
}
我們使用使用Result類型來存儲請求結果,mu 對請求的管理。(此處如果不清楚如何使用,後續詳細講解)
2. 定義一個請求結果的類型
type Result struct {
wg sync.WaitGroup
val interface{}
err error
}
好了,那麼我們如何處理併發時來的請求呢? 所有請求的結果該如何處理呢?
首先,我們應該在有請求時,開始攔截驗證是否同時有相同的請求訪問,如果有,阻塞,直到第一個訪問數據庫的請求結束,所有請求獲取到結果後結束。
代碼演示:
func (g *RequestGroup ) Do (key string, getDataFunc func()(interface{}, error)) (interface{}, error) {
g.mu.Lock() // 【1】
if g.m == nil {
g.m = make(map[string]*call)
}
if c, ok := g.m[key]; ok {
g.mu.Unlock()
c.wg.Wait() // 【4】
return c.val, c.err
}
c := new(Result)
c.wg.Add(1)
g.m[key] = c
// 首個請求類型已經存儲
g.mu.Unlock() // 【2】
c.val, c.err = getDataFunc()
c.wg.Done() // 【3】
g.mu.Lock()
delete(g.m, key) // 【5】每次完成將某個請求的標識刪除
g.mu.Unlock()
return c.val, c.err
}
- 假設N個併發請求,請求A 進入Do方法後,N-1個請求會被阻塞在【1】處,當A 將請求類型存儲時,釋放請求鎖,然後N-1個請求依次(通過Lock =》 Unlock)進入,同時阻塞在【4】位置。等待【3】釋放,整個正常進入最終獲取結果階段。
- 程序最後要刪除請求類型標識,否則下次請求進入還是緩存的數據。
驗證:
模仿併發請求
func main() {
for i := 0; i < 5; i++ {
wg.Add(1)
go func(i int) {
val, err := G.Do(i, "key", func() (interface{}, error) {
time.Sleep(3 * time.Second) // 模仿數據庫請求
ff++
return ff, nil
})
if err == nil {
fmt.Println(i, "獲取結果...", val)
}
wg.Done()
}(i)
}
wg.Wait()
fmt.Println("全部請求結束。。。。")
}
由於代碼比較亂,爲了方便演示,加入了一些標識在程序中,
完整代碼如下:
package main
import (
"fmt"
"sync"
"time"
)
type Result struct {
wg sync.WaitGroup
val interface{}
err error
}
type RequestGroup struct {
mu sync.Mutex
m map[string]*Result
}
var G = &RequestGroup{
m: make(map[string]*Result),
}
var ff = 0
var wg sync.WaitGroup
func main() {
for i := 0; i < 5; i++ {
wg.Add(1)
go func(i int) {
val, err := G.Do(i, "key", func() (interface{}, error) {
time.Sleep(3 * time.Second) // 模仿數據庫請求
ff++
return ff, nil
})
if err == nil {
fmt.Println(i, "獲取結果...", val)
}
wg.Done()
}(i)
}
wg.Wait()
fmt.Println("全部請求結束。。。。")
}
// 併發時,將相同key的請求wait等待第一個請求獲取結果
/**
非併發期間:key 請求 req1 -> 內存中沒有key-> 創建key=>val -> 釋放key=>val
併發期間: key 請求 req1 -> 內存中沒有key-> 創建key=>val -> 釋放key=>val
*/
func (g *RequestGroup) Do(idx int, key string, getDataFunc func() (interface{}, error)) (interface{}, error) {
fmt.Println(idx, "阻塞")
g.mu.Lock() // 【1】
time.Sleep(2 * time.Second)
fmt.Println(idx, "..進入了")
if g.m == nil {
g.m = make(map[string]*Result)
}
if c, ok := g.m[key]; ok {
g.mu.Unlock()
fmt.Println(idx, "......我阻塞再次.")
c.wg.Wait() // 【4】
return c.val, c.err
}
c := new(Result)
c.wg.Add(1)
g.m[key] = c
// 首個請求類型存儲
time.Sleep(3 * time.Second)
fmt.Println(idx, "....釋放鎖")
g.mu.Unlock() // 【2】
// 結果獲取
c.val, c.err = getDataFunc()
time.Sleep(15 * time.Second)
fmt.Println(idx, "..........釋放鎖2")
c.wg.Done() // 【3】
// 請求標識清理
g.mu.Lock()
delete(g.m, key) // 【5】每次完成將某個請求的標識刪除
g.mu.Unlock()
return c.val, c.err
}
可以說很細緻,明確的演示了整個程序的運行過程,如果覺得亂,可將演示代碼刪除。