Redis设计与实现笔记 - 数据结构篇

Redis设计与实现笔记 - 数据结构篇

相信在我们日常使用中,会经常跟 Redis 打交道。数据结构 String、Hash、List、Set 和 ZSet 都是常用的数据类型。对于使用场景,我们可以滔滔不绝地说很多,但是我们从来就没有关心过它们的底层实现,到底它们的数据是怎么存储的,代码是怎么实现的,使用上有什么值得注意的地方。带着这些疑问,我去查看了相关的书籍,对于实现有了大致的认识。希望你看完后也有所收获。

先统一名词:我们常用的 String、Hash、List、Set 和 ZSet 叫做对象(Object)。它们由如 SDS、LinkList、Skiplist 等基础数据结构组成。

数据结构

简单动态字符串 - SDS

struct __attribute__ ((__packed__)) sdshdr64 {uint64_t len; /* 已使用的长度 */uint64_t alloc; /* 分配的长度 不包含头部和空终止符号 */unsigned char flags; /* 3位最低有效位表示类型, 其余5个比特位未被使用 */char buf[];
};
  1. 常数复杂度获取字符串长度。C 语言中的传统字符串类型需要遍历整个字符串才能获取字符串长度,时间复杂度为 O(n),而 SDS 可以直接获取字符串长度,时间复杂度为 O(1)。

  2. 杜绝缓冲区溢出。SDS 在字符串末尾预留了额外的空间,当字符串长度增加时,可以直接使用预留的空间,避免了缓冲区溢出的问题。

  3. 减少修改字符串长度时带来的内存重分配次数。SDS 采用了空间预分配和惰性空间释放等策略,可以减少修改字符串长度时带来的内存重分配次数,提高性能。

  4. 可含有空字符等特殊字符

主要用于字符串对象的底层实现

链表

/* 双端链表节点 */
typedef struct listNode {/* 指向前驱节点的指针 */
struct listNode *prev;/* 指向后继节点的指针 */
struct listNode *next;/* void * 指针,指向具体的元素,节点可以是任意类型 */
void *value;
} listNode;/* 双端链表迭代器 */
typedef struct listIter {/* 指向遍历的下一个节点的指针 */listNode *next;/* 遍历的方向:从表头遍历还是从表尾遍历 */int direction;
} listIter;/* 双端链表* 有记录头尾两节点,支持从链表头部或者尾部进行遍历,是早期列表键 PUSH/POP 实现高效的关键* 每个链表节点有记录前驱节点和后继节点的指针,可以使得列表键支持往后或者往前进行遍历* 有额外用 len 存储链表长度,O(1) 的时间复杂度获取节点个数,是 LLEN 命令高效的关键 */
typedef struct list {/* 指向链表头节点的指针,支持从表头开始遍历 */listNode *head;/* 指向链表尾节点的指针,支持从表尾开始遍历 */listNode *tail;/* 各种类型的链表可以定义自己的复制函数 / 释放函数 / 比较函数 */void *(*dup)(void *ptr);void (*free)(void *ptr);int (*match)(void *ptr, void *key);/* 链表长度,即链表节点数量,O(1) 时间复杂度获取 */unsigned long len;
} list;
  1. 常数复杂度获取链表长度

  2. 双端链表实现

  3. 多态实现,各种类型的链表可以自己定义各自的复制函数 / 释放函数 / 比较函数

主要用于列表对象的底层实现

字典

typedef struct dictEntry {/* void * 类型的 key,可以指向任意类型的键 */void *key;/* 联合体 v 中包含了指向实际值的指针 *val、无符号的 64 位整数、有符号的 64 位整数,以及 double 双精度浮点数。* 这是一种节省内存的方式,因为当值为整数或者双精度浮点数时,由于它们本身就是 64 位的,void *val 指针也是占用 64 位(64 操作系统下),* 所以它们可以直接存在键值对的结构体中,避免再使用一个指针,从而节省内存开销(8 个字节)* 当然也可以是 void *,存储任何类型的数据,最早 redis1.0 版本就只是 void* */union {void *val;uint64_t u64;int64_t s64;double d;} v;struct dictEntry *next;     /* Next entry in the same hash bucket. *//* 同一个 hash 桶中的下一个条目.* 通过形成一个链表解决桶内的哈希冲突. */void *metadata[];           /* An arbitrary number of bytes (starting at a* pointer-aligned address) of size as returned* by dictType's dictEntryMetadataBytes(). *//* 一块任意长度的数据 (按 void* 的大小对齐),* 具体长度由 'dictType' 中的* dictEntryMetadataBytes() 返回. */
} dictEntry;typedef struct dict dict;/* 字典类型,因为我们会将字典用在各个地方,例如键空间、过期字典等等等,只要是想用字典(哈希表)的场景都可以用* 这样的话每种类型的字典,它对应的 key / value 肯定类型是不一致的,这就需要有一些自定义的方法,例如键值对复制、析构等 */
typedef struct dictType {/* 字典里哈希表的哈希算法,目前使用的是基于 DJB 实现的字符串哈希算法* 比较出名的有 siphash,redis 4.0 中引进了它。3.0 之前使用的是 DJBX33A,3.0 - 4.0 使用的是 MurmurHash2 */uint64_t (*hashFunction)(const void *key);/* 键拷贝 */void *(*keyDup)(dict *d, const void *key);/* 值拷贝 */void *(*valDup)(dict *d, const void *obj);/* 键比较 */int (*keyCompare)(dict *d, const void *key1, const void *key2);/* 键析构 */void (*keyDestructor)(dict *d, void *key);/* 值析构 */void (*valDestructor)(dict *d, void *obj);/* 字典里的哈希表是否允许扩容 */int (*expandAllowed)(size_t moreMem, double usedRatio);/* Allow a dictEntry to carry extra caller-defined metadata.  The* extra memory is initialized to 0 when a dictEntry is allocated. *//* 允许调用者向条目 (dictEntry) 中添加额外的元信息.* 这段额外信息的内存会在条目分配时被零初始化. */size_t (*dictEntryMetadataBytes)(dict *d);
} dictType;/* 通过指数计算哈希表的大小,见下面 exp,哈希表大小目前是严格的 2 的幂 */
#define DICTHT_SIZE(exp) ((exp) == -1 ? 0 : (unsigned long)1<<(exp))
/* 计算掩码,哈希表的长度 - 1,用于计算键在哈希表中的位置(下标索引) */
#define DICTHT_SIZE_MASK(exp) ((exp) == -1 ? 0 : (DICTHT_SIZE(exp))-1)/* 7.0 版本之前的字典结构
typedef struct dictht {dictEntry **table; // 8 bytesunsigned long size; // 8 bytesunsigned long sizemask; // 8 bytesunsigned long used; // 8 bytes
} dictht;typedef struct dict {dictType *type; // 8 bytesvoid *privdata; // 8 bytesdictht ht[2]; // 32 bytes * 2 = 64 byteslong rehashidx; // 8 bytesint16_t pauserehash; // 2 bytes
} dict;** 做的优化大概是这样的:* 1. 从字典结构里删除 privdata (这个扩展其实一直是个 dead code,会影响很多行,社区里的做法都是想尽量减少 diff 变更,避免说破坏 git blame log)* 2. 将 dictht 字典哈希表结构融合进 dict 字典结构里,相关元数据直接放到了 dict 中* 3. 去掉 sizemark 字段,这个值可以通过 size - 1 计算得到,这样就可以少 8 字节* 4. 将 size 字段转变为 size_exp(就是 2 的 n 次方,指数),因为 size 目前是严格都是 2 的幂,size_exp 存储指数而不是具体数值,size 内存占用从 8 字节降到了 1 字节** 内存方面:*   默认情况下通过 sizeof 我们是可以看到新 dict 是 56 个字节*   dict:一个指针 + 两个指针 + 两个 unsigned long + 一个 long + 一个 int16_t + 两个 char,总共实际上是 52 个字节,但是因为 jemalloc 内存分配机制,实际会分配 56 个字节*   而实际上因为对齐,最后的 int16_t pauserehash 和 char ht_size_exp[2] 加起来是占用 8 个字节,代码注释也有说,将小变量放到最后来获得最小的填充。*/struct dict {/* 字典类型,8 bytes */dictType *type;/* 字典中使用了两个哈希表,* (看看那些以 'ht_' 为前缀的成员, 它们都是一个长度为 2 的数组)** 我们可以将它们视为* struct{*   ht_table[2];*   ht_used[2];*   ht_size_exp[2];* } hash_table[2];* 为了优化字典的内存结构,* 减少对齐产生的空洞,* 我们将这些数据分散于整个结构体中.** 平时只使用下标为 0 的哈希表.* 当需要进行 rehash 时 ('rehashidx' != -1),* 下标为 1 的一组数据会作为一组新的哈希表,* 渐进地进行 rehash 避免一次性 rehash 造成长时间的阻塞.* 当 rehash 完成时, 将新的哈希表置入下标为 0 的组别中,* 同时将 'rehashidx' 置为 -1.*/dictEntry **ht_table[2];/* 哈希表存储的键数量,它与哈希表的大小 size 的比值就是 load factor 负载因子,* 值越大说明哈希碰撞的可能性也越大,字典的平均查找效率也越低* 理论上负载因子 <=1 的时候,字典能保持平均 O(1) 的时间复杂度查询* 当负载因子等于哈希表大小的时候,说明哈希表退化成链表了,此时查询的时间复杂度退化为 O(N)* redis 会监控字典的负载因子,在负载因子变大的时候,会对哈希表进行扩容,后面会提到的渐进式 rehash */unsigned long ht_used[2];long rehashidx; /* rehashing not in progress if rehashidx == -1 *//* rehash 的进度.* 如果此变量值为 -1, 则当前未进行 rehash. *//* Keep small vars at end for optimal (minimal) struct padding *//* 将小尺寸的变量置于结构体的尾部, 减少对齐产生的额外空间开销. */int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) *//* 如果此变量值 >0 表示 rehash 暂停* (<0 表示编写的代码出错了). *//* 存储哈希表大小的指数表示,通过这个可以直接计算出哈希表的大小,例如 exp = 10, size = 2 ** 10* 能避免说直接存储 size 的实际值,以前 8 字节存储的数值现在变成 1 字节进行存储 */signed char ht_size_exp[2]; /* exponent of size. (size = 1<<exp) *//* 哈希表大小的指数表示.* (以 2 为底, 大小 = 1 << 指数) */
};

结构体有点长,简单展示就是如下

  1. dictEntry 存的是一个链表,因为redis解决hash冲突的方式是使用链地址法,其他解决方法可参考  解决哈希冲突的常用方法分析 - 腾讯云

  2. 这里注意一下ht这个字段,正常来说一个ht[1]是不会使用的,只有在rehash过程中才会有值

rehash

字典的负载因子(load factor)超过一定阈值时就会启动rehash, 在过程中对于字典的增删改查都会先查一遍ht[0],然后把值迁移到ht[1],等到ht[0]迁移完毕就会释放ht[0],将ht[1]设置成ht[0]

主要用于哈希对象和数据库的底层实现,对,没错,整个数据库也是一个大哈希实现

跳表

/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {sds ele;double score;struct zskiplistNode *backward;struct zskiplistLevel {struct zskiplistNode *forward;unsigned long span;} level[];
} zskiplistNode;typedef struct zskiplist {struct zskiplistNode *header, *tail;unsigned long length;int level;
} zskiplist;
  1. zskiplist 使用双端链表实现

  2. 在遍历操作时只需要用到 forward 前行指针,查找过程

    1. 如果下一个节点比目标节点大,则移动到下一个节点

    2. 否则移动到下一层

    3. 重复以上步骤,直到找到目标值

跳表与字典结合作为有序集合的底层实现

整数集合

/* 整数集合 * 记录不包含重复元素的各个整数(由小到大的顺序) * 底层数组默认是 int16_t 类型, 可能随着新增元素的大小升级至 int32_t 或 int64_t 类型*/
typedef struct intset {/* 编码, 记录整数集合底层数组(contents)的类型*/uint32_t encoding;/* 记录整数集合包含的元素个数 */uint32_t length;/* 整数集合的底层实现, 虽声明为 int8_t 类型,但真正的类型取决于 encoding */int8_t contents[];
} intset;
  1. 支持类型 int16_t, int32_t, int64_t

  2. 在插入一个不同当前编码的值就会触发升级,遍历所有value转换类型,且不支持降级

原本 1, 2, 3

0-15位

16-31位

32-48位

48-127位

元素

1

2

3

新分配空间

插入 65535

0-31位

32-63位

64-95位

96-127位

元素

1

2

3

65535

用于当value都是整数,并且个数不超过512的集合底层实现

压缩列表

ziplist 压缩列表是由一系列字节数组表示的,每个字节数组可以表示一个节点的信息,包括节点的类型、长度和值等信息。

压缩列表的布局如下:

<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>

属性

类型

长度

用途

zlbytes

uint32_t

4字节

记录整个压缩列表占用的内存字节数:在对压缩列表进行内存重分配
或者计算zlend 的位置时使用

zltail

uint32_t

4字节

或者计算zlend 的位置时使用 记录压缩列表表尾节点西商压缩列表的起始地址有多少字节:通过这个
偏移量,程序无须遮历整个压缩列表就可以确定表尾节点的地址

zllen

uint16_t

2字节

记录了压缩列表包含的节点数量:当这个属性的值小于(65535)时,这个属性的值就是压缩列表包含节点的数量:当这个值等于 UINT16_MAx 时,节点的真实数量需要追历整个压缩列表才能计算得出

entry

列表节点

不定

压缩列表包含的各个节点,节点的长度由节点保存的内容决定

zlend

uint8_t

1字节

特殊值O x FF(十进制255),用于标记压缩列表的末端。

entry的布局如下

属性

用途

previous_entry_iength

前置节点长度记录了前一个节点的字节数,它的长度可以是 1 个字节或 5 个字节,具体占用的字节数取决于前一个节点的字节数,用于支持列表的反向遍历

encoding

节点类型记录了该节点存储的数据类型,它的长度为 1 个字节,

  • 字符串节点的类型为 0xc0,二进制表示为 11000000;

  • 整数节点的类型为 0x00,二进制表示为 00000000。

content

节点值记录了该节点存储的数据,它的长度为节点长度所记录的字节数。如果该节点是字符串节点,则节点值存储的是字符串的值;如果该节点是整数节点,则节点值存储的是整数的值。

连锁更新

由于previous_entry_length的长度是1或5,取决于前一个节点的长度,如果有个列表,每个节点的长度都是250-253之间,那么当第一个节点增加了长度,后续每一个节点需要增加长度,作者称这种现象为连锁更新,需要o(n)复杂度去更新每一个节点

为了解决这个问题, 后续在Redis7.0全面使用listpack代替了ziplist, 详细参与: Redis7.0代码分析:底层数据结构listpack实现原理 - 掘金

用于列表和哈希的底层实现

后续….

Redis3.2之后引入quicklist

引用

redis7源码中文注释

Redis设计与实现

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

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

相关文章

【软考-中级】系统集成项目管理工程师-人力资源管理历年案例

持续更新。。。。。。。。。。。。。。。 目录 2019 下 试题三(20分)背诵整理1. 冲突管理的6种方法2. 获取项目人力资源的依据 系列文章 2019 下 试题三(20分) 阅读下列说明&#xff0c;回答问题 1至问题 3&#xff0c;将解答填入答题纸的对应栏内     某公司承接了一个软件…

力扣第37题 解数独 c++ 难~ 回溯

题目 37. 解数独 困难 相关标签 数组 哈希表 回溯 矩阵 编写一个程序&#xff0c;通过填充空格来解决数独问题。 数独的解法需 遵循如下规则&#xff1a; 数字 1-9 在每一行只能出现一次。数字 1-9 在每一列只能出现一次。数字 1-9 在每一个以粗实线分隔的 3x3 宫…

小谈设计模式(29)—访问者模式

小谈设计模式&#xff08;29&#xff09;—访问者模式 专栏介绍专栏地址专栏介绍 访问者模式角色分析访问者被访问者 优缺点分析优点将数据结构与算法分离增加新的操作很容易增加新的数据结构很困难4 缺点增加新的数据结构比较困难增加新的操作会导致访问者类的数量增加34 总结…

解决Github Markdown图片显示残缺的问题

title: 解决Github Markdown图片显示残缺的问题 tags: 个人成长 categories:杂谈 在Github存放Markdown文档&#xff0c;如果图片没有存放在Github服务器上&#xff0c;github会尝试生成Github图片缓存&#xff0c;使用Github图片缓存&#xff0c;进行实际的展示。但比较蛋疼的…

2023年中国火焰切割机分类、产业链及市场规模分析[图]

火焰切割机是一种工业设备&#xff0c;用于利用高温火焰对金属材料进行切割和切割加工的过程。这种技术通常在金属切割、切割、焊接和熔化等领域中使用&#xff0c;通过将氧气和燃料混合产生的火焰来加热金属至高温&#xff0c;然后通过氧化反应将金属氧化物吹散&#xff0c;从…

嵌入式mqtt总线架构方案mosquitto+paho

一 mqtt通信模型 MQTT 协议提供一对多的消息发布&#xff0c;可以降低应用程序的耦合性&#xff0c;用户只需要编写极少量的应用代码就能完成一对多的消息发布与订阅&#xff0c;该协议是基于<客户端-服务器>模型&#xff0c;在协议中主要有三种身份&#xff1a;发布者&…

推荐一种更高效的打字输入法——双拼输入法

简介 双拼&#xff08;也称双打&#xff09;是一种建立在拼音输入法基础之上的文字输入方法&#xff0c;可视为全拼的一种改进。它通过将每个汉字拼音的声母和韵母各自映射到某个按键上&#xff0c;使得每个汉字最多用两个按键表示&#xff0c;从而极大地提高了拼音输入法的输…

LLM ReAct: 将推理和行为相结合的通用范式 学习记录

LLM ReAct 什么是ReAct? LLM ReAct 是一种将推理和行为相结合的通用范式,可以让大型语言模型(LLM)根据逻辑推理(Reason),构建完整系列行动(Act),从而达成期望目标。LLM ReAct 可以应用于多种语言和决策任务,例如问答、事实验证、交互式决策等,提高了 LLM 的效率、…

2022年亚太杯APMCM数学建模大赛B题高速列车的优化设计求解全过程文档及程序

2022年亚太杯APMCM数学建模大赛 B题 高速列车的优化设计 原题再现&#xff1a; 2022年4月12日&#xff0c;中国高铁复兴号CR450动车组在开放线上成功实现单车时速435公里&#xff0c;相对速度870公里&#xff0c;创造了高铁动车组列车穿越开放线和隧道速度的世界纪录。新一代…

用python写一个贪吃蛇的程序能运行能用键盘控制

用python写一个贪吃蛇的程序能运行能用键盘控制 1.源码2.运行效果 1.源码 开发库使用&#xff1a;pygame random 直接在终端运行&#xff1a;pip install pygame pycharm安装库&#xff1a;文件-设置-项目-Python 解释器 import pygame import random# 初始化pygame pygame…

2023年中国轮胎模具需求量、竞争格局及行业市场规模分析[图]

轮胎模具是轮胎生产线中的硫化成形装备&#xff0c;是高技术含量、高精度及高附加值的个性化模具产品&#xff0c;尤其是轮胎的花纹、图案、字体以及其他外观特征的成形都依赖于轮胎模具&#xff0c;因此其制造技术难度较高。其主要功能是通过所成型材料&#xff08;主要是橡塑…

最优化:建模、算法与理论(最优性理论2

5.7 约束优化最优性理论应用实例 5.7.1 仿射空间的投影问题 考虑优化问题 min ⁡ x ∈ R n 1 2 ∣ ∣ x − y ∣ ∣ 2 2 , s . t . A x b \min_{x{\in}R^n}\frac{1}{2}||x-y||_2^2,\\ s.t.{\quad}Axb x∈Rnmin​21​∣∣x−y∣∣22​,s.t.Axb 其中 A ∈ R m n , b ∈ R m …

2024免费的苹果电脑杀毒软件cleanmymac X

苹果电脑怎么杀毒&#xff1f;这个问题自从苹果电脑变得越来越普及&#xff0c;苹果电脑的安全性问题也逐渐成为我们关注的焦点。虽然苹果电脑的安全性相对较高&#xff0c;但仍然存在着一些潜在的威胁&#xff0c;比如流氓软件窥探隐私和恶意软件等。那么&#xff0c;苹果电脑…

【LeetCode:2316. 统计无向图中无法互相到达点对数 | BFS + 乘法原理】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

uniapp 小程序优惠劵样式

先看效果图 上代码 <view class"coupon"><view class"tickets" v-for"(item,index) in 10" :key"item"><view class"l-tickets"><view class"name">10元优惠劵</view><view cl…

基于Java的图书商城管理系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09; 代码参考数据库参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作者&am…

Linux之I2C应用编程

I2C-Tools的交叉编译 tar xvf i2c-tools-4.2.tar.xz 首先解压下压缩包 cd i2c-tools-4.2 进入 i2c-tools-4.2目录 make USE_STATIC_LIB1 执行 make 将i2cset ,i2cget ,i2cdump,i2cdetect,i2ctransfer放到板子上 命令直接操作IIC设备 命令行直接操作iic向AP3216C传感器获取数据…

初出茅庐的小李博客之Windows11运行Linux记录

安装教程 超简单&#xff0c;不安装虚拟机&#xff0c;Windows11运行Linuxhttps://zhuanlan.zhihu.com/p/393484912 注意事项 出现错误有可能是少了驱动 驱动下载地址 https://link.zhihu.com/?targethttps%3A//wslstorestorage.blob.core.windows.net/wslblob/wsl_updat…

Django和jQuery,实现Ajax表格数据分页展示

1.需求描述 当存在重新请求接口才能返回数据的功能时&#xff0c;若页面的内容很长&#xff0c;每次点击一个功能&#xff0c;页面又回到了顶部&#xff0c;对于用户的体验感不太友好&#xff0c;我们希望当用户点击这类的功能时&#xff0c;能直接加载到数据&#xff0c;请求…

安防视频监控系统EasyCVR视频汇聚存储平台定制化开发:新增kafka配置

安防视频监控/视频集中存储/云存储/磁盘阵列EasyCVR平台可拓展性强、视频能力灵活、部署轻快&#xff0c;可支持的主流标准协议有国标GB28181、RTSP/Onvif、RTMP等&#xff0c;以及支持厂家私有协议与SDK接入&#xff0c;包括海康Ehome、海大宇等设备的SDK等。平台可拓展性强、…