緩存,設計的初衷是爲了減小繁重的IO操做,增長系統併發能力。無論是 CPU多級緩存
,page cache
,仍是咱們業務中熟悉的 redis
緩存,本質都是將有限的熱點數據存儲在一個存取更快的存儲介質中。html
計算機自己的緩存設計就是 CPU 採起多級緩存。那對咱們服務來講,咱們是否是也能夠採用這種多級緩存的方式來組織咱們的緩存數據。同時 redis
的存取都會通過網絡IO,那咱們能不能把熱點數據直接存在本進程內,由進程本身緩存一份最近最熱的這批數據呢?git
這就引出了咱們今天探討的:local cache
,本地緩存,也叫進程緩存。github
本文帶你一塊兒探討下 go-zero
中進程緩存的設計。Let’s go!redis
做爲一個進程存儲設計,固然是 crud
都有的:sql
local cache
// 先初始化 local cache cache, err = collection.NewCache(time.Minute, collection.WithLimit(10)) if err != nil { log.Fatal(err) }
其中參數的含義:緩存
expire
:key統一的過時時間CacheOption
:cache設置。好比key的上限設置等// 1. add/update 增長/修改都是該API cache.Set("first", "first element") // 2. get 獲取key下的value value, ok := cache.Get("first") // 3. del 刪除一個key cache.Del("first")
Set(key, value)
設置緩存value, ok := Get(key)
讀取緩存Del(key)
刪除緩存cache.Take("first", func() (interface{}, error) { // 模擬邏輯寫入local cache time.Sleep(time.Millisecond * 100) return "first element", nil })
前面的 Set(key, value)
是單純將 <key, value>
加入緩存;Take(key, setFunc)
則是在 key 對於的 value 不存在時,執行傳入的 fetch
方法,將具體讀取邏輯交給開發者實現,並自動將結果放到緩存裏。微信
到這裏核心使用代碼基本就講完了,其實看起來仍是挺簡單的。也能夠到 https://github.com/tal-tech/g... 去看 test 中的使用。網絡
首先緩存實質是一個存儲有限熱點數據的介質,面臨如下的這些問題:多線程
下面來講說這3個方面咱們的設計實踐。併發
有限就意味着滿了要淘汰,這個就涉及到淘汰策略。cache
中使用的是:LRU
(最近最少使用)。
那淘汰怎麼發生呢? 有幾個選擇:
而 cache
中採起的是第一種 主動刪除。可是,主動刪除中遇到最大的問題是:
不斷循環,空消耗CPU資源,即便在額外的協程中這麼作,也是沒有必要的。
cache
中採起的是時間輪記錄額外過時通知,等過時 channel
中有通知時,而後觸發刪除回調。
有關 時間輪 更多的設計文章: https://go-zero.dev/cn/timing...
對於緩存來講,咱們須要知道這個緩存在使用額外空間和代碼的狀況下是否有價值,以及咱們想知道需不須要進一步優化過時時間或者緩存大小,全部這些咱們就很依賴統計能力了, go-zero
中 sqlc
和 mongoc
也一樣提供了統計能力。因此咱們在 cache
中也加入的緩存,爲開發者提供本地緩存監控的特性,在接入 ELK
時開發者能夠更直觀的監測到緩存的分佈狀況。
而設計其實也很簡單,就是:Get() 命中,就在統計 count 上加1便可。
func (c *Cache) Get(key string) (interface{}, bool) { value, ok := c.doGet(key) if ok { // 命中hit+1 c.stats.IncrementHit() } else { // 未命中miss+1 c.stats.IncrementMiss() } return value, ok }
當多個協程併發存取的時候,對於緩存來講,涉及的問題如下幾個:
LRU
中元素的移動過程衝突這種狀況下,寫衝突好解決,最簡單的方法就是 加鎖 :
// Set(key, value) func (c *Cache) Set(key string, value interface{}) { // 加鎖,而後將 <key, value> 做爲鍵值對寫入 cache 中的 map c.lock.Lock() _, ok := c.data[key] c.data[key] = value // lru add key c.lruCache.add(key) c.lock.Unlock() ... } // 還有一個在操做 LRU 的地方時:Get() func (c *Cache) doGet(key string) (interface{}, bool) { c.lock.Lock() defer c.lock.Unlock() // 當key存在時,則調整 LRU item 中的位置,這個過程也是加鎖的 value, ok := c.data[key] if ok { c.lruCache.add(key) } return value, ok }
而併發執行寫入邏輯,這個邏輯主要是開發者本身傳入的。而這個過程:
func (c *Cache) Take(key string, fetch func() (interface{}, error)) (interface{}, error) { // 1. 先獲取 doGet() 中的值 if val, ok := c.doGet(key); ok { c.stats.IncrementHit() return val, nil } var fresh bool // 2. 多協程中經過 sharedCalls 去獲取,一個協程獲取多個協程共享結果 val, err := c.barrier.Do(key, func() (interface{}, error) { // double check,防止屢次讀取 if val, ok := c.doGet(key); ok { return val, nil } ... // 重點是執行了傳入的緩存設置函數 val, err := fetch() ... c.Set(key, val) }) if err != nil { return nil, err } ... return val, nil }
而 sharedCalls
經過共享返回結果,節省了屢次執行函數,減小了協程競爭。
本篇文章講解了本地緩存設計實踐。從使用到設計思路,你也能夠根據你的業務動態修改 緩存的過時策略,加入你想要的統計指標,實現本身的本地緩存。
甚至能夠將本地緩存和 redis
結合,給服務提供多級緩存,這個就留到咱們下一篇文章:緩存在服務中的多級設計。
關於 go-zero
更多的設計和實現文章,能夠關注『微服務實踐』公衆號。
https://github.com/tal-tech/go-zero
歡迎使用 go-zero 並 star 支持咱們!
關注『微服務實踐』公衆號並點擊 進羣 獲取社區羣二維碼。
go-zero 系列文章見『微服務實踐』公衆號