Redis底层数据结构

  • 一、简单动态字符串SDS
    • 1. SDS
    • 2. 为什么Redis没用C语言原生字符串?
      • 2.1 C语言中的字符串
      • 2.2 使用SDS的好处
  • 二、链表linkedlist
  • 三、压缩列表(ziplist)
    • 1. ziplist底层存储结构
    • 2. entry节点的内部结构
  • 四、字典dict
    • 1. 扩容与缩容
    • 2. 渐进式rehash
    • 3. 在rehash过程中数据如何存取
  • 五、整数集合intset
  • 六、跳表skiplist

在《Redis数据类型与应用场景》一文中介绍了Redis五大数据类型的使用以及各类的应用场景,今天来介绍一下Redis五大数据类型底层的数据结构的实现,这篇文章是第一节,关于底层数据结构的剖析,第二节会将底层数据结构与五大数据类型联系起来讲解

一、简单动态字符串SDS

1. SDS

SDS(simple dynamic string),即简单动态字符串的抽象类型,是Redis默认的字符串,其定义如下(这是3.2之前的版本,后续深入讲解SDS的优化时会讲3.2之后的版本SDS的定义,目前只为了学习SDS大体的设计原理):

struct sdshdr{//记录buf数组中已使用的数量, 也为字符串长度int len;//记录 buf 数组中未使用的数量int free;//字节数组,用于保存字符串char buf[];
}

2. 为什么Redis没用C语言原生字符串?

Redis是由C语言开发的,一开始我也有疑问,为什么Redis没有用C语言原生的字符串来作为string类型的底层数据结构呢?那么首先要先了解一下C语言的字符串了

2.1 C语言中的字符串

所谓字符串本质上就是以\0(空字符)作为结尾的特殊字符数组,所以有以下几个点需要注意

① 当定义的字符数组最后没有以\0结尾且没有给定字符长度为实际长度加一时,它仅仅是字符数组而非字符串

# 它仅仅是一个字符数组而非字符串
char s1[] = {'h','e','l','l','0'};
# 这才是一个字符串
char s2[] = {'h','e','l','l','0', '\0'};

② 如果没有以\0结尾,那么你需要定义数组的长度比实际长度多一位

# 这也是一个字符串
char s3[6] = {'h','e','l','l','0'};
# 这仅仅是字符数组
char s4[5] = {'h','e','l','l','0'};

③ 直接定义字符串

char s5 = "hello";

以上不管是哪种方式,在C的底层,都会为这段字符串加上\0代表一个字符串的结尾

2.2 使用SDS的好处

  1. 二进制安全
    Redis不仅可以存储文本,还可以存储二进制文件,例如图片,而图片的内容很可能包含\0,如果用C语言原生的字符串表示,那遇到\0后会认为字符串结束了,导致无法正常获取。而SDS虽然底层也采用了char数组且以\0结尾,但并非以\0来确定字符串是否结束的,而是以len属性来判断字符串是否结束,所以可以确保二进制数据安全
  2. 查询字符串长度迅速
    C语言中,查询字符串的长度,往往采用循环的方式,如此,时间复杂度为O(N),而SDS封装了char并扩展了len属性,用它作为字符串长度,查询是直接返回即可,时间复杂度为O(1)。可以使用命令查看string类型的值的字符串长度
127.0.0.1:6379> set key str
OK
127.0.0.1:6379> strlen key
(integer) 3
  1. 减少内存重新分配次数
    C语言在对字符串操作前,需要进行至少一次的内存分配,而C语言也并不会记录字符串的长度,所以修改也是会进行内存重新分配的,如果不重新分配,字符串变长就会导致内存溢出,如果字符串变短,那么会有内存泄漏的问题,而内存的分配是比较耗费性能的,针对这一弊端,SDS进行了优化,利用属性lenfree设计了空间预分配和惰性空间释放机制。
    1. 空间预分配: 所谓预分配,也就是说在一次扩展操作中,扩展的空间大小会大于实际需要的空间大小,这样就可以减少连续对字符串进行增长操作时,对内存的重分配次数。
    1.1 预分配规则:
    SDS len<1M:分配len长度空间作为预分配空间,即长度翻倍;
    SDS len>=1M:分配1M空间作为预分配空间,即多分配1M长度;
    这样,在下次进行字符操作的时候,如果所需要的空间小于当前SDS free空间,则可以直接行操作,而不需要再执行内存扩展,重分配操作。如此一来,使得扩展操作所需的内存重分配次数变为<=1
    2. 惰性空间释放: 直白地说,当字符串有部分删除的时候,并不会立即执行内存重新分配,而是更改free属性的值,释放的内存都赋给free,以备下次字符串扩展的时候使用。此外Redis也提供了主动释放未使用内存的方法,还是比较灵活的
  2. 缓冲区内存溢出问题规避
    内存溢出即分配内存小于实际需要内存,C语言在对字符串进行扩展时,要特别注意内存分配情况,比如两个字符串拼接,要严格保证内存足够。而SDS在进行字符串修改的时候,会判断添加的字符串的长度加上原字符串的长度是否小于原字符串本身的内存长度,如果小,则直接拼接返回,如果大,则根据预分配规则进行自动扩容

二、链表linkedlist

linkedlist是一个标准的双向链表数据结构,其定义如下:

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

其特性如下:
        ① 双向:listNode具有前后指针
        ② 记录长度:通过len属性记录了链表长度,时间复杂度为O(1)
        ③ 值可存任意类型:可以看到listNodevalue采用指针的方式,可以保存不同类型的值
        ④ 保存双端:获取头和为的时间复杂度为O(1)

三、压缩列表(ziplist)

压缩列表并不是对数据利用某种算法进行压缩,而是将数据按照一定规则编码在一块连续的内存区域,目的是节省内存。官网对ziplist的定义如下:

ziplist是一种特殊编码的双链表,它的设计非常高效。它存储字符串和整数值,其中整数被编码为实际整数而不是一系列字符。它允许在O(1)时间内对列表的任一侧执行推送和弹出操作。但是,由于每个操作都需要重新分配ziplist使用的内存,因此实际的复杂性与ziplist使用的内存量有关

ziplist 是由一系列特殊编码的内存块构成的列表(像内存连续的数组,但每个元素长度不同), 一个 ziplist 可以包含多个节点(entry)ziplist将表中每一项存放在前后连续的地址空间内,每一项因占用的空间不同,而采用变长编码。
当元素个数较少时,Redisziplist 来存储数据,当元素个数超过某个值时,链表键中会把 ziplist 转化为 linkedlist

1. ziplist底层存储结构

字段 类型 长度 作用
zlbytes nint32_t 4字节 记录整个压缩列表占用的内存字节数,在对压缩列表进行内存重新分配或者计算zlend位置的时候使用
zltail nint32_t 4字节 记录到达尾节点的偏移量,通过偏移量可以在不遍历所有节点的情况下,直接找到尾节点
zllen nint16_t 2字节 ziplist中节点的数量。当这个值小于65535时,通过它就可以得出列表中的节点数量,但是当这个值等于65535时,就需要通过实际遍历计数得出列表节点数了
entry? 列表节点 不定长 压缩列表中的各个节点,数据存储于节点之上,由于数据内容不同,所以长度不定,采用了变长编码
zlend nint8_t 1字节 记录压缩列表的末端

2. entry节点的内部结构


previous_entry_length: 记录压缩列表前一个节点的字节长度,通过该值可以算出前一个节点的位置,用于从尾节点向前遍历。当前指针位置减去前一个节点的长度就是前一个节点的位置。

previous_entry_length是变长编码,有两种表示方法:
如果前一节点的长度小于 254 字节,则使用1字节(uint8_t)来存储prevrawlen;
如果前一节点的长度大于等于 254 字节,那么将第 1 个字节的值设为 254 ,然后用接下来的 4 个字节保存实际长度。

encoding/len: 当前节点的编码类型以及字节长度,用来解析content用的。encoding类型一共有两种,一种字节数组一种是整数,encoding区域长度为1字节、2字节或者5字节长
content: 保存了当前节点的值

四、字典dict

字典是一种存储键值的抽象数据结构,学过Java的朋友应该都知道HashMapHashtable,如果你知道这个,那么理解起来Redis的字典可以说毫不费力。
Redis中的字典以哈希表为底层数据结构,其定义如下:

typedef struct dictht{//哈希表数组dictEntry **table;//哈希表大小unsigned long size;//哈希表大小掩码,用于计算索引值//总是等于 size-1unsigned long sizemask;//该哈希表已有节点的数量unsigned long used;}dictht

由代码可以知道,哈希表是由dictEntry类型的数组组成的,dictEntry的结构如下:

typedef struct dictEntry{//键void *key;//值union{void *val;uint64_tu64;int64_ts64;}v;//指向下一个哈希表节点,形成链表struct dictEntry *next;
}dictEntry

看到这里,想必学Java的同学已经非常通透了,数据结构与HashMap非常类似,采用数组加链表的形式,解决哈希冲突也是利用了链地址法,且采用头插法,不了解的可以看下这篇HashMap底层原理

1. 扩容与缩容

当哈希表保存的键值对太多或者太少时,就要通过 rerehash(重新散列)来对哈希表进行相应的扩展或者收缩。步骤如下:

  1. 如果执行扩展操作,会基于原哈希表创建一个大小等于 原哈希表已使用的空间的2倍大小的哈希表。相反如果执行的是缩容操作,每次收缩是根据已使用空间缩小一倍创建一个新的哈希表。
  2. 重新利用哈希算法,计算索引值,然后将键值对放到新的哈希表位置上。
  3. 所有键值对都迁徙完毕后,释放原哈希表的内存空间

2. 渐进式rehash

上述介绍了扩容与缩容,那么Redis并不是一次性将数据迁移到新哈希表的,这也是考虑到了性能问题,如果我们的数据量小还好,一次性迁移,对性能没什么影响,那如果就几十万几百万的数据,进行一次性迁移,肯定会影响到性能。所以Redis采用了渐进式rehash,也就是迁移分为多次渐进式完成,有两种迁移策略:

  1. 主动:当访问老记录时,则迁移一部分,不是按访问顺序进行迁移的,它有一定的迁移顺序
  2. 事件轮询:以时间轮询的方式触发迁移,每次迁移一批

3. 在rehash过程中数据如何存取

查询可能会设计两张哈希表,先从旧哈希表查,如果查不到,则会从新哈希表查。而插入数据只会往新的哈希表添加

五、整数集合intset

整数集合(intset)是Redis用于保存整数值的集合抽象数据类型,它可以保存类型为int16_t、int32_t 或者int64_t 的整数值,并且保证集合中不会出现重复元素,其定义如下:

typedef struct intset{//编码方式uint32_t encoding;//集合包含的元素数量uint32_t length;//保存元素的数组int8_t contents[];
}intset;

contents[]: 整数集合的每个元素都是 contents 数组的一个数据项,它们按照从小到大的顺序排列,并且不包含任何重复项。虽然该数组生命为int8_t,但实际上并不保存任何int8_t类型的值,具体的类型是由属性encoding来决定的,而encoding提供了三个值,分别为INTSET_ENC_INT16INTSET_ENC_INT32INTSET_ENC_INT64

length: 记录了 contents 数组的大小。

注意: 当我们新加入的元素的长度大于集合原来的元素长度时,需要对整数集合进行升级,根据新元素类型扩展整数集合中数组的大小,给新元素分配空间,将数组原有元素逐个转换成与新元素类型相同的元素存入数组,最后将新元素加入,这种升级是不可逆操作,一旦升级只能保持升级后的状态

六、跳表skiplist

在我看来,跳表是对标准链表的一种优化,我们都知道链表的查询时间复杂度是O(N),而将链表优化为跳表后,查询便趋近于二分查找。我们先来看一下它的结构:

        跳表的数据结构有很多层,最底层链表包含了所有元素,每个节点都有两个指针,一个指向同层下一个节点,一个指向下一层同一个节点,我们看图可以理解为将节点向上冗余,做出索引层,仔细思考,有点二分搜索树的味道,如此一来,可以让查询趋近于二分查找,而且跳表还支持区间查找

跳表定义如下:

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

多个跳表节点构成跳表,跳表节点定义如下:

typedef struct zskiplistNode {//层struct zskiplistLevel{//前进指针struct zskiplistNode *forward;//跨度unsigned int span;}level[];//后退指针struct zskiplistNode *backward;//分值double score;//成员对象robj *obj;
} zskiplistNode

① 搜索:搜索还是比较好理解的,从最高层开始检索,如果比当前层的当前节点大并且比当前层的当前节点的下一个节点小,那么就向下寻找,也就是与当前层的当前节点的下一层的下一个节点比较,以此类推。例如,寻找节点11,先从最高层节点3开始,发现3<7<16,那么就向下一层找,与下一层的节点3的下一个节点7比较,发现比7大,然后将7看作当前节点,重复上述步骤,即7<11<16,继续与下一层的相同节点的下一个节点11比较,相等,返回

② 删除:在各个层中找到包含指定值的节点,然后将节点从链表中删除即可,如果删除以后只剩下头尾两个节点,则删除这一层。

③插入:跳表的插入比价复杂,选择在哪一层插入是随机的,有一种方法是假设抛一枚硬币,如果是正面就累加,直到遇见反面为止,最后记录正面的次数作为插入的层数。比如确定插入第x层,然后找到新节点在每一层的上一个节点,将新节点插入到每一层的上一个节点和下一个节点之间,插入是在底层到x层都发生的。

有关跳表的增删查我只能说这么多了,简单理解一下原理吧,这块比较复杂,我还没有那么多精力去深入了解,毕竟学Java不想看那么多C的代码哈哈哈

参考文章:
https://blog.csdn.net/u013536232/article/details/105476382/
https://zhuanlan.zhihu.com/p/102422311
https://www.cnblogs.com/ysocean/p/9080942.html#_label6
https://blog.csdn.net/qq193423571/article/details/81637075

Redis底层数据结构详解(一)相关推荐

  1. Redis底层数据结构详解

    Redis底层数据结构详解 我们知道Redis常用的数据结构有五种,String.List.Hash.Set.ZSet,其他的集中数据结构基本上也是用这五种实现的,那么,这五种是Redis提供给你的数 ...

  2. redis 底层数据结构详解

    目录 1.字符串 1.1 SDS定义 1.2 SDS1好处 2.列表 2.1 void 实现多态 3 字典 3.1   底层实现是hash表 3.2 字典结构 3.3 哈希算法 3.3.1 rehas ...

  3. [转]Redis内部数据结构详解-sds

    本文是<Redis内部数据结构详解>系列的第二篇,讲述Redis中使用最多的一个基础数据结构:sds. 不管在哪门编程语言当中,字符串都几乎是使用最多的数据结构.sds正是在Redis中被 ...

  4. 探索Redis设计与实现6:Redis内部数据结构详解——skiplist

    Redis内部数据结构详解(6)--skiplist  2016-10-05 本文是<Redis内部数据结构详解>系列的第六篇.在本文中,我们围绕一个Redis的内部数据结构--skipl ...

  5. Redis内部数据结构详解(2)——skiplist

    Redis里面使用skiplist是为了实现sorted set这种对外的数据结构.sorted set提供的操作非常丰富,可以满足非常多的应用场景.这也意味着,sorted set相对来说实现比较复 ...

  6. Redis内部数据结构详解之简单动态字符串(sds)

    本文所引用的源码全部来自Redis2.8.2版本. Redis中简单动态字符串sds数据结构与API相关文件是:sds.h, sds.c. 转载请注明,本文出自:http://blog.csdn.ne ...

  7. MySQL索引底层数据结构详解

    索引是帮助MySQL高效获取数据的排好序的数据结构 索引的数据结构: 1.二叉树 通过一个简单的插入你可以看到,二叉树的插入会根据每个节点进行判断,每一个节点右边的数据一定是大于等于这个节点数据,而他 ...

  8. Redis数据类型使用场景及有序集合SortedSet底层实现详解

    Redis常用数据类型有字符串String.字典dict.列表List.集合Set.有序集合SortedSet,本文将简单介绍各数据类型及其使用场景,并重点剖析有序集合SortedSet的实现. Li ...

  9. 万字长文的Redis五种数据结构详解(理论+实战),建议收藏。

    本文脑图 前言 Redis是基于c语言编写的开源非关系型内存数据库,可以用作数据库.缓存.消息中间件,这么优秀的东西一定要一点一点的吃透它. 关于Redis的文章之前也写过三篇,阅读量和读者的反映都还 ...

最新文章

  1. java 人事_java版简易人事管理系统
  2. JS——“==”与“===”
  3. 6工程文件夹作用_MCUXpresso IDE下SDK工程导入与workspace管理机制
  4. 鸿蒙系统适配机型_华为鸿蒙 OS 适配机型曝光,除了 Mate 40 还有这几款!
  5. 前端学习(1222):综合案例图书管理1
  6. 【讲师专访】Oracle ACE 总监侯圣文:不懂开发的运维终将被淘汰
  7. leetcode java输入输出方法,有关IntelliJ IDEA中LeetCode插件配置问题
  8. bcd转ascii码 流程图_4-20mA转RS485,MODBUS数据采集模块
  9. System.Data.OleDb.OleDbException: 未指定的错误的解决方法
  10. innodb_pool_buffer_size对innodb性能的影响
  11. LINUX 软件安装。
  12. spss多元线性回归散点图_SPSS线性回归|别人不想告诉你的其他操作我都总结好了(中)...
  13. python 面向对象编程、别人么样用_Python 中的面向对象没有意义
  14. gopup是疫情经济生活搜索指数数据接口
  15. POJ3737UmBasketella
  16. 学在信息——一方豪杰
  17. 组合数学——二项式反演
  18. 微信小程序——服务通知,发送订阅消息
  19. mysql用sql语句将表中学生_用sql语句创建学生表如何做
  20. 中国企业家:乔布斯给中国CEO的三堂必修课

热门文章

  1. python基础5-模块定义、导入方法、import本质、time和datetime、random、os、sys、shutil、shelve、xml、ConfigParser、hashlib、re
  2. 企业微信客户端开启调试模式
  3. php多表查询性能优化,MSSQL_SQL Server多表查询优化方案集锦,SQL Server多表查询的优化方案是 - phpStudy...
  4. 一文教你看懂Fama-French三因子模型
  5. 对逻辑斯蒂回归的一些细节剖析
  6. 手机APP开发之MIT Appinventor详细实战教程(一),利用通过蓝牙控制单片机,以及实现单片机与android设备之间的串口通信
  7. java 命令: jmap 命令使用 ( 查看内存使用、设置 )
  8. List 过滤、排序、校验等处理方法
  9. 装了4亿篇档案的AI和人辩论谁赢了?IBM最强AI辩手首次登上《自然》封面
  10. IDEA的使用大全(快捷键、TomCat、Maven......)