Go實現API併發請求的案例

場景:

    有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
}

可以說很細緻,明確的演示了整個程序的運行過程,如果覺得亂,可將演示代碼刪除。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章