Golang Cache2go

Cache2go是一个轻量级的Go缓存库,使用原生的map结构,支持过期清除,单从测试结果来看,并不能够支持很大规模的数据存取。但作为一个Go入门的项目进行学习是非常不错的。

Cache2go使用

直接看源代码不是不可以,但是先使用一下可能会更好的理解代码,我们就先从一些简单的例子入手,理解这个库。

首先定义一个cache并命名,通过Add函数将数据添加到cache中,注意添加的时候需要使用引用:

cache := cache2go.Cache("myCache")

val := myStruct{1, "", []byte{}}
cache.Add("key1", time.Second*2, &val)
res, err := cache.Value("key1")
if err == nil {
    fmt.Println("Found:", res.Data().(*myStruct).id)
} else {
    fmt.Println("Not Found.")
}

然后通过Value函数将数据取出,注意取出之后的对象并不是放进去的对象,需要调用Data函数取出放进去的对象,经过类型转换,即得到原来放进去的对象。

res, err = cache.Value("key1")
if err == nil {
    fmt.Println("Found:", res.Data().(*myStruct).id)
} else {
    fmt.Println("Not Found.")
}

在放入对象的时候,添加一个时间,可以定义数据的过期时间,单位为秒。过期时间是定时检查的,在主线程执行的过程中,过期检查也会异步执行。这里我们通过添加一个删除回调函数,来说明:

cache := cache2go.Cache("myCache")

val := myStruct{1, "", []byte{}}
cache.Add("key1", time.Second*2, &val)
res, err := cache.Value("key1")
if err == nil {
    fmt.Println("Found:", res.Data().(*myStruct).id)
} else {
    fmt.Println("Not Found.")
}

cache.SetAboutToDeleteItemCallback(func(e *cache2go.CacheItem) {
    fmt.Println("Delete:", e.Key(), e.Data().(*myStruct).id, e.CreatedOn())
})

time.Sleep(time.Second * 3)

执行的过程中可以看出在主线程sleep的这段时间内,删除的回调会被调用,过期检查是异步执行的。当然我们也可以设置过期时间为0,表示数据永不过期,对于这样的数据,可以使用Delete函数删除这样的数据,或者使用Flush函数删除cache中的全部数据。

cache.Add("key2", 0, &val)
cache.Delete("key2")
cache.Flush()

另外,cache提供dataloader回调。如果访问的key不存在,就会调用这个函数,这样就提供一种方式,当访问的数据不存在的时候,通过外部数据源找到数据,然后返回。在测试的例子中,我们编造一个数据,然后返回,可以看到,数据被正常的返回、使用了。

cache.SetDataLoader(func(key interface{}, args ...interface{}) *cache2go.CacheItem {
    val := "load item is " + key.(string)
    item := cache2go.NewCacheItem(key, 0, val)
    return item
})

res, err = cache.Value("someKey_1")
if err == nil {
    fmt.Println("Found value in cache:", res.Data())
} else {
    fmt.Println("Error retrieving value from cache:", err)
}

至此,我们基本了解了这个库的使用,下面我们就着手分析源代码了

Cache2go源码分析

cache.go

cache.go文件比较简短,主要作用就是创建一个cache,唯一值得注意的就是两次加锁:

func Cache(table string) *CacheTable {
	mutex.RLock()
	t, ok := cache[table]
	mutex.RUnlock()

	if !ok {
		mutex.Lock()
		t, ok = cache[table]
		// Double check whether the table exists or not.
		if !ok {
			t = &CacheTable{
				name:  table,
				items: make(map[interface{}]*CacheItem),
			}
			cache[table] = t
		}
		mutex.Unlock()
	}

	return t
}

第一次加的锁是一个读锁,如果存在,就直接返回,且不影响这个cache中的数据读取。当第一次不存在的时候,再次加锁,这次加的是一个普通锁,者是如果仍旧不存在,就创建一个新的cache。这样可以避免影响cache性能。

cacheitem.go

cacheitem.go文件中最重要的就是存储对象的结构:

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{})
}

从这个结构中可以看到,对象的key、value,创建、访问时间戳,还有访问次数等统计,每个item还有一个独立的过期处理回调。其它的函数都是对存储对象属性的访问。

cachetable.go

最后一个cachetable.go文件是所有缓存的核心,缓存的所有功能都来源于此,我们仍旧从数据结构入手:

// 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首先包含的是一个读写锁,用于存取数据,然后是表名和用于存放数据的map,这里可以看到数据的组织形式就是原生的map结构。随后两个属性是Timer和Duration分别用于定时清理过期数据和最近的一个过期数据。最后三个回调函数会在对应的时间点被调用,这里不再解释。

这里值得解释的一个函数是expirationCheck:


// 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()
}

这个函数就是数据过期机制的核心,可以看到,在刚开始就判断Timer是否为空,如果不为空先停止Timer。然后判断cleanupInterval是否为0,如果是第一次调用这个函数,就会为0,否者就是上一次统计最近一个过期数据的时间。判断完成之后,就开始遍历table,删除那些已经过期的数据,然后寻找一个最近过期的数据,并重新设置一个Timer,在数据过期之前调用。

以上就是这个库的全部分析,如果有可能想要分析一下groupcache,这个库的作者和memcache是同一个,也就是memcache的golang版本。相信这个库可以让我受益良多。