下图是简单动态字符串(simple dynamic string, SDS)的结构表示redis
SDS遵循C语言字符串以空字符结尾的惯例,保存空字符的1字节空间不计算在SDS的len属性里面,这个空字符对SDS的使用者来讲是彻底透明的。遵循空字符结尾的好处是,能够重用一些C字符串函数库中的函数。例如,有一个指向图2-1所示SDS的指针s,那么能够直接使用<stdio.h>/printf函数,经过执行如下语句:printf("%s", s->buf);算法
1.1 SDS与C字符串的区别数组
1.1.1 常数复杂度得到字符串长度安全
由于C字符串并不记录自身长度信息,因此为了获取一个C字符串长度,程序必须遍历整个字符串,对遇到的每一个字符进行计数,直到遇到表明字符串结尾的空字符为止,这个操做的复杂度为O(N),以下图所示。服务器
经过访问SDS的len属性,Redis将获取字符串长度所需的复杂度从O(N)下降到了O(1)。 数据结构
1.1.2 杜绝缓冲区溢出app
<string.h>/strcat函数能够将src字符串中的内容拼接到dest字符串的末尾: char *strcat (char *dest, const char *src)函数
strcat函数假定用户在执行这个函数时,已经为dest分配了足够多的内存,若是假设不成立,则会形成缓冲区溢出,致使s2保存的内容被意外修改了。性能
当SDS API须要对SDS进行修改时,API先检查SDS的空间是否知足修改所需的要求,若是不知足,API会自动将SDS的空间扩展至执行修改所需的大小,而后再进行修改。用户不须要手动修改SDS空间的大小。 优化
1.1.3 减小修改字符串时带来的内存重分配次数
对于一个包含N个字符的C字符串,底层实现是一个N+1个字符长的数组。每次缩短或者增长C字符串,都须要对数组作一次内存重分配:
因为内存重分配涉及复杂的算法,比较耗时。SDS经过空间预分配和惰性空间释放两种优化策略减小内存重分配次数
1. 空间预分配
空间预分配用于优化SDS的字符串增加操做:当SDS的API对一个SDS进行修改,并须要对SDS进行空间扩展,程序不只会分配必须的空间,还会为SDS分配额外的未使用空间。若是SDS长度小于1MB,程序分配和len属性一样大小的未使用空间;若是长度大于1MB,程序分配1MB的未使用空间。
2. 惰性空间释放
当SDS的API须要缩短SDS保存的字符串时,程序并不当即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,以备未来使用。
1.1.4 二进制安全
C字符串中的字符必须符合某种编码(好比ASCII),而且除了字符串的末尾以外,字符串里面不能包含空字符,不然最早被程序读入的空字符将被误认为是字符串结尾。这些限制使得C字符串只能保存文本数据,而不能保存图片、音频、视频、压缩文件这样的二进制数据。
SDS API都会以处理二进制的方式来处理SDS存放在buf数组里的数据,程序不会对其中的数据作任何限制、过滤或者假设,数据写入是什么样子,读取时就是什么样子。
1.1.5 兼容部分C字符串函数
经过在buf数组分配空间时多分配一个字节来容纳空字符,使得SDS能够重用一部分<string.h>库定义的函数。表2-2列出了SDS主要操做API。
每一个链表节点使用一个adlist.h/listNode结构来表示:
多个listNode能够经过prev和next指针组成双端链表:
list结构表示:
Redis链表特性:
3.1 字典结构
Redis字典使用哈希表做为底层实现,一个哈希表里面能够有多个哈希表节点,而每一个哈希表节点就保存了字典中的一个键值对。
哈希表结构及哈希表节点定义:
数据存放以下所示:
Redis中字典由dict.h/dict结构表示:
type属性和privdata属性是针对不一样类型的键值对,为建立多态字典而设置的:
ht[1]哈希表会在对ht[0]哈希表进行rehash时使用。
3.2 哈希算法
Redis计算哈希值和索引值的方法以下:
hash = dict->type->hashFunction(key);
index = hash & dict->ht[x].sizemask;
index表示放在dictEntry中的哪一个槽内
3.3 解决键冲突
当两个或以上的键被分配到了哈希表数组的同一个索引上面时,称为发生了哈希碰撞。Redis的哈希表经过使用链地址法来解决键冲突,被分配到同一个索引上的多个节点能够用这个单向链表链接起来。
由于dictEntry节点组成的链表没有指向链表表尾的指针,因此为了速度考虑,程序老是将节点添加到链表的表头位置(复杂度为O(1)),排在其余全部节点前面。
3.4 rehash
哈希表的扩展和收缩
知足下面任一条件,哈希表会进行扩展操做:
负载因子的计算:哈希表已保存节点数量/哈希表大小
当负载因子小于0.1时,程序自动对哈希表进行收缩操做。
Redis对字典的哈希表执行rehash的步骤以下:
1) 为字典ht[1]哈希表分配空间:
2) 将保存在ht[0]中的全部键值对rehash到ht[1]上面
3) 当ht[0]包含的全部键值对都迁移到ht[1]后,释放ht[0],将ht[1]设置为ht[0],并在ht[1]新建立一个空白哈希表,为下一次rehash作准备
示例步骤以下所示:
3.5 渐进式rehash
扩展或收缩哈希表须要将ht[0]里面的全部键值对rehash到ht[1]里面,可是,这个rehash动做并非一次性、集中式地完成的,而是屡次、渐进式地完成的。具体步骤以下:
1)为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表
2)在字典中维持一个索引计数器变量rehashidx,并将它的值设为0,表示rehash工做正式开始
3) 在rehash期间,每次对字典执行添加、删除、查找或者更新操做时,程序除了执行指定的操做之外,还会顺带将ht[0]哈希表在rehashidx索引上的全部键值对rehash到ht[1],当rehash完成后,将rehashidx属性值加一
4) 随着字典操做不断执行,最后在某个时间点上,ht[0]的全部键值对都会被rehash到ht[1],这时将rehashidx设置为-1,表示rehash操做结束。
字典主要API操做
若是一个有序集合包含的元素数量比较多,或者有序集合中元素的成员是比较长的字符串时,Redis就会使用跳跃表来做为有序集合键的底层实现。例如,fruit-price是一个有序集合键,以水果名为成员,水果价钱为分值。fruit-price有序集合的全部数据都保存在一个跳跃表里面,其中每一个跳跃表节点会保存一款水果的信息,全部水果按价钱由低到高排序。
4.1 跳跃表的实现
Redis跳跃表由redis.h/zskiplistNode(用于表示跳跃表节点)和redis.h/zskiplist(保存跳跃表节点相关信息)两个结构定义,示例以下:
4.2 跳跃表节点
跳跃表节点的实现由redis.h/zskiplistNode结构定义:
1. 层
每次建立一个新的跳跃表节点时,程序都根据幂次定律(power low,越大的数出现的几率越小)随机生成一个介于1和32之间的值做为level数组的大小,即层的高度,如图5-2所示。
跳跃表节点中每一个元素包含一个指向其余节点的指针,程序能够经过这些层加快访问其余节点的速度,通常来讲,层的数量越多,访问其余节点的速度就越快。
2. 前进指针
每一个层都有一个指向表尾方向的前进指针(level[i].forward属性),用于从表头向表尾方向访问节点。图5-3用虚线表示了程序从表头向表尾方向,遍历跳跃表中全部节点的路径。
3. 跨度
层的跨度(level[i].span)用于记录两个节点之间的距离:
遍历操做使用前进指针便可完成,跨度是用来计算排位的(节点在跳跃表中的位置)。
4. 后退指针
节点的后退指针(backward属性)用于从表尾向表头访问节点。
5. 分值和成员
节点的分值(score属性)是一个double类型的浮点数,跳跃表中的全部节点都按分值从小到大来排序。
节点的成员对象(obj属性,惟一的)是一个指针,它指向一个字符串对象,而字符串对象则保存一个SDS值。
4.3 跳跃表
zskiplist结构的定义以下:
header和tail指针分别指向跳跃表的表头和表尾节点,经过这两个指针,程序定位表头节点和表尾节点的复杂度为O(1).
length属性记录节点的数量,程序能够在O(1)复杂度内返回跳跃表的长度。
level属性用于得到跳跃表中层高最大的节点的层数量,表头节点的层高不计算在内。
4.4 跳跃表经常使用API
5.1 整数集合的实现
其中encoding有三种取值:
5.2 升级
若是新添加的元素类型比整数集合现有全部元素的类型都要长时,整数集合须要先进行升级(upgrade),而后再将新元素添加到整数集合里面。升级分三步进行:
1) 根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间
2) 将底层数组现有的全部元素都转换成与新元素相同的类型,并将类型转换后的元素放置到正确的位置上,并且在放置元素过程当中,须要维持底层数组的有序性质不变
3) 将元素添加到底层数组里面
contents数组中原先包含3个,每一个占用16位空间的元素。以后须要插入一个占用32位空间的元素65535,其过程以下所示:
5.3 升级的好处
5.3.1 提高灵活性
整数集合能够经过自动升级底层数组来适应新元素,能够随意地将int16_t,int32_t或int64_t类型的整数添加到集合中,没必要担忧出现类型错误。
5.3.2 节约内存
尽可能先用int16_t类型来保存元素,在必要时进行升级,以节约内存。
5.4 降级
整数集合不支持降级。
5.5 整数集合API
6.1 压缩列表的构成
压缩列表是Redis为了节约内存而开发的,由一系列特殊编码地连续内存块组成的顺序型数据结构。一个压缩列表能够包含任意多个节点(entry),每一个节点能够保存一个字节数组或者一个整数值。
下图为压缩列表各组成部分:
图7-2展现了一个压缩列表实例:
6.2 压缩列表节点的构成
下图为压缩列表各个组成部分:
1. previous_entry_length
节点的previous_entry_length属性以字节为单位,记录了压缩列表中前一个节点的长度:
若是有一个指向当前节点起始地址的指针c,能够根据下图计算出前一个节点的指针p。压缩列表从表尾向表头遍历操做就是基于这一原理实现的。
2. encoding
节点encoding属性记录了节点的content属性所保存数据的类型及长度。
值的最高位为00、0一、10是字节数组编码,数组的长度由编码除去最高两位以后的其余位记录
值的最高位以11开头的是整数编码
3. content
6.3 连锁更新
下面讨论一种对列表插入或删除节点时,会发生的极端状况——连锁更新。
前面小节提到过,若是前一个节点长度小于254字节,那么previous_entry_length属性须要用1字节来保存长度值;若是前一个节点长度大于254字节,那么previous_entry_length属性须要用5字节来保存长度值。
如今,考虑这样一种状况,在一个压缩列表中,存在多个连续的、长度介于250字节到253字节之间的节点e1至eN,以下图所示:
若是将一个长度大于等于254字节的新节点new设置为压缩列表的表头节点,以下图所示:
此时e1节点的previous_entry_length属性从原来的1字节扩展为5字节,e1本来的长度介于250字节到253字节之间,因为previous_entry_length长度改变,e1的长度变为介于254字节到257字节,从而引起e2的更新,e2又会引起e3的扩展,直到eN为止,并将此过程称为连锁更新。下图是另外一种引发连锁更新的状况:
由于连锁更新在最坏的状况下须要对压缩列表执行N次空间重分配操做,而每次空间重分配的最坏复杂度是O(N),因此连锁更新的最坏复杂度为O(N2) 。
可是,尽管连锁更新的复杂度较高,可是真正形成性能问题的概率很低:
压缩列表API