<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          防止緩存擊穿庫:singleflight 源碼分析

          共 3806字,需瀏覽 8分鐘

           ·

          2021-08-24 21:17

          singleflight通常被用來做防止緩存擊穿,代碼位置在https://github.com/golang/groupcache/tree/master/singleflight,在詳細(xì)介紹代碼內(nèi)容之前,我先區(qū)分下雪崩、穿透和擊穿:

           雪崩

                雪崩就是指緩存中大批量熱點數(shù)據(jù)同時過期緩存機器意外發(fā)生了全盤宕機后系統(tǒng)涌入大量查詢請求,因為大部分?jǐn)?shù)據(jù)在Redis層已經(jīng)失效,請求滲透到數(shù)據(jù)庫層,大批量請求猶如洪水一般涌入,引起數(shù)據(jù)庫壓力造成查詢堵塞甚至宕機。

          解決辦法:

                 將緩存失效時間分散開,比如每個key的過期時間是隨機,防止同一時間大量數(shù)據(jù)過期現(xiàn)象發(fā)生,這樣不會出現(xiàn)同一時間全部請求都落在數(shù)據(jù)庫層,如果緩存數(shù)據(jù)庫是分布式部署,將熱點數(shù)據(jù)均勻分布在不同Redis和數(shù)據(jù)庫中,有效分擔(dān)壓力,別一個人扛。

                  簡單粗暴,讓Redis數(shù)據(jù)永不過期(如果業(yè)務(wù)準(zhǔn)許,比如不用更新的名單類)。當(dāng)然,如果業(yè)務(wù)數(shù)據(jù)準(zhǔn)許的情況下可以,比如中獎名單用戶,每期用戶開獎后,名單不可能會變了,無需更新。

                   事前:redis 高可用,主從+哨兵,redis cluster,避免全盤崩潰。- 事中:本地 ehcache 緩存 + hystrix 限流&降級,避免 MySQL 被打死。- 事后:redis 持久化,一旦重啟,自動從磁盤上加載數(shù)據(jù),快速恢復(fù)緩存數(shù)據(jù)。

          緩存穿透

                      緩存穿透是指段時間涌入大量請求,緩存中查不到,每次你去數(shù)據(jù)庫里查,也查不到。(數(shù)據(jù)庫 id 是從 1 開始的,結(jié)果黑客發(fā)過來的請求 id 全部都是負(fù)數(shù)。)這樣的話,緩存中不會有,請求每次都“視緩存于無物”,直接查詢數(shù)據(jù)庫。這種惡意攻擊場景的緩存穿透就會直接把數(shù)據(jù)庫給打死。

                  解決方式很簡單,每次系統(tǒng) A 從數(shù)據(jù)庫中只要沒查到,就寫一個空值到緩存里去,比如 set -999 UNKNOWN。然后設(shè)置一個過期時間,這樣的話,下次有相同的 key 來訪問的時候,在緩存失效之前,都可以直接從緩存中取數(shù)據(jù)。

          緩存擊穿

                      緩存擊穿,某個 key 非常熱點,訪問非常頻繁,處于集中式高并發(fā)訪問的情況,當(dāng)這個 key 在失效的瞬間,大量的請求就擊穿了緩存,直接請求數(shù)據(jù)庫,就像是在一道屏障上鑿開了一個洞。

                 方法一:我們簡單粗暴點,直接讓熱點數(shù)據(jù)永遠(yuǎn)不過期,定時任務(wù)定期去刷新數(shù)據(jù)就可以了。不過這樣設(shè)置需要區(qū)分場景,比如某寶首頁可以這么做。

                  方法二:為了避免出現(xiàn)緩存擊穿的情況,我們可以在第一個請求去查詢數(shù)據(jù)庫的時候?qū)λ右粋€互斥鎖,其余的查詢請求都會被阻塞住,直到鎖被釋放,后面的線程進來發(fā)現(xiàn)已經(jīng)有緩存了,就直接走緩存,從而保護數(shù)據(jù)庫。但是也是由于它會阻塞其他的線程,此時系統(tǒng)吞吐量會下降。需要結(jié)合實際的業(yè)務(wù)去考慮是否要這么做。

                  方法三:就是singleflight的設(shè)計思路,也會使用互斥鎖,但是相對于方法二的加鎖粒度會更細(xì)

            

          singleflight 源碼分析

                  說完了singleflight的應(yīng)用場景,下面詳細(xì)分析下singleflight的源碼,源碼非常簡潔,目錄下就包含了兩個文件singleflight.go 和對應(yīng)的測試的測試文件singleflight_test.go

                   源碼中就定義了兩個結(jié)構(gòu)體和一個方法

          // call is an in-flight or completed Do calltype call struct {  wg  sync.WaitGroup  val interface{}  err error}
          // 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}

          通過call的waitGroup來阻塞相同key的請求,實現(xiàn)了一個指允許一個請求到后端,通過Group的m來實現(xiàn)相同key的數(shù)據(jù)共享,大家取同一份結(jié)果,下面看下Do函數(shù)的具體實現(xiàn):

          // 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.//同一個對象多次同時多次調(diào)用這個邏輯的時候,可以使用其中的一個去執(zhí)行func (g *Group) Do(key string, fn func()(interface{},error)) (interface{}, error ){    g.mu.Lock() //加鎖保護存放key的map,因為要并發(fā)執(zhí)行    if g.m == nil { //lazing make 方式建立        g.m = make(map[string]*call)    }    if c, ok := g.m[key]; ok { //如果map中已經(jīng)存在對這個key的處理那就等著吧        g.mu.Unlock() //解鎖,對map的操作已經(jīng)完畢        c.wg.Wait()        return c.val,c.err //map中只有一份key,所以只有一個c    }    c := new(call) //創(chuàng)建一個工作單元,只負(fù)責(zé)處理一種key    c.wg.Add(1)    g.m[key] = c //將key注冊到map中    g.mu.Unlock() //map的操做完成,解鎖        c.val, c.err = fn()//第一個注冊者去執(zhí)行    c.wg.Done()        g.mu.Lock()    delete(g.m,key) //對map進行操作,需要枷鎖    g.mu.Unlock()        return c.val, c.err //給第一個注冊者返回結(jié)果}

          執(zhí)行過程如下:     

          1,對于相同key的請求,大家搶鎖,只有第一個請求可以獲得鎖;

          2,然后查詢map發(fā)現(xiàn)沒有數(shù)據(jù),創(chuàng)建一個call,waitGroup加1,寫入map,然后釋放鎖;做到了鎖的粒度最小化。

          3,其他獲得鎖的請求,從map中取到call,由于函數(shù)fn還沒有執(zhí)行完畢,所以waitGroup還在等待狀態(tài),后面獲得鎖的請求都在等待這個waitGroup;

          4,當(dāng)函數(shù)執(zhí)行完畢以后,獲得了數(shù)據(jù),調(diào)用wg.Done()通知所有等待的請求獲取數(shù)據(jù),實現(xiàn)了大家共享一份數(shù)據(jù);

          5,然后加鎖做清理工作,清理掉map里存儲的數(shù)據(jù)。



          推薦閱讀


          福利

          我為大家整理了一份從入門到進階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門看什么,進階看什么。關(guān)注公眾號 「polarisxu」,回復(fù) ebook 獲??;還可以回復(fù)「進群」,和數(shù)萬 Gopher 交流學(xué)習(xí)。

          瀏覽 57
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  成人性爱片一区 | 干B网站 操你逼逼逼逼 | 久久婷婷国产综合精品_国产激情 | 69av网站 | 久草电影在线观看 |