本章节主要分析redis的几种常用的底层数据结构
一、字符串
1.1 数据结构
在redis内部,除去字符串字面量(无须进行修改的字符串)外,redis会使用一种SDS的数据结构来表示字符串,无论是key还是value。
如上图所示, SDS包含三个属性值
- free记录buf数组中未使用的字节的数量
- len记录buf数组中已使用的字节的数量
- buf是一个字节数组,用于保存字符串
1.2 SDS和C语言字符串比较的优势
- SDS获取字符串长度的时间复杂度为O(1),直接获取len,而C语言为O(N)
- 使用SDS api可以杜绝缓冲区溢出
- 可以减少修改字符串带来的内存重分配次数。通过空间预分配和惰性空间释放,将修改字符串需要重新分配内存的次数由N次减少为最多N次
- SDS不仅可以保存文本数据,还可以保存任意格式的二进制数据
- 兼容部分C字符串函数
二、hash字典
2.1 数据结构
上图是redis中整个字典的数据结构
- 字典:字典包括type、privdata、ht、rehashidx等字段,其中rehashidx是和渐进式rehash相关的属性,记录了rehash的进度,ht是一个长度为2的数组,正常使用的是ht[0],在进行扩容缩容rehash时,会使用到ht[1]。ht的元素就是下面的哈希表
- 哈希表:和java中hashmap类似,table是用来存放键值对的数组,size是hash表大小,sizemask是用来进行取模运算用的,used表示目前存在键值对的索引数量
- 键值对:属性包括key, value,next
2.2 redis字典的特性
redis中的字典,整体上和java 中的hashmap类似,但是其中也存在一些特殊的地方
- 字典的hash函数由type中的hashFunction实现,不同类型的字典hash计算方法不一样
- 为了防止rehash阻塞主线程,redis采用了一种渐进式rehash的算法,后续独立讲解
三、list列表
3.1 数据结构
- head指向链表头,tail指向链表尾
- len保存链表长度,保证取链表长度的时间复杂度为O(1)
- dup函数用于复制链表节点所保存的值;free函数用于释放链表节点所保存的值;match函数则用于对比链表节点所保存的值和另一个输入值是否相等。
- listNode中value可以是是一个void*指针,可以保存任意类型的值
3.2 链表特性
- 双向列表:删除,插入的时间复杂度为O(1),查找的时间复杂度为O(N)
- 无环:头节点prev和尾节点的next都是null
- 可快速定位到头和尾
四、intset
4.1 数据结构
intset是一种用于保存整数值的数据结构,可以保存16、32、64三种整数类型,并且保证元素唯一性
- encoding表示当前整数的编码方式
- length表示当前元素的个数
- contents存储集合中的元素
4.2 intset特性
- 当添加的元素类型比当前集合中元素类型更高时,需要先对整个集合元素进行升级,再添加元素
- intset中的元素是按顺序进行存放的,所有可以进行二分查找,查找效率较高
五、压缩列表
5.1数据结构
压缩列表是为了节约内存而开发的,由一系列特殊编码的连续内存块组成
- zlbytes记录整个列表占用的内存字节数
- zltail记录压缩列表的尾节点的偏移量
- zllen记录整个压缩列表中节点的个数
- entryX用以保存列表元素
- zlend特殊标志位,标志列表结束
每个entry由下图组成
- preivious_entry_length表示前面的entry的字节长度,在进行逆向遍历时,可以根据这个字段计算出前面entry的起始地址
- encoding记录了content属性保存的数据类型及长度
- content存放的具体内容
六、跳跃列表
6.1 数据结构
跳跃表是在链表的基础上采用分层的结构,和正常的链表不一样的是,跳跃列表的next指针是一个数组,一个节点可以指向多个next,并且对应不同测层次。节点代码如下
typedef struct zskiplistNode {
// 后退指针
struct zskiplistNode *backward;
// 分值
double score;
// 成员对象
robj *obj;
// 层
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];
} zskiplistNode;6.2 跳跃列表分析
跳跃列表的最底层是一个双向链表,在插入新的节点时,会根据概率,构造更高层次的单向链表,层次越高,概率越小,下面是一种简单的计算跳跃列表层次的代码
randomLevel()
level := 1
// random()返回一个[0...1)的随机数
while random() < p and level < MaxLevel do
level := level + 1
return level可以看出一个节点加入第二层链表的概率是p(p<1),加入第三层链表的概率是p*p。
- 跳跃列表的查询效率近似于二分查找,可以达到平均O(logN),最坏O(N)的时间复杂度
- 相比于平衡树,跳跃列表构造更加简单明了,并且插入操作更方便
七、redis中五种基本数据对象的实现方式
7.1 字符串对象
字符串对象的编码可以是int 、raw、embstr
- 如果一个字符串对象保存的是整数值,
并且这个整数值可以用
long类型来表示, 使用int - 如果字符串对象保存的是一个字符串值,
并且这个字符串值的长度大于
39字节,使用raw。 - 如果字符串对象保存的是一个字符串值,
并且这个字符串值的长度小于等于
39字节,使用embstr
7.2 列表对象
列表对象的编码可以是 ziplist(压缩列表) 或者 linkedlist(链表) 。
- 列表对象保存的所有字符串元素的长度都小于
64字节,且列表对象保存的元素数量小于512个,此时使用ziplist - 其他的时候使用
linkedlist
7.3 哈希对象
哈希对象的编码可以是 ziplist 或者 hashtable 。
- 哈希对象保存的所有键值对的键和值的字符串长度都小于
64字节,且哈希对象保存的键值对数量小于512个,此时使用ziplist - 其余时候使用
hashtable
7.4 集合对象
集合对象的编码可以是 intset 或者 hashtable 。
- 集合对象保存的所有元素都是整数值,且集合对象保存的元素数量不超过
512个,此时使用intset - 其余使用
hashtable
7.5 有序集合
有序集合的编码可以是 ziplist 或者 skiplist 。
- 有序集合保存的元素数量小于
128个,且有序集合保存的所有元素成员的长度都小于64字节,此时使用ziplist - 其余使用
skiplist