之前相关联的文章:
redis学习之一SDS
redis学习之二双端链表
redis学习之三字典
redis学习之四skiplist
redis学习之五ziplist
redis学习之六对象
redis学习之七字符串对象
redis学习之八列表对象
redis学习之九哈希对象
redis学习之十集合对象

说明:本文是基于redis2.9版本

先再看一下redisObject的定义:

typedef struct redisObject{
    //类型
    unsigned type:4;
    //编码
    unsigned encoding:4;
    //lru缓存淘汰机制信息
    unsigned lru:LRU_BITS;
    //引用计数器
    int refcount;
    //指向底层实现数据结构的指针
    void *ptr;
}robj;

有序集合的编码可以是ziplist或skiplist,当同时满足下面两个条件时使用ziplist编码,不满足的话会使用skiplist编码:

  1. 保存的元素不超过128个。
  2. 保存的元素成员长度不超过64字节。

这两个值也是可以通过zset-max-ziplist-value配置进行调整的。

先来说下ziplist编码有关的情况:每个元素使用两个紧挨在一起的压缩列表来保存数据,第一个节点保存元素的成员(member),第二个节点保存的是分值(score),分值从小到大排列,也就是分值较小的排在表头前面,较大的排在靠近表尾一侧。
看下下面的命令:

redis> ZADD price 8.5 apple 5.0 banana 6.0 cherry
(integer) 3

存储示意图为:
image.png

可以看的出来元素是按分值递增排序的。下面来看下使用skiplist编码是如何存储的,先看下示意图:
image.png
skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset包含一个字典和一个跳表:

typedef struct zset{
    zskiplist *zsl;
    dict *dict;
}zset;

我们分两部分分别说下dict与zsl,先看zsl:
zsl跳表按分值从小到大保存了所有集合元素,跳表节点里的object保存了元素成员,跳表里的score属性保存的是元素的分值。
再来看看dict里的内容:
dict存的是元素成员到分值的映射关系:dict的健保存了元素的成员,dict的值则保存了元素分值,通过这个关系,可以用O(1)的复杂度找到给定的分值。
需要说明下,尽管zset同时使用dict与zskiplist两种数据结构保存元素,但这两种数据结构会通过指针来共享相同元素的成员和分值,所以不会产生同一元素多次存储,也就不会造成内存的浪费。

最后,咱们来聊几个问题:

  • 为什么有序集合会同时使用dict与ziplist两种数据结构呢?
    根据上面提到的两种数据结构的存储原理是很好回答的:
  1. 使用dict是为了利用hash算法,以O(1)的时间复杂度查询成员的分值,这就是命令ZSCORE实现的基础。
  2. 使用skiplist让我们可以以O(logN)的时间复杂度进行根据分值查询成员、查询成员的排名操作,这就是命令ZRANK/ZRANGE实现的基础。
  • 分析下命令ZRANGE key start stop命令的时间复杂度
    我们知道这命令会返回有序集合中指定区间内的成员,时间复杂度为O(logN + M),其中 N 为有序集的基数,而 M 为结果集的基数。是这样的:通过skiplist可以以O(logN)的时间复杂度定位到范围start开始的元素,然后再以M的时间复杂度定位到stop结束的元素(redis时的跳表最下面的数据通过双向指针连接)。有个说明:zrange时间复杂度的说明
  • 分析下ZRANK key member的时间复杂度
    从文档上看这个命令是返回有序集 key 中成员 member 的排名,时间复杂度为O(logN),进行时间复杂度的分析我们需要看下ziplist里跨度(redis学习之四skiplist)的含义:跨度是用于记录两个节点之间的距离的,它是用来计算排位(rank)的,在查找某个节点过程中,将沿途访问过的所有层的跨度累计起来,得到的结果就是目标结点在ziplist里的排位,计算排位其实就相当于根据分值进行元素的查找,在查找的过程中累加跨度的常量计算可以忽略,这样整体时间复杂度就是O(logN)了。

本文参考的有:
黄健宏的《Redis设计与实现》一书


步履不停
38 声望13 粉丝

好走的都是下坡路