Redis经典五大数据类型源码及底层实现
- 一 面试题引入
- 二 Redis数据类型的底层数据结构
- 三 redis是字典数据库,KV键值对到底是什么?
- 3.1 怎样实现键值对(key-value)数据库的?
- 3.2 redisObject结构的作用
- 3.3 RedisObject各字段的含义
- 四 经典5大数据类型结构解析
- 4.1 String数据结构介绍
- 4.1.1 3大物理编码方式
- 4.1.2 SDS简单动态字符串
- 4.1.3 Redis为什么重新设计一个SDS数据结构?
- 4.1.4 小总结
- 4.2 Hash数据结构介绍
- 4.2.1 Ziplist介绍
- 4.2.2 zlentry压缩列表节点的构成
- 4.2.3 明明有链表了,为什么出来一个压缩链表?
- 4.2.4 ziplist总结
- 4.2.5 listpack
- 4.2.6 listpack结构
- 4.2.7 ziplist内存布局VS listpack内存布局
- 4.3 List数据结构介绍
- 4.4 Set数据结构介绍
- 4.5 ZSet数据结构介绍
- 4.5.1 skiplist跳表
- 4.5.2 跳表时间+空间复杂度介绍
- 4.5.3 skiplist的优缺点
一 面试题引入
- Redis的跳跃列表了解吗?这个数据结构有什么缺点?
- redis的数据结构都了解哪些?布隆过滤器怎么用?
- redis的多路io复用如何理解?为什么单线程还可以抗那么高的qps
- redis的zset底层实现?
- redis的跳表说一下,解决了哪些问题?时间复杂度和空间复杂度如何?
- redis的zset用的什么数据结构?
二 Redis数据类型的底层数据结构
- SDS动态字符串
- 双向链表
- 压缩列表ziplist
- 哈希表hashtable
- 跳转skiplist
- 整数集合intset
- 快速列表quicklist
- 紧凑列表listpack
三 redis是字典数据库,KV键值对到底是什么?
3.1 怎样实现键值对(key-value)数据库的?
Redis是key-value存储系统
- key一般都是String类型的字符串对象
- value类型则是redis对象(redisObject):Value可以是字符串对象,也可以是集合数据类型的对象,比如List对象、Hash对象、Set对象和Zset对象。
在Redis7中涉及到listpack紧凑列表的调整,listpack是用来替代ziplist的心数据类型,在7.0版本已经没有ziplist的配置了(6.0版本仅部分数据类型作为过渡阶段在使用)listpack已经替换了ziplist,类似hash-max-ziplist-entries的配置。
3.2 redisObject结构的作用
为了便于操作,Redis采用redisObject结构来统一五种不同的数据类型,这样所有的数据类型都可以以相同的形式在函数间传递而不用使用特定的类型结构。同时,为了识别不同的数据类型,redisObject中定义了type和encoding字段对不同的数据类型加以区别。简单地说,redisObject 就是String、hash、list、set、zset的父类
。可以在函数间传递时隐藏具体的类型信息,所以作者抽象了redisObject结构来达到同样的目的。
3.3 RedisObject各字段的含义
四 经典5大数据类型结构解析
Debug Object key…
4.1 String数据结构介绍
4.1.1 3大物理编码方式
- int:保存long型(长整型)的64位(8个字节)有符号整数。 只有整数才会使用int,如果是浮点数,Redis内部其实先将浮点数转化为字符串值,然后再保存。- embstr:代表embstr格式的SDS(Simple Dynamic String简单动态字符串),保存长度小于44字节的字符串。EMBSTR顾名思义即:embedded String,表示嵌入式的字符串。
- raw:保存长度大于44字节的字符串。
4.1.2 SDS简单动态字符串
Redis没有直接复用C语言的字符串,而是新建了属于自己的结构 ---- SDS。
在Redis数据库里,包含字符串值的键值对都是由SDS实现的(Redis中所有的键都是由字符串对象实现的即底层是由SDS实现,Redis中所有的值对象中包含的字符串对象底层也是由SDS实现)。
4.1.3 Redis为什么重新设计一个SDS数据结构?
4.1.4 小总结
Redis内部会根据用户给的不同键值而使用不同的编码格式,自适应地选择较优化的内部编码格式,而这一切对用户完全透明。
4.2 Hash数据结构介绍
4.2.1 Ziplist介绍
Ziplist压缩列表是一种紧凑编码格式,总体思想是多花时间来换取节约空间,即以部分读写性能为代价,来换取极高的内存空间利用率,因此只会用于字段个数少,且字段值比较小的场景。压缩列表内存利用率极高的原因与其连续内存的特性是分不开的。
类似于一种GC垃圾回收机制:标记–压缩算法。当一个hash对象,只包含少量键值对且每个键值对的键和值亚朵就是小整数,要么就是长度比较短的字符串,那么它拥ziplist作为底层实现。
ZipList为了节约内存而开发,它是由连续内存块组成的顺序型数据结构,有点类似于数组
zipList是一个经过特殊编码的双向链表,它不存储指向前一个链表节点prev和指向下一个链表节点的指针next
而是存储上一个节点长度和当前节点长度, 通过牺牲部分读写性能,来换取高效的内容空间利用率,节约内存,是一种时间换空间的思想。只用在字段个数少,字段值小的场景里面
。
ziplist各个组成单元:
4.2.2 zlentry压缩列表节点的构成
- prevlen:记录了前一个节点的长度
- encoding:记录了当前节点实际数据的类型以及长度
- data:记录了当前节点的实际数据
为什么entry这么设计?记录前一个节点的长度?
链表在内存中,一般是不连续的,遍历相对比较慢,而ziplist可以很好的解决这个问题。如果知道了当前的起始地址,因为entry是连续的,entry后一定是另一个entry,想知道下一个entry的地址,只要将当前的起始地址加上当前entry总长度。如果还想遍历下一个entry,只要继续同样的操作。
4.2.3 明明有链表了,为什么出来一个压缩链表?
4.2.4 ziplist总结
ziplist为了节省内存,采用了紧凑的连续存储。
ziplist是一个双向链表,可以在时间复杂度为O(1)下从头部、尾部进行pop或push。
新增或更新元素可能会出现连锁更新现象(致命缺掉导致被listpack替换)。
不能保存过多的元素,否则查询效率就会降低,数量小和内容小的情况下可以使用。
4.2.5 listpack
- hash-max-listpack-entries:使用紧凑列表保存时哈希集合中的最大元素个数。
- hash-max-listpack-value:使用紧凑列表保存时哈希集合中单个元素的最大长度。
Hash类型键的字段个数小于
hash-max-listpack-entries且每个字段名和字段值的长度小于
hash-max-listpack-value时,Redis才会使用OBJ_ENCODING_LISTPACK来存储该键,前述条件任意一个不满足则会转换为OBJ_ENCODING_HT的编码方式。
- 哈希对象保存的键值对数量小于512个。
- 所有的键值对的键和值的字符串长度都小于等于64byte(一个英文字母一个字节)时用listpack,反之用hashtable。
- listpack升级到hashtable可以,反过来降级不可以。
明明有ziplist了,为什么出来一个listpack紧凑列表?
ziplist的连锁更新问题:
listpack是Redis设计用来取代掉ziplist的数据结构,它通过每个节点记录自己的长度且放在节点的尾部,来彻底解决掉ziplist存在的连锁更新的问题。
4.2.6 listpack结构
listpack由4部分组成,total Bytes、Num Elem、Entry以及End。
entry结构:
- 当前元素的编码类型(entry-encoding)
- 元素数据(entry-data)
- 编码类型和元素数据这两部分的长度(entry-len)
4.2.7 ziplist内存布局VS listpack内存布局
和ziplist列表项类似,listpack列表项也包含了元数据信息和数据本身。不过,为了避免ziplist引起的连锁更新问题,listpack中的每个列表项不再像ziplist列表项那样保存其前一个列表项的长度。
4.3 List数据结构介绍
Redis6版本前的List的编码格式:list用quicklist来存储,quicklist存储了一个双向链表,每个节点都是一个ziplist
在Redis3.0之前,list采用的底层数据结构是ziplist压缩列表+linkedList双向链表。
在高版本的Redis中底层数据结构是quicklist(替换了ziplist + linkedList),而quicklist也用到了ziplist。
quicklist就是【双向链表+压缩链表】组合,因为一个quicklist就是一个链表,而链表中的每个元素又是一个压缩列表。
在较早版本的Redis中,list有两种底层实现:
- 当列表对象中元素的长度比较小伙子数量比较少的时候,采用压缩列表ziplist来存储。
- 当列表对象中元素的长度比较大伙子数量比较多的时候,则会使用双向链表linkedlist来存储。
两者各自的缺点:
- ziplist的优点是内存紧凑,访问效率高,缺点是更新效率低,并且数据量较大时,可能导致大量的内存复制。
- linkedlist的优点是节点修改的效率高,但是需要额外的内存开销,并且节点较多时,会产生大量的内存碎片。
为了结合两者的优点,在redis3.2之后,list的底层时间变为快速列表quicklist。
quicklist实际上是ziplist和linkedList的混合体,它将linkedList按段切分,每一段使用ziplist来紧凑存储,多个zipList之间使用双向指针串接起来。
Redis7的List是用quickList来存储,quickList存储了一个双向链表,每一个节点都是一个listpack。
4.4 Set数据结构介绍
Redis用intset或hashtable存储set。如果元素都是整数类型,就用insert存储。如果不是整数类型,就用hashtable(数组+链表的存储结构)。key就是元素的值,value为null。
4.5 ZSet数据结构介绍
当有序结合中包含的元素数量超过服务器属性server.zset_max_ziplist_entries/server.zset_max_listpack_entries的值(默认值为128),或者有序集合中新添加元素的member的长度大于服务器属性server.zset_max_ziplist_value/server.zset_max_listpack_value的值(默认值为64)时,redis会使用跳跃表
作为有序集合的底层实现。否则会使用ziplist/listpack作为有序集合的底层实现。
4.5.1 skiplist跳表
为什么引出跳表?
对于一个单链表来讲,即便链表中存储的数据是有序的
,如果我们要想在其中查找某个数据,也只能从头到尾遍历链表。这样查找效率会很低,时间复杂度会很高O(N)。
尝试空间换时间(升维),给链表加个索引,称为“索引升级”。
跳表是可以实现二分查找的有序链表,是一种以空间换取时间的结构。由于链表无法进行二分查找,因此借鉴数据库索引的思想,提取出链表中关键节点(索引),先在关键节点上查找,再进入下层链表查找,提取多层关键节点,就形成了跳跃表。但是,由于索引也要占据一定空间的,所以索引添加的越多,空间占用的越多。
总结来讲,跳表 = 链表 + 多级索引
4.5.2 跳表时间+空间复杂度介绍
跳表的时间复杂度是O(logN)
跳表的空间复杂度是O(N)
4.5.3 skiplist的优缺点
- 优点:跳表是一个最典型的空间换时间的解决方案,而且只有在
数据量较大的情况下
才能体现出来优势。而且应该是读多写少的情况下
才能使用,所以它的适用范围应该还是比较有限的。 - 缺点:维护成本相对较高,在单链表中,一旦定位好要插入的位置,插入节点的时间复杂度是很低的,就是O(1),但是现在或者删除时需要把所有索引都更新一遍,为了保证原始链表中数据的有序性,我们需要先找到要动作的位置,这个查找操作就会比较耗时最后在新增和删除的过程中的更新,时间复杂度也是O(logN)。