Redisソースコード分析(ジャンプテーブル)

ツリー構造では、一般的なバランスツリーはAVLツリーと赤黒ツリーですが、AVLツリーのバランスが過剰であるため、バランスを維持するためのコストが高すぎてあまり使用されません。ただし、回転アルゴリズムのいくつかはまだ学ぶ価値があります。代わりに、よりバランスの取れた赤黒木が使用されます。STLのマップとセットは赤黒木を使用して実装され、挿入と検索の効率はO(logN)です。

ジャンプテーブルも比較的バランスの取れたデータ構造です。赤黒木とは異なり、ツリー構造ではなくチェーン構造です。ただし、ジャンプテーブルの挿入検索効率もO(logN)であり、赤黒木は黒木戦いがあり、最も重要なことに、ジャンプは赤黒木よりも実装がはるかに簡単です。


配列構造と比較すると、リンクリストの挿入と削除の効率はO(1)ですが、リンクリスト内の要素を見つけたい場合は悪いでしょう。複雑さはO(N)になります。テーブルをジャンプするという概念だけで、検索効率を向上させることができます。いわゆるスキップテーブルは、スキップできるリンクリストです。バイナリ検索アルゴリズムを思い出すと、各検索がスキップされるため、二分法の効率が非常に高くなります。スキップテーブルの設計も、二分法の戦略に基づいています。スキップ検索を実現するには、もちろん、ジャンプリストの要素が順番になっている必要があります
。通常のリンクリストの各ノードは、次のノードへのポインタのみを保存し、次の隣接ノード、つまりジャンプにのみ移動できます。ワンステップ。一度に多くのステップをジャンプするために、ジャンプテーブルはノードの背後にあるさまざまなノードへの多くのポインタを保存します。

server.hヘッダーファイルには、ジャンプテーブルノードの定義とジャンプテーブルの定義があります。

/* 跳表节点 */
typedef struct zskiplistNode {
    
    
    robj *obj;  /* 数据 */
    double score; /* 分数 */
    struct zskiplistNode *backward; //前一个节点指针
    struct zskiplistLevel {
    
    
        struct zskiplistNode *forward; //后面某个节点,也就是next指针
        unsigned int span; //跨度
    } level[]; /* 跳表中保存了多个指向下一个节点的指针 */
} zskiplistNode;

ジャンプテーブルは実際にはキーと値のペアを保存する構造であり、objは実際のデータを保存し、スコアは並べ替えに使用されます。これにより、ジャンプテーブルが正常になります。さらに、レベル配列は次のノードへの複数のポインターを記録し、2つのノード間のスパンも記録します。便宜上、ジャンプテーブルノードの次のノードへの次のポインターは次のポインターと呼ばれ、レベル配列は呼び出されます。次の配列

/* 跳表 */
typedef struct zskiplist {
    
    
    struct zskiplistNode *header, *tail; //表头表尾
    unsigned long length; /* 跳表中节点个数 */
    int level; //跳表总层数
} zskiplist;

テーブルの先頭と末尾はジャンプテーブル定義に記録され、レベルは現在のジャンプテーブルのレイヤーの総数を記録します。
以下はジャンプテーブルの例です。各ノードにいくつかの次のポインタがあることがわかります。これらのポインタを介して、直線でジャンプできます。隣接するノードに移動するだけでなく、移動します。

ジャンプテーブルの操作
ジャンプテーブルの

作成ジャンプテーブルの作成は、zslCreate関数によって実装されます。この関数では、zslCreateNodeを呼び出して、主にメモリを適用して初期値を設定するジャンプテーブルノードを作成する必要があります。

//t_zset.c
/**
 * 创建一个跳表节点
 * level : 节点包含的层数,即节点next数组的大小,每一层有一个next指针 
 * score : 该节点的分值,用于使跳表数据有序
 * obj   : 跳表保存的数据
 **/
zskiplistNode *zslCreateNode(int level, double score, robj *obj) {
    
    
    /* 申请跳表节点内存 */
    zskiplistNode *zn = zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));
    /* 设置初值 */
    zn->score = score;
    zn->obj = obj;
    return zn;
}

/* 创建一个跳表 */
zskiplist *zslCreate(void) {
    
    
    int j;
    zskiplist *zsl;

    /* 申请跳表内存, 初始总层数为1 */
    zsl = zmalloc(sizeof(*zsl));
    zsl->level = 1;
    zsl->length = 0;
    /* 申请表头节点,默认有32层 */
    zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);
    /* 对每一层进行初始化 */
    for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {
    
    
        zsl->header->level[j].forward = NULL;
        zsl->header->level[j].span = 0;
    }
    zsl->header->backward = NULL;
    /* 设置表尾节点 */
    zsl->tail = NULL;
    return zsl;
}

テーブルヘッドノードはスキップテーブルの作成時に適用され、スキップテーブルデータの一部ではないことに注意してください。

検索操作
ジャンプテーブルのほとんどの操作は検索に基づいています。このような要件を考慮すると、ジャンプテーブルでノードを見つけることができます。ノードが存在しない場合は、ノードを挿入する位置を見つけます。Redisは検索機能を実現していませんが、後で説明しますが
、ジャンプテーブルの構造が次のようになっていると仮定すると、挿入および削除機能は検索基づいています。ノードのスキップリストを見つける必要があるため、23
ここに画像の説明を挿入
ステップのルックアップ操作のスコア次のように
層が発見されるスコアで次のノードのスコアを移動して比較を開始する。最上位から
次のノードのスコアは、あまり発見されるスコアより次のノードへの移動があれば
スコア場合次のノードが検出されるスコア以上である場合は、ダウングレードします。この時点で0番目のレイヤーであり、下降を続けることができない場合、現在のノード位置は
検索対象のノードの前のノードです。検索が完了した場合、次に見つかったノードのノードが検索対象のノードではないか、スコア23を挿入する必要がある位置です。

ここに画像の説明を挿入
検索操作ごとに、見つかったかどうかに関係なく、返されるノードはターゲット位置の前のノードであるため、返されるノードの次のノードのスコアと指定されたスコアの関係を判断して知る必要があります。何を見つけるかノードがホップテーブルにあるかどうか。もちろん、ノード21のスコアを見つけるなど、挿入する位置を取得することも可能であり、ノード20への戻り点も同様であり、次のノードはスコアが21の位置であり、挿入位置
挿入操作
挿入操作である。実際に実行されるノードの挿入により、検索機能が1回実行されると、一部のノードの次の配列が破棄されます。したがって、検索プロセスで各レイヤーの前のノードを記録する必要があります。22の挿入を例にとると、各レイヤーの前のノードは7、20、および20の3ノード(実際には2つ)です。

//t_zset.c
/* 在跳表中插入节点,值为score数据为obj */
/* 因为插入节点会破坏原跳表的结构,所以需要先找到会被破坏的那些节点 
 * 被破坏的节点是每一层插入位置的前一个节点,因为它的next数组需要更改 */
zskiplistNode *zslInsert(zskiplist *zsl, double score, robj *obj) {
    
    
    /* update保存每一层插入位置的前一个节点 */
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    unsigned int rank[ZSKIPLIST_MAXLEVEL];
    int i, level;

    serverAssert(!isnan(score));
    x = zsl->header;
    /* 寻找每一层插入位置的前一个节点 */
    for (i = zsl->level-1; i >= 0; i--) {
    
    
        rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
        /* 实际上就是跳表的查找规则
         * 如果当前层上的next指针指向的节点分值大于要查找的分值,则在同层移动
         * 如果当前层上的next指针指向的节点分值小于要查找的分值,则降层,不移动 */
        /* 而如果要查找每一层插入位置的上一个节点,那么降层时的节点就要要找的节点 */
        while (x->level[i].forward &&
            (x->level[i].forward->score < score ||
                (x->level[i].forward->score == score &&
                compareStringObjects(x->level[i].forward->obj,obj) < 0))) {
    
    
            rank[i] += x->level[i].span;
            /* 同层移动 */
            x = x->level[i].forward;
        }
        /* 如果一旦降层,当前节点就是要查找的节点 */
        update[i] = x;
    }
    /* 为新节点随机生成一个层数 */
    level = zslRandomLevel();
    /* 如果随机出的层数大于跳表总层数,那么将跳表扩层 */
    if (level > zsl->level) {
    
    
        for (i = zsl->level; i < level; i++) {
    
    
            rank[i] = 0;
            update[i] = zsl->header;
            update[i]->level[i].span = zsl->length;
        }
        zsl->level = level;
    }
    /* 创建插入的新节点 */
    x = zslCreateNode(level,score,obj);
    /* 因为新节点只存在[0 : level]层,所以对于高于level层的那些节点没有影响 */
    for (i = 0; i < level; i++) {
    
    
        /* 每一层都相当于链表插入 */
        x->level[i].forward = update[i]->level[i].forward;
        update[i]->level[i].forward = x;
        /* 更新跨度span */
        x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
        update[i]->level[i].span = (rank[0] - rank[i]) + 1;
    }
    /* 高层节点的跨度加一 */
    for (i = level; i < zsl->level; i++) {
    
    
        update[i]->level[i].span++;
    }

    /* 设置新节点的前一个节点指针 */
    x->backward = (update[0] == zsl->header) ? NULL : update[0];
    /* 如果插入位置是跳表末尾,更新表尾节点 */
    if (x->level[0].forward)
        x->level[0].forward->backward = x;
    else
        zsl->tail = x;
    /* 节点个数增加 */
    zsl->length++;
    return x;
}

削除操作
削除操作は、最初にジャンプテーブルで削除するノードを検索し、見つかった場合は削除します。削除と挿入は一部のノードの次のポインタを破壊
するため、一致するノードを見つけるためにzslDelete関数を更新する必要があり、zslDeleteNode関数を使用してジャンプテーブルからノードを削除することに注意してください。

//t_zset.c
/* 从跳表中删除节点 */
void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) {
    
    
    int i;
    /* 对于每一层,改变其next数组和跨度 */
    for (i = 0; i < zsl->level; i++) {
    
    
        /* 如果当前节点的当前层的next节点是要删除的节点,改变其next指针和跨度 */
        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--;
}

/* 删除分值score和数据obj匹配的节点 */
int zslDelete(zskiplist *zsl, double score, robj *obj) {
    
    
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    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,obj) < 0)))
            x = x->level[i].forward;
        update[i] = x;
    }

    /* 判断是否存在要删除的节点,x是插入位置前的节点,那么它的next指针就是需要删除的节点 */
    x = x->level[0].forward;
    if (x && score == x->score && equalStringObjects(x->obj,obj)) {
    
    
        /* 如果是,则调用删除节点操作 */
        zslDeleteNode(zsl, x, update);
        zslFreeNode(x);
        return 1;
    }
    return 0; /* not found */
}

ノードのランキングを計算する
Redisジャンプテーブルは、ジャンプテーブル内の特定のデータのランキングを計算できます。これは、zslGetRank関数によって完了されますが、この関数は引き続き検索メソッドを使用します。

//t_zset.c
/* 计算数据o在跳表中的排名 */
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;
        }
        /* 当降层后,说明当前层的下一个节点的值已经大于score了,需要降低一层继续查找
         * 当然也有可能已经找到,所以需要判断是否匹配 */
        if (x->obj && equalStringObjects(x->obj,o)) {
    
    
            return rank;
        }
    }
    return 0;
}

オブジェクトシステムのジャンプテーブルジャンプテーブル
は、順序セットの基礎となる実装として存在します。object.cファイルでは、順序セットの作成時にコードがジャンプテーブルに設定されていることがわかります。

//object.c
/* 创建有序集合对象,底层使用跳表实现 */
robj *createZsetObject(void) {
    
    
    /* 申请有序集合内存 */
    zset *zs = zmalloc(sizeof(*zs));
    robj *o;

    /* 为有序集合创建字典 */
    zs->dict = dictCreate(&zsetDictType,NULL);
    /* 创建有序集合中的跳表 */
    zs->zsl = zslCreate();
    o = createObject(OBJ_ZSET,zs);
    /* 设置编码格式为跳表 */
    o->encoding = OBJ_ENCODING_SKIPLIST;
    return o;
}

まとめ
ジャンプテーブルは、よりバランスの取れたデータ構造であり、実装が簡単で、挿入と削除などはすべて検索に基づいています。移動スパンが大きく、目的地にすばやく到達できるため、最初に上位レベルに移動しようとする検索であることがわかります。上位レベルに移動するための条件が満たされていない場合は、に移動します。最低レベル。

おすすめ

転載: blog.csdn.net/wangrenhaioylj/article/details/108920798