redis的数据类型是依赖具体编码类型(底层数据类型的标识)的数据结构来实现的。
如下图所示:
本文说明的是4种的编码类型的数据结构:动态字符串、双端链表、字典、跳跃表。
本文目录:
1、 简单动态字符串
2、双端链表
3、 字典
4、 跳跃表
本文内容:
1、 简单动态字符串
Sds (Simple Dynamic String,简单动态字符串)是 Redis 底层所使用的字符串表示,它被用
在几乎所有的 Redis 模块中。
本章将对 sds 的实现、性能和功能等方面进行介绍,并说明 Redis 使用 sds 而不是传统 C 字符
串的原因。
sds 的用途
Sds 在 Redis 中的主要作用有以下两个:
1. 实现字符串对象(StringObject);
2. 在 Redis 程序内部用作 char* 类型的替代品;
以下两个小节分别对这两种用途进行介绍。
实现字符串对象
Redis 是一个键值对数据库(key-value DB),数据库的值可以是字符串、集合、列表等多种类
型的对象,而数据库的键则总是字符串对象。
对于那些包含字符串值的字符串对象来说,每个字符串对象都包含一个 sds 值。
Note: “包含字符串值的字符串对象”,这种说法初听上去可能会有点奇怪,但是在 Redis 中,
一个字符串对象除了可以保存字符串值之外,还可以保存 long 类型的值,所以为了严谨起见,
这里需要强调一下:当字符串对象保存的是字符串时,它包含的才是 sds 值,否则的话,它就
是一个 long 类型的值。
举个例子,以下命令创建了一个新的数据库键值对,这个键值对的键和值都是字符串对象,它
们都包含一个 sds 值:
redis> SET book "Mastering C++ in 21 days"
OK
redis> GET book
"Mastering C++ in 21 days"
以下命令创建了另一个键值对,它的键是字符串对象,而值则是一个集合对象:
redis> SADD nosql "Redis" "MongoDB" "Neo4j"
(integer) 3
redis> SMEMBERS nosql
1) "Neo4j"
2) "Redis"
3) "MongoDB"
将 sds 代替 C 默认的 char* 类型
因为 char* 类型的功能单一,抽象层次低,并且不能高效地支持一些 Redis 常用的操作(比
如追加操作和长度计算操作),所以在 Redis 程序内部,绝大部分情况下都会使用 sds 而不是
char* 来表示字符串。
性能问题在稍后介绍 sds 定义的时候就会说到,因为我们还没有了解过 Redis 的其他功能模
块,所以也没办法详细地举例说那里用到了 sds ,不过在后面的章节中,我们会经常看到其他
模块(几乎每一个)都用到了 sds 类型值。
目前来说,只要记住这样一个事实即可:在 Redis 中,客户端传入服务器的协议内容、aof 缓
存、返回给客户端的回复,等等,这些重要的内容都是由都是由 sds 类型来保存的。
Redis 中的字符串
在 C 语言中,字符串可以用一个 \0 结尾的 char 数组来表示。
比如说,hello world 在 C 语言中就可以表示为 "hello world\0" 。
这种简单的字符串表示在大多数情况下都能满足要求,但是,它并不能高效地支持长度计算和
追加(append)这两种操作:
• 每次计算字符串长度(strlen(s))的复杂度为 (N ) 。
• 对字符串进行 N 次追加,必定需要对字符串进行 N 次内存重分配(realloc)。
在 Redis 内部,字符串的追加和长度计算并不少见,而 APPEND 和 STRLEN 更是这两种操
作在 Redis 命令中的直接映射,这两个简单的操作不应该成为性能的瓶颈。
另外,Redis 除了处理 C 字符串之外,还需要处理单纯的字节数组,以及服务器协议等内容,
所以为了方便起见,Redis 的字符串表示还应该是二进制安全的:程序不应对字符串里面保存
的数据做任何假设,数据可以是以 \0 结尾的 C 字符串,也可以是单纯的字节数组,或者其他
格式的数据。
考虑到这两个原因,Redis 使用 sds 类型替换了 C 语言的默认字符串表示:sds 既可以高效地
实现追加和长度计算,并且它还是二进制安全的。
sds 的实现
在前面的内容中,我们一直将 sds 作为一种抽象数据结构来说明,实际上,它的实现由以下两
部分组成:
typedef char *sds;
struct sdshdr {
// buf 已占用长度
int len;
// buf 剩余可用长度
int free;
// 实际保存字符串数据的地方
char buf[];
};
其中,类型 sds 是 char * 的别名 (alias),而结构 sdshdr 则保存了 len 、free 和 buf 三个
属性。
作为例子,以下是新创建的,同样保存 hello world 字符串的 sdshdr 结构:
struct sdshdr {
len = 11;
free = 0;
buf = "hello world\0"; // buf 的实际长度为 len + 1
};
通过 len 属性,sdshdr 可以实现复杂度为 (1) 的长度计算操作。
另一方面,通过对 buf 分配一些额外的空间,并使用 free 记录未使用空间的大小,sdshdr 可
以让执行追加操作所需的内存重分配次数大大减少,下一节我们就会来详细讨论这一点。
当然,sds 也对操作的正确实现提出了要求——所有处理 sdshdr 的函数,都必须正确地更新
len 和 free 属性,否则就会造成 bug 。
优化追加操作
在前面说到过,利用 sdshdr 结构,除了可以用 (1) 复杂度获取字符串的长度之外,还可以减
少追加 (append) 操作所需的内存重分配次数,以下就来详细解释这个优化的原理。
为了易于理解,我们用一个 Redis 执行实例作为例子,解释一下,当执行以下代码时,Redis
内部发生了什么:
redis> SET msg "hello world"
OK
redis> APPEND msg " again!"
(integer) 18
redis> GET msg
"hello world again!"
首先,SET 命令创建并保存 hello world 到一个 sdshdr 中,这个 sdshdr 的值如下:
struct sdshdr {
len = 11;
free = 0;
buf = "hello world\0";
}
当执行 APPEND 命令时,相应的 sdshdr 被更新,字符串 " again!" 会被追加到原来的
"hello world" 之后:
struct sdshdr {
len = 18;
free = 18;
// 空白的地方为预分配空间,共 18 + 18 + 1 个字节
buf = "hello world again!\0 ";
}
注意,当调用 SET 命令创建 sdshdr 时,sdshdr 的 free 属性为 0 ,Redis 也没有为 buf 创建
额外的空间——而在执行 APPEND 之后,Redis 为 buf 创建了多于所需空间一倍的大小。
在这个例子中,保存 "hello world again!" 共需要 18 + 1 个字节,但程序却为我们分配了
18 + 18 + 1 = 37 个字节——这样一来,如果将来再次对同一个 sdshdr 进行追加操作,只要
追加内容的长度不超过 free 属性的值,那么就不需要对 buf 进行内存重分配。
比如说,执行以下命令并不会引起 buf 的内存重分配,因为新追加的字符串长度小于 18 :
redis> APPEND msg " again!"
(integer) 25
再次执行 APPEND 命令之后,msg 的值所对应的 sdshdr 结构可以表示如下:
struct sdshdr {
len = 25;
free = 11;
// 空白的地方为预分配空间,共 18 + 18 + 1 个字节
buf = "hello world again! again!\0 ";
}
sds.c/sdsMakeRoomFor 函数描述了 sdshdr 的这种内存预分配优化策略,以下是这个函数的
伪代码版本:
def sdsMakeRoomFor(sdshdr, required_len):
# 预分配空间足够,无须再进行空间分配
if (sdshdr.free >= required_len):
return sdshdr
# 计算新字符串的总长度
newlen = sdshdr.len + required_len
# 如果新字符串的总长度小于 SDS_MAX_PREALLOC
# 那么为字符串分配 2 倍于所需长度的空间
# 否则就分配所需长度加上 SDS_MAX_PREALLOC 数量的空间
if newlen < SDS_MAX_PREALLOC:
newlen *= 2
else:
newlen += SDS_MAX_PREALLOC
# 分配内存
newsh = zrelloc(sdshdr, sizeof(struct sdshdr)+newlen+1)
# 更新 free 属性
newsh.free = newlen - sdshdr.len
# 返回
return newsh
在目前版本的 Redis 中,SDS_MAX_PREALLOC 的值为 1024 * 1024 ,也就是说,当大小小于
1MB 的字符串执行追加操作时,sdsMakeRoomFor 就为它们分配多于所需大小一倍的空间;当
字符串的大小大于 1MB ,那么 sdsMakeRoomFor 就为它们额外多分配 1MB 的空间。
Note: 这种分配策略会浪费内存吗?
执行过 APPEND 命令的字符串会带有额外的预分配空间,这些预分配空间不会被释放,除非
该字符串所对应的键被删除,或者等到关闭 Redis 之后,再次启动时重新载入的字符串对象将
不会有预分配空间。因为执行 APPEND 命令的字符串键数量通常并不多,占用内存的体积通常也不大,
所以这一般并不算什么问题。
另一方面,如果执行 APPEND 操作的键很多,而字符串的体积又很大的话,那可能就需要修
改 Redis 服务器,让它定时释放一些字符串键的预分配空间,从而更有效地使用内存。
sds 模块的 API
sds 模块基于 sds 类型和 sdshdr 结构提供了以下 API :
sds 还有另一部分功能性函数,比如 sdstolower 、sdstrim 、sdscmp ,等等,基本都是标准
C 字符串库函数的 sds 版本,这里不一一列举了。
小结
• Redis 的字符串表示为 sds ,而不是 C 字符串(以 \0 结尾的 char*)。
• 对比 C 字符串,sds 有以下特性:
– 可以高效地执行长度计算(strlen);
– 可以高效地执行追加操作(append);
– 二进制安全;
• sds 会为追加操作进行优化:加快追加操作的速度,并降低内存分配的次数,代价是多占
用了一些内存,而且这些内存不会被主动释放。
2、双端链表
双端链表作为一种通用的数据结构,在 Redis 内部使用得非常多:它既是 Redis 列表结构的底
层实现之一,还被大量 Redis 模块所使用,用于构建 Redis 的其他功能。
实现 Redis 的列表类型
双端链表还是 Redis 列表类型的底层实现之一,当对列表类型的键进行操作——比如执行
RPUSH 、LPOP 或 LLEN 等命令时,程序在底层操作的可能就是双端链表。
redis> RPUSH brands Apple Microsoft Google
(integer) 3
redis> LPOP brands
"Apple"
redis> LLEN brands
(integer) 2
redis> LRANGE brands 0 -1
1) "Microsoft"
2) "Google"
Note: Redis 列表使用两种数据结构作为底层实现:
1. 双端链表
2. 压缩列表
因为双端链表占用的内存比压缩列表要多,所以当创建新的列表键时,列表会优先考虑使用压
缩列表作为底层实现,并且在有需要的时候,才从压缩列表实现转换到双端链表实现。
Redis 自身功能的构建
除了实现列表类型以外,双端链表还被很多 Redis 内部模块所应用:
• 事务模块使用双端链表来按顺序保存输入的命令;
• 服务器模块使用双端链表来保存多个客户端;
• 订阅/发送模块使用双端链表来保存订阅模式的多个客户端;
• 事件模块使用双端链表来保存时间事件(time event);
类似的应用还有很多,在后续的章节中我们将看到,双端链表在 Redis 中发挥着重要的作用。
双端链表的实现
双端链表的实现由 listNode 和 list 两个数据结构构成,下图展示了由这两个结构组成的一
个双端链表实例:
其中,listNode 是双端链表的节点:
typedef struct listNode {
// 前驱节点
struct listNode *prev;
// 后继节点
struct listNode *next;
// 值
void *value;
} listNode;
而 list 则是双端链表本身:
typedef struct list {
// 表头指针
listNode *head;
// 表尾指针
listNode *tail;
// 节点数量
unsigned long len;
// 复制函数
void *(*dup)(void *ptr);
// 释放函数
void (*free)(void *ptr);
// 比对函数
int (*match)(void *ptr, void *key);
} list;
注意,listNode 的 value 属性的类型是 void * ,说明这个双端链表对节点所保存的值的类型
不做限制。
对于不同类型的值,有时候需要不同的函数来处理这些值,因此,list 类型保留了三个函数指
针——dup 、free 和 match ,分别用于处理值的复制、释放和对比匹配。在对节点的值进行处
理时,如果有给定这些函数,那么它们就会被调用。
举个例子:当删除一个 listNode 时,如果包含这个节点的 list 的 list->free 函数不为空,
那么删除函数就会先调用 list->free(listNode->value) 清空节点的值,再执行余下的删除
操作(比如说,释放节点)。
另外,从这两个数据结构的定义上,也可以它们的一些行为和性能特征:
• listNode 带有 prev 和 next 两个指针,因此,对链表的遍历可以在两个方向上进行:从表头到表尾,或者从表尾到表头。
• list 保存了 head 和 tail 两个指针,因此,对链表的表头和表尾进行插入的复杂度都为O(1)
——这是高效实现 LPUSH 、RPOP 、RPOPLPUSH 等命令的关键。
• list 带有保存节点数量的 len 属性,所以计算链表长度的复杂度仅为 (1) ,这也保证
了 LLEN 命令不会成为性能瓶颈。
以下是用于操作双端链表的 API ,它们的作用以及算法复杂度:
迭代器
Redis 为双端链表实现了一个迭代器 ,这个迭代器可以从两个方向对双端链表进行迭代:
• 沿着节点的 next 指针前进,从表头向表尾迭代;
• 沿着节点的 prev 指针前进,从表尾向表头迭代;
以下是迭代器的数据结构定义:
typedef struct listIter {
// 下一节点
listNode *next;
// 迭代方向
int direction;
} listIter;
direction 记录迭代应该从那里开始:
• 如果值为 adlist.h/AL_START_HEAD ,那么迭代器执行从表头到表尾的迭代;
• 如果值为 adlist.h/AL_START_TAIL ,那么迭代器执行从表尾到表头的迭代;
以下是迭代器的操作 API ,它们的作用以及算法复杂度:
小结
• Redis 实现了自己的双端链表结构。
• 双端链表主要有两个作用:
– 作为 Redis 列表类型的底层实现之一;
– 作为通用数据结构,被其他功能模块所使用;
• 双端链表及其节点的性能特性如下:
– 节点带有前驱和后继指针,访问前驱节点和后继节点的复杂度为 O(1) ,并且对链表
的迭代可以在从表头到表尾和从表尾到表头两个方向进行;
– 链表带有指向表头和表尾的指针,因此对表头和表尾进行处理的复杂度为 O(1) ;
– 链表带有记录节点数量的属性,所以可以在 O(1) 复杂度内返回链表的节点数量(长
度);
3、 字典
字典(dictionary),又名映射(map)或关联数组(associative array), 它是一种抽象数据结
构,由一集键值对(key-value pairs)组成,各个键值对的键各不相同,程序可以将新的键值对
添加到字典中,或者基于键进行查找、更新或删除等操作。
本章先对字典在 Redis 中的应用进行介绍,接着讲解字典的具体实现方式,以及这个字典实现
要解决的问题,最后,以对字典迭代器的介绍作为本章的结束。
字典的应用
字典在 Redis 中的应用广泛,使用频率可以说和 SDS 以及双端链表不相上下,基本上各个功
能模块都有用到字典的地方。
其中,字典的主要用途有以下两个:
1. 实现数据库键空间(key space);
2. 用作 Hash 类型键的其中一种底层实现;
分别介绍这两种用途:
实现数据库键空间
Redis 是一个键值对数据库,数据库中的键值对就由字典保存:每个数据库都有一个与之相对
应的字典,这个字典被称之为键空间(key space)。
当用户添加一个键值对到数据库时(不论键值对是什么类型),程序就将该键值对添加到键空
间;当用户从数据库中删除一个键值对时,程序就会将这个键值对从键空间中删除;等等。
举个例子,执行 FLUSHDB 可以清空键空间上的所有键值对数据:
redis> FLUSHDB
OK
执行 DBSIZE 则返回键空间上现有的键值对:
redis> DBSIZE
(integer) 0
还可以用 SET 设置一个字符串键到键空间,并用 GET 从键空间中取出该字符串键的值:
redis> SET number 10086
OK
redis> GET number
"10086"
redis> DBSIZE
(integer) 1
用作 Hash 数据类型的其中一种底层实现
Redis 的 Hash 类型键使用以下两种数据结构作为底层实现:
1. 字典;
2. 压缩列表;
因为压缩列表比字典更节省内存,所以程序在创建新 Hash 键时,默认使用压缩列表作为底层
实现,当有需要时,程序才会将底层实现从压缩列表转换到字典。
当用户操作一个 Hash 键时,键值在底层就可能是一个哈希表:
redis> HSET book name "The design and implementation of Redis"
(integer) 1
redis> HSET book type "source code analysis"
(integer) 1
redis> HSET book release-date "2013.3.8"
(integer) 1
redis> HGETALL book
1) "name"
2) "The design and implementation of Redis"
3) "type"
4) "source code analysis"
5) "release-date"
6) "2013.3.8"
字典的实现
实现字典的方法有很多种:
• 最简单的就是使用链表或数组,但是这种方式只适用于元素个数不多的情况下;
• 要兼顾高效和简单性,可以使用哈希表;
• 如果追求更为稳定的性能特征,并且希望高效地实现排序操作的话,则可以使用更为复
杂的平衡树;
在众多可能的实现中,Redis 选择了高效且实现简单的哈希表作为字典的底层实现。
dict.h/dict 给出了这个字典的定义:
/*
* 字典
*
* 每个字典使用两个哈希表,用于实现渐进式 rehash
*/
typedef struct dict {
// 特定于类型的处理函数
dictType *type;
// 类型处理函数的私有数据
void *privdata;
// 哈希表(2 个)
dictht ht[2];
// 记录 rehash 进度的标志,值为-1 表示 rehash 未进行
int rehashidx;
// 当前正在运作的安全迭代器数量
int iterators;
} dict;
以下是用于处理 dict 类型的 API ,它们的作用及相应的算法复杂度:
注意 dict 类型使用了两个指针分别指向两个哈希表。
其中,0 号哈希表(ht[0])是字典主要使用的哈希表,而 1 号哈希表(ht[1])则只有在程序
对 0 号哈希表进行 rehash 时才使用。
接下来两个小节将对哈希表的实现,以及哈希表所使用的哈希算法进行介绍。
哈希表实现
字典所使用的哈希表实现由 dict.h/dictht 类型定义:
/*
* 哈希表
*/
typedef struct dictht {
// 哈希表节点指针数组(俗称桶,bucket)
dictEntry **table;
// 指针数组的大小
unsigned long size;
// 指针数组的长度掩码,用于计算索引值
unsigned long sizemask;
// 哈希表现有的节点数量
unsigned long used;
} dictht;
table 属性是一个数组,数组的每个元素都是一个指向 dictEntry 结构的指针。
每个 dictEntry 都保存着一个键值对,以及一个指向另一个 dictEntry 结构的指针:
/*
* 哈希表节点
*/
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 链往后继节点
struct dictEntry *next;
} dictEntry;
next 属性指向另一个 dictEntry 结构,多个 dictEntry 可以通过 next 指针串连成链表,从
这里可以看出,dictht 使用链地址法来处理键碰撞:当多个不同的键拥有相同的哈希值时,哈
希表用一个链表将这些键连接起来。
下图展示了一个由 dictht 和数个 dictEntry 组成的哈希表例子:
如果再加上之前列出的 dict 类型,那么整个字典结构可以表示如下:
在上图的字典示例中,字典虽然创建了两个哈希表,但正在使用的只有 0 号哈希表,这说明字
典未进行 rehash 状态。
哈希算法
Redis 目前使用两种不同的哈希算法:
1. MurmurHash2 32 bit 算法:这种算法的分布率和速度都非常好,具体信息请参考 Mur-murHash 的主页:http://code.google.com/p/smhasher/ 。
2. 基 于 djb 算 法 实 现 的 一 个 大 小 写 无 关 散 列 算 法: 具 体 信 息 请 参 考
http://www.cse.yorku.ca/~oz/hash.html 。
使用哪种算法取决于具体应用所处理的数据:
• 命令表以及 Lua 脚本缓存都用到了算法 2 。
• 算法 1 的应用则更加广泛:数据库、集群、哈希键、阻塞操作等功能都用到了这个算法。
创建新字典
dictCreate 函数创建并返回一个新字典:
dict *d = dictCreate(&hash_type, NULL);
d 的值可以用图片表示如下:
新创建的两个哈希表都没有为 table 属性分配任何空间:
• ht[0]->table 的空间分配将在第一次往字典添加键值对时进行;
• ht[1]->table 的空间分配将在 rehash 开始时进行;
添加键值对到字典
根据字典所处的状态,将一个给定的键值对添加到字典可能会引起一系列复杂的操作:
• 如果字典为未初始化(也即是字典的 0 号哈希表的 table 属性为空),那么程序需要对 0
号哈希表进行初始化;
• 如果在插入时发生了键碰撞,那么程序需要处理碰撞;
• 如果插入新元素使得字典满足了 rehash 条件,那么需要启动相应的 rehash 程序;
当程序处理完以上三种情况之后,新的键值对才会被真正地添加到字典上。
4、 跳跃表
跳跃表(skiplist)是一种随机化的数据,由 William Pugh 在论文《Skip lists: a probabilistic
alternative to balanced trees》中提出,这种数据结构以有序的方式在层次化的链表中保存元
素,它的效率可以和平衡树媲美——查找、删除、添加等操作都可以在对数期望时间下完成,
并且比起平衡树来说,跳跃表的实现要简单直观得多。
以下是一个典型的跳跃表例子(图片来自维基百科):
从图中可以看到,跳跃表主要由以下部分构成:
• 表头(head):负责维护跳跃表的节点指针。
• 跳跃表节点:保存着元素值,以及多个层。
• 层:保存着指向其他元素的指针。高层的指针越过的元素数量大于等于低层的指针,为了
提高查找的效率,程序总是从高层先开始访问,然后随着元素值范围的缩小,慢慢降低层
次。
• 表尾:全部由 NULL 组成,表示跳跃表的末尾。
因为跳跃表的定义可以在任何一本算法或数据结构的书中找到,所以本章不介绍跳跃表的具体
实现方式或者具体的算法,而只介绍跳跃表在 Redis 的应用、核心数据结构和 API 。
跳跃表的实现
为了适应自身的功能需要,Redis 基于 William Pugh 论文中描述的跳跃表进行了以下修改:
1. 允许重复的 score 值:多个不同的 member 的 score 值可以相同。
2. 进行对比操作时,不仅要检查 score 值,还要检查 member :当 score 值可以重复时,
单靠 score 值无法判断一个元素的身份,所以需要连 member 域都一并检查才行。
3. 每个节点都带有一个高度为 1 层的后退指针,用于从表尾方向向表头方向迭代:当执行
ZREVRANGE 或 ZREVRANGEBYSCORE 这类以逆序处理有序集的命令时,就会用到这个属性。
这个修改版的跳跃表由 redis.h/zskiplist 结构定义:
typedef struct zskiplist {
// 头节点,尾节点
struct zskiplistNode *header, *tail;
// 节点数量
unsigned long length;
// 目前表内节点的最大层数
int level;
} zskiplist;
跳跃表的节点由 redis.h/zskiplistNode 定义:
typedef struct zskiplistNode {
// member 对象
robj *obj;
// 分值
double score;
// 后退指针
struct zskiplistNode *backward;
// 层
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 这个层跨越的节点数量
unsigned int span;
} level[];
} zskiplistNode;
以下是操作这两个数据结构的 API ,它们的作用以及相应的算法复杂度:
跳跃表的应用
和字典、链表或者字符串这几种在 Redis 中大量使用的数据结构不同,跳跃表在 Redis 的唯一
作用,就是实现有序集数据类型。
跳跃表将指向有序集的 score 值和 member 域的指针作为元素,并以 score 值为索引,对有序
集元素进行排序。
举个例子,以下代码就创建了一个带有 3 个元素的有序集:
redis> ZADD s 6 x 10 y 15 z
(integer) 3
redis> ZRANGE s 0 -1 WITHSCORES
1) "x"
2) "6"
3) "y"
4) "10"
5) "z"
6) "15"
在底层实现中,Redis 为 x 、y 和 z 三个 member 分别创建了三个字符串,并为 6 、10 和 15
分别创建三个 double 类型的值,然后用一个跳跃表将这些指针有序地保存起来,形成这样一
个跳跃表:
为了展示的方便,在图片中我们直接将 member 和 score 值包含在表节点中,但是在实际的定
义中,因为跳跃表要和另一个实现有序集的结构(字典)分享 member 和 score 值,所以跳跃
表只保存指向 member 和 score 的指针。
小结
• 跳跃表是一种随机化数据结构,它的查找、添加、删除操作都可以在对数期望时间下完
成。
• 跳跃表目前在 Redis 的唯一作用就是作为有序集类型的底层数据结构(之一,另一个构
成有序集的结构是字典)。
• 为了适应自身的需求,Redis 基于 William Pugh 论文中描述的跳跃表进行了修改,包括:
1. score 值可重复。
2. 对比一个元素需要同时检查它的 score 和 memeber 。
3. 每个节点带有高度为 1 层的后退指针,用于从表尾方向向表头方向迭代。