由于C语言内置的数据结构匮乏,Redis实现了一些自己的数据结构。
我们需要分清数据结构和Redis数据类型的区别:
string
、hash
、zset
、list
、set
你可能看的一头雾水,没关系,在数据结构一节之后你就会明白上面这些话的意思。你现在只需要知道,这一节要说的数据结构并不是我们所熟知的Redis五种数据类型,而是实现它们的基础。
由于C语言中的字符串比较底层,很多字符串函数效率不高,而且C原始的字符串对象不方便复用。
SDS是Redis对C语言字符串的包装,提供了一些更强大的特性。
len
来记录字符串的长度,这让函数sdslen
可以在O(1)
的时间复杂度下实现。sdscat
函数在连接两个字符串时,如果检测到目标字符串长度不够,会创建一个足够大小的新字符串来实现连接free
成员变量,记录当前SDS对象中没有被用到的空间,当你修剪一个字符串时,它只调节free
变量而已。这让修剪操作不需要内存重分配,并且让后续的连接操作提供直接复用这些空闲空间的可能性。函数 | 作用 | 时间复杂度 |
---|---|---|
sdsnew | 创建一个包含指定字符串的SDS | O(N) |
sdsempty | 创建一个空SDS | O(1) |
sdsfree | 释放给定的SDS | O(N) |
sdslen | 获取SDS长度 | O(1) |
sdsavail | 返回SDS空闲空间长度 | O(1) |
sdsdup | 创建SDS副本 | O(N) |
sdsclear | 清空SDS中的内容 | O(1) |
sdscat | 拼接字符串到SDS | O(N) |
sdscatsds | 拼接两个SDS | O(N) |
sdscpy | 将给定的C字符串复制到SDS中 | O(N) |
sdsgrowzero | 用空字符串将SDS扩展到指定长度 | O(N) |
sdsrange | 保留SDS给定区间内的数据 | O(N) |
sdstrim | 在SDS中移除所有在给定C字符串中出现过的字符 | O(N^2) |
sdscmp | 比较两个SDS是否相同 | O(N) |
链表作为Redis数据结构中list
的实现之一,在Redis数据结构里非常重要。
Redis的链表特性:
dup
属性是一个函数,用于复制链表节点所保存的值free
属性是一个函数,用于释放链表节点所保存的值match
属性是一个函数,用于对比链表节点所保存的值和另一个输入值是否相等函数 | 作用 | 时间复杂度 |
---|---|---|
listSetDupMethod | 设置dup函数 | O(1) |
listGetDupMethod | 获取dup函数 | O(1) |
listSetFreeMethod | 设置free函数 | O(1) |
listGetFreeMethod | 获取free函数 | O(1) |
listSetMatchMethod | 设置match函数 | O(1) |
listGetMatchMethod | 获取match函数 | O(1) |
listLength | 链表长度获取 | O(1) |
listFirst | 返回链表头节点 | O(1) |
listLast | 返回链表尾节点 | O(1) |
listPrevNode | 返回给定节点的前置节点 | O(1) |
listNextNode | 返回给定节点的后置节点 | O(1) |
listNodeValue | 返回给定节点的值 | O(1) |
listCreate | 创建新链表 | O(1) |
listAddNodeHead | 添加新节点到链表头 | O(1) |
listAddNodeTail | 添加新节点到链表尾 | O(1) |
listInsertNode | 将一个给定节点插入到另一个给定节点的之前或之后 | O(1) |
listSearchKey | 返回链表中包含指定值的节点 | O(N) |
listIndex | 返回链表在给定索引上的节点 | O(N) |
listDelNode | 从链表中删除给定节点 | O(N) |
listRotate | 把链表尾弹出,插入到链表头 | O(1) |
listDup | 复制给定链表的副本 | O(N) |
listRelease | 释放链表占用空间 | O(N) |
字典就是存储一个东西到另一个东西的映射,在Redis数据结构中,字典就是一个HashMap。
rehashindex
,它代表当前要移动底层数组中那一项的项目链表中的所有节点到新的哈希表中,rehashindex
初始是0,hash表每被请求一次,rehashindex
+1,同时底层数组中有一个链表中的所有节点被rehash到新哈希表。rehash期间,redis先在原哈希表中查询,如果查询不到,再尝试去新哈希表中查询,当rehash结束,rehashindex=-1
,源哈希表被销毁。函数 | 作用 | 时间复杂度 |
---|---|---|
dictCreate | 创建字典 | O(1) |
dictAdd | 向哈希表中添加键值对 | O(1) |
dictReplace | 根据指定的键修改值 | O(1) |
dictFetchValue | 返回指定键的值 | O(1) |
dictGetRandomKey | 返回随机键值对 | O(1) |
dictDelete | 删除一个键值对 | O(1) |
dictRelease | 释放字典 | O(N) |
跳表就是将链表之上使用链表再建立一层索引,达到几乎和平衡二叉树一样的搜索效率。这有点像数据库系统中的多级非聚集索引。
跳表这种数据结构不经常在教材上出现,但在很多设计中都常用,这里是一篇关于跳表的文章:数据结构与算法——跳表。
Redis中的跳跃表就用来实现有序集合,所以,跳跃表节点中提供一个score字段,用于对跳跃表节点进行排序。
函数 | 作用 | 时间复杂度 |
---|---|---|
zslCreate | 创建一个跳跃表 | O(1) |
zslFree | 释放跳跃表 | O(N) |
zslInsert | 向跳跃表中插入 | 平均O(logN),最坏O(N) |
zslDelete | 删除跳跃表中的一个节点 | 平均O(logN),最坏O(N) |
zslGetRank | 返回包含给定成员和分值的节点在跳跃表中的排位 | 平均O(logN),最坏O(N) |
zslGetElementByRank | 返回跳跃表在给定排位上的节点 | 平均O(logN),最坏O(N) |
zslIsInRange | 返回跳跃表中是否至少有一个元素在给定分值范围内 | O(1) 检查跳表头节点和尾节点即可 |
zslFirstInRange | 返回跳表中第一个在给定范围内的元素 | 平均O(logN),最坏O(N) |
zslLastInRange | 返回跳表中最后一个在给定范围内的元素 | 平均O(logN),最坏O(N) |
zslDeleteRangeByScore | 删除分值范围内所有节点 | O(N) N是被删除元素个数 |
zslDeleteRangeByRank | 删除指定排位范围的所有节点 | O(N) N是被删除元素个数 |
intset是一种简单的,基于数组的集合实现,它只能保存整数,这个整数的位数并不固定。下面是intset的定义
encoding
指定整数集合中,整数占用的位数,比如INTSET_ENC_INT16
代表content
中保存的实际是int16_t
类型的值。length
是元素数intset将维护底层数组中元素的有序性以及唯一性。
如果在一个encoding==INTSET_ENC_INT16
的intset
上插入一个32位整数,那么当前intset
需要被升级:
intset不支持降级
函数 | 作用 | 时间复杂度 |
---|---|---|
intsetNew | 创建一个新的整数集合 | O(1) |
intsetAdd | 添加一个元素到整数集合中 | O(N) |
intsetRemove | 从整数集合中移出一个元素 | O(N) |
intsetFind | 检查给定值是否存在于集合 | O(logN) 二分查找 |
intsetRandom | 从整数集合中随机返回一个元素 | O(1) |
intsetGet | 取出在给定索引上的元素 | O(1) |
intsetLen | 获取集合中元素数量 | O(1) |
intsetBlobLen | 获取集合占用字节数 | O(1) |
压缩列表是比刚才所说的链表更加紧凑的列表结构,它是经过编码的内存区块,你也可以理解为字节数组。下图是它的编码方式:
zlbytes
:4字节,记录压缩列表占用的内存字节数zltail
:4字节,记录压缩列表的尾节点距离压缩列表的起始地址的偏移量,这使得压缩列表可以在O(1)的时间复杂度下找到尾节点地址zllen
:2字节,压缩列表节点数量entryX
:具体的列表项,长度不定zlend
:1字节,用于标记压缩列表的尾部。和字符串尾部的\0
异曲同工。由于压缩列表的节点可以存储多种多样的数据(整数和字节数组),所以不能像整数集合一样简单,下面是节点的内部结构:
previous_entry_length
:前一个节点的长度,用于获得前一个节点的起始地址。当前一个节点的长度小于254字节,该字段可以用1个字节实现,否则就用5个字节实现
encoding
:占用1~5字节,记录content
的类型和长度。下面是该值于content
类型长度的对照表:
对于字节数组类型,encoding
开头两位代表类型,后面代表字节数组长度。
content
:实际保存节点值
考虑极端情况下,压缩列表中所有节点长度都为254,它们的previous_entry_length
字段只需要1字节就可以。现在,想要往压缩列表头部插入一个长度大于254字节的节点,那么压缩列表原先的第一个节点的previous_entry_length
就要由原先的1字节变成5字节,节点长度就需要变成258,那么后面的节点也要变,随后的所有节点都要变。
删除也可能引发相同的连锁更新。
连锁更新中的每一步都需要空间重分配,所以最坏情况下需要O(N^2)的时间复杂度完成更新。但这种最坏情况太难发生了。
函数 | 作用 | 时间复杂度 |
---|---|---|
ziplistNew | 创建一个新的压缩列表 | O(1) |
ziplistPush | 创建一个包含给定值的新节点,加到表头或表尾 | O(N),最坏O(N^2) |
ziplistInsert | 将包含新值的节点插入到给定节点后 | O(N),最坏O(N^2) |
ziplistIndex | 返回给定索引上的节点 | O(N) |
ziplistFind | 查找并返回包含了给定值的节点 | 由于节点值可能是字节数组,所以最坏情况下O(N^2) |
ziplistNext | 返回给定节点的下一个节点 | O(1) |
ziplistPrev | 返回给定节点的上一个节点 | O(1) |
ziplistGet | 获取给定节点所保存的值 | O(1) |
ziplistDelete | 从压缩列表中删除指定节点 | O(N),最坏O(N^2) |
ziplistDeleteRange | 从压缩列表中删除多个连续指定节点 | O(N),最坏O(N^2) |
ziplistBlobLen | 返回压缩列表占用字节数 | O(1) |
ziplistLen | 返回压缩列表目前包含的节点数量 | 节点数量小于65535时可以直接使用zllen ,所以是O(1),大于等于时就是O(N),需要手动遍历 |