Sorted Set 是什么?
有序集合(Sorted Set)是 Redis 中一种重要的数据类型,它本身是集合类型,同时也可以支持集合中的元素带有权重,并按权重排序。
- ZRANGEBYSCORE:按照元素权重返回一个范围内的元素
- ZSCORE:返回某个元素的权重值
Sorted Set 数据结构
- 结构定义:server.h
- 实现:t_zset.c
结构定义是 zset,里面包含哈希表 dict 和跳表 zsl。zset 充分利用了:
- 哈希表的高效单点查询特性(ZSCORE)
- 跳表的高效范围查询(ZRANGEBYSCORE)
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
Skiplist:用于快速查找、插入和删除操作,提供近似O(log N)的时间复杂度
Dictionary(Hashtables):用来存储成员与分数的映射关系,确保每个成员的唯一性
跳表(skiplist)
多层的有序链表。下面展示的是 3 层的跳表,头节点是一个 level 数组,作为 level0~level2 的头指针。
跳表节点的结构定义
typedef struct zskiplistNode {
// sorted set 中的元素
sds ele;
// 元素权重
double score;
// 后向指针(为了便于从跳表的尾节点倒序查找)
struct zskiplistNode *backward;
// 节点的 level 数组
struct zskiplistLevel {
// 每层上的前向指针
struct zskiplistNode *forward;
// 跨度,记录节点在某一层 *forward 指针和该节点,跨越了 level0 上的几个节点
unsigned long span;
} level[];
} zskiplistNode;
跳表的定义
typedef struct zskiplist {
// 头节点和尾节点
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
跳表节点查询
在查询某个节点时,跳表会从头节点的最高层开始,查找下一个节点:
访问下一个节点
- 当前节点的元素权重 < 要查找的权重
- 当前节点的元素权重 = 要查找的权重,且节点数据<要查找的数据
访问当前节点 level 数组的下一层指针
当前节点的元素权重 > 要查找的权重
//获取跳表的表头
x = zsl->header;
//从最大层数开始逐一遍历
for (i = zsl->level-1; i >= 0; i--) {
...
while (x->level[i].forward && (x->level[i].forward->score < score || (x->level[i].forward->score == score
&& sdscmp(x->level[i].forward->ele,ele) < 0))) {
...
x = x->level[i].forward;
}
...
}
层数设置
几种方法:
- 每层的节点数约是下一层节点数的一半。
- 好处:查找时类似于二分查找,查找复杂度可以减低到 O(logN)
- 坏处:每次插入/删除节点,都要调整后续节点层数,带来额外开销
随机生成每个节点的层数。Redis 跳表采用了这种方法。
Redis 中,跳表节点层数是由 zslRandomLevel 函数决定。
int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
其中每层增加的概率是 0.25,最大层数是 32。
#define ZSKIPLIST_MAXLEVEL 32 /* Should be enough for 2^64 elements */
#define ZSKIPLIST_P 0.25 /* Skiplist P = 1/4 */
跳表插入节点 zslInsert
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
unsigned int rank[ZSKIPLIST_MAXLEVEL];
int i, level;
serverAssert(!isnan(score));
x = zsl->header;
// 从最高层的 level 开始找
for (i = zsl->level-1; i >= 0; i--) {
// 每层待插入的位置
rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
// forward.score < 待插入 score || (forward.score < 待插入 score && forward.ele < ele)
while (x->level[i].forward &&
(x->level[i].forward->score < score ||
(x->level[i].forward->score == score &&
sdscmp(x->level[i].forward->ele, ele) < 0))) {
// 在同一层 level 找下一个节点
rank