redis的基本数据结构

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


一、基本数据机构有哪些?

我们熟知的redis的基本类型包括:string、hash、list、set、zset这些类型仅仅指的是redis的操作类型,在其底层存储的数据类型在底层数据的存储都是以redisObject对象进行存储,关于redisObject数据结构,如图所示:

typedef struct redisObject {
// 类型
unsigned type:4;
// 对齐位
unsigned notused:2;
// 编码方式
unsigned encoding:4;
// LRU 时间(相对于 server.lruclock)
unsigned lru:22;
// 引用计数
int refcount;
// 指向对象的值
void *ptr;
} robj;

需要关注的主要是 type,encoding,ptr,分别表示存储数据的类型,底层编码(底层存储结构)、指向存储地址的指针。这里的encoding就是redis的底层存储结构,类型包括:SDS(简单动态字符串)、int集合,hash字典、skiplist(跳跃表)、linkedlist(双端链表)、ziplist(压缩链表)。其基本关系为:
在这里插入图片描述

二、具体实现

1.SDS(简单动态字符串)

sds(Simple Dynamic String 简单动态字符串),是redis中保存字符串数据的一种底层数据结构,如果保存的如数字那么就是long类型的。其底层结构为:

struct sdshdr {
 // buf 已占用长度
int len;
 // buf 剩余可用长度
int free;
 // 实际保存字符串数据的地方
char buf[];
 };

其它具备的优点:
(1)二进制安全
(2)O(1)复杂度的获取字符串长度
(3)空间预分配
二进制安全:在c语言中,以\0表示字符串的结束,如果将包含了’\0’字符的数据存储到传统的C语言字符串中,\0之后的数据会被忽略,从而导致数据的截断或丢失。而在Redis中使用SDS作为字符串存储的数据结构,可以避免这个问题。SDS不依赖于结束标志来确定字符串的长度,而是在结构体中保存了字符串的长度信息。因此,SDS可以正确地存储包含了’\0’字符的数据,而不会导致数据的截断或丢失。
O(1)复杂度的获取字符串长度:通过len可以直接获取数据长度,字符串数组需要遍历后才能获取长度信息。
空间预分配:当sds的字符串数组空间不足需要扩容时,会采用一种预分配策略,也就是为字符串分配比实际需要更多的空间,以减少因为连续的字符串拼接或追加操作而导致的频繁内存重分配操作。这里具体的分配策略,需要根据存储字符串的大小有所区别:
1、当len小于1MB,此时存储数据,扩容是(你当前存储数据的空间len+还需要的空间长度)✖2.
比如char[] buff 保存了’hello world’字符串,此时len为11,如果此时的free=0,继续append一个字符串
" csdn",那么此时扩容后存储的sds结构为:

struct sdshdr {
 // buf 已占用长度
int len=16;
 // buf 剩余可用长度
int free=16;
 // 实际保存字符串数据的地方
char buf[]=“hello world csdn”;
 };

2、当len大于1MB时,Redis 会额外分配 1MB 的未使用空间。这是因为大字符串的扩展操作往往不会像小字符串那样频繁,因此不需要分配过多的预留空间,固定分配 1MB 的空间能够平衡内存的使用和操作的性能。

2.压缩列表

ziplist:既不是数组也不是链表,而是一块连续的内存块,每一个节点都可以存储一个字符数组,整数,链表。因为其存储数据结构紧凑,连续,并通过压缩元数据的方式(节点数据类型,长度通过不同编码的字节保存)保存数据,有效的减少内存占用,所以ziplist是很多类型默认保存数据的方式,比如:list,hash。
其底层数据结构如图所示:
在这里插入图片描述

摘录自《redis设计与实现》
其中entry保存着每一个数据,每个entry可以划分为以下部分:
在这里插入图片描述
pre_entry_length:记录上个entry的长度,通过遍历前置节点长度,ziplist可以实现倒序遍历。
encoding:记录content存储的数据类型,主要分为整数编码,字符串编码,压缩编码
length:当前entry的长度
content:存储数据的内容,根据encoding的不同,存储的数据也不同,当encoding是整数编码,content保存一个整数,当encoding是字符串编码,压缩编码,content保存指向字符串的指针。
需要注意的压缩列表特点:连续扩容、收缩
因为entry会保存前置节点的长度,前置节点的长度不同,entry占用的空间长度也不同,当插入一个新的entry时,之前entry保存的pre_length可能会随之改变, 导致空间改变从而引起扩容,如果后续的entry都因为前面的entry的导致空间大小改变,就形成了连锁扩容。同理,收缩就是当我们删除一个entry,导致后续的entry存储空间都变小。

3.双端链表

linked list:相比于普通的链表,双端链表的节点除了保存数据,也存储了指向前后节点的指针,这使得其可以很方便的进行双向遍历、插入,删除操作。
其底层代码所下:

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、next指针相互关联,如图所示:
在这里插入图片描述

4.跳跃表

skip list:其实跳跃表,就是带有层次结构的链表,每个节点都带有1个或多个随机数量的节点指针,通过不同的层级节点一层一层从上往下遍历,而不是按照链表整个横向遍历获取数据,从而提高效率。
其底层数据结构如图所示:
在这里插入图片描述
需要注意的是,插入数据并不是严格按照每个层级节点都是下面的一半,因为这样的话,插入新数据会导致这个节点后面的数据都需要变更,时间复杂度高,为了避免这种情况,skiplist插入数据,都是随机给定数据的层级,不要求层级之间的节点个数精确对应,但其基本效率越等于二分查找。

5.字典

hash字典:是与sds在redis中运用最多的底层结构了,不仅仅是用于保存redis的key值 ,redis的整个底层结构都是一个字典。除此之外,其订阅发布功能,也是通过字典实现,大概通过数组保存channel通过,每个channel保存其对应订阅的客户端,这样发布对应channel的消息,就能找对应接受的客户端。这里怎么理解这个"redis整个都是一个hash字典呢",可以把整个数据库理解为,hash表嵌套hash表。在数据库的最外层,有一个hash字典,这个hash表的桶是一个个redisDB,默认有16个,编号0-15,通过select n指令,可以选择对应的数据库,在每个redisDB里面又有2个hash,平时只使用一个hash表,当需要扩容时,通过渐进式rehash的方式,将hash表1中的数据复制到hash表2,在每个hash表中,存储redis的key-value是通过dictEntry对象存储,而dictEntry的里面又是通过redisObject对象保存key,value,是不是很惊喜,回到了文章最开始的源头,一切都串了起来。
整体结构如图所示:
在这里插入图片描述
关于字典扩容的渐进式rehash:主要是为了避免在数据迁移过程中,阻塞客户端请求处理,分为主被动方式,触发条件与存储数据数量与hash表桶数量之前的比率有关系,默认1:1,被动rehash时,当每次进行增删、查操作,redis会依次遍历hash桶,将第一次遇到的不为空的桶中的dictEntry全部rehash到新的hash表中。
最后简单说明一下从redis取值的流程:
1、根据选择的数据库,找到对应的redisDB
2、计算键的hash值,找到对应的hash桶的位置(可能存在一个或多个dictEntry)
3、遍历哈希桶中的dictEntry,找到键值与目标键值相等的dictEntry
4、根据找到的dictEntry,获取其值的redisObject。
5、根据redisObject中的encoding字段,判断值的底层存储结构。
6、根据底层存储结构,获取到具体的数据。


以上就是我想讲的 Redis 基本数据结构及其底层存储方式,读者可以深入了解 Redis 在内存中如何组织和管理数据。了解这些底层细节对于理解 Redis 的高效性和优势具有重要意义。

希望本文对读者有所帮助,若有任何不足之处,欢迎批评指正,共同学习进步。感谢您的阅读!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值