hello,大家好,欢迎来到银之庭。我是 Z,一个普通的程序员。最近工作中遇到了个场景,一个 go 服务调用下游 redis 的 QPS 太高,导致 redis 报错比较多,所以决定在 go 服务里起个本地缓存,减少对 redis 的访问,上线后效果还不错,对 redis 的访问一下降了 90% 左右。今天我们就来看下怎么用 go 语言实现个本地内存缓存吧。
1. 明确目标
首先,我们要明确需要实现哪些特性。本地内存缓存最基本的是个 K-V 的存储,key 一般是 string,value 为了通用,定义成 interface{}。另外,还要有过期删除功能,避免一直读到本地的缓存,数据更新没有及时同步,这个过期时间通常由调用方传入。最后,考虑需不需要限制内存使用,在我实际的场景中,我是没有限制的,因为我缓存的内容其实很少,而且我设置的过期时间也很短,确定不会占用很大内存。总结一下就是:
- 实现一个 K-V 存储,key 是 string,value 是 interface{}
- 支持指定 key 的过期时间,内部实现过期删除
- 可选实现限制内存使用
2. 代码实现
talk is cheap,下面直接贴完整代码,注释比较详细,相信大家都能看懂:
package main
import (
"fmt"
"sync"
"time"
)
// 缓存对象
type CacheItem struct {
Value interface{} // 实际缓存的对象
LifeTime time.Duration // 存活时间,上游传入
CreatedAt time.Time // 创建时间,和存活时间一起决定是否过期
}
// 缓存是否过期
func (item *CacheItem) Expired() bool {
return time.Now().Sub(item.CreatedAt) > item.LifeTime
}
// 本地缓存实现类
type LocalCache struct {
sync.RWMutex //继承读写锁,用于并发控制
Items map[string]*CacheItem // K-V存储
GCDuration int // 惰性删除,后台运行时间间隔,单位秒
}
// 新建本地缓存
func NewLocalCache(gcDuration int) *LocalCache {
localCache := &LocalCache{Items: map[string]*CacheItem{}, GCDuration: gcDuration}
// 启动协程,定期扫描过期键,进行删除
go localCache.GC()
return localCache
}
// 存入对象
func (cache *LocalCache) Put(key string, value interface{}, lifeTime time.Duration) {
cache.Lock()
defer cache.Unlock()
cache.Items[key] = &CacheItem{
Value: value,
LifeTime: lifeTime,
CreatedAt: time.Now(),
}
}
// 查询对象
func (cache *LocalCache) Get(key string) interface{} {
cache.RLock()
defer cache.RUnlock()
if item, ok := cache.Items[key]; ok {
if !item.Expired() {
return item
} else {
// 键已过期,直接删除
// 需要注意的是,这里不能调用cache.Del()方法,因为go的读写锁是不支持锁升级的,会发生死锁
delete(cache.Items, key)
}
}
return nil
}
// 删除缓存
func (cache *LocalCache) Del(key string) {
cache.Lock()
defer cache.Unlock()
if _, ok := cache.Items[key]; ok {
delete(cache.Items, key)
}
}
// 异步执行,扫描过期键并删除
func (cache *LocalCache) GC() {
for {
select {
case <-time.After(time.Duration(cache.GCDuration) * time.Second):
keysToExpire := []string{}
cache.RLock()
for key, item := range cache.Items {
if item.Expired() {
keysToExpire = append(keysToExpire, key)
}
}
cache.RUnlock()
for _, keyToExpire := range keysToExpire {
cache.Del(keyToExpire)
}
}
}
}
以上就是 go 语言里一个简单的本地缓存的实现了,如果大家只是轻度使用,不重度依赖本地缓存的话,直接自己手写一遍就行了,如果是重度依赖的话,建议还是找个开源的比较完善的实现,比如下面我要推荐的 go-cache 模块。
3. 开源实现:go-cache
点击查看源码:github 地址。
它的实现原理和上面的差不多,只是考虑了更多细节,比如不使用 defer,来提升性能,处理 gc 的协程监听了一个关闭管道,使得我们可以从外部停止 gc 协程,以及注册 finalizer 函数,保证可以优雅关闭 gc 协程,并提供了更多有用的 API,代码结构也更规范,合理,推荐大家使用。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于