日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
讓我們一起賞析Singleflight設(shè)計

[[411393]]

本文轉(zhuǎn)載自微信公眾號「Golang夢工廠」,作者AsongGo。轉(zhuǎn)載本文請聯(lián)系Golang夢工廠公眾號。

前言

哈嘍,大家好,我是asong。今天想與大家分享一下singleflight這個庫,singleflight僅僅只有100多行卻可以做到防止緩存擊穿,有點厲害哦!所以本文我們就一起來看一看他是怎么設(shè)計的~。

注意:本文基于 https://pkg.go.dev/golang.org/x/sync/singleflight進(jìn)行分析。

緩存擊穿

什么是緩存擊穿

平常在高并發(fā)系統(tǒng)中,會出現(xiàn)大量的請求同時查詢一個key的情況,假如此時這個熱key剛好失效了,就會導(dǎo)致大量的請求都打到數(shù)據(jù)庫上面去,這種現(xiàn)象就是緩存擊穿。緩存擊穿和緩存雪崩有點像,但是又有一點不一樣,緩存雪崩是因為大面積的緩存失效,打崩了DB,而緩存擊穿則是指一個key非常熱點,在不停的扛著高并發(fā),高并發(fā)集中對著這一個點進(jìn)行訪問,如果這個key在失效的瞬間,持續(xù)的并發(fā)到來就會穿破緩存,直接請求到數(shù)據(jù)庫,就像一個完好無損的桶上鑿開了一個洞,造成某一時刻數(shù)據(jù)庫請求量過大,壓力劇增!

如何解決

  • 方法一

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

  • 方法二

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

  • 方法三

方法三就是singleflight的設(shè)計思路,也會使用互斥鎖,但是相對于方法二的加鎖粒度會更細(xì),這里先簡單總結(jié)一下singleflight的設(shè)計原理,后面看源碼在具體分析。

singleflightd的設(shè)計思路就是將一組相同的請求合并成一個請求,使用map存儲,只會有一個請求到達(dá)mysql,使用sync.waitgroup包進(jìn)行同步,對所有的請求返回相同的結(jié)果。

截屏2021-07-14 下午8.30.56

源碼賞析

已經(jīng)迫不及待了,直奔主題吧,下面我們一起來看看singleflight是怎么設(shè)計的。

數(shù)據(jù)結(jié)構(gòu)

singleflight的結(jié)構(gòu)定義如下:

 
 
 
 
  1. type Group struct { 
  2.  mu sync.Mutex       // 互斥鎖,保證并發(fā)安全 
  3.  m  map[string]*call // 存儲相同的請求,key是相同的請求,value保存調(diào)用信息。 

Group結(jié)構(gòu)還是比較簡單的,只有兩個字段,m是一個map,key是相同請求的標(biāo)識,value是用來保存調(diào)用信息,這個map是懶加載,其實就是在使用時才會初始化;mu是互斥鎖,用來保證m的并發(fā)安全。m存儲調(diào)用信息也是單獨封裝了一個結(jié)構(gòu):

 
 
 
 
  1. type call struct { 
  2.  wg sync.WaitGroup 
  3.  // 存儲返回值,在wg done之前只會寫入一次 
  4.  val interface{} 
  5.   // 存儲返回的錯誤信息 
  6.  err error 
  7.  
  8.  // 標(biāo)識別是否調(diào)用了Forgot方法 
  9.  forgotten bool 
  10.  
  11.  // 統(tǒng)計相同請求的次數(shù),在wg done之前寫入 
  12.  dups  int 
  13.   // 使用DoChan方法使用,用channel進(jìn)行通知 
  14.  chans []chan<- Result 
  15. // Dochan方法時使用 
  16. type Result struct { 
  17.  Val    interface{} // 存儲返回值 
  18.  Err    error // 存儲返回的錯誤信息 
  19.  Shared bool // 標(biāo)示結(jié)果是否是共享結(jié)果 

Do方法

 
 
 
 
  1. // 入?yún)ⅲ簁ey:標(biāo)識相同請求,fn:要執(zhí)行的函數(shù) 
  2. // 返回值:v: 返回結(jié)果 err: 執(zhí)行的函數(shù)錯誤信息 shard: 是否是共享結(jié)果 
  3. func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) { 
  4.  // 代碼塊加鎖 
  5.  g.mu.Lock() 
  6.  // map進(jìn)行懶加載 
  7.  if g.m == nil { 
  8.    // map初始化 
  9.   g.m = make(map[string]*call) 
  10.  } 
  11.  // 判斷是否有相同請求 
  12.  if c, ok := g.m[key]; ok { 
  13.    // 相同請求次數(shù)+1 
  14.   c.dups++ 
  15.   // 解鎖就好了,只需要等待執(zhí)行結(jié)果了,不會有寫入操作了 
  16.   g.mu.Unlock() 
  17.   // 已有請求在執(zhí)行,只需要等待就好了 
  18.   c.wg.Wait() 
  19.   // 區(qū)分panic錯誤和runtime錯誤 
  20.   if e, ok := c.err.(*panicError); ok { 
  21.    panic(e) 
  22.   } else if c.err == errGoexit { 
  23.    runtime.Goexit() 
  24.   } 
  25.   return c.val, c.err, true 
  26.  } 
  27.  // 之前沒有這個請求,則需要new一個指針類型 
  28.  c := new(call) 
  29.  // sync.waitgroup的用法,只有一個請求運行,其他請求等待,所以只需要add(1) 
  30.  c.wg.Add(1) 
  31.  // m賦值 
  32.  g.m[key] = c 
  33.  // 沒有寫入操作了,解鎖即可 
  34.  g.mu.Unlock() 
  35.  // 唯一的請求該去執(zhí)行函數(shù)了 
  36.  g.doCall(c, key, fn) 
  37.  return c.val, c.err, c.dups > 0 

這里是唯一有疑問的應(yīng)該是區(qū)分panic和runtime錯誤部分吧,這個與下面的docall方法有關(guān)聯(lián),看完docall你就知道為什么了。

docall

 
 
 
 
  1. // doCall handles the single call for a key. 
  2. func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) { 
  3.   // 標(biāo)識是否正常返回 
  4.  normalReturn := false 
  5.   // 標(biāo)識別是否發(fā)生panic 
  6.  recovered := false 
  7.    
  8.  defer func() { 
  9.   // 通過這個來判斷是否是runtime導(dǎo)致直接退出了 
  10.   if !normalReturn && !recovered { 
  11.       // 返回runtime錯誤信息 
  12.    c.err = errGoexit 
  13.   } 
  14.  
  15.   c.wg.Done() 
  16.   g.mu.Lock() 
  17.   defer g.mu.Unlock() 
  18.     // 防止重復(fù)刪除key 
  19.   if !c.forgotten { 
  20.    delete(g.m, key) 
  21.   } 
  22.   // 檢測是否出現(xiàn)了panic錯誤 
  23.   if e, ok := c.err.(*panicError); ok { 
  24.    // 如果是調(diào)用了dochan方法,為了channel避免死鎖,這個panic要直接拋出去,不能recover住,要不就隱藏錯誤了 
  25.    if len(c.chans) > 0 { 
  26.     go panic(e) // 開一個寫成panic 
  27.     select {} // 保持住這個goroutine,這樣可以將panic寫入crash dump 
  28.    } else { 
  29.     panic(e) 
  30.    } 
  31.   } else if c.err == errGoexit { 
  32.    // runtime錯誤不需要做任何時,已經(jīng)退出了 
  33.   } else { 
  34.    // 正常返回的話直接向channel寫入數(shù)據(jù)就可以了 
  35.    for _, ch := range c.chans { 
  36.     ch <- Result{c.val, c.err, c.dups > 0} 
  37.    } 
  38.   } 
  39.  }() 
  40.   // 使用匿名函數(shù)目的是recover住panic,返回信息給上層 
  41.  func() { 
  42.   defer func() { 
  43.    if !normalReturn { 
  44.     // 發(fā)生了panic,我們recover住,然后把錯誤信息返回給上層 
  45.     if r := recover(); r != nil { 
  46.      c.err = newPanicError(r) 
  47.     } 
  48.    } 
  49.   }() 
  50.   // 執(zhí)行函數(shù) 
  51.   c.val, c.err = fn() 
  52.     // fn沒有發(fā)生panic 
  53.   normalReturn = true 
  54.  }() 
  55.  // 判斷執(zhí)行函數(shù)是否發(fā)生panic 
  56.  if !normalReturn { 
  57.   recovered = true 
  58.  } 

這里來簡單描述一下為什么區(qū)分panic和runtime錯誤,不區(qū)分的情況下如果調(diào)用出現(xiàn)了恐慌,但是鎖沒有被釋放,會導(dǎo)致使用相同key的所有后續(xù)調(diào)用都出現(xiàn)了死鎖,具體可以查看這個issue:https://github.com/golang/go/issues/33519。

Dochan和Forget方法

 
 
 
 
  1. //異步返回 
  2. // 入?yún)?shù):key:標(biāo)識相同請求,fn:要執(zhí)行的函數(shù) 
  3. // 出參數(shù):<- chan 等待接收結(jié)果的channel 
  4. func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result { 
  5.   // 初始化channel 
  6.  ch := make(chan Result, 1) 
  7.  g.mu.Lock() 
  8.   // 懶加載 
  9.  if g.m == nil { 
  10.   g.m = make(map[string]*call) 
  11.  } 
  12.   // 判斷是否有相同的請求 
  13.  if c, ok := g.m[key]; ok { 
  14.     //相同請求數(shù)量+1 
  15.   c.dups++ 
  16.     // 添加等待的chan 
  17.   c.chans = append(c.chans, ch) 
  18.   g.mu.Unlock() 
  19.   return ch 
  20.  } 
  21.  c := &call{chans: []chan<- Result{ch}} 
  22.  c.wg.Add(1) 
  23.  g.m[key] = c 
  24.  g.mu.Unlock() 
  25.  // 開一個寫成調(diào)用 
  26.  go g.doCall(c, key, fn) 
  27.  // 返回這個channel等待接收數(shù)據(jù) 
  28.  return ch 
  29. // 釋放某個 key 下次調(diào)用就不會阻塞等待了 
  30. func (g *Group) Forget(key string) { 
  31.  g.mu.Lock() 
  32.  if c, ok := g.m[key]; ok { 
  33.   c.forgotten = true 
  34.  } 
  35.  delete(g.m, key) 
  36.  g.mu.Unlock() 

注意事項

因為我們在使用singleflight時需要自己寫執(zhí)行函數(shù),所以如果我們寫的執(zhí)行函數(shù)一直循環(huán)住了,就會導(dǎo)致我們的整個程序處于循環(huán)的狀態(tài),積累越來越多的請求,所以在使用時,還是要注意一點的,比如這個例子:

 
 
 
 
  1. result, err, _ := d.singleGroup.Do(key, func() (interface{}, error) { 
  2.   for{ 
  3.    // TODO 
  4.   } 

不過這個問題一般也不會發(fā)生,我們在日常開發(fā)中都會使用context控制超時。

總結(jié)

 

好啦,這篇文章就到這里啦。因為最近我在項目中也使用singleflight這個庫,所以就看了一下源碼實現(xiàn),真的是厲害,這么短的代碼就實現(xiàn)了這么重要的功能,我怎么就想不到呢。所以說還是要多讀一些源碼庫,真的能學(xué)到好多,真是應(yīng)了那句話:你知道的越多,不知道的就越多!


網(wǎng)站題目:讓我們一起賞析Singleflight設(shè)計
文章URL:http://www.5511xx.com/article/ccssjio.html