【go语言开发】本地缓存的使用,从简单到复杂写一个本地缓存,并对比常用的开源库
本文主要介绍go语言中本地缓存的使用,首先由简单到复杂手写3个本地缓存示例,使用内置的sync,map等数据结构封装cache,然后介绍常见的一些开源库,以及对比常用的开源库
前言
本地缓存是指将一部分数据存储在应用程序本地内存中,以提高数据访问速度和应用程序性能的技术。
 使用本地缓存的优势:
- 提高应用程序性能
- 减少网络延迟
- 改善用户体验
- 降低外部存储系统的负荷
下面我们从简单到复杂写本地缓存
手写本地缓存
CacheNormal
在 Go 中,你可以使用内置的 sync 包和 map 数据结构来实现本地缓存。
我们首先定义了一个名为 Cache 的结构体,其中包含一个 data 字段,它是一个 map[string]interface{} 类型的数据结构,用于存储键值对。我们使用 sync.RWMutex 来保证并发安全性。
然后,我们定义了 Set 方法和 Get 方法,用于设置和获取缓存值。在 Set 方法中,我们使用互斥锁 mu 来保证并发安全。在 Get 方法中,我们使用读写锁 mu 的读锁来实现并发读取。
package cache
import (
	"sync"
)
type CacheNormal struct {
	data map[string]interface{}
	mu   sync.RWMutex
}
func NewCache() *CacheNormal {
	return &CacheNormal{
		data: make(map[string]interface{}),
	}
}
func (c *CacheNormal) Set(key string, value interface{}) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.data[key] = value
}
func (c *CacheNormal) Get(key string) (interface{}, bool) {
	c.mu.RLock()
	defer c.mu.RUnlock()
	value, ok := c.data[key]
	return value, ok
}
代码测试:
package cache
import (
	"fmt"
	"testing"
	"time"
)
func TestCacheNorm(t *testing.T) {
	cache := NewCache()
	// 设置缓存值
	cache.Set("key1", "value1")
	cache.Set("key2", "value2")
	// 读取缓存值
	value1, ok1 := cache.Get("key1")
	fmt.Println("Key1:", value1, ok1)
	value2, ok2 := cache.Get("key2")
	fmt.Println("Key2:", value2, ok2)
	// 等待一段时间
	time.Sleep(5 * time.Second)
	// 再次读取缓存值
	value1, ok1 = cache.Get("key1")
	fmt.Println("Key1:", value1, ok1)
	value2, ok2 = cache.Get("key2")
	fmt.Println("Key2:", value2, ok2)
}
结果展示:
 
 下面我们实现一个带有过期时间的本地缓存。
CacheEx
要实现带有过期时间的本地缓存,可以使用 Go 的 sync 包和 map 数据结构结合定时器(time.Timer)来实现。
我们定义了一个名为 CacheEx 的结构体,其中包含了一个用于存储缓存项的 data 字段,并且还有一个用于接收过期键的通道 expireCh。
通过调用 NewCacheEx 函数创建一个新的缓存对象,该函数会启动一个协程 startCleanup 来定期清理过期的缓存项。
使用 Set 方法来设置缓存值,并指定缓存项的过期时间。在这个方法中,我们使用互斥锁来保证并发安全性,并将缓存项的过期时间和值存储在 data 中。同时,我们还使用 scheduleExpiration 方法来安排过期时的清理操作。
使用 Get 方法来获取缓存值。在这个方法中,我们使用读锁来进行并发读取,并检查缓存项是否过期。如果缓存项存在且未过期,则返回对应的值;否则返回空值。
package cache
import (
	"sync"
	"time"
)
type CacheEx struct {
	data     map[string]cacheItem
	mu       sync.RWMutex
	expireCh chan string
}
type cacheItem struct {
	value      interface{}
	expiration time.Time
}
func NewCacheEx() *CacheEx {
	c := &CacheEx{
		data:     make(map[string]cacheItem),
		expireCh: make(chan string),
	}
	go c.startCleanup()
	return c
}
func (c *CacheEx) Set(key string, value interface{}, expiration time.Duration) {
	c.mu.Lock()
	defer c.mu.Unlock()
	expireTime := time.Now().Add(expiration)
	c.data[key] = cacheItem{
		value:      value,
		expiration: expireTime,
	}
	go c.scheduleExpiration(key, expireTime)
}
func (c *CacheEx) Get(key string) (interface{}, bool) {
	c.mu.RLock()
	defer c.mu.RUnlock()
	item, ok := c.data[key]
	if ok && item.expiration.After(time.Now()) {
		return item.value, true
	}
	return nil, false
}
func (c *CacheEx) Delete(key string) {
	c.mu.Lock()
	defer c.mu.Unlock()
	delete(c.data, key)
}
func (c *CacheEx) startCleanup() {
	for {
		key := <-c.expireCh
		c.Delete(key)
	}
}
func (c *CacheEx) scheduleExpiration(key string, expireTime time.Time) {
	duration := time.Until(expireTime)
	timer := time.NewTimer(duration)
	<-timer.C
	c.expireCh <- key
}
代码测试:
func TestCacheExpireTime(t *testing.T) {
	cache := NewCacheEx()
	// 设置缓存值,带有过期时间
	cache.Set("key1", "value1", 2*time.Second)
	cache.Set("key2", "value2", 5*time.Second)
	// 读取缓存值
	value1, ok1 := cache.Get("key1")
	fmt.Println("Key1:", value1, ok1)
	value2, ok2 := cache.Get("key2")
	fmt.Println("Key2:", value2, ok2)
	// 等待一段时间
	time.Sleep(3 * time.Second)
	// 再次读取缓存值
	value1, ok1 = cache.Get("key1")
	fmt.Println("Key1:", value1, ok1)
	value2, ok2 = cache.Get("key2")
	fmt.Println("Key2:", value2, ok2)
}
结果展示:
 
CacheV3
package cache
import (
	"sync"
	"time"
)
type item struct {
	value      interface{}
	expiration int64
}
type CacheV3 struct {
	items       sync.Map
	lock        sync.RWMutex
	defaultTTL  time.Duration
	maxCapacity int
	evictList   []interface{}
}
func NewCacheV3(defaultTTL time.Duration, maxCapacity int) *CacheV3 {
	return &CacheV3{
		defaultTTL:  defaultTTL,
		maxCapacity: maxCapacity,
		evictList:   make([]interface{}, 0, maxCapacity),
	}
}
func (c *CacheV3) Set(key string, value interface{}, ttl time.Duration) {
	c.lock.Lock()
	defer c.lock.Unlock()
	if c.cacheSize() >= c.maxCapacity {
		c.evict(1)
	}
	if ttl == 0 {
		ttl = c.defaultTTL
	}
	expiration := time.Now().Add(ttl).UnixNano()
	c.items.Store(key, &item{value, expiration})
	time.AfterFunc(ttl, func() {
		c.lock.Lock()
		defer c.lock.Unlock()
		if _, found := c.items.Load(key); found {
			c.items.Delete(key)
			c.evictList = append(c.evictList, key)
		}
	})
}
func (c *CacheV3) Get(key string) (interface{}, bool) {
	c.lock.RLock()
	defer c.lock.RUnlock()
	if val, found := c.items.Load(key); found {
		item := val.(*item)
		if item.expiration > 0 && time.Now().UnixNano() > item.expiration {
			c.items.Delete(key)
			return nil, false
		}
		return item.value, true
	}
	return nil, false
}
func (c *CacheV3) evict(count int) {
	for i := 0; i < count; i++ {
		key := c.evictList[0]
		c.evictList = c.evictList[1:]
		c.items.Delete(key)
	}
}
func (c *CacheV3) cacheSize() int {
	size := 0
	c.items.Range(func(_, _ interface{}) bool {
		size++
		return true
	})
	return size
}
代码测试:
func TestCacheV3(t *testing.T) {
	c := NewCacheV3(time.Minute, 100)
	c.Set("key1", "value1", time.Second*30)
	c.Set("key2", "value2", time.Minute)
	val, found := c.Get("key1")
	if found {
		fmt.Println(val)
	}
	time.Sleep(time.Second * 45)
	val, found = c.Get("key1")
	if found {
		fmt.Println(val)
	}
	time.Sleep(time.Second * 30)
	val, found = c.Get("key1")
	if found {
		fmt.Println(val)
	} else {
		fmt.Println("key1 expired")
	}
}
结果展示:
 
开源库
cache2go
最新代码请参考:https://github.com/muesli/cache2go
 以下代码仅供参考
type Item struct {
	//read write lock
	sync.RWMutex
	key  interface{}
	data interface{}
	// cache duration.
	duration time.Duration
	// create time
	createTime time.Time
	//last access time
	accessTime time.Time
	//visit times
	count int64
	// callback after deleting
	deleteCallback func(key interface{})
}
//create item.
func NewItem(key interface{}, duration time.Duration, data interface{}) *Item {
	t := time.Now()
	return &Item{
		key:            key,
		duration:       duration,
		createTime:     t,
		accessTime:     t,
		count:          0,
		deleteCallback: nil,
		data:           data,
	}
}
//keep alive
func (item *Item) KeepAlive() {
	item.Lock()
	defer item.Unlock()
	item.accessTime = time.Now()
	item.count++
}
func (item *Item) Duration() time.Duration {
	return item.duration
}
func (item *Item) AccessTime() time.Time {
	item.RLock()
	defer item.RUnlock()
	return item.accessTime
}
func (item *Item) CreateTime() time.Time {
	return item.createTime
}
func (item *Item) Count() int64 {
	item.RLock()
	defer item.RUnlock()
	return item.count
}
func (item *Item) Key() interface{} {
	return item.key
}
func (item *Item) Data() interface{} {
	return item.data
}
func (item *Item) SetDeleteCallback(f func(interface{})) {
	item.Lock()
	defer item.Unlock()
	item.deleteCallback = f
}
// table for managing cache items
type Table struct {
	sync.RWMutex
	//all cache items
	items map[interface{}]*Item
	// trigger cleanup
	cleanupTimer *time.Timer
	// cleanup interval
	cleanupInterval time.Duration
	loadData        func(key interface{}, args ...interface{}) *Item
	// callback after adding.
	addedCallback func(item *Item)
	// callback after deleting
	deleteCallback func(item *Item)
}
func (table *Table) Count() int {
	table.RLock()
	defer table.RUnlock()
	return len(table.items)
}
func (table *Table) Foreach(trans func(key interface{}, item *Item)) {
	table.RLock()
	defer table.RUnlock()
	for k, v := range table.items {
		trans(k, v)
	}
}
func (table *Table) SetDataLoader(f func(interface{}, ...interface{}) *Item) {
	table.Lock()
	defer table.Unlock()
	table.loadData = f
}
func (table *Table) SetAddedCallback(f func(*Item)) {
	table.Lock()
	defer table.Unlock()
	table.addedCallback = f
}
func (table *Table) SetDeleteCallback(f func(*Item)) {
	table.Lock()
	defer table.Unlock()
	table.deleteCallback = f
}
func (table *Table) RunWithRecovery(f func()) {
	defer func() {
		if err := recover(); err != nil {
			fmt.Printf("occur error %v \r\n", err)
		}
	}()
	f()
}
func (table *Table) checkExpire() {
	table.Lock()
	if table.cleanupTimer != nil {
		table.cleanupTimer.Stop()
	}
	if table.cleanupInterval > 0 {
		table.log("Expiration check triggered after %v for table", table.cleanupInterval)
	} else {
		table.log("Expiration check installed for table")
	}
	// in order to not take the lock. use temp items.
	items := table.items
	table.Unlock()
	//in order to make timer more precise, update now every loop.
	now := time.Now()
	smallestDuration := 0 * time.Second
	for key, item := range items {
		//take out our things, in order not to take the lock.
		item.RLock()
		duration := item.duration
		accessTime := item.accessTime
		item.RUnlock()
		// 0 means valid.
		if duration == 0 {
			continue
		}
		if now.Sub(accessTime) >= duration {
			//cache item expired.
			_, e := table.Delete(key)
			if e != nil {
				table.log("occur error while deleting %v", e.Error())
			}
		} else {
			//find the most possible expire item.
			if smallestDuration == 0 || duration-now.Sub(accessTime) < smallestDuration {
				smallestDuration = duration - now.Sub(accessTime)
			}
		}
	}
	//trigger next clean
	table.Lock()
	table.cleanupInterval = smallestDuration
	if smallestDuration > 0 {
		table.cleanupTimer = time.AfterFunc(smallestDuration, func() {
			go table.RunWithRecovery(table.checkExpire)
		})
	}
	table.Unlock()
}
// add item
func (table *Table) Add(key interface{}, duration time.Duration, data interface{}) *Item {
	item := NewItem(key, duration, data)
	table.Lock()
	table.log("Adding item with key %v and lifespan of %d to table", key, duration)
	table.items[key] = item
	expDur := table.cleanupInterval
	addedItem := table.addedCallback
	table.Unlock()
	if addedItem != nil {
		addedItem(item)
	}
	//find the most possible expire item.
	if duration > 0 && (expDur == 0 || duration < expDur) {
		table.checkExpire()
	}
	return item
}
func (table *Table) Delete(key interface{}) (*Item, error) {
	table.RLock()
	r, ok := table.items[key]
	if !ok {
		table.RUnlock()
		return nil, errors.New(fmt.Sprintf("no item with key %s", key))
	}
	deleteCallback := table.deleteCallback
	table.RUnlock()
	if deleteCallback != nil {
		deleteCallback(r)
	}
	r.RLock()
	defer r.RUnlock()
	if r.deleteCallback != nil {
		r.deleteCallback(key)
	}
	table.Lock()
	defer table.Unlock()
	table.log("Deleting item with key %v created on %s and hit %d times from table", key, r.createTime, r.count)
	delete(table.items, key)
	return r, nil
}
//check exist.
func (table *Table) Exists(key interface{}) bool {
	table.RLock()
	defer table.RUnlock()
	_, ok := table.items[key]
	return ok
}
//if exist, return false. if not exist add a key and return true.
func (table *Table) NotFoundAdd(key interface{}, lifeSpan time.Duration, data interface{}) bool {
	table.Lock()
	if _, ok := table.items[key]; ok {
		table.Unlock()
		return false
	}
	item := NewItem(key, lifeSpan, data)
	table.log("Adding item with key %v and lifespan of %d to table", key, lifeSpan)
	table.items[key] = item
	expDur := table.cleanupInterval
	addedItem := table.addedCallback
	table.Unlock()
	if addedItem != nil {
		addedItem(item)
	}
	if lifeSpan > 0 && (expDur == 0 || lifeSpan < expDur) {
		table.checkExpire()
	}
	return true
}
func (table *Table) Value(key interface{}, args ...interface{}) (*Item, error) {
	table.RLock()
	r, ok := table.items[key]
	loadData := table.loadData
	table.RUnlock()
	if ok {
		//update visit count and visit time.
		r.KeepAlive()
		return r, nil
	}
	if loadData != nil {
		item := loadData(key, args...)
		if item != nil {
			table.Add(key, item.duration, item.data)
			return item, nil
		}
		return nil, errors.New("cannot load item")
	}
	return nil, nil
}
// truncate a table.
func (table *Table) Truncate() {
	table.Lock()
	defer table.Unlock()
	table.log("Truncate table")
	table.items = make(map[interface{}]*Item)
	table.cleanupInterval = 0
	if table.cleanupTimer != nil {
		table.cleanupTimer.Stop()
	}
}
//support table sort
type ItemPair struct {
	Key         interface{}
	AccessCount int64
}
type ItemPairList []ItemPair
func (p ItemPairList) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }
func (p ItemPairList) Len() int           { return len(p) }
func (p ItemPairList) Less(i, j int) bool { return p[i].AccessCount > p[j].AccessCount }
//return most visited.
func (table *Table) MostAccessed(count int64) []*Item {
	table.RLock()
	defer table.RUnlock()
	p := make(ItemPairList, len(table.items))
	i := 0
	for k, v := range table.items {
		p[i] = ItemPair{k, v.count}
		i++
	}
	sort.Sort(p)
	var r []*Item
	c := int64(0)
	for _, v := range p {
		if c >= count {
			break
		}
		item, ok := table.items[v.Key]
		if ok {
			r = append(r, item)
		}
		c++
	}
	return r
}
// print log.
func (table *Table) log(format string, v ...interface{}) {
	//fmt.Printf(format+"\r\n", v)
}
func NewTable() *Table {
	return &Table{
		items: make(map[interface{}]*Item),
	}
}
go-cache
https://github.com/patrickmn/go-cache
-  优点: - 简单易用,适合快速集成到现有项目中。
- 支持过期时间,可以自动淘汰过期的缓存项。
- 支持多种数据类型的缓存。
 
-  缺点: - 性能略低于其他库,不适合高并发读写的场景。
- 不支持分布式缓存。
 
bigcache
https://github.com/allegro/bigcache
-  优点: - 高性能,适用于需要快速读写大量数据的场景。
- 使用murmurhash算法来计算哈希值,减少了哈希冲突。
- 使用多个shard来减少锁竞争。
 
-  缺点: - 不支持过期时间,只能手动清除过期的缓存项。
- 内存使用较高,不适合存储大量数据。
 
groupcache
https://github.com/golang/groupcache
-  优点: - 支持分布式缓存,可以在多台机器上共享缓存。
- 采用LRU算法来淘汰缓存项,具备一定的缓存性能。
- 提供一致性哈希算法,可以解决节点扩容等问题。
 
-  缺点: - 比较复杂,使用起来较为繁琐。
- 只支持字符串类型的键值对。
 
本地缓存对比
参考文档:

 下面对每个库的详细介绍:
- go-cache:
- 描述:go-cache是一款简单而有效的内存缓存库,支持设置过期时间和GC机制。
- 并发安全:是,使用Go的sync.Map实现数据的并发安全存储和访问。
- 存储限制:无,可以存储任意类型的数据。
- 淘汰策略:默认为LRU(最近最少使用)算法,也支持手动删除过期的缓存项。
- 分布式支持:不支持。
- freecache:
- 描述:freecache是一款高性能的内存缓存库,使用LRU算法进行缓存项的淘汰。
- 并发安全:是,使用读写锁实现并发安全访问。
- 存储限制:固定大小,需要在初始化时指定总共可以缓存的字节数。
- 淘汰策略:默认为LRU(最近最少使用)算法,不支持自定义。
- 分布式支持:不支持。
- bigcache:
- 描述:bigcache是一款高性能的内存缓存库,使用murmurhash哈希算法快速查找。
- 并发安全:是,使用多个读写锁来实现高并发的访问控制。
- 存储限制:固定大小,需要在初始化时指定最多可以缓存的条目数。
- 淘汰策略:默认为LRU(最近最少使用)算法,不支持自定义。
- 分布式支持:不支持。
- groupcache:
- 描述:groupcache是一款支持分布式缓存的库,提供一致性哈希和HTTP请求缓存功能。
- 并发安全:是,使用读写锁实现并发安全访问。
- 存储限制:无,可以存储任意类型的数据。
- 淘汰策略:支持自定义淘汰策略,例如手动删除过期的缓存项。
- 分布式支持:是,支持分布式缓存,将数据分片存储在多个节点上,通过查询一致性哈希环来确定数据所在的节点。
- gocache:
- 描述:gocache是一款快速、强大的内存缓存库,支持过期时间、并发安全和自定义淘汰策略。
- 并发安全:是,使用读写锁实现并发安全访问。
- 存储限制:无,可以存储任意类型的数据。
- 淘汰策略:默认为LRU(最近最少使用)算法,也支持自定义淘汰策略。
- 分布式支持:不支持。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!