[go語(yǔ)言]-深入理解singleflight

[toc]



前言

最近從java轉(zhuǎn)到go,來(lái)公司第一個(gè)開(kāi)發(fā)工作就是對(duì)一個(gè)資源請(qǐng)求去重復(fù)桩卵,最終發(fā)現(xiàn)這個(gè)singleflight這個(gè)好東西,分享一下产禾。

singleflight使用場(chǎng)景

  1. 緩存擊穿:緩存在某個(gè)時(shí)間點(diǎn)過(guò)期的時(shí)候衫生,恰好在這個(gè)時(shí)間點(diǎn)對(duì)這個(gè)Key有大量的并發(fā)請(qǐng)求過(guò)來(lái),這些請(qǐng)求發(fā)現(xiàn)緩存過(guò)期一般都會(huì)從后端DB加載數(shù)據(jù)并回設(shè)到緩存斯撮,這個(gè)時(shí)候大并發(fā)的請(qǐng)求可能會(huì)瞬間把后端DB壓垮。
    • 絕大多數(shù)公司都是這么用的
  2. 請(qǐng)求資源去重復(fù)
    • 我們的用法宗挥,需要改動(dòng)一行代碼透敌。

singleflight 簡(jiǎn)介

singleflightgolang.org/x/sync/singleflight 項(xiàng)目下话瞧,對(duì)外提供了以下幾個(gè)方法

//Do方法,傳入key,以及回調(diào)函數(shù)桨仿,如果key相同拉庵,fn方法只會(huì)執(zhí)行一次,同步等待
//返回值v:表示fn執(zhí)行結(jié)果
//返回值err:表示fn的返回的err
//返回值shared:表示是否是真實(shí)fn返回的還是從保存的map[key]返回的,也就是共享的
func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) {
//DoChan方法類(lèi)似Do方法警没,只是返回的是一個(gè)chan
func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result {
//設(shè)計(jì)Forget 控制key關(guān)聯(lián)的值是否失效,默認(rèn)以上兩個(gè)方法只要fn方法執(zhí)行完成后大州,內(nèi)部維護(hù)的fn的值也刪除(即并發(fā)結(jié)束后就失效了)
func (g *Group) Forget(key string) 

singleflight的使用

從singleflight的test探尋最簡(jiǎn)單用法

func TestDo(t *testing.T) {
    var g Group
    // key 可以理解資源的id
    v, err, _ := g.Do("key", func() (interface{}, error) {
    // do what you want
        return "bar", nil
    })
    if got, want := fmt.Sprintf("%v (%T)", v, v), "bar (string)"; got != want {
        t.Errorf("Do = %v; want %v", got, want)
    }
    if err != nil {
        t.Errorf("Do error = %v", err)
    }
}

驗(yàn)證并發(fā)重復(fù)請(qǐng)求

func process(g *Group, t *testing.T, ch chan int, key string) {
    for count := 0; count < 10; count++ {
        v, err, shared := g.Do(key, func() (interface{}, error) {
            time.Sleep(1000 * time.Millisecond)
            return "bar", nil
        })
        t.Log("v = ", v, " err = ", err, " shared =", shared, " ch :", ch, "g ", len(g.m))
        if got, want := fmt.Sprintf("%v (%T)", v, v), "bar (string)"; got != want {
            t.Errorf("Do = %v; want %v", got, want)
        }
        if err != nil {
            t.Errorf("Do error = %v", err)
        }
    }
    ch <- 1
}

func TestDo1(t *testing.T) {
    var g Group
    channels := make([]chan int, 10)
    key := "key"
    for i := 0; i < 10; i++ {
        channels[i] = make(chan int)
        go process(&g, t, channels[i], key)
    }
    for i, ch := range channels {
        <-ch
        fmt.Println("routine ", i, "quit!")
    }
}


  • 結(jié)果
image-20200920100920654

singleflight的原理

call

call 用來(lái)表示一個(gè)正在執(zhí)行或已完成的函數(shù)調(diào)用。

// 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和err用來(lái)記錄fn發(fā)放執(zhí)行的返回值
    val interface{}
    err error

    // forgotten indicates whether Forget was called with this call's key
    // while the call was still in flight.
    // 用來(lái)標(biāo)識(shí)fn方法執(zhí)行完成之后結(jié)果是否立馬刪除還是保留在singleflight中
    forgotten bool

    // 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 用來(lái)記錄fn方法執(zhí)行的次數(shù)
    dups  int
    //用來(lái)記錄DoChan中調(diào)用次數(shù)以及需要返回的數(shù)據(jù)
    chans []chan<- Result
}

Group

Group 可以看做是任務(wù)的分類(lèi)并徘。

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

Do 函數(shù)

// 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()
        return c.val, c.err, true
    }
    c := new(call)
    // 設(shè)置forgotten = true, doCall時(shí) 不再調(diào)用delete(g.m, key)
    // c.forgotten = true
    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 handles the single call for a key.
func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {
    c.val, c.err = fn()
    c.wg.Done()

    g.mu.Lock()
    if !c.forgotten {
        delete(g.m, key)
    }
    for _, ch := range c.chans {
        ch <- Result{c.val, c.err, c.dups > 0}
    }
    g.mu.Unlock()
}

在Do方法中是通過(guò)waitgroup來(lái)控制的初斑,主要流程如下:

  1. 在Group中設(shè)置了一個(gè)map辛润,如果key不存在,則實(shí)例化call(用來(lái)保存值信息)见秤,并將key=>call的對(duì)應(yīng)關(guān)系存入map中通過(guò)mutex保證了并發(fā)安全
  2. 如果已經(jīng)在調(diào)用中則key已經(jīng)存在map砂竖,則wg.Wait
  3. 在fn執(zhí)行結(jié)束之后(在doCall方法中執(zhí)行)執(zhí)行wg.Done
  4. 卡在第2步的方法得到執(zhí)行,返回結(jié)果

其他的DoChan方法也是類(lèi)似的邏輯鹃答,只是返回的是一個(gè)chan乎澄。

參考

singleflight包原理解析

使用Golang的singleflight防止緩存擊穿


你的鼓勵(lì)也是我創(chuàng)作的動(dòng)力

打賞地址

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市测摔,隨后出現(xiàn)的幾起案子置济,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,482評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件浙于,死亡現(xiàn)場(chǎng)離奇詭異修噪,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)路媚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)黄琼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人整慎,你說(shuō)我怎么就攤上這事脏款。” “怎么了裤园?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,762評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵撤师,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我拧揽,道長(zhǎng)剃盾,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,273評(píng)論 1 279
  • 正文 為了忘掉前任淤袜,我火速辦了婚禮痒谴,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘铡羡。我一直安慰自己积蔚,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評(píng)論 5 373
  • 文/花漫 我一把揭開(kāi)白布烦周。 她就那樣靜靜地躺著尽爆,像睡著了一般。 火紅的嫁衣襯著肌膚如雪读慎。 梳的紋絲不亂的頭發(fā)上漱贱,一...
    開(kāi)封第一講書(shū)人閱讀 49,046評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音夭委,去河邊找鬼幅狮。 笑死,一個(gè)胖子當(dāng)著我的面吹牛闰靴,可吹牛的內(nèi)容都是我干的彪笼。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼蚂且,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼配猫!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起杏死,我...
    開(kāi)封第一講書(shū)人閱讀 36,988評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤泵肄,失蹤者是張志新(化名)和其女友劉穎捆交,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體腐巢,經(jīng)...
    沈念sama閱讀 43,476評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡品追,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評(píng)論 2 324
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了冯丙。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片肉瓦。...
    茶點(diǎn)故事閱讀 38,064評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖胃惜,靈堂內(nèi)的尸體忽然破棺而出泞莉,到底是詐尸還是另有隱情,我是刑警寧澤船殉,帶...
    沈念sama閱讀 33,712評(píng)論 4 323
  • 正文 年R本政府宣布鲫趁,位于F島的核電站,受9級(jí)特大地震影響利虫,放射性物質(zhì)發(fā)生泄漏挨厚。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評(píng)論 3 307
  • 文/蒙蒙 一糠惫、第九天 我趴在偏房一處隱蔽的房頂上張望疫剃。 院中可真熱鬧,春花似錦寞钥、人聲如沸慌申。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,264評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至咨油,卻和暖如春您炉,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背役电。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,486評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工赚爵, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人法瑟。 一個(gè)月前我還...
    沈念sama閱讀 45,511評(píng)論 2 354
  • 正文 我出身青樓冀膝,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親霎挟。 傳聞我的和親對(duì)象是個(gè)殘疾皇子窝剖,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評(píng)論 2 345