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

news2024/11/19 4:25:19

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 bytes
    unsigned long size; // 8 bytes
    unsigned long sizemask; // 8 bytes
    unsigned long used; // 8 bytes
} dictht;

typedef struct dict {
    dictType *type; // 8 bytes
    void *privdata; // 8 bytes
    dictht ht[2]; // 32 bytes * 2 = 64 bytes
    long rehashidx; // 8 bytes
    int16_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.coloradmin.cn/o/1112621.html

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

相关文章

智慧矿山:煤流量检测AI算法——等级还是百分比显示,哪种更适合现场

智慧矿山作为矿山工业的重要发展方向&#xff0c;AI算法的应用正逐渐改变传统的矿山生产方式。在智慧矿山中&#xff0c;煤流量检测是其中一项重要的任务。然而&#xff0c;在煤流量检测中&#xff0c;是采用等级显示还是百分比显示更适合现场呢&#xff1f; 煤流量检测作为煤矿…

【试题022】C语言算数表达式例题

1.题目&#xff1a;设int a12&#xff0c;b10;&#xff0c;则表达式a%b/a*b的值为&#xff1f; 2.代码分析&#xff1a; #include <stdio.h> int main() {//设int a12&#xff0c;b10;&#xff0c;则表达式a%b/a*b的值为&#xff1f;int a 12,b 10;printf("%d\n…

Python 面向对象进阶

目录 1 面向对象三大特征介绍2 继承2.1 语法格式2.2 类成员的继承和重写2.3 查看类的继承层次结构 3 object根类3.1 dir()查看对象属性3.2 重写__str__()方法 4 多重继承5 MRO()6 super()获得父类定义7 多态8 特殊方法和运算符重载9 特殊属性10 对象的浅拷贝和深拷贝11 组合12 …

CSP赛前复习总结

文章目录 时空复杂度分析算法回顾基础算法贪心删数问题题目描述输入格式输出格式样例输入样例输出 贪心问题的具体应用双指针二分例题 分治 数据结构做题经历&#xff08;低级错误&#xff09;先从周考题说起FirstSecond 还有一两天就CSP了&#xff0c;特此为近期的复习进行总结…

运机集团-001288 基本面分析(20231019)

运机集团-001288 基本面分析 基本情况 公司名称&#xff1a;四川省自贡运输机械集团股份有限公司 A股简称&#xff1a;运机集团 成立日期&#xff1a;2003-09-28 上市日期&#xff1a;2021-11-01 所属行业&#xff1a;通用设备制造业 主营业务&#xff1a;以带式输送机为主的节…

计算机操作系统-第十一天

目录 1、进程的状态 创建态与就绪态 运行态 终止态 新建态 结束态 进程状态的转换 进程的组织方式 链接方式&#xff08;常见&#xff09; 索引方式&#xff08;少见&#xff09; 本节思维导图 1、进程的状态 创建态与就绪态 1、进程正在被创建时&#xff0c;处于…

03、爬取资料---但是失败,仅作为记录

1、找网址 进入直播间&#xff0c;里面的用户被设置不对外查看。 如图&#xff0c;找url 2、伪装 user-agent 用户代理 cookie 用户登录后保留的信息 登录信息&#xff1a;找cookie 浏览器信息&#xff1a;找user-agent user-agent 用户代理 cookie 用户登录后保留的信…

kubernetes集群编排

目录 k8s 集群部署 集群环境初始化 所有节点安装kubeadm 拉取集群所需镜像 集群初始化 安装flannel网络插件 设置kubectl命令补齐 k8s 集群部署 实验环境 主机名 ip 角色 k8s1&#xff08;上一章的docker1&#xff09; 192.168.81.10 reg.westos.org&#xff0c;harbor仓库…

机器学习中参数优化调试方法

1 超参数优化 调参即超参数优化&#xff0c;是指从超参数空间中选择一组合适的超参数&#xff0c;以权衡好模型的偏差(bias)和方差(variance)&#xff0c;从而提高模型效果及性能。常用的调参方法有&#xff1a; 人工手动调参 网格/随机搜索(Grid / Random Search) 贝叶斯优…

Kingbase备份与还原及表的约束(Kylin)

备份与还原 逻辑备份是对整个数据库好数据库中的部分对象利用逻辑备份工具导出数据到备份文件在需要数据恢复的情况下利用逻辑还原工具把备份文件恢复到数据库中 使用场景 逻辑备份主要用于数据库逻辑错误的恢复&#xff0c;恢复后对其他数据没有太大影响逻辑备份可用于在大…

Java:ApacheHttpClient连接寿命(timeToLive)未配置问题分析

一、问题描述 若 Apache HttpClient 未设置 timeToLive&#xff0c;通过服务域名访问服务的实例并且服务域名解析出的 IP 发生变化时&#xff0c;在短时间内会有部分请求出现连接异常错误。 二、问题分析 Apache HttpClient 通过服务域名从连接池获取连接&#xff0c;当连接…

IDEA中SpringBoot项目的yml多环境配置

SpringBoot的yml多环境配置 创建多个配置文件 application.yml #主配置文件 application-dev.yml #开发环境的配置 application-test.yml #测试环境的配置在application.yml中添加多环境配置属性 spring:profiles:active: profiles.active项目启动可能不会识别&#x…

简单了解一下:Node的util工具模块

了解util模块&#xff0c;知道怎么使用util来格式化字符串&#xff0c;把对象转化为字符串&#xff0c;检查对象类型。 那么util模块有哪些方法呢&#xff1f;如下图所示&#xff1a; 常用的几个方法&#xff1a; 格式化输出字符串 util提供的格式化方法为&#xff1a;form…

0基础学习PyFlink——模拟Hadoop流程

学习大数据还是绕不开始祖级别的技术hadoop。我们不用了解其太多&#xff0c;只要理解其大体流程&#xff0c;然后用python代码模拟主要流程来熟悉其思想。 还是以单词统计为例&#xff0c;如果使用hadoop流程实现&#xff0c;则如下图。 为什么要搞这么复杂呢&#xff1f; 顾…

快速入门python机器学习

文章目录 机器学习概述1.1 人工智能概述机器学习与人工智能、深度学习1.1.2 机器学习、深度学习能做些什么 1.2 什么是机器学习1.2.1 定义1.2.2 解释1.2.3 数据集构成 1.3 机器学习算法分类1.3.1 总结1.3.2 练习1.3.3 机器学习算法分类 1.4 机器学习开发流程&#xff08;了解&a…

小程序之自定义组件 结合案例

&#x1f3ac; 艳艳耶✌️&#xff1a;个人主页 &#x1f525; 个人专栏 &#xff1a;《Spring与Mybatis集成整合》《Vue.js使用》 ⛺️ 越努力 &#xff0c;越幸运。 1.自定义组件 开发者可以将页面内的功能模块抽象成自定义组件&#xff0c;以便在不同的页面中重复使用&#…

【基础知识补充】三个重要理化参数和计算预测、药代动力学(PK)、毒性预测 / ADME

一、药物理化性质预测 药物的理化性质包括分子的脂溶性、水溶性和解离常数&#xff08;Dissociation Constant&#xff09;pKa&#xff0c;这些理化参数与药代动力学性质、生物活性强度以及作用靶标的选择性密切相关&#xff0c;下面分别介绍。 1、脂溶性&#xff08;logP&am…

数据分析入门

B站&#xff1a;01第一课 数据分析岗位职责和数据分析师_哔哩哔哩_bilibili 一、岗位&#xff1a;数据分析师 Q1 数据分析师在公司做什么工作&#xff1f; 数据来源于公司核心业务&#xff0c;通过监测业务健康度来确定业务的健康状况&#xff1b; 通过对用户精细化分析&am…

一站式智慧校园解决方案 SaaS云平台智慧校园管理系统源码

SaaS云平台 智慧校园管理平台 教师端、家长端、学生端 智慧校园以互联网为基础&#xff0c;以“大数据云服务”为核心&#xff0c;融合校园教学、管理、生活软硬件平台&#xff0c;定义智慧校园新生活。智慧校园管理平台管理者、教师、学生、家长提供一站式智慧校园解决方案&a…

Zynq中断与AMP~双核串口环回之PS与PL通信

实现思路&#xff1a; 额外配置&#xff1a;通过PL配置计数器&#xff0c;向CPU0和CPU1发送硬中断。 1.串口中断CPU0&#xff0c;在中断中设置接收设置好字长的数据&#xff0c;如果这些数据的数值符合约定的命令&#xff0c;则关闭硬中断&#xff0c;并将这部分数据存入AxiLi…