成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

讓我們一起賞析Singleflight設計

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

[[411393]]

本文轉載自微信公眾號「Golang夢工廠」,作者AsongGo。轉載本文請聯系Golang夢工廠公眾號。

前言

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

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

緩存擊穿

什么是緩存擊穿

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

如何解決

  • 方法一

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

  • 方法二

為了避免出現緩存擊穿的情況,我們可以在第一個請求去查詢數據庫的時候對他加一個互斥鎖,其余的查詢請求都會被阻塞住,直到鎖被釋放,后面的線程進來發現已經有緩存了,就直接走緩存,從而保護數據庫。但是也是由于它會阻塞其他的線程,此時系統吞吐量會下降。需要結合實際的業務去考慮是否要這么做。

  • 方法三

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

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

截屏2021-07-14 下午8.30.56

源碼賞析

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

數據結構

singleflight的結構定義如下:

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

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

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

Do方法

  1. // 入參:key:標識相同請求,fn:要執行的函數 
  2. // 返回值:v: 返回結果 err: 執行的函數錯誤信息 shard: 是否是共享結果 
  3. func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) { 
  4.  // 代碼塊加鎖 
  5.  g.mu.Lock() 
  6.  // map進行懶加載 
  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.    // 相同請求次數+1 
  14.   c.dups++ 
  15.   // 解鎖就好了,只需要等待執行結果了,不會有寫入操作了 
  16.   g.mu.Unlock() 
  17.   // 已有請求在執行,只需要等待就好了 
  18.   c.wg.Wait() 
  19.   // 區分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.  // 唯一的請求該去執行函數了 
  36.  g.doCall(c, key, fn) 
  37.  return c.val, c.err, c.dups > 0 

這里是唯一有疑問的應該是區分panic和runtime錯誤部分吧,這個與下面的docall方法有關聯,看完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.   // 標識是否正常返回 
  4.  normalReturn := false 
  5.   // 標識別是否發生panic 
  6.  recovered := false 
  7.    
  8.  defer func() { 
  9.   // 通過這個來判斷是否是runtime導致直接退出了 
  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.     // 防止重復刪除key 
  19.   if !c.forgotten { 
  20.    delete(g.m, key
  21.   } 
  22.   // 檢測是否出現了panic錯誤 
  23.   if e, ok := c.err.(*panicError); ok { 
  24.    // 如果是調用了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錯誤不需要做任何時,已經退出了 
  33.   } else { 
  34.    // 正常返回的話直接向channel寫入數據就可以了 
  35.    for _, ch := range c.chans { 
  36.     ch <- Result{c.val, c.err, c.dups > 0} 
  37.    } 
  38.   } 
  39.  }() 
  40.   // 使用匿名函數目的是recover住panic,返回信息給上層 
  41.  func() { 
  42.   defer func() { 
  43.    if !normalReturn { 
  44.     // 發生了panic,我們recover住,然后把錯誤信息返回給上層 
  45.     if r := recover(); r != nil { 
  46.      c.err = newPanicError(r) 
  47.     } 
  48.    } 
  49.   }() 
  50.   // 執行函數 
  51.   c.val, c.err = fn() 
  52.     // fn沒有發生panic 
  53.   normalReturn = true 
  54.  }() 
  55.  // 判斷執行函數是否發生panic 
  56.  if !normalReturn { 
  57.   recovered = true 
  58.  } 

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

Dochan和Forget方法

  1. //異步返回 
  2. // 入參數:key:標識相同請求,fn:要執行的函數 
  3. // 出參數:<- chan 等待接收結果的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.     //相同請求數量+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.  // 開一個寫成調用 
  26.  go g.doCall(c, key, fn) 
  27.  // 返回這個channel等待接收數據 
  28.  return ch 
  29. // 釋放某個 key 下次調用就不會阻塞等待了 
  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時需要自己寫執行函數,所以如果我們寫的執行函數一直循環住了,就會導致我們的整個程序處于循環的狀態,積累越來越多的請求,所以在使用時,還是要注意一點的,比如這個例子:

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

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

總結

 

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

 

責任編輯:武曉燕 來源: Golang夢工廠
相關推薦

2022-03-31 18:59:43

數據庫InnoDBMySQL

2021-08-27 07:06:10

IOJava抽象

2021-12-29 08:27:05

ByteBuffer磁盤服務器

2022-03-08 17:52:58

TCP格式IP

2021-11-26 07:00:05

反轉整數數字

2022-02-14 10:16:22

Axios接口HTTP

2022-02-14 07:03:31

網站安全MFA

2016-09-06 10:39:30

Dell Techno

2022-06-26 09:40:55

Django框架服務

2023-08-14 08:38:26

反射reflect結構體

2022-07-10 23:15:46

Go語言內存

2023-08-02 08:35:54

文件操作數據源

2022-08-01 07:57:03

數組操作內存

2012-04-14 20:47:45

Android

2021-07-31 11:40:55

Openresty開源

2021-12-16 12:01:21

區塊鏈Libra貨幣

2014-02-25 08:59:14

2021-11-09 23:54:19

開發SMI Linkerd

2022-12-05 09:10:21

2021-02-23 09:21:29

代碼效率C++
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 欧美久久电影 | 日韩国产一区二区三区 | 成人欧美一区二区三区 | 在线免费观看毛片 | 国产日韩一区二区三区 | 成人影院av | 无码国模国产在线观看 | 欧美日韩免费在线 | 81精品国产乱码久久久久久 | 国产高清一二三区 | 精品久久久一区 | 91视频网 | 亚洲免费在线观看 | 国产精品一区视频 | www.一区二区三区 | 欧美在线观看一区 | 中文字幕在线一区 | 国产色片在线 | 看片wwwwwwwwwww| 999久久久久久久 | 精品中文字幕视频 | 亚洲h在线观看 | 免费不卡av| 日韩欧美大片 | 成人18亚洲xxoo | 在线一区| 日韩精品一区二区三区四区 | 亚洲精品www| 欧美婷婷| 精品久久久久香蕉网 | 午夜精品久久久久99蜜 | 日韩午夜一区二区三区 | 麻豆天堂 | 涩涩视频在线观看 | 毛片免费在线 | 国产91丝袜在线18 | 久久久久久久久久久久久9999 | av国产在线观看 | 国产成人久久精品一区二区三区 | 精品动漫一区 | 成人免费在线观看 |