上文介绍了Redis底层的几个数据结构,接下来可以看看Redis中的字符串,链表,集合,哈希表,有序集合是怎么实现的了,这几种类型的对象都用了之前介绍的至少一种数据结构。


Redis通过定义这五种不同类型的对象,可以在执行命令之前,根据对象的类型判断一个对象是否可以执行给定的命令。下面我们看下Redis中对象的结构


Redis对象主要包括type属性(说明对象是五种中的哪种类型),encoding属性(说明底层使用哪种数据结构),refcount(引用计数),lru(记录对象最后访问时间)。


一.字符串对象


字符串对象是我们使用非常频繁的一个对象,字符串对象的type为"string",encoding可以为int,raw或者embstr。


如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型表示,那么字符串对象会将编码设置为int。


如果字符串对象保存的是一个字符串值,并且这个字符串值的长度大于32字节,那么字符串对象将使用一个简单动态字符串来保存这个字符串值,对象编码为raw。


如果字符串对象保存的是一个字符串值,并且字符串值的长度小于等于32字节,那么字符串对象使用embstr编码保存字符串值。


embstr编码是专门用于保存短字符串的一种优化编码方式,这种编码和raw编码一样,都使用redisObject和sdshdr结构来表示字符串对象。但raw编码会调用两次内存分配函数分别创建redisObject结构和sdshdr结构,而embstr会通过调用一次内存分配函数分配一块连续空间,空间中依次包含redisObject和sdshdr结构。我们可以简单理解为embstr是raw的一种优化即可。


字符串对象在条件满足的时候会进行编码的转换,int和embstr会在一定条件转化为raw。对于int编码的字符串对象来说,如果我们向对象执行了一些命令,使得对象保存的不再是整数值,而是一个字符串值,字符串编码就会变为raw。


二.列表对象


列表对象的编码可以是ziplist(压缩列表)或者linkedlist(双向链表)。需要注意的是,linkedlist编码的列表对象在底层的双端链表中包含了多个字符串对象,字符串对象是Redis五种类型的对象中唯一一种会被其他四种类型对象嵌套的对象


当列表对象可以同时满足以下两个条件时,列表对象使用ziplist编码


1.列表对象保存的所有字符串元素长度都小于64字节。


2.列表对象保存的元素数量小于512个。


不满足这两个条件的列表对象需要使用linkedlist编码。


三.哈希对象


哈希对象的编码可以是ziplist或者hashtable


ziplist编码的哈希对象使用压缩列表作为底层实现,每当有新的键值对要加入到哈希对象时,程序会现将保存了键的压缩列表节点推入到压缩列表表尾,然后再将保存了值的压缩列表推入到压缩列表表尾。


因此,保存了同意键值对的两个节点总是紧挨在一起,保存键的节点在前,保存值的节点在后。先添加到哈希对象中的键值对会被放在压缩列表的表头方向,后添加到哈希对象中的键值对会被放在压缩列表表尾方向。


hashtable编码的哈希对象使用字典作为底层实现,哈希对象中的每个键值对都是用一个字典键值对来保存。


当哈希对象可以同时满足以下两个条件时,哈希对象使用ziplist编码:


1.哈希对象保存的所有键值对的键和值的字符串长度都小于64字节。


2.哈希对象保存的键值对数量小于512个。


不能满足这两个条件的哈希对象需要使用hashtable编码。


四.集合对象


集合对象的编码可以是intset或者hashtable


intset编码的集合对象使用证书集合作为底层实现,hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,而字典的值全部被设置为NULL。这一点和Java中的HashSet实现类似,HashSet底层也是通过HashMap来实现的,同样value也都为null。


当集合对象同时满足以下两个条件时,对象使用intset编码


1.集合对象保存的所有元素都是整数值。


2.集合对象保存的元素数量不超过512个。


不能满足这两个条件的集合对象需要使用hashtable编码。


五.有序集合对象


有序集合的编码可以是ziplist或者skiplist。


ziplist编码的压缩列表对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存。第一个节点保存元素的成员member,第二个元素保存元素的分值。


压缩列表内的集合元素按分值从小到大进行排序,分值较小的元素被防止在靠近表头的方向,分值较大的元素则被防止在靠近表尾的方向。


而skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表。其中字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值对都保存了一个集合元素,字典的键保存了元素的成员,字典的值则保存了元素的分值。通过字典,程序可以用0(1)复杂度查找给定成员的分值。


为什么有序集合同事使用跳跃表和字典来实现呢?主要是出于性能的考虑,如果我们只使用字典来实现有序集合,那么每次执行范围型操作如zrank,zrange等命令,程序都需要对字典保存的所有元素排序。如果我们只使用跳跃表来实现有序集合,那么每次执行根据成员查找分支这一操作的复杂度将从O(1)上升为O(logN)。所以Redis同时使用字典和跳跃表两种数据结构来实现有序集合。


当有序集合对象可以同时满足以下两个条件时,对象使用ziplist编码


1.有序集合保存的元素数量小于128个


2.有序集合保存的所有元素成员长度都小于64字节。


不能满足以上两个条件的有序集合对象将使用skiplist编码。


六.类型检查的实现


当我们使用set命令时,我们只能操作字符串对象,当我们使用lpush命令时我们只能操作list对象,所以每次执行命令之前都需要检查输入键的类型是否正确。类型检查是通过redis对象中的type属性来实现的。即每次执行命令前,服务器会先检查输入数据库键等待值对象是否为执行命令所需的类型,如果是的话就执行命令,否则返回类型错误。


七.内存回收


Redis使用引用计数技术实现了内存回收机制,通过这一机制,程序可以通过跟踪对象的引用计数信息,在适当的时候自动释放对象并进行内存回收。


对象的引用计数会随着对象的使用状态而不断变化,当创建一个对象时,引用计数值初始化为1,当对象被一个新程序使用时,引用计数值会+1,当对象不再被一个程序使用时,引用计数-1,,当对象的引用计数值变为0时,对象站东的内存会被释放。


八.对象的空转时长


lru属性记录了对象最后一次被命令程序访问的时间,对象的空转时长就是通过当前时间减去键的值对象的lru时间计算出来的,我们可以通过配置将空转时长较长的对象内存回收掉。


参考内容:Redis设计与实现-黄健宏