Go 每日一庫之 gocache

在先前幾周的時候,我完成了 Gocache[1],對於 Go 開發者而言,它是功能齊全且易於擴展的。

這個庫的設計目的是爲了解決在使用緩存或者使用多種(多級)緩存時所遇到的問題,它爲緩存方案制定了一個標準。

本文是 Go 語言中文網組織的 GCTT 翻譯,發佈在 Go 語言中文網公衆號,轉載請聯繫我們授權。

背景

當我一開始在爲 GraphQL 的 Go 項目構建緩存時,該項目已經包含了一套有簡單 API 的內存緩存,還使用了另外一套有不同 API 的內存緩存和加載緩存數據的代碼,它們實際上都是在只做了同一件事:緩存。

後來,我們又有了另一個需求:除了內存緩存外,我們還想添加一套基於 Redis 集羣的分佈式緩存,其主要目的是爲了在新版本上線時,避免 Kubernetes 的 Pods 使用的緩存爲空。

於是創造 Gocache 的契機出現了,是時候用一套統一的規則來管理多種緩存方案了,不管是內存、Redis、Memcache 或者其他任何形式的緩存。

哦,還不止這些,我們還希望緩存的數據可以被 Metrics 監控(後來被我們的 Prometheus 替代)。(譯者注:Metrics 和 Prometheus 均是監控工具。)

Gocache 項目誕生了:https://github.com/eko/gocache 。

存儲接口

首先,當你準備緩存一些數據時,你必須選擇緩存的存儲方式:簡單的直接放進內存?使用 Redis 或者 Memcache?或者其它某種形式的存儲。

目前,Gocache 已經實現了以下存儲方案:

所有的存儲方案都實現了一個非常簡單的接口:

type StoreInterface interface {
 Get(key interface{}) (interface{}, error)
 Set(key interface{}, value interface{}, options *Options) error
 Delete(key interface{}) error
 Invalidate(options InvalidateOptions) error
 Clear() error
 GetType() string
}

這個接口展示了可以對存儲器執行的所有操作,每個操作只調用了存儲器客戶端的必要方法。

這些存儲器都有不同的配置,具體配置取決於實現存儲器的客戶端,舉個例子,以下爲初始化 Memcache 存儲器的示例:

store := store.NewMemcache(
 memcache.New("10.0.0.1:11211""10.0.0.2:11211""10.0.0.3:11212"),
 &store.Options{
  Expiration: 10*time.Second,
 },
)

然後,必須將初始化存儲器的代碼放進緩存的構造函數中。

緩存接口

以下爲緩存接口,緩存接口和存儲接口是一樣的,畢竟,緩存就是對存儲器做一些操作。

type CacheInterface interface {
 Get(key interface{}) (interface{}, error)
 Set(key, object interface{}, options *store.Options) error
 Delete(key interface{}) error
 Invalidate(options store.InvalidateOptions) error
 Clear() error
 GetType() string
}

該接口包含了需要對緩存數據進行的所有必要操作:Set,Get,Delete,使某條緩存失效,清空緩存。如果需要的話,還可以使用 GetType 方法獲取緩存類型。

緩存接口已有以下實現:

最棒的是:這些緩存器都實現了相同的接口,所以它們可以很容易地相互組合。你的緩存可以同時具有鏈式、可自動加載數據、包含監控等特性。

還記得嗎?我們想要簡單的 API,以下爲使用 Memcache 的示例:

memcacheStore := store.NewMemcache(
 memcache.New("10.0.0.1:11211""10.0.0.2:11211""10.0.0.3:11212"),
 &store.Options{
  Expiration: 10*time.Second,
 },
)

cacheManager := cache.New(memcacheStore)
err := cacheManager.Set("my-key"[]byte("my-value")&cache.Options{
 Expiration: 15*time.Second, // 設置過期時間
})
if err != nil {
    panic(err)
}

value := cacheManager.Get("my-key")

cacheManager.Delete("my-key")

cacheManager.Clear() // 清空緩存

現在,假設你想要將已有的緩存修改爲一個鏈式緩存,該緩存包含 Ristretto(內存型)和 Redis 集羣,並且具備緩存數據序列化和監控特性:

// 初始化 Ristretto 和 Redis 客戶端
ristrettoCache, err := ristretto.NewCache(&ristretto.Config{NumCounters: 1000, MaxCost: 100, BufferItems: 64})
if err != nil {
    panic(err)
}

redisClient := redis.NewClient(&redis.Options{Addr: "127.0.0.1:6379"})

// 初始化存儲器
ristrettoStore := store.NewRistretto(ristrettoCache, nil)
redisStore := store.NewRedis(redisClient, &cache.Options{Expiration: 5*time.Second})

// 初始化 Prometheus 監控
promMetrics := metrics.NewPrometheus("my-amazing-app")

// 初始化鏈式緩存
cacheManager := cache.NewMetric(promMetrics, cache.NewChain(
    cache.New(ristrettoStore),
    cache.New(redisStore),
))

// 初始化序列化工具
marshal := marshaler.New(cacheManager)

key := BookQuery{Slug: "my-test-amazing-book"}
value := Book{ID: 1, Name: "My test amazing book", Slug: "my-test-amazing-book"}

// 插入緩存
err = marshal.Set(key, value)
if err != nil {
    panic(err)
}

returnedValue, err := marshal.Get(key, new(Book))
if err != nil {
    panic(err)
}

// Then, do what you want with the value

我們不對序列化做過多的討論,因爲這個是 Gocache 的另外一個特性:提供一套在存儲和取出緩存對象時可以自動序列化和反序列化緩存對象的工具。

該特性在使用對象作爲緩存 Key 時會很有用,它省去了在代碼中手動轉換對象的操作。

所有的這些特性:包含內存型和 Redis 的鏈式緩存、包含 Prometheus 監控功能和自動序列化功能,都可以在 20 行左右的代碼裏完成。

定製你自己的緩存

如果你想定製自己的緩存也很容易。

以下示例展示瞭如何給對緩存的每個操作添加日誌(這不是一個好主意,只是作爲示例):

package customcache

import (
 "log"

 "github.com/eko/gocache/cache"
 "github.com/eko/gocache/store"
)

const LoggableType = "loggable"

type LoggableCache struct {
 cache cache.CacheInterface
}

func NewLoggable(cache cache.CacheInterface) *LoggableCache {
 return &LoggableCache{
  cache: cache,
 }
}

func (c *LoggableCache) Get(key interface{}) (interface{}, error) {
 log.Print("Get some data...")
 return c.cache.Get(key)
}

func (c *LoggableCache) Set(key, object interface{}, options *store.Options) error {
 log.Print("Set some data...")
 return c.cache.Set(key, object, options)
}

func (c *LoggableCache) Delete(key interface{}) error {
 log.Print("Delete some data...")
 return c.cache.Delete(key)
}

func (c *LoggableCache) Invalidate(options store.InvalidateOptions) error {
 log.Print("Invalidate some data...")
 return c.cache.Invalidate(options)
}

func (c *LoggableCache) Clear() error {
 log.Print("Clear some data...")
 return c.cache.Clear()
}

func (c *LoggableCache) GetType() string {
 return LoggableType
}

通過同樣的方法,你也可以自己實現存儲接口。

如果你認爲其他人也可以從你的緩存實現中獲得啓發,請不要猶豫,直接在項目中發起合併請求。通過共同討論你的想法,我們會提供一個功能更加強大的緩存庫。

結論

在構建這個庫的過程中,我還嘗試改進了 Go 的社區工具。

希望你喜歡這篇博客,如果有需要的話,我非常樂意和你討論需求或者你的想法。

最後,如果你在緩存方面需要幫助,你可以隨時通過 Twitter 或者郵件聯繫我。


via: https://vincent.composieux.fr/article/i-wrote-gocache-a-complete-and-extensible-go-cache-library

作者:Vincent Blanchon[2] 譯者:beiping96[3] 校對:polaris1119[4]

本文由 GCTT[5] 原創編譯,Go 中文網 [6] 榮譽推出,發佈在 Go 語言中文網公衆號,轉載請聯繫我們授權。

參考資料

[1]

Gocache: https://github.com/eko/gocache

[2]

Vincent Blanchon: https://medium.com/@blanchon.vincent

[3]

beiping96: https://github.com/beiping96

[4]

polaris1119: https://github.com/polaris1119

[5]

GCTT: https://github.com/studygolang/GCTT

[6]

Go 中文網: https://studygolang.com/

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