一、简介
二、带着问题
三、源码
3.1 代码结构
3.2 核心代码
源码学习 https://github.com/muesli/cache2go
一、简介
cache2go是一个简单的缓存库,大概500行代码。
- 支持并发安全
- 支持过期清除
- 支持访问计数
- 配置回调函数
二、带着问题
- 并发的使用
- 过期清除如何实现的
- 回掉函数的配置
三、源码
3.1 代码结构
- Cache.go 缓存库
- CacheTable.go 缓存表
- CacheItem.go 缓存项
3.2 核心代码
- CacheItem
type CacheItem struct {
sync.RWMutex
key interface{}
data interface{}
//每个item都有一个访问时间和访问次数,以及存活时间。
lifeSpan time.Duration
createdOn time.Time
accessedOn time.Time
accessCount int64
aboutToExpire []func(key interface{}) //支持回调函数
}
每次访问item时更新accessedOn和accessCount;
每次清理时,判断now-accessedOn是否已大于存活时间,来决定清理;
// KeepAlive marks an item to be kept for another expireDuration period.
func (item *CacheItem) KeepAlive() {
item.Lock()
defer item.Unlock()
item.accessedOn = time.Now()
item.accessCount++
}
- CacheTable
// CacheTable is a table within the cache
type CacheTable struct {
sync.RWMutex
// The table's name.
name string
// All cached items.
items map[interface{}]*CacheItem
// Timer responsible for triggering cleanup.
cleanupTimer *time.Timer //以表的维度,来清理过期数据
// Current timer duration.
cleanupInterval time.Duration
// Callback method triggered when trying to load a non-existing key.
loadData func(key interface{}, args ...interface{}) *CacheItem
// Callback method triggered when adding a new item to the cache.
addedItem []func(item *CacheItem)
// Callback method triggered before deleting an item from the cache.
aboutToDeleteItem []func(item *CacheItem)
}
Add
回调函数的调用;
每次添加,如果存活时间<清理周期,会触发一次清理;
func (table *CacheTable) Add(key interface{}, lifeSpan time.Duration, data interface{}) *CacheItem {
item := NewCacheItem(key, lifeSpan, data)
// Add item to cache.
table.Lock()
table.items[item.key] = item
// Cache values so we don't keep blocking the mutex.
expDur := table.cleanupInterval
addedItem := table.addedItem
table.Unlock()
// Trigger callback after adding an item to cache. 回掉函数的触发
if addedItem != nil {
for _, callback := range addedItem {
callback(item)
}
}
// If we haven't set up any expiration check timer or found a more imminent item.
if item.lifeSpan > 0 && (expDur == 0 || item.lifeSpan < expDur) {
table.expirationCheck()
}
return item
}
Delete
// Delete an item from the cache.
func (table *CacheTable) Delete(key interface{}) (*CacheItem, error) {
table.Lock()
defer table.Unlock()
r, ok := table.items[key]
if !ok {
return nil, ErrKeyNotFound
}
// Cache value so we don't keep blocking the mutex.
aboutToDeleteItem := table.aboutToDeleteItem
table.Unlock()
// Trigger callbacks before deleting an item from cache.
if aboutToDeleteItem != nil {
for _, callback := range aboutToDeleteItem {
callback(r)
}
}
r.RLock()
defer r.RUnlock()
if r.aboutToExpire != nil {
for _, callback := range r.aboutToExpire {
callback(key)
}
}
table.Lock()
delete(table.items, key)
return r, nil
}
Value
如果存在,更新item的访问;
如果不存在,支持自定义加载函数;
func (table *CacheTable) Value(key interface{}, args ...interface{}) (*CacheItem, error) {
table.RLock()
r, ok := table.items[key]
loadData := table.loadData
table.RUnlock()
if ok {
// Update access counter and timestamp.
r.KeepAlive()
return r, nil
}
// Item doesn't exist in cache. Try and fetch it with a data-loader.
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
}
定期清理
清理实际上是遍历每个item,判断它的过期时间,过期就删除;
清理的触发:一是每次添加时,二是每次清理之后创建一个计时器;
非固定周期的清理,基于item的存活时间来调整;
// Expiration check loop, triggered by a self-adjusting timer.
func (table *CacheTable) expirationCheck() {
table.Lock()
if table.cleanupTimer != nil {
table.cleanupTimer.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)
}
// To be more accurate with timers, we would need to update 'now' on every
// loop iteration. Not sure it's really efficient though.
now := time.Now()
smallestDuration := 0 * time.Second
for key, item := range table.items {
// Cache values so we don't keep blocking the mutex.
item.RLock()
lifeSpan := item.lifeSpan
accessedOn := item.accessedOn
item.RUnlock()
if lifeSpan == 0 {
continue
}
if now.Sub(accessedOn) >= lifeSpan {
// Item has excessed its lifespan.
table.deleteInternal(key)
} else {
// Find the item chronologically closest to its end-of-lifespan.
if smallestDuration == 0 || lifeSpan-now.Sub(accessedOn) < smallestDuration {
smallestDuration = lifeSpan - now.Sub(accessedOn)
}
}
}
// Setup the interval for the next cleanup run.
table.cleanupInterval = smallestDuration
if smallestDuration > 0 {
table.cleanupTimer = time.AfterFunc(smallestDuration, func() {
go table.expirationCheck()
})
}
table.Unlock()
}
四、思考
- 锁的类型:创建后一成不变的可以不加锁,只读加读锁,写入加写锁;
- 锁的范围:加锁获取数据后,后续操作缓存数据不再加锁;
- 锁的粒度:对数据表的操作,获取表锁。对于某个item的操作,获取item的锁;