redis-zset数据结构探索
redis用的人比较多,其中zset大家都熟悉,主要用于排名场景。
zset数据结构,分成两部分,一部分是用于排序,一部分用于缓存键值。
先看看结构:
typedef struct zset { dict *dict; //缓存 zskiplist *zsl; //排序结构 } zset;
上面,跳跃表用于排序结构,可以按照名次,积分查找对应键, 时间复杂度: log(n)。
按照名次,积分范围查找一系列键时, 先查询满足条件的第一个键,然后当前键查找后续键, 时间复杂度: log(n) + o(m), n=总键数, m=查询结果键数。
跳跃表结构:
typedef struct zskiplist { struct zskiplistNode *header, *tail; //头结点:用于顺序查询,常用方式; 尾结点:用于倒序简单查询。 unsigned long length; //结点数 int level; //跳跃层级 } zskiplist;
结点结构:
typedef struct zskiplistNode { robj *obj; //键 double score; //积分 struct zskiplistNode *backward; //前一个结点, 和level[0]可看作双链表 struct zskiplistLevel { //跳跃层关系, 每层都是单链表 struct zskiplistNode *forward; //此层下一个结点 unsigned int span; //此层下一个结点和当前结点距离(两者隔了多少结点) } level[]; //最多32层 } zskiplistNode;
查询:
根据名次范围查询
void zrangeGenericCommand(client *c, int reverse) { ...... zset *zs = zobj->ptr; //zset结构变量 zskiplist *zsl = zs->zsl; //跳跃表 zskiplistNode *ln; robj *ele; /* Check if starting point is trivial, before doing log(N) lookup. */ if (reverse) { //是否倒序查询 ln = zsl->tail; //默认取尾结点 if (start > 0) ln = zslGetElementByRank(zsl,llen-start); //如果start>0, 则取对应结点 } else { ln = zsl->header->level[0].forward; //默认取第一个结点 if (start > 0) ln = zslGetElementByRank(zsl,start+1); } while(rangelen--) { //取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; //设置下一个结点 } ...... } /* Finds an element by its rank. The rank argument needs to be 1-based. */ 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) //如果有下一个结点,且下一个结点的名次<=rank { traversed += x->level[i].span; x = x->level[i].forward; } if (traversed == rank) { //找到对应名次的结点 return x; } } return NULL; }
zslGetElementByRank()时间复杂度理想值 = log(n)
如果有删除,添加操作,和查询类似,需要额外维护跳跃表关系。
/* Delete all the elements with score between min and max from the skiplist. * Min and max are inclusive, so a score >= min || score <= max is deleted. * Note that this function takes the reference to the hash table view of the * sorted set, in order to remove the elements from the hash table too. * 根据积分范围删除结点
*/ unsigned long zslDeleteRangeByScore(zskiplist *zsl, zrangespec *range, dict *dict) { zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x; //update维护跳跃表层级关系,用于zslDeleteNode() unsigned long removed = 0; int i; x = zsl->header; for (i = zsl->level-1; i >= 0; i--) { while (x->level[i].forward && (range->minex ? //不満足积分条件时,循环 x->level[i].forward->score <= range->min : x->level[i].forward->score < range->min)) x = x->level[i].forward; update[i] = x; //此层最接近于条件的结点 } /* Current node is the last with score < or <= min. */ x = x->level[0].forward; //第一个最可能满足条件的结点 /* Delete nodes while in range. */ while (x && (range->maxex ? x->score < range->max : x->score <= range->max)) //满足条件的结点 { zskiplistNode *next = x->level[0].forward; zslDeleteNode(zsl,x,update); //更新层级关系 dictDelete(dict,x->obj); //删除缓存 zslFreeNode(x); removed++; x = next; } return removed; } /* Internal function used by zslDelete, zslDeleteByScore and zslDeleteByRank */ void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) { int i; for (i = 0; i < zsl->level; i++) { //更新层级关系 if (update[i]->level[i].forward == x) { update[i]->level[i].span += x->level[i].span - 1; update[i]->level[i].forward = x->level[i].forward; } else { update[i]->level[i].span -= 1; } } if (x->level[0].forward) { //维护当前结点的下一个结点 x->level[0].forward->backward = x->backward; } else { zsl->tail = x->backward; //维护尾结点 } while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL) zsl->level--; //维护层数 zsl->length--; //维护结点数 }
根据键名查找积分, 有了zset->dict这个键值缓存,只需要时间复杂度0(1)
int zsetScore(robj *zobj, robj *member, double *score) { if (!zobj || !member) return C_ERR; if (zobj->encoding == OBJ_ENCODING_ZIPLIST) { //ziplit实现 if (zzlFind(zobj->ptr, member, score) == NULL) return C_ERR; } else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) { zset *zs = zobj->ptr; dictEntry *de = dictFind(zs->dict, member); //找到缓存entry if (de == NULL) return C_ERR; *score = *(double*)dictGetVal(de); //获取对应积分 } else { serverPanic("Unknown sorted set encoding"); } return C_OK; }
如果zset对象用ziplist实现,则查询和删除操作时间复杂度 = o(n)
...