1、Redis 的 zset 是一个复合结构,结构特点:

1)需要通过hash 结构来存储 value 和 score 的对应关系

2)要支持按照 score 来排序

3)还要按照指定 score 的范围来获取 value 列表的

4)要实现上诉的功能的结构,作者想到了跳跃列表

5)zset的结构:通过一个hash字典加跳跃列表skiplist

 

通过redis中zset类型来实现每日排行榜 redis的zset排序原理_Redis

2、跳跃列表结构

1)基本结构

通过redis中zset类型来实现每日排行榜 redis的zset排序原理_i++_02

解释:Redis的跳跃列表总共有64层,上图中每个kv对应的结构如下图中的zslnode结构,kv header也是zslnode结构,只不过value字段是null,score值是Double.MIN_VALUE,相邻来个kv之间用指针窜接起来,形成双向列表,而且是有序的,从小到大,每一层元素的遍历是从kv header开始

2)结构体

structzsl {
  zslnode* header; // 跳跃列表头指针
  int maxLevel; // 跳跃列表当前的最高层
map<string, zslnode*> ht; // hash 结构的所有键值对
}
structzslnode {
string value;
  double score;
  zslnode*[] forwards;  // 多层连接指针
  zslnode* backward;  // 回溯指针
}

2、查找

通过redis中zset类型来实现每日排行榜 redis的zset排序原理_i++_03

1)如果要定位到紫色的模块,首先从header往下定位,比header降一层,在定位比上层降一层的,通过3次定位,最后定位到紫色的那个元素,我们将中间一系列节点称为搜索路径

2)当插入一个新节点时,我们通过随机算法来定位新插入的节点有多少层

3、随机层数

最理想的是每一层的晋升概率都是50%

/* Returns a random level for the new skiplist node we are going to create.
 * The return value of this function is between 1 and ZSKIPLIST_MAXLEVEL
 * (both inclusive), with a powerlaw-alike distribution where higher
 * levels are less likely to be returned. */
intzslRandomLevel(void) {
    int level = 1;
    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
        level += 1;
    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;

但是Redis的标准源码的晋升概率只有25%

4、插入过程

/* Insert a new node in the skiplist. Assumes the element does not already
 * exist (up to the caller to enforce that). The skiplist takes ownership
 * of the passed SDS string 'ele'. */
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
// 存储搜索路径
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
// 存储经过的节点跨度
    unsignedint rank[ZSKIPLIST_MAXLEVEL];
    int i, level;
 
    serverAssert(!isnan(score));
    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];
// 如果score相等,还需要比较value
        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)))
        {
            rank[i] += x->level[i].span;
            x = x->level[i].forward;
        }
        update[i] = x;
    }
// 正式进入插入过程
/* we assume the element is not already inside, since we allow duplicated
     * scores, reinserting the same element should never happen since the
     * caller of zslInsert() should test in the hash table if the element is
     * already inside or not. */
// 随机一个层数
    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);
// 重排一下前向指针
    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++;
    }
// 重排一下后向指针
    x->backward = (update[0] == zsl->header) ? NULL : update[0];
    if (x->level[0].forward)
        x->level[0].forward->backward = x;
    else
        zsl->tail = x;
    zsl->length++;
    return x;
}

解析:首先我们在搜索合适插入点的过程中将「搜索路径」摸出来了,然后就创建新节点,创建的时候需要给这个节点随机分配一个层数,再将搜索路径上的节点和这个新节点通过前向后向指针串起来。如果分配的新节点的高度高于当前跳跃列表的最大高度,就需要更新一下跳跃列表的最大高度。

5、删除过程

首页找到搜索路径,然后对每一层相关节点排下向前向后指针,最后更新最高层maxLevel

 

6、更新过层

1)当我们调用zadd方法时,如果value不存在,则就是新增

2)如果value存在就是更新

3)如果该score值没有改变排序,那么位置就不需要调整,直接修改元素的score值

4)如果排序位置改变了,那就要调整位置了

5)调整位置源码如下

/* Remove and re-insert when score changes. */
    if (score != curscore) {
        zskiplistNode *node;
        serverAssert(zslDelete(zs->zsl,curscore,ele,&node));
        znode = zslInsert(zs->zsl,score,node->ele);
/* We reused the node->ele SDS string, free the node now
        * since zslInsert created a new one. */
        node->ele = NULL;
        zslFreeNode(node);
/* Note that we did not removed the original element from
        * the hash table representing the sorted set, so we just
        * update the score. */
        dictGetVal(de) = &znode->score; /* Update score ptr. */
        *flags |= ZADD_UPDATED;
        }
    return 1;

解析:通过上诉源码可以看出,更新时候,先找到value对应的score,然后删除,删除后在插入这个新的节点。

 

6、如果score值时相同处理逻辑

如果zset中的score值都相同,zset的排序首页比较score值,如果相同则比较value值。

 

7、元素排名(rank)规则

     Redis的skiplist的forward指针上增加了span属性,表示从前一个节点沿着当前层的forward指针跳到当前这个节点中间会跳过多少的节点,Redis在插入删除操作时会更新span的大小,当我们要计算一个元素的排名时,只需要将span进行相加就能算出元素的最终的rank值

struct zslforward {
  zslnode* item;
  long span;  // 跨度
}
struct zsl {
  String value;
  double score;
  zslforward*[] forwards;  // 多层连接指针
  zslnode* backward;  // 回溯指针
}