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

Golang 高性能無(wú) GC 的緩存庫(kù) bigcache 是怎么實(shí)現(xiàn)的?

開(kāi)發(fā) 后端 數(shù)據(jù)庫(kù)
假設(shè)我要在某個(gè)服務(wù)應(yīng)用里實(shí)現(xiàn)一個(gè)緩存組件去存各種類(lèi)型的數(shù)據(jù),該怎么實(shí)現(xiàn)這個(gè)組件呢?

我們寫(xiě)代碼的時(shí)候,經(jīng)常會(huì)需要從數(shù)據(jù)庫(kù)里讀取一些數(shù)據(jù),比如配置信息或者諸如每周熱點(diǎn)商品之類(lèi)的數(shù)據(jù)。

應(yīng)用讀取數(shù)據(jù)庫(kù)

如果這些數(shù)據(jù)既不經(jīng)常變化,又需要頻繁讀取,那比起每次都去讀數(shù)據(jù)庫(kù),更優(yōu)的解決方案就是將它們放到應(yīng)用的本地內(nèi)存里,這樣可以省下不少數(shù)據(jù)庫(kù) IO,性能嘎一下就上來(lái)了。

應(yīng)用優(yōu)先讀緩存

那么現(xiàn)在問(wèn)題就來(lái)了,假設(shè)我要在某個(gè)服務(wù)應(yīng)用里實(shí)現(xiàn)一個(gè)緩存組件去存各種類(lèi)型的數(shù)據(jù),該怎么實(shí)現(xiàn)這個(gè)組件呢?

從一個(gè) map 說(shuō)起

最簡(jiǎn)單的的方案就是使用 map,也就是字典,將需要保存的結(jié)構(gòu)以 key-value 的形式,保存到內(nèi)存中。比如系統(tǒng)配置,key 就叫 system_config,value 就是具體的配置內(nèi)容。需要讀取數(shù)據(jù)就用 v = m[key]來(lái)獲取數(shù)據(jù),需要寫(xiě)數(shù)據(jù)就執(zhí)行m[key] = v.

單線程讀寫(xiě)map

這樣看起來(lái)在單線程下是滿足需求了。但如果我想在多個(gè)線程(協(xié)程)里并發(fā)讀寫(xiě)這個(gè)緩存呢?那必然會(huì)發(fā)生競(jìng)態(tài)問(wèn)題。這就需要加個(gè)讀寫(xiě)鎖了。讀操作前后要加鎖和解鎖,也就是改成下面這樣。

RLock()
v = m[key]
RUnLock()

寫(xiě)操作也需要相應(yīng)修改:

Lock()
m[key] = v
UnLock()

多線程加鎖讀寫(xiě)map

這在讀寫(xiě)不頻繁的場(chǎng)景下是完全 ok 的,如果沒(méi)有什么性能要求,服務(wù)也沒(méi)出現(xiàn)什么瓶頸,就算新來(lái)的實(shí)習(xí)生笑它很 low,你也要有自信,這就是個(gè)好用的緩存組件。架構(gòu)就是這樣,能快速滿足需求,不出錯(cuò)就行。

但其實(shí)這個(gè)方案其實(shí)也有很大的問(wèn)題,如果讀寫(xiě) qps 非常高,那么就會(huì)有一堆請(qǐng)求爭(zhēng)搶同一個(gè) map 鎖,這對(duì)性能影響太大了。怎么解決呢?

將鎖粒度變小

上面的方案中,最大的問(wèn)題是所有讀寫(xiě)請(qǐng)求,都搶的同一個(gè)鎖,所以競(jìng)爭(zhēng)才大,如果能將一部分請(qǐng)求改為搶 A 鎖,另一部分請(qǐng)求改為搶 B 鎖,那競(jìng)爭(zhēng)就變小了。于是,我們可以將原來(lái)的一個(gè) map,進(jìn)行分片,變成多個(gè) map,每個(gè) map 都有自己的鎖。發(fā)生讀寫(xiě)操作時(shí),第一步先對(duì) key 進(jìn)行 hash 分片,獲取分片對(duì)應(yīng)的鎖后,再對(duì)分片 map 進(jìn)行讀寫(xiě)。只有落在同一個(gè)分片的請(qǐng)求才會(huì)發(fā)生鎖爭(zhēng)搶。也就是說(shuō) map 拆的越細(xì),鎖競(jìng)爭(zhēng)就越小。

分片鎖

像這種將資源分割成多個(gè)獨(dú)立的分片(segments/shard),每個(gè)段都有一個(gè)對(duì)應(yīng)的鎖來(lái)控制并發(fā)訪問(wèn)的控制機(jī)制, 其實(shí)就是所謂的分片(段)鎖??雌饋?lái)很完美,但其實(shí)還有問(wèn)題。

gc 帶來(lái)的問(wèn)題

像 C/C++這類(lèi)語(yǔ)言中,用戶(hù)申請(qǐng)的內(nèi)存需要由用戶(hù)自己寫(xiě)代碼去釋放,一不小心忘了釋放那就會(huì)發(fā)生內(nèi)存泄露,給程序員帶來(lái)了很大的心智負(fù)擔(dān)。為了避免這樣的問(wèn)題,一般高級(jí)語(yǔ)言里都會(huì)自帶 GC,也就是垃圾回收(Garbage Collection),說(shuō)白了就是程序員只管申請(qǐng)內(nèi)存,用完了系統(tǒng)會(huì)自動(dòng)回收釋放這些內(nèi)存。比如 golang,它會(huì)每隔一段時(shí)間就去掃描哪些變量?jī)?nèi)存是可以被回收的。對(duì)于指針類(lèi)型,golang 會(huì)先掃指針,再掃描指針指向的對(duì)象里的內(nèi)容。map緩存里放的東西少還好說(shuō),緩存里的 key-value 一多,那就喜提多遍瘋狂掃描,浪費(fèi),全是浪費(fèi),golang 你糊涂啊。

gc掃描指針對(duì)象

那有沒(méi)有辦法可以減少這部分 gc 掃描 成本呢?有。golang 對(duì)于key 和 value 都不含指針的的map,會(huì)選擇跳過(guò),不進(jìn)行 gc 掃描。所以我們需要想辦法將 map 里的內(nèi)容改成完全不含指針。原來(lái) map 中放的 key-value,key和value 都可能是指針結(jié)構(gòu)體。

1.對(duì)于 key

原來(lái) key 是用的字符串,在 golang 中字符串本質(zhì)上也是指針,于是我們將它進(jìn)行 hash 操作,將字符串轉(zhuǎn)為整形。信息經(jīng)過(guò) hash 操作后,有可能會(huì)丟掉部分信息,為了避免hash沖突時(shí)分不清具體是哪個(gè) key-value,我們會(huì)將 key 放到 value 中一起處理,繼續(xù)看下面。

2.對(duì)于 value

我們可以構(gòu)造一個(gè)超大的 byte 數(shù)組 buf,將原來(lái)的 key value 等信息經(jīng)過(guò)序列化,變成二進(jìn)制01串。將它存放到這個(gè)超大 buf 中,并記錄它在 超大 buf 中的位置 index。然后將這個(gè)位置 index 信息放到 map 的 value 位置上,也就是從 key-velue,變成了 key-index。

引入buf減少gc掃描

同時(shí)為了防止 buf 數(shù)組變得過(guò)大,占用過(guò)多內(nèi)存導(dǎo)致應(yīng)用oom,還可以采用 ringbuf 的結(jié)構(gòu),寫(xiě)到尾部就重頭開(kāi)始寫(xiě),如果 ringbuf 空間不夠,還能對(duì)它進(jìn)行擴(kuò)容。

ringbuf擴(kuò)容

3.寫(xiě)操作

對(duì)于寫(xiě)操作,程序先將 key 進(jìn)行 hash,得到所在分片 map,加鎖。

  • 如果不能從分片 map 里拿到 index,也就是 map 中沒(méi)舊數(shù)據(jù),那就找到 ringbuf 里的空位置后寫(xiě)入 value,再將index寫(xiě)入map。
  • 如果能從分片 map 里拿到 index,也就是 map 中有舊數(shù)據(jù),那就覆蓋寫(xiě) ringbuf。

然后解鎖,結(jié)束流程。

寫(xiě)分片map流程

4.讀操作

對(duì)于讀操作,程序同樣先對(duì) key 進(jìn)行 hash,得到分片 map。加鎖,從分片 map 里拿到 value 對(duì)應(yīng)的 index,拿著這個(gè) index 到 ringbuf 數(shù)組中去獲取到 value 的值,然后解鎖,結(jié)束流程。

讀分片map流程

到這里,我們可以發(fā)現(xiàn) map 的 key 和 value 都被改成了整形數(shù)字,也就省下了大量的 gc 掃描,大大提升了組件性能。其實(shí)這就是有名的高性能無(wú) GC 的緩存庫(kù) github.com/allegro/bigcache 的實(shí)現(xiàn)原理。

bigcache 的使用

它的使用方法大概像下面這樣。

package main

import (
    "fmt"
    "github.com/allegro/bigcache/v3"
)

func main() {
    // 設(shè)置 bigcache 配置參數(shù)
    cacheConfig := bigcache.Config{
        Shards: 1024, // 分片數(shù)量,提高并發(fā)性
    }

    // 初始化 bigcache
    cache, _ := bigcache.NewBigCache(cacheConfig)

    // 寫(xiě)緩存數(shù)據(jù)
    key := "歡迎關(guān)注"
    value := []byte("小白debug")
    cache.Set(key, value)

    // 讀緩存數(shù)據(jù)
    entry, _ := cache.Get(key)

    fmt.Printf("Entry: %s\n", entry)
}

說(shuō)白了就是 Get 方法讀緩存數(shù)據(jù),Set 方法寫(xiě)緩存數(shù)據(jù),比較簡(jiǎn)單?,F(xiàn)在,大概原理和使用方法我們都懂了,我們?cè)賮?lái)看下 bigcache 中,兩個(gè)我認(rèn)為挺巧妙的設(shè)計(jì)點(diǎn)。

ringbuf 中的數(shù)據(jù)格式

在前面的介紹中,我猜你心里可能有疑問(wèn),程序從 ringbuf 讀寫(xiě) value 的時(shí)候,ringbuf里面放的都是 01 二進(jìn)制數(shù)組,程序怎么知道該讀多少bit才算一個(gè)完整 value?bigcache 的解法非常值得學(xué)習(xí),它重新定義了一個(gè)新的數(shù)據(jù)格式。

ringbuf內(nèi)數(shù)據(jù)格式

  • length 表示 header 到 data 的數(shù)據(jù)長(zhǎng)度
  • header 是固定長(zhǎng)度
  • data 則是 key 和 value 的完整數(shù)據(jù)。

當(dāng)讀取 ringbuf 時(shí),我們會(huì)先讀到 length,有了它,我們就能在 ringbuf 里拿到 header 和 data,header 里又含有 key 的長(zhǎng)度,這樣就能在 data 里將 key 和 value 完整區(qū)分開(kāi)來(lái)。

很多網(wǎng)絡(luò)傳輸框架中都會(huì)用到類(lèi)似的方案,后面有機(jī)會(huì)跟大家細(xì)聊。

ringbuffer 的第 0 位

另外,還有個(gè)巧妙的設(shè)計(jì)是,在 bigcache 中, ringbuffer 的第 0 位并不用來(lái)存放任何數(shù)據(jù),這樣如果發(fā)現(xiàn) 分片 map 中得到數(shù)據(jù)的 index 為 0,就可以直接認(rèn)為沒(méi)有對(duì)應(yīng)的緩存數(shù)據(jù),那就不需要跑到 ringbuffer 里去撈一遍數(shù)據(jù)了,覺(jué)得學(xué)到了,記得在右下角給我點(diǎn)個(gè)贊。

ringbuf不使用第0位

bigcache 的缺點(diǎn)

bigcache 性能非常好,但也不是完全沒(méi)有問(wèn)題。比較明顯的是,它讀寫(xiě)數(shù)據(jù)時(shí),用的都是byte數(shù)組,但我們平時(shí)寫(xiě)代碼用的都是結(jié)構(gòu)體,為了讓結(jié)構(gòu)體和 byte 數(shù)組互轉(zhuǎn),我們就需要用到序列化和反序列化,這些都是成本。

另外它的緩存淘汰策略也比較粗暴,用的是 FIFO,不支持 LRU 或 LFU 的淘汰策略。

總結(jié)

  • 對(duì)于不頻繁讀寫(xiě)的場(chǎng)景,加鎖讀寫(xiě) map 就夠了。
  • 對(duì)于需要頻繁讀寫(xiě)的場(chǎng)景,可以使用分片鎖,減少鎖競(jìng)爭(zhēng)。
  • 對(duì)于 golang,map 中含指針的話會(huì)引發(fā) gc 掃描,為了降低這部分成本,引入了 ringbuf,map 的 value 則改為緩存對(duì)象在 ringbuf 中的 index,以此提升組件性能。以后面試官問(wèn)你看沒(méi)看過(guò)哪些優(yōu)秀組件的源碼的時(shí)候,你知道該怎么回答了吧?
責(zé)任編輯:趙寧寧 來(lái)源: 小白debug
相關(guān)推薦

2023-09-18 09:10:11

Golang高性能緩存庫(kù)

2021-05-27 10:02:57

Go緩存數(shù)據(jù)

2019-04-08 10:09:04

CPU緩存高性能

2024-09-06 07:55:42

2020-11-10 07:46:09

服務(wù)器高并發(fā)高性能

2023-03-13 07:40:44

高并發(fā)golang

2019-01-02 16:50:30

Golang彈幕

2019-01-02 16:38:37

Golang彈幕

2019-01-02 16:47:46

Golang彈幕

2024-04-25 10:09:02

2019-11-11 15:33:34

高并發(fā)緩存數(shù)據(jù)

2019-08-14 15:08:51

緩存存儲(chǔ)數(shù)據(jù)

2024-02-01 09:21:08

RevoltPHP高性能

2024-01-05 07:38:55

2023-11-19 23:24:21

Golang開(kāi)發(fā)

2014-07-04 10:41:19

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

2019-03-14 15:38:19

ReactJavascript前端

2024-12-25 14:03:03

2022-06-29 08:55:46

orjsonPythonJSON

2024-02-26 07:43:10

大語(yǔ)言模型LLM推理框架
點(diǎn)贊
收藏

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