自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

讓我們一起賞析Singleflight設(shè)計(jì)

開發(fā) 開發(fā)工具
今天想與大家分享一下singleflight這個(gè)庫,singleflight僅僅只有100多行卻可以做到防止緩存擊穿,有點(diǎn)厲害哦!所以本文我們就一起來看一看他是怎么設(shè)計(jì)的~。

[[411393]]

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

前言

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

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

緩存擊穿

什么是緩存擊穿

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

如何解決

  • 方法一

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

  • 方法二

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

  • 方法三

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

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

截屏2021-07-14 下午8.30.56

源碼賞析

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

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

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

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

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

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

Do方法

  1. // 入?yún)ⅲ?/span>key:標(biāo)識(shí)相同請求,fn:要執(zhí)行的函數(shù) 
  2. // 返回值:v: 返回結(jié)果 err: 執(zhí)行的函數(shù)錯(cuò)誤信息 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é)果了,不會(huì)有寫入操作了 
  16.   g.mu.Unlock() 
  17.   // 已有請求在執(zhí)行,只需要等待就好了 
  18.   c.wg.Wait() 
  19.   // 區(qū)分panic錯(cuò)誤和runtime錯(cuò)誤 
  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.  // 之前沒有這個(gè)請求,則需要new一個(gè)指針類型 
  28.  c := new(call) 
  29.  // sync.waitgroup的用法,只有一個(gè)請求運(yùn)行,其他請求等待,所以只需要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錯(cuò)誤部分吧,這個(gè)與下面的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)識(shí)是否正常返回 
  4.  normalReturn := false 
  5.   // 標(biāo)識(shí)別是否發(fā)生panic 
  6.  recovered := false 
  7.    
  8.  defer func() { 
  9.   // 通過這個(gè)來判斷是否是runtime導(dǎo)致直接退出了 
  10.   if !normalReturn && !recovered { 
  11.       // 返回runtime錯(cuò)誤信息 
  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錯(cuò)誤 
  23.   if e, ok := c.err.(*panicError); ok { 
  24.    // 如果是調(diào)用了dochan方法,為了channel避免死鎖,這個(gè)panic要直接拋出去,不能recover住,要不就隱藏錯(cuò)誤了 
  25.    if len(c.chans) > 0 { 
  26.     go panic(e) // 開一個(gè)寫成panic 
  27.     select {} // 保持住這個(gè)goroutine,這樣可以將panic寫入crash dump 
  28.    } else { 
  29.     panic(e) 
  30.    } 
  31.   } else if c.err == errGoexit { 
  32.    // runtime錯(cuò)誤不需要做任何時(shí),已經(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,我們r(jià)ecover住,然后把錯(cuò)誤信息返回給上層 
  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錯(cuò)誤,不區(qū)分的情況下如果調(diào)用出現(xiàn)了恐慌,但是鎖沒有被釋放,會(huì)導(dǎo)致使用相同key的所有后續(xù)調(diào)用都出現(xiàn)了死鎖,具體可以查看這個(gè)issue:https://github.com/golang/go/issues/33519。

Dochan和Forget方法

  1. //異步返回 
  2. // 入?yún)?shù):key:標(biāo)識(shí)相同請求,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.  // 開一個(gè)寫成調(diào)用 
  26.  go g.doCall(c, key, fn) 
  27.  // 返回這個(gè)channel等待接收數(shù)據(jù) 
  28.  return ch 
  29. // 釋放某個(gè) key 下次調(diào)用就不會(huì)阻塞等待了 
  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() 

注意事項(xiàng)

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

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

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

總結(jié)

 

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

 

責(zé)任編輯:武曉燕 來源: Golang夢工廠
相關(guān)推薦

2022-03-31 18:59:43

數(shù)據(jù)庫InnoDBMySQL

2021-08-27 07:06:10

IOJava抽象

2021-12-29 08:27:05

ByteBuffer磁盤服務(wù)器

2022-03-08 17:52:58

TCP格式IP

2021-11-26 07:00:05

反轉(zhuǎn)整數(shù)數(shù)字

2022-06-26 09:40:55

Django框架服務(wù)

2022-02-14 07:03:31

網(wǎng)站安全MFA

2016-09-06 10:39:30

Dell Techno

2022-02-14 10:16:22

Axios接口HTTP

2023-08-14 08:38:26

反射reflect結(jié)構(gòu)體

2022-07-10 23:15:46

Go語言內(nèi)存

2023-08-02 08:35:54

文件操作數(shù)據(jù)源

2022-08-01 07:57:03

數(shù)組操作內(nèi)存

2021-07-31 11:40:55

Openresty開源

2012-04-14 20:47:45

Android

2021-12-16 12:01:21

區(qū)塊鏈Libra貨幣

2021-11-09 23:54:19

開發(fā)SMI Linkerd

2014-02-25 08:59:14

2022-12-05 09:10:21

2021-03-18 00:04:13

C# 類型數(shù)據(jù)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)