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)
...