cache2go是一个用Go实现的并发安全的缓存库,实现了如下特性:
- 并发安全;
- 可设置每条缓存的超时时间;
- 内置缓存访问计数;
- 自调节的缓存过期检查;
- 可设置缓存增加/删除回调函数;
- and so on...
这个库代码量很少,核心代码就三个文件,里面设计的技术点主要包括读写锁、goroutine、map操作等。作为Go语言学习样例也非常不错。
1. 源码解析
cache2go中主要涉及两个类型CacheItem
、CacheTable
:
- 一个
CacheItem
代表一条缓存; - 一个
CacheTable
代表一个缓存表,由一条条缓存组成。
下面我们分别介绍。
1.1 单条缓存CacheItem
CacheItem
的结构如下:
type CacheItem struct {
sync.RWMutex
// 缓存项的key.
key interface{}
// 缓存项的值.
data interface{}
// 缓存项的生命期
lifeSpan time.Duration
// 缓存项的创建时间戳
createdOn time.Time
// 缓存项上次被访问的时间戳
accessedOn time.Time
// 缓存项被访问的次数
accessCount int64
// 缓存项被删除时的回调函数(删之前执行)
aboutToExpire func(key interface{})
}
从结构体可以看到:
data
是interface{}
类型的,也就是说缓存的值可以是任意类型;但key因为是map的key,所以必须是可比较的类型;- 每条缓存都可以设置生命期,即
lifeSpan
字段; - 每条缓存的访问技术就是有
accessCount
记录的; - 可以为每条缓存设置删除时的回调函数,该回调函数将在该条缓存被删除之前执行,其函数签名为
func(key interface{})
。
CacheItem
也定义了一些常用的方法来获取和设置其成员变量,都非常简单,这里就不介绍了。
1.2 缓存表CacheTable
CacheTable
结构如下:
type CacheTable struct {
sync.RWMutex
// 缓存表名
name string
// 缓存项
items map[interface{}]*CacheItem
// 触发缓存清理的定时器
cleanupTimer *time.Timer
// 缓存清理周期
cleanupInterval time.Duration
// 该缓存表的日志
logger *log.Logger
// 获取一个不存在的缓存项时的回调函数
loadData func(key interface{}, args ...interface{}) *CacheItem
// 向缓存表增加缓存项时的回调函数
addedItem func(item *CacheItem)
// 从缓存表删除一个缓存项时的回调函数
aboutToDeleteItem func(item *CacheItem)
}
从结构体可以看出缓存表就是由一个值类型为缓存项的map以及一些附加属性组成:
name
用来唯一标识一个缓存表,在创建缓存表时指定。items
是一个存储缓存项的map。cleanupTimer
和cleanupInterval
来控制多久更新一次缓存。- 相比于缓存项,缓存表多了一些回调函数。缓存表指定的回调函数作用于缓存表内的所有缓存项,而缓存项指定的回调函数仅对单条缓存有效。
缓存表提供了缓存常见的操作方法:
- 增:
Add
,NotFoundAdd
- 删:
Delete
- 查:
Value
- 是否存在:
Exists
- 缓存总数:
Count
- 缓存刷新:
Flush
- 缓存遍历:
Foreach
- 回调函数设置:
SetAboutToDeleteItemCallback
,SetAddedItemCallback
,SetDataLoader
- 访问最多的前几个缓存项:
MostAccessed
- and so on...
为了提供访问最多的前几个缓存项,cache2go又定义了CacheItemPair
和CacheItemPairList
。CacheItemPair
有缓存的key
和AccessCount
组成,而CacheItemPairList
则是CacheItemPair
组成的Slice,且实现了Sort
接口。
前面我们提到过cache2go的一个特性是"自调节的缓存过期检查",我们看一下这个特性的实现代码:
// 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)
}
// Cache value so we don't keep blocking the mutex.
items := table.items
table.Unlock()
// 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 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.Delete(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.Lock()
table.cleanupInterval = smallestDuration
if smallestDuration > 0 {
table.cleanupTimer = time.AfterFunc(smallestDuration, func() {
go table.expirationCheck()
})
}
table.Unlock()
}
代码中会去遍历所有缓存项,找到最快要被淘汰掉的缓存项的的时间作为cleanupInterval
,即下一次启动缓存刷新的时间,从而保证可以及时的更新缓存,可以看到其实质就是自调节下一次启动缓存更新的时间。另外我们也注意到,如果lifeSpan
设置为0的话,就不会被淘汰,即永久有效。
1.3 缓存
前面介绍了缓存项和由缓存项组成的缓存表,最后我们来看cache2go中的缓存:
var (
cache = make(map[string]*CacheTable)
mutex sync.RWMutex
)
// Cache returns the existing cache table with given name or creates a new one
// if the table does not exist yet.
func Cache(table string) *CacheTable {
mutex.RLock()
t, ok := cache[table]
mutex.RUnlock()
if !ok {
t = &CacheTable{
name: table,
items: make(map[interface{}]*CacheItem),
}
mutex.Lock()
cache[table] = t
mutex.Unlock()
}
return t
}
使用cache2go时,我们第一步一般都是调用上面的Cache
函数创建缓存,该函数会检查一个全局变量cache
(该变量是一个map,其值类型为*CacheTable
,key是缓存表的名字),如果该map中已经有名字为table
的缓存表,就返回该缓存表;否则就创建。该函数返回缓存表的指针,即*CacheTable
。也就是说在cache2go中,缓存表就代表一个缓存,而我们可以创建多个不同名字的缓存,存储在全局变量cache
中。
2. 使用示例
这里的三个例子均来自cache2go,分别演示了缓存的基本使用、回调函数的使用、dataloader回调的使用。
2.1 缓存的基本使用
package main
import (
"fmt"
"time"
"github.com/muesli/cache2go"
)
// Keys & values in cache2go can be off arbitrary types, e.g. a struct.
type myStruct struct {
text string
moreData []byte
}
func main() {
// Accessing a new cache table for the first time will create it.
cache := cache2go.Cache("myCache")
// We will put a new item in the cache. It will expire after
// not being accessed via Value(key) for more than 5 seconds.
val := myStruct{"This is a test!", []byte{}}
cache.Add("someKey", 5*time.Second, &val)
// Let's retrieve the item from the cache.
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)
}
// Wait for the item to expire in cache.
time.Sleep(6 * time.Second)
res, err = cache.Value("someKey")
if err != nil {
fmt.Println("Item is not cached (anymore).")
}
// Add another item that never expires.
cache.Add("someKey", 0, &val)
// cache2go supports a few handy callbacks and loading mechanisms.
cache.SetAboutToDeleteItemCallback(func(e *cache2go.CacheItem) {
fmt.Println("Deleting:", e.Key(), e.Data().(*myStruct).text, e.CreatedOn())
})
// Remove the item from the cache.
cache.Delete("someKey")
// And wipe the entire cache table.
cache.Flush()
}
2.2 回调函数的使用
package main
import (
"fmt"
"time"
"github.com/muesli/cache2go"
)
func main() {
cache := cache2go.Cache("myCache")
// This callback will be triggered every time a new item
// gets added to the cache.
cache.SetAddedItemCallback(func(entry *cache2go.CacheItem) {
fmt.Println("Added:", entry.Key(), entry.Data(), entry.CreatedOn())
})
// This callback will be triggered every time an item
// is about to be removed from the cache.
cache.SetAboutToDeleteItemCallback(func(entry *cache2go.CacheItem) {
fmt.Println("Deleting:", entry.Key(), entry.Data(), entry.CreatedOn())
})
// Caching a new item will execute the AddedItem callback.
cache.Add("someKey", 0, "This is a test!")
// Let's retrieve the item from the cache
res, err := cache.Value("someKey")
if err == nil {
fmt.Println("Found value in cache:", res.Data())
} else {
fmt.Println("Error retrieving value from cache:", err)
}
// Deleting the item will execute the AboutToDeleteItem callback.
cache.Delete("someKey")
// Caching a new item that expires in 3 seconds
res = cache.Add("anotherKey", 3*time.Second, "This is another test")
// This callback will be triggered when the item is about to expire
res.SetAboutToExpireCallback(func(key interface{}) {
fmt.Println("About to expire:", key.(string))
})
time.Sleep(5 * time.Second)
}
这个示例和上面的实例比较相似,只不过多设置了一些回调函数。
2.3 dataloader回调的使用
之前介绍的回调函数都是在添加或删除缓存表项时候触发,而这个dataloader回调则是在调用Value
时触发。即如果我们去查找某个key的缓存,如果找不到且我们设置了dataloader回调,就会执行该回调函数。这个功能还是挺实用的,举个例子比如我们缓存了数据库中的一些用户信息,如果我们可以设置dataloader回调,如果从缓存里面查找某个用户信息时没有找到,就从数据库中读取该用户信息并加到缓存里面,这个动作就可以加在dataloader回调里面。
package main
import (
"fmt"
"github.com/muesli/cache2go"
"strconv"
)
func main() {
cache := cache2go.Cache("myCache")
// The data loader gets called automatically whenever something
// tries to retrieve a non-existing key from the cache.
cache.SetDataLoader(func(key interface{}, args ...interface{}) *cache2go.CacheItem {
// Apply some clever loading logic here, e.g. read values for
// this key from database, network or file.
val := "This is a test with key " + key.(string)
// This helper method creates the cached item for us. Yay!
item := cache2go.NewCacheItem(key, 0, val)
return item
})
// Let's retrieve a few auto-generated items from the cache.
for i := 0; i < 10; i++ {
res, err := cache.Value("someKey_" + strconv.Itoa(i))
if err == nil {
fmt.Println("Found value in cache:", res.Data())
} else {
fmt.Println("Error retrieving value from cache:", err)
}
}
}
至此,cache2go就介绍完了。推荐有兴趣的同学读一下其实现代码,个人感觉非常不错。
评论已关闭