Redis系列——底层数据结构分析

本文深入探讨了Redis的内部数据结构,包括简单动态字符串SDS、链表、字典(详细介绍了rehash过程)、跳表以及整数集合和压缩列表。SDS优化了C语言字符串的管理,链表是双端的,字典使用哈希表实现并进行了rehash的详解,跳表用于有序集合,提供了高效的查找效率。整数集合适用于少量元素的集合,压缩列表则用于节省内存。

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

前言

redis作为k->v的数据库,广泛的使用在缓存中。我每次用redis时就直接调用api或者spring中封装好的对象,从来没有深入研究过内部结构。这也导致有时候面试被问到了,或者自己在调用的时候一知半解,出了问题不知道怎么去排查。
最近在看好多redis的技术书籍以及相关博客,也同时写一系列的redis文章,便于自己梳理脉络,也方便新手入门。

由于本人技术水平限制,错误在所难免,若有发现请及时留言,谢谢。

简单动态字符串SDS

redis用c语言编写。在c语言中字符串用以\0结尾的数组。但是redis新建了一种数据结构作为默认的字符串。
比如 set msg abc,建立了内容为msg的SDS作为键,内容为abc的SDS作为值的键值对。
在这里插入图片描述

SDS结构体
在这里插入图片描述

比起c语言中的字符串数组,多了用于管理的单元

结尾的\0不算在len里面,这样可以重用C语言中字符串的库

C语言中的字符串数组,在增长和缩短时需要对底层数组内存重新分配。由于redis是作为数据库使用的,那么就会频繁的增长和缩短字符串长度。

redis采用SDS结构做出了优化

1、空间预分配(优化增长)
在字符串增长时会为SDS分配额外的未使用空间。分配与len相同的最多1M额外空间。
2、惰性空间释放(优化缩短)
缩短字符串时,删除数组中的字符,但是不会释放空间,算成额外空间,增加free的值。

由于数据库可能保存二进制的数据,包括视频、音频等。所以如果\0作为结尾标志,那么会丢失掉后面的有效数据。SDS用len值判断数据有效范围。

链表

redis自己定义了一个双端链表

typedef struct listNode {
   
	// 指向前一个节点
	struct listNode *prev;
	// 指向后一个节点
	struct listNode *next;
	// 当前节点的值
	void *value;
}listNode

这个与通常的链表节点一致,不做解释。
在这里插入图片描述基于上述的链表节点,redis定义了自己的链表

typedef struct list {
   
	// 指向链表头
	listNode *head;
	// 指向链表尾
	listNode *tail;
	// 记录链表元素个数
	unsigned long len;
	// 复制节点函数
	void *(*dup)(void *ptr);
	// 节点值释放函数
	void *(free)(void *ptr);
	// 节点值对比函数
	int (*match)(void *ptr,void *key);
}list

在这里插入图片描述

可以看到表头节点的pre指向NULL,表尾节点的next指向NULL

字典

这一小节会比较繁琐,并且以上面两种数据结构为基础,要有耐心
说到字典,背过java八股文的朋友应该很熟悉hashmap,嘿嘿!
但是很少人了解过redis内部的map,毕竟面试问的很少嘛

一个键和一个值进行关联,关联的键和值成为键值对
redis数据库将字典作为底层的数据结构,对数据库crud就是对字典操作
比如 SET msg abc,在数据库中建立了一个键为msg,值为abc的键值对,键值对就是保存在字典中。
哈希键包含的键值对较多时,redis也使用字典作为底层实现
比如web包含许多哈希键
在这里插入图片描述

可以看到键为web,值为一个字典,字典包含许多键值对
键值对键baidu,值为baidu.com
键值对键google,值为google.com

那么这个具体是怎么实现的呢?
我们自顶向下的拆解这个复杂的数据结构

typedef struct dict {
   

    // 类型特定函数
    dictType *type;

    // 私有数据
    void *privdata;

    // 哈希表
    dictht ht[2];

    // rehash 索引
    // 当 rehash 不在进行时,值为 -1
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */

    // 目前正在运行的安全迭代器的数量
    int iterators; /* number of iterators currently running */

} dict;

dictType保存了许多用于操作键值对的函数,privatedata保存用于dictType的参数。
iterators 目前正在运行的安全迭代器的数量,我们放在后面讲解这个。
暂时忽略这几个参数的具体实现,我们重点讲存储的数据结构。
ht是保存了两个哈希表的数组,一般情况只使用ht[0],ht[1]在对ht[0]进行rehash操作时用。用trehashidx标志现在是否正在进行rehash。
那么我们在脑海中可以想象出来这样的结构
在这里插入图片描述

现在关键就在于如何这个用来保存键值对的结构是什么样子的。

typedef struct dictht {
   
    
    // 哈希表数组
    dictEntry **table;

    // 哈希表大小
    unsigned long size;
    
    // 哈希表大小掩码,用于计算索引值
    // 总是等于 size - 1
    unsigned long sizemask;

    // 该哈希表已有节点的数量
    unsigned long used;

} dictht;

具体实现如图所示,size表示table数据的大小,sizemask用于对hash值取模(等于size-1),used表示键值对的数量。

在这里插入图片描述

我们知道了键值对保存在dictEntry类型数组中,数组的每个元素都是一个链表
那么我们来看一下这个结构
哈希表的节点


                
### Redis 列表的底层数据结构 Redis列表采用快速链表(quicklist)作为其底层数据结构[^2]。这种设计融合了双向链表与压缩列表(ziplist)的优点。 #### 快速链表 (QuickList) 快速链表本质上是一种优化后的双向链表,其中每个节点不仅指向前后相邻的节点,还包含了一个压缩列表 ziplist。这意味着每一个 quicklist 节点实际上封装了一个小型的 ziplist 结构,从而允许更高效的内存管理和访问模式[^3]。 ```c typedef struct quicklist { // ...其他成员... quicklistNode *head; quicklistNode *tail; } quicklist; typedef struct quicklistNode { unsigned char encoding; /* 编码方式 */ int count; /* 当前ziplist中的元素数量 */ int mem_usage; /* 使用了多少字节 */ quicklistEntry entry; /* 用于迭代器 */ struct quicklistNode *prev, *next;/* 前驱和后继指针*/ unsigned char *zl; /* 指向ziplist的实际数据 */ } quicklistNode; ``` #### ZIPLIST 特性 Ziplist 是一种紧凑型线性序列化容器,在单个连续分配的缓冲区内存储一系列项。它特别适合于短字符串或小整数值,并能显著减少内存碎片并提高缓存命中率。然而,随着长度增加,操作效率可能会受到影响;因此,当达到一定阈值时,系统会选择切换至更为传统但占用更多空间的实现方法[^1]。 #### API 和自动调整机制 为了简化开发者的工作流程,Redis 提供了一系列高级别的命令接口来处理列表的操作,比如 LPUSH、RPUSH 等等。这些API能够透明地管理内部使用的具体数据结构形式——无论是初始状态下的ziplist还是扩展之后形成的完整双向链表形态。此外,根据实际应用场景的需求变化,Redis 还可以在运行期间动态改变所选用的数据结构类型以保持最佳性能表现。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值