Singleflight(合併請求)

王鹏鑫發表於2024-05-17

簡介

看到一個有意思的庫:

SingleFlight是Go語言提供的一個擴充套件包。作用是當有多個goroutine同時呼叫同一個函式的時候,只允許一個goroutine去呼叫這個函式,等到這個呼叫的goroutine返回結果的時候,再把結果返回給這幾個同時呼叫的goroutine,這樣可以減少併發呼叫的數量。

Singleflight是以阻塞讀的方式來控制向下遊請求的併發量的,在第一個goroutine請求沒有返回前,所有的請求都將被阻塞。極端情況下可能導致我們的程式hang住,由於連鎖反應甚至導致我們整個系統掛掉。

可以傳遞ctx,制定超時時間,避免執行緒的長時間阻塞。

假設第一個執行緒超時了,後續只要有結果還是可以正確返回值的。

實現原理

Singleflight使用互斥鎖Muext和Map來實現,Mutext用來保證併發時的讀寫安全,Map用來儲存同一個key的正在處理(in flight)的請求。

type Group struct {
  mu sync.Mutex       // protects m
  m  map[string]*call // lazily initialized
}

同時,Singleflight定義了一個call物件,call代表了正在執行的fn函式的請求或者是已經執行完成的請求。

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

主要看下Do方法,DoChan方法和Do方法類似

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 { // 存在相同的key
    c.dups++
    g.mu.Unlock()
    c.wg.Wait() // 等待這個key的第一個呼叫完成

    // ...
    
    return c.val, c.err, true // 複用第一個key的請求結果
  }
  c := new(call) // 第一個呼叫建立一個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
}
// doCall方法會實際的執行函式fn
func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {
  defer func() {
    // ...
    
    g.mu.Lock()
    defer g.mu.Unlock()
    c.wg.Done() // 阻塞等待完成
    if g.m[key] == c {
      delete(g.m, key) // 呼叫完成刪除這個key
    }
    
    // ...
  }()

  func() {
    // ...
    
    c.val, c.err = fn() // 真正呼叫,結果賦值給call
    
    // ...
  }()
}

應用場景

在快取擊穿的時候有用,假設大量的請求進來快取miss,請求擊穿到資料庫

有的資料當時是熱點,可能很快就變成了冷資料。針對類似的場景是不太合適設定不過期的,這個時候的Singleflight就派上用場了,可以避免大量的請求擊穿到資料庫,從而保護我們的服務穩定。

虛擬碼實現:

func getDataSingleFlight(key string) (interface{}, error) {
  v, err, _ := g.Do(key, func() (interface{}, error) {
    // 查快取
    data, err := getDataFromCache(key)
    if err == nil {
      return data, nil
    }
    if errors.Is(err, ErrNotFound) {
      // 查DB
      data, err := getDataFromDB(key)
      if err == nil {
        setCache(data) // 設定快取
        return data, nil
      }
      return nil, err
    }
    return nil, err // 快取出錯直接返回,防止穿透到DB
  })
  if err != nil {
    return nil, err
  }
  
  return v, nil
}

相關文章