精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

教你打造高性能的 Go 緩存庫

開發(fā) 前端
本文會通過模仿它寫一個簡單的緩存庫,從而研究其內核是如何實現(xiàn)這樣的目標的。希望各位能有所收獲。

我在看一些優(yōu)秀的開源庫的時候看到一個有意思的緩存庫 fastcache,在它的介紹主要有以下幾點特點:

  • 讀寫數(shù)據(jù)要快,即使在并發(fā)下;
  • 即使在數(shù) GB 的緩存中,也要保持很好的性能,以及盡可能減少 GC 次數(shù);
  • 設計盡可能簡單;

本文會通過模仿它寫一個簡單的緩存庫,從而研究其內核是如何實現(xiàn)這樣的目標的。希望各位能有所收獲。

設計思想

在項目中,我們經(jīng)常會用到 Go 緩存庫比如說 patrickmn/go-cache庫。但很多緩存庫其實都是用一個簡單的 Map 來存放數(shù)據(jù),這些庫在使用的時候,當并發(fā)低,數(shù)據(jù)量少的時候是沒有問題的,但是在數(shù)據(jù)量比較大并發(fā)比較高的時候會延長 GC 時間,增加內存分配次數(shù)。

比如,我們使用一個簡單的例子:

 

  1. func main() { 
  2.  a := make(map[string]string, 1e9) 
  3.  for i := 0; i < 10; i++ { 
  4.   runtime.GC() 
  5.  } 
  6.  runtime.KeepAlive(a) 

在這個例子中,預分配了大小是10億(1e9) 的 map,然后我們通過 gctrace 輸出一下 GC 情況:

做實驗的環(huán)境是 Linux,機器配置是 16C 8G ,想要更深入理解 GC,可以看這篇:《 Go 語言 GC 實現(xiàn)原理及源碼分析 https://www.luozhiyun.com/archives/475 》

 

  1. [root@localhost gotest]# GODEBUG=gctrace=1 go run main.go 
  2. ... 
  3. gc 6 @13.736s 17%: 0.010+1815+0.004 ms clock, 0.17+0/7254/21744+0.067 ms cpu, 73984->73984->73984 MB, 147968 MB goal, 16 P (forced) 
  4. gc 7 @15.551s 18%: 0.012+1796+0.005 ms clock, 0.20+0/7184/21537+0.082 ms cpu, 73984->73984->73984 MB, 147968 MB goal, 16 P (forced) 
  5. gc 8 @17.348s 19%: 0.008+1794+0.004 ms clock, 0.14+0/7176/21512+0.070 ms cpu, 73984->73984->73984 MB, 147968 MB goal, 16 P (forced) 
  6. gc 9 @19.143s 19%: 0.010+1819+0.005 ms clock, 0.16+0/7275/21745+0.085 ms cpu, 73984->73984->73984 MB, 147968 MB goal, 16 P (forced) 
  7. gc 10 @20.963s 19%: 0.011+1844+0.004 ms clock, 0.18+0/7373/22057+0.076 ms cpu, 73984->73984->73984 MB, 147968 MB goal, 16 P (forced) 

上面展示了最后 5 次 GC 的情況,下面我們看看具體的含義是什么:

 

  1. gc 1 @0.004s 4%: 0.22+1.4+0.021 ms clock, 1.7+0.009/0.40/0.073+0.16 ms cpu, 4->5->1 MB, 5 MB goal, 8 P  
  2. gc 10 @20.963s 19%: 0.011+1844+0.004 ms clock, 0.18+0/7373/22057+0.076 ms cpu, 73984->73984->73984 MB, 147968 MB goal, 16 P (forced) 
  3.  
  4. gc 10 :程序啟動以來第10次GC  
  5. @20.963s:距離程序啟動到現(xiàn)在的時間  
  6. 19%:當目前為止,GC 的標記工作所用的CPU時間占總CPU的百分比 

垃圾回收的時間

 

  1. gc 1 @0.004s 4%: 0.22+1.4+0.021 ms clock, 1.7+0.009/0.40/0.073+0.16 ms cpu, 4->5->1 MB, 5 MB goal, 8 P 
  2.  
  3. gc 10 @20.963s 19%: 0.011+1844+0.004 ms clock, 0.18+0/7373/22057+0.076 ms cpu, 73984->73984->73984 MB, 147968 MB goal, 16 P (forced) 
  4.  
  5. gc 10 :程序啟動以來第10次GC 
  6. @20.963s:距離程序啟動到現(xiàn)在的時間 
  7. 19%:當目前為止,GC 的標記工作所用的CPU時間占總CPU的百分比 
  8.  
  9. 垃圾回收的時間 
  10. 0.011 ms:標記開始 STW 時間 
  11. 1844 ms:并發(fā)標記時間 
  12. 0.004 ms:標記終止 STW 時間 
  13.  
  14. 垃圾回收占用cpu時間 
  15. 0.18 ms:標記開始 STW 時間 
  16. 0 ms:mutator assists占用的時間 
  17. 7373 ms:標記線程占用的時間 
  18. 22057 ms:idle mark workers占用的時間 
  19. 0.076 ms:標記終止 STW 時間 
  20.  
  21. 內存 
  22. 73984 MB:標記開始前堆占用大小 
  23. 73984 MB:標記結束后堆占用大小 
  24. 73984 MB:標記完成后存活堆的大小 
  25. 147968 MB goal:標記完成后正在使用的堆內存的目標大小 
  26.  
  27. 16 P:使用了多少處理器 

可以從上面的輸出看到每次 GC 處理的時間非常的長,占用的 CPU 資源也非常多。那么造成這樣的原因是什么呢?

string 實際上底層數(shù)據(jù)結構是由兩部分組成,其中包含指向字節(jié)數(shù)組的指針和數(shù)組的大小:

 

  1. type StringHeader struct { 
  2.  Data uintptr 
  3.  Len  int 

由于 StringHeader中包含指針,所以每次 GC 的時候都會掃描每個指針,那么在這個巨大的 map中是包含了非常多的指針的,所以造成了巨大的資源消耗。

在上面的例子 map a 中數(shù)據(jù)大概是這樣存儲:

一個 map 中里面有多個 bucket ,bucket 里面有一個 bmap 數(shù)組用來存放數(shù)據(jù),但是由于 key 和 value 都是 string 類型的,所以在 GC 的時候還需要根據(jù) StringHeader中的 Data指針掃描 string 數(shù)據(jù)。

對于這種情況,如果所有的 string 字節(jié)都在一個單一內存片段中,我們就可以通過偏移來追蹤某個字符串在這段內存中的開始和結束位置。通過追蹤偏移,我們不在需要在我們大數(shù)組中存儲指針,GC 也不在會被困擾。如下:

如同上面所示,如果我們將字符串中的字節(jié)數(shù)據(jù)拷貝到一個連續(xù)的字節(jié)數(shù)組 chunks 中,并為這個字節(jié)數(shù)組提前分配好內存,并且僅存儲字符串在數(shù)組中的偏移而不是指針。

除了上面所說的優(yōu)化內容以外,還有其他的方法嗎?

其實我們還可以直接從系統(tǒng) OS 中調用 mmap syscall 進行內存分配,這樣 GC 就永遠不會對這塊內存進行內存管理,因此也就不會掃描到它。如下:

 

  1. func main() { 
  2.     test := "hello syscall" 
  3.  data, _ := syscall.Mmap(-1, 0, 13, syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_ANON|syscall.MAP_PRIVATE) 
  4.  p := (*[13]byte)(unsafe.Pointer(&data[0])) 
  5.  
  6.  for i := 0; i < 13; i++ { 
  7.   p[i] = test[i] 
  8.  } 
  9.  fmt.Println(string(p[:])) 

通過系統(tǒng)調用直接向 OS 申請了 13bytes 的內存,然后將一個字符串寫入到申請的內存數(shù)組中。

所以我們也可以通過提前向 OS 申請一塊內存,而不是用的時候才申請內存,減少頻繁的內存分配從而達到提高性能的目的。

源碼實戰(zhàn)

API

我們在開發(fā)前先把這個庫的 API 定義一下:

func New

  1. func New(maxBytes int) *Cache 

創(chuàng)建一個 Cache 結構體,傳入預設的緩存大小,單位是字節(jié)。

func (*Cache) Get

  1. func (c *Cache) Get(k []byte) []byte 

獲取 Cache 中的值,傳入的參數(shù)是 byte 數(shù)組。

func (*Cache) Set

  1. func (c *Cache) Set(k, v []byte) 

設置鍵值對到緩存中,k 是鍵,v 是值,參數(shù)都是 byte 數(shù)組。

結構體

 

  1. const bucketsCount = 512 
  2.  
  3. type Cache struct { 
  4.  buckets [bucketsCount]bucket 
  5.  
  6. type bucket struct { 
  7.  // 讀寫鎖 
  8.  mu sync.RWMutex 
  9.  
  10.  // 二維數(shù)組,存放數(shù)據(jù)的地方,是一個環(huán)形鏈表 
  11.  chunks [][]byte 
  12.  
  13.  // 索引字典 
  14.  m map[uint64]uint64 
  15.  
  16.  // 索引值 
  17.  idx uint64 
  18.  
  19.  // chunks 被重寫的次數(shù),用來校驗環(huán)形鏈表中數(shù)據(jù)有效性 
  20.  gen uint64 

通過我們上面的分析,可以看到,實際上真正存放數(shù)據(jù)的地方是 chunks 二維數(shù)組,在實現(xiàn)上是通過 m 字段來映射索引路徑,根據(jù) chunks 和 gen 兩個字段來構建一個環(huán)形鏈表,環(huán)形鏈表每轉一圈 gen 就會加一。

初始化

 

  1. func New(maxBytes int) *Cache { 
  2.  if maxBytes <= 0 { 
  3.   panic(fmt.Errorf("maxBytes must be greater than 0; got %d", maxBytes)) 
  4.  } 
  5.  var c Cache 
  6.  // 算出每個桶的大小 
  7.  maxBucketBytes := uint64((maxBytes + bucketsCount - 1) / bucketsCount) 
  8.  for i := range c.buckets[:] { 
  9.     // 對桶進行初始化 
  10.   c.buckets[i].Init(maxBucketBytes) 
  11.  } 
  12.  return &c 

我們會設置一個 New 函數(shù)來初始化我們 Cache 結構體,在 Cache 結構體中會將緩存的數(shù)據(jù)大小平均分配到每個桶中,然后對每個桶進行初始化。

 

  1. const bucketSizeBits = 40 
  2. const maxBucketSize uint64 = 1 << bucketSizeBits 
  3. const chunkSize = 64 * 1024 
  4.  
  5. func (b *bucket) Init(maxBytes uint64) { 
  6.  if maxBytes == 0 { 
  7.   panic(fmt.Errorf("maxBytes cannot be zero")) 
  8.  } 
  9.   // 我們這里限制每個桶最大的大小是 1024 GB 
  10.  if maxBytes >= maxBucketSize { 
  11.   panic(fmt.Errorf("too big maxBytes=%d; should be smaller than %d", maxBytes, maxBucketSize)) 
  12.  } 
  13.  // 初始化 Chunks 中每個 Chunk 大小為 64 KB,計算 chunk 數(shù)量 
  14.  maxChunks := (maxBytes + chunkSize - 1) / chunkSize 
  15.  b.chunks = make([][]byte, maxChunks) 
  16.  b.m = make(map[uint64]uint64) 
  17.     // 初始化 bucket 結構體 
  18.  b.Reset() 

在這里會將桶里面的內存按 chunk 進行分配,每個 chunk 占用內存約為 64 KB。在最后會調用 bucket 的 Reset 方法對 bucket 結構體進行初始化。

 

  1. func (b *bucket) Reset() { 
  2.  b.mu.Lock() 
  3.  chunks := b.chunks 
  4.  // 遍歷 chunks 
  5.  for i := range chunks { 
  6.   // 將 chunk 中的內存歸還到緩存中 
  7.   putChunk(chunks[i]) 
  8.   chunks[i] = nil 
  9.  } 
  10.  // 刪除索引字典中所有的數(shù)據(jù) 
  11.  bm := b.m 
  12.  for k := range bm { 
  13.   delete(bm, k) 
  14.  } 
  15.  b.idx = 0 
  16.  b.gen = 1 
  17.  b.mu.Unlock() 

Reset 方法十分簡單,主要就是清空 chunks 數(shù)組、刪除索引字典中所有的數(shù)據(jù)以及重置索引 idx 和 gen 的值。

在上面這個方法中有一個 putChunk ,其實這個就是直接操作我們提前向 OS 申請好的內存,相應的還有一個 getChunk 方法。下面我們具體看看 Chunk 的操作。

Chunk 操作

getChunk

 

  1. const chunksPerAlloc = 1024 
  2. const chunkSize = 64 * 1024 
  3.  
  4. var ( 
  5.  freeChunks     []*[chunkSize]byte 
  6.  freeChunksLock sync.Mutex 
  7.  
  8. func getChunk() []byte { 
  9.  freeChunksLock.Lock() 
  10.  if len(freeChunks) == 0 { 
  11.   // 分配  64 * 1024 * 1024 = 64 MB 內存 
  12.   data, err := syscall.Mmap(-1, 0, chunkSize*chunksPerAlloc, syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_ANON|syscall.MAP_PRIVATE) 
  13.   if err != nil { 
  14.    panic(fmt.Errorf("cannot allocate %d bytes via mmap: %s", chunkSize*chunksPerAlloc, err)) 
  15.   } 
  16.         // 循環(huán)遍歷 data 數(shù)據(jù) 
  17.   for len(data) > 0 { 
  18.    //將從系統(tǒng)分配的內存分為 64 * 1024 = 64 KB 大小,存放到 freeChunks中 
  19.    p := (*[chunkSize]byte)(unsafe.Pointer(&data[0])) 
  20.    freeChunks = append(freeChunks, p) 
  21.    data = data[chunkSize:] 
  22.   } 
  23.  } 
  24.  //從 freeChunks 獲取最后一個元素 
  25.  n := len(freeChunks) - 1 
  26.  p := freeChunks[n] 
  27.  freeChunks[n] = nil 
  28.  freeChunks = freeChunks[:n] 
  29.  freeChunksLock.Unlock() 
  30.  return p[:] 

初次調用 getChunk 函數(shù)時會使用系統(tǒng)調用分配 64MB 的內存,然后循環(huán)將內存切成 1024 份,每份 64KB 放入到 freeChunks 空閑列表中。然后獲取每次都獲取 freeChunks 空閑列表最后一個元素 64KB 內存返回。需要注意的是 getChunk 會下下面將要介紹到的 Cache 的 set 方法中使用到,所以需要考慮到并發(fā)問題,所以在這里加了鎖。

putChunk

 

  1. func putChunk(chunk []byte) { 
  2.  if chunk == nil { 
  3.   return 
  4.  } 
  5.  chunk = chunk[:chunkSize] 
  6.  p := (*[chunkSize]byte)(unsafe.Pointer(&chunk[0])) 
  7.  
  8.  freeChunksLock.Lock() 
  9.  freeChunks = append(freeChunks, p) 
  10.  freeChunksLock.Unlock() 

putChunk 函數(shù)就是將內存數(shù)據(jù)還回到 freeChunks 空閑列表中,會在 bucket 的 Reset 方法中被調用。

Set

 

  1. const bucketsCount = 512 
  2.  
  3. func (c *Cache) Set(k, v []byte) { 
  4.  h := xxhash.Sum64(k) 
  5.  idx := h % bucketsCount 
  6.  c.buckets[idx].Set(k, v, h) 

Set 方法里面會根據(jù) k 的值做一個 hash,然后取模映射到 buckets 桶中,這里用的 hash 庫是 cespare/xxhash。

最主要的還是 buckets 里面的 Set 方法:

 

  1. func (b *bucket) Set(k, v []byte, h uint64) { 
  2.  // 限定 k v 大小不能超過 2bytes 
  3.  if len(k) >= (1<<16) || len(v) >= (1<<16) { 
  4.   return 
  5.  } 
  6.  // 4個byte 設置每條數(shù)據(jù)的數(shù)據(jù)頭 
  7.  var kvLenBuf [4]byte 
  8.   kvLenBuf[0] = byte(uint16(len(k)) >> 8) 
  9.  kvLenBuf[1] = byte(len(k)) 
  10.  kvLenBuf[2] = byte(uint16(len(v)) >> 8) 
  11.  kvLenBuf[3] = byte(len(v)) 
  12.  kvLen := uint64(len(kvLenBuf) + len(k) + len(v)) 
  13.  // 校驗一下大小 
  14.  if kvLen >= chunkSize { 
  15.   return 
  16.  } 
  17.  
  18.  b.mu.Lock() 
  19.  // 當前索引位置 
  20.  idx := b.idx 
  21.  // 存放完數(shù)據(jù)后索引的位置 
  22.  idxNew := idx + kvLen 
  23.  // 根據(jù)索引找到在 chunks 的位置 
  24.  chunkIdx := idx / chunkSize 
  25.  chunkIdxNew := idxNew / chunkSize 
  26.  // 新的索引是否超過當前索引 
  27.  // 因為還有chunkIdx等于chunkIdxNew情況,所以需要先判斷一下 
  28.  if chunkIdxNew > chunkIdx { 
  29.   // 校驗是否新索引已到chunks數(shù)組的邊界 
  30.   // 已到邊界,那么循環(huán)鏈表從頭開始 
  31.   if chunkIdxNew >= uint64(len(b.chunks)) { 
  32.    idx = 0 
  33.    idxNew = kvLen 
  34.    chunkIdx = 0 
  35.    b.gen++ 
  36.    // 當 gen 等于 1<<genSizeBits時,才會等于0 
  37.    // 也就是用來限定 gen 的邊界為1<<genSizeBits 
  38.    if b.gen&((1<<genSizeBits)-1) == 0 { 
  39.     b.gen++ 
  40.    } 
  41.   } else { 
  42.    // 未到 chunks數(shù)組的邊界,從下一個chunk開始 
  43.    idx = chunkIdxNew * chunkSize 
  44.    idxNew = idx + kvLen 
  45.    chunkIdx = chunkIdxNew 
  46.   } 
  47.   // 重置 chunks[chunkIdx] 
  48.   b.chunks[chunkIdx] = b.chunks[chunkIdx][:0] 
  49.  } 
  50.  chunk := b.chunks[chunkIdx] 
  51.  if chunk == nil { 
  52.   chunk = getChunk() 
  53.   // 清空切片 
  54.   chunk = chunk[:0] 
  55.  } 
  56.  // 將數(shù)據(jù) append 到 chunk 中 
  57.  chunk = append(chunk, kvLenBuf[:]...) 
  58.  chunk = append(chunk, k...) 
  59.  chunk = append(chunk, v...) 
  60.  b.chunks[chunkIdx] = chunk 
  61.  // 因為 idx 不能超過bucketSizeBits,所以用一個 uint64 同時表示gen和idx 
  62.  // 所以高于bucketSizeBits位置表示gen 
  63.  // 低于bucketSizeBits位置表示idx 
  64.  b.m[h] = idx | (b.gen << bucketSizeBits) 
  65.  b.idx = idxNew 
  66.  b.mu.Unlock() 
  1. 在這段代碼開頭實際上我會限制鍵值的大小不能超過 2bytes;
  2. 然后將 2bytes 大小長度的鍵值封裝到 4bytes 的 kvLenBuf 作為數(shù)據(jù)頭,數(shù)據(jù)頭和鍵值的總長度是不能超過一個 chunk 長度,也就是 64 * 1024;
  3. 然后計算出原索引 chunkIdx 和新索引 chunkIdxNew,用來判斷這次添加的數(shù)據(jù)加上原來的數(shù)據(jù)有沒有超過一個 chunk 長度;
  4. 根據(jù)新的索引找到對應的 chunks 中的位置,然后將鍵值以及 kvLenBuf 追加到 chunk 后面;
  5. 設置新的 idx 以及 m 字典對應的值,m 字典中存放的是 gen 和 idx 通過取與的放置存放。

在 Set 一個鍵值對會有 4bytes 的 kvLenBuf 作為數(shù)據(jù)頭,后面的數(shù)據(jù)會接著 key 和 value ,在 kvLenBuf 中,前兩個 byte 分別代表了 key 長度的低位和高位;后兩個 byte 分別代表了 value 長度的低位和高位,數(shù)據(jù)圖大致如下:

下面舉個例子來看看是是如何利用 chunks 這個二維數(shù)組來實現(xiàn)環(huán)形鏈表的。

我們在 bucket 的 Init 方法中會根據(jù)傳入 maxBytes 桶字節(jié)數(shù)來設置 chunks 的長度大小,由于每個 chunk 大小都是 64 * 1024bytes,那么我們設置 3 * 64 * 1024bytes 大小的桶,那么 chunks 數(shù)組長度就為 3。

如果當前算出 chunkIdx 在 chunks 數(shù)組為 1 的位置,并且在 chunks[1] 的位置中,還剩下 6bytes 未被使用,那么有如下幾種情況:

現(xiàn)在假設放入的鍵值長度都是 1byte,那么在 chunks[1] 的位置中剩下的 6bytes 剛好可以放下;

現(xiàn)在假設放入的鍵值長度超過了 1byte,那么在 chunks[1] 的位置中剩下的位置就放不下,只能放入到 chunks[2] 的位置中。

如果當前算出 chunkIdx 在 chunks 數(shù)組為 2 的位置,并且現(xiàn)在 Set 一個鍵值,經(jīng)過計算 chunkIdxNew 為 3,已經(jīng)超過了 chunks 數(shù)組長度,那么會將索引重置,重新將數(shù)據(jù)從 chunks[0] 開始放置,并將 gen 加一,表示已經(jīng)跑完一圈了。

Get

 

  1. func (c *Cache) Get(dst, k []byte) []byte { 
  2.    h := xxhash.Sum64(k) 
  3.    idx := h % bucketsCount 
  4.    dst, _ = c.buckets[idx].Get(dst, k, h, true
  5.    return dst 

這里和 Set 方法是一樣的,首先是要找到對應的桶的位置,然后才去桶里面拿數(shù)據(jù)。需要注意的是,這里的 dst 可以從外部傳入一個切片,以達到減少重復分配返回值。

 

  1. func (b *bucket) Get(dst, k []byte, h uint64,returnDst bool) ([]byte, bool) { 
  2.  found := false 
  3.  b.mu.RLock() 
  4.  v := b.m[h] 
  5.  bGen := b.gen & ((1 << genSizeBits) - 1) 
  6.  if v > 0 { 
  7.   // 高于bucketSizeBits位置表示gen 
  8.   gen := v >> bucketSizeBits 
  9.   // 低于bucketSizeBits位置表示idx 
  10.   idx := v & ((1 << bucketSizeBits) - 1) 
  11.   // 這里說明chunks還沒被寫滿 
  12.   if gen == bGen && idx < b.idx || 
  13.    // 這里說明chunks已被寫滿,并且當前數(shù)據(jù)沒有被覆蓋 
  14.    gen+1 == bGen && idx >= b.idx || 
  15.    // 這里是邊界條件gen已是最大,并且chunks已被寫滿bGen從1開始,,并且當前數(shù)據(jù)沒有被覆蓋 
  16.    gen == maxGen && bGen == 1 && idx >= b.idx { 
  17.    chunkIdx := idx / chunkSize 
  18.    // chunk 索引位置不能超過 chunks 數(shù)組長度 
  19.    if chunkIdx >= uint64(len(b.chunks)) { 
  20.     goto end 
  21.    } 
  22.    // 找到數(shù)據(jù)所在的 chunk 
  23.    chunk := b.chunks[chunkIdx] 
  24.    // 通過取模找到該key 對應的數(shù)據(jù)在 chunk 中的位置 
  25.    idx %= chunkSize 
  26.    if idx+4 >= chunkSize { 
  27.     goto end 
  28.    } 
  29.    // 前 4bytes 是數(shù)據(jù)頭 
  30.    kvLenBuf := chunk[idx : idx+4] 
  31.    // 通過數(shù)據(jù)頭算出鍵值的長度 
  32.    keyLen := (uint64(kvLenBuf[0]) << 8) | uint64(kvLenBuf[1]) 
  33.    valLen := (uint64(kvLenBuf[2]) << 8) | uint64(kvLenBuf[3]) 
  34.    idx += 4 
  35.    if idx+keyLen+valLen >= chunkSize { 
  36.     goto end 
  37.    } 
  38.    // 如果鍵值是一致的,表示找到該數(shù)據(jù) 
  39.    if string(k) == string(chunk[idx:idx+keyLen]) { 
  40.     idx += keyLen 
  41.     // 返回該鍵對應的值 
  42.     if returnDst { 
  43.      dst = append(dst, chunk[idx:idx+valLen]...) 
  44.     } 
  45.     found = true 
  46.    } 
  47.   } 
  48.  } 
  49. end
  50.  b.mu.RUnlock() 
  51.  return dst, found 

Get 方法主要是考慮環(huán)形鏈表的邊界問題。我們在 Set 方法中會將每一個 key 對應的 gen 和 idx 索引存放到 m 字典中,所以我們通過 hash 獲取 m 字典的值之后通過位運算就可以獲取到 gen 和 idx 索引。

找到 gen 和 idx 索引之后就是邊界條件的判斷了,用一個 if 條件來進行判斷:

  1. gen == bGen && idx < b.idx 

這里是判斷如果是在環(huán)形鏈表的同一次循環(huán)中,那么 key 對應的索引應該小于當前桶的索引;

  1. gen+1 == bGen && idx >= b.idx 

這里表示當前桶已經(jīng)進入到下一個循環(huán)中,所以需要判斷 key 對應的索引是不是大于當前索引,以表示當前 key 對應的值沒有被覆蓋;

  1. gen == maxGen && bGen == 1 && idx >= b.idx 

因為 gen 和 idx 索引要塞到 uint64 類型的字段中,所以留給 gen 的最大值只有 maxGen = 1<< 24 -1,超過了 maxGen 會讓 gen 從 1 開始。所以這里如果 key 對應 gen 等于 maxGen ,那么當前的 bGen 應該等于 1,并且 key 對應的索引還應該大于當前 idx,這樣才這個鍵值對才不會被覆蓋。

判斷完邊界條件之后就會找到對應的 chunk ,然后取模后找到數(shù)據(jù)位置,通過偏移量找到并取出值。

Benchmark

下面我上一下過后的 Benchmark:

代碼位置: https://github.com/devYun/mycache/blob/main/cache_timing_test.go

 

  1. GOMAXPROCS=4 go test -bench='Set|Get' -benchtime=10s 
  2. goos: linux 
  3. goarch: amd64 
  4. pkg: gotest 
  5. // GoCache 
  6. BenchmarkGoCacheSet-4                836          14595822 ns/op           4.49 MB/s     2167340 B/op    65576 allocs/op 
  7. BenchmarkGoCacheGet-4               3093           3619730 ns/op          18.11 MB/s        5194 B/op       23 allocs/op 
  8. BenchmarkGoCacheSetGet-4             236          54379268 ns/op           2.41 MB/s     2345868 B/op    65679 allocs/op 
  9. // BigCache 
  10. BenchmarkBigCacheSet-4              1393          12763995 ns/op           5.13 MB/s     6691115 B/op        8 allocs/op 
  11. BenchmarkBigCacheGet-4              2526           4342561 ns/op          15.09 MB/s      650870 B/op   131074 allocs/op 
  12. BenchmarkBigCacheSetGet-4           1063          11180201 ns/op          11.72 MB/s     4778699 B/op   131081 allocs/op 
  13. // standard map 
  14. BenchmarkStdMapSet-4                1484           7299296 ns/op           8.98 MB/s      270603 B/op    65537 allocs/op 
  15. BenchmarkStdMapGet-4                4278           2480523 ns/op          26.42 MB/s        2998 B/op       15 allocs/op 
  16. BenchmarkStdMapSetGet-4              343          39367319 ns/op           3.33 MB/s      298764 B/op    65543 allocs/op 
  17. // sync.map 
  18. BenchmarkSyncMapSet-4                756          15951363 ns/op           4.11 MB/s     3420214 B/op   262320 allocs/op 
  19. BenchmarkSyncMapGet-4              11826           1010283 ns/op          64.87 MB/s        1075 B/op       33 allocs/op 
  20. BenchmarkSyncMapSetGet-4            1910           5507036 ns/op          23.80 MB/s     3412764 B/op   262213 allocs/op 
  21. PASS 
  22. ok      gotest  215.182s 

上面的測試是 GoCache、BigCache、Map、sync.Map 的情況。下面是本篇文章中所開發(fā)的緩存庫的測試:

 

  1. // myCachce 
  2. BenchmarkCacheSet-4                 4371           2723208 ns/op          24.07 MB/s        1306 B/op        2 allocs/op 
  3. BenchmarkCacheGet-4                 6003           1884611 ns/op          34.77 MB/s         951 B/op        1 allocs/op 
  4. BenchmarkCacheSetGet-4              2044           6611759 ns/op          19.82 MB/s        2797 B/op        5 allocs/op 

可以看到內存分配是幾乎就不存在,操作速度在上面的庫中也是佼佼者的存在。

總結

在本文中根據(jù)其他緩存庫,并分析了如果用 Map 作為緩存所存在的問題,然后引出存在這個問題的原因,并提出解決方案;在我們的緩存庫中,第一是通過使用索引加內存塊的方式來存放緩存數(shù)據(jù),再來是通過 OS 系統(tǒng)調用來進行內存分配讓我們的緩存數(shù)據(jù)塊脫離了 GC 的控制,從而做到降低 GC 頻率提高并發(fā)的目的。

其實不只是緩存庫,在我們的項目中當遇到需要使用大量的帶指針的數(shù)據(jù)結構并需要長時間保持引用的時候,也是需要注意這樣做可能會引發(fā) GC 問題,從而給系統(tǒng)帶來隱患。

責任編輯:未麗燕 來源: 知乎
相關推薦

2021-08-13 09:06:52

Go高性能優(yōu)化

2022-03-21 14:13:22

Go語言編程

2024-12-25 14:03:03

2023-09-18 09:10:11

Golang高性能緩存庫

2023-03-10 09:11:52

高性能Go堆棧

2017-09-18 01:21:05

美團IDC集群銳捷網(wǎng)絡

2024-02-26 11:03:05

golang緩存數(shù)據(jù)庫

2011-07-01 09:36:30

高性能Web

2019-04-08 10:09:04

CPU緩存高性能

2011-08-30 15:21:36

Platform

2015-08-19 09:38:29

云集群高性能計算云計算

2024-04-28 10:17:30

gnetGo語言

2012-01-11 15:15:59

用戶體驗高性能

2014-07-04 10:41:19

redis數(shù)據(jù)庫緩存

2019-03-14 15:38:19

ReactJavascript前端

2015-09-23 09:40:17

高性能Java應用

2010-05-18 16:47:40

智能網(wǎng)絡上海通用Radware

2023-12-01 07:06:14

Go命令行性能

2023-12-14 08:01:08

事件管理器Go

2015-03-27 11:42:44

日志管理PHPSeasLog
點贊
收藏

51CTO技術棧公眾號

jizz国产在线| 毛茸茸多毛bbb毛多视频| 国产写真视频在线观看| 粉嫩嫩av羞羞动漫久久久| 国内精品久久久久影院 日本资源| 美女一区二区三区视频| www免费在线观看| 麻豆精品精品国产自在97香蕉| 亚洲视频一区二区| 欧美,日韩,国产在线| 99reav在线| av在线一区二区| 国产主播在线一区| 性无码专区无码| 99久久精品费精品国产| 亚洲精品99999| 成人综合久久网| 在线高清av| 亚洲免费观看高清完整版在线观看熊| 91精品免费久久久久久久久| 粉嫩aⅴ一区二区三区| 欧美oldwomenvideos| 日韩国产欧美区| 欧美性猛交xxxx乱大交91| 国产伦精品一区二区三区视频金莲| 99精品欧美一区二区三区小说| 欧美又大又粗又长| 日韩毛片无码永久免费看| av男人一区| 欧美美女bb生活片| 成年人免费大片| xxx在线免费观看| 亚洲欧美日韩久久精品| 欧美亚州在线观看| 婷婷在线免费视频| 国产在线不卡一卡二卡三卡四卡| 亚洲成av人在线观看| 自拍偷拍亚洲色图欧美| 国产精品一区在线看| 99久久婷婷国产综合精品| 97影院在线午夜| 国产精品女人久久久| 伊人成人在线视频| 亚洲欧美色图片| 99re久久精品国产| 9l亚洲国产成人精品一区二三| 日韩欧美在线观看| 免费毛片网站在线观看| 成人午夜影视| 国产不卡视频一区| 91九色对白| www.国产欧美| 国产精品一二三| 亚洲www永久成人夜色| 一级黄色片网站| 另类欧美日韩国产在线| 国产日产欧美a一级在线| 亚洲一区二区影视| 国产精品婷婷| 91av在线免费观看| 天堂а√在线中文在线新版| 国产精品日本| 国产精品69精品一区二区三区| 午夜爱爱毛片xxxx视频免费看| 欧美日韩一区二区三区四区不卡 | 国产亚洲精品自在久久| www.久久成人| 成人免费电影视频| 久久99精品久久久久久久青青日本| 羞羞色院91蜜桃| 免费成人在线影院| 成人免费视频在线观看超级碰| 欧美激情亚洲综合| 免费视频一区| 国产精品狼人色视频一区| 国产精品无码粉嫩小泬| 一区二区久久| 国产国产精品人在线视| 日韩在线观看第一页| 久久精品系列| 国产美女久久久| jlzzjlzzjlzz亚洲人| 麻豆久久久久久| 成人一区二区电影| 免费的黄色av| 国产情人综合久久777777| 一本色道久久综合亚洲二区三区| 免费黄色片在线观看| 国产精品视频在线看| 热久久最新网址| 电影在线观看一区| 欧美日韩一区不卡| 欧洲熟妇的性久久久久久| 免费久久精品| 成人97在线观看视频| 久久国产视频播放| 精品亚洲国内自在自线福利| 国产精品国模大尺度私拍| 国产一区二区三区中文字幕| 日韩中文字幕亚洲一区二区va在线 | 成人激情视频在线| 日本精品999| 91一区二区在线| 久久精彩视频| www.久久ai| 色综合久久99| 国产av一区二区三区传媒| 懂色av一区二区| 综合网中文字幕| 日本熟妇成熟毛茸茸| 免费高清在线一区| 91精品国产综合久久久久久蜜臀 | 国产视频在线播放| 欧美日韩性生活视频| 伊人影院综合在线| 四虎影视精品| 欧美高清在线视频观看不卡| 日韩美女视频网站| 韩国精品久久久| 日本不卡二区| av午夜在线观看| 9191国产精品| 男人的天堂av网| 一区二区三区国产盗摄| 日韩av电影在线免费播放| 国内精品久久久久久久久久| 中文字幕欧美区| 男人揉女人奶房视频60分| 8x国产一区二区三区精品推荐| 亚洲第一黄色网| 极品魔鬼身材女神啪啪精品| 日日夜夜一区二区| 91久久精品一区二区别| 性高潮久久久久久久久久| 亚洲制服丝袜在线| 亚洲国产综合av| 91久久久精品国产| 国产精品一区二区女厕厕| 精品无人乱码| 色拍拍在线精品视频8848| 成年人的黄色片| 国产一区亚洲| 翡翠波斯猫1977年美国| 国产在线网站| 色婷婷综合久色| 精品少妇人妻一区二区黑料社区| 亚洲成人三区| 91久久国产精品| 香蕉视频黄色片| 午夜精品爽啪视频| 国产二级一片内射视频播放 | 蜜臀久久久99精品久久久久久| 成人免费看吃奶视频网站| 日本中文在线| 9191成人精品久久| 朝桐光av在线| 成人午夜激情在线| 欧美一区二区中文字幕| 日韩欧美国产大片| 人九九综合九九宗合| 精品视频一二区| 欧美日韩一级二级三级| 任我爽在线视频| 久久一区亚洲| 先锋影音一区二区三区| 青青国产精品| 欧美黑人xxxx| 性xxxxbbbb| 欧美午夜片在线观看| 岛国片在线免费观看| 国产综合久久久久久久久久久久| 热re99久久精品国99热蜜月| 国产经典三级在线| 日韩av有码在线| 国产精品尤物视频| 最新中文字幕一区二区三区| 伊人av在线播放| 香蕉久久国产| 好吊妞www.84com只有这里才有精品 | 高清不卡一区| 国精产品一区一区三区有限在线| 99久久精品国产成人一区二区| 国产亚洲污的网站| jizzzz日本| 国产精品红桃| 日韩.欧美.亚洲| 国产精选久久| 51ⅴ精品国产91久久久久久| 1769在线观看| 亚洲成人动漫在线播放| 国产一区免费看| 亚洲激情网站免费观看| 黄色a一级视频| 久久69国产一区二区蜜臀| 亚洲精品一区二区三| 深夜激情久久| 九色成人免费视频| 九九在线视频| 亚洲精品一区二区精华| 在线观看av大片| 亚洲风情在线资源站| 18啪啪污污免费网站| 国产iv一区二区三区| 亚洲精品一二三四五区| 精品国产精品| 国产精品日韩欧美综合| 黄色在线观看www| 大胆欧美人体视频| porn视频在线观看| 欧美精品一级二级三级| 97在线观看视频免费| 99re免费视频精品全部| 日韩欧美中文在线视频| 日本大胆欧美人术艺术动态| 给我免费播放片在线观看| 日韩欧美高清一区二区三区| 91成人性视频| 日本动漫理论片在线观看网站 | 亚洲 欧美 激情 另类| 欧美午夜精品伦理| 欧美国产在线看| ...xxx性欧美| 99re6热在线精品视频| 91毛片在线观看| 亚洲av成人片无码| 久久精品官网| 国产精品成人久久电影| 91精品国产91久久综合| 日韩中文一区| 美女精品一区最新中文字幕一区二区三区| 欧美专区在线播放| av福利在线导航| 色综合久久久久久中文网| 亚洲av片在线观看| 欧美日韩精品一区二区三区蜜桃| 欧美爱爱小视频| 久久综合狠狠综合久久综合88| 亚洲欧美久久久久| 日韩精品1区2区3区| 2021国产视频| 一本一道久久a久久精品蜜桃| 久久99欧美| 九色丨蝌蚪丨成人| 精品不卡在线| 日韩高清电影免费| 国语精品中文字幕| 精品国产鲁一鲁****| 91视频九色网站| 国产精品视频一区二区三区综合 | 三级成人黄色影院| 麻豆乱码国产一区二区三区| 米奇精品一区二区三区| 久久国内精品一国内精品| 黄视频网站在线看| 美女av一区二区| 欧美色图天堂| 中文字幕最新精品| 黄色免费在线观看网站| 久久亚洲影音av资源网| 色在线视频网| 亚洲3p在线观看| 亚洲精品福利电影| 国产精品日韩欧美大师| 超碰国产精品一区二页| 亚洲影院色无极综合| 大奶一区二区三区| 欧美另类一区| 国产精品三p一区二区| 久久99欧美| 色97色成人| 成人在线免费高清视频| 精品成人久久| 成人免费观看毛片| 日本aⅴ亚洲精品中文乱码| 日本欧美黄色片| 久久精品人人| 在线播放黄色av| 不卡一卡二卡三乱码免费网站| 69久久精品无码一区二区| 丰满少妇久久久久久久| 韩国三级在线看| 国产又粗又猛又爽又黄91精品| 永久免费的av网站| 国产成人亚洲综合a∨婷婷图片 | 成人av在线播放| 激情欧美一区二区三区中文字幕| 97se亚洲国产一区二区三区| 蜜桃导航-精品导航| 羞羞色午夜精品一区二区三区| 一区二区三区四区五区视频| 九一成人免费视频| 日本不卡一二三区| 欧美日韩视频| 999在线免费视频| 国产98色在线|日韩| 免费看91的网站| 亚洲一区二区三区四区在线| 免费一级片视频| 日本韩国视频一区二区| 性中国古装videossex| 亚洲片在线资源| 丰满大乳少妇在线观看网站| 97久久精品在线| 国产精品日本一区二区三区在线| 成人免费视频视频在| 欧美在线电影| 国产黄页在线观看| 国产精品一区二区在线观看不卡 | 精品国产乱子伦| 欧美制服丝袜第一页| 亚洲女人18毛片水真多| 亚洲精品久久久久中文字幕欢迎你| 日韩精品系列| 欧美激情一区二区三区在线视频观看 | 国产日韩欧美视频在线观看| 亚洲欧美国产一本综合首页| 日本资源在线| 91人人爽人人爽人人精88v| 精品久久美女| 黄色一级片播放| 成人黄色在线看| 欧美日韩在线视频免费播放| 欧美日韩精品欧美日韩精品一| 国产情侣在线播放| 日韩中文第一页| 日本另类视频| 久久精品一区二区三区不卡免费视频| 欧美色图一区| 亚洲人成无码www久久久| 成人不卡免费av| 国产探花视频在线播放| 精品美女永久免费视频| 男人天堂一区二区| 欧美国产日韩中文字幕在线| 亚洲精选av| 一卡二卡三卡视频| 青青草原综合久久大伊人精品优势| av在线免费观看不卡| 亚洲欧美色综合| www.com在线观看| 欧美精品videosex性欧美| 天堂va欧美ⅴa亚洲va一国产| 蜜桃av久久久亚洲精品| 亚欧成人精品| 国产 欧美 在线| 欧美在线一二三| 91网页在线观看| 国产精品视频专区| 91欧美国产| 青青草精品在线| 亚洲午夜久久久久| 少妇人妻偷人精品一区二区| www亚洲欧美| 国产高清亚洲| 黄网站色视频免费观看| 美腿丝袜亚洲综合| 亚洲区一区二区三| 欧美一区午夜精品| 日本天码aⅴ片在线电影网站| 国产精品欧美日韩久久| 91麻豆国产自产在线观看亚洲 | 精品在线免费观看| 蜜臀久久精品久久久用户群体| 在线观看91精品国产入口| 成全电影播放在线观看国语| 91精品国产综合久久久久久久久 | 在线观看欧美日韩| 9999精品视频| 欧美成人高潮一二区在线看| 国内精品免费**视频| 青青草手机视频在线观看| 亚洲成色999久久网站| 成人福利视频| 亚洲午夜精品久久| 日韩高清电影一区| 国产熟妇搡bbbb搡bbbb| 欧洲中文字幕精品| 色呦呦久久久| 欧美影视一区二区| 国产精品一区二区x88av| 91在线看视频| 中文字幕9999| 国产精品xxx在线观看| 国产av人人夜夜澡人人爽麻豆| 国产成人激情av| 在线视频一区二区三区四区| 日韩视频在线一区| 精品亚洲免a| 九九九九九伊人| 岛国精品视频在线播放| 黄色网址在线免费播放| 久久国产精品久久精品国产| 激情综合网av| 黄色在线视频网址| 欧美成人免费全部| 九色精品91| 日韩黄色一区二区| 欧美日韩视频一区二区| 粉嫩av在线播放| 91九色国产社区在线观看|