真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

怎么打造高性能的Go緩存庫(kù)

本篇內(nèi)容主要講解“怎么打造高性能的 Go 緩存庫(kù)”,感興趣的朋友不妨來看看。本文介紹的方法操作簡(jiǎn)單快捷,實(shí)用性強(qiáng)。下面就讓小編來帶大家學(xué)習(xí)“怎么打造高性能的 Go 緩存庫(kù)”吧!

成都創(chuàng)新互聯(lián)總部坐落于成都市區(qū),致力網(wǎng)站建設(shè)服務(wù)有網(wǎng)站建設(shè)、成都網(wǎng)站設(shè)計(jì)、網(wǎng)絡(luò)營(yíng)銷策劃、網(wǎng)頁(yè)設(shè)計(jì)、網(wǎng)站維護(hù)、公眾號(hào)搭建、小程序制作、軟件開發(fā)等為企業(yè)提供一整套的信息化建設(shè)解決方案。創(chuàng)造真正意義上的網(wǎng)站建設(shè),為互聯(lián)網(wǎng)品牌在互動(dòng)行銷領(lǐng)域創(chuàng)造價(jià)值而不懈努力!

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

  • 讀寫數(shù)據(jù)要快,即使在并發(fā)下;

  • 即使在數(shù) GB 的緩存中,也要保持很好的性能,以及盡可能減少 GC 次數(shù);

  • 設(shè)計(jì)盡可能簡(jiǎn)單;

本文會(huì)通過模仿它寫一個(gè)簡(jiǎn)單的緩存庫(kù),從而研究其內(nèi)核是如何實(shí)現(xiàn)這樣的目標(biāo)的。希望各位能有所收獲。

設(shè)計(jì)思想

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

比如,我們使用一個(gè)簡(jiǎn)單的例子:

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

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

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

[root@localhost gotest]# GODEBUG=gctrace=1 go run main.go ... 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) 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) 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) 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) 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 的情況,下面我們看看具體的含義是什么:

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  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)  gc 10 :程序啟動(dòng)以來第10次GC  @20.963s:距離程序啟動(dòng)到現(xiàn)在的時(shí)間  19%:當(dāng)目前為止,GC 的標(biāo)記工作所用的CPU時(shí)間占總CPU的百分比

垃圾回收的時(shí)間

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  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)  gc 10 :程序啟動(dòng)以來第10次GC @20.963s:距離程序啟動(dòng)到現(xiàn)在的時(shí)間 19%:當(dāng)目前為止,GC 的標(biāo)記工作所用的CPU時(shí)間占總CPU的百分比  垃圾回收的時(shí)間 0.011 ms:標(biāo)記開始 STW 時(shí)間 1844 ms:并發(fā)標(biāo)記時(shí)間 0.004 ms:標(biāo)記終止 STW 時(shí)間  垃圾回收占用cpu時(shí)間 0.18 ms:標(biāo)記開始 STW 時(shí)間 0 ms:mutator assists占用的時(shí)間 7373 ms:標(biāo)記線程占用的時(shí)間 22057 ms:idle mark workers占用的時(shí)間 0.076 ms:標(biāo)記終止 STW 時(shí)間  內(nèi)存 73984 MB:標(biāo)記開始前堆占用大小 73984 MB:標(biāo)記結(jié)束后堆占用大小 73984 MB:標(biāo)記完成后存活堆的大小 147968 MB goal:標(biāo)記完成后正在使用的堆內(nèi)存的目標(biāo)大小  16 P:使用了多少處理器

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

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

type StringHeader struct {  Data uintptr  Len  int }

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

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

怎么打造高性能的 Go 緩存庫(kù)

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

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

怎么打造高性能的 Go 緩存庫(kù)

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

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

其實(shí)我們還可以直接從系統(tǒng) OS 中調(diào)用 mmap syscall 進(jìn)行內(nèi)存分配,這樣 GC  就永遠(yuǎn)不會(huì)對(duì)這塊內(nèi)存進(jìn)行內(nèi)存管理,因此也就不會(huì)掃描到它。如下:

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

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

所以我們也可以通過提前向 OS 申請(qǐng)一塊內(nèi)存,而不是用的時(shí)候才申請(qǐng)內(nèi)存,減少頻繁的內(nèi)存分配從而達(dá)到提高性能的目的。

源碼實(shí)戰(zhàn)

API

我們?cè)陂_發(fā)前先把這個(gè)庫(kù)的 API 定義一下:

func New

func New(maxBytes int) *Cache

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

func (*Cache) Get

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

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

func (*Cache) Set

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

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

結(jié)構(gòu)體

const bucketsCount = 512  type Cache struct {  buckets [bucketsCount]bucket }  type bucket struct {  // 讀寫鎖  mu sync.RWMutex   // 二維數(shù)組,存放數(shù)據(jù)的地方,是一個(gè)環(huán)形鏈表  chunks [][]byte   // 索引字典  m map[uint64]uint64   // 索引值  idx uint64   // chunks 被重寫的次數(shù),用來校驗(yàn)環(huán)形鏈表中數(shù)據(jù)有效性  gen uint64 }

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

怎么打造高性能的 Go 緩存庫(kù)

初始化

func New(maxBytes int) *Cache {  if maxBytes <= 0 {   panic(fmt.Errorf("maxBytes must be greater than 0; got %d", maxBytes))  }  var c Cache  // 算出每個(gè)桶的大小  maxBucketBytes := uint64((maxBytes + bucketsCount - 1) / bucketsCount)  for i := range c.buckets[:] {     // 對(duì)桶進(jìn)行初始化   c.buckets[i].Init(maxBucketBytes)  }  return &c }

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

const bucketSizeBits = 40 const maxBucketSize uint64 = 1 << bucketSizeBits const chunkSize = 64 * 1024  func (b *bucket) Init(maxBytes uint64) {  if maxBytes == 0 {   panic(fmt.Errorf("maxBytes cannot be zero"))  }   // 我們這里限制每個(gè)桶最大的大小是 1024 GB  if maxBytes >= maxBucketSize {   panic(fmt.Errorf("too big maxBytes=%d; should be smaller than %d", maxBytes, maxBucketSize))  }  // 初始化 Chunks 中每個(gè) Chunk 大小為 64 KB,計(jì)算 chunk 數(shù)量  maxChunks := (maxBytes + chunkSize - 1) / chunkSize  b.chunks = make([][]byte, maxChunks)  b.m = make(map[uint64]uint64)     // 初始化 bucket 結(jié)構(gòu)體  b.Reset() }

在這里會(huì)將桶里面的內(nèi)存按 chunk 進(jìn)行分配,每個(gè) chunk 占用內(nèi)存約為 64 KB。在最后會(huì)調(diào)用 bucket 的 Reset 方法對(duì)  bucket 結(jié)構(gòu)體進(jìn)行初始化。

func (b *bucket) Reset() {  b.mu.Lock()  chunks := b.chunks  // 遍歷 chunks  for i := range chunks {   // 將 chunk 中的內(nèi)存歸還到緩存中   putChunk(chunks[i])   chunks[i] = nil  }  // 刪除索引字典中所有的數(shù)據(jù)  bm := b.m  for k := range bm {   delete(bm, k)  }  b.idx = 0  b.gen = 1  b.mu.Unlock() }

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

在上面這個(gè)方法中有一個(gè) putChunk ,其實(shí)這個(gè)就是直接操作我們提前向 OS 申請(qǐng)好的內(nèi)存,相應(yīng)的還有一個(gè) getChunk 方法。下面我們具體看看  Chunk 的操作。

Chunk 操作

getChunk

const chunksPerAlloc = 1024 const chunkSize = 64 * 1024  var (  freeChunks     []*[chunkSize]byte  freeChunksLock sync.Mutex )  func getChunk() []byte {  freeChunksLock.Lock()  if len(freeChunks) == 0 {   // 分配  64 * 1024 * 1024 = 64 MB 內(nèi)存   data, err := syscall.Mmap(-1, 0, chunkSize*chunksPerAlloc, syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_ANON|syscall.MAP_PRIVATE)   if err != nil {    panic(fmt.Errorf("cannot allocate %d bytes via mmap: %s", chunkSize*chunksPerAlloc, err))   }         // 循環(huán)遍歷 data 數(shù)據(jù)   for len(data) > 0 {    //將從系統(tǒng)分配的內(nèi)存分為 64 * 1024 = 64 KB 大小,存放到 freeChunks中    p := (*[chunkSize]byte)(unsafe.Pointer(&data[0]))    freeChunks = append(freeChunks, p)    data = data[chunkSize:]   }  }  //從 freeChunks 獲取最后一個(gè)元素  n := len(freeChunks) - 1  p := freeChunks[n]  freeChunks[n] = nil  freeChunks = freeChunks[:n]  freeChunksLock.Unlock()  return p[:] }

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

putChunk

func putChunk(chunk []byte) {  if chunk == nil {   return  }  chunk = chunk[:chunkSize]  p := (*[chunkSize]byte)(unsafe.Pointer(&chunk[0]))   freeChunksLock.Lock()  freeChunks = append(freeChunks, p)  freeChunksLock.Unlock() }

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

Set

const bucketsCount = 512  func (c *Cache) Set(k, v []byte) {  h := xxhash.Sum64(k)  idx := h % bucketsCount  c.buckets[idx].Set(k, v, h) }

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

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

func (b *bucket) Set(k, v []byte, h uint64) {  // 限定 k v 大小不能超過 2bytes  if len(k) >= (1<<16) || len(v) >= (1<<16) {   return  }  // 4個(gè)byte 設(shè)置每條數(shù)據(jù)的數(shù)據(jù)頭  var kvLenBuf [4]byte   kvLenBuf[0] = byte(uint16(len(k)) >> 8)  kvLenBuf[1] = byte(len(k))  kvLenBuf[2] = byte(uint16(len(v)) >> 8)  kvLenBuf[3] = byte(len(v))  kvLen := uint64(len(kvLenBuf) + len(k) + len(v))  // 校驗(yàn)一下大小  if kvLen >= chunkSize {   return  }   b.mu.Lock()  // 當(dāng)前索引位置  idx := b.idx  // 存放完數(shù)據(jù)后索引的位置  idxNew := idx + kvLen  // 根據(jù)索引找到在 chunks 的位置  chunkIdx := idx / chunkSize  chunkIdxNew := idxNew / chunkSize  // 新的索引是否超過當(dāng)前索引  // 因?yàn)檫€有chunkIdx等于chunkIdxNew情況,所以需要先判斷一下  if chunkIdxNew > chunkIdx {   // 校驗(yàn)是否新索引已到chunks數(shù)組的邊界   // 已到邊界,那么循環(huán)鏈表從頭開始   if chunkIdxNew >= uint64(len(b.chunks)) {    idx = 0    idxNew = kvLen    chunkIdx = 0    b.gen++    // 當(dāng) gen 等于 1<
  1. 鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)

  2. 在這段代碼開頭實(shí)際上我會(huì)限制鍵值的大小不能超過 2bytes;

  3. 然后將 2bytes 大小長(zhǎng)度的鍵值封裝到 4bytes 的 kvLenBuf 作為數(shù)據(jù)頭,數(shù)據(jù)頭和鍵值的總長(zhǎng)度是不能超過一個(gè) chunk 長(zhǎng)度,也就是  64 * 1024;

  4. 然后計(jì)算出原索引 chunkIdx 和新索引 chunkIdxNew,用來判斷這次添加的數(shù)據(jù)加上原來的數(shù)據(jù)有沒有超過一個(gè) chunk 長(zhǎng)度;

  5. 根據(jù)新的索引找到對(duì)應(yīng)的 chunks 中的位置,然后將鍵值以及 kvLenBuf 追加到 chunk 后面;

  6. 設(shè)置新的 idx 以及 m 字典對(duì)應(yīng)的值,m 字典中存放的是 gen 和 idx 通過取與的放置存放。

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

怎么打造高性能的 Go 緩存庫(kù)

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

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

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

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

怎么打造高性能的 Go 緩存庫(kù)

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

怎么打造高性能的 Go 緩存庫(kù)

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

怎么打造高性能的 Go 緩存庫(kù)

Get

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

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

func (b *bucket) Get(dst, k []byte, h uint64,returnDst bool) ([]byte, bool) {  found := false  b.mu.RLock()  v := b.m[h]  bGen := b.gen & ((1 << genSizeBits) - 1)  if v > 0 {   // 高于bucketSizeBits位置表示gen   gen := v >> bucketSizeBits   // 低于bucketSizeBits位置表示idx   idx := v & ((1 << bucketSizeBits) - 1)   // 這里說明chunks還沒被寫滿   if gen == bGen && idx < b.idx ||    // 這里說明chunks已被寫滿,并且當(dāng)前數(shù)據(jù)沒有被覆蓋    gen+1 == bGen && idx >= b.idx ||    // 這里是邊界條件gen已是最大,并且chunks已被寫滿bGen從1開始,,并且當(dāng)前數(shù)據(jù)沒有被覆蓋    gen == maxGen && bGen == 1 && idx >= b.idx {    chunkIdx := idx / chunkSize    // chunk 索引位置不能超過 chunks 數(shù)組長(zhǎng)度    if chunkIdx >= uint64(len(b.chunks)) {     goto end    }    // 找到數(shù)據(jù)所在的 chunk    chunk := b.chunks[chunkIdx]    // 通過取模找到該key 對(duì)應(yīng)的數(shù)據(jù)在 chunk 中的位置    idx %= chunkSize    if idx+4 >= chunkSize {     goto end    }    // 前 4bytes 是數(shù)據(jù)頭    kvLenBuf := chunk[idx : idx+4]    // 通過數(shù)據(jù)頭算出鍵值的長(zhǎng)度    keyLen := (uint64(kvLenBuf[0]) << 8) | uint64(kvLenBuf[1])    valLen := (uint64(kvLenBuf[2]) << 8) | uint64(kvLenBuf[3])    idx += 4    if idx+keyLen+valLen >= chunkSize {     goto end    }    // 如果鍵值是一致的,表示找到該數(shù)據(jù)    if string(k) == string(chunk[idx:idx+keyLen]) {     idx += keyLen     // 返回該鍵對(duì)應(yīng)的值     if returnDst {      dst = append(dst, chunk[idx:idx+valLen]...)     }     found = true    }   }  } end:  b.mu.RUnlock()  return dst, found }

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

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

gen == bGen && idx < b.idx

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

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

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

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

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

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

怎么打造高性能的 Go 緩存庫(kù)

Benchmark

下面我上一下過后的 Benchmark:

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

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

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

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

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

總結(jié)

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

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

到此,相信大家對(duì)“怎么打造高性能的 Go 緩存庫(kù)”有了更深的了解,不妨來實(shí)際操作一番吧!這里是創(chuàng)新互聯(lián)網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!


名稱欄目:怎么打造高性能的Go緩存庫(kù)
鏈接地址:http://weahome.cn/article/pcdosi.html

其他資訊

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部