本文来自程序员历小冰的投稿node
在此感谢历小冰同窗的分享程序员
Redis是一个开源的 key-value 存储系统,它使用六种底层数据结构构建了包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象的对象系统。 redis
今天咱们就经过12张图来全面了解一下它的数据结构和对象系统的实现原理。算法
本文的内容以下:数据库
首先介绍六种基础数据结构:动态字符串,链表,字典,跳跃表,整数集合和压缩列表。数组
其次介绍 Redis 的对象系统中的字符串对象(String)、列表对象(List)、哈希对象(Hash)、集合对象(Set)和有序集合对象(ZSet)。缓存
最后介绍 Redis 的键空间和过时键( expire )实现。bash
Redis 使用动态字符串 SDS 来表示字符串值。下图展现了一个值为 Redis 的 SDS结构 :服务器
len: 表示字符串的真正长度(不包含NULL结束符在内)。数据结构
alloc: 表示字符串的最大容量(不包含最后多余的那个字节)。
flags: 老是占用一个字节。其中的最低3个bit用来表示header的类型。
buf: 字符数组。
SDS 的结构能够减小修改字符串时带来的内存重分配的次数,这依赖于内存预分配和惰性空间释放两大机制。
当 SDS 须要被修改,而且要对 SDS 进行空间扩展时,Redis 不只会为 SDS 分配修改所必需要的空间,还会为 SDS 分配额外的未使用的空间。
若是修改后, SDS 的长度(也就是len属性的值)将小于 1MB ,那么 Redis 预分配和 len 属性相同大小的未使用空间。
若是修改后, SDS 的长度将大于 1MB ,那么 Redis 会分配 1MB 的未使用空间。
好比说,进行修改后 SDS 的 len 长度为20字节,小于 1MB,那么 Redis 会预先再分配 20 字节的空间, SDS 的 buf数组的实际长度(除去最后一字节)变为 20 + 20 = 40 字节。当 SDS的 len 长度大于 1MB时,则只会再多分配 1MB的空间。
相似的,当 SDS 缩短其保存的字符串长度时,并不会当即释放多出来的字节,而是等待以后使用。
链表在 Redis 中的应用很是普遍,好比列表对象的底层实现之一就是链表。除了链表对象外,发布和订阅、慢查询、监视器等功能也用到了链表。
Redis 的链表是双向链表,示意图如上图所示。链表是最为常见的数据结构,这里就不在细说。
Redis 的链表结构的dup 、 free 和 match 成员属性是用于实现多态链表所需的类型特定函数:
dup 函数用于复制链表节点所保存的值,用于深度拷贝。
free 函数用于释放链表节点所保存的值。
match 函数则用于对比链表节点所保存的值和另外一个输入值是否相等。
字典被普遍用于实现 Redis 的各类功能,包括键空间和哈希对象。其示意图以下所示。
Redis 使用 MurmurHash2 算法来计算键的哈希值,而且使用链地址法来解决键冲突,被分配到同一个索引的多个键值对会链接成一个单向链表。
Redis 使用跳跃表做为有序集合对象的底层实现之一。它以有序的方式在层次化的链表中保存元素, 效率和平衡树媲美 —— 查找、删除、添加等操做均可以在对数指望时间下完成, 而且比起平衡树来讲, 跳跃表的实现要简单直观得多。
跳表的示意图如上图所示,这里只简单说一下它的核心思想,并不进行详细的解释。
如示意图所示,zskiplistNode 是跳跃表的节点,其 ele 是保持的元素值,score 是分值,节点按照其 score 值进行有序排列,而 level 数组就是其所谓的层次化链表的体现。
每一个 node 的 level 数组大小都不一样, level 数组中的值是指向下一个 node 的指针和 跨度值 (span),跨度值是两个节点的score的差值。越高层的 level 数组值的跨度值就越大,底层的 level 数组值的跨度值越小。
level 数组就像是不一样刻度的尺子。度量长度时,先用大刻度估计范围,再不断地用缩小刻度,进行精确逼近。
当在跳跃表中查询一个元素值时,都先从第一个节点的最顶层的 level 开始。好比说,在上图的跳表中查询 o2 元素时,先从o1 的节点开始,由于 zskiplist 的 header 指针指向它。
先从其 level[3] 开始查询,发现其跨度是 2,o1 节点的 score 是1.0,因此加起来为 3.0,大于 o2 的 score 值2.0。因此,咱们能够知道 o2 节点在 o1 和 o3 节点之间。这时,就改用小刻度的尺子了。就用level[1]的指针,顺利找到 o2 节点。
整数集合 intset 是集合对象的底层实现之一,当一个集合只包含整数值元素,而且这个集合的元素数量很少时, Redis 就会使用整数集合做为集合对象的底层实现。
如上图所示,整数集合的 encoding 表示它的类型,有int16t,int32t 或者int64_t。其每一个元素都是 contents 数组的一个数组项,各个项在数组中按值的大小从小到大有序的排列,而且数组中不包含任何重复项。length 属性就是整数集合包含的元素数量。
压缩队列 ziplist 是列表对象和哈希对象的底层实现之一。当知足必定条件时,列表对象和哈希对象都会以压缩队列为底层实现。
压缩队列是 Redis 为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构。它的属性值有:
zlbytes : 长度为 4 字节,记录整个压缩数组的内存字节数。
zltail : 长度为 4 字节,记录压缩队列表尾节点距离压缩队列的起始地址有多少字节,经过该属性能够直接肯定尾节点的地址。
zllen : 长度为 2 字节,包含的节点数。当属性值小于 INT16_MAX时,该值就是节点总数,不然须要遍历整个队列才能肯定总数。
zlend : 长度为 1 字节,特殊值,用于标记压缩队列的末端。
中间每一个节点 entry 由三部分组成:
previous_entry_length : 压缩列表中前一个节点的长度,和当前的地址进行指针运算,计算出前一个节点的起始地址。
encoding: 节点保存数据的类型和长度
content :节点值,能够为一个字节数组或者整数。
上面介绍了 6 种底层数据结构,Redis 并无直接使用这些数据结构来实现键值数据库,而是基于这些数据结构建立了一个对象系统.
这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合这五种类型的对象,每一个对象都使用到了至少一种前边讲的底层数据结构。
Redis 根据不一样的使用场景和内容大小来判断对象使用哪一种数据结构,从而优化对象在不一样场景下的使用效率和内存占用。
Redis 的 redisObject 结构的定义以下所示。
typedef struct redisObject { unsigned type:4; unsigned encoding:4; unsigned lru:LRU_BITS; int refcount; void *ptr;} robj;复制代码
其中 type 是对象类型,包括REDISSTRING, REDISLIST, REDISHASH, REDISSET 和 REDIS_ZSET。
encoding是指对象使用的数据结构,全集以下。
咱们首先来看字符串对象的实现,以下图所示。
若是一个字符串对象保存的是一个字符串值,而且长度大于32字节,那么该字符串对象将使用 SDS 进行保存,并将对象的编码设置为 raw,如图的上半部分所示。
若是字符串的长度小于32字节,那么字符串对象将使用embstr 编码方式来保存。
embstr 编码是专门用于保存短字符串的一种优化编码方式,这个编码的组成和 raw 编码一致,都使用 redisObject 结构和 sdshdr 结构来保存字符串,如上图的下半部所示。
可是 raw 编码会调用两次内存分配来分别建立上述两个结构,而 embstr 则经过一次内存分配来分配一块连续的空间,空间中一次包含两个结构。
embstr 只需一次内存分配,并且在同一块连续的内存中,更好的利用缓存带来的优点,可是 embstr 是只读的,不能进行修改,当一个 embstr 编码的字符串对象进行 append 操做时, redis 会现将其转变为 raw 编码再进行操做。
列表对象的编码能够是 ziplist 或 linkedlist。其示意图以下所示。
当列表对象能够同时知足如下两个条件时,列表对象使用 ziplist 编码:
列表对象保存的全部字符串元素的长度都小于 64 字节。
列表对象保存的元素数量数量小于 512 个。
不能知足这两个条件的列表对象须要使用 linkedlist 编码或者转换为 linkedlist 编码。
哈希对象的编码可使用 ziplist 或 dict。其示意图以下所示。
当哈希对象使用压缩队列做为底层实现时,程序将键值对紧挨着插入到压缩队列中,保存键的节点在前,保存值的节点在后。以下图的上半部分所示,该哈希有两个键值对,分别是 name:Tom 和 age:25。
当哈希对象能够同时知足如下两个条件时,哈希对象使用 ziplist 编码:
哈希对象保存的全部键值对的键和值的字符串长度都小于64字节。
哈希对象保存的键值对数量小于512个。
不能知足这两个条件的哈希对象须要使用 dict 编码或者转换为 dict 编码。
集合对象的编码可使用 intset 或者 dict。
intset 编码的集合对象使用整数集合最为底层实现,全部元素都被保存在整数集合里边。
而使用 dict 进行编码时,字典的每个键都是一个字符串对象,每一个字符串对象就是一个集合元素,而字典的值所有都被设置为NULL。以下图所示。
当集合对象能够同时知足如下两个条件时,对象使用 intset 编码:
集合对象保存的全部元素都是整数值。
集合对象保存的元素数量不超过512个。
不然使用 dict 进行编码。
有序集合的编码能够为 ziplist 或者 skiplist。
有序集合使用 ziplist 编码时,每一个集合元素使用两个紧挨在一块儿的压缩列表节点表示,前一个节点是元素的值,第二个节点是元素的分值,也就是排序比较的数值。
压缩列表内的集合元素按照分值从小到大进行排序,以下图上半部分所示。
有序集合使用 skiplist 编码时使用 zset 结构做为底层实现,一个 zet 结构同时包含一个字典和一个跳跃表。
其中,跳跃表按照分值从小到大保存全部元素,每一个跳跃表节点保存一个元素,其score值是元素的分值。而字典则建立一个一个从成员到分值的映射,字典的键是集合成员的值,字典的值是集合成员的分值。经过字典能够在O(1)复杂度查找给定成员的分值。以下图所示。
跳跃表和字典中的集合元素值对象都是共享的,因此不会额外消耗内存。
当有序集合对象能够同时知足如下两个条件时,对象使用 ziplist 编码:
有序集合保存的元素数量少于128个;
有序集合保存的全部元素的长度都小于64字节。
不然使用 skiplist 编码。
Redis 服务器都有多个 Redis 数据库,每一个Redis 数据都有本身独立的键值空间。每一个 Redis 数据库使用 dict 保存数据库中全部的键值对。
键空间的键也就是数据库的键,每一个键都是一个字符串对象,而值对象可能为字符串对象、列表对象、哈希表对象、集合对象和有序集合对象中的一种对象。
除了键空间,Redis 也使用 dict 结构来保存键的过时时间,其键是键空间中的键值,而值是过时时间,如上图所示。
经过过时字典,Redis 能够直接判断一个键是否过时,首先查看该键是否存在于过时字典,若是存在,则比较该键的过时时间和当前服务器时间戳,若是大于,则该键过时,不然未过时。
END
我的公众号:石杉的架构笔记(ID:shishan100)
欢迎长按下图关注公众号:石杉的架构笔记!
公众号后台回复资料,获取做者独家秘制学习资料
石杉的架构笔记,BAT架构经验倾囊相授