fastcache 是一个线程安全并且支持大量数据存储的高性能缓存组件库。
这是官方 Github
主页上的项目介绍,和 fasthttp
名字一样以 fast
打头,作者对项目代码的自信程度可见一斑。此外该库的核心代码非常轻量, 笔者本着学习的目的分析下内部的代码实现。
基准测试
官方给出了 fastcache
, bigcache
, 标准库 map
, sync.Map
的基准测试比较结果。
GOMAXPROCS=4 go test github.com/VictoriaMetrics/fastcache -bench='Set|Get' -benchtime=10s
goos: linux
goarch: amd64
pkg: github.com/VictoriaMetrics/fastcache
BenchmarkBigCacheSet-4 2000 10566656 ns/op 6.20 MB/s 4660369 B/op 6 allocs/op
BenchmarkBigCacheGet-4 2000 6902694 ns/op 9.49 MB/s 684169 B/op 131076 allocs/op
BenchmarkBigCacheSetGet-4 1000 17579118 ns/op 7.46 MB/s 5046744 B/op 131083 allocs/op
BenchmarkCacheSet-4 5000 3808874 ns/op 17.21 MB/s 1142 B/op 2 allocs/op
BenchmarkCacheGet-4 5000 3293849 ns/op 19.90 MB/s 1140 B/op 2 allocs/op
BenchmarkCacheSetGet-4 2000 8456061 ns/op 15.50 MB/s 2857 B/op 5 allocs/op
BenchmarkStdMapSet-4 2000 10559382 ns/op 6.21 MB/s 268413 B/op 65537 allocs/op
BenchmarkStdMapGet-4 5000 2687404 ns/op 24.39 MB/s 2558 B/op 13 allocs/op
BenchmarkStdMapSetGet-4 100 154641257 ns/op 0.85 MB/s 387405 B/op 65558 allocs/op
BenchmarkSyncMapSet-4 500 24703219 ns/op 2.65 MB/s 3426543 B/op 262411 allocs/op
BenchmarkSyncMapGet-4 5000 2265892 ns/op 28.92 MB/s 2545 B/op 79 allocs/op
BenchmarkSyncMapSetGet-4 1000 14595535 ns/op 8.98 MB/s 3417190 B/op 262277 allocs/op
从测试的结果中可以看到:
fastcache
在所有操作上都要比bigcache
快fastcache
在只写 读写混合
操作比标准库的map, sync.Map
要快,只读
操作比后者要慢
组件特性
- 高性能
- 线程安全
- 设计为存储大量数据 (没有 GC 开销)
- 自动删除比较旧数据
- 使用简单
- 源代码简单且非常轻量
- 缓存数据可以保存到文件,也可以从文件中加载
示例
代码语言:javascript复制package main
import (
"fmt"
"github.com/VictoriaMetrics/fastcache"
)
func main() {
// 初始化一个大小为 32MB 的缓存
cache := fastcache.New(32 * 1024 * 1024)
key := []byte(`hello`)
val := []byte(`world`)
cache.Set(key, val) // 设置 K-V
fmt.Println(cache.Has(key)) // true
fmt.Println(cache.Has([]byte(`hello2`))) // false
fmt.Printf("hello = %sn", cache.Get(nil, key)) // hello= world
cache.Del(key)
fmt.Println(cache.Has(key)) // fasle
}
从示例代码可以看到,除了初始化时需要指定缓存的大小,组件提供的 API 就是常规的 “键值对” 语义操作,例如 Get
, Set
, Del
等。
高性能设计细节
fastcache
采用类似 bigcache
的设计思路:
缓存
由许多桶
组成,每个桶都持有一个锁 (分段锁),这样可以提高多核 CPU 的性能,因为多个 CPU 可以同时访问不同的桶- 每个桶由一个哈希索引 (数据块的位置) 和 65536 个数据块组成,每个桶的指针数量最多为
桶的存储容量 / 64KB
(这里指 bucket.chunks 字段)。 例如,64GB 缓存将包含大约 1M 指针,而类似大小的 map[string][]byte 将包含大约 1B 指针,这将导致巨大的 GC 开销
从设计上来说,和每个桶持有一个大的数据块相比,fastcache
采用的 64KB 的数据块减少了内存碎片和总内存使用量。 此外当从 全局数据块空闲区
获取数据块时,会直接调用 Mmap
分配到堆外内存,减少了总内存使用量,因为 GC 会更频繁地收集未使用的内存,无需调整 GOGC。
使用约束和限制
fastcache
组件库的使用有 4 个约束条件,在技术选型的时候比较重要,不过从下面的 4 点要求来看,实际应用中可以通过设计合理的数据类型来规避这些约束条件。
- 缓存数据的
key
和value
数据类型必须是[]byte
, 如果是其他类型,必须在存储前转换为[]byte
- 缓存数据大小超过
64K
, 必须调用SetBig
方法存储 - 缓存数据没有过期时间,只有当缓存数据的数量溢出时,才会删除比较旧的数据,通用的实践是将过期时间存入数据中
- 缓存数据采用
环形缓冲区
存储,这意味着数据量过大的情况下,新的数据会重写并覆盖掉旧的数据
此外值得注意的是,Set 方法并没有返回值来表示操作的执行结果,这种设计丢失了方法语义,并且在某些极端情况下造成难以排查的 Bug。
小结
本文主要分析了 fastcache
组件的缓存设计与实现,我们可以从中学习到 3 个非常重要的设计技巧: 采用分段锁降低锁的粒度
, 采用指纹 哈希索引快速定位数据位置
, map 使用 [uint64]uint32 作为非指针优化从而避免 GC
, 在组件的基础上,也许我们可以进一步优化 (例如将桶的锁粒度细化到数据块)?感兴趣的读者可以通过修改源代码进行测试 (毕竟 fastcache 的源代码非常轻量)。 此外需要注意的是,fastcache
提供的 Get
, Set
方法参数类型都是 []byte
, 这意味着在调用方法前,必须现将具体的数据类型或对象转换为 []byte
, 这会带来额外的 CPU 消耗。 最后,fastcache
提供的 缓存数据 <=> 文件
写入和加载功能以及 全局数据块空闲区
,由于时间关系,本文不再分析其具体代码实现。