cache2go是一个用Go实现的并发安全的缓存库,实现了如下特性:

  • 并发安全;
  • 可设置每条缓存的超时时间;
  • 内置缓存访问计数;
  • 自调节的缓存过期检查;
  • 可设置缓存增加/删除回调函数;
  • and so on...

这个库代码量很少,核心代码就三个文件,里面设计的技术点主要包括读写锁、goroutine、map操作等。作为Go语言学习样例也非常不错。

1. 源码解析

cache2go中主要涉及两个类型CacheItemCacheTable

  • 一个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{})
}

从结构体可以看到:

  • datainterface{}类型的,也就是说缓存的值可以是任意类型;但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。
  • cleanupTimercleanupInterval来控制多久更新一次缓存。
  • 相比于缓存项,缓存表多了一些回调函数。缓存表指定的回调函数作用于缓存表内的所有缓存项,而缓存项指定的回调函数仅对单条缓存有效。

缓存表提供了缓存常见的操作方法:

  • 增:AddNotFoundAdd
  • 删:Delete
  • 查:Value
  • 是否存在:Exists
  • 缓存总数:Count
  • 缓存刷新:Flush
  • 缓存遍历:Foreach
  • 回调函数设置:SetAboutToDeleteItemCallbackSetAddedItemCallbackSetDataLoader
  • 访问最多的前几个缓存项:MostAccessed
  • and so on...

为了提供访问最多的前几个缓存项,cache2go又定义了CacheItemPairCacheItemPairListCacheItemPair有缓存的keyAccessCount组成,而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就介绍完了。推荐有兴趣的同学读一下其实现代码,个人感觉非常不错。