鍍金池/ 教程/ 大數(shù)據(jù)/ Redis 數(shù)據(jù)結(jié)構(gòu) skiplist
Redis 數(shù)據(jù)淘汰機(jī)制
積分排行榜
小剖 Memcache
Redis 數(shù)據(jù)結(jié)構(gòu) intset
分布式鎖
從哪里開始讀起,怎么讀
Redis 數(shù)據(jù)結(jié)構(gòu) dict
不在浮沙筑高臺
Redis 集群(上)
Redis 監(jiān)視器
源碼閱讀工具
Redis 日志和斷言
內(nèi)存數(shù)據(jù)管理
Redis 數(shù)據(jù)結(jié)構(gòu)綜述
源碼日志
Web 服務(wù)器存儲 session
消息中間件
Redis 與 Lua 腳本
什么樣的源代碼適合閱讀
Redis 數(shù)據(jù)結(jié)構(gòu) sds
Memcached slab 分配策略
訂閱發(fā)布機(jī)制
Redis 是如何提供服務(wù)的
Redis 事務(wù)機(jī)制
Redis 集群(下)
主從復(fù)制
Redis 應(yīng)用
RDB 持久化策略
Redis 數(shù)據(jù)遷移
Redis 事件驅(qū)動詳解
初探 Redis
Redis 與 Memcache
AOF 持久化策略
Redis 數(shù)據(jù)結(jié)構(gòu) redisOb
作者簡介
Redis 數(shù)據(jù)結(jié)構(gòu) ziplist
Redis 數(shù)據(jù)結(jié)構(gòu) skiplist
Redis 哨兵機(jī)制

Redis 數(shù)據(jù)結(jié)構(gòu) skiplist

概述

跳表(skiplist)是一個特俗的鏈表,相比一般的鏈表,有更高的查找效率,其效率可比擬于二叉查找樹。

一張關(guān)于跳表和跳表搜索過程如下圖:

http://wiki.jikexueyuan.com/project/redis/images/redis11.png" alt="" />

在圖中,需要尋找 68,在給出的查找過程中,利用跳表數(shù)據(jù)結(jié)構(gòu)優(yōu)勢,只比較了 3 次,橫箭頭不比較,豎箭頭比較。由此可見,跳表預(yù)先間隔地保存了有序鏈表中的節(jié)點(diǎn),從而在查找過程中能達(dá)到類似于二分搜索的效果,而二分搜索思想就是通過比較中點(diǎn)數(shù)據(jù)放棄另一半的查找,從而節(jié)省一半的查找時間。缺點(diǎn)即浪費(fèi)了空間,自古空間和時間兩難全。

插播一段:跳表在 1990 年由 William Pugh 提出,而紅黑樹早在 1972 年由魯?shù)婪颉へ悹柊l(fā)明了。紅黑樹在空間和時間效率上略勝跳表一籌,但跳表實現(xiàn)相對簡單得到程序猿們的青睞。Redis 和 Leveldb 中都有采用跳表。 這篇文章,借著 Redis 的源碼了解跳表的實現(xiàn)。

跳表的數(shù)據(jù)結(jié)構(gòu)

http://wiki.jikexueyuan.com/project/redis/images/redis12.png" alt="" />

從上圖中,總結(jié)跳表的性質(zhì):

  1. 由很多層結(jié)構(gòu)組成
  2. 每一層都是一個有序的鏈表
  3. 最底層(Level 1) 的鏈表包含所有元素
  4. 如果一個元素出現(xiàn)在 Level i 的鏈表中,則它在 Level i 之下的鏈表也都會出現(xiàn)。
  5. 每個節(jié)點(diǎn)包含兩個指針,一個指向同一鏈表中的下一個元素,一個指向下面一層的元素。

Redis 中跳表數(shù)據(jù)結(jié)構(gòu)定義:

// 跳表節(jié)點(diǎn)結(jié)構(gòu)體
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
    // 節(jié)點(diǎn)數(shù)據(jù)
    robj *obj;
    // 分?jǐn)?shù),游戲分?jǐn)?shù)?按游戲分?jǐn)?shù)排序
    double score;
    // 后驅(qū)指針
    struct zskiplistNode *backward;
    // 前驅(qū)指針數(shù)組TODO
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        // 調(diào)到下一個數(shù)據(jù)項需要走多少步,這在計算rank 的非常有幫助
        unsigned int span;
    } level[];
} zskiplistNode;

typedef struct zskiplist {
    // 跳表頭尾指針
    struct zskiplistNode *header, *tail;
    // 跳表的長度
    unsigned long length;
    // 跳表的高度
    int level;
} zskiplist;

特別的,在上圖中似乎每個數(shù)據(jù)都被保存了多次,其實只保存了一次。

在 struct zskiplistNode 中數(shù)據(jù)和指針是分開存儲的,struct zskiplistLevel 即是一個描述跳表層級的數(shù)據(jù)結(jié)構(gòu)。我們可以看到,一個節(jié)點(diǎn)主要由有一個存儲真實數(shù)據(jù)的指針,一個后驅(qū)指針,和多個前驅(qū)指針。

TODO 可以在這里插入一張表跳表實際數(shù)據(jù)結(jié)構(gòu)的示意圖。

跳表的插入

跳表算法描述如下:找出每一層新插入數(shù)據(jù)位置的前驅(qū)并保存,在 Redis 中跳表插入是根據(jù) score/member 的大?。床欢梢詤⒖?redis ZADD 命令)來決定插入的位置;將新數(shù)據(jù)插入到指定位置,并調(diào)整指針,在 redis 中還會調(diào)整 span。

什么是 span?

http://wiki.jikexueyuan.com/project/redis/images/redis13.png" alt="" />

span 即從兩個相鄰節(jié)點(diǎn)間隔了多少節(jié)點(diǎn)。譬如 level 1,-1 的 span 就是 1;level 2,-1 的 span 為 2。 因為新出入數(shù)據(jù)的層數(shù)是隨機(jī)的,有兩種情況 ① 小于等于原有的層數(shù);② 大于原有的層數(shù)。需要做特殊處理。

1)小于等于原有的層數(shù)

http://wiki.jikexueyuan.com/project/redis/images/redis14.png" alt="" />

redis 中跳表插入算法的具體實現(xiàn):

zskiplistNode *zslInsert(zskiplist *zsl, double score, robj *obj) {
        // update 是插入節(jié)點(diǎn)所在位置的前一個節(jié)點(diǎn)。我們在學(xué)習(xí)鏈表插入的時候,需要找到插入
        // 位置的前一個節(jié)點(diǎn)。因為在跳表中一個節(jié)點(diǎn)是有多個前驅(qū)指針的,所以這里需要保存的
        // 是多個節(jié)點(diǎn),而不是一個節(jié)點(diǎn)
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    unsigned int rank[ZSKIPLIST_MAXLEVEL];
    int i, level;
    redisAssert(!isnan(score));
    x = zsl->header;
    // 遍歷skiplist 中所有的層,找到數(shù)據(jù)將要插入的位置,并保存在update 中
    for (i = zsl->level-1; i >= 0; i--) {
        /* store rank that is crossed to reach the insert position */
        rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
        // 鏈表的搜索
        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] 記錄了新數(shù)據(jù)項的前驅(qū)
        update[i] = x;
    }
    // random 一個level,是隨機(jī)的
    /* we assume the key is not already inside, since we allow duplicated
    * scores, and the re-insertion of score and redis object should never
    * happen since the caller of zslInsert() should test in the hash table
    * if the element is already inside or not. */
    level = zslRandomLevel();
    // random level 比原有的zsl->level 大,需要增加skiplist 的level
    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);
    for (i = 0; i < level; i++) {
        // 新節(jié)點(diǎn)項插到update[i] 的后面
        x->level[i].forward = update[i]->level[i].forward;
        update[i]->level[i].forward = x;
        /* update span covered by update[i] as x is inserted here */
        x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
        update[i]->level[i].span = (rank[0] - rank[i]) + 1;
    }
    // 更高的level 尚未調(diào)整span
    /* increment span for untouched levels */
    for (i = level; i < zsl->level; i++) {
    update[i]->level[i].span++;
    }
    // 調(diào)整新節(jié)點(diǎn)的后驅(qū)指針
    x->backward = (update[0] == zsl->header) ? NULL : update[0];
    if (x->level[0].forward)
        x->level[0].forward->backward = x;
    else
        zsl->tail = x;
    // 調(diào)整skiplist 的長度
        zsl->length++;
    return x;
}

跳表的刪除

跳表的刪除算和插入算法步驟類似:找出每一層需刪除數(shù)據(jù)的前驅(qū)并保存;接著調(diào)整指針,在 Redis 中還會調(diào)整 span。

http://wiki.jikexueyuan.com/project/redis/images/redis15.png" alt="" />

Redis 中跳表刪除算法的具體實現(xiàn):

// x 是需要刪除的節(jié)點(diǎn)
// update 是每一個層x 的前驅(qū)數(shù)組
/* Internal function used by zslDelete, zslDeleteByScore and zslDeleteByRank */
void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) {
    int i;
    // 調(diào)整span 和forward 指針
    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].forward == NULL,只調(diào)整span
        update[i]->level[i].span -= 1;
        }
    }
    // 調(diào)整后驅(qū)指針
    if (x->level[0].forward) {
        x->level[0].forward->backward = x->backward;
    } else {
        zsl->tail = x->backward;
    }
    // 刪除某一個節(jié)點(diǎn)后,層數(shù)level 可能降低,調(diào)整level
    while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL)
        zsl->level--;
    // 調(diào)整跳表的長度
        zsl->length--;
}

Redis 中的跳表

Redis 中結(jié)合跳表(skiplist)和哈希表(dict)形成一個新的數(shù)據(jù)結(jié)構(gòu) zset。添加 dict 是為了快速定位跳表中是否存在某個 member!

typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;

Redis 選用 skiplist 場景

ZXX 命令是針對有序集合(sorted set)的,譬如:

ZADD
ZCARD
ZCOUNT
ZINCRBY
ZINTERSTORE
ZLEXCOUNT
ZRANGE
ZRANGEBYLEX
ZRANGEBYSCORE
ZRANK
ZREM
ZREMRANGEBYLEX
ZREMRANGEBYRANK
ZREMRANGEBYSCORE
ZREVRANGE
ZREVRANGEBYSCORE
ZREVRANK
ZSCAN
ZSCORE
ZUNIONSTORE
Redis