bigcache源码解析

1. 设计目标

Bigcache 是用 Golang 实现的本地内存缓存的开源库,主打的就是可缓存数据量大,查询速度快。 在其官方的介绍文章《 Writing a very fast cache service with millions of entries in Go 》一文中,明确提出的 bigcache 的设计目标:

  1. 多: 缓存的元素数量非常大,可以达到百万级或千万级。
  2. 快: 对延迟有非常高的要求,平均延迟要求在 5 毫秒以内。redis 、memcached 之类的就不在考虑范围内了,毕竟用 Redis 还要多走一遍网络 IO 。
  3. 稳: 99.9 分位延迟应在 10 毫秒左右,99.999 分位延迟应在 400 毫秒左右。

目前有许多开源的 cache 库,大部分都是基于 map 实现的,例如 go-cache,ttl-cache 等。bigcache 明确指出,当数据量巨大时,直接基于 map 实现的 cache 库将出现严重的性能问题,这也是他们设计了一个全新的 cache 库的原因。

本文将通过分析 bigcache v3.1.0 的源码,揭秘 bigcache 如何解决现有 map 库的性能缺陷,以极致的性能优化,实现超高性能的缓存库。

2. 整体原理

2.1. 时序图

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

2.1.1. 为什么分片数需要是2的幂次

这样分片数减1之后,前几位都是1,可以通过位运算&取hash的后几位,即通过位运算实现hash取余,更加高效

1. hashkey和分片数都为uint64,
2. 如分片数为16,减1后为15,二进制为为00000000 00001111
3. 如果hashkey为17,二进制为0000000 00010001
4. hashkey&(shareds-1)=17&(16-1)=1
5. 17%16=1, 即hashkey&(shareds-1)能通过位运算实现取余

3. 数据结构

img

4. 注意点

  1. bigcache处理hash冲突的方式是把map和entries里的值删掉,然后再重新插入。之前的值会被删除

5. 初始化

func newBigCache(ctx context.Context, config Config, clock clock) (*BigCache, error) {// Shards数需要是2的次幂if !isPowerOfTwo(config.Shards) {return nil, errors.New("Shards number must be power of two")}if config.MaxEntryByte < 0 {return nil, errors.New("MaxEntrySize must be >= 0")}if config.MaxEntriesInWindow < 0 {return nil, errors.New("MaxEntriesInWindow must be >= 0")}if config.HardMaxCacheSize < 0 {return nil, errors.New("HardMaxCacheSize must be >= 0")}lifeWindowSeconds := uint64(config.LifeWindow.Seconds())if config.CleanWindow > 0 && lifeWindowSeconds == 0 {return nil, errors.New("LifeWindow must be >= 1s when CleanWindow is set")}if config.Hasher == nil {config.Hasher = newDefaultHasher()}cache := &BigCache{shards:     make([]*cacheShard, config.Shards),lifeWindow: lifeWindowSeconds,clock:      clock,hash:       config.Hasher,config:     config,shardMask:  uint64(config.Shards - 1),close:      make(chan struct{}),}var onRemove func(wrappedEntry []byte, reason RemoveReason)if config.OnRemoveWithMetadata != nil {onRemove = cache.providedOnRemoveWithMetadata} else if config.OnRemove != nil {onRemove = cache.providedOnRemove} else if config.OnRemoveWithReason != nil {onRemove = cache.providedOnRemoveWithReason} else {onRemove = cache.notProvidedOnRemove}for i := 0; i < config.Shards; i++ {cache.shards[i] = initNewShard(config, onRemove, clock)}if config.CleanWindow > 0 {go func() {ticker := time.NewTicker(config.CleanWindow)defer ticker.Stop()for {select {case <-ctx.Done():returncase t := <-ticker.C:cache.cleanUp(uint64(t.Unix()))case <-cache.close:return}}}()}return cache, nil
}

6. set

func (c *BigCache) Set(key string, value []byte) error {hashedKey := c.hash.Sum64(key)shard := c.getShard(hashedKey)return shard.set(key, hashedKey, value)
}

核心代码在shard.go上

func (s *cacheShard) set(key string, hashedKey uint64, value []byte) error {currentTimestamp := uint64(s.clock.Epoch())s.lock.Lock()//检查 hashmap 中是否已有相同的 hashedKey。如果有,则清理掉//获取先前的条目,并调用 resetHashFromEntry 函数重置该条目的哈希值(通常是为了清理之前的哈希映射)。//然后,从 hashmap 中删除旧的 hashedKey 条目if previousIndex := s.hashmap[hashedKey]; previousIndex != 0 {if previousEntry, err := s.entries.Get(int(previousIndex)); err == nil {//打上一个已处理的标记,保证数据在淘汰的时候不再去调用OnRemove的callbackresetHashFromEntry(previousEntry)//remove hashkeydelete(s.hashmap, hashedKey)}}//如果清理(淘汰)功能未启用,则检查缓存中是否存在最旧的条目(oldestEntry)。//如果存在,调用 s.onEvict 方法处理淘汰操作,将最旧的条目移除或进行其他处理if !s.cleanEnabled {if oldestEntry, err := s.entries.Peek(); err == nil {s.onEvict(oldestEntry, currentTimestamp, s.removeOldestEntry)}}// 使用 wrapEntry 函数将条目封装起来。wrapEntry 函数创建一个新的条目对象,//包括时间戳、哈希键、原始键、条目数据以及一个缓冲区(entryBuffer),以便在缓存中进行存储w := wrapEntry(currentTimestamp, hashedKey, key, value, &s.entryBuffer)//使用循环将封装的条目 (w) 推入 entries 数据结构中。如果成功,更新 hashmap 中的索引,并解锁 lock,然后返回 nil 表示成功。//如果推入操作失败(例如由于空间不足),调用 s.removeOldestEntry(NoSpace) 尝试移除最旧的条目以腾出空间。//如果此操作仍未成功(即条目太大无法放入缓存),则解锁并返回一个错误,表示条目超出了最大缓存分片大小for {// 将包装过的value放入entries中if index, err := s.entries.Push(w); err == nil {s.hashmap[hashedKey] = uint64(index)s.lock.Unlock()return nil}if s.removeOldestEntry(NoSpace) != nil {s.lock.Unlock()return errors.New("entry is bigger than max shard size")}}
}

7. get

func (c *BigCache) Get(key string) ([]byte, error) {hashedKey := c.hash.Sum64(key)shard := c.getShard(hashedKey)return shard.get(key, hashedKey)
}
func (s *cacheShard) get(key string, hashedKey uint64) ([]byte, error) {s.lock.RLock()wrappedEntry, err := s.getWrappedEntry(hashedKey)if err != nil {s.lock.RUnlock()return nil, err}//zhmark 2024/8/21 有可能因为不同的key生成的hashedKey相同,虽然这次get时候的hashkey在map中存在,//但是实际上是其他key存入的,这个时候实际上是没有set过这个key的if entryKey := readKeyFromEntry(wrappedEntry); key != entryKey {s.lock.RUnlock()s.collision()if s.isVerbose {// 发生碰撞s.logger.Printf("Collision detected. Both %q and %q have the same hash %x", key, entryKey, hashedKey)}return nil, ErrEntryNotFound}value := readEntry(wrappedEntry)s.lock.RUnlock()s.hit(hashedKey)return value, nil
}

8. 设计优点

8.1. 处理并发访问

8.1.1. 设计点1:将数据打散后存储

通用解法: 缓存支持并发访问是很基本的要求,比较常见的解决访问是对缓存整体加读写锁,在同一时间只允许一个协程修改缓存内容。这样的缺点是锁可能会阻塞后续的操作,而且高频的加锁、解锁操作会导致缓存性能降低。

设计点: BigCache使用一个shardshard数组来存储数据,将数据打散到不同的shardshard里,每个shardshard里都有一个小的locklock,从而减小了锁的粒度,提高访问性能。

8.1.2. 设计点2:打散数据过程中借助位运算加快计算速度

接下来看一下将某个数据放到缓存的过程的源代码:

// Set saves entry under the key
func (c *BigCache) Set(key string, entry []byte) error {hashedKey := c.hash.Sum64(key)shard := c.getShard(hashedKey)return shard.set(key, hashedKey, entry)
}
func (c *BigCache) getShard(hashedKey uint64) (shard *cacheShard) {return c.shards[hashedKey&c.shardMask]
}

可以得到setset的过程如下:

  • 进行hashhash操作,将stringstring类型keykey哈希为一个uint64uint64类型的hashedKeyhashedKey
  • 根据hashedKeyhashedKey做shardingsharding,最后落到的shardshard的下标为hashedKey%nhashedKey%n,其中nn是分片数量。理想情况下,每次请求会均匀地落在各自的分片上,单个shardshard的压力就会很小。
  • 调用对应shardshard的set方法来设置缓存

设计点:
当nn为22的幂次方的时候,对于任意的xx,下面的公式都成立的。

x mod N=(x&(N−1))x mod N=(x&(N−1))

所以可以借助位运算快速计算余数,因此倒推回去 缓存分片数必须要设置为22****的幂次方

8.1.3. 设计点3 避免栈上的内存分配

默认的哈希算法为fnv64fnv64算法,该算法采用位运算的方式在栈上运算,避免了在堆上分配内存

package bigcache// newDefaultHasher returns a new 64-bit FNV-1a Hasher which makes no memory allocations.
// Its Sum64 method will lay the value out in big-endian byte order.
// See https://en.wikipedia.org/wiki/Fowler–Noll–Vo_hash_function
func newDefaultHasher() Hasher {return fnv64a{}
}type fnv64a struct{}const (// offset64 FNVa offset basis. See https://en.wikipedia.org/wiki/Fowler–Noll–Vo_hash_function#FNV-1a_hashoffset64 = 14695981039346656037// prime64 FNVa prime value. See https://en.wikipedia.org/wiki/Fowler–Noll–Vo_hash_function#FNV-1a_hashprime64 = 1099511628211
)// Sum64 gets the string and returns its uint64 hash value.
func (f fnv64a) Sum64(key string) uint64 {var hash uint64 = offset64for i := 0; i < len(key); i++ {hash ^= uint64(key[i])hash *= prime64}return hash
}

8.2. 减少GC开销

8.2.1. 设计点1:利用go1.5+特性,减少GC扫描

golanggolang里实现缓存最简单的方式是mapmap来存储元素,比如map[string]Itemmap[string]Item。
使用mapmap的缺点为垃圾回收器GCGC会在标记阶段访问mapmap里的每一个元素,当mapmap里存储了大量数据的时候会降低程序性能。

BigCacheBigCache使用了go1.5go1.5版本以后的特性:如果使用的map的key和value中都不包含指针,那么GC会忽略这个map
具体而言,BigCacheBigCache使用map[uint64]uint32map[uint64]uint32
来存储数据,不包含指针,GCGC就会自动忽略这个mapmap。

mapmap的keykey存储的是缓存的keykey经过hashhash函数后得到的值
mapmap的valuevalue存储的是序列化后的数据在全局[]byte[]byte中的下标。
因为BigCacheBigCache是将存入缓存的valuevalue序列化为bytebyte数组,然后将该数组追加到全局的bytebyte数组里(说明:结合前面的打散思想可以得知一个shardshard对应一个全局的bytebyte数组
这样做的缺点是删除元素的开销会很大,因此BigCacheBigCache里也没有提供删除指定keykey的接口,删除元素靠的是全局的过期时间或是缓存的容量上限,是先进先出的队列类型的过期

9. 可优化点

  1. shard的默认为1024,不需要使用方配置

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1522328.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

“20人+14天”,个人开发者如何通过 Google Play 谷歌封闭测试

个人开发者的应用测试要求 为了帮助开发者提供高品质的应用从而带给用户更优质的使用体验&#xff0c;Google为所有在2023年11月13日之后创建的个人开发者账号增加了一项要求&#xff1a; 至少有20名测试人员在过去至少14天内选择持续参与测试。 满足这项要求后即可申请正式版…

SqlServer: 安装或升级到SqlServer2022

一、下载安装包。 https://info.microsoft.com/ww-landing-sql-server-2022.html?lcidzh-CN 简单注册一下之后&#xff0c;就可以下载安装包了。 或者在我的资源中下载&#xff1a; https://download.csdn.net/download/yenange/89709660 系统要求&#xff1a; https://…

<数据集>遥感航拍飞机和船舶和识别数据集<目标检测>

数据集格式&#xff1a;VOCYOLO格式 图片数量&#xff1a;19973张 标注数量(xml文件个数)&#xff1a;19973 标注数量(txt文件个数)&#xff1a;19973 标注类别数&#xff1a;2 标注类别名称&#xff1a;[ship,plane] 序号类别名称图片数框数1ship17575416292plane239815…

微信小程序webgl 显示图片

// wxml <view class"container"><!-- 加载地图容器 --><canvas type"webgl" id"testMap" style"width: 100%; height: 100%;" disable-scroll bindtouchstart"touchStart" bindtouchmove"touchMove&qu…

二开PHP泛目录生成源码 可生成新闻页面和关键词页面——码山侠

PS 本资源提供给大家学习及参考研究借鉴美工之用&#xff0c;请勿用于商业和非法用途&#xff0c;无任何技术支持&#xff01; 下载i5i.net 泛目录可以用来提升网站收录和排名 合理运用目录可以达到快速出词和出权重的效果 程序小 基本的服务器都带的得动 打开i5i.net——…

HarmonyOS开发实战( Beta5版)不要使用函数/方法作为复用组件的入参规范实践

概述 在滑动场景下&#xff0c;常常会对同一类自定义组件的实例进行频繁的创建与销毁。此时可以考虑通过组件复用减少频繁创建与销毁的能耗。组件复用时&#xff0c;可能存在许多影响组件复用效率的操作&#xff0c;本篇文章将重点介绍如何通过组件复用四板斧提升复用性能。 组…

惠中科技光伏清洗剂:科技创新引领绿色清洁新风尚

惠中科技光伏清洗剂&#xff1a;科技创新引领绿色清洁新风尚 在光伏产业蓬勃发展的今天&#xff0c;光伏板的清洁问题日益凸显&#xff0c;成为影响发电效率的关键因素之一。面对传统清洗方法效率低、成本高、环境影响大等痛点&#xff0c;惠中科技以科技创新为驱动&#xff0…

HIVE 数据仓库工具之第二部分(数据库相关操作)

HIVE 数据仓库工具之第二部分&#xff08;数据库相关操作&#xff09; 一、Hive 对数据库的操作1.1 创建数据库1.1.1 创建数据库语法1.1.3 示例 1.2 使用数据库1.2.1 使用数据库语法1.2.2 示例 1.3 修改数据库1.3.1 修改数据库的语法1.3.2 示例 1.4 删除数据库1.4.1 删除数据库…

【IT工具】Windows下XMind安装教程【不要米】及常用快捷键

目录 下载相关资料安装改造备注附录&#xff1a;Xmind 快捷键 下载相关资料 下载地址&#xff1a;链接: https://pan.baidu.com/s/1aSvhE_U2WKGQ3oaGvcaHqA?pwd6666 提取码: 6666 安装 双击Xmind.exe安装 安装完成之后&#xff0c;不要登录&#xff0c;关闭就行 改造 …

数据结构(单向链表)

单向链表代码 #ifndef _LINK_H_#define _LINK_H_typedef int DataType;typedef struct node {DataType data;struct node *pnext; }Link_Node_t;typedef struct link {Link_Node_t *phead;int clen; }Link_t;extern Link_t *link_creat(); extern int push_link_head(Link_t *…

2018年系统架构师案例分析试题一

目录 案例 【题目】 【问题 1】(8 分) 【问题 2】(8 分) 【问题 3】(9 分) 【答案】 【问题 1】解析 【问题 2】解析 【问题 3】解析 相关推荐 案例 阅读以下关于软件系统设计的叙述&#xff0c;在答题纸上回答问题 1 至问题 3。 【题目】 某文化产业集团委托软件公…

【flask】python框架flask的hello world

创建一个py文件&#xff0c;写如下内容 # save this as app.py from flask import Flaskapp Flask(__name__)app.route("/") def hello():return "Hello, World!"如下图 在此py文件路径下启动cmd&#xff0c;输入 flask run结果如下图 在浏览器中访问…

计算机毕业设计选题推荐-企业人事管理系统-Java/Python项目实战

✨作者主页&#xff1a;IT毕设梦工厂✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Py…

算法练习: 矩阵置零

给定一个 m x n 的矩阵&#xff0c;如果一个元素为 0 &#xff0c;则将其所在行和列的所有元素都设为 0 。请使用 原地 算法 要实现这个功能&#xff0c;我们可以使用原地算法。首先&#xff0c;我们需要两个额外的数组来记录哪些行和列需要被置为0。然后&#xff0c;我们遍历整…

3600关成语填字APP游戏ACCESS\EXCEL数据库

成语类的APP游戏在最近一两年内非常的火爆&#xff0c;其主要原因是几乎所有中国人都能够冲个几十上百关&#xff0c;学习和趣味共享。看图猜成语类的数据之前已经弄到过很多&#xff0c;今天这份成语填字的倒是头一份。 该数据做成的APP效果如下&#xff1a; 数据以\符号分隔…

Windows 11 下使用 MSVC 2022 编译64位Nginx

一、软件准备 1、安装 Visual Studio 2022 包含单个组件&#xff1a; .NET Framework 4.6.1 目标包.NET Framework 4.6.1 SDKWindows 通用 C 运行时Windows 通用 CRT SDKMSVC v142 - VS 2019 C x64/x86 生成工具(v14.26)对 v142 生成工具(14.21)的 C/CLI 支持Clang compile fo…

Confluence8.5.14安装

一、Centos8、安装jdk11(略) 二、mysql数据库 1、mysql安装包下载: MySQL :: Download MySQL Community Server 2、安装: https://downloads.mysql.com/archives/get/p/23/file/mysql-8.0.37-1.el8.x86_64.rpm-bundle.tar tar -xvf mysql-8.0.37-1.el8.x86_64.rpm-bund…

【Hadoop|HDFS篇】HDFS概述

1. HDFS产出背景及定义 1.1 HDFS产生背景 随着数据量越来越大&#xff0c;在一个操作系统存不下所有的数据&#xff0c;那么就分配到更多的操作系 统管理的磁盘中&#xff0c;但是不方便管理和维护&#xff0c;迫切需要一种系统来管理多台机器上的文件&#xff0c;这 就是分布…

智能化升级:AI在客服知识库中的应用

引言 在数字化时代&#xff0c;客户服务已成为企业竞争的关键一环。随着人工智能&#xff08;AI&#xff09;技术的飞速发展&#xff0c;传统客服模式正经历着前所未有的变革。AI与客服知识库的深度融合&#xff0c;不仅极大地提升了客服处理的效率与准确性&#xff0c;还为用…

vue3集成sql语句编辑器

使用的是codemirror 安装 pnpm add codemirror vue-codemirror --savepnpm add codemirror/lang-sqlpnpm add codemirror/theme-one-dark使用 <template><codemirror v-model"configSql" placeholder"Code goes here..." ref"codemirrorR…