cache2go源码阅读
最近做的一个tinyurl里用到了一个cache2go的库, 代码不多, 功能代码不到1000行, 但是对理解Go并发编程很有用, 这里来阅读一下.
代码结构
代码结构很简单, 实现功能的文件其实只有三个
├── LICENSE.txt
├── README.md
├── benchmark_test.go
├── cache.go // 功能文件
├── cache_test.go
├── cacheitem.go // 功能文件
├── cachetable.go // 功能文件
├── errors.go
└── examples
├── callbacks
│ └── callbacks.go
├── dataloader
│ └── dataloader.go
└── mycachedapp
└── mycachedapp.go
数据结构
cacheitem
除了实现对data访问的功能, 还实现了读锁, 统计访问次数, 生命周期等功能
// cacheitem
type CacheItem struct {
sync.RWMutex
// The item's key.
key interface{}
// The item's data.
data interface{}
// How long will the item live in the cache when not being accessed/kept alive.
lifeSpan time.Duration
// Creation timestamp.
createdOn time.Time
// Last access timestamp.
accessedOn time.Time
// How often the item was accessed.
accessCount int64
// Callback method triggered right before removing the item from the cache
aboutToExpire func(key interface{})
}
cachetable
cacheitem的容器, 除了实现对cacheitem的操作, 还实现了定时检查过期, 触发器, 迭代器, 和简单的统计功能
// cachetabel
// 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
// The logger used for this table.
logger *log.Logger
// 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)
}
笔记
以下就是随手记的, 并没有什么条理
- 与单线程不同的是, 在对cachetable操作时都要加上读写锁, 保证并发访问正确
- 增/删项目时, 前后都可以设置触发函数
过期检查
思想就是在item找到生命周期最短的那个, 设置一个定时器到时进行过期检查, 这样可以防止, 单独开一个Goroutine空转影响性能 在每次Add时要调用一下. 因为增加之后需要对找新的生命周期最短的
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()
}