目录
一、cache2go 源码地址
https://github.com/muesli/cache2go
根据网上极其雷同的众多推荐称,此为对go语言入门小白非常友好的开源项目。废话少说,下面我从我自己的角度做一下学习笔记(欢迎指正)。
二、cache2Go干啥用的
根据一些网上此源码读者的总结,以及我对该项目粗浅的学习,此项目可实现并发安全的缓存。
通过此项目可以学习到锁的使用、缓存命中和过期的处理思想。
三、本次阅读源码采用的方式
1、自底向上法:从最底层代码开始读起,逐层向上阅读其调用方具体实现。
2、阅读基础源码后,对具体例子进行学习
四、代码目录结构
其中examples里面是具体的示例,我们读完主要源码文件后阅读此部分内容。
两个_test.go文件为测试文件
errors.go为错误说明变量,不重要
核心的三个文件是 cache.go 、cachetable.go 、cacheitem.go 其中调用为从左到右,根据网上大佬建议,我们从item开始阅读,采用自底向上法。
五、cacheItem.go 源码解读
此文件主要定义Item结构体,以及一些对Item的处理,例如获取创建时间、key、data、存活期、设置访问时间和访问次数、对item过期清理前回调函数的增删等功能。
我将我的解读以中文注释的方式填鸭在源码中,如有问题欢迎指正。
type CacheItem struct {
//定义读写锁
//这里涉及到一个知识点,即golang中有两种锁,一种是互斥锁,一种是读写锁,互斥锁比较简单粗暴,性能较差,此处作者采用的是读写锁
sync.RWMutex
//item的key
key interface{}
//item的value
data interface{}
//一个item多久不被访问就被清理
lifeSpan time.Duration
//创建时间
createdOn time.Time
//访问时间
accessedOn time.Time
//访问次数
accessCount int64
//在item删除前触发回调方法,可以触发多个函数
aboutToExpire []func(key interface{})
}
//创建Item的方法
//指定创建时间和访问时间为当前时间,并将参数中的key、data、lifeSpan分别赋值
//返回初始化后的Item(注意此处有个知识点,在go语言中,struct类型是传值,因此需要采用指针的形式返回)
func NewCacheItem(key interface{}, lifeSpan time.Duration, data interface{}) *CacheItem {
t := time.Now()
return &CacheItem{
key: key,
lifeSpan: lifeSpan,
createdOn: t,
accessedOn: t,
accessCount: 0,
aboutToExpire: nil,
data: data,
}
}
以上代码片段做了两件事
1、定义了一个struct,用于存放 item 的相关信息。
2、给CacheItem类型分配一片内存空间并返回指向这片内存空间的指针。注意,此处也可采用new的方式,从文档看,new方法和&方法没有本质区别。
//此方法返回Item的 lifeSpan 的值,由于存活周期不会更改,因此无需加锁
func (item *CacheItem) LifeSpan() time.Duration {
return item.lifeSpan
}
//获取Item的创建时间,由于创建时间不会更改,因此无需加锁
func (item *CacheItem) CreatedOn() time.Time {
return item.createdOn
}
//获取Item的Key,由于Key不会更改,因此无需加锁
func (item *CacheItem) Key() interface{} {
return item.key
}
//获取Item的Data,由于Data不会更改,因此无需加锁
func (item *CacheItem) Data() interface{} {
return item.data
}
上面的四个方法,都是直接返回Item中的值,因为这四个值都是一旦设定不可更改。因此无需加锁。
//此方法重新设置Item的访问日期为当前时间,并将访问次数+1
//为防止对同一Item的并发操作,对其进行读写锁的写锁加锁
func (item *CacheItem) KeepAlive() {
item.Lock()
defer item.Unlock()
item.accessedOn = time.Now()
item.accessCount++
}
//获取Item上一次访问的时间
//由于此时间可改,因此需加锁处理
func (item *CacheItem) AccessedOn() time.Time {
item.Lock()
defer item.Unlock()
return item.accessedOn
}
以上方法都做了写锁处理,因为这两个字段在设置之后可以进行编辑,为了保证数据安全,此处加锁处理。
//清理Item的回调函数列表
func (item *CacheItem) RemoveAboutToExpireCallback() {
item.Lock()
defer item.Unlock()
//这里有一个小知识点
//如果要清空一个slice,那么可以简单的赋值为nil,垃圾回收器会自动回收原有的数据。
//但是如果还需要使用 slice 底层内存,那么最佳的方式是 re-slice:[:0]
//此功能需要的是回收原有数据,不保留底层内存,因此采用nil的方法清空
item.aboutToExpire = nil
}
//重置Item的回调函数列表
func (item *CacheItem) SetAboutToExpireCallback(f func(interface{})) {
if len(item.aboutToExpire) > 0 {
item.RemoveAboutToExpireCallback()
}
item.Lock()
defer item.Unlock()
item.aboutToExpire = append(item.aboutToExpire, f)
}
//Item的回调函数列表追加一个新函数
func (item *CacheItem) AddAboutToExpireCallback(f func(interface{})) {
item.Lock()
defer item.Unlock()
item.aboutToExpire = append(item.aboutToExpire, f)
}
以上三个方法同样需要加锁处理,且均为对回调函数列表的处理,分别为清除、重置和新增。
六、cachetable.go 源码解读
下面一块是table的结构体,没什么特殊内容
type CacheTable struct {
//定义读写锁
sync.RWMutex
//定义table的name
name string
//定义Items,以map形式存放
items map[interface{}]*CacheItem
//负责触发清理的计时器
cleanupTimer *time.Timer
//当前的定时器时间区间
cleanupInterval time.Duration
//用于记录日志
logger *log.Logger
//当试图加载一个不存在的key时触发的回调方法,并返回item
loadData func(key interface{}, args ...interface{}) *CacheItem
//当向缓存增加一个新的item时触发的回调方法
addedItem []func(item *CacheItem)
//当从缓存删除一个item时触发的回调方法
aboutToDeleteItem []func(item *CacheItem)
}
下面是一些非常简单的取值或赋值操作
//读取当前Table中item的个数
func (table *CacheTable) Count() int {
table.Lock()
defer table.Unlock()
return len(table.items)
}
//根据参数指定的函数遍历处理所有item
func (table *CacheTable) Foreach(trans func(key interface{}, item *CacheItem)) {
table.Lock()
defer table.Unlock()
for k, v := range table.items {
trans(k, v)
}
}
//英翻:SetDataLoader配置数据加载器回调,当试图访问不存在的键时将调用该回调。The key and 0…n个额外的参数被传递给回调函数。
func (table *CacheTable) SetDataLoader(f func(interface{}, ...interface{}) *CacheItem) {
table.Lock()
defer table.Unlock()
table.loadData = f
}
func (table *CacheTable) RemoveAddedItemCallbacks() {
table.Lock()
defer table.Unlock()
table.addedItem = nil
}
//这个方法和item中的一样
func (table *CacheTable) SetAddedItemCallback(f func(item *CacheItem)) {
if len(table.addedItem) > 0 {
table.RemoveAddedItemCallbacks()
}
table.Lock()
defer table.Unlock()
table.addedItem = append(table.addedItem, f)
}
func (table *CacheTable) SetAboutToDeleteItemCallback(f func(item *CacheItem)) {
if len(table.addedItem) > 0 {
table.RemoveAddedItemCallbacks()
}
table.Lock()
defer table.Unlock()
table.addedItem = append(table.addedItem, f)
}
func (table *CacheTable) SetLogger(logger *log.Logger) {
table.Lock()
defer table.Unlock()
table.logger = logger
}
func (table *CacheTable) log(v ...interface{}) {
if table.logger == nil {
return
}
table.logger.Println(v...)
}
下面是一个比较重要的过期检查方法
//过期检查循环
//循环每一个item的过期时间,若未设定,则continue,若设定了,则取到最近要过期的一条,获取其剩余生命时间,并指定在这个时间之后再次执行循环检查
//若item已过期,则执行清理操作,注意需要同时触发table和item的相关回调函数
//删除的内部处理
//1.table的删除前回调
//2.item的删除前回调
//3.清理table的item
func (table *CacheTable) deleteInternal(key interface{}) (*CacheItem, error) {
r, ok := table.items[key]
if !ok {
return nil, ErrKeyNotFound
}
aboutToDeleteItem := table.aboutToDeleteItem
//由于后续的动作可能耗时比较长,所以将数据拿到后,关闭锁,待处理完再重新加锁
table.Unlock()
//删除前的回调处理
if aboutToDeleteItem != nil {
for _, callback := range aboutToDeleteItem {
callback(r)
}
}
//由于是对item中的回调处理
r.RLock()
defer r.Unlock()
if r.aboutToExpire != nil {
for _, callback := range r.aboutToExpire {
callback(key)
}
}
table.Lock() //上面解锁,下面加锁,正好是一对儿
table.log("Deleting item with key ", key, " created on ", r.createdOn, " and hit ", r.accessCount, " times from table ", table.name)
delete(table.items, key)
return r, nil
}
//过期检查循环
//循环每一个item的过期时间,若未设定,则continue,若设定了,则取到最近要过期的一条,获取其剩余生命时间,并指定在这个时间之后再次执行循环检查
//若item已过期,则执行清理操作,注意需要同时触发table和item的相关回调函数
func (table *CacheTable) expirationCheck() {
table.Lock()
if table.cleanupTimer != nil {
table.cleanupTimer.Stop() //这里为啥stop呢
}
if table.cleanupInterval > 0 {
table.log("Expiration check triggered after ", table.cleanupInterval, " for table ", table.name)
} else {
table.log("Expiration check installed for table ", table.name)
}
now := time.Now()
smallestDuration := 0 * time.Second
for key, item := range table.items {
item.RLock() //对item加读锁,禁止此期间对item的修改
lifeSpan := item.lifeSpan
accessedOn := item.accessedOn
item.RUnlock()
if lifeSpan == 0 {
continue //当为0时,默认不检查过期
}
if now.Sub(accessedOn) >= lifeSpan {
//item最近一次访问时间距离现在的时间差已超过设定的超时时间,因此需要进行清除动作
_, _ = table.deleteInternal(key)
} else {
if smallestDuration == 0 || lifeSpan-now.Sub(accessedOn) < smallestDuration {
smallestDuration = lifeSpan - now.Sub(accessedOn) //取最近要失效的item所剩存活时间
}
}
}
//将触发清理的时间间隔设置为最近要失效的item所剩存活时间
table.cleanupInterval = smallestDuration
if smallestDuration > 0 {
table.cleanupTimer = time.AfterFunc(smallestDuration, func() {
//设置定时任务,在最近要失效的item所剩存活时间之后,立刻再次执行过期检查,协程方式
go table.expirationCheck()
})
}
table.Unlock()
}
下面这一组主要是增加item和删除item以及清空table中所有item的相关方法,也比较简单
func (table *CacheTable) addInternal(item *CacheItem) {
table.log("Adding item with key ", item.key, " add lifespan of ", item.lifeSpan, " to table ", table.name)
table.items[item.key] = item
expDur := table.cleanupInterval
addedItem := table.addedItem
//刚开始读这里时没明白,为啥要在上层lock,在这一层unlock,后来查看了这个方法的调用方,才发现这完全是由于调用方的实现导致的
table.Unlock()
if addedItem != nil {
//执行增加条目回调函数
for _, callback := range addedItem {
callback(item)
}
}
//如果未设置过期时间,或指定的过期时间比现有table设定的清理定时器触发时间要短,则触发一次过期检查
if item.lifeSpan > 0 && (expDur == 0 || item.lifeSpan < expDur) {
table.expirationCheck()
}
}
//向table增加item
func (table *CacheTable) Add(key interface{}, lifeSpan time.Duration, data interface{}) *CacheItem {
item := NewCacheItem(key, lifeSpan, data)
table.Lock()
table.addInternal(item)
return item
}
func (table *CacheTable) Delete(key interface{}) (*CacheItem, error) {
table.Lock()
defer table.Unlock()
return table.deleteInternal(key)
}
//单纯检查key对应的item是否存在,有就true,没有就false,没有多余动作
func (table *CacheTable) Exists(key interface{}) bool {
table.RLock()
defer table.RLock()
_, ok := table.items[key]
return ok
}
//判断key是否已存在,若存在,则返回false,否则,增加一条,并返回true
func (table *CacheTable) NotFoundAdd(key interface{}, lifeSpan time.Duration, data interface{}) bool {
table.Lock()
if _, ok := table.items[key]; ok {
table.Unlock()
return false
}
item := NewCacheItem(key, lifeSpan, data)
table.addInternal(item)
return true
}
//判断key是否存在,存在则重置生命周期
//若不存在,看是否设定了对应的回调,若设定了回调,且回调成功,则将返回值增加item
//若不存在,且未设定回调,返回对应的错误信息,提示没找到key
func (table *CacheTable) Value(key interface{}, args ...interface{}) (*CacheItem, error) {
table.RLock()
r, ok := table.items[key]
loadData := table.loadData
table.Unlock()
if ok {
//如果key存在,更新一下item的最近访问时间和访问次数,防过期
r.KeepAlive()
return r, nil
}
//数据不存在且设定了对应的回调函数,则进行触发动作,并在触发成功后根据返回值增加item
if loadData != nil {
item := loadData(key, args...)
if item != nil {
table.Add(key, item.lifeSpan, item.data)
return item, nil
}
return nil, ErrKeyNotFoundOrLoadable
}
return nil, ErrKeyNotFound
}
//清空table的条目和定时清理信息
func (table *CacheTable) Flush() {
table.Lock()
defer table.Unlock()
table.log("Flushing table ", table.name)
table.items = make(map[interface{}]*CacheItem)
table.cleanupInterval = 0
if table.cleanupTimer != nil {
table.cleanupTimer.Stop() //停止计时
}
}
下面这组方法,是根据访问次数从大到小排序所有item。这里隐藏的一个知识点是,slice顺序,map乱序,因此使用slice
//下面是一套排序方法组, 重写sort
type CacheItemPair struct {
Key interface{}
AccessCount int64
}
type CacheItemPairList []CacheItemPair
func (p CacheItemPairList) Swap(i, j int) {
p[i], p[j] = p[j], p[i]
}
func (p CacheItemPairList) Len() int {
return len(p)
}
func (p CacheItemPairList) Less(i, j int) bool {
return p[i].AccessCount > p[j].AccessCount
}
//将items按访问次数从大到小排序后返回
func (table *CacheTable) MostAccessed(count int64) []*CacheItem {
table.RLock()
defer table.Unlock()
p := make(CacheItemPairList, len(table.items))
i := 0
for k, v := range table.items {
p[i] = CacheItemPair{k, v.accessCount}
i++
}
sort.Sort(p)
var r []*CacheItem
c := int64(0)
for _, v := range p {
if c >= count {
break
}
item, ok := table.items[v.Key]
if ok {
r = append(r, item)
}
c++
}
return r
}
七、cache.go
var (
cache = make(map[string]*CacheTable)
mutex sync.RWMutex
)
//定义一个cache池子,根据name来找,若存在,则返回,不存在则创建一个返回
func Cache(table string) *CacheTable {
mutex.RLock()
t, ok := cache[table]
mutex.RUnlock()
if !ok {
mutex.Lock()
t, ok = cache[table] //上面已经解锁,为防冲突,写锁后二次检查
if !ok {
t = &CacheTable{
name: table,
items: make(map[interface{}]*CacheItem),
}
cache[table] = t
}
mutex.Unlock()
}
return t
}
上面这段也没什么比较重要的知识点。一个关于并发安全的小细节就是在下面写锁锁定后,又判断了一下table是否已存在于缓存中。即做了二次检查。
八、示例
mycachedapp.go (说明,由于我开发环境原因,只能使用test文件测试以及运行执行结果,因此下面的TestApp替换源码的main)
// Keys & values in cache2go can be of arbitrary types, e.g. a struct.
type myStruct struct {
text string
moreData []byte
}
func TestApp(t *testing.T) {
//调用cache.go的cache方法,如果有name=myCache的Table就直接返回,没有就创建一个
cache := cache2go.Cache("myCache")
//定义一个myStruct类型的变量,并初始化
val := myStruct{"This is a test!", []byte{}}
//向name=myCache的table中增加一个key为someKey的item
cache.Add("someKey", 5*time.Second, &val)
//调用cacheTable.go文件的Value方法
res, err := cache.Value("someKey")
if err == nil {
fmt.Println("Found value in cache:", res.Data().(*myStruct).text)
} else {
fmt.Println("Error retrieving value from cache:", err)
}
//强制休眠6S,目的是使key=someKey的item过期
time.Sleep(6 * time.Second)
res, err = cache.Value("someKey")
if err != nil {
//由于没有找到,且没有定义增加item的回调,因此会返回ErrKeyNotFound
fmt.Println("Item is not cached (anymore). ", err.Error())
}
//重新新增key=someKey的item,不设置生存周期
cache.Add("someKey", 0, &val)
//设置一个删除时的回调
cache.SetAboutToDeleteItemCallback(func(e *cache2go.CacheItem) {
fmt.Println("Deleting:", e.Key(), e.Data().(*myStruct).text, e.CreatedOn())
})
//删除时触发删除动作和上面的回调函数
cache.Delete("someKey")
//cache的table清空所有item
cache.Flush()
}
运行结果
Found value in cache: This is a test!
Item is not cached (anymore). Key not found in cache
Deleting: someKey This is a test! 2021-01-15 14:42:48.9542564 +0800 CST m=+6.006042201
九、本文推荐参考阅读
https://segmentfault.com/a/1190000022844771
https://segmentfault.com/a/1190000019682392
http://bigdatadecode.club/golang-cache2go-src.html
https://juejin.cn/post/6856706924680282119
https://www.shuzhiduo.com/A/QW5Y00YGJm/