Go 緩存系列之 go-cache

一句話描述

go-cache [1] 基於內存的 K/V 存儲 / 緩存 : (類似於 Memcached),適用於單機應用程序

簡介

go-cache 是什麼?

基於內存的 K/V 存儲 / 緩存 : (類似於 Memcached),適用於單機應用程序 ,支持刪除,過期,默認 Cache 共享鎖,

大量 key 的情況下會造成鎖競爭嚴重

爲什麼選擇 go-cache?

可以存儲任何對象(在給定的持續時間內或永久存儲),並且可以由多個 goroutine 安全地使用緩存。

Example

package main

import (
 "fmt"
 "time"

 "github.com/patrickmn/go-cache"
)

type MyStruct struct {
 Name string
}

func main() {
 // 設置超時時間和清理時間
 c := cache.New(5*time.Minute, 10*time.Minute)

 // 設置緩存值並帶上過期時間
 c.Set("foo", "bar", cache.DefaultExpiration)


 // 設置沒有過期時間的KEY,這個KEY不會被自動清除,想清除使用:c.Delete("baz")
 c.Set("baz", 42, cache.NoExpiration)


 var foo interface{}
 var found bool
 // 獲取值
 foo, found = c.Get("foo")
 if found {
  fmt.Println(foo)
 }

 var foos string
 // 獲取值, 並斷言
 if x, found := c.Get("foo"); found {
  foos = x.(string)
  fmt.Println(foos)
 }
 // 對結構體指針進行操作
 var my *MyStruct
 c.Set("foo", &MyStruct{Name: "NameName"}, cache.DefaultExpiration)
 if x, found := c.Get("foo"); found {
  my = x.(*MyStruct)
  // ...
 }
 fmt.Println(my)
}

源碼分析

源碼分析主要針對核心的存儲結構、Set、Get、Delete、定時清理邏輯進行分析。包含整體的邏輯架構

核心的存儲結構

package cache

// Item 每一個具體緩存值
type Item struct {
 Object     interface{}
 Expiration int64 // 過期時間:設置時間+緩存時長
}

// Cache 整體緩存
type Cache struct {
 *cache
}

// cache 整體緩存
type cache struct {
 defaultExpiration time.Duration // 默認超時時間
 items             map[string]Item // KV對
 mu                sync.RWMutex // 讀寫鎖,在操作(增加,刪除)緩存時使用
 onEvicted         func(string, interface{}) // 刪除KEY時的CallBack函數
 janitor           *janitor // 定時清空緩存的結構
}

// janitor  定時清空緩存的結構
type janitor struct {
 Interval time.Duration // 多長時間掃描一次緩存
 stop     chan bool // 是否需要停止
}

Set

package cache
func (c *cache) Set(k string, x interface{}, d time.Duration) {
 // "Inlining" of set
 var e int64
 if d == DefaultExpiration {
  d = c.defaultExpiration
 }
 if d > 0 {
  e = time.Now().Add(d).UnixNano()
 }
 c.mu.Lock() // 這裏可以使用defer?
 c.items[k] = Item{
  Object:     x, // 實際的數據
  Expiration: e, // 下次過期時間
 }
 c.mu.Unlock()
}

Get

package cache
func (c *cache) Get(k string) (interface{}, bool) {
 c.mu.RLock() // 加鎖,限制併發讀寫
 item, found := c.items[k] // 在 items 這個 map[string]Item 查找數據
 if !found {
  c.mu.RUnlock()
  return nil, false
 }
 if item.Expiration > 0 {
  if time.Now().UnixNano() > item.Expiration { // 已經過期,直接返回nil,爲什麼在這裏不直接就刪除了呢?
   c.mu.RUnlock()
   return nil, false
  }
 }
 c.mu.RUnlock()
 return item.Object, true
}

Delete

package cache
// Delete an item from the cache. Does nothing if the key is not in the cache.
func (c *cache) Delete(k string) {
 c.mu.Lock()
 v, evicted := c.delete(k)
 c.mu.Unlock()
 if evicted {
  c.onEvicted(k, v) // 刪除KEY時的CallBack
 }
}

func (c *cache) delete(k string) (interface{}, bool) {
 if c.onEvicted != nil {
  if v, found := c.items[k]; found {
   delete(c.items, k)
   return v.Object, true
  }
 }
 delete(c.items, k)
 return nil, false
}

定時清理邏輯

package cache
func newCacheWithJanitor(de time.Duration, ci time.Duration, m map[string]Item) *Cache {
 c := newCache(de, m)
 C := &Cache{c}
 if ci > 0 {
  runJanitor(c, ci) // 定時運行清除過期KEY
  runtime.SetFinalizer(C, stopJanitor) // 當C被GC回收時,會停止runJanitor 中的協程
 }
 return C
}

func runJanitor(c *cache, ci time.Duration) {
 j := &janitor{
  Interval: ci,
  stop:     make(chan bool),
 }
 c.janitor = j
 go j.Run(c) // 新的協程做過期刪除邏輯
}

func (j *janitor) Run(c *cache) {
 ticker := time.NewTicker(j.Interval)
 for {
  select {
  case <-ticker.C: // 每到一個週期就全部遍歷一次
   c.DeleteExpired() // 實際的刪除邏輯
  case <-j.stop:
   ticker.Stop()
   return
  }
 }
}

// Delete all expired items from the cache.
func (c *cache) DeleteExpired() {
 var evictedItems []keyAndValue
 now := time.Now().UnixNano()
 c.mu.Lock()
 for k, v := range c.items { // 加鎖遍歷整個列表
  // "Inlining" of expired
  if v.Expiration > 0 && now > v.Expiration {
   ov, evicted := c.delete(k)
   if evicted {
    evictedItems = append(evictedItems, keyAndValue{k, ov})
   }
  }
 }
 c.mu.Unlock()
 for _, v := range evictedItems {
  c.onEvicted(v.key, v.value)
 }
}

思考

Lock 的使用

使用讀寫鎖?

鎖的粒度是否可以變更小?

使用 sync.map?

runtime.SetFinalizer

在實際的編程中,我們都希望每個對象釋放時執行一個方法,在該方法內執行一些計數、釋放或特定的要求, 以往都是在對象指針置 nil 前調用一個特定的方法, golang 提供了 runtime.SetFinalizer 函數,當 GC 準備釋放對象時,會回調該函數指定的方法,非常方便和有效。

對象可以關聯一個 SetFinalizer 函數, 當 gc 檢測到 unreachable 對象有關聯的 SetFinalizer 函數時, 會執行關聯的 SetFinalizer 函數, 同時取消關聯。這樣當下一次 gc 的時候, 對象重新處於 unreachable 狀態 並且沒有 SetFinalizer 關聯, 就會被回收。

Doc

https://pkg.go.dev/github.com/patrickmn/go-cache

比較

相似的庫

參考資料

[1]  go-cache : https://github.com/patrickmn/go-cache

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/hz7mViPQIwca0mSylfS-ww