集合对象
集合对象的编码可以是intset(整数集合)或者hashtable(哈希表)
使用intset编码作为底层的集合对象,所插入的元素都被保存在整数集合的数组里面
使用hashtable编码作为底层的集合对象,所插入的元素都被保存在哈希表中(键值对是两个SDS对象,都存放在dictEntry结点中),插入的元素充当键值对的键,而键值对的值全部设为NULL。
下图是整数集合作为底层实现
下图是哈希表作为底层实现
编码的转换
当集合对象可以同时满足以下两个条件时,使用intset编码
- 集合对象保存的值全部都是整数值
- 集合对象保存的元素数量不超过512个(便于查找的效率,整数集合的底层数组是有序的,可以用二分法查找,但可能是由于哈希表更加散列,根据哈希值得到索引值去找到的链表排除的元素更多)
- 如果不能满足上面的两个条件,底层就会使用hashtable编码
注意
第二个条件的上限值也是可以通过配置文件修改的,具体参数如下
- set-max-intset-entries
下图是,集合对象保存的值存在非整数,从而进行的转换
下图是集合对象保存的值超过512个,从intset变成了hashtable
集合命令的实现
命令 | 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
可以看到这个有序结合的编码是ziplist,那么其底层结构如下图所示
使用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;
下面是一个跳跃表图
跳跃表的每个结点都储存了一个集合元素,即score和member,跳跃表结点的score用来存储score,obj用来储存member,跳跃表结点里面的数组存储的就是各级的索引链表,底层的跳跃表是用来实现ZRANK、ZRANGE等命令的。
ZRANGE是返回指定范围内的score member对,因为最下层会形成一个链表,而且已经排好序的,只要返回特定范围即可,ZRANK是返回指定成员(member)所在的位置,也是通过遍历下层的链表,对比member即可,时间复杂度为。
底层的dict:字典
除此之外,zset结构中还有dict字典,字典里面的内容其实也是插入的(score,member),并且使用member来映射score,这样就可实现的复杂度来找到member对应的score(前提没有发生哈希碰撞)。ZSCORE命令就是使用dict命令来实现的。
但字典里面并不会根据score进行排序,因为member是进行哈希,得到哈希值,然后根据哈希值再计算得到索引值,所以是无序的,对于那些ZRANGE、ZRANK这些要排序的命令来说,排序时间复杂度至少要,而且还要消耗的空间资源来存储排序好的(score,member)
注意
虽然zset结构同时使用了字典和跳跃表,但其实里面的元素并不是重复的,因为这两个底层都是使用同一个指针来共用同一个(score,member),即字典和跳跃表里面的元素都是同一个对象,避免了浪费内存的现象。
为什么采用字典和跳跃表去实现有序集合
理论上,单独使用字典或者跳跃表都是可以去实现有序集合的,但是单独使用的效率都会低于同时使用字典和跳跃表去实现。
当单独使用字典的时候,对于根据member去找到对应的score,时间复杂度仅仅为【当前前提是没有发生哈希碰撞】,但如果去执行一些需要进行排序的操作,比如ZRANGE、ZRANK,那么就需要至少的时间复杂度去进行排序,还要用的空间复杂度去储存排序好的(score,member)
当单独使用跳跃表去实现的时候,对于ZRANGE、ZRANK这些需要排序的操作,只要遍历最下层链表即可,时间复杂度顶多也就是,但是对于根据member去找score时,时间复杂度就为比要慢得多了,由跳跃表的高度来决定。
编码的转换
当有序集合对象可以同时满足以下两个条件时,会使用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 | 遍历压缩列表,查找指定成员,返回旁边的分值结点 | 直接从字典中取出给定成员的分值(哈希获得哈希值再获得索引值) |