一 序
书上本节照例比较简单,介绍两种编码及转换方式。所以还是分为编码跟命令实现两部分。
二 zset
2.1 编码
有序集合对象的底层实现类型如下表:
编码—encoding | 对象—ptr |
---|---|
OBJ_ENCODING_SKIPLIST | 跳跃表和字典实现的有序集合对象 |
OBJ_ENCODING_ZIPLIST | 压缩列表实现的有序集合对象 |
关于底层的数据结构,参见:《redis设计与实现》-第7章压缩列表ziplist ,《redis设计与实现》-第5章跳跃表 ,《redis设计与实现》-4 字典 。
编码为 OBJ_ENCODING_SKIPLIST 的底层的数据结构是 跳跃表和字典。因此就抽象了一个zset结构用来保存这两种结构。源码在server.h
typedef struct zset {
dict *dict; //字典
zskiplist *zsl; //跳跃表
} zset; //有序集合跳跃表类型
- 跳跃表和平衡树的元素都是有序排列,而哈希表不是有序的。因此在哈希表上的查找只能是单个key的查找,不适合做范围查找。
- 跳跃表和平衡树做范围查找时,跳跃表算法简单,实现方便,而平衡树逻辑复杂。
- 查找单个key,跳跃表和平衡树的平均时间复杂度都为O(logN),而哈希表的时间复杂度为O(N)。
-
下面贴一段书上的原话。
为什么有序集合需要同时使用跳跃表和字典来实现?
在理论上来说, 有序集合可以单独使用字典或者跳跃表的其中一种数据结构来实现, 但无论单独使用字典还是跳跃表, 在性能上对比起同时使用字典和跳跃表都会有所降低。
举个例子, 如果我们只使用字典来实现有序集合, 那么虽然以 O(1) 复杂度查找成员的分值这一特性会被保留, 但是, 因为字典以无序的方式来保存集合元素, 所以每次在执行范围型操作 —— 比如 ZRANK 、 ZRANGE 等命令时, 程序都需要对字典保存的所有元素进行排序, 完成这种排序需要至少 O(N \log N) 时间复杂度, 以及额外的 O(N) 内存空间 (因为要创建一个数组来保存排序后的元素)。
另一方面, 如果我们只使用跳跃表来实现有序集合, 那么跳跃表执行范围型操作的所有优点都会被保留, 但因为没有了字典, 所以根据成员查找分值这一操作的复杂度将从 O(1) 上升为 O(\log N) 。
因为以上原因, 为了让有序集合的查找和范围型操作都尽可能快地执行, Redis 选择了同时使用字典和跳跃表两种数据结构来实现有序集合。
redis中实现有序集合的办法是:跳跃表+字典(哈希表)
2.2 结构
书上也有例子,但是我觉得网上的这个图更清晰易懂,就不用书上例子了。例子来自men_ven. 感谢原作者,真实用心画图整理了。我是喜欢偷懒,就是截个书上的图。
我们创建一个视频播放量的有序集合,有体育sport、卡通carton、电视剧telepaly这三个成员
127.0.0.1:6379> ZADD video:view 1234 sport 888 carton 2345 teleplay
(integer) 3
127.0.0.1:6379> ZRANGE video:view 0 -1 WITHSCORES
1) "carton"
2) "88888"
3) "sport"
4) "123456"
5) "teleplay"
6) "234567"
127.0.0.1:6379> OBJECT ENCODING video:view
"ziplist"
我们可以查看到编码类型是压缩列表,这个 video:view 对象的空间结构如下图所示:
当有序集合类型达到配置文件中的阈值条件时,就会发生编码类型的转换,转换为OBJ_ENCODING_SKIPLIST类型,而且转换是双向的,可以相互转换,默认的条件如下
#define OBJ_ZSET_MAX_ZIPLIST_ENTRIES 128 // ziplist中最多存放的节点数
#define OBJ_ZSET_MAX_ZIPLIST_VALUE 64 // ziplist中最大存放的数据长度
还是上面的例子使用OBJ_ENCODING_SKIPLIST编码方式展示可能的空间结构:
注意红色:BJ_ENCODING_SKIPLIST 编码方式的有序集合,他的成员在两种数据结构中是共享的,只是简单的增加了引用计数,实际上内存只有一份成员数据。所以不会造成任何数据重复,浪费内存。
有序集合基于ziplist的接口在server.h
zskiplist *zslCreate(void);//创建一个新的跳跃表。
void zslFree(zskiplist *zsl);//释放给定跳跃表,以及表中包含的所有节点
zskiplistNode *zslInsert(zskiplist *zsl, double score, robj *obj);//skiplist 插入节点
unsigned char *zzlInsert(unsigned char *zl, robj *ele, double score);//将ele元素和分值score插入在ziplist中,从小到大排序
int zslDelete(zskiplist *zsl, double score, robj *obj);//在一个zskiplist中删除一个节点
zskiplistNode *zslFirstInRange(zskiplist *zsl, zrangespec *range);//找到一个zskiplist中第一个出现在range中的节点
zskiplistNode *zslLastInRange(zskiplist *zsl, zrangespec *range);//找到一个zskiplist中最后一个出现在range中的节点
double zzlGetScore(unsigned char *sptr);// 从sptr指向的entry中取出有序集合的分值
void zzlNext(unsigned char *zl, unsigned char **eptr, unsigned char **sptr);// 将当前的元素指针eptr和当前元素分值的指针sptr都指向下一个元素和元素的分值
void zzlPrev(unsigned char *zl, unsigned char **eptr, unsigned char **sptr);// 将当前的元素指针eptr和当前元素分值的指针sptr都指向上一个元素和元素的分值
unsigned int zsetLength(robj *zobj);// 返回有序集合的元素个数
void zsetConvert(robj *zobj, int encoding);// 将有序集合对象的编码转换为encoding制定的编码类型
void zsetConvertToZiplistIfNeeded(robj *zobj, size_t maxelelen);// 按需转换编码成OBJ_ENCODING_ZIPLIST
int zsetScore(robj *zobj, robj *member, double *score);// 将有序集合的member成员的分值保存到score中
unsigned long zslGetRank(zskiplist *zsl, double score, robj *o);//计算排位
2.3有序集合的迭代器
有序集合在t_zset.c中定义了迭代器,迭代器适用于有序集合和集合
// 集合类型的迭代器
typedef struct {
robj *subject; //所属的对象
int type; /* Set, sorted set */ //对象的类型,可以是集合或有序集合
int encoding; //编码
double weight; //权重
union {
/* Set iterators. */ //集合迭代器
union _iterset {
struct { //intset迭代器
intset *is; //所属的intset
int ii; //intset节点下标
} is;
struct { //字典迭代器
dict *dict; //所属的字典
dictIterator *di; //字典的迭代器
dictEntry *de; //当前指向的字典节点
} ht;
} set;
/* Sorted set iterators. */ //有序集合迭代器
union _iterzset { //ziplist迭代器
struct {
unsigned char *zl; //所属的ziplist
unsigned char *eptr, *sptr; //当前指向的元素和分值
} zl;
struct { //zset迭代器
zset *zs; //所属的zset
zskiplistNode *node; //当前指向的跳跃节点
} sl;
} zset;
} iter;
} zsetopsrc;
// 集合迭代器和有序集合迭代器是一个共用体union,因此,不会浪费空间。
在实现迭代的过程中,由于迭代器指向的节点可能数据类型各不相同,有对象、有字符串、有整数,因此Redis又定义了一个保存这些类型的统一结构:源码在t_zset.c
/* Store value retrieved from the iterator. */
// 从集合类型迭代器中恢复存储的值
typedef struct {
int flags;
unsigned char _buf[32]; /* Private buffer. */
// 保存元素的各种类型
robj *ele; //对象
unsigned char *estr; //ziplist
unsigned int elen;
long long ell;
// 保存分值
double score;
} zsetopval;
定义了zsetopval结构,还可以实现内部各种类型的数据进行转换,例如:根据estr的值,转换成ele对象类型。
当然,还为迭代器定义了一些接口,使用起来方便,函数名字都是以zui开头,实现是t_zset.c
2.4. 有序集合的范围限定方式
Redis有序集合有三种范围限定
- 根据字典区间
- 根据排位区间
- 根据分值区间
以上三种主要依赖两种排序方式
- 分值序
- 字典序
以上两种排序所表示的范围,依赖于两个结构,一个规定分值序的范围和边界,一个规定字典序的范围和边界。源码在server.h
/* Struct to hold a inclusive/exclusive range spec by score comparison. */
//分值排序的范围和边界
typedef struct {
double min, max;//最小值,最大值
//minex表示最小值是否包含在这个范围,1表示不包含,0表示包含
//maxex表示最小值是否包含在这个范围,1表示不包含,0表示包含
int minex, maxex; /* are min or max exclusive? */
} zrangespec;
/* Struct to hold an inclusive/exclusive range spec by lexicographic comparison. */
// 字典排序的范围和边界
typedef struct {
robj *min, *max; /* May be set to shared.(minstring|maxstring) 最小值、最大值*/
//minex表示最小值是否包含在这个范围,1表示不包含,0表示包含
//maxex表示最小值是否包含在这个范围,1表示不包含,0表示包含
int minex, maxex; /* are min or max exclusive? */
} zlexrangespec;
三 命令实现
看了下代码,有序集合代码大概是集合的3倍,加注释3000行左右。所以重点关注两类命令。
3.1 求交集zinterstore, 求并集zunionstore
void zunionstoreCommand(client *c) {
zunionInterGenericCommand(c,c->argv[1], SET_OP_UNION);
}
void zinterstoreCommand(client *c) {
zunionInterGenericCommand(c,c->argv[1], SET_OP_INTER);
}
void zunionInterGenericCommand(client *c, robj *dstkey, int op) {
int i, j;
long setnum;
int aggregate = REDIS_AGGR_SUM; //默认聚合方式是求和
zsetopsrc *src;
zsetopval zval;
robj *tmp;
unsigned int maxelelen = 0;
robj *dstobj;
zset *dstzset;
zskiplistNode *znode;
int touched = 0;
/* expect setnum input keys to be given */
//取出要遍历的集合的个数
if ((getLongFromObjectOrReply(c, c->argv[2], &setnum, NULL) != C_OK))
return;//不存在,返回
if (setnum < 1) {
addReplyError(c,
"at least 1 input key is needed for ZUNIONSTORE/ZINTERSTORE");
return;
}
/* test if the expected number of keys would overflow */
if (setnum > c->argc-3) {//参数不匹配,提示语法错误
addReply(c,shared.syntaxerr);
return;
}
/* read keys to be used for input */
//为每一个key创建一个迭代器
src = zcalloc(sizeof(zsetopsrc) * setnum);
for (i = 0, j = 3; i < setnum; i++, j++) {// why j =3?
robj *obj = lookupKeyWrite(c->db,c->argv[j]);//以写的方式读出有序集合
if (obj != NULL) {
if (obj->type != OBJ_ZSET && obj->type != OBJ_SET) {//类型判断是否集合
zfree(src); //释放
addReply(c,shared.wrongtypeerr);//返回client 类型错误
return;
}
//赋值每个集合对象的迭代器
src[i].subject = obj;
src[i].type = obj->type;
src[i].encoding = obj->encoding;
} else {
src[i].subject = NULL;
}
/* Default all weights to 1. */
//默认权重为1.0
src[i].weight = 1.0;
}
/* parse optional extra arguments */
//解析扩展参数
if (j < c->argc) {
int remaining = c->argc - j;
while (remaining) {
//解析权重参数
if (remaining >= (setnum + 1) && !strcasecmp(c->argv[j]->ptr,"weights")) {
j++; remaining--;
//遍历设置的权重参数,设置到集合迭代器中
for (i = 0; i < setnum; i++, j++, remaining--) {
if (getDoubleFromObjectOrReply(c,c->argv[j],&src[i].weight,
"weight value is not a float") != C_OK)
{
zfree(src);
return;
}
}
} else if (remaining >= 2 && !strcasecmp(c->argv[j]->ptr,"aggregate")) {//解析聚合方式aggregate参数
j++; remaining--;
if (!strcasecmp(c->argv[j]->ptr,"sum")) {//求和
aggregate = REDIS_AGGR_SUM;
} else if (!strcasecmp(c->argv[j]->ptr,"min")) {//最小
aggregate = REDIS_AGGR_MIN;
} else if (!strcasecmp(c->argv[j]->ptr,"max")) {//最大
aggregate = REDIS_AGGR_MAX;
} else {//语法错误
zfree(src);
addReply(c,shared.syntaxerr);
return;
}
j++; remaining--;
} else {//不是以上两种参数,发送语法错误信息
zfree(src);
addReply(c,shared.syntaxerr);
return;
}
}
}
/* sort sets from the smallest to largest, this will improve our
* algorithm's performance */
// 对所有集合进行排序(按元素的从小到大),以提高算法性能
qsort(src,setnum,sizeof(zsetopsrc),zuiCompareByCardinality);
//创建结果集对象
dstobj = createZsetObject();
dstzset = dstobj->ptr;
memset(&zval, 0, sizeof(zval)); //初始化保存有序集合元素和分值的结构
if (op == SET_OP_INTER) { // ZINTERSTORE命令
/* Skip everything if the smallest input is empty. */
if (zuiLength(&src[0]) > 0) { // 只处理非空集合
/* Precondition: as src[0] is non-empty and the inputs are ordered
* by size, all src[i > 0] are non-empty too. */
// 初始化元素最少的有序集合
zuiInitIterator(&src[0]);
// 遍历元素最少的有序集合src[0]的每一个元素,将元素的分值和数值保存到zval中
while (zuiNext(&src[0],&zval)) {
double score, value;
// 计算加权分值
score = src[0].weight * zval.score;
if (isnan(score)) score = 0;//非数字则分数为0
// 遍历其后的每一个有序集合,分别跟元素最少的有序集合做运算
for (j = 1; j < setnum; j++) {
/* It is not safe to access the zset we are
* iterating, so explicitly check for equal object. */
// 如果输入的key一样(这里有个条件:在某个 key 输入了两次,并且这个 key 是所有输入集合中基数最小的集合时会出)
//满足这个条件的情况下,那么我们可以直接计算聚合值,不必进行 zuiFind 去确保元素是否出现
if (src[j].subject == src[0].subject) {
// 计算新的加权分值
value = zval.score*src[j].weight;
// 根据集合方式sum、min、max保存结果值到score
zunionInterAggregate(&score,value,aggregate);
//key不同的情况下, 如果能在其他集合找到当前迭代到的元素的话
} else if (zuiFind(&src[j],&zval,&value)) {
value *= src[j].weight;// 计算加权后的分值
zunionInterAggregate(&score,value,aggregate); // 根据集合方式sum、min、max保存结果值到score
} else {// 如果当前元素没出现在某个集合,那么跳出 for 循环,处理下个元素
break;
}
}
/* Only continue when present in every input. */
//走到这里,没有跳出for循环,那么该元素一定是每个key集合中都存在,才将此元素添加进结果集合中
if (j == setnum) {
// 取出值对象
tmp = zuiObjectFromValue(&zval);
znode = zslInsert(dstzset->zsl,score,tmp); //插入到有序集合中
incrRefCount(tmp); /* added to skiplist 加入到跳表*/
dictAdd(dstzset->dict,tmp,&znode->score);//加入到字典中
incrRefCount(tmp); /* added to dictionary */
if (sdsEncodedObject(tmp)) {//如果是字符串编码的对象
if (sdslen(tmp->ptr) > maxelelen)//更新最大长度
maxelelen = sdslen(tmp->ptr);
}
}
}
zuiClearIterator(&src[0]);//释放集合类型的迭代器
}
} else if (op == SET_OP_UNION) { // ZUNIONSTORE命令
dict *accumulator = dictCreate(&setDictType,NULL);//创建一个临时字典作为结果集字典
dictIterator *di;
dictEntry *de;
double score;
if (setnum) {
/* Our union is at least as large as the largest set.
* Resize the dictionary ASAP to avoid useless rehashing. */
// 为了尽可能的减少rehash操作,计算出最大有序集合元素个数,并集至少和最大的集合一样大(上面已经排序过, 最后一个是最大的集合),因此按需扩展结果集字典
dictExpand(accumulator,zuiLength(&src[setnum-1]));
}
/* Step 1: Create a dictionary of elements -> aggregated-scores
* by iterating one sorted set after the other. */
// 1.遍历所有的集合
for (i = 0; i < setnum; i++) {
if (zuiLength(&src[i]) == 0) continue; // 跳过空集合
zuiInitIterator(&src[i]);//初始化集合迭代器
while (zuiNext(&src[i],&zval)) {// 遍历当前集合的所有元素,迭代器当前指向的元素保存在zval中
/* Initialize value */
score = src[i].weight * zval.score; //初始化分值
if (isnan(score)) score = 0;//非数字(可能存在溢出的可能)为0
/* Search for this element in the accumulating dictionary. */
/* 查找元素是否已经在结果集(accumulator)字典中 */
de = dictFind(accumulator,zuiObjectFromValue(&zval));
/* If we don't have it, we need to create a new entry. */
// 如果在结果集中找不到,则加入到结果集中
if (de == NULL) {
tmp = zuiObjectFromValue(&zval);//创建元素对象
/* Remember the longest single element encountered,
* to understand if it's possible to convert to ziplist
* at the end. */
/* 记录元素最长的值, 后面用于判断是否需要对集合进行转换*/
if (sdsEncodedObject(tmp)) { //字符串类型的话要更新maxelelen
if (sdslen(tmp->ptr) > maxelelen)
maxelelen = sdslen(tmp->ptr);
}
/* Add the element with its initial score. */
/* 直接添加到结果集字典中 */
de = dictAddRaw(accumulator,tmp);
incrRefCount(tmp);
dictSetDoubleVal(de,score);//设置元素的分值
} else {// 如果在结果集中可以找到
/* Update the score with the score of the new instance
* of the element found in the current sorted set.
*
* Here we access directly the dictEntry double
* value inside the union as it is a big speedup
* compared to using the getDouble/setDouble API. */
//更新加权分值
zunionInterAggregate(&de->v.d,score,aggregate);
}
}
zuiClearIterator(&src[i]);//清理当前集合的迭代器
}
/* Step 2: convert the dictionary into the final sorted set.
*/ 把字典变成有序集合
// 2. 创建字典的迭代器,遍历结果集字典
di = dictGetIterator(accumulator);
/* We now are aware of the final size of the resulting sorted set,
* let's resize the dictionary embedded inside the sorted set to the
* right size, in order to save rehashing time. */
// 按需扩展结果集合中的字典成员
dictExpand(dstzset->dict,dictSize(accumulator));
// 遍历结果集字典,
while((de = dictNext(di)) != NULL) {
// 获得元素对象和分值
robj *ele = dictGetKey(de);
score = dictGetDoubleVal(de);
// 插入到结果集合的跳跃表中
znode = zslInsert(dstzset->zsl,score,ele);
incrRefCount(ele); /* added to skiplist */
// 添加到结果集合的字典中,注意,这里才是真正的结果集字典,上面accumulator是临时的,不要被注释误会
dictAdd(dstzset->dict,ele,&znode->score);
incrRefCount(ele); /* added to dictionary */
}
dictReleaseIterator(di); //释放字典的迭代器
/* We can free the accumulator dictionary now. */
dictRelease(accumulator);//释放临时保存结果的字典
} else {
serverPanic("Unknown operator");
}
if (dbDelete(c->db,dstkey))// 删除已经存在的dstkey,并做标记
touched = 1;
if (dstzset->zsl->length) {// 如果结果集合的长度不为 0
zsetConvertToZiplistIfNeeded(dstobj,maxelelen); //是否需要进行编码转换
dbAdd(c->db,dstkey,dstobj);//将数据库中的dstkey和dstobj关联成对
addReplyLongLong(c,zsetLength(dstobj)); // 回复结果集合的长度给client
signalModifiedKey(c->db,dstkey);//发送键被修改的信号
notifyKeyspaceEvent(NOTIFY_ZSET,
(op == SET_OP_UNION) ? "zunionstore" : "zinterstore",
dstkey,c->db->id);//发送对应的事件通知 zunionstore or zinterstore
server.dirty++;//更新脏键
} else {//结果集为空
decrRefCount(dstobj);//释放对象
addReply(c,shared.czero);//发送0给client
if (touched) {
signalModifiedKey(c->db,dstkey);
notifyKeyspaceEvent(NOTIFY_GENERIC,"del",dstkey,c->db->id);
server.dirty++;
}
}
zfree(src);//释放集合迭代器数组空间
}
有序集合计算交集的一般算法:先将所有集合按元素个数从小到大排序,然后以元素个数最少的集合,也就是第一个集合为基数,遍历该集合的所有元素,如果元素在之后的所有集合都存在,则加入结果集中。
有序集合计算并集的一般算法:先将所有集合按元素个数从小到大排序,遍历所有的集合的所有元素,将元素添加到结果集中,如果元素已不存在,则添加,否则操作下一个元素。
3.2 zrange:
获取一个位置范围内的元素, 命令格式: ZRANGE key start stop [WITHSCORES], start, stop元素代表位置下标, 从0开始.
下标参数 start 和 stop 都以0为底,也就是说,以0表示有序集第一个成员,以1表示有序集第二个成员,以此类推。
你也可以使用负数下标,以-1表示最后一个成员,-2表示倒数第二个成员,以此类推。
源码在t_zset.c
//获取一个位置范围内的元素, 命令格式: ZRANGE key start stop [WITHSCORES]
void zrangeCommand(client *c) {
zrangeGenericCommand(c,0);
}
void zrevrangeCommand(client *c) {
zrangeGenericCommand(c,1);
}
底层的实现是zrangeGenericCommand.
void zrangeGenericCommand(client *c, int reverse) {
robj *key = c->argv[1];
robj *zobj;
int withscores = 0;
long start;
long end;
int llen;
int rangelen;
//取出start 跟end 参数
if ((getLongFromObjectOrReply(c, c->argv[2], &start, NULL) != C_OK) ||
(getLongFromObjectOrReply(c, c->argv[3], &end, NULL) != C_OK)) return;
//确定是否显示分值
if (c->argc == 5 && !strcasecmp(c->argv[4]->ptr,"withscores")) {
withscores = 1;
} else if (c->argc >= 5) {//语法错误
addReply(c,shared.syntaxerr);
return;
}
// 取出有序集合对象
if ((zobj = lookupKeyReadOrReply(c,key,shared.emptymultibulk)) == NULL
|| checkType(c,zobj,OBJ_ZSET)) return;
/* Sanitize indexes. */
//索引设置为正
llen = zsetLength(zobj);
if (start < 0) start = llen+start;
if (end < 0) end = llen+end;
if (start < 0) start = 0;
/* Invariant: start >= 0, so this test will be true when end < 0.
* The range is empty when start > end or start >= length.
* 索引校验:是否符合条件
*/
if (start > end || start >= llen) {
addReply(c,shared.emptymultibulk);
return;
}
//超出范围:end 设置为集合尾元素
if (end >= llen) end = llen-1;
rangelen = (end-start)+1;
/* Return the result in form of a multi-bulk reply */
addReplyMultiBulkLen(c, withscores ? (rangelen*2) : rangelen);
if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {//ziplist
unsigned char *zl = zobj->ptr;
unsigned char *eptr, *sptr;
unsigned char *vstr;
unsigned int vlen;
long long vlong;
/* ziplist首先找到start位置的元素, 然后依次遍历rangelen个元素, 返回给客户端*/
if (reverse)
eptr = ziplistIndex(zl,-2-(2*start));
else
eptr = ziplistIndex(zl,2*start);
serverAssertWithInfo(c,zobj,eptr != NULL);
sptr = ziplistNext(zl,eptr);
while (rangelen--) {//取出元素
serverAssertWithInfo(c,zobj,eptr != NULL && sptr != NULL);
serverAssertWithInfo(c,zobj,ziplistGet(eptr,&vstr,&vlen,&vlong));
if (vstr == NULL)
addReplyBulkLongLong(c,vlong);
else
addReplyBulkCBuffer(c,vstr,vlen);
if (withscores)
addReplyDouble(c,zzlGetScore(sptr));
if (reverse)
zzlPrev(zl,&eptr,&sptr);
else
zzlNext(zl,&eptr,&sptr);
}
} else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {//skiplist
zset *zs = zobj->ptr;
zskiplist *zsl = zs->zsl;
zskiplistNode *ln;
robj *ele;
/* Check if starting point is trivial, before doing log(N) lookup. */
/* ziplist首先找到start位置的元素, 然后依次遍历rangelen个元素, 返回给客户端*/
if (reverse) {
ln = zsl->tail;
if (start > 0)
ln = zslGetElementByRank(zsl,llen-start);
} else {
ln = zsl->header->level[0].forward;
if (start > 0)
ln = zslGetElementByRank(zsl,start+1);
}
while(rangelen--) {//取出元素
serverAssertWithInfo(c,zobj,ln != NULL);
ele = ln->obj;
addReplyBulk(c,ele);
if (withscores)
addReplyDouble(c,ln->score);
ln = reverse ? ln->backward : ln->level[0].forward;
}
} else {
serverPanic("Unknown sorted set encoding");
}
}
跟跳表有关的是:
/* Finds an element by its rank. The rank argument needs to be 1-based. */
//根据排位在跳跃表中查找元素。排位的起始值为 1 。
zskiplistNode* zslGetElementByRank(zskiplist *zsl, unsigned long rank) {
zskiplistNode *x;
unsigned long traversed = 0;
int i;
x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) {//从高到低遍历
// 遍历跳跃表并累积越过的节点数量
while (x->level[i].forward && (traversed + x->level[i].span) <= rank)
{
traversed += x->level[i].span;
x = x->level[i].forward;
}
// 如果越过的节点数量已经等于 rank,就找到了
if (traversed == rank) {
return x;
}
}
return NULL;
}
看起来代码很长,除了开始的校验之外,是区分了ziplist 跟跳表的。
3.3 ZRANK ZREVRANK命令底层实现
ZRANK key member
返回有序集 key
中成员 member
的排名。其中有序集成员按 score
值递增(从小到大)顺序排列。排名以 0
为底,也就是说, score
值最小的成员排名为 0
。
void zrankCommand(client *c) {
zrankGenericCommand(c, 0);
}
void zrevrankCommand(client *c) {
zrankGenericCommand(c, 1);
}
void zrankGenericCommand(client *c, int reverse) {
robj *key = c->argv[1];
robj *ele = c->argv[2];
robj *zobj;
unsigned long llen;
unsigned long rank;
// 以读操作取出有序集合对象
if ((zobj = lookupKeyReadOrReply(c,key,shared.nullbulk)) == NULL ||
checkType(c,zobj,OBJ_ZSET)) return;
llen = zsetLength(zobj); // 元素数量
serverAssertWithInfo(c,ele,sdsEncodedObject(ele));//参数类型校验
if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {//ziplist
unsigned char *zl = zobj->ptr;
unsigned char *eptr, *sptr;
eptr = ziplistIndex(zl,0);//元素节点地址
serverAssertWithInfo(c,zobj,eptr != NULL);
sptr = ziplistNext(zl,eptr);//分值节点地址
serverAssertWithInfo(c,zobj,sptr != NULL);
rank = 1;//排名位置
while(eptr != NULL) {
// 比较当前元素和member参数的大小
if (ziplistCompare(eptr,ele->ptr,sdslen(ele->ptr)))
break;
rank++;//排名+1
zzlNext(zl,&eptr,&sptr); //指向下个元素和分值
}
// 发送排位值
if (eptr != NULL) {// ZRANK 还是 ZREVRANK ?
if (reverse)
addReplyLongLong(c,llen-rank);
else
addReplyLongLong(c,rank-1);
} else {
addReply(c,shared.nullbulk);
}
} else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {
zset *zs = zobj->ptr;
zskiplist *zsl = zs->zsl;
dictEntry *de;
double score;
ele = c->argv[2];//member参数
de = dictFind(zs->dict,ele); // 从字典中取出保存member的节点地址
if (de != NULL) {
score = *(double*)dictGetVal(de);//获取分值
rank = zslGetRank(zsl,score,ele);//计算排名
serverAssertWithInfo(c,ele,rank); /* Existing elements always have a rank. */
// 发送排位
if (reverse) // ZRANK 还是 ZREVRANK ?
addReplyLongLong(c,llen-rank);
else
addReplyLongLong(c,rank-1);
} else {
addReply(c,shared.nullbulk);
}
} else {
serverPanic("Unknown sorted set encoding");
}
}
其中跳跃表查找排位 方法如下:
/* Find the rank for an element by both score and key.
* Returns 0 when the element cannot be found, rank otherwise.
* Note that the rank is 1-based due to the span of zsl->header to the
* first element.查找包含给定分值和成员对象的节点在跳跃表中的排位。
*/
unsigned long zslGetRank(zskiplist *zsl, double score, robj *o) {
zskiplistNode *x;
unsigned long rank = 0;
int i;
//遍历整个跳跃表
x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) {
// 遍历节点并对比元素
while (x->level[i].forward &&
// 比对分值
(x->level[i].forward->score < score ||
(x->level[i].forward->score == score &&
//分数相同, 比对成员对象
compareStringObjects(x->level[i].forward->obj,o) <= 0))) {
// 累积跨越的节点数量
rank += x->level[i].span;
// 沿着前进指针遍历跳跃表
x = x->level[i].forward;
}
/* x might be equal to zsl->header, so test if obj is non-NULL */
// 必须确保不仅分值相等,而且成员对象也要相等
if (x->obj && equalStringObjects(x->obj,o)) {
return rank;
}
}
return 0;
}
参考:
https://blog.csdn.net/men_wen/article/details/71043205