Redis底层数据结构(图文详解)

article/2025/9/13 22:56:10

目录

前言

Redis为什么要使用2个对象?两个对象的好处

redisObject对象解析

String 类型

1、int 整数值实现

2、embstr  

3、raw 

List 类型

1、压缩链表:ziplist

2、双向链表:linkedlist

3、快速列表:quicklist

Hash 类型

Hashtable

哈希表的扩展和收缩

rehash

渐进式hash

Set 类型

intset 整数集合

Zset 类型

zkiplist 跳表


前言

redis是通过对象来表示存储的数据的,redis 也是键值对存储的方式,那么每存储一条数据,redis至少会生成2个对象,一个是redisObject,用来描述具体数据的类型的,比如用的是那种数据类型,底层用了哪种数据结构,还有一个对象就是具体存储的数据。 这个存储对象数据就是通过redisObject这个对象的指针来指引的。

Redis为什么要使用2个对象?两个对象的好处

  1. redis在执行命令的时候,就可以通过redisObject 的类型和编码来确定是否可以执行相应的命令,不用等操作具体的数据是才发现不行
  2. 针对不同的场景,可以为对象设置不同的数据结构,从而优化了对象在不同场景下的使用效率
  3. 可以基于redisObject中的refcount 引用计数进行内存回收机制,自动释放对象所占用的内存
  4. 可以让多个数据库来共享一个对象来节省空间
  5. redis可以根据REDIS_LRU_BITS 记录最后一次访问时间,针对时间较长对象的进行删除

redisObject对象解析

redisObject结构

typedef struct redisObject{//类型unsigned type:4;//编码unsigned encoding:4;//对象最后一次被访问的时间unsigned lru:REDIS_LRU_BITS//引用计数int refcount//指向底层实现数据结构的指针void *ptr;….. 
} 

ptr指针指向的就是每种数据类型的具体的数据结构

type 表示的是使用的那种数据结构,常用的有5种,string,hash,list,set,zset。 type就是来存这个的

编码常量encoding 

编码对应的底层数据结构

REDIS_ENCODING_INT

long类型的整数

REDIS_ENCODING_EMBSTR

embstr编码的简单动态字符串

REDIS_ENCODING_RAW

简单动态字符串

REDIS_ENCODING_HT

字典

REDIS_ENCODING_LINKEDLIST

双向链表

REDIS_ENCODING_ZIPLIST

压缩列表

REDIS_ENCODING_INTSET

整数集合

REDIS_ENCODING_SKIPLIST

跳跃表和字典

encoding 标注了这个对象具体的数据结构类型

每一种redisObject对象对应底层都会有至少2种数据结构

类型

编码

对象

String

int

整数值实现

String

embstr

sds实现 <=39 字节

String

raw

sds实现 > 39字节

List

ziplist

压缩列表实现

List

linkedlist

双端链表实现

Set

intset

整数集合使用

Set

hashtable

字典实现

Hash

ziplist

压缩列表实现

Hash

hashtable

字典使用

Sorted set

ziplist

压缩列表实现

Sorted set

skiplist

跳跃表和字典

介绍完了redisObject了,下面介绍每一种具体的数据类型及其对应的底层数据结构

String 类型

在c语音中,定义的字符串是不可被修改的,因此redis设计了可变的字符串长度对象,接SDS(simple dynamic string),实现原理:

struct sdshdr{//记录buf数组中已存的字节数量,也就是sds保存的字符串长度int len;// buf数组中剩余可用的字节数量int free;//字节数组,用来存储字符串的char buf[];
}

参数解析:

  1. len : 保存的字符串长度。获取字符串的长度就是O(1)
  2. free:剩余可用存储字符串的长度
  3. buf:保存字符串

这样设计的优点:

  1. 当用户修改字符串时sds api会先检查空间是否满足需求,如果满足,直接执行修改操作,如果不满足,将空间修改至满足需求的大小,然后再执行修改操作
  2. 空间预分配
    1. 如果修改后的sds的字符串小于1MB时(也就是len的长度小于1MB),那么程序会分配与len属性相同大小的未使用空间(就是再给未使用空间free也分配与len相同的空间)   例:字符串大小为600k,那么会分配600k给这个字符串使用,再分配600k的free空间在那。
    2. 惰性空间释放,当缩短sds的存储内容时,并不会立即使用内存重分配来回收字符串缩短后的空间,而是通过free将空闲的空间记录起来,等待将来使用。真正需要释放内存的时候,通过调用api来释放内存
    3. 通过空间预分配操作,redis有效的减少了执行字符串增长所需要的内存分配次数
    4. 如果修改后sds大于1MB时(也就是len的长度大于等于1MB),那么程序会分配1MB的未使用空间  例:字符串大小为3MB,那么会分配3MB给这个字符串使用,再分配1MB的free空间在那。

字符串由3种编码实现。

1、int 整数值实现

存储的数据是整数时,redis会将键值设置为int类型来进行存储,对应的编码类型是REDIS_ENCODING_INT

 

2、embstr  

由sds实现 ,字节数 <= 39

存储的数据是字符串时,且字节数小于等于39 ,用的是embstr

优点:1、创建字符串对象由两次变成了一次

           2、连续的内存,更好的利用缓存优势

缺点:1、由于是连续的空间,所以适合只读,如果修改的话,就会变成raw

           2、由于是连续的空间,所以值适合小字符串

3、raw 

由sds实现 字节数 > 39

List 类型

list是由压缩链表和双向链表实现的

typedef struct listNode {// 前置节点struct listNode *prev;// 后置节点struct listNode *next;// 存储的数据void *value;
} listNode;typedef struct list {// 链表头节点listNode *head;// 链表尾节点listNode *tail;// 节点值复制函数void *(*dup)(void *ptr);// 节点值释放函数void (*free)(void *ptr);// 节点值对比函数int (*match)(void *ptr, void *key);// 链表所有节点数量unsigned long len;
} list;

1、压缩链表:ziplist

适用于长度较小的值,因为他是由连续的空间实现的。

存取的效率高,内存占用小,但由于内存是连续的,在修改的时候要重新分配内存

在数据量比较小的时候使用的是ziplist

当list对象同时满足以下两个条件是,使用的ziplist编码

 1.list对象保存的所有字符串元素长度都小于64字节

 2.list对象保存的元素数量小于512个

缺点:

  1. 极端情况下会出现连锁更新现象。因为我们的previous_entry_length 记录了entry 的长度,如果每个长度都是250 - 253之间,那么如果在头部插一个新的entry节点,第一个节点的长度大于254字节,那么后一个节点的previous_entry_length 值就会从1个字节变为 5字节,那么这个entry的长度就大于了254字节,后面一个节点的previous_entry_length 值也得更新,后面的都得更新,导致整行的列表都得更新。
  2. 只能存储小数据的值

2、双向链表:linkedlist

在使用redis的list数据结构时,存储数据较大时,list对象已经不满足上面描述的ziplist条件,则会使用linkedlist

优点:修改效率高

缺点:保存前后指针,会占内存。

3、快速列表:quicklist

结合了压缩列表和双向链表的优点,也解决了他们的缺点 

// 快速列表节点
typedef struct quicklistNode {struct quicklistNode *prev; //上一个node节点struct quicklistNode *next; //下一个nodeunsigned char *zl;            //保存的数据 压缩前ziplist 压缩后压缩的数据unsigned int sz;             /* ziplist size in bytes */unsigned int count : 16;     /* count of items in ziplist */unsigned int encoding : 2;   /* RAW==1 or LZF==2 */unsigned int container : 2;  /* NONE==1 or ZIPLIST==2 */unsigned int recompress : 1; /* was this node previous compressed? */unsigned int attempted_compress : 1; /* node can't compress; too small */unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;// 快速列表
typedef struct quicklist {quicklistNode *head;quicklistNode *tail;unsigned long count;        /* total count of all entries in all ziplists */unsigned long len;          /* number of quicklistNodes */int fill : 16;              /* fill factor for individual nodes ziplist大小设置 */unsigned int compress : 16; /* depth of end nodes not to compress;0=off 节点压缩深度设置*/
} quicklist;// 被压缩的列表
typedef struct quicklistLZF {unsigned int sz; /* LZF size in bytes*/char compressed[];
} quicklistLZF;

Hash 类型

底层是由ziplist 或hashtable 实现的

ziplist在上面介绍过了,下面来聊聊hashtable

Hashtable

 一个哈希表里面可以有多个哈希节点,而每个哈希节点就保存了字典中的一个键值对

// 字典 数据结构
typedef struct dict {//管理两个dictht,主要用于动态扩容。//类型特定函数dictType *type;//私有数据void *privdata;// 哈希表dictht ht[2];// rehash索引,当rehash不再进行是,值为 -1long rehashidx; /* 扩容标志rehashing not in progress if rehashidx == -1 */unsigned long iterators; /* number of iterators currently running */
} dict;/* This is our hash table structure. Every dictionary has two of this as we* implement incremental rehashing, for the old to the new table. */
//定义一个hash桶,用来管理hashtable
typedef struct dictht {// hash表数组,所谓的桶dictEntry **table;// hash表大小,元素个数unsigned long size;// hash表大小掩码,用于计算索引值,值总是size -1 unsigned long sizemask;// 该hash表已有的节点数量unsigned long used;
} dictht;//hash节点
typedef struct dictEntry {//键void *key;// 值union { void *val;uint64_t u64;int64_t s64;double d;} v;// 指向下一个节点,解决碰撞冲突struct dictEntry *next;
} dictEntry;//定义hash需要使用的函数
typedef struct dictType {// 计算哈希值的函数uint64_t (*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, void *key);// 销毁值的函数void (*valDestructor)(void *privdata, void *obj);
} dictType;

 参数解析:

dict中

type标识dictType类型,privatedata是指向特定数据类型的数组;

ht包含了两个哈希表的数组,其中一个ht[0]保存哈希表,ht[1]只会在对ht[0]进行rehash时使用

dicht中

table是一个数组,其中一个数组元素都是一个指向哈希表节点的指针,每一个哈希表节点都保存了一对键值对。

size记录了table的大小,也即是哈希表的大小

used记录了当前已使用节点的数量

sizemask=size-1 ,决定了一个键应该放tabl数组的哪个索引上

dicEntry中

key保存键值对中的键,

v保存这键值对中的值

next为指向另一个哈希表几点的指针,当有hash冲突的时候

 

哈希表的扩展和收缩

当以下条件中的任意一个被满足时,程序会自动开始对哈希表执行扩展操作

1:服务器目前没有在执行bgsave命令或者bgrewriteaof命令、并且哈希表的负载因子大于等于1

2:服务器目前正在执行bgsave命令或者bgrewriteaof命令并且哈希表的负载因子大于等于5

rehash

rehash 跟Java中的hashmap扩容机制差不多

随着操作的不断执行,哈希表保存的键值对会逐渐地增多或减少,为了让哈希表的负载因子维持在一个合理的

范围内当哈希表保存的键值对数量太多或者太少时程序需要对哈希表的大小进行相应的扩展或者收缩

redis对字典的哈希表执行rehash的步骤如下;

1、为字典ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作以及ht[0]当前包含的键值对数量(ht[0].used)

2、如果执行的是扩展操作,那么ht[1]的大小为第一个大于等于ht[0].used*2的n次幂

3、如果执行的是收缩操作,那么ht[1]的大小为第一个大于等于ht[0].used的2的n次幂

4、将保存在ht[0]中的所有键值对rehash到ht[1]上面rehash指的是重新计算键的哈希值和索引值,然后将键值对放置到ht[1]哈希表的指定位置上

5、当ht[0]包含的所有键值对都迁移到了ht[1]之后释放ht[0]将ht[1]设置为ht[0],并在ht[1]新创键一个空白哈希表为下一次rehash做准备

为了避免rehash对服务器性能造成影响,服务器不是一次性将ht[0]里面的所有键值对全部rehash到ht[1]而是分多次将ht[0]里面的键值对慢慢地rehash到ht[1]

在进行渐进式rehash的过程中,字典会同时使用ht[0] ht[1]两个哈希表所以在渐进式rehash进行期间字典的删除 查找 更新等操作会在两个哈希表上进行 新添加到字典的键值对一律会保存到ht[1]里面则ht[0]不再进行任何添加操作

渐进式hash

如果哈希表里保存的键值对数量是四百万、四千万甚至四亿个键值对,那么要一次性将这些键值对全部rehash到ht[1]的话,庞大的计算量可能会导致服务器在一段时间内停止服务。因此,为了避免rehash对服务器性能造成影响,服务器不是一次性将 ht[0]里面的所有键值对全部rehash到ht[1],而是分多次、渐进式地将ht[0]里面的键值对慢慢地rehash到ht[1]。

以下是哈希表渐进式 rehash 的详细步骤:

1、为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表。

2、在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作正式开始。

3、在rehash进行期间,**每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,**还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对

4、rehash到ht[1],当rehash工作完成之后,程序将rehashidx+1(表示下次将rehash下一个桶)。

5、随着字典操作的不断执行,最终在某个时间点上,ht[0]的所有键值对都会被rehash至ht[1],这时程序将rehashidx属性的值设为-1,表示rehash操作已完成。

渐进式rehash的好处在于它采取分而治之的方式,将rehash键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式rehash 而带来的庞大计算量。而memcached通过也需要rehash,但是它是另外单独开一个线程,专门执行rehash操作。这就是区别与Redis的做法,个人任何Redis的作者方法更胜一筹。因为这种分治算法,将全部负载,均摊到了每次的操作过程中,在单个线程中实现。这个就是迁移线程,memcached中的迁移线程。

Set 类型

底层使用的是intset 或hashtable 实现的

hashtable 上面介绍过了,下面介绍intset

intset 整数集合

有序不重复的连续空间,特性如下:

        1、节约内存,但由于是连续空间,修改效率不高

        2、集合中的数都是整数时,且数据量不超过512个时,使用intset

        3、整数集合是集合键的底层实现之一

       4、 整数集合的底层实现是数组,这个数组以有序、无重复的方式保存集合元素,在有需要的时候,程序会根据新添加元素的类型,改变这个数组的类型。

        5、升级操作为整数集合带来了操作上的灵活性,并且尽可能的节约了内存。

        6、整数集合只支持升级操作,不支持降级操作

typedef struct intset {// 编码类型 int16_t、int32_t、int64_tuint32_t encoding;  // 长度 最大长度:2^32uint32_t length;  // 用来存储数据的柔性数组  int8_t contents[];  
} intset;

Zset 类型

底层是由ziplist 或 skiplist 实现的,ziplist 上面介绍过了,下面介绍skiplist

zkiplist 跳表

跳表(Skiplist)是一个特殊的链表,相比一般的链表,有更高的查找效率,可比拟二叉查找树,平均期望的查找、插入、删除时间复杂度都是O(logn)。具有层次结构的链表,可支持范围查询

跳表的第一层是一个双向链表,其他层级的都是单向链表

它由很多层结构组成,每一层都是一个有序的链表,默认是升序,也可以根据创建映射时所提供的Comparator进行排序,具体取决于使用的构造方法
最底层(Level 1)的链表包含所有元素
如果一个元素出现在Level i 的链表中,则它在Level i 之下的链表也都会出现
每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素
 

// 跳表节点
typedef struct zskiplistNode {// 存放至该节点的数据robj *obj;// 数据的分数值,zset根据这个分数值进行升序排序double score;// 指向链表的上一个节点struct zskiplistNode *backward;// 跳表的层级,每一条数据代表着一层struct zskiplistLevel {// 指向下一个节点的指针struct zskiplistNode *forward;// 表示这个指针跳跃了多少个节点unsigned int span;} level[];
} zskiplistNode;// 跳表
typedef struct zskiplist {// 头节点指针,尾节点指针struct zskiplistNode *header, *tail;// 跳表的长度,不包括头指针unsigned long length;// 跳表的层数int level;
} zskiplist;

到这里我们常用的redis的几种数据结构以及底层的实现原理也大致了解完了。

个人觉得配合图理解起来更为直观一些。

我本人在工作中也经常通过图来描述一些复杂的场景。有些场景文字描述比较难以理解,但是图片可以更形象,更直观的表达出来。


http://chatgpt.dhexx.cn/article/45jA3RcO.shtml

相关文章

Redis数据结构之hash

对象类数据的存储如果具有较频繁的更新需求操作会显得笨重&#xff0c;这里我们可以用redis的hash数据类型解决。 一、hash类型 新的存储需求&#xff1a;对一系列存储的数据进行编组&#xff0c;方便管理&#xff0c;典型应用存储对象信息 需要的存储结构&#xff1a;一个存储…

Redis数据结构之Zset

文章目录 一、zset数据结构二、跳表skipList什么是跳表&#xff1f;1.跳表的查找2.跳表的插入3.跳表的删除4.跳表的更新 一、zset数据结构 相比于set&#xff0c;sorted set 增加了一个权重参数 score&#xff0c;使得集合中的元素能够按 score 进行有序排列&#xff0c;还可以…

Redis数据结构有哪些

Redis数据结构有哪些 一、Redis数据结构 一、Redis数据结构 Redis是一种基于内存的数据库&#xff0c;并且提供一定的持久化功能&#xff0c;它是一种键值&#xff08;key-value&#xff09;数据库&#xff0c;使用 key 作为 索引找到当前缓存的数据&#xff0c;并且返回给程序…

「Redis数据结构」压缩列表(ZipList)

「Redis数据结构」压缩列表&#xff08;ZipList&#xff09; 文章目录 「Redis数据结构」压缩列表&#xff08;ZipList&#xff09;一、概述二、结构三、连锁更新问题四、压缩列表的缺陷五、小结参考 ZipList 是一种特殊的“双端链表” &#xff0c;由一系列特殊编码的连续内存…

redis数据结构hash

Redis数据结构之hash Hash存储结构 Hash是一个string 类型的field和value的映射表。Hash特别适合存储对象&#xff0c;相对于将对象的每个字段存成单个string 类型。一个对象存储在Hash类型中会占用更少的内存&#xff0c;并且可以更方便的存取整个对象。 我们简单举个实例来…

简述redis数据结构

String&#xff1a;字符串 List&#xff1a;列表 Hash&#xff1a;哈希表 Set&#xff1a;无序集合 Sorted Set&#xff1a;有序集合 bitmap&#xff1a;布隆过滤器 GeoHash&#xff1a;坐标&#xff0c;借助Sorted Set实现&#xff0c;通过zset的score进行排序就可以得到坐标附…

Redis数据结构之——hash

写在前面 以下内容是基于Redis 6.2.6 版本整理总结 一、Redis 数据结构hash的编码格式 Redis中hash数据类型使用了两种编码格式:ziplist(压缩列表)、hashtable(哈希表) 在redis.conf配置文件中&#xff0c;有以下两个参数&#xff0c;意思为&#xff1a;当节点数量小于512并…

Redis底层数据结构详解

Redis底层数据结构详解 我们知道Redis常用的数据结构有五种&#xff0c;String、List、Hash、Set、ZSet&#xff0c;其他的集中数据结构基本上也是用这五种实现的&#xff0c;那么&#xff0c;这五种是Redis提供给你的数据结构&#xff0c;这五种数据结构式怎么实现的&#xf…

Redis的五种基础数据结构

Redis的五种基础数据结构 Redis有5种基础数据结构&#xff0c;分别为:String(字符串)&#xff0c;list(列表)&#xff0c;hash(字典)&#xff0c;set(集合)和zset(有序集合)。 1.String(字符串) 字符串的结构 字符串String是Redis最简单的数据结构&#xff0c;它的内部表示…

Redis的数据结构

一、redis的数据结构 1、String字符串类型 Redis的String能够表示字符串、整数、浮点数三种值的类型 应用场景&#xff1a; 普通的赋值使用incr、decr命令进行递增和递减统计数据。用于实现乐观锁watch(事物)setNx实现分布式锁 底层数据类型&#xff1a; // 数据结构 stru…

为了拿捏 Redis 数据结构,我画了 40 张图(完整版)

大家好&#xff0c;我是小林。 Redis 为什么那么快&#xff1f; 除了它是内存数据库&#xff0c;使得所有的操作都在内存上进行之外&#xff0c;还有一个重要因素&#xff0c;它实现的数据结构&#xff0c;使得我们对数据进行增删查改操作时&#xff0c;Redis 能高效的处理。…

IDEA 配置Services面板

IDEA中的services窗口是一个管理所有服务的地方&#xff0c;特别是在使用spring cloud项目开发中&#xff0c;对多个微服务的管理非常实用。 配置方式一&#xff1a; 在IDEA中可通过配置将services窗口面板显示出来。 配置方式二&#xff1a; 找到项目.idea目录下worksp…

如何在IDEA配置Tomcat

1、点击RUN下拉的Edit Configurations 2、点击左上角号&#xff0c;下拉找到Tomcat Server&#xff0c;选择Local 3、 配置Tmcat信息 4、点击Deployment,选择右边的号&#xff0c;选择Artifict 5、Application context这里只写个“/”就可以了 6、点击Apply&#xff0c;然后返回…

idea配置代理服务器

1、查看代理服务器ip和端口 使用http代理 2、idea中配置代理服务器 选择File --> Settings --> 搜索HTTP Proxy --> 选择Manual proxy configuration --> 选择HTTP输入ip和端口 --> 点击Apply 3、配置jvm启动参数 -Djava.net.useSystemProxiestrue 完成!

IDEA配置安卓环境

IDEA是JetBrains公司推出的Java集成开发环境 &#xff0c;这学期又开了安卓课&#xff0c;少不了安卓环境&#xff0c;不过android stdio是完全支持安卓的&#xff0c;不适合开发java项目&#xff0c;作为小白&#xff0c;我觉得IDEA一个软件就够了&#xff0c;当然&#xff0c…

idea配置maven仓库

1、去达内开发文档服务器下载对应插件 达内开发文档服务器 找到配置文件下载 &#xff0c;下载阿里云Maven创库配置 以上下载压缩包先不动 2、创建maven项目 打开idea file-----new project 点击左侧选择maven然后next 3、然后查看对应的maven路径 点击file---setting 每个…

IDEA配置本地Maven

IDEA配置Maven: 参考视频教程:https://www.bilibili.com/video/BV1Qf4y1T7Hx?p45 总体流程预览: 创建一个空的项目IDEA配置Maven环境Maven 坐标详解IDEA创建Maven项目IDEA导入Maven项目maven Helper插件依赖管理 1.创建一个空的项目 File—>New—>Project—>Empt…

idea配置tomcat

1.添加tomcat。 2.点击Edit Configurations后打开如下界面。| 3.点击加号 4.在这里配置tomcat信息&#xff0c; 5.填写好基本tomcat信息后&#xff0c;点击ok。我们就配置好了Tomcat&#xff0c;此时我们会看到。项目中显示了我们刚才配置的的Tomcat。 但是直接运行的话…

idea配置docker

IDEA作为开发电脑&#xff0c;要远程连接到另一台Linux电脑上部署的Docker服务&#xff0c;我们登陆到机器上&#xff0c;进行docker远程访问配置&#xff1a; IDEAdocker实践 - 邹姣姣 - 博客园 idea里 选择Plugins&#xff0c;插件 已经安装了docker插件 连接远程Docker服…

idea配置maven

idea配置maven 下载配置maven(如果有请跳过)打开idea开始配置maven附录&#xff1a; 下载配置maven(如果有请跳过) 首先你先得安装配置你本机maven的环境&#xff0c;官方下载maven&#xff0c;附带下载maven&#xff1a; 下载maven链接 我选择的时3.6.3-bin.zip&#xff0c…