本地缓存库分析(四):fastcache

文章目录

  • 本系列
  • 前言
  • 设计
    • 索引和数组
    • 怎么判断是否被覆盖
    • 其他问题
  • 源码走读
    • 数据结构
    • set
    • get
  • 总结

本系列

  • 本地缓存库分析(一):golang-lru
  • 本地缓存库分析(二):bigcache
  • 本地缓存库分析(三):freecache
  • 本地缓存库分析(四):fastcache(本文)
  • 本地缓存库分析(五):groupcache(未完待续)

前言

什么是fastcache?是一款go实现的本地缓存库,具有以下特点:

  1. 支持高并发
  2. 支持存储大量数据,但对GC压力小
  3. 严格内存限制
  4. 代码非常简单,易于理解

本文将剖析其设计及代码实现

源码:https://github.com/VictoriaMetrics/fastcache,版本:v1.12.2


设计

索引和数组

和bigcache的设计很类似,fastcache将整个缓存分为512个bucket,每个bucket都自己的锁,单独控制并发

完整的KV数据放在bucket的底层数组中,用 idx 记录下次要从哪个位置开始写

底层数组可以被循环使用,写满之后从头开始写,这样被覆盖的部分就失效了

字段 gen 代表当前是从头开始写的第几轮。每次写满了要从头开始写时,gen++

在这里插入图片描述

]

每个bucket用 map[uint64]uint64 类型作为索引,key为真实key的hash值,value的高24位存储被写入时,bucket的gen值,低40位存储在底层数组中的位置
在这里插入图片描述
为啥要存gen值?用于判断是否被覆盖,下一节会详细说明


底层数据被划分成N个chunk,每个chunk的大小 固定为64KB

为啥要切分成很多小数组,而不像bigCache,freeacache一样用一个大数组装下所有数据呢?

官方说法是能减少内存碎片。我理解是能减少外部碎片,如果都是一次性分配一片很大的空间,最后会因为要分配N个字节连续的空间,剩下的总空间够用,大于N,但没有连续的一片空间,导致内存无法分配的窘境,而每次分配一片小空间能解决外部碎片的问题

但这么做带来的代价就是指针数量增加但增加的个数有限,为chunk的数量。根据官方的说法,64GB 的数据会多100万(64GB/64KB=100万)个指针,而同样的数据规模,用常规的map[string][]byte做缓存需要10亿个指针,因此这点开销还是比较小的

现在已知idx,怎么定位底层数据entry到在哪个chunk呢?

chunkIdx := idx / chunkSize

计算在该chunk的哪个位置:

idx % chunkSize

entry在底层数据中的内存布局为:2个字节key长度,2个字节val长度,然后是key,value

在这里插入图片描述

如果一行chunk剩余空间不够写,会从下一行chunk开头再写


怎么判断是否被覆盖

一旦最后一个chunk被写满后,就会从idx=0位置,也就是第0个chunk的开头位置开始写。这里可以看出 内存淘汰方式为FIFO

那么在Get时就需要有机制知道某个key是否被覆盖了,以免获取到非法的数据

bigcache,freecache在覆盖老数据时,用了特殊的技巧知道老entry的开始位置,也就能去删除该entry的索引

但fastcache中并没有这么做,而是在Get时惰性判断,并巧妙地利用了在写entry那一时刻的 bucket.gen 信息

以下2种情况,entry是可用的:

  • entry和bucket的gen相同,且entry的idx < bucket的idx,说明entry和bucket在同一轮,肯定没被覆盖
  • entry的gen比b的gen小1,且entry的idx >= bucket的idx,说明bucket比entry大一轮,已经发生了重写,但还没覆盖到entry所在的位置,该entry也是合法的

其他任何情况entry都是非法的

entry和bucket的gen相同:合法

在这里插入图片描述

entry和bucket的gen有的相同,有的不同,就看entry的idx在bucket的idx之前还是之后:
在这里插入图片描述

还有个问题,entry中的gen只有24位,如果bucket的gen一直增加,发生溢出后,和entry中存的gen相同了咋办?那上面的判定岂不是会出错

fastcache考虑到了这个问题,在每一轮开始时,都会检查一遍所有KV,将已失效的从索引m中删除

其他问题

  • hash冲突:fastcache没有解决hash冲突的问题,一旦遇到hash冲突,老的数据就会被覆盖

    • 不过不用太担心,因为hash值有64位,hash冲突的概率非常低
  • 过期时间:fastcahce不支设置过期时间。用户可以将过期时间塞到value中,在get时手动检查


源码走读

上面对fastcache的整体设计进行了详细分析,下面进行源码走读


数据结构

type bucket struct {mu sync.RWMutex// 一堆chunk,每个chunk大小固定为64KB// 当一条KV在chunk剩余空间放不下时,会从新的chunk开始写,有一定空间浪费chunks [][]byte// key:hash值,value:写入chunks的idx,和gen的或值m map[uint64]uint64// 指向下一个要写入的位置idx uint64// 表示chunks写满后,从头开始循环的次数gen uint64
}

初始化bucket,就是计算需要多少个chunk,并分配空间:

const chunkSize = 64 * 1024func (b *bucket) Init(maxBytes uint64) {// 根据maxBytes计算需要多少个chunkmaxChunks := (maxBytes + chunkSize - 1) / chunkSizeb.chunks = make([][]byte, maxChunks)b.m = make(map[uint64]uint64)b.Reset()
}

set

  1. 从idx开始写
  2. 如果当前chunk满了,从下个chunk开始写
  3. 如果最后一个chunk也满了,从头开始写,并递增bucket.gen
func (b *bucket) Set(k, v []byte, h uint64) {atomic.AddUint64(&b.setCalls, 1)// KV的长度都必须在2个字节能装下// 也就是value最多64kbif len(k) >= (1<<16) || len(v) >= (1<<16) {return}var kvLenBuf [4]bytekvLenBuf[0] = byte(uint16(len(k)) >> 8)kvLenBuf[1] = byte(len(k))kvLenBuf[2] = byte(uint16(len(v)) >> 8)kvLenBuf[3] = byte(len(v))// KV的长度 + K的长度 + V的长度,必须 < 64KBkvLen := uint64(len(kvLenBuf) + len(k) + len(v))if kvLen >= chunkSize {return}chunks := b.chunksneedClean := falseb.mu.Lock()idx := b.idxidxNew := idx + kvLen// 现在idx在哪个chunkchunkIdx := idx / chunkSize// 写完后idx在哪个chunkchunkIdxNew := idxNew / chunkSize// 用了新的chunkif chunkIdxNew > chunkIdx {// 到头了,要从第一个chunk开始写if chunkIdxNew >= uint64(len(chunks)) {idx = 0idxNew = kvLenchunkIdx = 0// 新的一轮b.gen++// 如果gen超过了24位,需要++,也就是在24位中不能有0if b.gen&((1<<genSizeBits)-1) == 0 {b.gen++}needClean = true} else {// 新的一行开始位置idx = chunkIdxNew * chunkSize// 写完后idxNew在哪idxNew = idx + kvLenchunkIdx = chunkIdxNew}chunks[chunkIdx] = chunks[chunkIdx][:0]}// 惰性初始化空间chunk := chunks[chunkIdx]if chunk == nil {chunk = getChunk()chunk = chunk[:0]}// 写KV的长度chunk = append(chunk, kvLenBuf[:]...)// 写kchunk = append(chunk, k...)// 写vchunk = append(chunk, v...)chunks[chunkIdx] = chunk// 将idx和当前的gen组合成索引的valueb.m[h] = idx | (b.gen << bucketSizeBits)b.idx = idxNewif needClean {b.cleanLocked()}b.mu.Unlock()
}

当开始新的一轮后,需要清除所有失效KV的索引:

func (b *bucket) cleanLocked() {bGen := b.gen & ((1 << genSizeBits) - 1)bIdx := b.idxbm := b.mnewItems := 0for _, v := range bm {// v的gengen := v >> bucketSizeBits// v的idxidx := v & ((1 << bucketSizeBits) - 1)// b的gen比v的gen大1,且v的idx在b的idx后面,说明v还没被覆盖if (gen+1 == bGen || gen == maxGen && bGen == 1) && idx >= bIdx ||// v的gen和b的gen相同,且v的dix小于b的idx,也说明v还没被覆盖gen == bGen && idx < bIdx {newItems++}}// 到这说明有item被覆盖了if newItems < len(bm) {bmNew := make(map[uint64]uint64, newItems)for k, v := range bm {// v的gengen := v >> bucketSizeBits// v的idxidx := v & ((1 << bucketSizeBits) - 1)// 将没有被覆盖的KV重新建个索引,并替换原有索引,这样失效的KV就没索引,再也找不到了if (gen+1 == bGen || gen == maxGen && bGen == 1) && idx >= bIdx ||gen == bGen && idx < bIdx {bmNew[k] = v}}b.m = bmNew}
}

get

  1. 从索引中,根据key的hash值拿到entry的idx和gen
  2. 根据idx个gen判断是否是否被覆盖,如果没被覆盖,去chunks找到数据返回
func (b *bucket) Get(dst, k []byte, h uint64, returnDst bool) ([]byte, bool) {atomic.AddUint64(&b.getCalls, 1)found := falsechunks := b.chunksb.mu.RLock()v := b.m[h]// bucket的genbGen := b.gen & ((1 << genSizeBits) - 1)if v > 0 {// v的gen// v的高24位:gen,低40位:indexgen := v >> bucketSizeBitsidx := v & ((1 << bucketSizeBits) - 1)// v和b的gen相同,且v的idx < b的idx,说明是合法的if gen == bGen && idx < b.idx ||// v的gen比b的gen小1,且v的idx >= b的idx,说明还没覆盖到v,v也是合法的gen+1 == bGen && idx >= b.idx ||// 此时也是:v的gen比b的gen小1,且v的idx >= b的idx,说明还没覆盖到v,v也是合法的gen == maxGen && bGen == 1 && idx >= b.idx {// 在第几个chunkchunkIdx := idx / chunkSize// 定位到chunkchunk := chunks[chunkIdx]// 定位到在该chunk的哪个位置idx %= chunkSize// 接下来4个字节是kv的长度kvLenBuf := chunk[idx : idx+4]keyLen := (uint64(kvLenBuf[0]) << 8) | uint64(kvLenBuf[1])valLen := (uint64(kvLenBuf[2]) << 8) | uint64(kvLenBuf[3])idx += 4// key值匹配if string(k) == string(chunk[idx:idx+keyLen]) {idx += keyLenif returnDst {// 读取value,返回dst = append(dst, chunk[idx:idx+valLen]...)}found = true} else {atomic.AddUint64(&b.collisions, 1)}}}b.mu.RUnlock()if !found {atomic.AddUint64(&b.misses, 1)}return dst, found
}

总结

最后看看fastcache解决了哪些原生缓存的问题:

问题解决
锁竞争严重解决,用了多个segment
大量缓存写入,导致gc标记阶段占用cpu多解决,指针数量约等于 总数据量/64KB 个
内存占用不可控解决,底层数组占用空间是固定的。索引占用的不固定,但索引占用空间小
不支持缓存按时效性淘汰按FIFO淘汰
不支持缓存过期不支持
缓存数据可以被污染解决,使用序列化后的字节数组

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

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

相关文章

安科瑞5G基站直流叠光监控系统-安科瑞黄安南

基站现状和趋势 5G基站是专门提供5G网络服务的公用移动通信基站。5G基站主要用于提供5G空口协议功能&#xff0c;支持与用户设备、核心网之间的通信。按照逻辑功能划分&#xff0c;5G基站可分为5G基带单元与5G射频单元&#xff0c;二者之间可通过CPRI或eCPRI接口连接。 2019年…

Pr 视频效果:过渡

效果面板/视频效果/过渡 Video Effects/Transition Adobe Premiere Pro 的视频效果中&#xff0c;过渡 Transition效果组用于创建单个剪辑内过渡效果的一组视频效果。这些效果可以增强视频的视觉连贯性&#xff0c;添加创意性的视觉转换&#xff0c;为观众提供流畅的观看体验。…

DataX 的安装配置和使用 (详细版)

1&#xff0c;上传解压 1&#xff0c;开始上传安装包到你虚拟机上放置安装包的文件夹 2&#xff0c;开始解压 ,配置环境变量 1、上传 /opt/modules 2、解压 tar -zxvf datax.tar.gz -C /opt/installs 3、修改 vi /etc/profile 配置环境变量&#xff1a; export DAT…

zookeeper安装

安装之前&#xff1a;先关闭三台服务器的防火墙&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; systemctl stop firewalld systemctl disable firewalld 1)上传 /opt/modules下面 2&#xff09;解压 /opt/installs下面 tar -zxvf zookeeper-3.4.10.tar.gz …

Nature文章《deep learning》文章翻译

这篇文章是对Nature上《deep learning》文章的翻译。原作者 Yann LeCun, Yoshua Bengio& Geoffrey Hinton。 这篇文章的中心思想是深入探讨深度学习在机器学习中的革命性贡献&#xff0c;重点介绍其在特征学习、监督学习、无监督学习等方面的突破&#xff0c;并阐述其在图…

动态规划—整数拆分

class Solution {public int integerBreak(int n) {int[] dp new int[n1];dp[2] 1;for(int i 3; i< n; i){for(int j 1; j< i/2; j){//j拆i&#xff0c;只需要遍历到 i/2 就可以&#xff0c;后面没有必要遍历dp[i] Math.max(dp[i], Math.max(j*(i-j) , j*dp[i-j]));…

OceanBase V4.3.3,首个面向实时分析场景的GA版本发布

在10月23日举办的 OceanBase年度发布会 上&#xff0c;我们怀着激动之情&#xff0c;正式向大家宣布了 OceanBase 4.3.3 GA 版的正式发布&#xff0c;这也是OceanBase 为实时分析&#xff08;AP&#xff09;场景打造的首个GA版本。 2024 年初&#xff0c;我们推出了 4.3.0 版本…

儿童安全座椅行业全面深入分析

儿童安全座椅就是一种专为不同体重&#xff08;或年龄段&#xff09;的儿童设计&#xff0c;将孩子束缚在安全座椅内&#xff0c;能有效提高儿童乘车安全的座椅。欧洲强制性执行标准ECE R44/03的定义是&#xff1a;能够固定到机动车辆上&#xff0c;带有ISOFIX接口、LATCH接口的…

算法笔记:Day-09(初始动态规划)

509. 斐波那契数 斐波那契数 &#xff08;通常用 F(n) 表示&#xff09;形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始&#xff0c;后面的每一项数字都是前面两项数字的和。也就是&#xff1a; F(0) 0&#xff0c;F(1) 1 F(n) F(n - 1) F(n - 2)&#xff0c;其中 …

HTTP和HTTPS 的作用和应用场景 (python 爬虫简单入门)

HTTP和HTTPS HTTP HTTP协议&#xff08;HyperText Transfer Protocol&#xff0c;超文本传输协议&#xff09;&#xff1a;是一种发布和接收 HTML页面的方法。 HTTP的端口号为80 HTTPS HTTPS&#xff08;Hypertext Transfer Protocol over Secure Socket Layer&#xff09;…

Java多线程编程(三)一>详解synchronized, 死锁,wait和notify

目录&#xff1a; 一.synchronized 的使用&#xff1a; 二. 常见死锁情况&#xff1a; 三 .如何避免死锁&#xff1a; 四.wait和notify 一.synchronized 的使用&#xff1a; 我们知道synchronized锁具有互斥的特点&#xff1a; synchronized 会起到互斥效果, 某个线程…

linux入门——“初识make”

make是linux中的自动化构建工具&#xff0c;一般来说系统会自带make&#xff0c;如果没有&#xff0c;那么可以使用命令“sudo apt install -y make”来安装。 1.初识make make使用的前提是维护makefile/Makefile文件&#xff0c;需要在自己的目录下自己创建。 我在此目录下创…

【K8S系列】Kubernetes 中 Pod 无法通过 Service 名称访问服务的 DNS 解析失败问题【已解决】

在 Kubernetes 中&#xff0c;Service 提供了一种稳定的方式&#xff0c;通过名称访问一组 Pod。当其他 Pod 无法通过 Service 名称访问服务&#xff0c;并且出现 DNS 解析失败时&#xff0c;通常会导致应用无法正常工作。本文将详细分析此问题的常见原因及其解决方案。 一、问…

关于分布式事务,你知道多少?如何落地?

很多人估计会说&#xff0c;我在项目中完全没有涉及到过分布式事务&#xff0c;而面试官老喜欢问&#xff0c;真TM烦&#xff01; 本文就来聊聊分布式事务&#xff0c;有哪些方案和实现。文章有点长&#xff0c;可以先收藏&#xff0c;有时间了慢慢看。 什么是事务&#xff1f;…

SIwave:释放 Resonant Mode Solver 的强大功能

SIwave 是一种电源完整性和信号完整性工具。本文的重点是 Resonant 模式求解器。 进行谐振计算的主要原因是确定 Powerplane 中 Cap 去耦的最佳位置。Powerplane 的大小由最大预期电流和允许的最大电压降决定。然而&#xff0c;即使是最好的设计也没有足够的电容来将宽带频谱的…

【VS+QT】联合开发踩坑记录

0. 写在前面 因为目前在做自动化产线集成软件开发相关的工作&#xff0c;需要用到QT&#xff0c;所以选择了VS联合开发&#xff0c;方便调试。学习QT的过程中也踩了很多坑&#xff0c;在此记录一下&#xff0c;提供给各位参考。 1. 环境配置 Win11Visual Studio 2019Qt 5.12…

【LeetCode】每日一题 2024_11_1 超级饮料的最大强化能量(DP)

前言 每天和你一起刷 LeetCode 每日一题~ LeetCode 启动&#xff01; 题目&#xff1a;超级饮料的最大强化能量 代码与解题思路 先读题&#xff1a; 题目给了两个数组&#xff0c;长度为 n&#xff0c;题目要求在 n 个小时内选择饮料&#xff0c;一个小时可以选一瓶&#x…

IBM服务器修改IMM的IP方法

服务器设备&#xff1a;IBM x3550 M4 Server IMM默认IP地址&#xff1a;192.168.70.125 用户名&#xff1a;USERID 密码&#xff1a;PASSW0RD&#xff08;注意是零0&#xff09; 1.服务器开机按F1进入BIOS界面 2.进入System Settings 3.进入Integrated Management Module 4.…

【MATLAB代码】一维UKF的IMM,模型有CV和CA

目录 ​编辑 代码介绍 主要功能 UKF 更新函数 总结 代码介绍 这段 MATLAB 代码实现了一维无迹卡尔曼滤波&#xff08;UKF&#xff09;与交互多模型&#xff08;IMM&#xff09;结合的算法&#xff0c;旨在对非线性动态系统进行状态估计。代码中的模型包括恒速&#xff08…

Java对象、类、接口——针对实习面试

目录 Java对象、类、接口你知道类和对象的区别吗&#xff1f;抽象类和接口有什么共同点&#xff1f;抽象类和接口有什么区别&#xff1f;说一下面向对象的三大特征及其特点&#xff1f;你知道Java中方法重载和重写的区别吗&#xff1f;静态成员和非静态成员有什么区别&#xff…