Redis跳跃表详解
1.前言
??自己學(xué)跳躍表是因?yàn)楫?dāng)初聽(tīng)人說(shuō)想要找一份高薪工作, Redis跳躍表是要知道的. 當(dāng)時(shí)學(xué)的時(shí)候也是網(wǎng)上的文章反復(fù)看, 花了幾個(gè)晚上才徹底弄明白, 所以在此記錄一下吧, 為了下次面試好回顧
2.跳躍表基本概念準(zhǔn)備
跳躍表是有序集合(zset)的底層實(shí)現(xiàn)之一。
2.1跳躍表的數(shù)據(jù)結(jié)構(gòu)
跳躍表zskiplist定義在server.h中
header; 跳躍表的表頭節(jié)點(diǎn)
tail: 指向跳躍表的表尾節(jié)點(diǎn)
level: 記錄目前跳躍表內(nèi), 層數(shù)最大的那個(gè)節(jié)點(diǎn)的層數(shù)(表頭節(jié)點(diǎn)的層數(shù)不計(jì)算在內(nèi). 因?yàn)樗膶訑?shù)為level31: 從level0開(kāi)始, 所以是32層)
length: 記錄跳躍表的長(zhǎng)度, 也就是, 跳躍表目前包含節(jié)點(diǎn)的數(shù)量(表頭節(jié)點(diǎn)不計(jì)算在內(nèi))
2.2跳躍表節(jié)點(diǎn)的數(shù)據(jù)結(jié)構(gòu)
跳躍表的節(jié)點(diǎn)zskiplistNode定義在server.h中, 定義如下
robj: RedisObject的別名, 在跳躍表中它的類(lèi)型是sds字符串
score: 浮點(diǎn)類(lèi)型的數(shù)值
backward: 后退指針, 指向跳躍表當(dāng)前節(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn)的指針
level[]: 數(shù)組中的一個(gè)元素包含下列兩項(xiàng)
??forward: 前進(jìn)指針
??span: 當(dāng)前層跨越的節(jié)點(diǎn)數(shù)量
節(jié)點(diǎn)按分值從低到高排列, 分值相同時(shí)按 robj字典順序排列, 而不是按對(duì)象本身的大小
一個(gè)節(jié)點(diǎn)在每一層都有一個(gè)forward指針(各層的forward指針可能相同, 可能不同)
如圖:
那么對(duì)應(yīng)節(jié)點(diǎn)8而言:
??節(jié)點(diǎn)8的level0的forward為節(jié)點(diǎn)9, span值為1
??節(jié)點(diǎn)8的level1的forward為節(jié)點(diǎn)12, span值為4
??節(jié)點(diǎn)8的level2的forward為節(jié)點(diǎn)16, span值為8
所以, forward指針指向的是當(dāng)前節(jié)點(diǎn)的當(dāng)前l(fā)evel層所能指向的最右的節(jié)點(diǎn)(其最大的level層 >= 當(dāng)前節(jié)點(diǎn)的當(dāng)前l(fā)evel層), 而span就是這個(gè)過(guò)程中跨越的節(jié)點(diǎn)數(shù). 將上述節(jié)點(diǎn)8的三個(gè)level層依次帶入, 應(yīng)該就能理解上面這句話(huà)了.
2.3Rank
它代表每個(gè)節(jié)點(diǎn)在跳躍表中的相對(duì)位置(類(lèi)似數(shù)組下標(biāo))
如上圖中節(jié)點(diǎn)8的rank值為 8 , 從第一個(gè)節(jié)點(diǎn)開(kāi)始(不包含頭節(jié)點(diǎn)) rank = 1, 然后依次類(lèi)推.
3.跳躍表的創(chuàng)建
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;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; }其中:
??ZSKIPLIST_MAXLEVEL,這個(gè)是跳躍表的最大層數(shù),源碼里通過(guò)宏定義設(shè)置為了32,也就是說(shuō),節(jié)點(diǎn)再多,也不會(huì)超過(guò)32層(level31)
??header節(jié)點(diǎn)的初始化: 創(chuàng)建跳躍表時(shí), 初始化的header節(jié)點(diǎn)的level數(shù)組是有32層(level31)的, 且每一層的forward指向的都是null, 這里的知識(shí)在后面節(jié)點(diǎn)插入時(shí)會(huì)用到.
??而一般節(jié)點(diǎn)的level層數(shù)是在節(jié)點(diǎn)插入到跳躍表時(shí) 隨機(jī)給定的, 根據(jù)一個(gè)隨機(jī)算法:
上圖是隨機(jī)給定每一層的概率
4.跳躍表的插入
??終于到了最關(guān)鍵的部分, 這里弄明白, 跳躍表也就拿下了. 下面給出插入函數(shù):
zskiplistNode *zslInsert(zskiplist *zsl, double score, robj *obj) {// 記錄尋找元素過(guò)程中,每層能到達(dá)的最右節(jié)點(diǎn)zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;// 記錄尋找元素過(guò)程中,每層所跨越的節(jié)點(diǎn)數(shù)unsigned int rank[ZSKIPLIST_MAXLEVEL];int i, level;redisAssert(!isnan(score));x = zsl->header;// 記錄沿途訪(fǎng)問(wèn)的節(jié)點(diǎn),并計(jì)數(shù) span 等屬性// 平均 O(log N) ,最壞 O(N)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];// 右節(jié)點(diǎn)不為空while (x->level[i].forward && // 右節(jié)點(diǎn)的 score 比給定 score 小(x->level[i].forward->score < score || // 右節(jié)點(diǎn)的 score 相同,但節(jié)點(diǎn)的 member 比輸入 member 要小(x->level[i].forward->score == score && compareStringObjects(x->level[i].forward->obj,obj) < 0))) {// 記錄跨越了多少個(gè)元素rank[i] += x->level[i].span;// 繼續(xù)向右前進(jìn)x = x->level[i].forward;}// 保存訪(fǎng)問(wèn)節(jié)點(diǎn)update[i] = x;}/* we assume the key is not already inside, since we allow duplicated* scores, and the re-insertion of score and redis object should never* happpen since the caller of zslInsert() should test in the hash table* if the element is already inside or not. */// 因?yàn)檫@個(gè)函數(shù)不可能處理兩個(gè)元素的 member 和 score 都相同的情況,// 所以直接創(chuàng)建新節(jié)點(diǎn),不用檢查存在性// 計(jì)算新的隨機(jī)層數(shù)level = zslRandomLevel();// 如果 level 比當(dāng)前 skiplist 的最大層數(shù)還要大// 那么更新 zsl->level 參數(shù)// 并且初始化 update 和 rank 參數(shù)在相應(yīng)的層的數(shù)據(jù)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;}// 創(chuàng)建新節(jié)點(diǎn)x = zslCreateNode(level,score,obj);// 根據(jù) update 和 rank 兩個(gè)數(shù)組的資料,初始化新節(jié)點(diǎn)// 并設(shè)置相應(yīng)的指針// O(N)for (i = 0; i < level; i++) {// 設(shè)置指針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 */// 設(shè)置 spanx->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);update[i]->level[i].span = (rank[0] - rank[i]) + 1;}/* increment span for untouched levels */// 更新沿途訪(fǎng)問(wèn)節(jié)點(diǎn)的 span 值for (i = level; i < zsl->level; i++) {update[i]->level[i].span++;}// 設(shè)置后退指針x->backward = (update[0] == zsl->header) ? NULL : update[0];// 設(shè)置 x 的前進(jìn)指針if (x->level[0].forward)x->level[0].forward->backward = x;else// 這個(gè)是新的表尾節(jié)點(diǎn)zsl->tail = x;// 更新跳躍表節(jié)點(diǎn)數(shù)量zsl->length++;return x; }那么如何理解上述函數(shù)就是關(guān)鍵了.
4.1兩個(gè)關(guān)鍵的數(shù)組
這兩個(gè)數(shù)組一定要理解!!!
update[]: 數(shù)組的每個(gè)元素update[i] --> XXX節(jié)點(diǎn): XXX節(jié)點(diǎn)的第i層的forward指向插入節(jié)點(diǎn)
rank[]: 每一層中XXX節(jié)點(diǎn)的rank值(后面用來(lái)計(jì)算span的)
update[]: 記錄尋找元素過(guò)程中, 每層能到達(dá)的最左節(jié)點(diǎn)(XXX節(jié)點(diǎn))
rank[]: 最左節(jié)點(diǎn)的rank
補(bǔ)充: 最左節(jié)點(diǎn)是對(duì)插入節(jié)點(diǎn)而言, 如果根據(jù)后面介紹的, 從header開(kāi)始遍歷, 尋找update[i], 則可以看成最右節(jié)點(diǎn).
4.2插入一個(gè)新節(jié)點(diǎn)時(shí)涉及的操作
??前面說(shuō)過(guò), 跳躍表插入節(jié)點(diǎn)時(shí), level是隨機(jī)給的, 我們這里假設(shè)插入節(jié)點(diǎn)的分值為9.5, 隨機(jī)生成的層數(shù)是2, 那么此時(shí)跳躍表如圖:
對(duì)跳躍表而言, 插入一個(gè)新節(jié)點(diǎn)所涉及的操作有:
??插入節(jié)點(diǎn)每一層的forward和span獲取
??每一層最左節(jié)點(diǎn)的forward和span更新
插入節(jié)點(diǎn)9.5之后:
level0:
??插入節(jié)點(diǎn)的forward可以根據(jù)score排序得到, span為1
??update[0]是插入節(jié)點(diǎn)的backward, span為1
level1:
??8 ~ 12之間的span = span8span_{8}span8? + 1 =>
??8 ~ 9.5 + 9.5 ~ 12 = span8span_{8}span8? + 1 =>
??求出8 ~ 9.5,也就同樣能求出 9.5 ~ 12
??8 ~ 9.5 = 8 ~ 9 + 1
??????=rank(節(jié)點(diǎn)9) - rank(節(jié)點(diǎn)8) + 1
??span8span_{8}span8?表示的是插入之前, 節(jié)點(diǎn)8在level1的span值
對(duì)上述公式的解釋:
??我要更新插入節(jié)點(diǎn)的level1層的最左節(jié)點(diǎn)的forward和span, forward為update[1], span就需要根據(jù)上述公式計(jì)算得到, 而剛好節(jié)點(diǎn)9為update[0], 節(jié)點(diǎn)8為update[1], 所以:
???=rank(update[0]) - rank(update[1]) + 1
同理: 對(duì)于更新插入節(jié)點(diǎn)的第i層的最左節(jié)點(diǎn)的span值
??抽象為: rank(update[0]) - rank(update[level.i]) + 1
??????=rank(0) - rank(i) + 1
這個(gè)公式求得的是: 插入節(jié)點(diǎn)的第i層對(duì)應(yīng)的最左節(jié)點(diǎn)的第i層span值.
此時(shí)我們?cè)賮?lái)看看這段:
每一層對(duì)應(yīng)的最左節(jié)點(diǎn)的forward更新為update[i], span可以根據(jù)rank(0) - rank(i) + 1計(jì)算來(lái)更新
插入節(jié)點(diǎn)第i層的forward為update[i]在插入之前的forward(插入之后update[i]的第i層forward指向的是插入節(jié)點(diǎn)), span值獲取: “8 ~ 9.5根據(jù)公式計(jì)算得到了, 9.5 ~ 12自然也就知道了”. 這么說(shuō)不知道能不能理解.
而這個(gè)計(jì)算公式在源碼中也有出處:
所以關(guān)鍵就是找到插入節(jié)點(diǎn)的update[] 和 rank[].
4.2遍歷,記錄update和rank
對(duì)應(yīng)源碼:
// 記錄尋找元素過(guò)程中,每層能到達(dá)的最右節(jié)點(diǎn)zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;// 記錄尋找元素過(guò)程中,每層所跨越的節(jié)點(diǎn)數(shù)unsigned int rank[ZSKIPLIST_MAXLEVEL];int i, level;redisAssert(!isnan(score));x = zsl->header;// 記錄沿途訪(fǎng)問(wèn)的節(jié)點(diǎn),并計(jì)數(shù) span 等屬性// 平均 O(log N) ,最壞 O(N)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];// 右節(jié)點(diǎn)不為空while (x->level[i].forward && // 右節(jié)點(diǎn)的 score 比給定 score 小(x->level[i].forward->score < score || // 右節(jié)點(diǎn)的 score 相同,但節(jié)點(diǎn)的 member 比輸入 member 要小(x->level[i].forward->score == score && compareStringObjects(x->level[i].forward->obj,obj) < 0))) {// 記錄跨越了多少個(gè)元素rank[i] += x->level[i].span;// 繼續(xù)向右前進(jìn)x = x->level[i].forward;}// 保存訪(fǎng)問(wèn)節(jié)點(diǎn)update[i] = x;}update和rank數(shù)組的值可以通過(guò)一次逐層的遍歷確定
從跳躍表當(dāng)前的最大層數(shù)開(kāi)始遍歷, 遍歷到最底層為止.
?????update[]??????rank[]
length - 1層:
??X的head的level-1層(從0層開(kāi)始, 這里的level是指跳躍表中當(dāng)前的最大層數(shù))指向節(jié)點(diǎn)X1(當(dāng)前跳躍表內(nèi)最高層), 判斷:
??1.X1的level-1層的forward是否為null
????如:從 X1到表尾的所有節(jié)點(diǎn)的level層數(shù)都小于 X1的level層數(shù), 此時(shí)length - 1 層的forward為null
??2.X1的score是否比插入節(jié)點(diǎn)的大
??滿(mǎn)足其中之一 --> 跳出循環(huán)
??update[length -1] = header, rank[length - 1] = 0
??不滿(mǎn)足其中之一 -->
??找到X1的length-1層的forward指向的節(jié)點(diǎn)X2(能到這 說(shuō)明: X1與X2的level層數(shù)相等的), 判斷:
??是否滿(mǎn)足上述條件之一
??滿(mǎn)足 --> 跳出循環(huán)
??update[length - 1] = X1, rank[length - 1] = X1在表中的rank值
length - 2層:
??X1的次高層的forward指向節(jié)點(diǎn)X3, 判斷:
??…
??不滿(mǎn)足 -->
??X3的最高層forward指向的節(jié)點(diǎn) X4滿(mǎn)不滿(mǎn)足, X4的最高層forward指向的節(jié)點(diǎn)滿(mǎn)不滿(mǎn)足 假設(shè)滿(mǎn)足:
??update[length - 2] = X4, rank[length - 2] = X4在跳躍表的rank值
length -3層:
??X4的次高層…
此時(shí)我們得到完整的update[] 和 rank[]
結(jié)合圖示理解:
保姆級(jí)說(shuō)明:
結(jié)合上述兩幅圖, 在插入節(jié)點(diǎn)后是如何通過(guò)一次遍歷獲取update[], rank[]數(shù)組的:
??1.首先在插入9.5節(jié)點(diǎn)之前, 跳躍表中l(wèi)evel的值為3, 所以我們來(lái)到header節(jié)點(diǎn)的level-1層, 也就是level2, 其中的forward指向的是節(jié)點(diǎn)8(為什么header的level2中會(huì)有值, 下面會(huì)說(shuō)明, 別急)
??2.判斷循環(huán)條件, 不滿(mǎn)足, 繼續(xù). 節(jié)點(diǎn)8的level2層的forward指向節(jié)點(diǎn)16, 滿(mǎn)足. 此時(shí)update[2] = 節(jié)點(diǎn)8, rank[2] = 8
??3.從節(jié)點(diǎn)8的次高層level1出發(fā), forward指向的是節(jié)點(diǎn)12, 判斷循環(huán)條件, 滿(mǎn)足. 此時(shí)update[1] = 節(jié)點(diǎn)8, rank[1] = 8
??4.從節(jié)點(diǎn)8的次次高層level0出發(fā), forward指向的是節(jié)點(diǎn)9, 判斷循環(huán)條件, 不滿(mǎn)足, 繼續(xù), 節(jié)點(diǎn)9的最高層forward指向節(jié)點(diǎn)10, 判斷循環(huán)條件, 滿(mǎn)足, update[0] = 節(jié)點(diǎn)9, rank[0] = 9
??如果上述內(nèi)容都理解了, 相信跳躍表的插入流程在腦子里大致是能跑同了, 但感覺(jué)還有些模糊地帶, 接下來(lái)我們就來(lái)探索這些模糊地帶, 全面理解Redis跳躍表.
5.如果插入節(jié)點(diǎn)隨機(jī)生成的層數(shù)比當(dāng)前跳躍表的最大層數(shù)大
??上文中提到過(guò): 為什么header的level2中會(huì)有值. 接下來(lái)我們就來(lái)解釋這個(gè)問(wèn)題
??當(dāng)插入節(jié)點(diǎn)隨機(jī)生成的層數(shù)(j)比當(dāng)前跳躍表的最大層數(shù)(level)大, 對(duì)于update[], rank[]中 數(shù)組下標(biāo)≤ level 的依然可以通過(guò)一次遍歷得到, 而對(duì)于數(shù)組下標(biāo): level <數(shù)組下標(biāo) ≤ j的, 此時(shí)顯然:
??update[level + …] = header, rank[level+ …] = 0
??多出來(lái)的這些層的header的span:
????= rank[0] - rank[i] + 1
????=rank[0] + 1
而對(duì)于插入節(jié)點(diǎn)的那些 大于等于level層的forward = null, 所以也就沒(méi)有span值
6.更新未涉及到的層
??如果隨機(jī)生成的層數(shù)小于之前跳躍表中的層數(shù), 那么大于隨機(jī)生成的層數(shù)在創(chuàng)建新節(jié)點(diǎn)的過(guò)程中就沒(méi)有被操作到(比如上述中, update[2] = 節(jié)點(diǎn)8 就沒(méi)有被使用到,), 對(duì)于這些沒(méi)有操作到的層, 里面的update節(jié)點(diǎn)對(duì)應(yīng)的span應(yīng)當(dāng)+1(因?yàn)椴迦肓艘粋€(gè)新節(jié)點(diǎn)), forward不變.
??至此: 更新每一個(gè)update[i]的forward和span完成
??“物理的科學(xué)大廈已經(jīng)建成, 剩下的只有大廈旁的兩朵烏云, 后人只需在此基礎(chǔ)上修補(bǔ), 完善”. 原話(huà)找不到了, 湊合用吧!
7.設(shè)置后繼指針
??針對(duì)每一層的調(diào)整已經(jīng)全部完成了, 也就是level數(shù)組已經(jīng)搞定, 接下來(lái), 處理一個(gè)backward指針, 首先新節(jié)點(diǎn)的backward要指向前一個(gè)節(jié)點(diǎn), 然后, 新節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)要將backward指向新節(jié)點(diǎn).
8.更新跳躍表節(jié)點(diǎn)個(gè)數(shù)
??最后, 全部搞定, 把跳躍表個(gè)數(shù)加1即可, 理解了插入, 對(duì)跳躍表可以說(shuō)是基本掌握了. 因?yàn)檎莆樟瞬迦? 也就掌握了跳躍表的其他操作(如 刪除).
??其實(shí)就是覺(jué)得為了面試學(xué)到這, 用來(lái)裝逼應(yīng)該足夠了. 當(dāng)然時(shí)間夠的還可以了解一下Redis的一致性hash, Redis的內(nèi)存淘汰機(jī)制, 這兩個(gè)理解起來(lái)會(huì)容易很多.
Redis的一致性hash:
https://blog.csdn.net/qq_21125183/article/details/90019034?
Redis的內(nèi)存淘汰機(jī)制: 第9題有稍微說(shuō)明
https://blog.csdn.net/weixin_43179522/article/details/109318370
9.補(bǔ)充
??跳躍表是一種隨機(jī)化的數(shù)據(jù)結(jié)構(gòu), 在查找, 插入和刪除這些字典操作上, 其效率可比擬于平衡二叉樹(shù)(如紅黑樹(shù)), 大多數(shù)操作只需要O(log n)平均時(shí)間. 為什么時(shí)間能從O(n)提升到O(log n)
??用下圖舉個(gè)例子吧:
??當(dāng)我們要查找新插入的節(jié)點(diǎn)9.5時(shí), 如果不是用的跳躍表, 那么需要從節(jié)點(diǎn)1開(kāi)始, 找到節(jié)點(diǎn)2, 然后一直找到節(jié)點(diǎn)9.5, 花費(fèi)的時(shí)間為10.
??而如果是跳躍表結(jié)構(gòu), 我們從header的level-1層出發(fā), 根據(jù)span和forward來(lái)到節(jié)點(diǎn)8, 發(fā)現(xiàn)節(jié)點(diǎn)8的score < 節(jié)點(diǎn)9.5的score, 然后我們接著從節(jié)點(diǎn)8的level2出發(fā), 來(lái)到節(jié)點(diǎn)16, 發(fā)現(xiàn)節(jié)點(diǎn)16score > 節(jié)點(diǎn)9.5的score, 所以我們重新從節(jié)點(diǎn)8的level1出發(fā), 來(lái)到節(jié)點(diǎn)9.5. 找到節(jié)點(diǎn), 返回結(jié)果. 時(shí)間花費(fèi): header -> 節(jié)點(diǎn)8 -> 節(jié)點(diǎn)16, 節(jié)點(diǎn)8 -> 節(jié)點(diǎn)9.5 = 3
??總結(jié): 跳躍表根據(jù)level數(shù)組, 相當(dāng)于為跳躍表構(gòu)建了多級(jí)索引, 每一層level都是一級(jí)索引, 從而減少了查詢(xún)時(shí)間.
10.參考鏈接
https://blog.csdn.net/u013536232/article/details/105476382?
https://blog.csdn.net/weixin_30398227/article/details/94981429?
https://blog.csdn.net/universe_ant/article/details/51134020?
??在此對(duì)這三篇博客的作者提供的思路表示感謝.
??對(duì)了, 本來(lái)學(xué)這個(gè)是想面試中裝逼用的, 結(jié)果愣是沒(méi)人問(wèn). 我真的! 哎, 就像沈佳宜說(shuō)的: “人生本來(lái)很多努力都是徒勞無(wú)功的”. 至少它很難, 但你學(xué)會(huì)了!!!
??最后, 如果覺(jué)得通過(guò)這篇博客理解了Redis跳躍表, 求個(gè)點(diǎn)贊, 收藏. 寫(xiě)博客后才發(fā)現(xiàn), 碼字也不是個(gè)輕松活.
總結(jié)
以上是生活随笔為你收集整理的Redis跳跃表详解的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Redis面试题相关知识整理
- 下一篇: javaweb学习总结(三十九):数据库