Redis第一部分数据结构与对象

本文深入探讨Redis中的数据结构,包括其独特的SDS字符串实现、链表、字典、跳表及集合等。揭示了这些数据结构如何提升Redis的性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

为何而写

很多是自己看黄健宏老师的《Redis设计与实现》以及相关博客总结,算是自己的复习笔记。也能帮助大家面试装13什么的~

redis的数据结构

一说redis常说的五种类型是什么,我们都会脱口而出string,list,hash,set,zset。但是了解越深,才会发现有趣的东西(装逼)

字符串

我们常见的string,也是最常用的。但是就普通的string也埋藏了很多。redis是用C编写的,但是并没有用C自带的字符串。而是redis有自己的字符串,叫SDS(简单动态字符串,simple dynamic string)

redis会用sds.h/sdshdr结构表示SDS的值

当执行 SET msg “app”
就会创建一个新的键值对
键值对的键是一个字符串对象,这个对象的底层是一个保存的msg的SDS
键值对的值也是一个字符串对象,也是保存了个app的SDS

struct sdshdr {
	//已经使用了的字符串数量
	int  len;
	//在buf数组中未使用的字节的数量
	int free;
	//字节数组,保存字符串用的
	char buf[];
};

下图就是创建了对象存的结构
free 0 代表着SDS还没分配任何没使用的
len 3 就是app的长度
buf 就是个char类型数组,存的对应的app字符,但是SDS也遵循了C的字符串规则,用’\0’结尾,但是保存空字符的是不会算准SDS的len属性里。好处就是,SDS能直接用一些C字符串函数库里的函数
在这里插入图片描述
如果当分配了未使用空间,那么对于的free就会有对应长度

那么为什么,redis会采用SDS来存储字符对象,而不是用C语言自带的字符串?

这是因为当用C自带的String的时候,并不会记录字符长度,当需要获取长度的时候,是需要对整个字符串进行遍历,一直遍历到“\0”,来获取长度,那么需索的复杂度就是o(n),但是在SDS中是记录了len属性来保存字符长度,那么当redis需要获取字符串长度的时候,可以直接获取,那么复杂度就会是O(1),那么效率就会高很多。

杜绝缓冲区溢出

这里也是为何采用了SDS,却没用C自带的字符串的原因之一。
在C语言中,字符串不记录字符长度就会带来一个问题,容易造成缓冲区溢出
有个例子,就是内存紧靠着来个C字符串S1和S2,当S1准备进行拼接的时候,但是又没有给S1分配足够的内存,那么执行strcat函数后,就会将S1的数据溢出到S2的数据中,导致S2的数据异常。

但是redis所用的SDS的空间分配策略就杜绝了缓冲区溢出的可能,当使用SDS的API对SDS进行修改,SDS会先检查空间是否满足,不满足,就是将已有空间扩展到满足所需大小,也避免了手动修改SDS的空间大小,从而避免了缓冲区溢出。

减少修改字符串的内存重分配次数

这里还是和C字符串不会记录长度有关,C的字符串底层用的是个N+1的数组,这样在字符串进行增加和截断的时候,就会频繁的进行内存重分配的问题。
如果不进行重分配就会出现问题
1-当进行增加操作的时候,如果不对已有的内存进行扩展,就会造成内存溢出的问题
2-当进行C字符串的截断操作 ,已经分配的内存,不进行释放,那么也会造成内存泄露问题。
上面所说是一种通病。

那么redis采用的SDS中free就是为了避免这种C的频繁更改缺陷,SDS采用未使用空间使得buf数组的长度不一定就会是长度+1,数组中可以包含未使用的空间。
通过空间的预分配,SDS实现了空间预分配和惰性空间释放这俩种优化策略,感觉很逼格满满的词,其实本质都是很棒的一种优化

1-空间预分配
关于分配未使用空间数量的是有个公式计算的
当len属性值小于1MB的时候,进行对字符串的修改,len变成了15,那么未分配的空间free也会变为15,那么buf的实际长度就变成了 15+15+1的长度
当len属性值大于等于1MB的时候,那么程序会分配1MB的未使用空间,就是当修改字符串后,len为3MB,那么实际的buf长度就是1MB+3MB+1byte
通过这种空间预分配的策略,redis可以减少内存重分配次数,如何减少呢。
例如:当有一个A的SDS字符串"abc",进行修改后,追加了三个字符"def",def肯定比1mb小,修改后,就会给对应的free的属性分配len+3大小的空间,也就是"abcdef"大小的空间。 如果再对这个A进行追加“g”,这时SDS会先看free的未使用数组的长度是否可以放下这个"g",放的下就不会分配新的内存,而是直接使用free的空间,此时free就会减去"g"大小的空间。 就是这种优化流程,称之为空间预分配,从而减少了每次修改都会重分配内存的弊端

2-惰性空间释放
实现这种优化也是和SDS的free属性相关,当对字符串进行截断操作的时候,余下的空间,就会给到free属性。当再对字符串追加字符的时候,就可以用到这部分未使用的空间。也不需要担心内存浪费,SDS也有真正释放SDS中free属性的API。

二进制安全

C字符串中是不允许中间包含空字符的,因为C字符串结尾是用"\0"表示结尾,中间出现空字符,就会出现读取数据缺失的问题。
但在redis的SDS的API都是二进制安全的,是用处理二进制数据的规则实现。存入什么样的数据,取出就会是什么样的数据。这也是buf叫字节数组的原因。当存放到SDS数组中的数据中间包含了空字符,也不需要担心,因为SDS是包含了数据len的长度。

链表

redis构建了自己的链表。redis的很多功能,发布订阅,慢查询,监视器等都用到了链表。
链表的节点采用的adlist.h/listNode结构

typedef struct listNode {
	//前置节点
	struct listNode *prev;
	//后置节点
	struct listNode *next;
	//节点的值
	void *value;
}listNode;

这样一个节点中有前置节点和后置节点就组成了双端链表。

redis还采用list包含listNode来提升操作

typedef struct list{
	//链表的头节点
	listNode  *head;
	//链表尾
	listNode  *tail;
	//链表所包含的节点数量
	unsigned long len;
	//复制函数,释放函数,节点对比函数等
	//节点的值
}list;

redis链表的实现是双端链表

字典

的
字典结构在很多语言中都包含,并且是一种k-v效率很高的结构,redis编写的C中没有内置,就编写了属于自己的字典实现,上图就是大致redis的字典结构。

哈希算法

哈希算法有很多,redis本质还是根据添加的键值对,基于键计算出哈希值和索引值,根据索引值放到哈希表的数组指定索引上。

键冲突问题

当有俩个或者更多键值对分配到了哈希表数组中同一个索引上时,采用了链表的操作,就是dictEntry中的next属性,来指向下一个键值对,来解决的键冲突问题。

rehash

redis服务不断接收执行指令,哈希表保存的键值对会慢慢变多或者减少,基于要保证负载因子的合理范围,当超过阈值就会进行rehash来扩大或者缩小哈希表大小。
这里就会用到字典总的ht属性,ht属性中就是俩个哈希表数组。 比如ht[1]会基于操作和ht[0]的大小来分配空间,分配好后,把ht[0]中的数据rehash到ht[1] 空间中
当全部迁移完毕后,就会释放ht[0]并将ht[1]设置为ht[0],并在ht[1]新创建一个空白的哈希表。

渐进式hash

在进行rehash的时候,并非一次性的将ht[0]的数据迁移到ht[1]中,因为当数据量太大的时候,rehash是全量的就会造成服务不可用。在redis进行rehash的时候是分多次,渐进式的完成。大致redis进行操作的时候,会维护ht[1],同时将命中ht[0]的哈希表中某链表rehash到新的ht[1]中。

跳表
zset的结构

redis的zset接口存储数据的时候,会采用跳表结构来进行存储数据。跳表是基于单链表加索引的方式实现的,它是以空间换时间的方式来提升查找速度。
Reids的跳跃表实现由zskiplist和zskiplistNode俩个结构组成,其中zskiplist是用于保存跳表信息,zskipListNode是用于表示跳表节点。

跳跃表节点

typedef struct zskiplistNode{
	//后退指针:指向当前节点的前一个节点
	struct zskiplistNode *backward;
	//分值:在跳跃表中,节点按各自所保存的分值从小到大排列
	double score;
	//成员对象:用于保存各个node节点中的成员对象
	robj *obj;
	//层:节点中会用l1,l2,l3这种标识来标记节点的各个层数,l1对应第一层,以此类推。
	struct zskiplistLevel{
		//前进指针:用于访问表尾方向的其他节点
		struct zskiplistNode *forward;
		//跨度:记录前进指针所指向节点和当前节点的距离。
		unsigned int span;
	} level[];
}zskiplistNode;

跳跃表结构

typedef struct zskiplist {
	//头尾阶段
	structz zskiplistNode *header,*tail;
	//跳表中节点数量
	unsigned long length;
	//最大节点层数
	int level;
}zskiplist;

跳表节点的层高是1-32的随机数。

集合

redis中set结构采用了hashtable和intset(整数集合),当一个集合只包含整数值元素,并且~这个集合的元素数量不多,redis就会用整数集合作为集合键的底层实现。

typedef struct intset {
	//编码格式
	uint32_t encoding;
	//对应的是contents 数组的长度
	unint32_t length;
	//保存元素的数组
	int8_t contents[];
	
} intset;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值