REDIS系列之底层数据结构

2024-09-03 13:58

本文主要是介绍REDIS系列之底层数据结构,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Redis是一款优秀的key-value数据库,其中存储的键值对都是有对象(Object)组成,可以存储字符串对象、哈希对象、列表对象、集合对象、有序集合对象;由于C语言中没有相关对象的实现,Redis自身扩种底层的数据结构实现上述对象的存储,本文将对REDIS数据库底层的数据结构进行介绍。


1. SDS字符串

Redis并没有直接采用C语言中字符串表示(以空字符结尾的字符数据,一下简称C字符串),而是扩展一种名为简单动态字符串(Simple Dynamic String, SDS)的数据结构,并将SDS作为Redis默认字符串表示。其主要出于安全性、效率性考虑。SDS数据结构如下:

typdef struct sdshdr{//记录buff数组中以使用字节的数量  //相当于sds字符串的长度  int len;  //buff中卫使用字节的数量int free;//字节数组用于保存字符串char *buff;} SDS;
效率性

C字符串以‘\0’结尾,获取字符串长度需要遍历数组,其时间复杂度O(N),伪代码如下:

  int cnt = 0;  while( *buff++ != '\0'){cnt++;}return cnt;

对于SDS字符串来说,sdshr->len即是字符串长度,时间复杂度为O(1).
字符串数据结构是在程序中经常会使用到的数据结构,Redis把字符串长度时间复杂度从O(N)降到O(1),这确保获取字符串长度的工作不会成为Redis的性能瓶颈。

安全性

除了获取字符串的复杂度高之外,C字符串不记录自身长度带来另一个问题是容器造成缓冲区溢出。举个例子, strcat函数可以将src字符串中的内容拼接到dest字符串的末尾:
char *strcat(char *dest,char *src)
假如在在内存中有两个C字符串s1="hello"s2="redis",如图所示:

这里写图片描述
如果程序执行stcat(s1," world");将s1的内容修改为“hello world”,那么在strcat函数执行之后,s1数据将溢出,s2的数据将被破坏。
与C字符串不同,SDS自动空间分配策略防止发生缓冲区溢出的可能性;当SDS API需要对SDS进行修改时,API首先检查SDS空间是否满足修改的要求,如果不满足的话,API将进行空间扩展至执行修改所需大小,然后执行拼接,伪代码如下:

  strcat(SDS dest,SDS src){if(src->len > dest->free){//重新分配空间,再复制}else{//复制字符串}}

此外,当SDS的API对一个SDS进行修改,发生缓冲区扩充时候,redis采用空间预分配机制,即程序不仅分配SDS修改所必须要的空间,还会分配额外的未使用空间。具体策略分为以下两个方面:

  • 如果修改后的SDS的长度(len属性)小于1M,则程序在分配缓冲区时,回额外分配len长度同样大小的未使用空间(free属性)。
  • 如果修改后的SDS的长度大于1M,则程序在分配缓冲区时,会额外分配1M未使用的空间(free属性)。
二进制安全性

C字符串的长度以’\0’结尾,并且除了结尾字符可以是’\0’,字符串里面不能包含空字符,否则程序最先被程序读入的空字符会被误认为是字符串的结尾,这限制C字符串之鞥保存文本数据,不能保存图像、音频、压缩文件这样的二进制文件。但是Redis采用len属性标志字符串的结尾,这使得Redis可以存储多种类型数据,是二进制安全。

总结

Redis只会使用C字符串作为字面量,大多数情况下Redis使用SDS座位字符串的表示。
比起C字符串,SDS具有如下优点:

  • 常熟复杂度获取字符串长度
  • 避免缓冲区溢出
  • 预分配机制减少修改字符串时内存冲分配次数。
  • 二进制安全

2.链表

链表作为一种高效的数据存储结构内嵌在许多高级语言中,但是C语言没有list结构,因此Redis定义一种链表结构,其数据结构如下:

//链表节点
typedef struct {//前置节点struct listNode *prev;//后置节点struct *listNode *next;//节点值void *value;} listNode;//链表
typedef struct {//头指针listNode *head;//尾指针listNode *tail;//长度unsigned long len;//节点值复制函数void *(*dup)(void *ptr);//节点值释放函数void *(*free)(void *ptr);//节点值对比函数int (*match)(void *ptr,void *key);
}

list结构为链表提供了表头指针head、表尾指针tail以及链表长度计数器le,而dup、free\match函数则是用于实现多态链表所需的类型特定函数。
下图是一个list链表结构示意图:
这里写图片描述

总结

  • 链表是一个双端链表,每个链表节点有一个listNode结构来表示
  • 每个链表使用list结构来表示,这个结构带有头指针、尾指针以及长度等信息
  • Redis链表可以保存各种不同类型的值
  • 广泛应用于实现redis各种功能

3.字典

字典又叫关联数组、映射(map),是一种保存键值对的抽象数据结构。字典在Redis中应用相当广泛,器数据库就是使用字典座位底层实现,对数据库的增删改查操作也是构建在对字典的操作之上。
其数据结构如下所示:

typedef struct dictht {// 哈希表数组dictEntry **table;//哈希表大小unsigned long size;//哈希表大小掩码,用于计算索引值unsigned long sizemask;//该hash表已有节点的数量unsigned long used;} dictht;typedef struct dictEntry {//键void *key;//值union {void *val;uint64_tu64;int64_ts64;} v;//指向下个哈希表节点,形成链表struct dictEntry *next;} dictEntry;typedef struct dict{//类型特定函数dictType *type;//私有数据void *pricdata;//hash表dictht ht[2];//rehash索引int rehashidx;} dict;

如图是一个例子:
这里写图片描述

哈希表扩容

哈希表扩容步骤如下:

  • 计算扩容后的内存,分配相应存储空间赋值给ht[1]
  • ht[0]中元素根据hash算法全部重新映射到ht[1]空间中
  • 回收原来h[0]内存空间
  • ht[1]赋值给ht[0]

哈希表中元素个数可能很多,如果一次性进行rehash将会耗费大量时间阻塞服务端对客户端服务进程,因此哈希表采用一种渐进式hash算法,即是将rehash动作分多次、渐进式地完成。其步骤如下:

  • dict数据结构中rehashidx标志 rehash的位置。
  • ht[1]分配内存空间,让字典同时持有ht[0]ht[1]两个hash表。
  • 在rehash期间,每次对字典进行添加、删除、查找、或者更新操作时候程序处理执行指定操作意外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当rehash完成之后,程序将rehashidx属性的值+1
  • 随着字典操作的不断执行,最终ht[0]内的所有键值对都会被rehash到ht[1],这时将rehashidx置为-1,表示rehash完成。
  • ht[1]赋值给ht[0],回收ht[0]空间。

总结

  • 字典被广泛应用于实现Redis各种功能
  • Redis中的字典使用hash表作为底层实现,每个字典带有两个哈希表,一个平时使用,另一个仅在进行rehash时使用
  • 哈希表采用开链方式解决键冲突
  • 在对哈希表进行扩展操作时,程序将现有的键值对rehash到新哈希表中,并且这个过程并不是一次性完成的,而是渐进式完成

4.压缩列表

压缩列表ziplist则是列表键和哈希键的底层实现,当存储的元素较少或者长度较短时候,为节约内存,则使用连续的存储空间存储元素。压缩列表是为了提高内存的使用率,是由一系列特殊编码的连续内存块组成的顺序型数据结构。一个压缩列表尅包含任意多个节点,每个节点可以保存字节数组或者整数值。
其数据结构如下:

typedef struct ziplist{//记录整个压缩列表占用的字节数uint32_t zlbytes;//记录压缩列表的尾节点距离起始节点字节数uint32_t zltail;//记录压缩列表包含节点数量uint16_t zllen;//存储的元素zlentry extryX;//压缩列表结尾特殊标记uint8_t zlend;} ziplist;typedef struct zlentry {//前一个节点字节长度uint previous_entry_length;//编码方式uint encoding;//字节数组uint8_t contents[];}

如下图是一个例子:

这里写图片描述

总结

  • 压缩列表是一种提高内存使用效率顺序型数据结构
  • 压缩列表可以包含多个节点,每个节点保存一个字节数组或者整数
  • 压缩列表被用作列表键和哈希键的底层实现之一

5.整数集合

整数集合intset是Redis底层存储整数值的集合抽象数据结构,可以保存int16_tint32_tint64_t的整数值,并且保证集合中不会出现重复元素。其数据结构如下:

typedef struct intset{//编码方式uint32_t encoding;//数组长度,即是集合元素个数uint32_t length;//保存整数int8_t contents[];} intset;
  • contents是底层缓冲区实现,保存数据本身,其数据按照大小顺序排列。
  • encoding编码方式,标志数据是16位、32位还是64位编码方式。
  • length表示contents中存储整数的个数。

下图是一个例子:

这里写图片描述
当向集合中添加新元素,如果现有编码长度小于新元素的长度,就需要进行集合编码升级,升级分为3步:

  • 根据新编码方式,计算底层数组的长度,为新的数组分配内存空间。
  • 将原有数组中元素进行扩充编码方式,并放在心数组中合适位置。
  • 维护新数组的有序性,并将contents指向新数组,回收旧数组。

总结

  • 整数集合是集合键的底层实现之一
  • 其底层实现为数组,数组以有序、无重复的方式保存几个元素
  • 整数集合支持自动升级编码方式
  • 整数集合不支持降级操作

6.跳跃表

在介绍跳跃表之前,先考虑一个问题?如何维护一个有序的链表?

常规的方式就是在插入一个元素时,从头遍历链表进行大小比较,选择合适的位置进行插入,其时间复杂度是O(n),那么怎么借助于链表的有序性减少比较的次数?链表的内存非连续性二分法是无法实现的,但可不可以借助于二分法思想进行关键点比较过滤掉一些无效的数值比较?这就是跳跃表的核心思想。其原理如下

在这里插入图片描述

跳跃表分为多个层,每个层之间是一个有序链表,上层元素有一个指针指向下层元素,查找和插入操作自上而下遍历,同一层自左向右遍历。

对于一个有序链表1->3->5->7->9->11,按照上面的想法,我们是否可以借助索引的思想,将链表中一些关键的节点提取出来(此处我们提取出1、5、9)作为一个有序链表,当我们插入元素8时,只需要比较节点5、9之间的元素就可以确定元素8的插入位置,从而省略1-5和9-∞的元素的比较。

更近一步,我们是不是可以将链表1->5->9继续提出关键节点作为更上一层,从而进一步缩减比较次数。(理论上最上一次只有2个节点)

对上上述链表,其效果不是很明显,如果对于10w甚至更长的链表其比较次数将大大降低。

但是同时从上述结构可以看出,跳跃链表是用空间获取时间。

redis中跳跃表结构如下

/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {// member 对象robj *obj;// 分值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中常用的5中底层数据结构,其作为五大对象Object的底层实现基本数据结构,理解其原理很容易理解Object实现原理以及常用命令的实现原理。接下来文章将介绍五大对象stringsetsortedsethashlist的实现原理。

参考文献《Redis设计与实现(第二版)》

这篇关于REDIS系列之底层数据结构的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1133156

相关文章

Redis主从/哨兵机制原理分析

《Redis主从/哨兵机制原理分析》本文介绍了Redis的主从复制和哨兵机制,主从复制实现了数据的热备份和负载均衡,而哨兵机制可以监控Redis集群,实现自动故障转移,哨兵机制通过监控、下线、选举和故... 目录一、主从复制1.1 什么是主从复制1.2 主从复制的作用1.3 主从复制原理1.3.1 全量复制

Redis延迟队列的实现示例

《Redis延迟队列的实现示例》Redis延迟队列是一种使用Redis实现的消息队列,本文主要介绍了Redis延迟队列的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习... 目录一、什么是 Redis 延迟队列二、实现原理三、Java 代码示例四、注意事项五、使用 Redi

Redis缓存问题与缓存更新机制详解

《Redis缓存问题与缓存更新机制详解》本文主要介绍了缓存问题及其解决方案,包括缓存穿透、缓存击穿、缓存雪崩等问题的成因以及相应的预防和解决方法,同时,还详细探讨了缓存更新机制,包括不同情况下的缓存更... 目录一、缓存问题1.1 缓存穿透1.1.1 问题来源1.1.2 解决方案1.2 缓存击穿1.2.1

redis-cli命令行工具的使用小结

《redis-cli命令行工具的使用小结》redis-cli是Redis的命令行客户端,支持多种参数用于连接、操作和管理Redis数据库,本文给大家介绍redis-cli命令行工具的使用小结,感兴趣的... 目录基本连接参数基本连接方式连接远程服务器带密码连接操作与格式参数-r参数重复执行命令-i参数指定命

深入理解Redis大key的危害及解决方案

《深入理解Redis大key的危害及解决方案》本文主要介绍了深入理解Redis大key的危害及解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着... 目录一、背景二、什么是大key三、大key评价标准四、大key 产生的原因与场景五、大key影响与危

Redis主从复制的原理分析

《Redis主从复制的原理分析》Redis主从复制通过将数据镜像到多个从节点,实现高可用性和扩展性,主从复制包括初次全量同步和增量同步两个阶段,为优化复制性能,可以采用AOF持久化、调整复制超时时间、... 目录Redis主从复制的原理主从复制概述配置主从复制数据同步过程复制一致性与延迟故障转移机制监控与维

Redis过期键删除策略解读

《Redis过期键删除策略解读》Redis通过惰性删除策略和定期删除策略来管理过期键,惰性删除策略在键被访问时检查是否过期并删除,节省CPU开销但可能导致过期键滞留,定期删除策略定期扫描并删除过期键,... 目录1.Redis使用两种不同的策略来删除过期键,分别是惰性删除策略和定期删除策略1.1惰性删除策略

Linux(Centos7)安装Mysql/Redis/MinIO方式

《Linux(Centos7)安装Mysql/Redis/MinIO方式》文章总结:介绍了如何安装MySQL和Redis,以及如何配置它们为开机自启,还详细讲解了如何安装MinIO,包括配置Syste... 目录安装mysql安装Redis安装MinIO总结安装Mysql安装Redis搜索Red

Redis连接失败:客户端IP不在白名单中的问题分析与解决方案

《Redis连接失败:客户端IP不在白名单中的问题分析与解决方案》在现代分布式系统中,Redis作为一种高性能的内存数据库,被广泛应用于缓存、消息队列、会话存储等场景,然而,在实际使用过程中,我们可能... 目录一、问题背景二、错误分析1. 错误信息解读2. 根本原因三、解决方案1. 将客户端IP添加到Re

详谈redis跟数据库的数据同步问题

《详谈redis跟数据库的数据同步问题》文章讨论了在Redis和数据库数据一致性问题上的解决方案,主要比较了先更新Redis缓存再更新数据库和先更新数据库再更新Redis缓存两种方案,文章指出,删除R... 目录一、Redis 数据库数据一致性的解决方案1.1、更新Redis缓存、删除Redis缓存的区别二