1. git仓库
https://github.com/abbothzhang/fastcache
2. 整体原理
- initCache时不会申请内存,只有第一次set时候才会申请,且会一次性申请64MB,后面不够了又一次性申请1024*64MB大小内存
2.1. 时序图
3. 高性能原因
- 将cache分为512个bucket,每个bucket一个锁,将锁竞争维度降低, 增加并发度
- map定义为map[uint64]uint64, value里只存索引,真正的值放在二维数组里,这样GC时无需遍历,减少stw
- 堆外内存申请二维数组,无需GC
- 使用很多位运算,快速且节省空间
4. 注意点
- 一次会申请1024个chunk大小的内存,即1024*64KB=64MB大小的内存,如果初始化cache时候设置的缓存大小小于64MB,也会申请这么大
- 没有过期时间设置,FIFO的过期方式, 只靠缓存环覆盖
- 内存申请到初始化时设置的最大内存后,就会一直保持,不会释放
- 缓存数据大小超过
64K
, 需要调用SetBig
方法存储
5. 数据结构
- chunk为byte数组,作为环形缓冲区使用
6. 初始化
6.1. 初始化入口
func New(maxBytes int) *Cache {
if maxBytes <= 0 {
panic(fmt.Errorf("maxBytes must be greater than 0; got %d", maxBytes))
}
var c Cache
maxBucketBytes := uint64((maxBytes + bucketsCount - 1) / bucketsCount)
for i := range c.buckets[:] {
c.buckets[i].Init(maxBucketBytes)
}
return &c
}
6.2. bucket初始化
下面是bucket
的初始化方法,需要注意的是其仅仅初始化了b.chunks
的大小,并没有初始化单个chunk
的内存空间(即chunkSize
字节)。chunk
的初始化是在实际使用时从freeChunks
申请的,这样可以避免预先分配冗余内存。这种方式有点类似底层的虚拟内存的概念,只有在真正使用的时候才会分配内存。后面会看到freeChunks
是如何申请内存的
func (b *bucket) Init(maxBytes uint64) {
if maxBytes == 0 {
panic(fmt.Errorf("maxBytes cannot be zero"))
}
if maxBytes >= maxBucketSize {
panic(fmt.Errorf("too big maxBytes=%d; should be smaller than %d", maxBytes, maxBucketSize))
}
maxChunks := (maxBytes + chunkSize - 1) / chunkSize
b.chunks = make([][]byte, maxChunks)
b.m = make(map[uint64]uint64)
b.Reset()
}
6.3. 内存申请
func getChunk() []byte {
freeChunksLock.Lock()
// 检查是否有可用的内存块,如果没有,则开辟
if len(freeChunks) == 0 {
// Allocate offheap memory, so GOGC won't take into account cache size.
// This should reduce free memory waste.
//使用 unix.Mmap 分配一块较大的匿名内存区域 (chunkSize*chunksPerAlloc 字节),这块内存不会被 Go 的垃圾回收器(GOGCÿ