集合对象

集合对象的编码可以是intset(整数集合)或者hashtable(哈希表)

使用intset编码作为底层的集合对象,所插入的元素都被保存在整数集合的数组里面

使用hashtable编码作为底层的集合对象,所插入的元素都被保存在哈希表中(键值对是两个SDS对象,都存放在dictEntry结点中),插入的元素充当键值对的键,而键值对的值全部设为NULL。

下图是整数集合作为底层实现

Redis 有序集合双重排序 redis有序map_有序集合


下图是哈希表作为底层实现

Redis 有序集合双重排序 redis有序map_有序集合_02

编码的转换

当集合对象可以同时满足以下两个条件时,使用intset编码

  • 集合对象保存的值全部都是整数值
  • 集合对象保存的元素数量不超过512个(便于查找的效率,整数集合的底层数组是有序的,可以用二分法查找,但可能是由于哈希表更加散列,根据哈希值得到索引值去找到的链表排除的元素更多)
  • 如果不能满足上面的两个条件,底层就会使用hashtable编码

注意

第二个条件的上限值也是可以通过配置文件修改的,具体参数如下

  • set-max-intset-entries

下图是,集合对象保存的值存在非整数,从而进行的转换

Redis 有序集合双重排序 redis有序map_Redis 有序集合双重排序_03


下图是集合对象保存的值超过512个,从intset变成了hashtable

Redis 有序集合双重排序 redis有序map_结点_04

集合命令的实现

命令

inset编码

hashtable编码

sadd

调用intsetAdd函数,将所有新元素添加到整数集合的数组中

调用dictAdd函数,以新元素为键,NULL为值,将键值对添加到字典里面

scard

调用intsetLen函数,返回整数集合所包含的元素数量

调用dictSize函数,返回字典所包含的键值对数量

sismember

调用intsetFind函数,在整数集合中查找给定的元素

调用dictFind函数,在字典中的键查找给定的元素

smembers

遍历整个整数集合,使用intsetGet函数返回集合元素

遍历整个字典,使用dictGetKey函数返回字典的键作为集合元素

srandmember

调用intsetRandom函数,从整数集合中随机返回一个元素

调用dictGetRandomKey函数,从字典中随机返回一个字典键

spop

调用intsetRandom函数,从整数集合中随机取出一个元素,并且将该元素返回给客户端,然后调用intsetRemove函数,将随机元素从整数集合中删掉

调用dictGetRandomKey函数,从字典中随机取出一个字典键并且返回给客户端,然后调用dictDelete函数,从字典中删除对应的键值对

srem

调用intsetRemove函数,从整数集合中删除所有给定的元素

调用dictDelete函数,从字典中删除所有键为给定元素的键值对

有序集合对象

有序集合对象存储的也是键值对(score:member),根据score进行排序(score必须可以转为double,从小到大进行排序),通过比较member来返回键值对,所以member必须要唯一。

有序集合的encoding编码可以是ziplist(压缩列表)或者skiplist(跳表)

使用ziplist编码的有序集合

ziplist编码的有序集合对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表结点来标识(跟哈希对象一样),第一个结点保存的是成员(member,与哈希表的key一样,都是摆第一个结点,因为有序集合是根据member去找值的),而第二个成员保存的是分值(score)。

压缩列表内的结点按照score进行从小到大排序,分值较小的元素被放置在靠近表头的位置,而分值较大的元素则被放置在靠近表尾的位置。

举个栗子

zadd me 2.2 price 3.3 age 18.0 me
object encoding me

Redis 有序集合双重排序 redis有序map_跳跃表_05


可以看到这个有序结合的编码是ziplist,那么其底层结构如下图所示

Redis 有序集合双重排序 redis有序map_跳跃表_06

使用skiplist编码的有序集合对象

当如果使用skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表。

typedef zset(
	zskiplist *zsl; //底层跳跃表
    dict *dict; //底层字典
)zset;
底层的zskiplist:底层跳跃表

回顾一下之前的跳跃表结点和跳跃表

跳跃表结点由redis.h/zskiplistNode结构定义

typedef struct zskiplistNode{
    //后退指针
 	struct zskiplistNode *backward;
    //分值key
    double score;
    //成员对象value
    robj *obj;
    //层(也是一个结构体,不过是一个数组)
    struct zskiplistLevel{
        //前进指针
        struct zskiplistNode *forward;
        //跨度
		unsigned int span;
    }level[];
}zskiplistNode;

下面是一个跳跃表图

Redis 有序集合双重排序 redis有序map_Redis 有序集合双重排序_07


跳跃表的每个结点都储存了一个集合元素,即score和member,跳跃表结点的score用来存储score,obj用来储存member,跳跃表结点里面的数组存储的就是各级的索引链表,底层的跳跃表是用来实现ZRANK、ZRANGE等命令的。

ZRANGE是返回指定范围内的score member对,因为最下层会形成一个链表,而且已经排好序的,只要返回特定范围即可,ZRANK是返回指定成员(member)所在的位置,也是通过遍历下层的链表,对比member即可,时间复杂度为Redis 有序集合双重排序 redis有序map_跳跃表_08

底层的dict:字典

除此之外,zset结构中还有dict字典,字典里面的内容其实也是插入的(score,member),并且使用member来映射score,这样就可实现Redis 有序集合双重排序 redis有序map_结点_09的复杂度来找到member对应的score(前提没有发生哈希碰撞)。ZSCORE命令就是使用dict命令来实现的。

但字典里面并不会根据score进行排序,因为member是进行哈希,得到哈希值,然后根据哈希值再计算得到索引值,所以是无序的,对于那些ZRANGE、ZRANK这些要排序的命令来说,排序时间复杂度至少要Redis 有序集合双重排序 redis有序map_redis_10,而且还要消耗Redis 有序集合双重排序 redis有序map_跳跃表_08的空间资源来存储排序好的(score,member)

注意

虽然zset结构同时使用了字典和跳跃表,但其实里面的元素并不是重复的,因为这两个底层都是使用同一个指针来共用同一个(score,member),即字典和跳跃表里面的元素都是同一个对象,避免了浪费内存的现象。

为什么采用字典和跳跃表去实现有序集合

理论上,单独使用字典或者跳跃表都是可以去实现有序集合的,但是单独使用的效率都会低于同时使用字典和跳跃表去实现。

当单独使用字典的时候,对于根据member去找到对应的score,时间复杂度仅仅为Redis 有序集合双重排序 redis有序map_结点_09【当前前提是没有发生哈希碰撞】,但如果去执行一些需要进行排序的操作,比如ZRANGE、ZRANK,那么就需要至少Redis 有序集合双重排序 redis有序map_redis_13的时间复杂度去进行排序,还要用Redis 有序集合双重排序 redis有序map_跳跃表_08的空间复杂度去储存排序好的(score,member)

当单独使用跳跃表去实现的时候,对于ZRANGE、ZRANK这些需要排序的操作,只要遍历最下层链表即可,时间复杂度顶多也就是Redis 有序集合双重排序 redis有序map_跳跃表_08,但是对于根据member去找score时,时间复杂度就为Redis 有序集合双重排序 redis有序map_跳跃表_16Redis 有序集合双重排序 redis有序map_结点_09要慢得多了,由跳跃表的高度来决定。

编码的转换

当有序集合对象可以同时满足以下两个条件时,会使用Ziplist编码(与前面一样,都是看连锁更新的代价)

  • 有序集合保存的元素数量小于128个(前面的是512个)
  • 有序结合保存的所有元素成员的长度都小于64字节

如果不能同时满足上面两个条件,就会变成skiplist编码,同理这个限制条件也是可以改的

  • zset-max-ziplist-entries 数量
  • zset-max-ziplist-value 长度

命令

ziplist

zset

zadd

将成员和分值作为两个结点分别先后插入到压缩列表

将新元素添加到跳跃表后,然后字典那边进行关联

zcard

使用压缩列表的zlen属性,除以2返回

访问跳跃表结构的length属性,这里不需要除以2,因为结点的数量就是(member,score)的数量

zcount

遍历压缩列表,统计分值在指定范围内的结点数量

遍历跳跃表,统计分支在指定范围内的结点数量

zrange

从表头向表尾遍历压缩列表,返回给定索引范围内的所有元素

从表头向表尾遍历跳跃表(最下层的链表),返回给定索引范围的所有元素

zrevrange

从表尾向表头遍历压缩列表,返回给定索引范围内的所有元素

从表尾向表头遍历跳跃表,与zrange操作类似

zrank

从表头向表尾遍历压缩列表,匹配给定的成员,并沿途记录结点的数量,当匹配成功时,返回结点的数量,途径结点的数量就是它的排名

从表头向表尾遍历跳跃表,匹配给定的成员,并沿途记录结点的数量,当匹配成功时,返回结点的数量

zrerank

从表头向表尾遍历压缩列表,与zrank类似

从表尾向表头遍历跳跃表,操作与zrank类似

zrem

遍历压缩列表,删除所有包含给定成员的结点,以及旁边的分值结点(易引起连锁更新)

遍历跳跃表,删除所有包含了给定成员的结点,并且在字典中进行解除关联

zscore

遍历压缩列表,查找指定成员,返回旁边的分值结点

直接从字典中取出给定成员的分值(哈希获得哈希值再获得索引值)