【go语言开发】本地缓存的使用,从简单到复杂写一个本地缓存,并对比常用的开源库

本文主要介绍go语言中本地缓存的使用,首先由简单到复杂手写3个本地缓存示例,使用内置的sync,map等数据结构封装cache,然后介绍常见的一些开源库,以及对比常用的开源库

文章目录

前言

本地缓存 是指将一部分数据存储在应用程序本地内存中,以提高数据访问速度和应用程序性能的技术。

使用本地缓存的优势:

  • 提高应用程序性能
  • 减少网络延迟
  • 改善用户体验
  • 降低外部存储系统的负荷

下面我们从简单到复杂写本地缓存

手写本地缓存

CacheNormal

在 Go 中,你可以使用内置的 sync 包和 map 数据结构来实现本地缓存。

我们首先定义了一个名为 Cache 的结构体,其中包含一个 data 字段,它是一个 map[string]interface{} 类型的数据结构,用于存储键值对。我们使用 sync.RWMutex 来保证并发安全性。

然后,我们定义了 Set 方法和 Get 方法,用于设置和获取缓存值。在 Set 方法中,我们使用互斥锁 mu 来保证并发安全。在 Get 方法中,我们使用读写锁 mu 的读锁来实现并发读取。

go 复制代码
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
}

代码测试:

go 复制代码
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 方法来获取缓存值。在这个方法中,我们使用读锁来进行并发读取,并检查缓存项是否过期。如果缓存项存在且未过期,则返回对应的值;否则返回空值。

go 复制代码
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
}

代码测试:

go 复制代码
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

go 复制代码
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
}

代码测试:

go 复制代码
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

以下代码仅供参考

go 复制代码
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算法来淘汰缓存项,具备一定的缓存性能。
    • 提供一致性哈希算法,可以解决节点扩容等问题。
  • 缺点:

    • 比较复杂,使用起来较为繁琐。
    • 只支持字符串类型的键值对。

本地缓存对比

参考文档:

下面对每个库的详细介绍:

  1. go-cache:
  • 描述:go-cache是一款简单而有效的内存缓存库,支持设置过期时间和GC机制。
  • 并发安全:是,使用Go的sync.Map实现数据的并发安全存储和访问。
  • 存储限制:无,可以存储任意类型的数据。
  • 淘汰策略:默认为LRU(最近最少使用)算法,也支持手动删除过期的缓存项。
  • 分布式支持:不支持。
  1. freecache:
  • 描述:freecache是一款高性能的内存缓存库,使用LRU算法进行缓存项的淘汰。
  • 并发安全:是,使用读写锁实现并发安全访问。
  • 存储限制:固定大小,需要在初始化时指定总共可以缓存的字节数。
  • 淘汰策略:默认为LRU(最近最少使用)算法,不支持自定义。
  • 分布式支持:不支持。
  1. bigcache:
  • 描述:bigcache是一款高性能的内存缓存库,使用murmurhash哈希算法快速查找。
  • 并发安全:是,使用多个读写锁来实现高并发的访问控制。
  • 存储限制:固定大小,需要在初始化时指定最多可以缓存的条目数。
  • 淘汰策略:默认为LRU(最近最少使用)算法,不支持自定义。
  • 分布式支持:不支持。
  1. groupcache:
  • 描述:groupcache是一款支持分布式缓存的库,提供一致性哈希和HTTP请求缓存功能。
  • 并发安全:是,使用读写锁实现并发安全访问。
  • 存储限制:无,可以存储任意类型的数据。
  • 淘汰策略:支持自定义淘汰策略,例如手动删除过期的缓存项。
  • 分布式支持:是,支持分布式缓存,将数据分片存储在多个节点上,通过查询一致性哈希环来确定数据所在的节点。
  1. gocache:
  • 描述:gocache是一款快速、强大的内存缓存库,支持过期时间、并发安全和自定义淘汰策略。
  • 并发安全:是,使用读写锁实现并发安全访问。
  • 存储限制:无,可以存储任意类型的数据。
  • 淘汰策略:默认为LRU(最近最少使用)算法,也支持自定义淘汰策略。
  • 分布式支持:不支持。
相关推荐
奥顺互联V8 分钟前
一次性部署:使用Docker部署PHP应用
大数据·mysql·开源·php
Algorithm157643 分钟前
云原生相关的 Go 语言工程师技术路线(含博客网址导航)
开发语言·云原生·golang
Narutolxy2 小时前
深入探讨 Go 中的高级表单验证与翻译:Gin 与 Validator 的实践之道20241223
开发语言·golang·gin
Hello.Reader2 小时前
全面解析 Golang Gin 框架
开发语言·golang·gin
m0_748241702 小时前
DuetWebControl 开源项目常见问题解决方案
开源
岁月变迁呀11 小时前
Redis梳理
数据库·redis·缓存
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭12 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
Code apprenticeship13 小时前
怎么利用Redis实现延时队列?
数据库·redis·缓存
pubuzhixing14 小时前
开源白板新方案:Plait 同时支持 Angular 和 React 啦!
前端·开源·github
fpcc15 小时前
跟我学c++中级篇——C++中的缓存利用
c++·缓存