你应该学会的 bigcache 优化技巧( 三 )


type cacheShard struct { hashmapmap[uint64]uint32 entriesqueue.BytesQueue locksync.RWMutex entryBuffer []byte onRemoveonRemoveCallback isVerbosebool statsEnabled bool loggerLogger clockclock lifeWindowuint64 hashmapStats map[uint64]uint32 statsStats}func (s *cacheShard) set(key string, hashedKey uint64, entry []byte) error { currentTimestamp := uint64(s.clock.epoch()) s.lock.Lock()// 查找是否已经存在了对应的缓存对象 , 如果存在 , 将它的值置为空 if previousIndex := s.hashmap[hashedKey]; previousIndex != 0 {if previousEntry, err := s.entries.Get(int(previousIndex)); err == nil {resetKeyFromEntry(previousEntry)} }// 触发是否要移除最老的缓存对象 if oldestEntry, err := s.entries.Peek(); err == nil {s.onEvict(oldestEntry, currentTimestamp, s.removeOldestEntry) }// 将对象放入到一个字节数组中 , 如果已有的字节数组(slice)可以放得下此对象 , 则重用 , 否则新建一个字节数组 w := wrapEntry(currentTimestamp, hashedKey, key, entry, &s.entryBuffer) for {// 尝试放入到字节队列中 , 成功则加入到map中if index, err := s.entries.Push(w); err == nil {s.hashmap[hashedKey] = uint32(index)s.lock.Unlock()return nil}// 如果空间不足 , 移除最老的元素if s.removeOldestEntry(NoSpace) != nil {s.lock.Unlock()return fmt.Errorf("entry is bigger than max shard size")} }}func wrapEntry(timestamp uint64, hash uint64, key string, entry []byte, buffer *[]byte) []byte { keyLength := len(key) blobLength := len(entry) + headersSizeInBytes + keyLength if blobLength > len(*buffer) {*buffer = make([]byte, blobLength) } blob := *buffer binary.LittleEndian.PutUint64(blob, timestamp) binary.LittleEndian.PutUint64(blob[timestampSizeInBytes:], hash) binary.LittleEndian.PutUint16(blob[timestampSizeInBytes+hashSizeInBytes:], uint16(keyLength)) copy(blob[headersSizeInBytes:], key) copy(blob[headersSizeInBytes+keyLength:], entry) return blob[:blobLength]}queue.BytesQueue 是一个字节数组 , 可以做到按需分配 。当加入一个[]byte时 , 它会把数据 copy 到尾部 。
值得注意的是删除缓存元素的时候 bigcache 只是把它的索引从map[uint64]uint32中删除了 , 并把它在queue.BytesQueue队列中的长度置为 0 。那么删除操作会不会在queue.BytesQueue中造成很多的“虫洞”?从它的实现上来看 , 会, 而且这些"虫洞"不会被整理 , 也不会被移除 。因为它的底层是使用一个字节数组实现的 , "虫洞"的移除是一个耗时的操作 , 会导致锁的持有时间过长 。那么寻找合适的"虫洞"重用呢?虽然遍历的方法寻找"虫洞"也是一个比较耗时的操作 , 我觉得这里有优化的空间 。
bigcache 只能等待清理最老的元素的时候把这些"虫洞"删除掉 。
剔除对于 bigcache 来说 , 剔除还有意义吗?或许有 。如果我们不想使用某个 key 的缓存对象 , 我们可以把它删除 。
首先 , 在增加一个元素之前 , 会检查最老的元素要不要删除 。
if oldestEntry, err := s.entries.Peek(); err == nil { s.onEvict(oldestEntry, currentTimestamp, s.removeOldestEntry)}其次 , 在添加一个元素失败后 , 会清理空间删除最老的元素 。
同时 ,  还会专门有一个定时的清理 goroutine, 负责移除过期数据 。
另外需要注意的是缓存对象没有读取的时候刷新过期时间的功能 , 所以放入的缓存对象最终免不了过期的命运 。
另外所有的缓存对象的 lifewindow 都是一样的 , 比如 30 分钟、两小时 。
所以 , 如果你真的使用 bigcache, 还是得需要注意它的这些设计 , 看看这些设计是否和你的场景相吻合 。
fastcachebigcache 在特定时候还是有问题 , 就是当 queue.BytesQueue 的容量不够的时候 , 它会进行扩展 , 扩展是一个很重的操作 , 它会复制原来的数据到新的字节数组上 。
fasthttp 的作者采用类似 bigcache 的思想实现了fastcache[15] , 他使用 chunks [][]byte替换 queue.BytesQueue , chunk 是一个 ring buffer, 每个 chunk 64KB 。
type bucket struct { mu sync.RWMutex // chunks is a ring buffer with encoded (k, v) pairs. // It consists of 64KB chunks. chunks [][]byte // m maps hash(k) to idx of (k, v) pair in chunks. m map[uint64]uint64 // idx points to chunks for writing the next (k, v) pair. idx uint64 // gen is the generation of chunks. gen uint64 getCallsuint64 setCallsuint64 missesuint64 collisionsuint64 corruptions uint64}虽然chunks [][]byte也包含很多的 chunk, 但是由于 chunk 的 size 比较大 , 所以可以大大缩小垃圾回收需要 mark/scan 的对象的数量 。带来的好处就是扩容的时候只需要增加更多的 chunk 即可 。


推荐阅读