什么是go-cache

KV存储引擎有很多,常用的如redis,rocksdb等,如果在实际使用中只是在内存中实现一个简单的kv缓存,使用上述引擎就太大费周章了。在Golang中可以使用go-cache这个package实现一个轻量级基于内存的kv存储或缓存。GitHub源码地址是:https://github.com/patrickmn/go-cache 。
go-cache这个包实际上是在内存中实现了一个线程安全的map[string]interface{},可以将任何类型的对象作为value,不需要通过网络序列化或传输数据,适用于单机应用。对于每组KV数据可以设置不同的TTL(也可以永久存储),并可以自动实现过期清理。
在使用时一般都是将go-cache作为数据缓存来使用,而不是持久性的数据存储。对于停机后快速恢复的场景,go-cache支持将缓存数据保存到文件,恢复时从文件中load数据加载到内存。

如何使用go-cache

常用接口分析

对于数据库的基本操作,无外乎关心的CRUD(增删改查),对应到go-cache中的接口如下:

func New(defaultExpiration, cleanupInterval time.Duration) *Cachefunc NewFrom(defaultExpiration, cleanupInterval time.Duration, items map[string]Item) *Cachefunc (c Cache) Add(k string, x interface{}, d time.Duration) errorfunc (c Cache) Set(k string, x interface{}, d time.Duration)func (c Cache) SetDefault(k string, x interface{})func (c Cache) Get(k string) (interface{}, bool)func (c Cache) GetWithExpiration(k string) (interface{}, time.Time, bool)Setfunc (c Cache) Replace(k string, x interface{}, d time.Duration) errorfunc (c Cache) Decrement(k string, n int64) errorDecrementXXXfunc (c *cache) DecrementInt8(k string, n int8) (int8, error)func (c Cache) Increment(k string, n int64) errorDecrementIncrementXXXDecrementXXXfunc (c Cache) Delete(k string)func (c Cache) DeleteExpired()func (c Cache) Flush()func (c Cache) ItemCount() intfunc (c *cache) OnEvicted(f func(string, interface{}))

安装go-cache包

介绍了go-cache的常用接口,接下来从代码中看看如何使用。在coding前需要安装go-cache,命令如下。

go get github.com/patrickmn/go-cache

一个Demo

如何在golang中使用上述接口实现kv数据库的增删改查,接下来看一个demo。其他更多接口的用法和更详细的说明,可以参考GoDoc。

import (
	"fmt"
	"time"
	
	"github.com/patrickmn/go-cache" // 使用前先import包
)

func main() {
	// 创建一个cache对象,默认ttl 5分钟,每10分钟对过期数据进行一次清理
	c := cache.New(5*time.Minute, 10*time.Minute)

	// Set一个KV,key是"foo",value是"bar"
	// TTL是默认值(上面创建对象的入参,也可以设置不同的值)5分钟
	c.Set("foo", "bar", cache.DefaultExpiration)

	// Set了一个没有TTL的KV,只有调用delete接口指定key时才会删除
	c.Set("baz", 42, cache.NoExpiration)

	// 从cache中获取key对应的value
	foo, found := c.Get("foo")
	if found {
		fmt.Println(foo)
	}

	// 如果想提高性能,存储指针类型的值
	c.Set("foo", &MyStruct, cache.DefaultExpiration)
	if x, found := c.Get("foo"); found {
		foo := x.(*MyStruct)
			// ...
	}
}

源码分析

1. 常量:内部定义的两个常量`NoExpiration`和`DefaultExpiration`,可以作为上面接口中的入参,`NoExpiration`表示没有设置有效时间,`DefaultExpiration`表示使用New()或NewFrom()创建cache对象时传入的默认有效时间。
const (
	NoExpiration time.Duration = -1
	DefaultExpiration time.Duration = 0
)
2.  Item:cache中存储的value类型,Object是真正的值,Expiration表示过期时间。可以使用Item的```Expired()```接口确定是否到期,实现方式是过比较当前时间和Item设置的到期时间来判断是否过期。
type Item struct {
	Object     interface{}
	Expiration int64
}

func (item Item) Expired() bool {
	if item.Expiration == 0 {
		return false
	}
	return time.Now().UnixNano() > item.Expiration
}
3. cache:go-cache的核心数据结构,其中定义了每条记录的默认过期时间,底层的存储结构等信息。
type cache struct {
	defaultExpiration time.Duration              // 默认过期时间
	items             map[string]Item            // 底层存储结构,使用map实现 
	mu                sync.RWMutex               // map本身非线程安全,操作时需要加锁
	onEvicted         func(string, interface{})  // 回调函数,当记录被删除时触发相应操作
	janitor           *janitor                   // 用于定时轮询失效的key
}
4. janitor:用于定时轮询失效的key,其中定义了轮询的周期和一个无缓存的channel,用来接收结束信息。
type janitor struct {
	Interval time.Duration // 定时轮询周期
	stop     chan bool     // 用来接收结束信息
}

func (j *janitor) Run(c *cache) {
	ticker := time.NewTicker(j.Interval) // 创建一个timeTicker定时触发
	for {
		select {
		case <-ticker.C:
			c.DeleteExpired()            // 调用DeleteExpired接口处理删除过期记录
		case <-j.stop:
			ticker.Stop()
			return
		}
	}
}
runtime.SetFinalizer
func stopJanitor(c *Cache) {
	c.janitor.stop <- true
}

func newCacheWithJanitor(de time.Duration, ci time.Duration, m map[string]Item) *Cache {
	c := newCache(de, m)
	// This trick ensures that the janitor goroutine (which--granted it
	// was enabled--is running DeleteExpired on c forever) does not keep
	// the returned C object from being garbage collected. When it is
	// garbage collected, the finalizer stops the janitor goroutine, after
	// which c can be collected.
	C := &Cache{c}
	if ci > 0 {
		runJanitor(c, ci)
		runtime.SetFinalizer(C, stopJanitor)
	}
	return C
}

可能的泄漏场景如下,使用者创建了一个cache对象,在使用后置为nil,在使用者看来在gc的时候会被回收,但是因为有goroutine在引用,在gc的时候不会被回收,因此导致了内存泄漏。

    c := cache.New()
    // do some operation
    c = nil
runtime.SetFinalizer

总结

  1. go-cache的源码代码里很小,代码结构和处理逻辑都比较简单,可以作为golang新手阅读的很好的素材。
  2. 对于单机轻量级的内存缓存如果仅从功能实现角度考虑,go-cache是一个不错的选择,使用简单。
  3. 但在实际使用中需要注意:
    • go-cache没有对内存使用大小或存储数量进行限制,可能会造成内存峰值较高;
    • go-cache中存储的value尽量使用指针类型,相比于存储对象,不仅在性能上会提高,在内存占用上也会有优势。由于golang的gc机制,map在扩容后原来占用的内存不会立刻释放,因此如果value存储的是对象会造成占用大量内存无法释放。