Redis——zset底层结构

redis中 zset 底层采用散列表+跳跃列表(skiplist)来存储数据。

散列表不用多说,set 底层采用散列表来存储,value都为null,通过散列表key的唯一性保证set中元素的不重复。

跳跃列表的结构:

上图就是跳跃列表的示意图,图中只画了四层,Redis 的跳跃表共有 64 层,意味着最 多可以容纳 2^64 次方个元素。

每一个 kv 块对应的结构如下面的代码中的 zslnode 结构,kv header 也是这个结构,只不过 value 字段是 null 值,score为Double.MIN_VALUE,用来垫底的。

底层的 kv 之间使用指针串起来形成了双向链表结构,它们是有序排列的,从小到大。

struct zslnode {
    string value;
    double score;
    zslnode*[] forwards; // 多层连接指针
    zslnode* backward; // 回溯指针
}

不同的 kv 层高可能不一样,层数越高的 kv 越少。同一层的 kv 会使用指针串起来。每一个层元素的遍历都是从 kv header 出发。

随机层数

对于新插入的节点,需要调用一个随机算法分配合理的层数。

直观上期望的目标是 50% 的 Level1,25% 的 Level2,12.5% 的 Level3,一直到最顶层 2^-63,因为这里每一层的晋升概率是 50%。

不过 Redis 标准源码中的晋升概率只有 25%,也就是代码中的 ZSKIPLIST_P 的值。所以官方的跳跃列表更加的扁平化,层高相对较低,在单个层上需要遍历的节点数量会稍多一点。

int zslRandomLevel(void) {
    int level = 1;
    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
    level += 1;
    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

因为层数一般不高,所以遍历的时候从顶层开始往下遍历会非常浪费。跳跃列表会记录一下当前的最高层数 maxLevel,遍历时从这个 maxLevel 开始遍历性能就会提高很多。

查找过程

通过逐层查找的方式来查找数据,时间复杂度为 O(logn)

如图所示,我们要定位到那个紫色的 kv,需要从 header 的最高层开始遍历找到第一个节点 (最后一个比「我」小的元素)。

然后从这个节点开始降一层再遍历找到第二个节点 (最后一个比「我」小的元素)。

然后一直降到最底层进行遍历就找到了期望的节点 (最底层的最后一个比「我」小的元素)。

插入过程

查找过程中已经找到 最底层的最后一个比“我”小的元素,插入时,只要在这个元素后插入元素即可。

插入时,需修改前后元素的指针。如果新节点的高度大于当前的最大高度,需更新当前的最大高度。

如果所有元素的score值都相同呢?

在一个极端的情况下,zset 中所有的 score 值都是一样的,zset 的查找性能会退化为O(n) 么?Redis 作者自然考虑到了这一点,所以 zset 的排序元素不只看 score 值,如果score 值相同还需要再比较 value 值 (字符串比较)。

 

 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值