1. 简介
github仓库地址:https://github.com/patrickmn/go-cache
文档地址:https://pkg.go.dev/github.com/patrickmn/go-cache
go-cache 是一个 Go 语言实现的缓存库,用于在本地内存中保存 key/value 形式的缓存数据,适用于单机应用程序的缓存使用,支持删除、过期的功能。
go-cache 的主要优点在于,实现了线程安全的 map[string]interface{} 结构,以供多个协程安全地使用,并且缓存可以带有过期时间也可以长期存储。
go-cache 使用一个读写互斥锁来给一个KV缓存数据对象进行加锁,在大量 key 的情况下会造成锁竞争严重的情况。
2. 使用
安装方式:
使用 go get 将 go-cache 包下载到 GOPATH 指定的目录下。
go get github.com/patrickmn/go-cache
使用示例如下,引用自官方仓库 README 文件的使用示例。
import (
"time"
"github.com/patrickmn/go-cache"
)
func main() {
// 创建缓存,设置默认过期时间5分钟,每10分钟清除过期项
c := cache.New(5*time.Minute, 10*time.Minute)
// 设置key/value,过期时间使用缓存默认过期时间
c.Set("foo", "bar", cache.DefaultExpiration)
// 设置key/value,指定过期时间
c.Set("a", "b", time.Hour)
// 设置key/value,无过期时间
c.Set("baz", 42, cache.NoExpiration)
// 删除key
c.Delete("baz")
// 根据key获取value,并返回判断是否存在key,返回的value类型为interface{},需要转换为对应格式
if x, found := c.Get("foo"); found {
foo := x.(string)
}
// 保存指针提升性能
c.Set("foo", &MyStruct, cache.DefaultExpiration)
if x, found := c.Get("foo"); found {
foo := x.(*MyStruct)
}
}
接下来对每一段调用进行说明。
在 import 引入该包后,调用 New 函数创建一个缓存对象,并返回一个缓存指针变量。这里需要传递两个参数,第一个参数是指定 key/value 的默认过期时间,后面在设置 key/value 数据的时候可以指定过期时间,可以指定为使用缓存默认过期时间,也就是这里参数设置的时间。第二个参数表示清除过期的缓存数据的时间间隔,创建缓存对象后,会新建一个协程,定期对过期的数据进行清除,这里参数指定的就是这个定时清除的时间间隔。
// 创建缓存,设置默认过期时间5分钟,每10分钟清除过期项
c := cache.New(5*time.Minute, 10*time.Minute)
创建了缓存对象之后,就可以设置 key/value 数据保存到缓存对象中,以供需要的时候读取。这里的第三个参数的数据类型为 time.Duration,表示过期时间。下面三个方法调用分别对应三种用法,第一种是使用缓存对象创建时设置的默认过期时间,第二种是指定特定过期时间,第三种则无过期时间,也就是永不过期。
// 设置key/value,过期时间使用缓存默认过期时间
c.Set("foo", "bar", cache.DefaultExpiration)
// 设置key/value,指定过期时间
c.Set("a", "b", time.Hour)
// 设置key/value,无过期时间
c.Set("baz", 42, cache.NoExpiration)
删除方法非常简单,传入 key 的值即可,类似于对 map 对象的 delete 操作。
// 删除key
c.Delete("baz")
获取方法传入 key 的值,返回两个值,一个是 interface{} 类型的 value,一个是布尔类型的变量,用于表示是否存在 key/value,类似于对 map 对象的下标操作。当第二个返回值为 true 时,可以对第一个返回值进行类型转换,获得需要的数据。需要注意的是,类型转换有可能会失败,建议也加上能否转换成功的判断。
// 根据key获取value,并返回判断是否存在key,返回的value类型为interface{},需要转换为对应格式
if x, found := c.Get("foo"); found {
foo := x.(string)
}
下面进行源码分析的时候我们将会看到,对缓存对象设置进去的 key/value,数据会被保存到一个 map 中,对于 value 大小比较大的对象,将会使得缓存对象占用比较大的内存。因此,go-cache 的作者在说明文档中建议我们,可以保存对象的指针而非对象本身,小小的调整将可以减小内存占用,提高性能。
// 保存指针提升性能
c.Set("foo", &MyStruct, cache.DefaultExpiration)
if x, found := c.Get("foo"); found {
foo := x.(*MyStruct)
}
3. 开发文档
3.1 常量
这是 go-cache 库包含的两个常量,NoExpiration 用 -1 表示不过期,也就是永久有效,DefaultExpiration 用 0 表示使用缓存创建时设置的默认过期时间。
在设置具体 key 的过期时间时,代码将会判断传入的值,除了这两个设定好的过期时间,大于 0 的过期时间才是设置有效的过期时间。
const (
// 不过期
NoExpiration time.Duration = -1
// 使用创建cache对象时设置的默认过期时间
DefaultExpiration time.Duration = 0
)
3.2 Cache
Cache 结构体表示一个缓存对象,其中存储了许多 key/value 的映射关系,并且每个 key 都可以设置相应的过期时间。
类型定义:
type Cache struct {
// contains filtered or unexported fields
}
下面简要筛选出比较重要常用的方法的函数说明,包含创建、获取、设置、删除等操作。
// New 创建cache对象,指定默认过期时间和清理过期item的时间间隔
func New(defaultExpiration, cleanupInterval time.Duration) *Cache
// NewFrom 从map创建cache对象
func NewFrom(defaultExpiration, cleanupInterval time.Duration, items map[string]Item) *Cache
// Get 获取key对应的value
func (c Cache) Get(k string) (interface{}, bool)
func (c Cache) GetWithExpiration(k string) (interface{}, time.Time, bool)
// Set 设置key/value,如果已存在则替换
func (c Cache) Set(k string, x interface{}, d time.Duration)
// SetDefault 设置key/value,使用默认过期时间
func (c Cache) SetDefault(k string, x interface{})
// Add 添加key/value到缓存,仅当key不存在或已过期
func (c Cache) Add(k string, x interface{}, d time.Duration) error
// Replace 设置key对应的value,仅当key已存在且未过期
func (c Cache) Replace(k string, x interface{}, d time.Duration) error
// Delete 删除key
func (c Cache) Delete(k string)
// ItemCount item的数量,包含过期的key
func (c Cache) ItemCount() int
// Items 返回所有未过期的item
func (c Cache) Items() map[string]Item
// DeleteExpired 从缓存删除所有过期的key
func (c Cache) DeleteExpired()
// Flush 从缓存清楚所有key
func (c Cache) Flush()
// 将value加上n
func (c Cache) Increment(k string, n int64) error
func (c Cache) IncrementFloat64(k string, n float64) (float64, error)
func (c Cache) IncrementInt(k string, n int) (int, error)
func (c Cache) IncrementInt64(k string, n int64) (int64, error)
// 将value减去n
func (c Cache) Decrement(k string, n int64) error
func (c Cache) DecrementFloat64(k string, n float64) (float64, error)
func (c Cache) DecrementInt(k string, n int) (int, error)
func (c Cache) DecrementInt64(k string, n int64) (int64, error)
3.3 Item
Item 结构体表示具体存储的 value 以及过期时间,这两个数据打包起来,被存储在 Cache 结构体。
类型定义:
type Item struct {
Object interface{}
Expiration int64
}
Item 结构体只有一个方法,判断是否过期。
// Expired 是否已过期
func (item Item) Expired() bool
4. 实现原理
4.1 数据结构
Item 数据结构定义包含一个 interface{} 类型成员,用于保存缓存的对象。还有一个 int64 类型的过期时间成员,用来保存纳秒级的时间戳,如果是 0 则表示永不过期。
判断 Item 是否过期,需要判断是否为永不过期,再将设置的过期时间和当前时间做比较。
type Item struct {
Object interface{}
Expiration int64
}
// Returns true if the item has expired.
func (item Item) Expired() bool {
if item.Expiration == 0 {
return false
}
return time.Now().UnixNano() > item.Expiration
}
Cache 数据结构中包含一个对外不可见的结构体 cache 的指针。cache 结构体包含默认过期时间,也就是创建对象时设置的参数,一个类型为 map[string]Item 的 key/value 映射关系表,一个读写互斥锁,防止出现多协程读写造成的数据问题,一个删除key时的回调函数,一个定期清理器,定期删除过期元素。
type Cache struct {
*cache
}
type cache struct {
defaultExpiration time.Duration // 默认过期时间
items map[string]Item // 保存key/value
mu sync.RWMutex // 读写互斥锁
onEvicted func(string, interface{}) // 删除key时的回调函数
janitor *janitor // 定期清理器
}
4.2 函数和方法
go-cache 针对 Cache 类型的函数和方法有很多,这里只选出 New、Get、Set、Delete 这几个较为重要的函数和方法进行详细介绍说明。
4.2.1 New
New 函数会先进行初始化工作,初始化各个变量成员,然后会创建了一个 janitor 协程,这个协程将进行过期 item 的定时清理工作。
func New(defaultExpiration, cleanupInterval time.Duration) *Cache {
items := make(map[string]Item)
return newCacheWithJanitor(defaultExpiration, cleanupInterval, items)
}
func newCacheWithJanitor(de time.Duration, ci time.Duration, m map[string]Item) *Cache {
c := newCache(de, m)
C := &Cache{c}
if ci > 0 {
runJanitor(c, ci) // 执行janitor协程,定时清理过期item
runtime.SetFinalizer(C, stopJanitor) // 当进行垃圾回收时,发送信号以停止janitor协程
}
return C
}
func newCache(de time.Duration, m map[string]Item) *cache {
if de == 0 {
de = -1
}
c := &cache{
defaultExpiration: de,
items: m,
}
return c
}
janitor 可以理解为一个定期清理器。创建 Cache 时将会连带启动一个 janitor 协程,这个协程会定期清理过期 item,并且会监听包含停止信号的通道,以便终结协程。
type janitor struct {
Interval time.Duration // 清理时间间隔
stop chan bool // 是否停止
}
func runJanitor(c *cache, ci time.Duration) {
j := &janitor{
Interval: ci,
stop: make(chan bool),
}
c.janitor = j
go j.Run(c) // 新建janitor协程
}
func (j *janitor) Run(c *cache) {
ticker := time.NewTicker(j.Interval) // 启动定时器,时间间隔为创建Cache时参数设定的清理时间间隔
for {
select {
case <-ticker.C: // 定时器触发
c.DeleteExpired() // 清理过期item
case <-j.stop: // 接收到停止信号返回
ticker.Stop()
return
}
}
}
func stopJanitor(c *Cache) {
c.janitor.stop <- true // 对cache对象垃圾回收时,发送信号到通道
}
4.2.2 Get
Get 方法会先给缓存加上读锁,获取对应的 value,然后判断这个 value 是否有过期时间和是否已经过期,如果未过期则返回,最后解除读锁。
func (c *cache) Get(k string) (interface{}, bool) {
c.mu.RLock() // 加读锁
item, found := c.items[k] // 读取value
if !found {
c.mu.RUnlock()
return nil, false
}
if item.Expiration > 0 {
if time.Now().UnixNano() > item.Expiration {
c.mu.RUnlock()
return nil, false
}
}
c.mu.RUnlock() // 解锁
return item.Object, true
}
4.2.3 Set
Set 方法先给缓存加上写锁,然后设置 key/value,最后解除写锁。
func (c *cache) Set(k string, x interface{}, d time.Duration) {
var e int64
if d == DefaultExpiration {
d = c.defaultExpiration
}
if d > 0 {
e = time.Now().Add(d).UnixNano() // 设置过期时间
}
c.mu.Lock() // 加写锁
c.items[k] = Item{ // 设置key/value
Object: x,
Expiration: e,
}
c.mu.Unlock() // 解锁
}
4.2.4 Delete
Delete 方法先给缓存加上写锁,然后从映射表中删除 key,如果设置了回调函数则执行,最后解除写锁。
func (c *cache) Delete(k string) {
c.mu.Lock() // 加写锁
v, evicted := c.delete(k) // 删除并获得对应的value
c.mu.Unlock() // 解锁
if evicted {
c.onEvicted(k, v) // 删除key时的回调函数
}
}
func (c *cache) delete(k string) (interface{}, bool) {
if c.onEvicted != nil {
if v, found := c.items[k]; found {
delete(c.items, k)
return v.Object, true
}
}
delete(c.items, k) // 删除key
return nil, false
}