作者: Moon-Light-Dream
什么是 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 中的接口如下:
创建对象: 在使用前需要先创建 cache 对象
func New(defaultExpiration, cleanupInterval time.Duration) *Cache
: 指定默认有效时间和清除间隔, 创建 cache 对象.
如果 defaultExpiration<1 或是 NoExpiration,kv 中的数据不会被清理, 必须手动调用接口删除.
如果 cleanupInterval<1, 不会自动触发清理逻辑, 要手动触发 c.DeleteExpired().
func NewFrom(defaultExpiration, cleanupInterval time.Duration, items map[string]Item) *Cache
: 与上面接口的不同是, 入参增加了一个 map, 可以将已有数据按格式构造好, 直接创建 cache.
C(Create): 增加一条数据, go-cache 中有几个接口都能实现新增的功能, 但使用场景不同
func (c Cache) Add(k string, x interface{}, d time.Duration) error
: 只有当 key 不存在或 key 对应的 value 已经过期时, 可以增加成功; 否则, 会返回 error.
func (c Cache) Set(k string, x interface{}, d time.Duration)
: 在 cache 中增加一条 kv 记录.
如果 key 不存在, 增加一个 kv 记录; 如果 key 已经存在, 用新的 value 覆盖旧的 value.
对于有效时间 d, 如果是 0(DefaultExpiration)使用默认有效时间; 如果是 - 1(NoExpiration), 表示没有过期时间.
func (c Cache) SetDefault(k string, x interface{})
: 与 Set 用法一样, 只是这里的 TTL 使用默认有效时间.
R(Read): 只支持按 key 进行读取
func (c Cache) Get(k string) (interface{}, bool)
: 通过 key 获取 value, 如果 cache 中没有 key, 返回的 value 为 nil, 同时返回一个 bool 类型的参数表示 key 是否存在.
func (c Cache) GetWithExpiration(k string) (interface{}, time.Time, bool)
: 与 Get 接口的区别是, 返回参数中增加了 key 有效期的信息, 如果是不会过期的 key, 返回的是 time.Time 类型的零值.
U(Update): 按 key 进行更新
直接使用 Set 接口, 上面提到如果 key 已经存在会用新的 value 覆盖旧的 value, 也可以达到更新的效果.
func (c Cache) Replace(k string, x interface{}, d time.Duration) error
: 如果 key 存在且为过期, 将对应 value 更新为新的值; 否则返回 error.
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.
DecrementXXX: 对于 Decrement 接口中提到的各种类型, 还有对应的接口来处理, 同时这些接口可以得到 value 变化后的结果. 如
func (c *cache) DecrementInt8(k string, n int8) (int8, error)
, 从返回值中可以获取到 value-n 后的结果.
func (c Cache) Increment(k string, n int64) error
: 使用方法与 Decrement 相同, 将 key 对应的 value 加 n.
IncrementXXX: 使用方法与 DecrementXXX 相同.
- D(Delete)
- func (c Cache) Delete(k string)
: 按照 key 删除记录, 如果 key 不存在直接忽略, 不会报错.
func (c Cache) DeleteExpired()
: 在 cache 中删除所有已经过期的记录. cache 在声明的时候会指定自动清理的时间间隔, 使用者也可以通过这个接口手动触发.
func (c Cache) Flush()
: 将 cache 清空, 删除所有记录.
其他接口:
func (c Cache) ItemCount() int
: 返回 cache 中的记录数量. 需要注意的是, 返回的数值可能会比实际能获取到的数值大, 对于已经过期但还没有即使清理的记录也会被统计.
func (c *cache) OnEvicted(f func(string, interface{}))
: 设置一个回调函数 (可选项), 当一条记录从 cache 中删除(使用者主动 delete 或 cache 自助清理过期记录) 时, 调用该函数. 设置为 nil 关闭操作.
安装 go-cache 包
介绍了 go-cache 的常用接口, 接下来从代码中看看如何使用. 在 coding 前需要安装 go-cache, 命令如下.
go get GitHub.com/patrickmn/go-cache
一个 Demo
如何在 golang 中使用上述接口实现 kv 数据库的增删改查, 接下来看一个 demo. 其他更多接口的用法和更详细的说明, 可以参考 GoDoc https://godoc.org/github.com/patrickmn/go-cache .
- 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
- }
- }
- }
对于 janitor 的处理, 这里使用的技巧值得学习, 下面这段代码是在 New() cache 对象时, 会同时开启一个 goroutine 跑 janitor, 在 run 之后可以看到做了 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
解决方案可以增加 Close 接口, 在使用后调用 Close 接口, 通过 channel 传递信息结束 goroutine, 但如果使用者在使用后忘了调用 Close 接口, 还是会造成内存泄漏.
另外一种解决方法是使用 runtime.SetFinalizer, 不需要用户显式关闭, gc 在检查 C 这个对象没有引用之后, gc 会执行关联的 SetFinalizer 函数, 主动终止 goroutine, 并取消对象 C 与 SetFinalizer 函数的关联关系. 这样下次 gc 时, 对象 C 没有任何引用, 就可以被 gc 回收了.
总结
go-cache 的源码代码里很小, 代码结构和处理逻辑都比较简单, 可以作为 golang 新手阅读的很好的素材.
对于单机轻量级的内存缓存如果仅从功能实现角度考虑, go-cache 是一个不错的选择, 使用简单.
但在实际使用中需要注意:
go-cache 没有对内存使用大小或存储数量进行限制, 可能会造成内存峰值较高;
go-cache 中存储的 value 尽量使用指针类型, 相比于存储对象, 不仅在性能上会提高, 在内存占用上也会有优势. 由于 golang 的 gc 机制, map 在扩容后原来占用的内存不会立刻释放, 因此如果 value 存储的是对象会造成占用大量内存无法释放.
来源: https://www.cnblogs.com/Moon-Light-Dream/p/12494683.html