cache 2.單機并發緩存

0.對原教程的一些見解

個人認為原教程中兩點知識的引入不夠友好。

首先是只讀數據結構?ByteView?的引入使用是有點迷茫的,可能不能很好理解為什么需要ByteView。

第二是主體結構 Group的引入也疑惑。其實要是熟悉groupcache,那對結構Group的使用是清晰明白的。而看該教程的人可能是沒有了解過groupcache,直接就引入結構Group,可能不好理解。這一章節希望可以講明白這兩點。

1.統一的緩存的value對象

//該類型實現了NodeValue接口
type String stringfunc (d String) Len() int {return len(d)
}

在上節講解中, 我們存入的每一個元素(鍵值對)都要計算大小。為了能計算大小,那存入緩存的 value 對象必須實現NodeValue接口的Len()方法。上一節的測試用例中存儲的value對象是String(也即是string)。

那么問題來了, 我們存入的 value 可能是 string, int,?也可能自定義的結構體User等等。如果為每一種類型都實現一個 Len() 方法那確實是繁瑣。因此,我們希望將存入的每個 value 都轉化為統一的類型, 比如:字節數組 []byte。

我們可以抽象了一個只讀數據結構?ByteView?用來表示緩存值

ByteView 只有一個數據成員,b []byte,b 將會存儲真實的緩存值。

b?是只讀的,使用?ByteSlice()?方法返回一個拷貝,防止緩存值被外部程序修改。

//緩存值的抽象與封裝
type ByteView struct {b []byte
}func (v ByteView) Len() int {return len(v.b)
}func (v ByteView) ByteSlice() []byte {return cloneByte(v.b)
}func cloneByte(b []byte) []byte {c := make([]byte, len(b))copy(c, b)return c
}func (v ByteView) String() string {return string(v.b)
}

2.實現緩存并發讀寫

上一節實現的LRU算法是不支持并發讀寫的。Go中map不是線程安全的。要實現并發讀寫map,需要加鎖,可以使用sync.Mutex。

sync.Mutex 是一個互斥鎖,可以由不同的協程加鎖和解鎖。

先回顧下上一節定義的緩存的整體數據結構

type Cache struct {maxBytes  int64      //允許的能使用的最大內存nbytes    int64      //已使用的內存ll        *list.List //雙向鏈表cache     map[string]*list.ElementOnEvicted func(key string, value NodeValue)
}

要是想的簡單點,我們可以在該結構體Cache內部加上sync.Mutex并修改其方法的部分原有邏輯來實現并發讀寫。但這樣就破壞了對擴展開放,對修改關閉的面向對象原則。這是不好的。

?定義加鎖的緩存對象

我們可以在Cache結構體基礎上再封裝一個可以支持并發讀寫的對象。

type cache struct {mutex      sync.Mutexlru        *lru.CachecacheBytes int64
}

顯然,該新對象中是需要有個互斥鎖變量。而每個緩存對象都有能使用的最大內存量上限,使用cacheBytes?字段來存儲這個值。

該cache對象也基于互斥鎖和lru封裝了?get 和 add 方法。

func (c *cache) add(key string, value ByteView) {c.mutex.Lock()defer c.mutex.Unlock()if c.lru == nil {c.lru = lru.New(c.cacheBytes, nil)}c.lru.Add(key, value)
}func (c *cache) get(key string) (value ByteView, ok bool) {c.mutex.Lock()defer c.mutex.Unlock()if c.lru == nil {return}if v, ok := c.lru.Get(key); ok {return v.(ByteView), ok}return
}

3.提升緩存并發讀寫能力

互斥鎖引發的性能問題

引入鎖之后,可能會引起性能問題,思考如下場景:

當有 A個線程訪問庫存的緩存數據時, 我們給?cache?對象加了鎖, 如果此時有 B個線程來訪問商品緩存數據,這 A + B 個線程就需要共同競爭一把鎖。

要是線程數量大的話,對性能是有影響的,那是因為所有的緩存都被一把鎖把持住。那要是我們可以把緩存進行分組,這樣首先就可以不用所有的線程都去搶一把鎖了。

將緩存數據進行分組

為了提高緩存系統的并發讀寫的性能(降低鎖的競爭程度),?我們想想是否可以再細分鎖的范圍,分段鎖的設計。

可以理解成是先分段再鎖,將原本的所有緩存分成了若干段,分別將這若干段放在了不同的組中,每個組有各自的鎖,以此提高效率。

如此設計之后,?不同組的存緩數據就隔離了起來, 訪問同一組數據的線程才會互相競爭。

這就引出了Group這個結構。

4.Group結構

定義一個分組結構,從上圖也可知道,要去訪問緩存,就需去找到該組,那如何辨別是這個組呢,這里就是通過組的名字去辨別的,每個組都有個名字。

// 緊接著我們定義一個 分組 類型
type Group struct {name      string // 分組名稱mainCache cache  // 單個緩存對象
}

這時有多個組后,那如何通過組名字快速找到該組了?還是要用map。那肯定又涉及到多個線程并發讀寫?groups?。這里是找到對應組名字的組而加鎖的。我們可以考慮用?讀寫鎖?來解決這個問題。

這里使用讀寫鎖應該比使用互斥鎖可以提高并發度。

來看看創建組和通過名字獲取組的函數

var (rwMu   sync.RWMutexgroups = make(map[string]*Group)
)func NewGroup(name string, cacheBytes int64) *Group {rwMu.Lock()defer rwMu.Unlock()g := &Group{name:      name,mainCache: cache{cacheBytes: cacheBytes},}groups[name] = greturn g
}// 獲取 Group 對象的方法
func GetGroup(name string) *Group {rwMu.RLock()defer rwMu.RUnlock()g := groups[name]return g
}

緩存查詢回調方法

我們要考慮一種情況:如果緩存不存在,應從數據源(文件,數據庫等)獲取數據并添加到緩存中。

該Cache 是否應該支持多種數據源的配置呢?不應該,一是數據源的種類太多,沒辦法都實現;二是擴展性不好。如何從源頭獲取數據,應該是用戶決定的事情,我們就把這件事交給用戶好了。因此,我們設計了一個回調函數(callback),在緩存不存在時,就可以調用該函數,得到源數據。

這個回調方法我們可以直接定義在上面的 Get 方法的入參中,也可以放在 Group 對象中,為了方便,我們放在Group內。

type Group struct {name      string // 組名mainCache cache  // 單個緩存對象// 新增回調函數getter    Getter}type Getter interface {Get(key string) ([]byte, error)
}type GetterFunc func(key string) ([]byte, error)func (f GetterFunc) Get(key string) ([]byte, error) {return f(key)
}

?函數類型實現某一個接口,稱之為接口型函數,那么該函數也是接口。

其好處:當一個函數的參數類型是接口,那使用者在調用時既能夠傳入函數作為參數,也能夠傳入實現了該接口的結構體作為參數

接口型函數不太理解的話,可以看Go接口型函數。

接口型函數在這章節的最后測試中也會進行講解的,測試中有例子。

?Group 的 Get 方法

首先從本地緩存中查找,若是有則直接返回該緩存數據即可。

若是緩存不存在(即是沒擊中),則調用 load?方法,調用用戶回調函數?g.getter.Get()?獲取源數據,并且將源數據添加到緩存 mainCache 中。

func (g *Group) Get(key string) (ByteView, error) {if v, ok := g.mainCache.get(key); ok {return v, nil}return g.load(key)
}func (g *Group) load(key string) (ByteView, error) {bytes, err := g.getter.Get(key)if err != nil {return ByteView{}, err}value := ByteView{b: cloneByte(bytes)}g.mainCache.add(key, value)    //將源數據添加到緩存mainCachereturn value, nil
}

至此,這一章節的單機并發緩存就已經完成了。

5.測試

// 緩存中沒有的話,就從該db中查找
var db = map[string]string{"tom":  "100","jack": "200","sam":  "444",
}// 統計某個鍵調用回調函數的次數
var loadCounts = make(map[string]int, len(db))

創建 group 實例,并測試?Get?方法。

主要測試了兩種情況

  • 1)在緩存為空的情況下,能夠通過回調函數獲取到源數據。
  • 2)在緩存已經存在的情況下,是否直接從緩存中獲取,為了實現這一點,使用?loadCounts?統計某個鍵調用回調函數的次數,如果次數大于1,則表示調用了多次回調函數,沒有緩存。
func main() {//傳函數入參    cache.GetterFunc(funcCbGet)是進行類型轉換,不是執行函數cache := cache.NewGroup("scores", 2<<10, cache.GetterFunc(funcCbGet))//傳結構體入參,也可以// cbGet := &search{}// cache := cache.NewGroup("scores", 2<<10, cbGet)for k, v := range db {if view, err := cache.Get(k); err != nil || view.String() != v {fmt.Println("failed to get value of ",k)}if _, err := cache.Get(k); err != nil || loadCounts[k] > 1 {fmt.Printf("cache %s miss", k)}}if view, err := cache.Get("unknown"); err == nil {fmt.Printf("the value of unknow should be empty, but %s got", view)}else {fmt.Println(err)}
}// 函數的
func funcCbGet(key string) ([]byte, error) {fmt.Println("callback search key: ", key)if v, ok := db[key]; ok {if _, ok := loadCounts[key]; !ok {loadCounts[key] = 0}loadCounts[key] += 1return []byte(v), nil}return nil, fmt.Errorf("%s not exit", key)
}// 結構體,實現了Getter接口的Get方法,
type search struct {
}func (s *search) Get(key string) ([]byte, error) {fmt.Println("struct callback search key: ", key)if v, ok := db[key]; ok {if _, ok := loadCounts[key]; !ok {loadCounts[key] = 0}loadCounts[key] += 1return []byte(v), nil}return nil, fmt.Errorf("%s not exit", key)
}

討論接口型函數

NewGroup中的最后一個參數類型是接口類型。

這里既可以傳入函數,也可以傳入結構體變量。

而按照這個例子,傳入函數是很方便的。只寫一個函數就行,而做成結構體的話,還需要新建一個結構體類型,再實現Get方法,這就是很麻煩的。

這里可能就有疑惑了,大家通過這個例子明白,這樣做是既可以傳入函數,也可以傳入結構體變量。但從這例子來看,沒必要這樣做,就只是傳函數就行啦,沒必要把NewGroup的最后那個參數類型做成接口類型,只弄成函數類型就行啦。

這是這個例子的,要是在其他更加復雜的情況呢。比如:如果對數據庫的操作需要很多信息,地址、用戶名、密碼,還有很多中間狀態需要保持,比如超時、重連、加鎖等等。這種情況下,更適合將其封裝為一個結構體,再把該結構體傳入更好。

既能夠將普通的函數類型(需類型轉換)作為參數,也可以將結構體作為參數,使用更為靈活,可讀性也更好,這就是接口型函數的價值。

這樣就不用等我們想要用結構體傳參時候,發現類型不符合,傳參失敗就需要修改代碼,這時候就麻煩了。

完整代碼:https://github.com/liwook/Go-projects/tree/main/go-cache/2-single-node

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/208468.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/208468.shtml
英文地址,請注明出處:http://en.pswp.cn/news/208468.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

線性回歸與邏輯回歸:深入解析機器學習的基石模型

目錄 一、線性回歸 二、邏輯回歸 邏輯回歸算法和 KNN 算法的區別 分類算法評價維度

QT作業2

使用手動連接&#xff0c;將登錄框中的取消按鈕使用qt4版本的連接到自定義的槽函數中&#xff0c;在自定義的槽函數中調用關閉函數 將登錄按鈕使用qt5版本的連接到自定義的槽函數中&#xff0c;在槽函數中判斷ui界面上輸入的賬號是否為"admin"&#xff0c;密碼是否為…

Navicat 技術指引 | 適用于 GaussDB 分布式的數據查看器

Navicat Premium&#xff08;16.3.3 Windows 版或以上&#xff09;正式支持 GaussDB 分布式數據庫。GaussDB 分布式模式更適合對系統可用性和數據處理能力要求較高的場景。Navicat 工具不僅提供可視化數據查看和編輯功能&#xff0c;還提供強大的高階功能&#xff08;如模型、結…

微服務學習:Nacos微服務架構中的服務注冊、服務發現和動態配置Nacos下載

Nacos的主要用途包括&#xff1a; 服務注冊與發現&#xff1a;Nacos提供了服務注冊和發現的功能&#xff0c;服務提供者可以將自己的服務注冊到Nacos服務器上&#xff0c;服務消費者則可以通過Nacos來發現可用的服務實例&#xff0c;從而實現服務調用。 動態配置管理&#xff…

聚觀早報 |華為暢享 70正式開售;夢餉科技雙12玩法

【聚觀365】12月8日消息 華為暢享 70正式開售 夢餉科技雙12玩法 華為Mate X5應對火海挑戰 谷歌發布AI模型Gemini 字節跳動開啟新一輪回購 華為暢享 70正式開售 精致外觀與創新科技兼具的華為暢享 70正式開售&#xff0c;1199元起搭載6000mAh超大電池&#xff0c;帶來超強…

機器視覺相機鏡頭光源選型

鏡頭選型工具 - HiTools - 海康威視 Hikvisionhttps://www.hikvision.com/cn/support/tools/hitools/cl8a9de13648c56d7f/ 海康機器人-機器視覺產品頁杭州海康機器人股份有限公司海康機器人HIKROBOT是面向全球的機器視覺和移動機器人產品及解決方案提供商&#xff0c;業務聚焦于…

oracle與sqlsever的區別

oracle與sqlsever的區別 區別一 oracle字符之間連接用|| sqlserver字符之間連接用區別二 oracle字段重命名用as sqlserver字段重命名用區別三 oracle判空用nvl sqlserver判空用isnull區別四 oracle多列合并成一列 select assid, LISTAGG(name, ) within group (order by…

Navicat 技術指引 | 適用于 GaussDB 分布式的數據生成功能

Navicat Premium&#xff08;16.3.3 Windows 版或以上&#xff09;正式支持 GaussDB 分布式數據庫。GaussDB 分布式模式更適合對系統可用性和數據處理能力要求較高的場景。Navicat 工具不僅提供可視化數據查看和編輯功能&#xff0c;還提供強大的高階功能&#xff08;如模型、結…

GPTs的創建與使用,自定義GPTs中的Actions示例用法 定義和執行特定任務的功能模塊 通過API與外部系統或服務的交互

Name 等 Logo:自動生成 Name 介紹 Description 介紹 Instructions 要求或命令等 比如用中文回復&#xff0c;角色。 Knowledge 上傳你的知識庫&#xff0c;如果你有某一垂直行業的數據&#xff0c;基于數據來回答。比如我有某個芯片的指令集。 Capabilities 都要 Actions&…

Flink 使用場景

Apache Flink 功能強大&#xff0c;支持開發和運行多種不同種類的應用程序。它的主要特性包括&#xff1a;批流一體化、精密的狀態管理、事件時間支持以及精確一次的狀態一致性保障等。Flink 不僅可以運行在包括 YARN、 Mesos、K8s 在內的多種資源管理框架上&#xff0c;還支持…

工業IC是什么

工業IC 電子元器件百科 文章目錄 工業IC前言一、工業IC是什么二、工業IC的類別三、工業IC應用實例四、工業IC作用原理總結前言 工業IC包括微控制器(MCU)、采樣芯片、模擬-數字轉換器(ADC)、電源管理芯片、驅動芯片等。它們被廣泛應用于各個行業的工業控制和自動化系統中,…

2023年泰國加密市場概覽

一、泰國區塊鏈及加密生態概覽 1.加密貨幣數據分析平臺訪問人數火爆 2023年CoinMarketCap網站的平均月訪問量為64.8萬人次&#xff0c;占全國總人口的0.94%&#xff0c;泰國的人均訪問量比美國高出0.21%。 1.2泰國加密資產交易量可觀 根據CoinGecko上泰國領先的數字資產交易所…

vue3遞歸組件---樹形組件

第一種方式&#xff0c;直接自己調用自己 Tree.vue <template><div class"tree"><div v-for"(item, index) in data" :key"item.name">每一層 {{ item.name }}<Tree v-if"item?.children?.length" :dataitem…

linux如何清空文件內容

在做系統運維工作時&#xff0c;有時會發現一個問題&#xff1a;某些存儲空間的使用率過高。換句話說就是空間快被堆滿了&#xff0c;需要釋放空間。大多數情況下&#xff0c;導致空間不足的罪魁禍首通常是一些log日志文件。對于某些特殊系統來說&#xff0c;日志文件還不能直接…

AGM離線下載器使用說明

AGM專用離線下載器示意圖&#xff1a; 供電方式&#xff1a; 通過 USB 接口給下載器供電&#xff0c;跳線 JP 斷開。如果客戶 PCB 的 JTAG 口不能提供 3.3V 電源&#xff0c;或僅需燒寫下載器&#xff0c;尚未連接用戶 PCB 時&#xff0c;采用此種方式供電。 或者&#xff1a…

Linux中的網絡時間服務器

本章主要介紹網絡時間的服務器 使用chrony配置時間服務器配置chrony客戶端服務器同步時間 1.1 時間同步的重要性 一些服務對時間要求非常嚴格&#xff0c;例如如圖所示的由三臺服務器搭建的ceph集群 這三臺服務器的時間必須保持一致&#xff0c;如果不一致&#xff0c;就會顯…

Django講課筆記01:初探Django框架

文章目錄 一、學習目標二、課程導入&#xff08;一&#xff09;課程簡介&#xff08;二&#xff09;課程目標&#xff08;三&#xff09;適用人群&#xff08;四&#xff09;教學方式&#xff08;五&#xff09;評估方式&#xff08;六&#xff09;參考教材 三、新課講授&#…

android項目實戰之編輯器集成

引言 項目需要用到編輯器&#xff0c;采用RichEditor&#xff0c;如下效果 實現 1. 引入庫2 implementation jp.wasabeef:richeditor-android:2.0.0 2. XML <LinearLayout xmlns:android"http://schemas.android.com/apk/res/android"android:layout_width&q…

LeetCode:2008. 出租車的最大盈利(dp C++)

目錄 2008. 出租車的最大盈利 題目描述&#xff1a; 實現代碼與解析&#xff1a; DP 二分&#xff08;兩種寫法&#xff09; 原理思路&#xff1a; 2008. 出租車的最大盈利 題目描述&#xff1a; 你駕駛出租車行駛在一條有 n 個地點的路上。這 n 個地點從近到遠編號為 1 …

如何使用 Wordpress?托管, 網站, 插件, 緩存

這是該系列教程的第一個教程&#xff0c;最終將在運行高性能 LEMP 堆棧的阿里云 ECS 實例上運行一個新的 WordPress 站點。 在本教程中&#xff0c;我們將創建一個運行 Ubuntu 16.04 的實例&#xff0c;然后通過創建超級用戶并禁用 root 登錄來保護服務器&#xff0c;最后配置…