Redis深度歷險-跳錶 Redis深度歷險-跳錶

Redis深度歷險-跳錶

跳錶是一個比較經典的數據結構,非常有用但又不像紅黑樹那麼複雜,非常值得學習;在Ridis中除了提供set這種無序集合,還提供了zset這種有序集合,有序集合就是使用跳錶實現的

跳錶的意義

跳錶是一種結合了二分法和鏈表特性的數據結構,使用鏈表的方式提供快速的插入和刪除,使用二分法的思想快速定位數據。

鏈表

鏈表是最基礎的數據結構,其特性有

  • 在任意位置快速的插入和刪除
  • 不允許隨機訪問數據

跳錶

對於一個有序鏈表來說,在進行插入數據時必須從頭到尾進行遍歷以查找插入的位置,這個時間複雜度時O(n)級別的,對於Redis是不可接受的,而跳錶將查找這個時間複雜度降低爲O(logn)

跳錶是基於鏈表實現的,鏈表無法實現二分法的原因就是無法快速定位中間節點;跳錶的實現原理就是:

  • 維護一個多層次的鏈表
  • 上一層鏈表是下一層的索引鏈表,原始鏈表的每兩個結點有一個結點在索引鏈表當中,並保持順序
  • 在查找時從最上層開始逐層往下查找,每次都能排出底層一半的數據
  • 最終到達底層,底層含有所有數據

跳錶的問題

  最理想的狀態是每一層的三個節點l, m, r,其中m節點剛好屬於下一層l,r的中間,但是在實際當中鏈表不停的變化這樣的實現是非常複雜的,Redis使用的是隨機的方式。

Redis中的跳錶

結構體

typedef struct zskiplistNode {
    sds ele;                                                            //節點數據
    double score;                                                   //節點權值
    struct zskiplistNode *backward;             //前一個節點
    struct zskiplistLevel { 
        struct zskiplistNode *forward;      //下一個節點
        unsigned long span;                             //跨度,到下一個節點的距離
    } level[];
} zskiplistNode;

Redis中並不是純粹的鏈表而是爲了實現zset來的,zset存儲的是字符串而且爲每個字符串存儲一個權值;

  • 一個節點可能會存在於多層中
  • level字段中的forward存儲來了該節點在每一層的下一個節點
  • level字段中的span存儲來該節點到每一層下一個節點的距離
typedef struct zskiplist {
    struct zskiplistNode *header, *tail;        //鏈表頭尾節點
    unsigned long length;                                       //鏈表總長度
    int level;                                                          //此鏈表在第幾層
} zskiplist;
typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;

真正的zset中還使用了一個字典,用來根據字符串查找權值

創建

zskiplist *zslCreate(void) {
    int j;
    zskiplist *zsl;

    zsl = zmalloc(sizeof(*zsl));
    zsl->level = 1;
    zsl->length = 0;
    //創建一個虛擬頭節點,此節點存儲在所有層中
    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;
}

一個跳錶最多32層

插入

zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
    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];
        //按score查找,在score一致的情況下會比較字符串
        while (x->level[i].forward &&
                (x->level[i].forward->score < score ||
                    (x->level[i].forward->score == score &&
                    sdscmp(x->level[i].forward->ele,ele) < 0)))
        {
            rank[i] += x->level[i].span;
            x = x->level[i].forward;
        }
        update[i] = x;
    }
    //update[i]表示:如果數據在第i層插入那麼數據將會插入在update[i]後
    //rank[i]表示:頭節點到update[i]的距離
  
    //新插入的數據最高存儲在level層中,level是隨機的到的
    level = zslRandomLevel();
    //如果插入的數據大於當前高度,更新rank和update以及跳錶的高度
    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;
    }
    
    //創建一個層數爲score的節點
    x = zslCreateNode(level,score,ele);
  
    //更新[0, level)層的數據,插入到update[i]後
    for (i = 0; i < level; i++) {
        //元素插入到節點update[i]後面
        x->level[i].forward = update[i]->level[i].forward;
        update[i]->level[i].forward = x;
        
        //在第0層存儲的是所有數據, 而新元素插入在update[i]後,頭節點到新元素的距離是rank[0]+1
        //新元素插入到update[i]後,所以update[i]的跨度變爲到新元素的距離,即頭節點到新元素(rank[0]+1)的距離減去rank[i]
        
        //新元素的跨度就是到update[i]下一個元素的距離,這就是一個簡單的減法了
        x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
        update[i]->level[i].span = (rank[0] - rank[i]) + 1;
    }

    //更新update[i]的跨度數據
    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;
}

這裏的跨度問題會導致整個流程變得複雜,不過了解span是什麼意思就可以了,不必過分深究實現細節;

重要的是這裏使用的zslCreateNode(level,score,ele)隨機算法

層數隨機算法

每隔節點層數的選擇是非常關鍵的,如果選的不好可能導致跳錶的退化

int zslRandomLevel(void) {
    int level = 1;
    //ZSKIPLIST_P等於0.25
    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
        level += 1;
    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

這裏使用了很簡單的算法(random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF)的概率是1/4,即意味着從下一層到上一層的概率約爲1/4,類似於一顆四叉樹

範圍刪除元素

unsigned long zslDeleteRangeByRank(zskiplist *zsl, unsigned int start, unsigned int end, dict *dict) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    unsigned long traversed = 0, removed = 0;
    int i;
        
    //從上往下遍歷,traversed存儲的是頭節點到節點x的距離
    //update[i]存儲的是第i層距離小於start的元素,再往後的節點從頭節點到此節點的距離就大雨start了
    x = zsl->header;
    for (i = zsl->level-1; i >= 0; i--) {
        while (x->level[i].forward && (traversed + x->level[i].span) < start) {
            traversed += x->level[i].span;
            x = x->level[i].forward;
        }
        update[i] = x;
    }
        
   //從x的下一個元素開始刪除,直到距離大於end
    traversed++;
    x = x->level[0].forward;
    while (x && traversed <= end) {
        zskiplistNode *next = x->level[0].forward;
        zslDeleteNode(zsl,x,update);
        dictDelete(dict,x->ele);
        zslFreeNode(x);
        removed++;
        traversed++;
        x = next;
    }
    return removed;
}

此函數刪除的是整個集合第start到第end個元素,這個時候span字段就顯示出作用來了,在刪除時不涉及任何層數的調整

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章