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

news2024/9/20 7:50:13

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 Cache
    maxBucketBytes := 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) / chunkSize
	b.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) - 1
	p := freeChunks[n]
	freeChunks[n] = nil
	freeChunks = 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和value
	if 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]byte
	kvLenBuf[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.chunks
	needClean := false
	b.mu.Lock()
	idx := b.idx
	//计算新的写入位置:idxNew 是在当前索引 idx 的基础上加上 kvLen(键值对的总长度),计算出插入操作后的新位置。
	idxNew := idx + kvLen
	//计算 chunkIdx(当前块索引)和 chunkIdxNew(新块索引)
	chunkIdx := idx / chunkSize
	chunkIdxNew := idxNew / chunkSize
	//如果新块索引超出了现有块的范围,需要新创建块
	//如果超出块数组长度,重置索引和长度,增加生成代数 b.gen,并可能清理旧块。
	//否则,调整当前块的起始索引

	if chunkIdxNew > chunkIdx {
		// 如果新的块索引 chunkIdxNew 超过了当前已分配的块的数量(即 chunks 切片的长度),说明需要重新初始化块
		//如果下一个数据块的索引 大于 数据块的数量
		if chunkIdxNew >= uint64(len(chunks)) {
			// 此时采用环形缓冲区的方式: 从头开始存储数据
			//将 idx 和 chunkIdx 重置为 0,并将 idxNew 设为 kvLen,这表示从新的块开始写入数据
			idx = 0
			idxNew = kvLen
			chunkIdx = 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 * chunkSize
			idxNew = idx + kvLen
			chunkIdx = 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 = idxNew
	if 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 := false
	chunks := b.chunks
	b.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 >> bucketSizeBits
		idx := 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 / chunkSize
			if chunkIdx >= uint64(len(chunks)) {
				// 如果计算出的 chunkIdx 超出了 chunks 的范围,说明数据可能在文件加载过程中被损坏。
				// 增加腐败计数器,然后跳转到 end 标签以解锁资源并返回。
				atomic.AddUint64(&b.corruptions, 1)
				goto end
			}
			chunk := chunks[chunkIdx]
			idx %= chunkSize
			if 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 += 4
			if 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 += keyLen
				if returnDst { // 如果 returnDst 为 true,将值追加到 dst
					dst = 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.coloradmin.cn/o/2080132.html

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

相关文章

Unity(2022.3.41LTS) - 网格,纹理,材质

目录 零.简介 一、网格&#xff08;Mesh&#xff09; 二、材质&#xff08;Material&#xff09; 三、纹理&#xff08;Texture&#xff09; 四、三者之间的关系 零.简介 在 Unity 中&#xff0c;网格&#xff08;Mesh&#xff09;、纹理&#xff08;Texture&#xff09;和…

软考评测知识点

常见的存储单位&#xff1a; 1B8bit 1TB1024GB 1GBMBKBB 机器数&#xff1a;将符号数字化的数&#xff0c;是数字在计算机中的二进制表示形式。&#xff08;最高位0表示正数&#xff0c;1表示负数&#xff09; 二进制正数的原码、反码、补码不变&#xff0c;移码等于补码符号位…

外包干了两年,快要废了。。。

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 简单的说下&#xff0c;我大学的一个同学&#xff0c;毕业后我自己去了自研的公司&#xff0c;他去了外包&#xff0c;快两年了我薪资、技术各个方面都有了很大的…

Ubuntu下部署Hadoop集群+Hive(三)

Hive部署 准备环境 apache-hive-4.0.0-bin.tar.gz、mysql-connector-j-8.1.0.jar 如果是离线安装的话&#xff0c;使用mysql-8.0.34-1.el7.x86_64.rpm-bundle.tar&#xff0c;在线安装的话则不用&#xff1b; hive下载地址&#xff1a;Index of /hive (apache.org) mysql …

面试中的SEO优化:从基本概念到实用策略

前言 为什么要学习SEO SEO对于Web站点很重要&#xff0c;有助于优化网页在搜索引擎中的排名&#xff0c;提升网站可见性和流量。掌握SEO技术可以确保网页结构和内容对搜索引擎友好&#xff0c;从而提高用户访问量和用户体验。而且SEO被面试问的很多 SEO是什么&#xff1f; …

day02-面向对象-多态抽象类接口

一、⭐多态⭐ 1.1 概述 1.多态是在继承/实现情况下的一种现象, 表现为对象多态和行为多态 ​ 2.⭐对象多态写法&#xff1a; ​继承&#xff1a;父类 变量 new 子类1()&#xff1b; ​父类 变量 new 子类2()&#xff1b;实现&#xff1a;接口 变量 new 实现类(); ​ 3.多态…

Comsol 微穿孔板吸声性能优化、提升吸声系数

微穿孔板吸声体是由穿孔直径在1毫米以下的薄板和板后空腔组成的共振吸声结构。与传统的吸声材料及普通穿孔板吸声体相比,微穿孔板吸声体清洁,可收回重复利用,不燃,坚固,重量轻,由于不需另加纤维等多孔性吸声材料即可获得良好的吸声性能,且制造不受材料限制,不污染环境,已成功应…

【Python 千题 —— 基础篇】简易银行

= Python 千题持续更新中 …… 脑图地址 👉:⭐https://twilight-fanyi.gitee.io/mind-map/Python千题.html⭐ 题目描述 题目描述 编写一个面向对象的程序,模拟一个简化的银行系统。要求定义一个 BankAccount 类,具有基本的存款、取款和查询余额的功能。然后,创建一个 S…

HPM5301系列--VSCODE开发环境问题修复(一)

一、目的 问题描述&#xff1a;在配置工程和编译工程时出现以下提示&#xff0c;并且无法进入调试。 [cpptools] The build configurations generated do not contain the active build configuration. Using "release" for CMAKE_BUILD_TYPE instead of "Relea…

自定义注解+拦截器+多线程,实现字典值的翻译

上一篇,自定义注解拦截器,实现字段加解密操作,奈何公司的这个项目里没有字典值翻译的功能,正好可以再自定义注解拦截器方式的基础上,扩展一下 第一步,新建一个注解 Target(ElementType.FIELD) Retention(RetentionPolicy.RUNTIME) public interface Dict {//对应数据字典的cod…

IO进程线程8月27日

1&#xff0c;思维导图 2&#xff0c;使用两个线程分别复制文件的上下两部分到同一个文件 #include<myhead.h> sem_t fastsem; //pthread_mutex_t fastmutex; void *capy_up(void *c) { // pthread_mutex_lock(&fastmutex);int len*(int *)c;int fp1open("./1…

STM32的IAP

STM32的IAP(In-Application Programming,在应用编程)地址主要涉及IAP程序本身的存储地址以及它所要操作的应用程序(APP)的存储地址。这些地址通常与STM32的FLASH存储器映射相关,并且可以根据具体的STM32型号和IAP程序的设计进行调整。 1. IAP程序存储地址 IAP程序本身…

可视耳勺好用吗?四大可视挖耳勺超值好物分享!

在近年来&#xff0c;可视挖耳勺以其高效的清洁效能&#xff0c;逐渐成为备受青睐的护理产品设备。面对市面上琳琅满目的可视挖耳勺品牌&#xff0c;其质量参差不齐&#xff0c;用户在选择时往往面临着挑战。劣质可视挖耳勺不仅不能达到应有的清洁效果&#xff0c;还可能由于不…

我要做全栈:自学前端第一天

大家好&#xff0c;今天要介绍的是我自学前端的一些经验。 前端想要知道学习哪些内容&#xff0c;首先要知道前端的结构是什么样的&#xff0c;前端是有哪些东西构成的。 所以我先了解了前端的构成是由三部分&#xff1a; 1、HTML&#xff1a;定义了网页的结构 2、CSS&…

DDOS攻击学习-渗透测试-域名信息收集

文章目录 wordpress漏洞利用域名信息收集域名介绍域名分类 whoiswhois反查子域名收集子域名发现网络空间安全搜索引擎SSL证书查询js文件发现子域名 wordpress漏洞利用 这个一般都需要安装wordpress服务使用wpscan扫描&#xff0c;但现在一般很少人知道或者使用wordpress所以这个…

Tkinter Checkbutton设置了一个多选,为什么初始值都是勾选的:

代码如下&#xff1a; from tkinter import *master Tk()renyuan ["唐僧", "沙僧", "悟空", "八戒"]def r_change():rec ""ci 0for el in vars:rec el.get() "、"ci 1rec "九点" rec "离…

论文速览【LLM】 —— 【ORLM】Training Large Language Models for Optimization Modeling

标题&#xff1a;ORLM: Training Large Language Models for Optimization Modeling文章链接&#xff1a;ORLM: Training Large Language Models for Optimization Modeling代码&#xff1a;Cardinal-Operations/ORLM发表&#xff1a;2024领域&#xff1a;使用 LLM 解决运筹优化…

浙商之源——龙游商帮丨情义担当与信誉丰碑——姜益大布行

在龙游这片古老而繁华的土地上&#xff0c;流传着一段关于龙商精神的光辉篇章——姜益大的故事。这不仅是一段商业传奇&#xff0c;更是龙游商人智慧、勇气与诚信的生动写照。 初来乍到&#xff0c;逆锋起笔 清朝同治六年(1867)&#xff0c;安徽绩溪人姜德明在龙游商帮朋友点拨…

《JavaEE进阶》----2.<Spring前传:Maven项目管理工具>

本篇博客讲解我们在使用Spring框架时所要用到的Maven这个项目管理工具 它可以更方便的管理我们的项目&#xff0c;比如通过 1.常用的Maven命令来进行编译、测试、打包、清理包等等&#xff0c;不仅如此&#xff0c; 2.Maven还可以对依赖进行管理&#xff0c;方便我们添加依赖、…

信息打点-资产泄露CMS识别Git监控SVNDS_Store备份

知识点&#xff1a; 1、CMS指纹识别源码获取方式&#xff1b; 2、习惯&配置&特性等获取方式&#xff1b; 3、托管资产平台资源搜索监控&#xff1b; 详细点&#xff1a; 参考&#xff1a;https://www.secpulse.com/archives/124398.html 源码泄露原因&#xff1a; …