面试中,redis也是很受面试官亲睐的一部分。我向在这里讲的是redis的底层数据结构,而不是你理解的五大数据结构。你有没有想过redis底层是怎样的数据结构呢,他们和我们java中的HashMap、List、等使用的数据结构有什么区别呢。

1. 字符串处理(string)

我们都知道redis是用C语言写,但是C语言处理字符串和数组的成本是很高的,下面我分别说几个例子。

没有数据结构支撑的几个问题

  1. 及其容易造成缓冲区溢出问题,比如用strcat(),在用这个函数之前必须要先给目标变量分配足够的空间,否则就会溢出。
  2. 如果要获取字符串的长度,没有数据结构的支撑,可能就需要遍历,它的复杂度是O(N)
  3. 内存重分配。C字符串的每次变更(曾长或缩短)都会对数组作内存重分配。同样,如果是缩短,没有处理好多余的空间,也会造成内存泄漏。

好了,Redis自己构建了一种名叫Simple dynamic string(SDS)的数据结构,他分别对这几个问题作了处理。我们先来看看它的结构源码:

  1. struct sdshdr{
  2. //记录buf数组中已使用字节的数量
  3. //等于 SDS 保存字符串的长度
  4. int len;
  5. //记录 buf 数组中未使用字节的数量
  6. int free;
  7. //字节数组,用于保存字符串
  8. char buf[];
  9. }

再来说说它的优点:

  1. 开发者不用担心字符串变更造成的内存溢出问题。
  2. 常数时间复杂度获取字符串长度len字段
  3. 空间预分配free字段,会默认留够一定的空间防止多次重分配内存。

更多了解:https://redis.io/topics/internals-sds

这就是string的底层实现,更是redis对所有字符串数据的处理方式(SDS会被嵌套到别的数据结构里使用)。

2. 链表

Redis的链表在双向链表上扩展了头、尾节点、元素数等属性。

2.1 源码

ListNode节点数据结构:

  1. typedef struct listNode{
  2. //前置节点
  3. struct listNode *prev;
  4. //后置节点
  5. struct listNode *next;
  6. //节点的值
  7. void *value;
  8. }listNode

链表数据结构:

  1. typedef struct list{
  2. //表头节点
  3. listNode *head;
  4. //表尾节点
  5. listNode *tail;
  6. //链表所包含的节点数量
  7. unsigned long len;
  8. //节点值复制函数
  9. void (*free) (void *ptr);
  10. //节点值释放函数
  11. void (*free) (void *ptr);
  12. //节点值对比函数
  13. int (*match) (void *ptr,void *key);
  14. }list;

从上面可以看到,Redis的链表有这几个特点:

  1. 可以直接获得头、尾节点。
  2. 常数时间复杂度得到链表长度。
  3. 是双向链表。

3. 字典(Hash)

Redis的Hash,就是在数组+链表的基础上,进行了一些rehash优化等。

3.1 数据结构源码

哈希表:

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

Hash表节点:

  1. typedef struct dictEntry {
  2. // 键
  3. void *key;
  4. // 值
  5. union {
  6. void *val;
  7. uint64_t u64;
  8. int64_t s64;
  9. } v;
  10. // 指向下个哈希表节点,形成链表
  11. struct dictEntry *next; // 单链表结构
  12. } dictEntry;

字典:

  1. typedef struct dict {
  2. // 类型特定函数
  3. dictType *type;
  4. // 私有数据
  5. void *privdata;
  6. // 哈希表
  7. dictht ht[2];
  8. // rehash 索引
  9. // 当 rehash 不在进行时,值为 -1
  10. int rehashidx; /* rehashing not in progress if rehashidx == -1 */
  11. } dict;

可以看出:

  1. Reids的Hash采用链地址法来处理冲突,然后它没有使用红黑树优化。
  2. 哈希表节点采用单链表结构。
  3. rehash优化。

下面我们讲一下它的rehash优化。

3.2 rehash

当哈希表的键对泰国或者太少,就需要对哈希表的大小进行调整,redis是如何调整的呢?

  1. 我们仔细可以看到dict结构里有个字段dictht ht[2]代表有两个dictht数组。第一步就是为ht[1]哈希表分配空间,大小取决于ht[0]当前使用的情况。
  2. 将保存在ht[0]中的数据rehash(重新计算哈希值)到ht[1]上。
  3. 当ht[0]中所有键值对都迁移到ht[1]后,释放ht[0],将ht[1]设置为ht[0],并ht[1]初始化,为下一次rehash做准备。

3.3 渐进式rehash

我们在3.2中看到,redis处理rehash的流程,但是更细一点的讲,它如何进行数据迁的呢?

这就涉及到了渐进式rehash,redis考虑到大量数据迁移带来的cpu繁忙(可能导致一段时间内停止服务),所以采用了渐进式rehash的方案。步骤如下:

  1. 为ht[1]分配空间,同时持有两个哈希表(一个空表、一个有数据)。
  2. 维持一个技术器rehashidx,初始值0。
  3. 每次对字典增删改查,会顺带将ht[0]中的数据迁移到ht[1],rehashidx++(注意:ht[0]中的数据是只减不增的)。
  4. 直到rehash操作完成,rehashidx值设为-1。

它的好处:采用分而治之的思想,将庞大的迁移工作量划分到每一次CURD中,避免了服务繁忙。

4. 跳跃表

这个数据结构是我面试中见过最多的,它其实特别简单。学过的人可能都知道,它和平衡树性能很相似,但为什么不用平衡树而用skipList呢?

4.1 skipList & AVL 之间的选择

  1. 从算法实现难度上来比较,skiplist比平衡树要简单得多。
  2. 平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。
  3. 查找单个key,skiplist和平衡树的时间复杂度都为O(log n),大体相当。
  4. 在做范围查找的时候,平衡树比skiplist操作要复杂。
  5. skiplist和各种平衡树(如AVL、红黑树等)的元素是有序排列的。

可以看到,skipList中的元素是有序的,所以跳跃表在redis中用在有序集合键、集群节点内部数据结构

4.2 源码

跳跃表节点:

  1. typedef struct zskiplistNode {
  2.  
  3. // 后退指针
  4. struct zskiplistNode *backward;
  5.  
  6. // 分值
  7. double score;
  8.  
  9. // 成员对象
  10. robj *obj;
  11.  
  12. // 层
  13. struct zskiplistLevel {
  14.  
  15. // 前进指针
  16. struct zskiplistNode *forward;
  17.  
  18. // 跨度
  19. unsigned int span;
  20.  
  21. } level[];
  22.  
  23. } zskiplistNode;

跳跃表:

  1. typedef struct zskiplist {
  2.  
  3. // 表头节点和表尾节点
  4. struct zskiplistNode *header, *tail;
  5.  
  6. // 表中节点的数量
  7. unsigned long length;
  8.  
  9. // 表中层数最大的节点的层数
  10. int level;
  11.  
  12. } zskiplist;

它有几个概念:

4.2.1 层(level[])

层,也就是level[]字段,层的数量越多,访问节点速度越快。(因为它相当于是索引,层数越多,它索引就越细,就能很快找到索引值)

4.2.2 前进指针(forward)

层中有一个forward字段,用于从表头向表尾方向访问。

4.2.3 跨度(span)

用于记录两个节点之间的距离

4.2.4 后退指针(backward)

用于从表尾向表头方向访问。

案例

  1. level0 1---------->5
  2. level1 1---->3---->5
  3. level2 1->2->3->4->5->6->7->8

比如我要找键为6的元素,在level0中直接定位到5,然后再往后走一个元素就找到了。

5. 整数集合(intset)

Reids对整数存储专门作了优化,intset就是redis用于保存整数值的集合数据结构。当一个结合中只包含整数元素,redis就会用这个来存储。

  1. 127.0.0.1:6379[2]> sadd number 1 2 3 4 5 6
  2. (integer) 6
  3. 127.0.0.1:6379[2]> object encoding number
  4. "intset"

源码

intset数据结构:

  1. typedef struct intset {
  2.  
  3. // 编码方式
  4. uint32_t encoding;
  5.  
  6. // 集合包含的元素数量
  7. uint32_t length;
  8.  
  9. // 保存元素的数组
  10. int8_t contents[];
  11.  
  12. } intset;

你肯定很好奇编码方式(encoding)字段是干嘛用的呢?

  • 如果 encoding 属性的值为 INTSET_ENC_INT16 , 那么 contents 就是一个 int16_t 类型的数组, 数组里的每个项都是一个 int16_t 类型的整数值 (最小值为 -32,768 ,最大值为 32,767 )。
  • 如果 encoding 属性的值为 INTSET_ENC_INT32 , 那么 contents 就是一个 int32_t 类型的数组, 数组里的每个项都是一个 int32_t 类型的整数值 (最小值为 -2,147,483,648 ,最大值为 2,147,483,647 )。
  • 如果 encoding 属性的值为 INTSET_ENC_INT64 , 那么 contents 就是一个 int64_t 类型的数组, 数组里的每个项都是一个 int64_t 类型的整数值 (最小值为 -9,223,372,036,854,775,808 ,最大值为 9,223,372,036,854,775,807 )。

说白了就是根据contents字段来判断用哪个int类型更好,也就是对int存储作了优化。

说到优化,那redis如何作的呢?就涉及到了升级。

5.1 encoding升级

如果我们有个Int16类型的整数集合,现在要将65535(int32)加进这个集合,int16是存储不下的,所以就要对整数集合进行升级。

它是怎么升级的呢(过程)?

假如现在有2个int16的元素:1和2,新加入1个int32位的元素65535。

  1. 内存重分配,新加入后应该是3个元素,所以分配3*32-1=95位。
  2. 选择最大的数65535, 放到(95-32+1, 95)位这个内存段中,然后2放到(95-32-32+1+1, 95-32)位...依次类推。
升级的好处是什么呢?
  1. 提高了整数集合的灵活性。
  2. 尽可能节约内存(能用小的就不用大的)。
5.2 不支持降级

按照上面的例子,如果我把65535又删掉,encoding会不会又回到Int16呢,答案是不会的。官方没有给出理由,我觉得应该是降低性能消耗吧,毕竟调整一次是O(N)的时间复杂度。

6. 压缩列表(ziplist)

ziplist是redis为了节约内存而开发的顺序型数据结构。它被用在列表键和哈希键中。一般用于小数据存储。

引用https://segmentfault.com/a/1190000016901154中的两个图:

6.1 源码

ziplist没有明确定义结构体,这里只作大概的演示。

  1. typedef struct entry {
  2. /*前一个元素长度需要空间和前一个元素长度*/
  3. unsigned int prevlengh;
  4. /*元素内容编码*/
  5. unsigned char encoding;
  6. /*元素实际内容*/
  7. unsigned char *data;
  8. }zlentry;
  1. typedef struct ziplist{
  2. /*ziplist分配的内存大小*/
  3. uint32_t zlbytes;
  4. /*达到尾部的偏移量*/
  5. uint32_t zltail;
  6. /*存储元素实体个数*/
  7. uint16_t zllen;
  8. /*存储内容实体元素*/
  9. unsigned char* entry[];
  10. /*尾部标识*/
  11. unsigned char zlend;
  12. }ziplist;

第一次看可能会特别蒙蔽,你细细的把我这段话看完就一定能懂。

Entry的分析

entry结构体里面有三个重要的字段:

  1. previous_entry_length: 这个字段记录了ziplist中前一个节点的长度,什么意思?就是说通过该属性可以进行指针运算达到表尾向表头遍历,这个字段还有一个大问题下面会讲。
  2. encoding:记录了数据类型(int16? string?)和长度。
  3. data/content: 记录数据。
连锁更新
previous_entry_length字段的分析

上面有说到,previous_entry_length这个字段存放上个节点的长度,那默认长度给分配多少呢?redis是这样分的,如果前节点长度小于254,就分配1字节,大于的话分配5字节,那问题就来了。

如果前一个节点的长度刚开始小于254字节,后来大于254,那不就存放不下了吗? 这就涉及到previous_entry_length的更新,但是改一个肯定不行阿,后面的节点内存信息都需要改。所以就需要重新分配内存,然后连锁更新包括该受影响节点后面的所有节点。

除了增加新节点会引发连锁更新、删除节点也会触发。

7. 快速列表(quicklist)

一个由ziplist组成的双向链表。但是一个quicklist可以有多个quicklist节点,它很像B树的存储方式。是在redis3.2版本中新加的数据结构,用在列表的底层实现。

结构体源码

表头结构:

  1. typedef struct quicklist {
  2. //指向头部(最左边)quicklist节点的指针
  3. quicklistNode *head;
  4.  
  5. //指向尾部(最右边)quicklist节点的指针
  6. quicklistNode *tail;
  7.  
  8. //ziplist中的entry节点计数器
  9. unsigned long count; /* total count of all entries in all ziplists */
  10.  
  11. //quicklist的quicklistNode节点计数器
  12. unsigned int len; /* number of quicklistNodes */
  13.  
  14. //保存ziplist的大小,配置文件设定,占16bits
  15. int fill : 16; /* fill factor for individual nodes */
  16.  
  17. //保存压缩程度值,配置文件设定,占16bits,0表示不压缩
  18. unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
  19. } quicklist;

quicklist节点结构:

  1. typedef struct quicklistNode {
  2. struct quicklistNode *prev; //前驱节点指针
  3. struct quicklistNode *next; //后继节点指针
  4.  
  5. //不设置压缩数据参数recompress时指向一个ziplist结构
  6. //设置压缩数据参数recompress指向quicklistLZF结构
  7. unsigned char *zl;
  8.  
  9. //压缩列表ziplist的总长度
  10. unsigned int sz; /* ziplist size in bytes */
  11.  
  12. //ziplist中包的节点数,占16 bits长度
  13. unsigned int count : 16; /* count of items in ziplist */
  14.  
  15. //表示是否采用了LZF压缩算法压缩quicklist节点,1表示压缩过,2表示没压缩,占2 bits长度
  16. unsigned int encoding : 2; /* RAW==1 or LZF==2 */
  17.  
  18. //表示一个quicklistNode节点是否采用ziplist结构保存数据,2表示压缩了,1表示没压缩,默认是2,占2bits长度
  19. unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
  20.  
  21. //标记quicklist节点的ziplist之前是否被解压缩过,占1bit长度
  22. //如果recompress为1,则等待被再次压缩
  23. unsigned int recompress : 1; /* was this node previous compressed? */
  24.  
  25. //测试时使用
  26. unsigned int attempted_compress : 1; /* node can't compress; too small */
  27.  
  28. //额外扩展位,占10bits长度
  29. unsigned int extra : 10; /* more bits to steal for future usage */
  30. } quicklistNode;
相关配置

在redis.conf中的ADVANCED CONFIG部分:

  1. list-max-ziplist-size -2
  2. list-compress-depth 0
list-max-ziplist-size参数

我们来详细解释一下list-max-ziplist-size这个参数的含义。它可以取正值,也可以取负值。

当取正值的时候,表示按照数据项个数来限定每个quicklist节点上的ziplist长度。比如,当这个参数配置成5的时候,表示每个quicklist节点的ziplist最多包含5个数据项。

当取负值的时候,表示按照占用字节数来限定每个quicklist节点上的ziplist长度。这时,它只能取-1到-5这五个值,每个值含义如下:

-5: 每个quicklist节点上的ziplist大小不能超过64 Kb。(注:1kb => 1024 bytes)

-4: 每个quicklist节点上的ziplist大小不能超过32 Kb。

-3: 每个quicklist节点上的ziplist大小不能超过16 Kb。

-2: 每个quicklist节点上的ziplist大小不能超过8 Kb。(-2是Redis给出的默认值)

list-compress-depth参数

这个参数表示一个quicklist两端不被压缩的节点个数。注:这里的节点个数是指quicklist双向链表的节点个数,而不是指ziplist里面的数据项个数。实际上,一个quicklist节点上的ziplist,如果被压缩,就是整体被压缩的。

参数list-compress-depth的取值含义如下:

0: 是个特殊值,表示都不压缩。这是Redis的默认值。 1: 表示quicklist两端各有1个节点不压缩,中间的节点压缩。 2: 表示quicklist两端各有2个节点不压缩,中间的节点压缩。 3: 表示quicklist两端各有3个节点不压缩,中间的节点压缩。 依此类推…

Redis对于quicklist内部节点的压缩算法,采用的LZF——一种无损压缩算法。

面试官:你看过Redis数据结构底层实现吗?的更多相关文章

  1. 面试官:你对Redis缓存了解吗?面对这11道面试题你是否有很多问号?

    前言 关于Redis的知识,总结了一个脑图分享给大家 1.在项目中缓存是如何使用的?为什么要用缓存?缓存使用不当会造成什么后果? 面试官心理分析 这个问题,互联网公司必问,要是一个人连缓存都不太清楚, ...

  2. Redis数据结构底层知识总结

    Redis数据结构底层总结 本篇文章是基于作者黄建宏写的书Redis设计与实现而做的笔记 数据结构与对象 Redis中数据结构的底层实现包括以下对象: 对象 解释 简单动态字符串 字符串的底层实现 链 ...

  3. Redis专题(2):Redis数据结构底层探秘

    前言 上篇文章Redis闲谈(1):构建知识图谱介绍了redis的基本概念.优缺点以及它的内存淘汰机制,相信大家对redis有了初步的认识.互联网的很多应用场景都有着Redis的身影,它能做的事情远远 ...

  4. 面试官问我,Redis分布式锁如何续期?懵了。

    前言 上一篇[面试官问我,使用Dubbo有没有遇到一些坑?我笑了.]之后,又有一位粉丝和我说在面试过程中被虐了.鉴于这位粉丝是之前肥朝的粉丝,而且周一又要开启新一轮的面试,为了回馈他长期以来的支持,所 ...

  5. 面试官:你确定 Redis 是单线程的进程吗?

    作者:小林coding 计算机八股文网站:https://xiaolincoding.com 大家好,我是小林. 这次主要分享 Redis 线程模型篇的面试题. Redis 是单线程吗? Redis ...

  6. 面试官:“看你简历上写熟悉 Handler 机制,那聊聊 IdleHandler 吧?”

    一. 序 Handler 机制算是 Android 基本功,面试常客.但现在面试,多数已经不会直接让你讲讲 Handler 的机制,Looper 是如何循环的,MessageQueue 是如何管理 M ...

  7. 阿里P7岗位面试,面试官问我:为什么HashMap底层树化标准的元素个数是8

    前言 先声明一下,本文有点标题党了,像我这样的菜鸡何德何能去面试阿里的P7岗啊,不过,这确实是阿里p7级岗位的面试题,当然,参加面试的人不是我,而是我部门的一个大佬.他把自己的面试经验分享给了我,也让 ...

  8. 女朋友面试回来抱怨说会redis,面试官问了一堆redis

    Redis 优缺点及特点 什么是Redis?简述它的优缺点? Redis本质上是一个Key-Value类型的内存数据库,类似MemoryCache,整个数据库统统加载在内存当中进行操作,定期通过异步操 ...

  9. 面试官: 说说看, 什么是 Hook (钩子) 线程以及应用场景?

    文章首发自个人微信号: 小哈学Java 个人网站地址: https://www.exception.site/java-concurrency/java-concurrency-hook-thread ...

随机推荐

  1. 在项目中常用的JS方法封装

    使用方法简单,只需要放在你的 utils.js 工具文件中,直接export const 加上下面封装方法,在别的文件中使用 {方法1,方法2,方法3...}引用后直接使用即可. 01.输入一个值.返 ...

  2. TopCoder12727 「SRM590Hard」FoxAndCity 最小割离散变量模型

    问题描述 一张 \(N\) 个点无向图,边权都为 \(1\) ,添加若干条边,最小化 \(\sum\limits_{1 \le i \le n,i \in N_{+}}{(a_i-b_i)^2}\). ...

  3. RocketMQ 升级到主从切换(DLedger、多副本)实战

    目录 1.RocketMQ DLedger 多副本即主从切换核心配置参数详解 2.搭建主从同步环境 3.主从同步集群升级到DLedger 3.1 部署架构 3.2 升级步骤 3.3 验证消息发送与消息 ...

  4. Web前端基础(11):JavaScript(五)

    1. 初识DOM 1.1 DOM介绍 1.1.3 什么是DOM DOM:文档对象模型.DOM 为文档提供了结构化表示,并定义了如何通过脚本来访问文档结构.目的其实就是为了能让js操作html元素而制定 ...

  5. idea中tomcat乱码

    idea中tomcat乱码 解决方案: a. file - settings - 搜File Encodings,改为utf-8 b.打开idea工作目录bin,在idea64.exe.vmoptio ...

  6. 一文学会JVM性能优化

    实战性能优化 1 重新认知JVM 之前我们画过一张图,是从Class文件到类装载器,再到运行时数据区的过程,现在咱们把这张图不妨丰富完善一下,展示了JVM的大体物理结构图. 执行引擎:用于执行JVM字 ...

  7. JQuery iframe宽高度自适应浏览器窗口大小的解决方法

    iframe宽高度自适应浏览器窗口大小的解决方法   by:授客 QQ:1033553122 1.   测试环境 JQuery-3.2.1.min.js 下载地址: https://gitee.com ...

  8. Pandas处理超大规模数据

    对于超大规模的csv文件,我们无法一下将其读入内存当中,只能分块一部分一部分的进行读取: 首先进行如下操作: import pandas as pd reader = pd.read_csv('dat ...

  9. iOS----------如何给github的README添加图片

    1.在你的项目中建一个文件夹,专门存放图片.如果想简单操作的话,可以截个图直接拉到项目中. 2.将建好的文件夹上传到github进行同步 3.在github上找到图片的URL地址 4.按照如下格式   ...

  10. easyUI+servlet+mysql项目总结

    项目介绍 利用easyUI做前端框架,进行数据展示和用户数据收集 使用servlet做后端的控制层,并调用业务逻辑组件的业务逻辑方法,处理用户请求,根据不同处理结果返回不同的结果到前端 mysql进行 ...