在Redis中有序集合对象和集合对象类型一样,都是用于多个字符串key数据的集合型对象,有序集合与集合对象一致,都不允许出现相同的key;但是与集合对象类型不同的是,集合对象类型本质上是采用哈希方法进行编码的,因为其保存的key都是无序的,而有序集合会为每一个key关联一个double
类型的分值,基于这个分值,有序集合会对其内部存储的key按照分值从小到大的顺序进行排序。
与散列对象、集合对象一样,有序集合对象同样是采用两个底层编码方式来实现。对于有序集合对象来说,底层有压缩链表编码方式OBJ_ENCODING_ZIPLIST
以及跳跃表编码方式OBJ_ENCODING_SKIPLIST
,对于OBJ_ENCODING_ZIPLIST
编码来说,其底层就是使用压缩链表来存储数据的;而OBJ_ENCODING_SKIPLIST
编码,Redis定义了一个数据结构:
typedef struct zset
{
dict *dict;
zskiplist *zsl;
} zset;
在这个数据结构之中,zset.dict
这个哈希表中存储了key与score之间的映射关系,而zset.zskiplist
这个跳跃表则是维护了按照分值的升序方式对所有key的排序信息。
同时,Redis还定义了两个阈值,redisServer.zset_max_ziplist_entries
以及redisServer.zset_max_ziplist_value
,用于在创建有序集合时选择编码方式,以及在操作有序集合时执行编码转换,在默认的情况下,Redis会选择压缩链表作为有序集合的编码方式。在这两个阈值中redisServer.zset_max_ziplist_entries
表示当使用压缩链表编码方式的有序集合最多可以存储元素的个数,当压缩链表中的元素个数超过这个阈值,那么这个有序集合会被转换成跳跃表的编码方式,同时这个阈值可以通过配置文件在启动时进行调整,如果被设置为0,那么可以认为当前的Redis永远都会使用跳跃表作为有序集合的编码方式;redisServer.zset_max_ziplist_value
则是表示使用压缩链表编码时,元素最大的长度,当向一个有序集合中插入的key的长度大于这个阈值的时候,便会采用跳跃表的方式进行编码。
基于压缩链表的底层操作
对于使用压缩链表编码方式的有序集合,用户数据都是按照key-score的方式依次存储在压缩链表之中,存储在压缩链表中的key-score数据是按照score分值从小到大进行存储的。在向压缩链表之中插入一个key-socre的时候,需要先从压缩链表的头部向后遍历,找到一个合适的位置,然后在调用两次压缩链表的插入接口,将key-score分别插入到表中。
Redis在src/t_zset.c源文件之中,在压缩链表基础操作的基础上,又进行了一层封装,用于实现有序集合的一些底层操作,这里仅仅简介相关函数的用途,不会对其具体实现进行讲解,相关的内容可以参考跳跃表的文章。
数据的获取与比较
double zzlGetScore(unsigned char *sptr);
sds ziplistGetObject(unsigned char *sptr);
int zzlCompareElements(unsigned char *eptr, unsigned char *cstr, unsigned int clen);
unsigned int zzlLength(unsigned char *zl);
上述这四个函数中:
zzlLength
这个函数用于获取压缩链表中存储的key-score数据的个数。zzlGetScore
,用于从给定的数据指针之中返回其所表示的分值score。ziplistGetObject
,用于从给定的数据指针中返回其所表示的数据key。zzlCompareElements
,用于比较eptr
所表示的key与cstr
和clen
所表示的数据。
数据的遍历
void zzlNext(unsigned char *zl, unsigned char **eptr, unsigned char **sptr);
void zzlPrev(unsigned char *zl, unsigned char **eptr, unsigned char **sptr);
上述两个函数,都是给定一个压缩链表指针,以及对应的key-score的指针,分别会返回前一个key-score或者后一个key-score
的指针
数据的范围操作
int zzlIsInRange(unsigned char *zl, zrangespec *range);
unsigned char *zzlFirstInRange(unsigned char *zl, zrangespec *range);
unsigned char *zzlLastInRange(unsigned char *zl, zrangespec *range);
int zzlLexValueGteMin(unsigned char *p, zlexrangespec *spec);
int zzlLexValueLteMax(unsigned char *p, zlexrangespec *spec);
int zzlIsInLexRange(unsigned char *zl, zlexrangespec *range);
unsigned char *zzlFirstInLexRange(unsigned char *zl, zlexrangespec *range);
unsigned char *zzlLastInLexRange(unsigned char *zl, zlexrangespec *range);
上面的几个函数可以分成两类,分配是用于处理浮点数范围区间zrangespec
以及字典范围区间zlexrangespec
,以浮点树范围区间操作为例:
zzlIsInRange
,这个函数用于判断这个一个有序集合底层的压缩链表是否有一部分落在给定的范围range
内。zzlFirstInRange
,这个函数用于获取压缩链表之中落在range
范围内的第一个元素。zzlLastInRange
,这个函数用于获取压缩链表之中落在range
范围内的最后一个元素。
数据的插入、查找
unsigned char *zzlFind(unsigned char *zl, sds ele, double *score);
unsigned char *zzlInsertAt(unsigned char *zl, unsigned char *eptr, sds ele, double score);
unsigned char *zzlInsert(unsigned char *zl, sds ele, double score);
在这几个函数之中:
zzlFind
这个函数使用顺序查找的方式,从压缩链表之中查找ele
以及score
对应的key-score,如果找到,那么会返回指向key数据的指针,否则返回NULL
指针。zzlInsertAt
用于将一个由ele
和score
构成的key-score插入到eptr
所指向的元素后面,需要注意的是,这个函数是一个比较底层的函数,它在执行的时候,不会对数据的合法性进行校验,需要调用者在调用前保证数据的合法性。zzlInsert
用于向压缩链表中插入一个由ele
和score
构成的key-score,这个函数会在插入前先搜索到合适的插入位置,然后在调用zzlInsertAt
进行数据插入,因此这个函数会保证插入后的压缩链表的顺序。
数据的批量删除
unsigned char *zzlDelete(unsigned char *zl, unsigned char *eptr);
unsigned char *zzlDeleteRangeByScore(unsigned char *zl, zrangespec *range, unsigned long *deleted);
unsigned char *zzlDeleteRangeByLex(unsigned char *zl, zlexrangespec *range, unsigned long *deleted);
unsigned char *zzlDeleteRangeByRank(unsigned char *zl, unsigned int start, unsigned int end, unsigned long *deleted);
zzlDelete
这个函数是删除操作的基础,用于从压缩链表中删除由eptr
所指向的key-score,并返回删除后的压缩链表指针。zzlDeleteRangeByScore
、zzlDeleteRangeByLex
、zzlDeleteRangeByRank
这三个函数都是用于执行批量删除的函数,分别对应按照分值范围、字典范围以及下标范围进行删除,函数会返回删除后的压缩链表指针,同时通过参数deleted
来返回给调用者被删除元素的个数。
有序集合对象的底层操作
首先Redis定义了一个获取有序集合对象大小的接口:
unsigned long zsetLength(const robj *zobj);
这个接口会根据底层编码类型的不同,分别调用zzlLength
或者直接访问zskiplist.length
字段来获取有序集合对象的大小。
接下来,我们来看一下Redis中有序集合的编码转换函数:
void zsetConvert(robj *zobj, int encoding);
void zsetConvertToZiplistIfNeeded(robj *zobj, size_t maxelelen);
在这里,有序集合与其他Redis的对象类型不同的地方在于,其他的具有多种底层编码类型的类型转换都是单一方向的;而有序集合的编码转换则是双向的,也就是说可以实现从压缩链表实现方式转换到跳跃表的编码方式,同时还可以进行反向的转换。这样当有序集合中的元素个数以及单一元素大小已经处于系统的阈值之内,那么我们便可以通过调用zsetConvertToZiplistIfNeeded
这个函数将一个跳跃表数据转化为压缩链表数据实现。
在了解了有序集合的编码转换函数之后,我们可以看一下如何在有序集合之中获取给定元素对应的分值:
int zsetScore(robj *zobj, sds member, double *score);
这个zsetScore
函数用户获取给定member
数据对应的分值,通过参数score
来返回这个分值结果:
- 对于使用
OBJ_ENCODING_SKIPLIST
编码的有序集合,可以直接从zset
的底层哈希表中,通过dictFind
函数,来查找member
对应的分值。 - 对于使用
OBJ_ENCODING_ZIPLIST
编码的有序集合,则调用上面的zzlFind
函数从底层的压缩链表搜索数据。
接下来,我们看一下用于向有序集合之中插入与更新数据的函数接口:
int zsetAdd(robj *zobj, double score, sds ele, int *flags, double *newscore);
这个函数用于向有序集合之中插入一个新的元素,或者更新一个已有元素的分值。根据参数flags
传入的数值不同,这个函数会执行不同的策略:
ZADD_INCR
,在当前元素分值的基础上加上给定的分值,用以更新元素分值;如果对应元素的key不存在,那么就假定其分值为0。ZADD_NX
,拥有这个标记,那么只会在给定的key元素不存在的时候,才会执行操作。ZADD_XX
,拥有这个标记,那么只会在给定的key元素存在的时候,才会执行操作。
同时,操作的结果标记也会通过flags
这个参数返回,而能够返回的标记有:
ZADD_NAN
,这是一个失败的结果,由于分值不是数值所导致。ZADD_ADDED
,元素已经被添加。ZADD_UPDATED
,元素已经被更新。ZADD_NOP
,由于带着ZADD_NX
或者ZADD_XX
标记而导致操作失败。
最终,如果函数执行成功,那么会返回1,同时通过flags
返回标记;如果函数执行发生错误,那么会返回0。在向采用OBJ_ENCODING_ZIPLIST
编码的有序集合中插入新元素的时候,如果有序集合中元素的个数达到阈值,或者单一元素的长度达到阈值,那么会调用zsetConvert
函数,将其转换为OBJ_ENCODING_SKIPLIST
编码格式。
在了解向有序集合之中插入元素的函数之后,我们可以通过下面这个函数来实现从有序集合之中删除一个给定的元素:
int zsetDel(robj *zobj, sds ele);
这个函数在执行成功时,会返回1,当待删除的元素不存在于有序集合中的时候,会返回0。在删除元素的同时,会检查删除
最后便是如何获取一个给定的元素在有序集合之中排序:
long zsetRank(robj *zobj, sds ele, int reverse);
这个函数可以根据reserve
参数的不同,返回一个从0开始的正向排序值,或者一个反向排序值。针对使用OBJ_ENCODING_ZIPLIST
编码的有序集合,会遍历整个底层的压缩链表,通过ziplistCompare
函数,来进行查找;对于使用OBJ_ENCODING_SKIPLIST
的有序集合,则会先从zset
中的哈希表中,查找对应的分值,在使用分值在跳跃表中通过zslGetRank
来查找对应的顺序。
喜欢的同学可以扫描二维码,关注我的微信公众号,马基雅维利incoding