redis核心数据结构源码分析

news2024/11/13 15:17:21

dictEntry和redisObject

在 Redis 的实现中,当一个键值对被创建并存储时,键通常是一个字符串,而值则是一个 redisObject。因此,在 dictEntry 结构中,key 成员指向的是一个字符串,而 v.val 成员则指向一个 redisObject。这意味着,当你在 Redis 中存储一个值时,你实际上是在字典中插入一个 dictEntry,其中 dictEntry 的值部分指向一个包含实际数据和元数据的 redisObject

dictEntry

struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;     /* Next entry in the same hash bucket. */
};
/*位置:dict.c*/

一个dictEntry就是哈希表中的一个结点。

1.void *val:这是指向键的指针。在 Redis 中,键通常是字符串,但这里使用void * 是为了增加灵活性,允许键的类型在底层实现中变化。

2.union { ... } v;

这是一个联合体(union),就是哈希表结点中的value。

void *val;:一个通用指针,通常用于指向更复杂的数据结构,如链表、压缩列表或字符串。

uint64_t u64;:一个无符号 64 位整数,用于存储较小的数值类型。

int64_t s64;:一个带符号 64 位整数,用于存储较小的数值类型。

double d;:一个双精度浮点数,用于存储实数。

在任何给定时刻,联合体中只有一个成员会被使用,具体取决于值的实际类型。这种设计节省了内存,因为不需要为每种类型分配独立的空间。

3.struct dictEntry *next;  

链地址法解决哈希冲突

redisObject

struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;
    void *ptr;
};
/*位置:server.h*/

1.unsigned type:4;

4位的type表示具体的数据类型。包括OBJ_STRING,OBJ_LIST,OBJ_HASH,OBJ_SET,OBJ_ZSET

可通过type命令查看

2.unsigned encoding:4;

4位的encoding表示该类型的物理编码方式(如下),同一种数据类型可能有不同的编码方式。(比如String就提供了3种:int embstr raw)

#define OBJ_ENCODING_RAW 0     /* Raw representation */
#define OBJ_ENCODING_INT 1     /* Encoded as integer */
#define OBJ_ENCODING_HT 2      /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3  /* No longer used: old hash encoding. */
#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
#define OBJ_ENCODING_ZIPLIST 5 /* No longer used: old list/hash/zset encoding. */
#define OBJ_ENCODING_INTSET 6  /* Encoded as intset */
#define OBJ_ENCODING_SKIPLIST 7  /* Encoded as skiplist */
#define OBJ_ENCODING_EMBSTR 8  /* Embedded sds string encoding */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of listpacks */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */
#define OBJ_ENCODING_LISTPACK 11 /* Encoded as a listpack */
#define OBJ_ENCODING_LISTPACK_EX 12 /* Encoded as listpack, extended with metadata */
/*位置:server.h*/

可通过object encoding命令查看 

3.unsigned lru:LRU_BITS;

对象最后一次被访问的时间戳,与内存回收有关。

4.int refcount

refcount表示对象的引用计数(可类比JVM中的引用计数法)

5.void *ptr

ptr指针指向实际对象

五大基本数据类型和底层数据类型对应关系

redis6

redis7

五大基本数据类型源码分析

 String

三大物理编码方式

1.int

保存long型的64位(8个字节)有符号整数

只有整数才会使用int,如果是浮点数,Redis内部其实先将浮点数转化为字符串值,然后再保存。

2.embstr

embedded string,表示嵌入式的String。代表embstr格式的SDS(simple dynamic string,简单动态字符串),保存长度小于44字节的字符串。

嵌入式怎么理解?

robj *createEmbeddedStringObject(const char *ptr, size_t len) {
    robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr8)+len+1);
    struct sdshdr8 *sh = (void*)(o+1);

    o->type = OBJ_STRING;
    o->encoding = OBJ_ENCODING_EMBSTR;
    o->ptr = sh+1;//sh 是 sdshdr8 结构体的指针,它紧跟在 redisObject 结构体之后在内存中分配。
    o->refcount = 1;
    o->lru = 0;

    sh->len = len;
    sh->alloc = len;
    sh->flags = SDS_TYPE_8;
    if (ptr == SDS_NOINIT)
        sh->buf[len] = '\0';
    else if (ptr) {
        memcpy(sh->buf,ptr,len);
        sh->buf[len] = '\0';
    } else {
        memset(sh->buf,0,len+1);
    }
    return o;
}
/*位置:object.c*/

字符串 sds结构体与其对应的 redisObject 对象分配在同一块连续的内存空间,字符串sds嵌入在redisObject对象之中一样。 

3.raw

保存长度大于44字节的字符串

源码证明:

#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
robj *createStringObject(const char *ptr, size_t len) {
    if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) //长度小于44
        return createEmbeddedStringObject(ptr,len);//emb编码
    else
        return createRawStringObject(ptr,len);//raw编码
}
/*位置:object.c*/

SDS 

typedef char *sds;

/* Note: sdshdr5 is never used, we just access the flags byte directly.
 * However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
/*位置:sds.h*/
  •  len:字符串的实际长度(不包括末尾的空字符),对sdshdr8的uint8_t 来说,可存储0-255的长度。
  • alloc:为字符串分配的总空间大小(同样不包括结构体头部和空终止符)。
  • flags:标志位。低 3 位被用于表示 SDS 的类型,而剩下的 5 位目前是未使用的。
  • buf:实际存储字符串的字符数组,SDS本质上也是一个字符数组。

为什么要重新设计一个简单动态字符串(SDS),而不直接用c语言的char[]?

  • 字符串长度处理:c的cha[]要遍历得到长度,时间复杂度为O(n),SDS与len,直接读取即可,时间复杂度为O(1)。
  • 内存分配:预先分配更多的空间,减少频繁的内存重新分配;且SDS缩短时并不会回收多余的空间,而是用free将多余的空间记录下来,如果后序需要再分配,直接使用free标记的字段,而不用重新申请。
  • 因为c的char[]将\0作为字符串结束的标志,如果字符串内容中含有\0,会错把该字符当做结束标志。而SDS根据len判断字符串结束,不会有该问题。

Hash

redis6

redis中Hash结构由zipList和Hashtable构成。当hashkey满足1.哈希对象保存的键值对数量小于512个; 2.所有的键值对的健和值的字符串长度都小于等于64byte(一个英文字母一个字节)时用ziplist,反之用hashtable。ziplist升级到hashtable可以,反过来降级不可以。

ziplist:

ziplist 是一个经过特殊编码的双向链表,它的设计目标是节约内存。它可以存储字符串或者整数。其中整数是按二进制进行编码的,而不是字符串序列。它能以 O(1) 的时间复杂度在列表的两端进行 push 和 pop 操作。但是由于每个操作都需要对 ziplist 所使用的内存进行重新分配,所以实际操作的复杂度与 ziplist 占用内存大小有关。(官方注释)

因为 ziplist 的设计目标是为了 节约内存,而链表的各项之间需要使用指针连接起来,这种方式会带来大量的内存碎片,而且地址指针也会占用额外的内存,(压缩的地方)这与 ziplist 的设计初衷不符。而且后面我们看了 ziplist 的数据结构就会发现,ziplist 实际上是一块连续的内存。

因此我们可以这么理解:ziplist 是一个特殊的双向链表,特殊之处在于:没有维护双向指针,prev、next,而是存储了上一个 entry 的长度和当前 entry 的长度,通过长度推算下一个元素。

也就是:

  • 压缩列表本质上就是一个字节数组
  • 是 Redis 为了节约内存而设计的一种线性结构
  • 可以包含多个元素,每个元素可以是一个字节数组或一个整数

zipList结构:

 

每个entry结构:

 previous_entry_length 字段表示前一个元素的字节长度

encoding 字段表示当前元素的编码,记录了节点的 content 字段所保存数据的类型以及长度

content 字段存储节点的值,节点值可以是一个字节数组或者整数,值的类型和长度由节点的 encoding 属性决定。

ziplist的连锁更新问题:

previous_entry_length 属性都记录了前一个节点的长度:

  • 如果前一节点的长度小于 254 字节,那么 previous_entry_length 属性需要用 1 字节长的空间来保存这个长度值。
  • 如果前一节点的长度大于等于 254 字节,那么 previous_entry_length 属性需要用 5 字节长的空间来保存这个长度值。

现在假设一个压缩列表中有多个连续的、长度在 250~253 之间的节点,如下图:

因为这些节点长度值小于 254 字节,所以 prevlen 属性需要用 1 字节的空间来保存这个长度值,一切OK,O(∩_∩)O哈哈~

这时,如果将一个长度大于等于 254 字节的新节点加入到压缩列表的表头节点,即新节点将成为entry1的前置节点,如下图:

因为entry1节点的prevlen属性只有1个字节大小,无法保存新节点的长度,此时就需要对压缩列表的空间重分配操作并将entry1节点的prevlen 属性从原来的 1 字节大小扩展为 5 字节大小。

连续更新问题出现

entry1节点原本的长度在250~253之间,因为刚才的扩展空间,此时entry1节点的长度就大于等于254,因此原本entry2节点保存entry1节点的 prevlen属性也必须从1字节扩展至5字节大小。entry1节点影响entry2节点,entry2节点影响entry3节点......一直持续到结尾。这种在特殊情况下产生的连续多次空间扩展操作就叫做「连锁更新」 

为了解决连锁更新,redis7中出现了listpack(紧凑列表)

redis7

listpack(1.哈希对家保存的键值对数量小于512个;2.所有的键值对的健和值的字符串长度都小于等于64byte(一个英文字母一个字节)时用listpack,反之用nashtable listpack升级到hashtable可以,反过来降级不可以)

和ziplist列表项类似,listpack列表项也包含了元数据信息和数据本身。不过,为了避免ziplist引起的连锁更新问题,listpack中的每个列表项不再像ziplist列表项那样保存其前一个列表项的长度。

List

redis6

list用quicklist来存储,quicklist存储了一个双向链表,每个节点都是一个ziplist。

quicklist源码:

quicklistNode:

  

redis7

 Iist用quicklist来存储,quicklist存储了一个双向链表,每个节点都是一个listpack

Set

intset或hashtable

集合元素都是long类型并且元素个数<=set-max-intset-entries,编码就是intset,反之就是hashtable

zset

redis6是ziplist+skiplist。redis7是listpack+skiplist

当ZSet的元素数量比较少时,Redis会采用ZipList(ListPack)来存储ZSet的数据。ZipList(ListPack)是一种紧凑的列表结构,它通过连续存储元素来节约内存空间。当ZSet的元素数量增多时,Redis会自动将ZipList(ListPack)转换为SkipList,以保持元素的有序性和支持范围查询操作。

skiplist

时间复杂度:O(n)。空间复杂度:O(n) 

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

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

相关文章

《深入理解JAVA虚拟机(第2版)》- 第2章 - 学习笔记

第二章 Java内存区域与内存溢出异常 2.1 概述 JVM是自动内存管理 2.2 运行时数据区 所谓运行时数据区是JVM在运行Java程序的时候将所管理的内存划分为几块不同的数据区域&#xff0c;分为&#xff1a;程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区&#xff0c;如下…

樊振东代言LV旗下品牌,媒介易解读冠军代言背后的商业逻辑

​近日&#xff0c;法国奢侈品巨头LVMH旗下的德国知名拉杆箱品牌RIMOWA宣布&#xff0c;中国乒乓球奥运冠军樊振东成为其品牌挚友。这一合作不仅是RIMOWA对樊振东在巴黎奥运会上出色表现的认可&#xff0c;也标志着品牌与体育冠军联手打造品牌形象的趋势日益显著。 樊振东&…

Moco求解最优化问题使用教程

理论部分 最简单的例子&#xff0c;流程 输出结果分析 理论部分 moco最终是调用CasAdi求解器来进行求解 对不常见的几个符号表达式含义进行解释&#xff1a; 多刚体动力学公式代表系统中&#xff0c;f_inertial (惯性力和科里奥利力)&#xff1b;f_app (外力和接触力)&…

SQL注入-ctfshow

首先还是对sql的具体分析和讲解 原理&#xff1a; SQL注入是一种安全漏洞&#xff0c;它允许攻击者通过在应用程序的输入中插入或者操作SQL命令来改变后端数据库的查询和操作。SQL注入的主要原因是代码中直接将用户输入与SQL命令拼接在一起&#xff0c;没有进行适当的验证或清…

网络安全可以从事哪些岗位岗位职责是什么网络安全专业的就业前景

网络安全可以从事哪些岗位 伴随着社会的发展&#xff0c;网络安全被列为国家安全战略的一部分&#xff0c;因此越来越多的行业开始迫切需要网安人员&#xff0c;也有不少人转行学习网络安全。那么网络安全可以从事哪些岗位?岗位职责是什么?相信很多人都不太了解&#xff0c;…

『功能项目』着色器光透魔法球Shaders【09】

我们打开上一篇08技能释放的项目&#xff0c; 本章要做的事情是为魔法球增添一个光透效果shaders。 首先在Assets中创建一个Shaders文件夹 接着将场景中的灯光调暗一些&#xff08;避免灯光太强压过将要设置半透明光透效果的魔法球&#xff09; 将新Resources中的Shpere拖拽至…

MQTT服务器-mosquitto配置

我们要使用ESP8266使得STM32能够和服务器之间传递数据&#xff0c;需要有一台MQTT服务器。当然读者可以使用腾讯云、阿里云、OneNet等平台提供的MQTT服务更方便一些。 逻辑是这样的&#xff1a;我们首先需要一台服务器A作为中转站&#xff0c;然后我们的STM32作为客户端能够发…

Java接口中的长连接与短连接详解:概念、应用场景及实现

个人名片 &#x1f393;作者简介&#xff1a;java领域优质创作者 &#x1f310;个人主页&#xff1a;码农阿豪 &#x1f4de;工作室&#xff1a;新空间代码工作室&#xff08;提供各种软件服务&#xff09; &#x1f48c;个人邮箱&#xff1a;[2435024119qq.com] &#x1f4f1…

2024年十款好用的图纸加密软件推荐|有效的图纸加密方法分享

在数字化时代&#xff0c;图纸作为企业的重要资产&#xff0c;其安全性显得尤为重要。随着技术的不断进步&#xff0c;市场上涌现出多款优秀的图纸加密软件&#xff0c;为企业和个人提供了强有力的安全保障。本文将为您推荐2024年十款好用的图纸加密软件。 1.安秉图纸加密软件…

JavaSE 面试题 46-50

使用runnable需重写run方法&#xff0c;而且返回值为viod型&#xff0c;等于没有返回值&#xff1b; Thread 类在调用 start()函数后就是执行的 是 Runnable 的 run()函数。 callable需重写call方法&#xff0c;call方法可以有返回值&#xff0c;支持泛型而且可以捕获解决异常&…

秋招突击——知识复习——HTTP/2、HTTP/3的改良

文章目录 引言正文HTTP/1.1与HTTP/1.01、长连接代替短链接2、管道传输缺点 HTTP2.0和HTTP1.11、头部压缩2、二进制格式3、并发传输4、服务器主动推送资源缺点 HTTP/3和HTTP/21、无队头阻塞2、更快的连接建立3、连接迁移 面试题1、HTTP是长连接还是短链接&#xff1f;2、HTTP长连…

中年男明星们,正在视频号“收割”50+姐姐

还记得几年前&#xff0c;抖音上许多大妈和“靳东谈恋爱”的事情吗&#xff1f; 尽管那些靳东高仿号&#xff0c;发的是花花绿绿、粗制滥造的视频&#xff0c;明眼人一看就知真假&#xff0c;但仍有众多大妈痴迷。 如今&#xff0c;在视频号上也有一群姐姐们&#xff0c;“迷恋…

模块化沙箱有几种类型?各类模块化沙箱的功能是什么?

模块化沙箱有几种类型&#xff1f;各类模块化沙箱的功能是什么&#xff1f; 模块化沙箱是一种高灵活性和高扩展性的数据安全产品&#xff0c;通过选择不同的沙箱模块&#xff0c;满足不同的安全需求。 模块化沙箱是SDC沙箱的几种表现形式的总称&#xff0c;模块化沙箱总共分为…

Kubemetes高级调度

一组特殊的容器 初始化容器是用来进行初始化操作的&#xff0c;在很多情况下&#xff0c;程序的启动需要依赖各类配置&#xff0c;资源&#xff0c;但是又不能继承在原有的启动命令或者镜像当中&#xff0c;因为程序的镜像可能并没有加载配置命令&#xff0c;此时InitContaine…

Dubbo ZooKeeper Spring Boot整合

依赖配置 1. Dubbo 起步依赖 Dubbo 是一款高性能的 Java RPC 框架&#xff0c;用于快速开发高性能的服务。 <dependency><groupId>org.apache.dubbo</groupId><artifactId>dubbo-spring-boot-starter</artifactId><version>${dubbo.ver…

Linux中的常见命令——用户管理命令

1、useradd添加新用户 基本语法 语法功能描述useradd 用户名添加新用户useradd -g 组名 用户名添加新用户到某个组 实操案例 1、添加一个新用户【此时的用户是没有密码的】 [rootcentos100 ~]# cd /home [rootcentos100 home]# ls www zss [rootcentos100 home]# useradd…

如果自学通过PMP?

自学&#xff1f;现在没有自学通道啦&#xff0c;要通过有R.E.P授权的机构学习&#xff0c;获得35个PDU才能报考哦~ 所以要报培训班~ 一是&#xff0c;PMP 官方的报考条件需要35个PDU&#xff0c;就是要报机构学习后获得。个人报考渠道去年就关闭了&#xff0c;只能通过机构报…

算法-初阶

文章目录 -1.C 标准0.语法基础 1. C头文件2. C命名空间3. 主函数4. 变量类型5. ASCII码6. 注释1.顺序结构 一、代码示例二、例题1&#xff1a;求圆的面积三、例题2&#xff1a;求解一元二次方程四、总结&#xff1a;2.分支结构 一、代码示例二、例题1&#xff1a;判断一个数是否…

必看 | CDP盘活存量客户的5大步骤和3个应用案例

​“我们有几十家门店&#xff0c;也有APP、小程序和网站&#xff0c;客户信息散落在各系统中&#xff0c;比较杂乱&#xff0c;一直没有统一去维护&#xff0c;现在想把存量客户运营起来&#xff0c;你们CDP能做吗&#xff1f;”类似的咨询&#xff0c;我们几乎每天都会接到。…

工业三防平板全面提升工厂的工作效率

在当今高度自动化和智能化的工业时代&#xff0c;工厂的工作效率成为了企业竞争力的关键因素。而工业三防平板的出现&#xff0c;犹如一颗璀璨的新星&#xff0c;为工厂带来了全新的变革&#xff0c;全面提升了工厂的工作效率。 工业三防平板具备防水、防尘、防摔的特性无论是潮…