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 即可 。
推荐阅读
- 手把手教你分析具体链接的谷歌曝光量和点击率
- 养颜美容 几款科学饮茶方推荐
- 职场霸凌|网络诽谤、职场霸凌……最高法院法官为你答疑
- 螃蟹要怎样蒸应该怎样放在锅里 螃蟹放盘子里蒸还是直接放篦子上蒸
- 怎么对付瞧不起那些亲戚的人 怎么对付欺负你的亲戚长辈
- 美甲|美甲工具和各种必用品
- 你喂员工吃草,却指望他们有狼性?
- 想要加薪却张不开嘴?学会“福格行为模型”,加薪真没那么难
- 买芝士时,选“再制干酪”还是“奶酪”?有啥区别?学会别挑错了
- 温柔的穿搭也太招人喜欢了!学会这么搭配,优雅高级