golang防緩存擊穿利器--singleflight

緩存擊穿

    給緩存加一個過時時間,下次未命中緩存時再去從數據源獲取結果寫入新的緩存,這個是後端開發人員再熟悉不過的基操。本人以前在作直播平臺活動業務的時候,當時帶着這份再熟練不過的自信,把複雜的數據庫鏈表語句寫好,各類微服務之間調用撈數據最後算好的結果,丟進了緩存而後設了一個過時時間,當時噼裏啪啦兩下寫完代碼以爲穩如鐵蛋,結果在活動快結束以前,數據庫很友好的掛掉了。當時回去查看監控後發現,是在活動快結束前,大量用戶都在瘋狂的刷活動頁,致使緩存過時的瞬間有大量未命中緩存的請求直接打到數據庫上所致使的,因此這個經典的問題稍不注意仍是害死人前端

    防緩存擊穿的方式有不少種,好比經過計劃任務來跟新緩存使得從前端過來的全部請求都是從緩存讀取等等。以前讀過 groupCache的源碼,發現裏面有一個頗有意思的庫,叫singleFlight, 由於groupCache從節點上獲取緩存若是未命中,則會去其餘節點尋找,其餘節點尚未的話再從數據源獲取,因此這個步驟對於防擊穿很是有必要。singleFlight使得groupCache在多個併發請求對一個失效的key進行源數據獲取時,只讓其中一個獲得執行,其他阻塞等待到執行的那個請求完成後,將結果傳遞給阻塞的其餘請求達到防止擊穿的效果。git

SingleFlight 使用Demo

本文模擬一個數據源是從調用rpc獲取的場景
圖片描述
而後再模擬一百個併發請求在緩存失效的瞬間同時調用rpc訪問源數據
圖片描述
效果
圖片描述
能夠看到100個併發請求從源數據獲取時,rpcServer端只收到了來自client 17的請求,而其他99個最後也都獲得了正確的返回值。github

SingleFlight 源碼剖析

在看完singleFlight的實際效果後,欣喜若狂,想必其實現應該至關複雜吧, 結果翻看源碼一看, 100行不到的代碼就解決了這麼個業務痛點, 不得不佩服。golang

package singlefilght

import "sync"

type Group struct {
    mu sync.Mutex
    m map[string]*Call // 對於每個須要獲取的key有一個對應的call
}

// call表明須要被執行的函數
type Call struct {
    wg sync.WaitGroup // 用於阻塞這個調用call的其餘請求
    val interface{} // 函數執行後的結果
    err error         // 函數執行後的error
}

func (g *Group) Do(key string, fn func()(interface{}, error)) (interface{}, error) {

    g.mu.Lock()
    if g.m == nil {
        g.m = make(map[string]*Call)
    }
    
    // 若是獲取當前key的函數正在被執行,則阻塞等待執行中的,等待其執行完畢後獲取它的執行結果
    if c, ok := g.m[key]; ok {
        g.mu.Unlock()
        c.wg.Wait()
        return c.val, c.err
    }

    // 初始化一個call,往map中寫後就解
    c := new(Call)
    c.wg.Add(1)
    g.m[key] = c
    g.mu.Unlock()
    
  // 執行獲取key的函數,並將結果賦值給這個Call
    c.val, c.err = fn()
    c.wg.Done()
    
    // 從新上鎖刪除key
    g.mu.Lock()
    delete(g.m, key)
    g.mu.Unlock()

    return c.val, c.err

}

    對的沒看錯, 就這麼100行不到的代碼就能解決緩存擊穿的問題,這算是我寫過最愉快的一篇博了,同時也推薦你們去讀一讀groupCache這個項目的源碼,會有更多驚喜的發現數據庫

相關文章
相關標籤/搜索