1. 简介
有序集合(zset)在插入元素时需要指定一个score,集合内部根据这个score进行排序,常被用在排行榜,有序队列等场景中。它的常用命令如下:
// 插入,更新
ZADD cities 1 beijing
ZADD cities 2 shanghai
ZADD cities 3 guangzhou
ZADD cities 4 shenzhen
// 删除
ZREM cities beijing
// 查询分数
ZSCORE cities shanghai
// 返回分数排名在范围内的成员(从小到大)
ZRANGE cities 0 2 withscores
// 返回分数排名在范围内的成员(从大到小)
ZREVRANGE cities 0 2 withscores
// 返回有序集合成员数
ZCARD cities
// 计算有序集合中在区间分数内的成员数
ZCOUNT cities 1 3
// 指定成员的分数加上增量
ZINCRBY cities 10 shanghai
2. 创建zset
// size_hint: indicates approximately how many items will be added
// value_len_hint: indicates the approximate individual size of the added elements
robj *zsetTypeCreate(size_t size_hint, size_t val_len_hint) {
if (size_hint <= server.zset_max_listpack_entries &&
val_len_hint <= server.zset_max_listpack_value)
{
return createZsetListpackObject();
}
robj *zobj = createZsetObject();
zset *zs = zobj->ptr;
dictExpand(zs->dict, size_hint);
return zobj;
}
上面的代码是zset的创建函数,可以看到zset在内部有两种表示形式:
- ListpackObject
ListpackObject 使用的编码类型是 OBJ_ENCODING_LISTPACK
,是一种压缩列表。当预估插入的元素数量和元素长度不超过阈值时使用。
- ZsetObject
ZsetObject 使用的编码类型是 OBJ_ENCODING_SKIPLIST
,是一种多级链表+哈希表的结构。下面是zset的定义:
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
// 多级链表
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
// 多级链表节点
typedef struct zskiplistNode {
// 插入元素
sds ele;
// 分数
double score;
// 指向前一个节点
struct zskiplistNode *backward;
// 指向下一个节点, level数组中每一个元素代表一层链表
struct zskiplistLevel {
struct zskiplistNode *forward;
// 跨度,用于计算节点在整个链表中的排位
unsigned long span;
} level[];
} zskiplistNode;
多级链表图示,借用wiki上的一张图:
借着这张图说一下span的含义,对于1号节点来说:
- level0 的 span=1,因为顺着forward指针会经过1个节点(2号节点)
- level1 的 span=2,因为顺着forward指针会经过2个节点(2,3号节点)
- level2 的 span=3,因为顺着forward指针会经过3个节点(2,3,4号节点)
- level3 的 span=9,因为顺着forward指针会经过9个节点(2,3,4,5,6,7,8,9,10号节点)
3. 插入元素
int zsetAdd(robj *zobj, double score, sds ele, int in_flags, int *out_flags, double *newscore) {
if (zobj->encoding == OBJ_ENCODING_LISTPACK){
...
}
if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {
zset *zs = zobj->ptr;
zskiplistNode *znode;
dictEntry *de;
de = dictFind(zs->dict,ele);
if (de != NULL) { // 元素已存在
curscore = *(double*)dictGetVal(de);
if (incr) {
score += curscore;
}
/* Remove and re-insert when score changes. */
if (score != curscore) {
znode = zslUpdateScore(zs->zsl,curscore,ele,score);
dictSetVal(zs->dict, de, &znode->score); /* Update score ptr. */
}
return 1;
} else if(!xx) { // 元素不存在
ele = sdsdup(ele);
znode = zslInsert(zs->zsl,score,ele);
serverAssert(dictAdd(zs->dict,ele,&znode->score) == DICT_OK);
if (newscore) *newscore = score;
return 1;
}
}
}
可以看到,在插入元素时,根据zset的编码类型也会分成两种,我们跳过LISTPACK,直接看SKIPLIST的处理过程。首先会判断当前插入的元素是否已存在,若存在主要逻辑就是对score的更新;若不存在就需要插入一个新节点,同时记录在dict中。 我们重点看两个zsl的操作函数:
3.1 zslInsert
// Insert a new node in the skiplist. Assumes the element does not already exist
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
unsigned long rank[ZSKIPLIST_MAXLEVEL];
int i, level;
x = zsl->header;
// 遍历每一层,找出要更新的节点
for (i = zsl->level-1; i >= 0; i--) {
/* store rank that is crossed to reach the insert position */
rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
while (x->level[i].forward && // 下一个节点不为null
(x->level[i].forward->score < score || // 下一个节点的score<待插入节点的score
(x->level[i].forward->score == score && // 若score相同,但下一个节点元素的字典序在前
sdscmp(x->level[i].forward->ele,ele) < 0)))
{
rank[i] += x->level[i].span;
x = x->level[i].forward;
}
update[i] = x;
}
// 随机层数
level = zslRandomLevel();
if (level > zsl->level) {
for (i = zsl->level; i < level; i++) {
rank[i] = 0;
update[i] = zsl->header;
update[i]->level[i].span = zsl->length;
}
zsl->level = level;
}
x = zslCreateNode(level,score,ele);
// 插入节点处理forward指针
for (i = 0; i < level; i++) {
x->level[i].forward = update[i]->level[i].forward;
update[i]->level[i].forward = x;
/* update span covered by update[i] as x is inserted here */
x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
update[i]->level[i].span = (rank[0] - rank[i]) + 1;
}
/* increment span for untouched levels */
for (i = level; i < zsl->level; i++) {
update[i]->level[i].span++;
}
// 插入节点处理backward指针
x->backward = (update[0] == zsl->header) ? NULL : update[0];
if (x->level[0].forward)
x->level[0].forward->backward = x;
else
zsl->tail = x;
// 新节点插入,链表长度+1
zsl->length++;
return x;
}
注意:在插入节点的时候,新节点的层数是随机的。
// 每增加一层的概率是25%
#define ZSKIPLIST_P 0.25
// 最大随机数 (INT_MAX)
#define RAND_MAX 2147483647
// 最高层数
#define ZSKIPLIST_MAXLEVEL 32
int zslRandomLevel(void) {
static const int threshold = ZSKIPLIST_P*RAND_MAX;
int level = 1;
// 每次有25%的几率增加一层
while (random() < threshold)
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
3.2 zslUpdateScore
// this function attempts to just update the node, in case after the score update,
// the node would be exactly at the same position. Otherwise the skiplist is
// modified by removing and re-adding a new element
zskiplistNode *zslUpdateScore(zskiplist *zsl, double curscore, sds ele, double newscore) {
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
int i;
// 遍历每一层,找到要更新的节点
x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) {
while (x->level[i].forward &&
(x->level[i].forward->score < curscore ||
(x->level[i].forward->score == curscore &&
sdscmp(x->level[i].forward->ele,ele) < 0)))
{
x = x->level[i].forward;
}
update[i] = x;
}
// 走完上面的遍历后,此时x指向的是目标节点的前一个节点,我们用forward跳到目标节点
x = x->level[0].forward;
serverAssert(x && curscore == x->score && sdscmp(x->ele,ele) == 0);
// 如果newscore大于前一个节点的score,小于后一个节点的score,
// 则说明更新后节点的位置不需要发生改变,原地更新即可
if ((x->backward == NULL || x->backward->score < newscore) &&
(x->level[0].forward == NULL || x->level[0].forward->score > newscore))
{
x->score = newscore;
return x;
}
// 位置发生改变,则需要删除节点后插入一个新节点
zslDeleteNode(zsl, x, update);
zskiplistNode *newnode = zslInsert(zsl,newscore,x->ele);
x->ele = NULL;
zslFreeNode(x);
return newnode;
}
4. 总结
本文简单介绍了redis中有序集合(zset)的数据结构,以及它是如何插入元素的。zset使用多级链表来维护节点有序性,插入节点时需要根据score找到合适的插入位置,并通过随机的方式决定节点层数。