跳跃表:
跳跃表是一种有序数据结构,通过在每个节点维持多个指向其他节点的指针,达到快速访问节点的目的。redis使用跳跃表作为有序集合键的实现,如果一个有序集合包含额元素数量比较多,又或者有序集合中元素的成员是比较长的字符串时,redis会使用
跳跃表作为有序集合键的实现。
redis只在有序集合键和集群节点中用作内部数据结构。
跳跃表zskiplist{
zskiplistnode header :指向跳跃表的表头节点。
zskiplistnode tail:指向跳跃表的表尾节点
level:记录目前跳跃表,层数最大的节点所在层数
length:记录跳跃表长度,不包含表头节点
}
zskiplistnode{
zskiplistnode *backward 后退指针,在程序从表尾向前遍历时使用
score 分数
*obj 成员对象
zslistlevel{
zskiplistnode *forward 访问位于表尾方向的其他节点
int span 表示当前节点与本层所指向的下一个节点的距离
}
}
节点的分值是一个double类型的浮点数,跳跃表中的所有节点都按分值大小排序
节点的成员对象是一个指针,它指向一个字符串对象,而字符串对象则保存着一个sds值。
同一个跳跃表,各个节点保存的成员对象必须是唯一的,但是多个节点保存的分值可以是相同的:分值相同的节点将按照成员对象在字典序中的大小来进行排序,成员对象较小的节点会排在前面,靠近表头方向,而成员对象
较大的节点则会排在后面。靠近表尾方向。
整数集合:
整数结合是集合键的底层实现,
intset{
encoding 编码方式:int16_t int32_t int64_t
int8_t contents[] 保存元素的数组 //contents数组所保存的值取决于编码的方式,且数组中的元素都是从小到大排序的
}
整数集合的升级操作,如果向数组中元素添加了编码方式更高的整数,则会对整数集合进行升级
根据新元素的类型,扩展底层数组空间的大小,并为新元素分配空间,将底层数组现有的元素进行类型转换,转换到与新元素相同的类型,并将转换后的元素放在正确的位置上,
这个正确的位置,要维持数组中元素的大小排序,最后一步需要改变encoding的值为最高类型的值
整数集合的优点是可以通过自动升级底层数组来适应新元素,可以随意将不同类型编码的整数,加入到集合中。同时
可以节约内存,只有在添加更高编码的元素时才会升级集合,可以尽量节省内存。
整数集合不支持降级操作。
压缩列表:
压缩列表是哈希键与列表键的底层实现,当一个列表键只包含少量列表项,并且列表项要么就是小整数值,长度比较短的字符串,那么reids会使用压缩列表来做列表键的底层实现。
压缩列表是redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序性数据结构,一个压缩列表可以包含任意多个节点,每个节点保存一个字节数组或者整数值
压缩列表中的压缩节点即entryx的构成:
{
previous_entry_length
encoding
content
}entryx
previous_entry_length 保存的是前一个字节的长度,如果前一个节点的长度<254字节,那么previous_entry_length属性的长度为一字节(注:前一个节点的长度>=254字节,指实际分配内存空间大于254字节,而我们这里所说的属性长度为一字节仅仅是用来记录这个长度值,相当于记录数字,这里容易弄混),如果前一个节点长度>=254字节,那么previous_entry_length属性的长度为5字节,其中属性的第一字节会被设置为0xEE(十进制254),而之后的四个字节则用于保存前一字节的长度
压缩列表可以通过表尾指针所指向表尾的节点减去当前及节点的previous_entry_length值,即可以得到前一个节点所在位置,这就是压缩列表从表尾向表头的遍历操作
压缩列表存在连锁更新问题:即如果在表头插入一个大于254字节长度的节点,原来表头节点的previous_entry_length属性仅为一字节无法保存现表头长度,则需要变化扩展previous_entry_length属性为5字节,如果恰好当前节点长度介于250到253之间,则扩展后使得当前节点长度>=254字节,使得下一个节点也无法保存,继续扩展下一个节点可能出现连锁反应,引发由内存空间重分配引发的性能问题。
redis对象:
redis没有使用之前讲到的数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,系统包含了 字符串对象,列表对象,哈希对象,集合对象,有序集合对象,五种类型
reredis使用对象来表示数据库中的键和值,每次使用reids的数据库创建一个键值对时,至少会创建两个对象,
redisobject结构如下所示:
redisobject{
unsigned type :上面提到的五种类型
encoding:编码:对向的ptr指针指向对象的底层实现数据结构,而这些数据结构由对象的encoding属性决定
void * ptr 指向底层实现数据结构的指针
}
记住每一个种对象所使用的的编码方式中种类可以有多种,例如 String 对象编码方式可以有 整数值或者采用embstr编码的简单动态字符串实现
即 redis_striing 编码方式有 redis_encoding_int 和 redis_encoding_embstr
使用object encoding 命令可以查看一个数据库键的值对象的编码
字符串对象的编码可以是int raw embstr
当字符串对象所保存字符串的值小于等于39字节,那么将采用embstr编码
embstr编码是专门用于保存段字符串的一种优化编码方式,这种编码和raw编码一样都是用redisobject结构和sdshdr结构,而embstr编码则通过调用一次内存分配函数来分配一块连续的空间,
空间中依次包含redisobject和sdshdr两个结构。embstr编码优点将次分配内存和释放内存对于系统调用的次数,同时enmstr编码的字符串对象所有的数据都保存在一块连续的内存里面,所以采用embstr编码的字符串对象比raw编码的字符串对象
能够更好地利用缓存带来的优势。
列表对象可以采用压缩列表与双端链表编码:
当列表对象可以满足下列两个条件时,列表对象可以使用ziplist编码:
列表对象所保存的所有字符串元素长度都小于64字节,
列表对象保存的元素数量小于512个,
如果不能满足这两个条件,则会采用linkedlist编码。
哈希对象的编码可以是ziplist和hashtable
对于压缩列表:保存了同一个键值对的两个节点总是紧挨在一起,保存键的节点在前,保存值得节点在后;先添加到哈希对象中的键值对会被放到压缩列表的表头方向,而后来添加到哈希表对象中的键值对会被放到
压缩列表的表尾方向。
hashtable编码的哈希对象底层使用字典作为顶层实现。哈希对象中的每个键值对使用一个字典键值对来保存
编码转换:
当哈希对象保存的所有键值对的键和值的字符串长度都小于64字节,
哈希对象保存的键值对数量小于512个,不能满足这两个条件的哈希对象需要使用hashtable编码
集合对象的编码可以是intset或者hashtablle
如果是使用hashtable编码作为集合的底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而字典的值则全部被设置为null。
有序集合对象:
编码可以是ziplist和skiplist
压缩列表作为顶层实现,则压缩列表内的集合元素按照分值从小到大进行排序,分值较小的元素被放置在靠近表头的位置,而分值较大的元素则被放在靠近表尾的位置
较复杂的是skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表
zet{
zskiplist *zsl;
dict *dict
}zset
对于zskiplist使用跳跃表节点的score属性保存分值,object属性保存元素成员
对于字典使用,字典的键保存了元素的值,字典的值保存了元素的得分
这样的目的是使用zskiplist可以降低范围查询的时间复杂度:如执行zrank,zrange命令
使用dict的原因是可以以O(1)的时间复杂度的到元素的分值,如果上述两只结构只使用一个的话,必然会导致某一个命令的时间复杂度上升。
有序集合对象可以同时满足一下两个条件时对象会使用ziplist编码:
有序集合对象保存元素数量小于128
有序集合保存的所有元素成员的长度都小于64字节
redisobject对象中内存回收:
redis在自己的对象系统中构建了一个引用计数技术实现内存的回收机制,通过这一机制,程序通过跟踪对象的引用计数信息,在适当的时候自动释放对象进行内存回收
每个对象的引用计数用redisobject对象的refcount 属性记录
创建一个新对象时,引用计数值会被初始化为1
当对象被一个新程序使用时,它的引用计数值会被增1
当对象不再被一个程序使用时,它的引用计数值减一
当对象的引用计数值为0时,对象占用的内存会被释放
redisobject对象中对象共享:
对象的引用计数属性还带有对象共享的作用,如果键a创建了一个包含整数值100的字符串对象作为值对象,如果键b也要创建一个同样保存了整数值100的字符串对象作为值对象,
那么键b则会和a共享一个字符串对象
实现步骤:
将数据库键的值指针指向一个现有的值对象,将被共享的值对象的引用计数增1
目前来说redis会在初始化服务器时,创建一万个字符串对象,这些对象包含了从1到9999的所有整数值,当服务器需要用到0到9999的字符串对象时,服务器就会使用这些共享对象,而不是创建新对象
对象的空转时长:
redisobject.lru属性:该属性记录了对象最后一次被命令程序访问的时间。
通过使用object idletime 命令可以打印出给定键的空转时长,这一空转时长就是通过将当前时间减去键的值对象的lru时间计算得到
键的空转时长还有一个作用就是如果服务器打开了maxmemory选项,并且服务器用于内存回收的算法为volatile-lru或者allkeys-lru,那么当服务器占用的内存数超过了maxmemory选项所设置的上限值时,
空转时长较高的那部分键会优先被服务器释放,从而回收内存