Redis String 类型底层揭秘

目录

前言

String 类型低层数据结构

节省内存的数据结构


前言

Redis 的 string 是个 “万金油” ,这么评价它不为过. 它可以保存Long 类型整数,字符串, 甚至二进制也可以保存。对于key,value 这样的单值,查询以及插入都是O(1)时间复杂度。满脑子都是它的优点,真的就那么好吗?如果不了解它的底层结构,会有很多坑的,下面让我们细细说来。

有这样一个需求,我们要开发一个图片存储系统,要求这个系统能快速地记录图片ID和图片在存储系统中保存的ID(图片存储对象ID)。同时还更够根据图片ID快速查询图片存储对象ID。我们现在假设图片ID和图片存储对象ID都是10位数据,而且图片数量非常多。一个图片ID 和存储对象ID正好一一对应,是典型的“键 - 单值”模式。所谓的“单值”,就是指键值对中的值就是一个值,而不是一个集合,这和 String 类型提供的“一个键对应一个值的数据”的保存形式刚好契合。

v1v2

v1:存储图片ID和图片存储对象前Reids 内存

v2:存储图片ID和图片存储对象后Reids 内存

v2.used_memory-v1.used_memory=96B ,也就是说明key,value 存储用了96个字节。一组图片的ID以及存储对象ID实际上只需要16字节就可以了。

我们来分析下。图片 ID 和图片存储对象 ID 都是 10 位数,我们可以用两个 8 字节的 Long 类型表示这两个 ID。因为8字节的Long类型表示两个ID。因为8字节的Long类型最大可以表示2的64次方的数值,肯定可以表示10位数,为啥用了96个字节呢?我们先聊聊string 底层数据结构。

String 类型底层数据结构

其实,除了记录实际的数据,String 类型还需要额外的内存空间记录数据长度、空间使用等信息,这些信息也叫元数据。当实际保存数据较小时,元数据空间很大,那就很不划算了。

大家在用Redis 的 String 类型时有没有考虑过一个问题,为啥String 那么神奇,既可以保存int,还可以保存字符串,还可以保存二进制数据。这些其实都与它低层的编码有关,不同的数可能会用不同的编码。String 编码有int、embstr 和 raw 这三种编码模式.为了详细说明,我用一张图表示:

通过图可以看出,embstr 和 Raw 编码有len,alloc, buf

  1. buf:字节数组,保存实际数据。为了表示字节数组的结束,Redis 会自动在数组最后加一个“\0”,这就会额外占用 1 个字节的开销

  2. len:占 4 个字节,表示 buf 的已用长度

  3. alloc:也占个 4 字节,表示 buf 的实际分配长度,一般大于 len

len+alloc+buf = SDS

int编码:RedisObject = 元数据+INT,其他的编码 RedisObject = 元数据+ptr

通过图我们也可以发现embstr编码的ptr 与 SDS 是一块连续的内存区域,这样就可以避免内存碎片。当保存Long 类型数据时,RedisObject 中的指针就直接赋值为整数数据了,这样就不用额外的指针再指向整数了,节省了指针的空间开销。

上面的描述我们发现RedisObject 就是我们保存的数据,那么我们是怎么找到数据呢。不要忘了Redis 会使用一个全局的哈希表保存所有的键值对。哈希表的每一项是一个Entry 也就是我们所说的哈希桶,哈希桶每一项是一个, dictEntry 的结构体,用来指向一个键值对。dictEntry 结构中有三个 8 字节的指针,分别指向 key、value 以及下一个 dictEntry。为了详细描述这个结构,我画了一个图。

通过这些知识点我们可以分析出为什么我们会存储96个字节,key,value 会根据hash 算法,在全局哈希表表找到一个哈希桶,哈希桶每个元素就是一个Entry,那么我们的key就是放在key指针所引用的RedisObject结构体,value 就是放在value指针所引用的RedisObject 的结构体。一个key,value 所占用的空间根据图所示应该包含entry 的大小以及它们自己的RedisObject。

sum(entry) = key指针占用空间+value 指针占用空间+next指针占空空间 = 8B+8B+8B=32B,为啥是32呢。由于Redis 使用的内存分配库 jemalloc 。jemalloc 有一个特点就是分配内存一定是N 的 2 的幂次数,这样可以避免经常分配内存而影响系统的性能。所以就是32B

sum(redisobject) = sum(key的redisobject)+sum(value的redisobject) = 16B+16B = 32B

一共花费了多少内存空间?sum(key+value) = sum(entry) +sum(redisobject) = 64B,可能又有人问 96B-64B=32B去哪了,那是Entry(新申请的哈希桶)的大小是32B.这个就不能算是String消耗的内存空间。明明有效数据就是16B,有48B浪费了,如果有1亿张图片呢?1亿*48B就有那么多浪费,真的是真金白银呀,有没有更加节省的内存呢?当然有,那就是ziplist。

节省内存的数据结构

Redis 有一种底层数据结构,叫压缩列表(ziplist). 这个数据结构下面我们详细讲解.我找了一个图,可以展开分析下

zlbytes 表示列表长度,zltail表示列表尾,zllen 偏移量,zlend 表示列表结束

entry 是有以下结构组成 = prev_len+len+encoding+content

prev_len:表示前一个 entry 的长度。prev_len 有两种取值情况:1 字节或 5 字节。取值 1 字节时,表示上一个 entry 的长度小于 254 字节。虽然 1 字节的值能表示的数值范围是 0 到 255,但是压缩列表中 zlend 的取值默认是 255,因此,就默认用 255 表示整个压缩列表的结束,其他表示长度的地方就不能再用 255 这个值了。所以,当上一个 entry 长度小于 254 字节时,prev_len 取值为 1 字节,否则,就取值为 5 字节。

len:表示自身长度,4 字节;

encoding:表示编码方式,1 字节;

content:保存实际数据。

这些 entry 会挨个儿放置在内存中,不需要再用额外的指针进行连接,这样就可以节省指针所占用的空间。

如果我们用ziplist结构存储数据时消耗的内存是sum(entry)= sum(prev_len)+sum(len)+sum(encoding)+sum(content)=1+4+1+8=14B实际分配了16B。

这种数据结构在Redis 中有Hash、List、Sorted set 集合类型。这种方案节省内存的原因是一个key对应一个集合数据,保存数据很多也只用一个dictEntry结构这样就节省内存,key ,value 各用一个元数据。当数据多的时候,空间复杂度就被分摊了,这样就节省内存了。

怎么保存hash的结构,我们需要对原始的单值的key用二级编码方式拆分两部分,前一部分作为Hash集合的key,后一部分作为hash集合value 的key。

以图片 ID 1101000060 和图片存储对象 ID 3302000080 为例,我们可以把图片 ID 的前 7 位(1101000)作为 Hash 类型的键,把图片 ID 的最后 3 位(060)和图片存储对象 ID 分别作为 Hash 类型值中的 key 和 value。

hset 1101000 060 3302000080

把最后 3 位作为 Hash 类型值中的 key,也是根据配置文件中的设置,只有这样才能利用ziplist 的结构,如果超过了这个阈值,那么就只能用hash表存储数据呢,并不能有效的节省内存了。

这两个阈值分别对应以下两个配置项:

hash-max-ziplist-entries:表示用压缩列表保存时哈希集合中的最大元素个数。

hash-max-ziplist-value:表示用压缩列表保存时哈希集合中单个元素的最大长度。

hash-max-ziplist-value 这个我们一定是可以满足的,hash-max-ziplist-entries 这个值一般设置成1000,三位数最大值是999加上一个特殊值000刚好1000。这个值不建议设置很大,ziplist 除了最开始的原始和最后的原始查找的时间复杂度是O(1),其他位置的元素时间复杂度是O(n),值越大,获取元素越低效

除了用hash 结构,也可以用 Sorted Set,zadd 1101000 3302000080 060 可以把value 当作score 存储。显然 Sorted Set 在插入数据性能上没有hash 高效,这是因为Sorted Set插入数据时首先根据score找到对应位置,然后在插入进去,而Hash在插入元素时,只需要将新的元素插入到ziplist的尾部即可,不需要定位到指定位置

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

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

相关文章

【C++】结构体内存对齐详解

规则 1.第一个成员在结构体变量偏移量为0 的地址处,也就是第一个成员必须从头开始。 2.其他成员的偏移量为对齐数**(该成员的大小 与 编译器默认的一个对齐数 中的较小值)**的整数倍。 3.结构体总大小对最大对齐数(通过最大成员来确定)的整数…

Oracle 直接路径插入(Direct-Path Insert)

直接路径插入(Direct Path Insert)是Oracle一种数据加载提速技术,可以在使用insert语句或SQL*Loader工具大批量加载数据时使用。直接路径插入处理策略与普通insert语句完全不同,Oracle会通过牺牲空间,安全性&#xff0…

opengles 绘制图元 ——glDrawArrays() 相关API介绍 (十)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、opengles3.0 绘制图元介绍二、绘图图元 API 介绍1. glDrawArrays()1.1 glDrawArrays()函数原型1.2 GL_TRIANGLES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN 三者的区别1.3 使用GL_TRIANGLES, G…

springboot+maven项目导入本地jar包,以有打包错误问题

1 本地jar包放置路径为: 2添加Modules File->project settings–>Modules–>Dependencies–>–>, 3 添加 Libraies 至此 项目即可成功运行。 mvn 打包错误,需要 运行以下命令 mvn install:install-file -Dfile${project.basedir}/s…

52.2k star! 自己部署gpt4free, 免费使用各种GPT

GPT4Free是一个由开发者Xtekky在GitHub上发布的开源项目,它可以免费地使用GPT-3.5、GPT-4、llama、gemini-pro、bard、claude等多种大模型。截止到当前(2024.1.30)已经有52.2k star,可见其受欢迎程度。 github地址:https://github.com/xtekky…

拜登:“一切非 Rust 项目均为非法”,开发界要大变天?

文章目录 科技巨头应为安全漏洞负起责任使用其他语言的开发者​该何去何从? 白宫国家网络总监办公室(ONCD,以下简称网总办)在本周一发布的报告中说道:“程序员编写代码并非没有后果,他们的⼯作⽅式于国家利…

Leetcode 134. 加油站 java版 如何解决环路加油站算法

# 官网链接:. - 力扣(LeetCode) 1. 问题描述: 在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。 你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i1 个加油站需要消耗汽油 cost[i] 升…

C语言之操作符详解

文章目录 一、算术操作符二、移位操作符1、 原码、反码、补码2、左移操作符3、右移操作符 三、位操作符1、按位与【&】2、按位或【|】3、按位异或【^】4、按位取反【~】5、两道面试题6、进制定位将变量a的第n位置为1将变量a的第n位置为0 四、赋值操作符1、复合赋值符 五、单…

Linux系统---nginx(4)负载均衡

目录 1、服务器配置指令 ​编辑 1.1 服务器指令表 1.2 服务器指令参数 2、负载均衡策略指令 2.1 轮询 (1) 加权轮询 (2) 平滑轮询 2.2 URL 哈希(一致性哈希) 2.3 IP哈希策略 2.4 最少连接 Nginx 负载均衡是由代理模块和上…

【STM32】STM32学习笔记-WDG看门狗(46)

00. 目录 文章目录 00. 目录01. WDG简介02. IWDG概述03. IWDG框图04. IWDG键寄存器05. WWDG简介06. WWDG框图07. WWDG工作特性08. IWDG和WWDG对比09. 预留10. 附录 01. WDG简介 WDG(Watchdog)看门狗 看门狗可以监控程序的运行状态,当程序因为…

嵌入式烧录报错:板端IP与PC的IP相同

报错: 配置 实际上我配置并没有错。 服务器IP(就是本机)、板端IP、网关。此处网关必须与板子IP配套(可以不存在)。 解决 我网卡配置了多个IP。一番删除添加还是报错。 于是点击服务器IP,换成别的&#x…

鸿蒙OS应用编程实战:构建未来应用的基石

💂 个人网站:【 海拥】【神级代码资源网站】【办公神器】🤟 基于Web端打造的:👉轻量化工具创作平台💅 想寻找共同学习交流的小伙伴,请点击【全栈技术交流群】 引言 鸿蒙OS(HarmonyOS&#xff0…

CentOS7如何使用Docker部署Wiki.Js知识库并实现公网远程访问?

文章目录 1. 安装Docker2. 获取Wiki.js镜像3. 本地服务器打开Wiki.js并添加知识库内容4. 实现公网访问Wiki.js5. 固定Wiki.js公网地址 不管是在企业中还是在自己的个人知识整理上,我们都需要通过某种方式来有条理的组织相应的知识架构,那么一个好的知识整…

网络工程师笔记4

协议 端口号 FTP 21、20 HTTP 80 Telnet 23 SMTP 25 TCP头部 TCP三次握手 TCP重传机制:超…

【ArcPy】验证输入字段是否有效

实例展示 代码 def __init__(self):self.parameters arcpy.GetParameterInfo() def updateMessages(self):if(self.parameters[0].value and self.parameters[1].value):shpPathself.parameters[0].valueAsTextfileNameself.parameters[1].valueAsTextworkspacearcpy.Describe…

仿牛客网项目---用户注册登录功能的实现

从今天开始我们来写一个新项目,这个项目是一个完整的校园论坛的项目。主要功能模块:用户登录注册,帖子发布和热帖排行,点赞关注,发送私信,消息通知,社区搜索等。这篇文章我们先试着写一下用户的…

如何在群晖NAS中开启FTP服务并实现公网环境访问内网服务

文章目录 1. 群晖安装Cpolar2. 创建FTP公网地址3. 开启群晖FTP服务4. 群晖FTP远程连接5. 固定FTP公网地址6. 固定FTP地址连接 本文主要介绍如何在群晖NAS中开启FTP服务并结合cpolar内网穿透工具,实现使用固定公网地址远程访问群晖FTP服务实现文件上传下载。 Cpolar内…

SHARE 100M PRO:航测新高度,精准捕捉每一帧

SHARE 100M PRO:单镜头航测相机的革新,巡检效率与精度的新标杆 在航测和巡检领域,精确的数据采集对于确保项目成功至关重要。SHARE 100M PRO,作为一款单镜头航测相机,以其卓越的性能和创新技术,正在重新定…

【活动】前端世界的“祖传代码”探秘:从古老魔法到现代重构

作为一名前端工程师,我时常在项目中邂逅那些被岁月打磨过的“祖传代码”。它们就像古老的魔法书页,用HTML标签堆砌起的城堡、CSS样式表中的炼金术,以及JavaScript早期版本中舞动的符咒。这些代码承载着先驱们的探索精神和独特智慧&#xff0c…

智慧应急:构建全方位、立体化的安全保障网络

一、引言 在信息化、智能化快速发展的今天,传统的应急管理模式已难以满足现代社会对安全保障的需求。智慧应急作为一种全新的安全管理模式,旨在通过集成物联网、大数据、云计算、人工智能等先进技术,实现对应急事件的快速响应、精准决策和高…