Go常见localcache实现
localcache一般有以下几种实现方式:
map
sync.Map
- 基于以上二者封装的复合型
map
少量数据的缓存直接使用原生map和sync.Map
也没有问题,如果数据量和访问量都很大,就要根据业务场景基于map进行特殊设计,目前流行的localcache几乎都是这样设计的。
再选型的时候,一般会倾向希望localcache至少包含如下几个特性的一种或多种:
- 延时低&容纳百万对象
- 高并发访问
- 可以设置过期时间
- 不需要持久化
- API使用简单
这里特别先说明的是,在Go1.5版本里新增了一个优化:当map里key和value都不包含指针则GC扫描忽略,详情:https://github.com/golang/go/issues/9477, 为了减少gc的影响降低延迟,下面介绍的一些库有不少是基于这个特性进行特殊设计的。
目前还没有同时支持上述5个特性的localcache,希望达到高性能往往需要更复杂的数据结构,而在这上面增加特性会大大增加整体架构的复杂度,我们希望localcache是简单可依赖的。
bigcache:0GC
官方说明:Fast, concurrent, evicting in-memory cache written to keep big number of entries without impact on performance. BigCache keeps entries on heap but omits GC for them
github地址:https://github.com/allegro/bigcache
官方博客有详细实现说明:https://blog.allegro.tech/2016/03/writing-fast-cache-service-in-go.html
我们先看看基本的用法:
// 配置项介绍
config := bigcache.Config{
// 设置分区的数量,必须是2的整倍数
Shards: 1024,
// LifeWindow后,缓存对象被认为不活跃,但并不会删除对象
LifeWindow: 5 * time.Second,
// CleanWindow后,会删除被认为不活跃的对象,0代表不操作;
CleanWindow: 3 * time.Second,
// 设置最大存储对象数量,仅在初始化时可以设置
MaxEntriesInWindow: 1000 * 10 * 60,
// 缓存对象的最大字节数,仅在初始化时可以设置
MaxEntrySize: 500,
// 是否打印内存分配信息
Verbose: true,
// 设置缓存最大值(单位为MB),0表示无限制
HardMaxCacheSize: 8192,
// 在缓存过期或者被删除时,可设置回调函数,参数是(key、val),默认是nil不设置
OnRemove: callBack,
// 在缓存过期或者被删除时,可设置回调函数,参数是(key、val,reason)默认是nil不设置
OnRemoveWithReason: nil,
}
// 初始化
cache, _ := NewBigCache(config)
// 设置缓存
cache.Set("key", []byte("value"))
// 获取缓存
cachedValue, _ := cache.Get("key")
bigcache的核心设计有两项:
- 分片(shard),其实通过分片降低访问压力是cache常见的操作;
- 使用BytesQueue避免GC开销,BytesQueue的实现原理利用了上述所说的Go1.5的特性,在map里不产生指针、不触发GC。
BytesQueue是bigcache的核心数据结构。为了达到0 GC,BytesQueue使用了一个索引结构map[uint64]uint32以及一个实际存储数据的数组[]byte,[]byte根据bigcache创建时的初始参数(有默认值)进行初始化,当[]byte容量不足的时候会进行2倍扩容。
值得注意的是删除缓存元素的时候bigcache只是在map[uint64]uint32中删除了它的索引,byte数组里的空间是不会释放的。
缺点:
1.bigcache具备缓存过期的功能,但这是依赖定时扫描以及调用Set时进行判断以及淘汰的,会导致淘汰不及时。并且过期时间只能提前设置,一个实例里只能设置成相同的过期时间;
2.hash冲突返回失败,而不会进行兼容;
3.不能进行更新操作;
4.需要调用者自行进行序列化。
fastcache:比bigcache更快
官方说明:fast thread-safe inmemory cache for big number of entries in Go。
github地址:https://github.com/VictoriaMetrics/fastcache
这个库是fasthttp的作者开发的,思路和bigcache一致,但是对于bigcache里BytesQueue的设计进行改进,使用一个环形数组
[][]byte来实现,扩容的时候只需要进行append即可。
先看看用法:
c := New(1024)// 设置最大容量
defer c.Reset()
c.Set([]byte("key"), []byte("value"))
if v := c.Get(nil, []byte("key")); string(v) != "value" {
t.Fatalf("unexpected value obtained; got %q; want %q", v, "value")
}
可以看到,相比较bigcache来说,api更加简单了,官方给出来Benchmarks,性能表现也比bigcache优秀。
Set操作比sync.map快8倍,比bigcache快了3倍;Get操作比sync.map快4倍,比bigcache快2倍。
核心设计:
- 分片(shard),分成512个bucket;
- 使用环形数组和bigcache一样实现map的0GC;
- 使用Mmap来分配内存,脱离GC约束,去掉数组里GC扫描带来的性能压力;
- bucket里的每个chunk 64k,避免CPU伪共享。
缺点:
不支持过期时间
freecache:更好用的高性能localcache
A cache library for Go with zero GC overhead and high concurrent performance.
github地址:https://github.com/coocood/freecache
这里引用一个详细的实现说明:https://blog.csdn.net/chizhenlian/article/details/108435024
先看下使用例子:
cacheSize := 100 * 1024 * 1024
cache := freecache.NewCache(cacheSize)
key := []byte("abc")
val := []byte("def")
expire := 60 // expire in 60 seconds
cache.Set(key, val, expire)
got, err := cache.Get(key)
if err != nil {
fmt.Println(err)
} else {
fmt.Printf("%s\n", got)
}
affected := cache.Del(key)
fmt.Println("deleted key ", affected)
fmt.Println("entry count ", cache.EntryCount())
可以看到相比于bigcache和fastcache,它还支持对每个key设置过期时间,并支持根据空间大小进行LRU淘汰。
核心设计:
- 分片(shard),划分了256个segments;
- 实现类似于bigcache的索引操作,slotsData存放索引,RingBuf存放具体数据;
- 只存在 512 个指针(每个segments两个指针,slotsData和RingBuf切片;
- 存储空间都是预先分配好的。
有意思的是,它还提供了一个兼容Redis协议的server服务(server/main.go),支持get/set/del等常用kv命令,这样就给远程服务提供接口操作本地cache了。虽然实际用途不大,但是对于学习Redis协议和TCP服务有参考意义。
缺点:
- 环形数组RingBuffer的内存是预先分配的,所以一开始就会占用较大的内存;
- 数据过期不会清理存储空间,只是做标志。
groupcache:分布式缓存
Groupcache is a distributed caching and cache-filling library, intended as a replacement for a pool of memcached nodes in many cases.
github地址:https://github.com/golang/groupcache
从官方定义可以看到,它是一个分布式缓存,定位更类似于轻量级的memcached,并且作为Lib和业务服务部署在一起。
考虑这样的一个场景:业务服务首先读localcache,读不到数据才读DB,如果服务节点有100个,那依然会对db产生100个请求。而groupcache可以不仅可以读取本地cache,还可以通过内置的远程调用读取其他节点的本地cache。
也就是如果一个key的缓存并不在本地,groupcache会将cache请求根据key的hash值(默认CRC32)自动转发到给其他成员并将结果返回给caller。
有几点核心设计:
- 使用一致性Hash解决节点动态上下线的路由问题;
- 使用singleflight组件解决缓存击穿。
缺点:
- 没有提供动态管理服务节点的能力,需要用户自行接入名字服务;
- 远程调用使用HTTP,性能可能不佳;
- 不能进行更新、删除、缓存过期操作,使用场景少;
- 用起来会使得架构更复杂,契合的场景不多,不如直接使用Redis。
go-cache:简单好用的localcache
go-cache is an in-memory key:value store/cache similar to memcached that is suitable for applications running on a single machine.
这个也是这几个localcache里最简单的一个库了,核心代码文件只有两个。核心实现就是map[string]Item和sync.RWMutex,外加过期Evicted的封装。
c := cache.New(5*time.Minute, 1*time.Minute) // 创建一个实例,默认5分钟过期,1分钟扫描一次缓存Key c.Set("foo", "bar", 20*time.Millisecond)) // 20ms后过期 foo, found := c.Get("foo") //注意这里返回来的是interface{} if found { fmt.Println(foo) }
如上面代码实例可以看出,优点如下:
- 线程安全
- api使用很简单
- 支持对单个key设置过期时间
性能来说不如上述几个localcache,但是胜在使用方便,如果业务只需要缓存万级别的小key,可以大胆使用。