Redis - 底层数据结构

news2024/11/17 1:57:17

简介

Redis 的底层数据结构主要以下几种:

  • SDS(Simple Dynamic String, 简单动态字符串)
  • ZipList(压缩列表)
  • QuickList(快表)
  • Dict(字典)
  • IntSet(整数集合)
  • ZSkipList(跳跃表)

简单动态字符串

在 Redis 中,并不会直接使用 C 语言自带的字符串结构作为实际的存储结构,而只是将字符串作为字面量使用,大多数情况使用自定义的 SDS 来表示字符串。

SDS 主要用于储存 Redis 的默认字符串表示、AOF 模块中的 AOF 缓冲区、客户端状态输入缓冲区。它的定义如下:

 
struct sdshdr {
int len; // 记录 buf 数组中已使用字节的数量,等于 SDS 所保存的字符串的长度
int alloc; // 记录 buf 数组中未使用字节的数量
char buf[]; // 字节数组,用于保存字符串
};

优点

相对于 C 语言的字符串实现,Redis 实现的 SDS 有以下优点:

  • 通过记录 len 属性,实现常数级时间复杂度获取字符串长度
  • 通过检查 len 属性,避免字符串在修改时出现缓冲区溢出的情况
  • 通过记录 len 属性和 alloc 属性,对于修改字符串实现了空间预分配和惰性空间释放
  • 实际存储的 buf 是一个字节数组,可以实现 SDS 安全操作二进制数据
  • SDS 仍然以 \0 作为字符串结尾的标识,这样可以重用 C 语言字符串的部分函数

空间预分配

当 SDS 修改时需要扩展空间大小,程序不仅会为 SDS 扩展修改所需的空间,还会为 SDS 分配额外的未使用空间。这额外空间一般是 len 的大小,最大不超过 1MB。

这样可以减少连续执行字符串增长操作所需的内存重分配次数。

惰性空间释放

当 SDS 修改时需要缩短空间大小,程序并不会立即将多出来的空间进行空间重分配,而是使用 alloc 属性将这些空间大小记录下来,以待后续使用。

而且 SDS 也提供手动释放未使用空间的方法,这样可以避免浪费内存。

压缩列表

ZipList 实际是由一系列特殊编码的连续内存块组成的顺序型数据结构,是 Hash 类型底层实现的一种方式。

结构

一个 ZipList 结构由 zlbyteszltailzllenentrieszlend 这些属性组成,这些属性顺序连接在一起组成了 ZipList:

ZipList 结构

zlbytes 用于记录 ZipList 占用的内存字节数,在对 ZipList 进行内存重分配或者计算 zlend 的位置时使用。

zltail 记录了 ZipList 表尾结点距离 ZipList 的起始地址有多少个字节,Redis 可以通过这个属性快速确定表尾结点的地址。

zllen 记录了 ZipList 包含的结点数量,当这个属性小于 UINT16_MAX(65535) 时,这个值就是 ZipList 包含的结点数量;这个属性大于 UINT16_MAX 时,则需要遍历整个 ZipList 才能计算得出结点数量。一个 ZipList 可以包含任意多个结点,每个结点可以保存一个字节数组或者一个整数值。

zlend 定义了特殊值 OxFF 用于标记 ZipList 的末端。

优点

ZipList 是一种节省内存的列表结构,对于普通的数组来说,其中每个元素占用的空间大小取决于最大的元素,而 ZipList 解决了这个问题。

因此,ZipList 在设计的时候,尽量让每个元素按照实际的内容大小存储,所以增加了 encoding 属性,使得程序可以根据不同的 encoding 属性来细化存储大小。

由于数组每个元素都占用相同的内存空间,在遍历数组时非常方便。

而 ZipList 每个元素存储的内存空间不一样,为了解决倒序遍历的问题,增加了 prevlen 属性来定位上一个元素的起始位置。

缺点

ZipList 内部的数据存储是一段连续的空间,并且没有预留内存空间,在移除结点时也是立即缩容,这表示每次写操作都会进行内存分配操作。

第二个缺点就是,在某种情况下,ZipList 会出现连锁更新的问题。

连锁更新

ZipList 存储了 prevlen 属性表示前一个元素的长度,如果前一个元素长度小于 254 个字节,则 prevlen 使用 1 个字节保存这个长度值,如果前一个结点大于 254 个字节,则 prevlen 使用 5 个字节保存这个长度值。

如果 ZipList 中正好存在连续多个长度介于 250~253 个字节的结点,这时需要在 ZipList 前面插入一个大于等于 254 个字节的新结点,后一个结点的 prevlen 需要从 1 个字节转换成 5 个字节,则后一个结点也会大于等于 254 个字节,后续的结点以此类推,将会造成这部分结点出现连续更新。

快表

Redis 在 3.2 版本之后新增了快表数据结构,它是一种以 ZipList 为结点的双端链表结构,可以理解成分段的 ZipList 链接在一起。

快表结构

在 3.2 版本之前,Redis 使用 ZipList 或 LinkedList 来实现 List 类型,并且有一个选择的标准:

  • 保存的所有字符串元素的长度都小于 64 字节,且保存的元素数量小于 512 个,选择使用 ZipList
  • 否则使用 LinkedList 数据结构替代 ZipList

ZipList 要求有一段连续的内存空间,而使用 LinkedList 分配内存又会出现大量的内存碎片,因此 QuickList 对此做了优化,既避免出现大量的内存碎片,又避免一次性占用内存过大。

字典

字典在 Redis 中的应用非常广泛,比如 Redis 的数据库就是使用字典来作为底层实现的,对数据库的增、删、改、查操作都是构建在对字典的操作之上的。

哈希表结点

字典存储数据的最小结构就是哈希表结点,Redis 中的哈希表结点使用 dictEntry 结构表示,每个 dictEntry 都保存着一个键值对:

 
typedef struct dictEntry {
void *key; // 键值对的键
union { // 键值对的值
void *val; // 可以是一个指针
uint64_t u64; // 可以是一个 uint64_t 整数
int64_t s64; // 可以是一个 int64_t 整数
} v;
struct dictEntry *next; // 指向下个哈希表节点,形成链表
} dictEntry;

这里值得注意的就是,next 指针会指向下一个哈希表结点,而它的功能就是用于解决哈希冲突,由此可见 Redis 的哈希表解决哈希冲突的方法是链地址法。

哈希表

哈希表是由多个哈希表结点组成的,Redis 中自定义的哈希表结构如下:

 
typedef struct dictht {
dictEntry **table; // 哈希表数组
unsigned long size; // 哈希表大小
unsigned long sizemask; // 哈希表大小掩码,用于计算索引值,总是等于 size - 1
unsigned long used; // 该哈希表已有节点的数量
} dictht;

一般的,哈希表的物理存储结构都是数组,Redis 的哈希表结构也是如此,而这个结点数组中的每个元素都是一个指向 dictEntry 结构的指针。

字典结构

Redis 为了使哈希表结构更加具有通用性,最后是在自定义的 dictht 哈希表结构外层再包一层字典结构,即是 dict 结构:

 
typedef struct dict {
dictType *type; // 类型特定函数
void *privdata; // 私有数据
dictht ht[2]; // 哈希表
int rehashidx; // rehash 索引,当 rehash 不在进行时,值为 -1
} dict;

这里展示了另一个 dictType 的结构,其实这个结构保存了一簇用于操作特定类型键值对的函数,Redis 会为用途不同的字典设置不同的类型特定函数。以下是 dictType 的结构定义:

 
typedof struct dictType {
unsigned int (*hashFunction)(const void *key); // 计算哈希值的函数
void *(*keyDup)(void *privData, const void *key); // 复制键的函数
void *(*valDup)(void *privData, const void *obj); // 复制值的函数
int (*keyCompare)(void *privData, const void *key1, const void *key2); // 对比键的函数
void *(*keyDestructor)(void *privData, const void *key); // 销毁键的函数
void *(*keyDestructor)(void *privData, const void *obj); // 销毁值的函数
} dictType;

其实 dict 结构的 type 属性和 privdata 属性是针对不同类型的键值对,为创建多态字典而设置的。其中 privData 属性保存了需要传给那些类型特定函数的可选参数。

需要注意一下,字典结构的 ht 属性是一个长度为 2 的数组,也就是说,这个字典结构存储了两个 dictType 结构,其中一个用于存储实际使用的哈希表,另一个用于存储重新哈希的临时哈希表。

这个重新哈希还涉及到了 rehashidx 属性,表示的是重新哈希当前的进度。

哈希算法

当要将一个新的键值对添加到字典里面时,程序会先根据键值对的键计算出哈希值和索引值,然后再根据索引值,将包含新键值对的哈希表结点放到哈希表数组的指定索引上。

Redis 计算哈希值和索引值的流程是:通过 dict 中的 type 属性找到计算哈希值的函数,然后通过函数计算出对应的哈希值;确定对应的 dictht 结构之后,再根据 sizemask 和哈希值计算出索引值。

Redis 使用 MurmurHash2 算法计算键的哈希值,其优点就是对于有规律的输入值也能给出很好的随机分布性,并且算法的计算速度也非常快。

哈希冲突

相同的哈希值会计算出相同的索引值,这就会导致哈希冲突。

Redis 使用了链地址法解决哈希冲突,每一个哈希表结点都有一个 next 指针,多个冲突的哈希表结点就会使用这个 next 指针构成一个单向链表,这就解决了键冲突的问题。

这里需要注意一点,由于哈希表结点不存储链表的尾结点,为了速度考虑,哈希冲突时构建的单向链表使用头插法插入一个链表结点。

重新哈希

随着操作不断执行,哈希表保存的数据会逐渐增多或减少,为了让哈希表的负载因子维持在一个合理的范围内,Redis 会在必要的时候进行重新哈希的操作。

重新哈希指的是重新计算哈希表结点的哈希值和索引值,然后将键值对放到 ht 数组的另一个哈希表中。

Redis 对哈希表进行扩展操作的两个条件如下:

  • 服务器目前没有正在执行 BGSAVE 命令或 BGREWRITEAOF 命令,并且哈希表的负载因子大于等于 1。
  • 服务器目前正在执行 BGSAVE 命令或 BGREWRITEAOF 命令,并且哈希表的负载因子大于等于 5。

其中负载因子 = 哈希表已保存结点数量 / 哈希表大小。

另一方面,当哈希表的负载因子小于 0.1 时,Redis 会自动开始对哈希表进行收缩操作。

Redis 做自动扩展的条件包含两种情况的原因是,执行 BGSAVE 和 BGREWRITEAOF 命令的是服务器的子进程,而大多数操作系统都采用写时复制技术以优化子进程的使用效率,所以在子进程存在期间,服务器会提高执行扩展操作所需的负载因子。

渐进式重新哈希

为了避免因为重新哈希导致停止服务的情况,Redis 做重新哈希不是一次性完成的,而是分多次、渐进式地完成的。这也是 dict 结构中存在 ht 数组的原因。

渐进式重新哈希的好处在于它采取了分而治之的方式,将重新哈希所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上,从而避免集中式重新哈希而带来的庞大计算量。

整数集合

整数集合被 Redis 用于保存整数值的不重复集合,以下是整数集合的实现:

 
typedef struct intset {
uint32_t encoding; // 编码方式
uint32_t length; // 集合包含的元素数量
int8_t contents[]; // 保存元素的数组
} intset;

其中 contents 数组中存储的是整数集合中的元素,各个项按照从小到大进行排列,且数组中不包含任何重复值。

length 属性记录了整数集合包含的元素个数,也相当于 contents 的数组长度。

encoding 记录着整数集合的编码方式,虽然 contents 的定义是 int8_t 类型,但实际上 contents 数组存储元素的真正类型取决于 encoding 的值。

升级

整数集合的 contents 属性可以存储 int16int32 或 int64 三种类型之一的数值,如果原本只存储了 int16 类型的 contents 数组插入一个 int32 类型的数值,这时就涉及到整数集合的升级操作。

每当要将一个整数插入到整机集合中时,Redis 都会先判断新元素的类型是否会比已存在的元素类型长,如果存在这种情况,整数集合需要先进行升级,才能将新元素添加到整数集合里面。具体的步骤如下:

  1. 根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间;
  2. 将现有元素都转换成与新元素的类型相同,并将转换类型后的数值放置到正确的位上,并保持原数组的顺序不变;
  3. 最后改变 encoding 的值,并将 length 加 1

整数集合的升级操作是不可逆的,一旦对数组进行了升级,编码就会一直保持升级后的状态。

升级的好处

整数集合的升级策略有两个好处,一个是提升整数集合的灵活性,另一个是尽可能地节约内存。

因为 C 语言是静态类型语言,不同类型的整数值需要用不同的数组存储,而整数集合通过升级策略将有原本不同类型的整数添加到同一个数组中,减少了类型错误的情况。

同样的,整数集合通过使用一个数组存储了三种不同类型的整数,又确保升级操作只会在有需要的时候进行,这可以尽量节省内存。

跳表

跳表是一种有序的数据结构,它通过在每个结点中维持多个指向其他结点的指针,从而达到快速访问结点的目的。

跳表结构

Redis 的跳表包括了两个结构,一个是跳表结点的结构,另一个是存储跳表结点的外部结构。

跳表结点

以下是跳表结点的结构定义:

 
typedef struct zskiplistNode {
struct zskiplistLevel { // 索引层
struct zskiplistNode *forward; // 前进指针
unsigned int span; // 跨度
} level[];
robj *obj; // 成员对象
double score; // 分值
struct zskiplistNode *backward; // 后退指针
} zskiplistNode;

这里只是一个跳表结点的结构,概念比较多。

跳表的 zskiplistLevel 是一个数组,数组的长度表示结点有多少层索引,其中每个元素都包含一个指向其他结点的指针,程序可以通过这些指针加快访问其他结点的速度。每次创建一个新的跳表结点的时候,程序都会根据幂次定律随机生成一个介于 1 和 32 之间的值作为数组的大小。

forward 是指每个索引层都包含指向下一个具有相同高度索引层的结点。也可以将前进指针理解成链表的 next 指针,从相同层级的角度上看,每一个相同层级的结点都组成了类似于链表的结构。

span 记录了两个结点之间的距离,实际上是用来计算排位的:在查找某个结点的过程中,将沿途访问过的所有层的跨度累积起来,得到的结果就是目标结点在跳跃表的排位。

backward 用于从表尾向表头访问结点,对于最底层的链表来说,前进指针和后退指针使得这个链表成为一个双向链表。

结点的 score 即是 Redis 的有序集合中的分值。结点的成员是一个指向 SDS 对象的指针,这个 SDS 对象存储当前结点的值。对于相同分值的成员,Redis 会按照成员对象在字典序中的大小来进行排序,成员对象较小的结点会排在前面,而成员对象较大的结点会排在后面。

跳表

仅使用多个跳表结点就可以实现跳表,但是新增外部跳表结构可以使得程序更方便处理跳表。Redis 的跳表结构如下:

 
typedef struct zskiplist {
struct zskiplistNode *header, *tail; // 头节点,尾节点
unsigned long length; // 节点数量
int level; // 目前表内节点的最大层数
} zskiplist;

其中 head 指针和 tail 指针分别指向跳表的表头和表尾,通过这两个指针,Redis 定位跳表表头结点和表尾结点的时间复杂度为 O(1)。

通过记录 length 属性,Redis 可以在 O(1) 的时间复杂度内返回跳表的长度。

跳表使用 level 属性记录了表内结点的最大层数,但这个是不包含表头结点的层高的。

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

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

相关文章

hvv 云安全专项检测工具

过去的一年里,我们可能已经注意到了一个明显的趋势:安全对抗正逐步迁移至云端。 如:今年hw的要求中增加了云资产相关的得分项计算 如:越来越多的安全工具开始专注于云安全检测方向 如:越来越多的安全峰会加入了云原…

“一种蒸发冷凝水去除氨氮装置”技术专利

蒸发器中随着母盐液的蒸浓,溶液温度升高,由于氨氮易挥发的特性,游离氨挥发于水蒸气中,从而导致冷凝水中氨氮浓度上升,而影响出水水质。 “一种蒸发冷凝水去除氨氮装置”专利(证书号:第19422309号;专利号&am…

【前端】javascript+html+css 家具销售网站(代码+报告)

👉博__主👈:米码收割机 👉技__能👈:C/Python语言 👉公众号👈:测试开发自动化【获取源码商业合作】 👉荣__誉👈:阿里云博客专家博主、5…

移动端个人中心UI设计

效果图 源码如下 页面设计 <template><div class"container"><!-- 顶部用户信息 start--><div class"header"><div class"user-info"><van-image class"user-img" round width"70" :sr…

【打造超酷的GitHub主页】

文章目录 Github状态信息-api账户信息统计最常用语言Repo卡片 社交统计统计访问次数徽标 首先上地址&#xff1a;https://gitee.com/java_wxid/giteeprofile/blob/master/README.md 为了照顾一部分网络较差的的朋友们&#xff0c;这里使用国内的gitee仓库将主页代码提供给大家&…

C语言 位运算符 “|“ 的5种高级用法

前言 在上一篇文章中&#xff0c;我们介绍了&运算符的高级用法&#xff0c;本篇文章&#xff0c;我们将介绍| 运算符的一些高级用法。 一、人物简介 第一位闪亮登场&#xff0c;有请今后会一直教我们C语言的老师 —— 自在。 第二位上场的是和我们一起学习的小白程序猿 —…

16、博客列表加载效果 - 博客界面改造文章(202307)

最近闲来无事&#xff0c;就想着把博客界面弄得再有点动画效果&#xff0c;于是就找了延迟函数&#xff0c;把博客的内容列表加载的动画写出来了。 该动画效果还是挺炫的&#xff0c;但是因为刷新页面&#xff0c;下面列表的显示需要拉动到底下才看到&#xff0c;所以这里只能记…

微信小程序监测版本更新

在index.js里面 不放到app.js里面是因为有登录页面&#xff0c;在登录页面显示更新不太友好 onShow() {const updateManager wx.getUpdateManager()// 请求完新版本信息的回调updateManager.onCheckForUpdate(res > {if (res.hasUpdate) {// 新版本下载成功updateManage…

【腾讯云 Cloud Studio 实战训练营】体验搭建软件系统,无经验也能做开发

文章目录 前言IDE 解放开发者的生产力功能强大的 IDE快速搭建 Vue 开发环境注册 Cloud Studio进入Vue预置开发环境安装相关依赖包主文件 main.js 引入相关库和包首页增加移动端默认样式增加主要代码 IDE 的适用场景总结 前言 云计算技术的不断发展为代码开发带来了全新的体验&…

【Uniapp】支付链转二维码

前言 提示&#xff1a;这个是一个很小的项目&#xff0c;大概30分钟就能搞定 实现方式&#xff1a;输入支付代码&#xff0c;存储到对应的数据库表中&#xff0c;二维码访问一个PHP文件通过id来进行重定向&#xff0c;这样就可以使每张二维码都是固定的&#xff0c;替换二维码…

CS5213 国产HDMI转VGA带音频方案芯片|CS5213规格书|CS5213原理图

集睿致远/ASL推出的CS5213芯片是一个国产HDMI&#xff08;高清多媒体接口&#xff09;到VGA桥接芯片。它将HDMI信号转换为标准VGA信号它可以在适配器、智能电缆等设备中设计 CS5213特征 将HDMI信号转换为VGA输出 支持数字信号到模似信号的转换 支持 HDCP 1.0/1.1/1.2 操作简…

OpenAI API访问速度不佳?试试用Vercel来加速!

前言 众所周知&#xff0c;使用openAI API在国内访问体验并不佳&#xff0c;经常遇到访问较慢或者访问失败的问题。本文着重讲讲怎么解决这个问题&#xff0c;让我们日常开发和使用能够更方便的体验到AI带来的便利 为了帮大家省钱&#xff0c;也为了方便操作&#xff0c;实现…

论文解读|进一步融合:体积融合中6D姿态估计的多对象推理

原创 | 文 BFT机器人 01 背景 机器人等智能设备需要从它们的车载视觉系统中获得高效的基于物体的场景表示&#xff0c;以解释接触、物理和遮挡。已识别的精确对象模型将与未识别结构的非参数重建一起发挥重要作用。 本文提出了一个系统用于估计实时的接触和遮挡的精确姿态。从…

手写openFeign

目录 背景步骤应用级别&#xff1a;1、建立被调用方2、建立调用方引入依赖 3、实现效果 原理级别&#xff1a;调动方代码&#xff08;注意impl在启动的时候里边是空的&#xff09;引入jar包的代码&#xff08;写好以后推到nexus上&#xff0c;再拉到上述调用方服务中&#xff0…

收集用户隐私行为规范与修改指引

为更好地保护用户隐私信息&#xff0c;优化用户体验&#xff0c;平台对小程序内的收集用户隐私行为进行规范&#xff0c;开发者可自查所涉及的违规内容&#xff0c;并参照修改指引进行相应整改。 常见违规内容 一、隐私政策协议默示同意 小程序在收集用户数据前&#xff0c;…

【高级程序设计语言C++】二叉搜索树

1. 二叉搜索树的概念2. 二叉搜索树的功能2.1. 二叉搜索树的简单模型2.2. 二叉搜索树的查找2.3. 二叉搜索树的插入2.4. 二叉搜索树的删除 3. 二叉搜索树的性能分析 1. 二叉搜索树的概念 二叉搜索树&#xff08;Binary Search Tree&#xff0c;简称BST&#xff09;是一种常见的二…

【C/C++】类之间的纵向关系——继承的概念

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞 关注支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; &#x1f525;c系列专栏&#xff1a;C/C零基础到精通 &#x1f525; 给大…

C++——继承(2)详解

目录 一.子类和父类对象的赋值转换 子类对象赋值父类对象的另外两种方式&#xff1a; 总结&#xff1a; 二.父类与子类的作用域 1. 在继承体系中基类和派生类都有独立的作用域。 例&#xff1a; 2.作用域练习 练习1&#xff1a; 解决方法: 一.子类和父类对象的赋值转换 …

深入理解Java类加载机制中的双亲委派模型--根据源码探讨

前言&#xff1a; 今天和大家探讨一道Java中经典的面试题&#xff0c;这道面试题经常出现在各个公司的面试中&#xff0c;本篇文章主要讲解ava类加载机制中的双亲委派模型的知识。该专栏比较适合刚入坑Java的小白以及准备秋招的大佬阅读。 如果文章有什么需要改进的地方欢迎大…

Cesium态势标绘专题-三角旗标、矩形旗标、曲线旗标(标绘+编辑)

标绘专题介绍:态势标绘专题介绍_总要学点什么的博客-CSDN博客 入口文件:Cesium态势标绘专题-入口_总要学点什么的博客-CSDN博客 辅助文件:Cesium态势标绘专题-辅助文件_总要学点什么的博客-CSDN博客 本专题没有废话,只有代码,代码中涉及到的引入文件方法,从上面三个链…