列表对象的编码能够是ziplist(压缩列表)或者linkedlist(双端链表),当列表对象包含的元素比较少时会会使用压缩列表,不然会使用双端链表。具体策略是,当列表对象同时知足如下两个条件时,将使用压缩列表编码:html
一、列表对象保存的全部字符串元素的长度都小于64个字节
node
二、列表对象保存的元素数量小于512个redis
若是上述两个条件的任何一个不能被知足,将使用双端链表编码,以上两个条件的上限值是能够经过配置文件中的list-max-ziplist-value、list-max-ziplist-entries来修改。数据库
若是压缩列表编码的列表对象,再也不知足上述两个条件时,将会被转换为双端列表编码的格式,这种策略的优势是:api
一、由于压缩列表比双端链表更节省内存,而且在元素数量较少时,在内存中以连续块方式保存的压缩列表比起双端链表能够更快的被载入到缓存中。
二、随着列表对象包含的元素愈来愈多,使用压缩列表来保存元素的优点逐渐消失,对象就会将底层实现从压缩列表转向功能更强、也更适合保存大量元素的双端链表上面。数组
【结构】缓存
压缩列表(ziplist)是列表对象与哈希对象的底层数据结构,压缩列表的节点能够保存一个字节数组或者一个整数值。服务器
对于列表对象而言,当一个列表键只包含少许列表项,而且存储的整数、字符串都是比较短小的,将使用压缩列表。数据结构
对于哈希对象而言,当一个哈希键只包含少许键值对,而且键和值是小的整数或短的字符串时,将使用压缩列表。函数
压缩列表的总体数据结构以下图所示:
压缩列表编码的列表对象结构图以下所示:
压缩列表的各个属性含义:
zlbytes属性:记录压缩列表占用的总的内存字节数;在对压缩列表进行内存重分配或计算压缩列表末端位置(即zlend属性所在位置)时须要用到该属性。
zltail属性:记录压缩列表的最后一个列表节点的位置距离整个列表起始地址有多少字节,经过该属性,能够直接定位表尾节点的地址。
zllen属性:记录压缩列表的节点数量,当节点数量小于UNINT16_MAX(65535)时,该属性会记录节点数量的值,当节点数量大于65535时就再也不存储,须要遍历整个压缩列表才能知道节点的总数量。
entryX属性:列表节点,用于存储实际的数值,每一个压缩列表节点都由previous_entry_length、encoding、content三个部分组成。
previous_entry_length属性:记录了前一个节点的长度。
encoding属性:实际要保存值的数据类型及长度。
content属性:实际在节点中保存的值,能够是一个字节数组或一个整数。
zlend属性:是一个特殊值OxFF(十进制255),用于标记压缩列表的末端。
包含三个节点的压缩列表结构示意图:
【列表节点中3个属性的详细说明】
一、previous_entry_length属性:
该属性记录了压缩列表中前一个节点的长度,单位是字节,长度能够是1字节或5字节,具体策略为:
若是前一个节点的长度小于254字节,previous_entry_length属性将用1个字节的长度。
若是前一个节点的长度大于等于254字节,previous_entry_length属性将用5个字节的长度,第1个字节为固定的OxFE(十进制254),以后的4个字节被用来保存前一个节点的长度。
压缩列表从表位向表头遍历的原理:
向压缩列表中存储数据的时候,最早存储的数据被放在列表尾部,新插入的数据会被放到旧数据的前面,读取压缩列表的顺序是由表尾到表头(读取最旧的数据到最新的数据)。
在读取数据的时候,会先读取zltail属性,经过该属性能够直接定位到压缩列表的表尾节点,而后经过previous_entry_length属性,依次找到上一个节点的数据,由后向前,能够依次读取出全部的数据。
二、encoding属性
该属性记录了实际要保存值的数据类型及长度。
三、content属性
该属性记录了保存节点的值,节点值能够是一个字节数组如”hello world”或者整数。
总结:压缩列表是一种为节约内存而开发的顺序型数据结构。
压缩列表能够包含多个节点,每一个节点能够保存一个字节数组或整数值,能够被用于列表对象和哈希对象。
添加新节点到压缩列表,或者从压缩列表中删除节点,可能会引起连锁更新操做,但这种概率并不高。
当一个列表键包含了数量比较多的元素,或者列表中包含的元素都是比较长的字符串时,redis会使用链表做为列表键的底层实现。除了list列表对象、zset有序集合对象用到了链表以外,发布与订阅、慢查询、监视器等功能也用到了链表,redis服务器自己还使用链表来保存多个客户端的状态信息,以及使用链表来构建客户端输出缓冲区(output buffer)。
【链表结构】
链表结构由list+listnode两部分组成。
list结构:
head:表头节点。
tail:表尾节点。
len:链表所包含的节点数量。
dup:节点值复制函数,用于复制链表节点所保存的值。
free:节点值释放函数,用户释放链表节点所保存的值。
match:节点值对比函数,用于对比链表节点所保存的值和另外一个输入值是否相等。
dup、free、match是用于实现多态链表所需的类型特定函数。
链表节点(listNode)结构:
prev 前置节点。
next 后置节点。
value 节点的值。
【链表结构的特色】
双端:链表节点带有prev和next指针,获取某个节点的前置节点和后置节点的复杂度都是O(1)。
无环:表头节点的prev指针和表尾节点的next指针都指向NULL,对链表的访问以NULL为终点。
有表头指针和表尾指针:经过list结构的head指针和tail指针,程序获取链表的表头节点和表尾节点的复杂度为O(1)。
有链表长度计数器:list结构的len属性能够对链表节点进行计数,程序获取链表节点数量的复杂度为O(1)。
多态:链表节点使用value属性保存节点值,而且能够经过list结构的dup、free、match三个属性为节点值设置类型特定函数,因此链表能够用于保存各类不一样类型的值。
linkedlist编码的列表对象,在存储上有如下特色:
inkedlist编码的列表对象,每一个双端链表节点(node)都保存了一个字符串对象中的字符串,即字符串对象嵌套在列表结构中,以下图所示:
哈希对象的编码能够是ziplist(压缩列表)或者Hashtable(字典),当哈希对象同时知足如下两个条件时,哈希对象使用压缩列表编码:
一、哈希对象保存的全部键值对的键和值的字符串的长度都小于64个字节
二、哈希对象保存的键值对数量小于512个
当其中一个条件不能知足时,哈希对象使用hashtable(字典)编码的格式,这两个条件的上限值能够经过配置文件中的hash-max-ziplist-value选项和hash-max-ziplist-entries选项来修改。
若是压缩列表编码的哈希对象,再也不知足上述两个条件时,将被转换为字典编码的格式。
压缩列表编码的哈希对象在存储上有如下特色:
一、保存了同一键值对的两个节点老是紧挨在一块儿,保存键的节点在前,保存值的节点在后。
二、先添加到哈希对象的键值对会被放在压缩列表的表头方向,而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向。
结构示意图以下:
【字典】
特色:字典是以key-value方式存储数据的一种结构,字典中的每一个键都是独一无二的,程序能够在字典中根据键查找、更新、删除与之关联的值,当字典中存储的数据过多或过少,都会经过rehash从新分配字典的大小和结构。
用途:一、redis的数据库就是使用字典来做为底层实现的,对数据库的增删改查是构建在对字典的操做之上的;
二、是哈希键的底层实现之一,当一个哈希键包含的键值对比较多,或者键值对中的元素都是比较长的字符串时,redis会使用字典做为哈希键的底层实现;
三、是集合底层的实现之一。
结构:字典+哈希表+哈希节点。如图:
其中dict是字典结构,dictht是哈希表的结构,dictEntry*是哈希节点的结构,最右边是哈希节点保存的键值对的值,k0、k1是键,v0、v1是对应的值。
【字典结构】
字典结构dict.h/dict有如下几个参数:
具体说明以下:
ht属性是一个包含两个项的数组,数组中的每个项都是一个dictht哈希表,通常状况下,字典只是用ht[0]哈希表,ht[1]哈希表只会在rehash时使用。
rehashidx属性,记录了rehash的进度,若是没有在rehash,则值为-1。
type和dicttype属性,保证了redis能够存储不一样类型的键值对。
【哈希表结构】一个空的哈希表(没有任何键值对)总体结构图以下所示:
哈希表由dict.h/dictht结构定义,里面具体有table、size、used、sizemask几个属性:
具体说明以下:
table属性是一个数组,数组中的每一个元素都是一个指向dict.h/dictEntry(哈希表节点)结构的指针,每一个dictEntry结构保存着一个键值对。
size属性记录了哈希表的大小,也即table数组的大小。
used属性记录了哈希表目前已有节点(键值对)的数量。
sizemask属性的值老是等于size-1,这个属性和哈希值一块儿决定一个键应该被放到table数组的哪一个索引上面。
【哈希表节点】
由于dictentry结构中并无属性指向链表表尾的指针,因此为了速度考虑,程序老是将新节点添加到链表的表头位置(复杂度O(1)),排在其它已有节点的前面。
【rehash--从新散列】
为保持哈希表中数据分配的合理性,当哈希表保存的键值对数量太多或者太少时,程序须要对哈希表的大小进行相应的扩展和收缩,对哈希表的扩展和收缩是经过执行rehash(从新散列)操做完成的,redis对字典的哈希表进行rehash的步骤以下:
一、为字典的ht[1]哈希表分配空间,分配策略为:
若是执行的是扩展操做,那么ht[1]的大小为第一个大于等于ht[0].used2的2的n次幂(示例:若是used=3,size=3,32=6,第一个大于等于6且是2的n次幂的值是8,因此ht[1]的size大小为8)。
若是执行的是收缩操做,那么ht[1]的大小为第一个大于等于ht[0].used的2的n次幂(示例:若是used=3,但size=10,ht[1]的size值将为4,used是哈希表中实际存储的数据的节点数,size是哈希表大小便可以存储多少个数据节点)。
二、将保存在ht[0]中的全部键值对rehash到ht[1]。
三、释放ht[0],将ht[1]设置为ht[0],并在ht[1]新建一个空白哈希表,为下一次rehash作准备。
【哈希表的扩展与收缩】
当哈希表中的数据过于紧密或疏松时,会对哈希表进行扩展与收缩,具体策略以下:
负载因子 = 哈希表已保存节点数量 / 哈希表大小
load_factor = ht[0].used/ht[0].size
当哈希表的负载因子小于0.1时,程序自动开始对哈希表执行收缩操做。
当如下条件中的任意一个被知足时,程序会自动开始对哈希表执行扩展操做:
1.服务器目前没有在执行BGSAVE或BGREWRITEAOF命令,而且哈希表的负载因子大于等于1。
2.服务器目前正在BGSAVE或BGREWRITEAOF命令,而且哈希表的负载因子大于等于5。
当正在执行BGSAVE或BGREWRITEAOF命令时,redis会建立子进程,会用到写时复制技术,须要占用部份内存,此时尽量避免对哈希表进行扩展,避免消耗没必要要的内存。
【渐进式rehash】
为了不rehash对服务器性能形成影响,服务器不是一次将ht[0]里面全部的键值对所有rehash到ht[1],而是分批次渐进式的rehash,在渐进式rehash期间,字典会同时使用ht[0]、ht[1]两个哈希表,期间对字典的查、删、改都是在两个哈希表中进行的。若是要在字典里查找一个键,会先在ht[0]里面查找,若是没找到,会继续到ht[1]里进行查找。可是新增长的键值对一概会被保存到ht[1]中,因此ht[0]里的键值对只减不增。
字典编码的哈希对象,在存储上有如下特色:字典中的每一个键、值都是一个字符串对象
集合对象能够用来存储不重复的数据。集合对象的编码能够是intset(整数集合)或者hashtable(字典):
具体策略为:
当集合对象同时知足如下两个条件时,将使用intset编码:
一、集合对象保存的全部元素都是整数值
二、集合对象保存的元素数量不超过512个
不能知足这两个条件的集合对象须要使用hashtable编码,上述两个条件能够经过配置文件中的set-max-intset-entries选项来修改。
当intset编码的集合对象再也不知足上述两个条件时,将会转换为hashtable编码的格式。
整数集合(intset)是用来存储整数集合的结构,它能够保存int16_t、int32_t或int64_t的整数,而且保证数据不会重复。
【结构】
contents数组里记录了具体的数据,数组中的数据按值的大小从小到大有序地排列。
length属性记录了数据的个数,即contents数组的长度。
enconding属性记录了数组中数据的类型,能够为intset_enc_int1六、intset_enc_int3二、intset_enc_int64,对应的类型分别是int16_t、int32_t、int64_t 。
【升级】
当新添加的数据比整数集合现有的数据类型都要长时,须要先对整数集合进行升级,而后再添加新数据。
升级步骤:
根据新数据的类型,扩展整数集合底层数组的空间大小。
将原有数据的类型均转换为新类型。
添加新数据。
由于每次向整数集合添加新元素均可能引发升级,而每次升级都须要对底层数组中已有的全部元素进行类型转换,因此向整数集合添加新元素的时间复杂度为O(N)。
整数集合不支持降级操做,一旦对数组进行了升级,编码就会一直保持升级后对状态。
有序集合对编码能够是ziplist或者skiplist。
具体策略规则是:
当有序集合对象能够同时知足如下两个条件时,将使用ziplist编码:
一、有序集合保存的元素数量小于128个
二、有序集合保存的全部元素成员对长度都小于64字节
不能知足以上两个条件的有序集合对象都使用skiplist编码,以上两个条件的上限值能够经过配置文件中的zset-max-ziplist-entries和zset-max-ziplist-value选项来修改。
当ziplist编码的有序集合再也不符合上述两个条件时,将被转换为skiplist编码的格式。
ziplist编码的有序集合的存储方式:
ziplist编码以压缩列表结构做为底层实现,每一个集合元素使用两个紧挨在一块儿对压缩列表节点来保存。第一个节点保存元素的成员(member),第二个元素保存元素的分值(score)。压缩列表内对元素按分值大小进行拍下,分值较小对元素放在靠近表头对位置,结构示意图以下所示:
压缩列表结构以前已进行过详细说明,这里就再也不赘述,下面将详细说明下skiplist编码。
【Skiplist-字典+跳跃表】
skiplist编码的有序集合对象使用zset结构做为底层实现,一个zset结构同时包含一个字典和一个跳跃表,dict指针指向的是字典结构,zsl指针指向的是跳跃表结构,见下图。
字典结构以前已有过详细说明,这里再也不赘述,下面先说一下跳跃表结构:
若是一个有序集合包含的元素数量比较多,或者保存的字符串长度较长,redis将使用跳跃表做为有序集合键的底层实现。redis只在两个地方用到了跳跃表,一个是实现有序集合键,另外一个是在集群节点中用做内部数据结构。
【跳跃表结构】
跳跃表由zskiplist和zskiplistNode两个结构组成,其中zskiplist用于保存跳跃表信息(好比表头节点、表尾节点、长度),而zskiplistNode则用于表示跳跃表节点。
上图左侧是zskiplist结构,其包含如下属性:
Header:指向跳跃表的表头节点。
Tail:指向跳跃表的表尾节点。
Level:记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内)。
Length:记录跳跃表的长度,即目前节点的数量。
上图右侧是四个zskiplistNode结构,该结构有如下属性:
层(level):L一、L二、L3等字样表示各节点的层,连线上带有数字箭头的是前进指针,箭头上的数字表示跨度。前进节点能够挨个遍历,也能够一次跳过多个节点
后退(backward)指针:节点中用BW字样标记的是后退指针。后退节点每次只能退至前一个节点。
分值(score):各个节点中的1.0、2.0和3.0是节点保存的分值,在跳跃表中,节点按各自所保存的分值从小到大排列
成员对象(obj):各个节点中的o一、o2和o3是节点所保存的成员对象。
【分值和成员】
节点的分值(score属性)是一个double类型的浮点数,跳跃表中的全部节点都按分值从小到大来排序。
节点的成员对象(obj属性)是一个指针,它指向一个字符串对象,而字符串对象则保存着一个SDS值。
在同一个跳跃表中,各个节点保存的成员对象必须是惟一的,可是多个节点保存的分值却能够是相同的,分值相同的节点将按照成员对象在字典序中的大小来进行排序,成员对象较小的节点会排在前面(靠近表头的位置)。
zset结构中的zsl跳跃表按分值从小到大保存了全部集合元素,每一个跳跃表节点都保存了一个集合元素:跳跃表节点的object属性保存了元素的成员,而跳跃表节点的score属性则保存了元素的分值。经过这个跳跃表,程序能够对有序集合进行范围操做,如zrank、zrange等命令就是基于跳跃表api来实现的。
除此以外,zset结构中的dict字典为有序集合建立了一个从成员到分值的映射,字典中的每一个键值对都保存了一个集合元素:字典的键保存了元素的成员,而字典的值则保存了元素的分值。经过这个字典,程序能够用O(1)复杂度查找给定成员的分值,zscore命令就是根据这一特性实现的,而不少其它有序集合命令都在实现的内部用到了这一特性。
有序集合每一个元素的成员都是一个字符串对象,而每一个元素的分值都是一个double类型的浮点数。值得一提的是,虽然zset结构同时使用跳跃表和字典来保存有序集合元素,但这两种数据结构都会经过指针来共享相同元素的成员和分值,因此同时使用跳跃表和字典来保存集合元素不会产生任何重复成员或分值,也不会所以浪费额外的内存。
参考资料:云栖社区