go 语言实现简易内存缓存

本贴最后更新于 1257 天前,其中的信息可能已经事过景迁

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,代码结构也更规范,合理,推荐大家使用。

  • golang

    Go 语言是 Google 推出的一种全新的编程语言,可以在不损失应用程序性能的情况下降低代码的复杂性。谷歌首席软件工程师罗布派克(Rob Pike)说:我们之所以开发 Go,是因为过去 10 多年间软件开发的难度令人沮丧。Go 是谷歌 2009 发布的第二款编程语言。

    492 引用 • 1383 回帖 • 375 关注
  • 缓存
    42 引用 • 70 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...