地址

1
github.com/patrickmn/go-cache

简介

  • go-cache实质上就是**拥有过期时间并且线程安全的map[string]interface{}**,可以被多个goroutine安全访问。
  • 对于每组KV数据可以设置不同的TTL(也可以永久存储),并可以自动实现过期清理。
  • go-cache是一款类似于 memached 的key/value 缓存软件。它比较适用于单机执行的应用程序。
  • 在使用时一般都是将go-cache作为数据缓存来使用,而不是持久性的数据存储。对于停机后快速恢复的场景,go-cache支持将缓存数据保存到文件,恢复时从文件中load数据加载到内存。

入门使用

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
package main

import (
"log"
"time"
"github.com/patrickmn/go-cache"
)


func main(){
c := cache.New(30*time.Second, 10*time.Second)
c.Set("Title", "Spring Festival", cache.DefaultExpiration)

value, found := c.Get("Title")
if found {
log.Println("found:", value)
} else {
log.Println("not found")
}

time.Sleep(60*time.Second)
log.Println("sleep 60s...")

value, found = c.Get("Title")
if found {
log.Println("found:", value)
} else {
log.Println("not found")
}
}

// 2019/07/08 20:21:13 found: Spring Festival
// 2019/07/08 20:22:13 sleep 60s...
// 2019/07/08 20:22:13 not found

cache定期清除缓存中的过期key,是通过一个常驻goroutine实现的。

常用接口分析

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

New

在使用前需要先创建cache对象

  1. func New(defaultExpiration, cleanupInterval time.Duration) *Cache:指定默认有效时间和清除间隔,创建cache对象。
    • 如果defaultExpiration<1或是NoExpiration,kv中的数据不会被清理,必须手动调用接口删除。
    • 如果cleanupInterval<1,不会自动触发清理逻辑,要手动触发c.DeleteExpired()。
  2. func NewFrom(defaultExpiration, cleanupInterval time.Duration, items map[string]Item) *Cache:与上面接口的不同是,入参增加了一个map,可以将已有数据按格式构造好,直接创建cache。

C

增加一条数据,go-cache中有几个接口都能实现新增的功能,但使用场景不同

  1. func (c Cache) Add(k string, x interface{}, d time.Duration) error:只有当key不存在或key对应的value已经过期时,可以增加成功;否则,会返回error。
  2. func (c Cache) Set(k string, x interface{}, d time.Duration):在cache中增加一条kv记录。
    • 如果key不存在,增加一个kv记录;如果key已经存在,用新的value覆盖旧的value。
    • 对于有效时间d,如果是0(DefaultExpiration)使用默认有效时间;如果是-1(NoExpiration),表示没有过期时间。
  3. func (c Cache) SetDefault(k string, x interface{}):与Set用法一样,只是这里的TTL使用默认有效时间。

R

只支持按key进行读取

  1. func (c Cache) Get(k string) (interface{}, bool) :通过key获取value,如果cache中没有key,返回的value为nil,同时返回一个bool类型的参数表示key是否存在。
  2. func (c Cache) GetWithExpiration(k string) (interface{}, time.Time, bool):与Get接口的区别是,返回参数中增加了key有效期的信息,如果是不会过期的key,返回的是time.Time类型的零值。

U

按key进行更新

  1. 直接使用Set接口,上面提到如果key已经存在会用新的value覆盖旧的value,也可以达到更新的效果。
  2. func (c Cache) Replace(k string, x interface{}, d time.Duration) error:如果key存在且为过期,将对应value更新为新的值;否则返回error。
  3. func (c Cache) Decrement(k string, n int64) error:对于cache中value是int, int8, int16, int32, int64, uintptr, uint,uint8, uint32, or uint64, float32,float64这些类型记录,可以使用该接口,将value值减n。如果key不存在或value不是上述类型,会返回error。
  4. DecrementXXX:对于Decrement接口中提到的各种类型,还有对应的接口来处理,同时这些接口可以得到value变化后的结果。如func (c *cache) DecrementInt8(k string, n int8) (int8, error),从返回值中可以获取到value-n后的结果。
  5. func (c Cache) Increment(k string, n int64) error:使用方法与Decrement相同,将key对应的value加n。
  6. IncrementXXX:使用方法与DecrementXXX相同。

D

  1. func (c Cache) Delete(k string):按照key删除记录,如果key不存在直接忽略,不会报错。
  2. func (c Cache) DeleteExpired():在cache中删除所有已经过期的记录。cache在声明的时候会指定自动清理的时间间隔,使用者也可以通过这个接口手动触发。
  3. func (c Cache) Flush():将cache清空,删除所有记录。

other

  1. func (c Cache) ItemCount() int:返回cache中的记录数量。需要注意的是,返回的数值可能会比实际能获取到的数值大,对于已经过期但还没有即使清理的记录也会被统计。
  2. func (c *cache) OnEvicted(f func(string, interface{})):设置一个回调函数(可选项),当一条记录从cache中删除(使用者主动delete或cache自助清理过期记录)时,调用该函数。设置为nil关闭操作。

go-cache核心代码

  1. 常量:内部定义的两个常量NoExpirationDefaultExpiration,可以作为上面接口中的入参,NoExpiration表示没有设置有效时间,DefaultExpiration表示使用New()或NewFrom()创建cache对象时传入的默认有效时间。

  2. Item:cache中存储的value类型,Object是真正的值,Expiration表示过期时间。可以使用Item的Expired()接口确定是否到期,实现方式是过比较当前时间和Item设置的到期时间来判断是否过期。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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的核心数据结构,其中定义了每条记录的默认过期时间,底层的存储结构等信息。

    1
    2
    3
    4
    5
    6
    7
    type cache struct {
    defaultExpiration time.Duration //默认的通用key实效时长
    items map[string]Item //底层的map存储
    mu sync.RWMutex //由于map是非线程安全的,增加的全局锁
    onEvicted func(string, interface{}) //失效key时,会触发的回调函数
    janitor *janitor //监视器,Goroutine,定时轮询用于失效key
    }
  4. janitor:用于定时轮询失效的key,其中定义了轮询的周期和一个无缓存的channel,用来接收结束信息。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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
    }
    }
    }

对于janitor的处理,这里使用的技巧值得学习 ,

下面这段代码是在New() cache对象时,会同时开启一个goroutine跑janitor,在run之后可以看到做了runtime.SetFinalizer的处理,这样处理了可能存在的内存泄漏问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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的时候不会被回收,因此导致了内存泄漏。

1
2
3
c := cache.New()
// do some operation
c = nil

解决方案可以增加Close接口,在使用后调用Close接口,通过channel传递信息结束goroutine,但如果使用者在使用后忘了调用Close接口,还是会造成内存泄漏。
另外一种解决方法是使用runtime.SetFinalizer,不需要用户显式关闭, gc在检查C这个对象没有引用之后, gc会执行关联的SetFinalizer函数,主动终止goroutine,并取消对象C与SetFinalizer函数的关联关系。这样下次gc时,对象C没有任何引用,就可以被gc回收了。

总结

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