redis zset 是怎么实现创建和插入元素的?

214 阅读5分钟

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在内部有两种表示形式:

  1. ListpackObject

ListpackObject 使用的编码类型是 OBJ_ENCODING_LISTPACK,是一种压缩列表。当预估插入的元素数量和元素长度不超过阈值时使用。

  1. 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上的一张图: image.png

借着这张图说一下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找到合适的插入位置,并通过随机的方式决定节点层数。

5. 资料

Skip list - Wikipedia

Redis 数据结构 | 小林coding (xiaolincoding.com)

Redis 有序集合(sorted set) | 菜鸟教程 (runoob.com)