本文主要介绍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算法来淘汰缓存项,具备一定的缓存性能。
- 提供一致性哈希算法,可以解决节点扩容等问题。
-
缺点:
- 比较复杂,使用起来较为繁琐。
- 只支持字符串类型的键值对。
本地缓存对比
参考文档:
下面对每个库的详细介绍:
- go-cache:
- 描述:go-cache是一款简单而有效的内存缓存库,支持设置过期时间和GC机制。
- 并发安全:是,使用Go的sync.Map实现数据的并发安全存储和访问。
- 存储限制:无,可以存储任意类型的数据。
- 淘汰策略:默认为LRU(最近最少使用)算法,也支持手动删除过期的缓存项。
- 分布式支持:不支持。
- freecache:
- 描述:freecache是一款高性能的内存缓存库,使用LRU算法进行缓存项的淘汰。
- 并发安全:是,使用读写锁实现并发安全访问。
- 存储限制:固定大小,需要在初始化时指定总共可以缓存的字节数。
- 淘汰策略:默认为LRU(最近最少使用)算法,不支持自定义。
- 分布式支持:不支持。
- bigcache:
- 描述:bigcache是一款高性能的内存缓存库,使用murmurhash哈希算法快速查找。
- 并发安全:是,使用多个读写锁来实现高并发的访问控制。
- 存储限制:固定大小,需要在初始化时指定最多可以缓存的条目数。
- 淘汰策略:默认为LRU(最近最少使用)算法,不支持自定义。
- 分布式支持:不支持。
- groupcache:
- 描述:groupcache是一款支持分布式缓存的库,提供一致性哈希和HTTP请求缓存功能。
- 并发安全:是,使用读写锁实现并发安全访问。
- 存储限制:无,可以存储任意类型的数据。
- 淘汰策略:支持自定义淘汰策略,例如手动删除过期的缓存项。
- 分布式支持:是,支持分布式缓存,将数据分片存储在多个节点上,通过查询一致性哈希环来确定数据所在的节点。
- gocache:
- 描述:gocache是一款快速、强大的内存缓存库,支持过期时间、并发安全和自定义淘汰策略。
- 并发安全:是,使用读写锁实现并发安全访问。
- 存储限制:无,可以存储任意类型的数据。
- 淘汰策略:默认为LRU(最近最少使用)算法,也支持自定义淘汰策略。
- 分布式支持:不支持。