I wrote Gocache: a complete and extensible Go cache library

Gocache: 一个功能齐全且易于扩展的Go缓存库
https://vincent.composieux.fr/article/i-wrote-gocache-a-complete-and-extensible-go-cache-library

In the previous weeks, I wrote Gocache, an extensible and full of set cache library for Go developers.
在先前几周的时候,我完成了Gocache,对于Go开发者而言,它是功能齐全且易于扩展的。

The goal of this library is to provide all that you need to start caching your data, maybe use multiple (chaning) cache, retrieve some metrics about things you cache and all the needs you could encounter.
这个库的设计目的是为了解决在使用缓存或者使用多种(多级)缓存时所遇到的问题,它为缓存方案制定了一个标准。

History

背景

When I started working on implementing a cache on a GraphQL Go project, it already had a memory cache that used a little library that have a simple API but also used another memory cache library to load data using batch mode with a different library and API, to do the same thing: cache items.
当我一开始在为GraphQL的Go项目构建缓存时,该项目已经包含了一套有简单API的内存缓存,还使用了另外一套有不同API的内存缓存和加载缓存数据的代码,它们实际上都是在只做了同一件事:缓存。

Later, we had another need: in addition of this memory cache, we wanted to add a layer of distributed cache using Redis principally to avoid our new Kubernetes pods to have an empty cache when rolling a new version of the application into production.
后来,我们又有了另一个需求:除了内存缓存外,我们还想添加一套基于Redis集群的分布式缓存,其主要目的是为了在新版本上线时,避免Kubernetes的Pods使用的缓存为空。

Here comes the Gocache idea: it’s time to have a unified API to rule multiple cache storages: memory, redis, memcache or whatever you want.
于是创造Gocache的契机出现了,是时候用一套统一的规则来管理多种缓存方案了,不管是内存、Redis、Memcache或者其他任何形式的缓存。

Oh, that’s not all, we also wanted to have metrics (to be scrapped by our Prometheus server) on these cache items.
哦,还不止这些,我们还希望缓存的数据可以被Metrics监控(后来被我们的Prometheus替代)。(译者注:Metrics和Prometheus均是监控工具。)

Project Gocache is born: https://github.com/eko/gocache.
Gocache项目诞生了:https://github.com/eko/gocache

Store adapters

存储接口

First of all, when you want to cache items, you have to select where you want to cache your items: in memory? in a shared redis or memcache? or maybe in another storage.
首先,当你准备缓存一些数据时,你必须选择缓存的存储方式:简单的直接放进内存?使用Redis或者Memcache?或者其它某种形式的存储。

At this time, Gocache have the following stores implemented:

  • Bigcache: An in memory store,
  • Ristretto: Another in memory store provided by DGraph,
  • Memcache: A memcache store based on the bradfitz/gomemcache client library,
  • Redis: A redis store based on the go-redis/redis client library.

目前,Gocache已经实现了以下存储方案:

  • Bigcache: 简单的内存存储。
  • Ristretto: 由DGraph提供的内存存储。
  • Memcache: 基于bradfitz/gomemcache的Memcache存储。
  • Redis: 基于go-redis/redis的Redis存储。

All of these stores implement a really simple API that respect the following interface:
所有的存储方案都实现了一个非常简单的接口:

1
2
3
4
5
6
7
8
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
}

This interface represents all the actions you can do on the stores and each of them call the necessary methods in the client libraries.
这个接口展示了可以对存储器执行的所有操作,每个操作只调用了存储器客户端的必要方法。

All of these stores have different configuration, depending on the client libraries you want to use, for instance, to initialize a Memcache store:
这些存储器都有不同的配置,具体配置取决于实现存储器的客户端,举个例子,以下为初始化Memcache存储器的示例:

1
2
3
4
5
6
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,
},
)

Then, the initialized store have have to be passed into a cache object constructor.
然后,必须将初始化存储器的代码放进缓存的构造函数中。

Cache adapters

缓存接口

One cache interface to rule them all. The cache interface is quite the same to the store one because basically, a cache will do actions on a store:
以下为缓存接口,缓存接口和存储接口是一样的,毕竟,缓存就是对存储器做一些操作。

1
2
3
4
5
6
7
8
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
}

With this interface, I have all the necessary actions I have to perform on my cache items: Set, Get, Delete, Invalidate data, Clear all the cache and another method (GetType) that allows to know what is the current cache item, useful in some cases.
该接口包含了需要对缓存数据进行的所有必要操作:Set,Get,Delete,使某条缓存失效,清空缓存。如果需要的话,还可以使用GetType方法获取缓存类型。

Starting from this interface, the implemented cache types are the following:

  • Cache: The basic cache that allows to manipulate data from the given stores,
  • Chain: A special cache adapter that allows to chain multiple cache (could be because you have a memory cache, a redis cache, etc…),
  • Loadable: A special cache adapter that allows to specify a kind of callback function to automatically reload data into your cache if expired or invalidated,
  • Metric: A special cache adapter that allows to store metrics about your cache data: how many items setted, getted, invalidated, successfully or not.

缓存接口已有以下实现:

  • Cache: 基础版,直接操作存储器。
  • Chain: 链式缓存,它允许使用多级缓存(项目中可能同时存在内存缓存,Redis缓存等等)。
  • Loadable: 可自动加载数据的缓存,它可以指定回调函数,在缓存过期或失效的时候,会自动通过回调函数将数据加载进缓存中。
  • Metric: 内嵌监控的缓存,它会收集缓存的一些指标,比如Set、Get、失效和成功的缓存数量。

The beauty comes when all of these cache implements the same interface and can be wrapped each other: a metrics cache can take a loadable cache that can take a chained cache that can take multiple caches.
最棒的是:这些缓存器都实现了相同的接口,所以它们可以很容易地相互组合。你的缓存可以同时具有链式、可自动加载数据、包含监控等特性。

Remember, I wanted to have a clean API. Here is a simple Memcache example:
还记得吗?我们想要简单的API,以下为使用Memcache的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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, // Override default value of 10 seconds defined in the store
})
if err != nil {
panic(err)
}

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

cacheManager.Delete("my-key")

cacheManager.Clear() // Clears the entire cache, in case you want to flush all cache

Now, let’s say you want to have a chained cache with a memory Ristretto store and a distributed Redis store as a fallback, with a marshaller and metrics in bonus:
现在,假设你想要将已有的缓存修改为一个链式缓存,该缓存包含Ristretto(内存型)和Redis集群,并且具备缓存数据序列化和监控特性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// Initialize Ristretto cache and Redis client
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"})

// Initialize stores
ristrettoStore := store.NewRistretto(ristrettoCache, nil)
redisStore := store.NewRedis(redisClient, &cache.Options{Expiration: 5*time.Second})

// Initialize Prometheus metrics
promMetrics := metrics.NewPrometheus("my-amazing-app")

// Initialize chained cache
cacheManager := cache.NewMetric(promMetrics, cache.NewChain(
cache.New(ristrettoStore),
cache.New(redisStore),
))

// Initializes a marshaler
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"}

// Set the value in cache using given key
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

We didn’t talked about the Marshaler yet but it’s another feature of Gocache: we provided a service to help your automatically marshal/unmarshal your objects from/to your storages.
我们不对序列化做过多的讨论,因为这个是Gocache的另外一个特性:提供一套在存储和取出缓存对象时可以自动序列化和反序列化缓存对象的工具。

That’s useful when working with struct objects as key and other than memory storages because you have to convert your objects into bytes.
该特性在使用对象作为缓存Key时会很有用,它省去了在代码中手动转换对象的操作。

All of these features: a chained cache with memory & redis, Prometheus metrics and marshaler are ready in only ~20 lines of code.
所有的这些特性:包含内存型和Redis的链式缓存、包含Prometheus监控功能和自动序列化功能,都可以在20行左右的代码里完成。

Write your own cache or storage

定制你自己的缓存

If you want to implement your own proprietary cache, it’s also really easy to do.
如果你想定制自己的缓存也很容易。

Here is a simple example in case you want to log each action that is done in your cache (not really a good idea but well, that’s simple todo as an example):
以下示例展示了如何给对缓存的每个操作添加日志(这不是一个好主意,只是作为示例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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
}

In a same way, you can also implement a custom storage.
通过同样的方法,你也可以自己实现存储接口。

If you think other people can benefit your cache or storage implementation, please do not hesitate to open a pull request and contribute directly on the project so we could discuss your idea together and bring a more powerful cache library.
如果你认为其他人也可以从你的缓存实现中获得启发,请不要犹豫,直接在项目中发起合并请求。通过共同讨论你的想法,我们会提供一个功能更加强大的缓存库。

Conclusion

结论

By writing this library, I also try to improve the Go community tools to help build better softwares.
在构建这个库的过程中,我还尝试改进了Go的社区工具。

I hope you enjoyed this post and I would be happy to discuss about your needs or special use cases if you have some.
希望你喜欢这篇博客,如果有需要的话,我非常乐意和你讨论需求或者你的想法。

Finally, if you need some help implementing something about caching, do not hesitate to contact me via Twitter or email.
最后,如果你在缓存方面需要帮助,你可以随时通过Twitter或者邮件联系我。