golang本地缓存fastcache高性能实现原理

1. git仓库

https://github.com/abbothzhang/fastcache

2. 整体原理

  1. initCache时不会申请内存,只有第一次set时候才会申请,且会一次性申请64MB,后面不够了又一次性申请1024*64MB大小内存

2.1. 时序图

在这里插入图片描述

3. 高性能原因

  1. 将cache分为512个bucket,每个bucket一个锁,将锁竞争维度降低, 增加并发度
  2. map定义为map[uint64]uint64, value里只存索引,真正的值放在二维数组里,这样GC时无需遍历,减少stw
  3. 堆外内存申请二维数组,无需GC
  4. 使用很多位运算,快速且节省空间

4. 注意点

  1. 一次会申请1024个chunk大小的内存,即1024*64KB=64MB大小的内存,如果初始化cache时候设置的缓存大小小于64MB,也会申请这么大
  2. 没有过期时间设置,FIFO的过期方式, 只靠缓存环覆盖
  3. 内存申请到初始化时设置的最大内存后,就会一直保持,不会释放
  • 缓存数据大小超过 64K, 需要调用 SetBig 方法存储

5. 数据结构

  1. chunk为byte数组,作为环形缓冲区使用

img

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 CachemaxBucketBytes := 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) / chunkSizeb.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)计入,从而减少内存浪费。data, err := unix.Mmap(-1, 0, chunkSize*chunksPerAlloc, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_ANON|unix.MAP_PRIVATE)if err != nil {panic(fmt.Errorf("cannot allocate %d bytes via mmap: %s", chunkSize*chunksPerAlloc, err))}//将这块大内存分割成多个 chunkSize 大小的小块,每个小块被添加到 freeChunks 切片中。data 切片被逐步分割并转换成 *chunkSize 类型的指针for len(data) > 0 {p := (*[chunkSize]byte)(unsafe.Pointer(&data[0]))freeChunks = append(freeChunks, p)data = data[chunkSize:]}}//从 freeChunks 切片中取出最后一个块,将其从切片中移除,并将其内容清空以防止泄露n := len(freeChunks) - 1p := freeChunks[n]freeChunks[n] = nilfreeChunks = freeChunks[:n]freeChunksLock.Unlock()return p[:]
}

7. set

func (b *bucket) Set(key, value []byte, h uint64) {// 原子地增加存储调用次数计数器atomic.AddUint64(&b.setCalls, 1)// 先行判断key、value大小,如果键 k 或值 v 的长度大于等于 65536(1<<16),方法会返回,因为下面的代码限制了只用16位存key和valueif len(key) >= (1<<16) || len(value) >= (1<<16) {// Too big key or value - its length cannot be encoded// with 2 bytes (see below). Skip the entry.return}// zhmark kvLenBuf 表示 {key + value} 的指纹//vLenBuf:用 4 字节存储键和值的长度(各用 2 字节编码),分别存储键的高 8 位和低 8 位长度,以及值的高 8 位和低 8 位长度,作为指纹var kvLenBuf [4]bytekvLenBuf[0] = byte(uint16(len(key)) >> 8)// byte(len(k)) 只保留了 len(k) 的低 8 位kvLenBuf[1] = byte(len(key))kvLenBuf[2] = byte(uint16(len(value)) >> 8)kvLenBuf[3] = byte(len(value))//kvLen:计算键值对的总长度,包括 kvLenBuf、键 k 和值 v 的长度kvLen := uint64(len(kvLenBuf) + len(key) + len(value))// 如果 kvLen 大于或等于 chunkSize(块大小),方法返回,因为键值对太大,不能存储在一个块中if kvLen >= chunkSize {// Do not store too big keys and values, since they do not// fit a chunk.return}chunks := b.chunksneedClean := falseb.mu.Lock()idx := b.idx//计算新的写入位置:idxNew 是在当前索引 idx 的基础上加上 kvLen(键值对的总长度),计算出插入操作后的新位置。idxNew := idx + kvLen//计算 chunkIdx(当前块索引)和 chunkIdxNew(新块索引)chunkIdx := idx / chunkSizechunkIdxNew := idxNew / chunkSize//如果新块索引超出了现有块的范围,需要新创建块//如果超出块数组长度,重置索引和长度,增加生成代数 b.gen,并可能清理旧块。//否则,调整当前块的起始索引if chunkIdxNew > chunkIdx {// 如果新的块索引 chunkIdxNew 超过了当前已分配的块的数量(即 chunks 切片的长度),说明需要重新初始化块//如果下一个数据块的索引 大于 数据块的数量if chunkIdxNew >= uint64(len(chunks)) {// 此时采用环形缓冲区的方式: 从头开始存储数据//将 idx 和 chunkIdx 重置为 0,并将 idxNew 设为 kvLen,这表示从新的块开始写入数据idx = 0idxNew = kvLenchunkIdx = 0//b.gen 是用于生成新的块标识符的代数。增加生成代数,并在生成代数满足一定条件时(如位掩码操作),进行额外的增加操作。//这通常用于生成唯一的块版本标识符,帮助区分不同版本的块b.gen++// 如果重写次数达到上限,那么重新开始计算// (1<<genSizeBits)-1 1先移位genSizeBits,再-1,生成genSizeBits个1// b.gen&(1<<genSizeBits)-1,表示取b.gen的低genSizeBits位,如果低genSizeBits位都是0,if b.gen&((1<<genSizeBits)-1) == 0 {b.gen++}//设定 needClean 为 true,表示需要清理旧的块(或做其他必要的管理操作),这通常是在块已满或达到一定的容量时进行的维护操作needClean = true} else {//如果 chunkIdxNew 没有超过现有块的数量,则更新当前索引 idx 和新的索引 idxNew,并设置 chunkIdx 为 chunkIdxNew。//这表示继续在当前块内写入数据,更新索引以反映新的写入位置idx = chunkIdxNew * chunkSizeidxNew = idx + kvLenchunkIdx = chunkIdxNew}//清空当前块 chunks[chunkIdx] 的内容。//虽然 chunks[chunkIdx] 被重新分配内存,//但这一步骤确保当前块的内容被清空,以便新的数据可以被正确地追加到块中// todo:2024/8/26 为什么要清理当前块数据chunks[chunkIdx] = chunks[chunkIdx][:0]}//获取或创建块 chunk。chunk := chunks[chunkIdx]if chunk == nil {chunk = getChunk()chunk = chunk[:0]}// 指纹写入数据块chunk = append(chunk, kvLenBuf[:]...)// key 写入数据块chunk = append(chunk, key...)// value 写入数据块chunk = append(chunk, value...)// 更新数据块信息chunks[chunkIdx] = chunk// 更新哈希表 b.m 以映射哈希值 h 到当前的存储位置和版本号// b.gen只用后24位,左移40位后,b.gen的值完全位于最右边// 再和idx或一下,即把gen的高位放到idx里,两个值能存一起b.m[h] = idx | (b.gen << bucketSizeBits)//更新桶的索引 b.idx 为新的位置b.idx = idxNewif needClean {// 如果缓冲区重写了,重新解析和构建数据哈希索引b.cleanLocked()}b.mu.Unlock()
}

8. get

func (b *bucket) Get(dst, key []byte, hash uint64, returnDst bool) ([]byte, bool) {atomic.AddUint64(&b.getCalls, 1)// 初始化 found 变量为 false,表示默认没有找到匹配的数据found := falsechunks := b.chunksb.mu.RLock()mapValueGenIdx := b.m[hash]// bGen 获取当前bucket的版本号,防止因为覆盖写被误读取// 通过位掩码 (1 << genSizeBits) - 1,bGen 提取了 b.gen 的低 genSizeBits 位。这个掩码确保只保留生成代数的有效部分,忽略其他位currentGen := b.gen & ((1 << genSizeBits) - 1)if mapValueGenIdx > 0 { // 如果 value 大于 0,说明存在可能的有效数据// 检查 v 是否有效且符合当前代数 bGen// 从 value 中提取生成代数 gen 和索引 idx。bucketSizeBits 表示索引部分的位数gen := mapValueGenIdx >> bucketSizeBitsidx := mapValueGenIdx & ((1 << bucketSizeBits) - 1)// 检查提取的生成代数和索引是否有效。确保数据没有被回收或被其他操作覆盖// gen == bGen && idx < b.idx: 如果当前的桶版本号一致,并且索引小于当前的,那么是OK的// gen+1 == bGen && idx >= b.idx:如果桶版本号比当前版本号低,但是idx比当前idx高,说明还没被覆盖,还是可以读取的// gen == maxGen && currentGen == 1 && idx >= b.idx:如果达到最大版本,但是当前又是重写到1了,idx比当前idx高,说明还没被覆盖,还是可以读取的if (gen == currentGen && idx < b.idx) || (gen+1 == currentGen && idx >= b.idx) || (gen == maxGen && currentGen == 1 && idx >= b.idx) {// 计算数据块的索引chunkIdx := idx / chunkSizeif chunkIdx >= uint64(len(chunks)) {// 如果计算出的 chunkIdx 超出了 chunks 的范围,说明数据可能在文件加载过程中被损坏。// 增加腐败计数器,然后跳转到 end 标签以解锁资源并返回。atomic.AddUint64(&b.corruptions, 1)goto end}chunk := chunks[chunkIdx]idx %= chunkSizeif idx+4 >= chunkSize {// 如果计算出的索引加上 4个字节 超出了 chunk 的范围,说明数据可能在文件加载过程中被损坏。// 增加腐败计数器,然后跳转到 end 标签以解锁资源并返回。atomic.AddUint64(&b.corruptions, 1)goto end}kvLenBuf := chunk[idx : idx+4]                             // 提取包含键值长度的 4 字节数据keyLen := (uint64(kvLenBuf[0]) << 8) | uint64(kvLenBuf[1]) // 解析键的长度valLen := (uint64(kvLenBuf[2]) << 8) | uint64(kvLenBuf[3]) // 解析值的长度idx += 4if idx+keyLen+valLen >= chunkSize {// 如果计算出的索引加上 keyLen 和 valLen 超出了 chunk 的范围,说明数据可能在文件加载过程中被损坏。// 增加腐败计数器,然后跳转到 end 标签以解锁资源并返回。atomic.AddUint64(&b.corruptions, 1)goto end}if string(key) == string(chunk[idx:idx+keyLen]) { // 如果键匹配,防止hash碰撞idx += keyLenif returnDst { // 如果 returnDst 为 true,将值追加到 dstdst = append(dst, chunk[idx:idx+valLen]...)}found = true} else {// 如果键不匹配,增加冲突计数器atomic.AddUint64(&b.collisions, 1)}}}
end:b.mu.RUnlock() // 释放只读锁if !found {// 如果没有找到匹配项,增加未命中计数器atomic.AddUint64(&b.misses, 1)}return dst, found // 返回结果
}

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++奇迹之旅:深度解析list的模拟实现

文章目录 &#x1f4dd;前言&#x1f320;list节点&#x1f309;list &#x1f320;迭代器的创建&#x1f309;const迭代器 &#x1f320;代码&#x1f6a9;总结 &#x1f4dd;前言 &#x1f320;list节点 我们先建立一个列表里的节点类listnode&#xff0c;用来构造list的节…

数据仓库系列 3:数据仓库的主要组成部分有哪些?

你是否曾经好奇过,当你在网上购物或使用手机应用时,背后的数据是如何被存储和分析的?答案就在数据仓库中。本文将为你揭开数据仓库的神秘面纱,深入探讨其核心组成部分,以及这些组件如何协同工作,将海量数据转化为有价值的商业洞察。 目录 引言:数据仓库的魔力1. 数据源和数据…

[Algorithm][综合训练][体育课测验(二)][合唱队形][宵暗的妖怪]详细讲解

目录 1.体育课测验(二)1.题目链接2.算法原理详解 && 代码实现 2.合唱队形1.题目链接2.算法原理详解 && 代码实现 3.宵暗的妖怪1.题目链接2.算法原理详解 && 代码实现 1.体育课测验(二) 1.题目链接 体育课测验(二) 2.算法原理详解 && 代码实现…

解决Selenium已安装,在pycharm导入时报错

搭建设selenium环境时&#xff0c;selenium已安装&#xff0c;但是在pycharm中使用“from selenium import webdriver”语句时红线报错 解决方案&#xff1a; 1.file->settings进入设置 2.点击加号&#xff0c;搜索‘selenium’安装 3&#xff0c;等待安装完成&#xff0…

项目技巧二

目录 java中Date和mysql数据库datetime数据类型 注意&#xff1a; 在yml文件中配置成员变量的值 1.写一个yml文件 2.写一个与yml相互映射的类来读取yml的属性信息 3.在其他子模块的配置类中开启此类&#xff0c;读取yml文件的内容信息 4.直接依赖注入&#xff08;因为已…

Java多进程调用dll程序和exe程序

&#x1f3af;导读&#xff1a;本文介绍了使用Java调用本地DLL及EXE程序的方法。针对DLL调用&#xff0c;文章提供了基于Java Native Access (JNA) 库的具体实现方案&#xff0c;包括定义Java接口以映射DLL中的函数&#xff0c;并展示了如何加载DLL及调用其中的方法。对于EXE程…

opencv之几何变换

文章目录 1 前言2 线性几何变换的主要类型2.1 平移 (Translation)&#xff1a;2.1.1 定义2.1.2代码 2.2 缩放 (Scaling)&#xff1a;2.2.1 定义2.2.2 代码 2.3 旋转 (Rotation)&#xff1a;2.3.1 定义2.3.2 代码 2.4 仿射变换 (Affine Transformation)&#xff1a;2.4.1 定义2.…

数据源10min自动断开连接导致查询抛异常(未获取可用连接)

由于个人能力有限&#xff0c;本文章仅仅代表本人想法&#xff0c;若有不对请即时指出&#xff0c;若有侵权&#xff0c;请联系本人。 1 背景 工作中引入druid来管理数据源连接&#xff0c;由于数据源每隔10分钟强制管理空闲超过10分钟的连接&#xff0c;导致每隔10分钟出现1…

3D打印透气钢与传统透气钢的差异

透气钢作为一种集金属强度与透气性能于一体的特殊材料&#xff0c;在注塑模具领域扮演着关键角色&#xff0c;通过有效排除模具内困气&#xff0c;显著提升制品成型质量与生产效率。当前&#xff0c;市场上主流的透气钢产品多源自日本、美国&#xff0c;其高昂成本与技术壁垒限…

Golang | Leetcode Golang题解之第388题文件的最长绝对路径

题目&#xff1a; 题解&#xff1a; func lengthLongestPath(input string) (ans int) {n : len(input)level : make([]int, n1)for i : 0; i < n; {// 检测当前文件的深度depth : 1for ; i < n && input[i] \t; i {depth}// 统计当前文件名的长度length, isFi…

生成艺术,作品鉴赏:物似主人形

2001年&#xff0c;当21岁的我&#xff0c;还在恒基伟业当高级工程师时。我有一个女同事&#xff0c;她有个特别大的杯子用来喝水&#xff0c;不夸张的说&#xff0c;是那种我从来没见过的大杯子&#xff0c;由于她是很大只的那种&#xff0c;她便自嘲说&#xff1a;「物似主人…

【Kubernetes部署篇】二进制搭建K8s高可用集群1.26.15版本(超详细,可跟做)

文章目录 一、服务器环境信息及部署规划1、K8S服务器信息及网段规划2、服务器部署架构规划3、组件版本信息4、实验架构图 二、初始化环境操作1、关闭防火墙2、配置本地域名解析3、配置服务器时间保持一致4、禁用swap交换分区(K8S强制要求禁用)5、配置主机之间无密码登录6、修改…

JVM2-JVM组成、字节码文件、类的生命周期、类加载器

Java虚拟机的组成 Java虚拟机主要分为以下几个组成部分&#xff1a; 类加载子系统&#xff1a;核心组件类加载器&#xff0c;负责将字节码文件中的内容加载到内存中运行时数据区&#xff1a;JVM管理的内存&#xff0c;创建出来的对象、类的信息等内容都会放在这块区域中执行引…

RelativeLayout相对布局

activity_relative_layout.xml <?xml version"1.0" encoding"utf-8"?> <RelativeLayout xmlns:android"http://schemas.android.com/apk/res/android"android:layout_width"match_parent"android:layout_height"150dp…

QT QGraphicsView实现预览图片显示缩略图功能

QT QGraphicsView实现预览图片显示缩略图功能QT creator Qt5.15.2 头文件&#xff1a; #ifndef TGRAPHICSVIEW_H #define TGRAPHICSVIEW_H#include <QGraphicsView> #include <QMainWindow> #include <QObject> #include <QWidget>class TGraphicsVie…

Oracle 客户端 PL/SQL Developer 15.0.4 安装与使用

目录 官网下载与安装 切换中文与注册 连接Oracle数据库 tnsnames.ora 文件使用 Oracle 客户端 PL/SQL Developer 12.0.7 安装、数据导出、Oracle 执行/解释计划、for update。 官网下载与安装 1、官网&#xff1a;https://www.allroundautomations.com/products/pl-sql-d…

P01-何谓Java方法

P01-何谓Java方法 一、System.out.println()分析 二、剖析方法 谈到方法&#xff0c;我就突然想到了c函数&#xff1a; 其实&#xff1a;Java 方法和 C 函数在许多方面确实有类似之处&#xff0c;但它们也存在一些显著的差异。下面是它们的一些共同点和不同点&#xff1a; 共同…

DORIS - DORIS简介

前言 本博文基于DORIS的2.1.5版本。apache-doris-2.1.5-bin-x64.tar.gz 是什么&#xff1f; DORIS官网 Apache Doris 是一款基于 MPP 架构的高性能、实时的分析型数据库&#xff0c;以高效、简单、统一的特点被人们所熟知&#xff0c;仅需亚秒级响应时间即可返回海量数据下的…

【第0004页 · 递归】生成括号对

【前言】本文以及之后的一些题解都会陆续整理到目录中&#xff0c;若想了解全部题解整理&#xff0c;请看这里&#xff1a; 第0004页 生成括号对 今天这题有点难绷&#xff0c;从某种程度上来说应该是第二次写这个问题了&#xff0c;但还是卡住了&#xff0c;现在我们来看一下…

安防视频汇聚平台EasyCVR启动后无法访问登录页面是什么原因?

安防视频监控/视频集中存储/云存储/磁盘阵列EasyCVR平台基于云边端一体化架构&#xff0c;兼容性强、支持多协议接入&#xff0c;包括国标GB/T28181协议、部标JT808、GA/T1400协议、RTMP、RTSP/Onvif协议、海康Ehome、海康SDK、大华SDK、华为SDK、宇视SDK、乐橙SDK、萤石云SDK等…