目錄
1.跳躍表節點 zskiplistNode -源碼位置 redis/src/server.h
2.跳躍表結構 zskiplist -源碼位置 redis/src/server.h
3.zset 有序集合的新增 -源碼位置redis/src/t_zet.c
4..zset 有序集合的刪除 -源碼位置redis/src/t_zet.c
5.刪除跳躍表 -源碼位置redis/src/t_zet.c
說到跳躍表需要先對有序鏈表做一定了解
1.有序鏈表
我們知道在有序鏈表中元素都是有序排列的,平均時間複雜度是O(N), 並且每個節點都有指向下一節點的指針,最後一個節點指向NULL ,查詢時間複雜度是O(n)。 插入和刪除的操作也都是需要找到合適位置再做next 指針的修改操作。
對於圖中的有序鏈表,如果我們要查詢到值爲27的節點,是需要比較4次纔可以。 2 --> 14--> 19--> 27
那如果對於有序鏈表我們做了分層,每一層都是一個有序鏈表,並且在查找時候從最高層級開始,比較節點後如果大於查找值或是Next 指向NULL ,則降一層級查找。這樣查詢的方式效率會有部分提升的。
2.跳躍表
在跳躍表中查詢值爲27 的節點。
1.第一步: 比較第二層 第0個節點是2,小於 27 ,向後比較第二層第1 個節點是19,小於27 。繼續比較發現19 next 指向NULL
2.第二步:跳躍到第1層值爲19的節點next 指向 52,大於27 ;
3.第三步:跳躍到第0層,節點27就是要查詢的節點,結束。
總結是比較了3次 。 2 --> 19 --> 52 ;
如果節點數量比較多時,跳過節點越多查詢效率會大大提升。
以上就是跳躍表的設計思想。
3.跳躍表的應用
在Redis 中,跳躍表主要用於有序集合zset的底層實現 ;另一種有序集合的底層實現是壓縮列表。在Redis 源碼中我們可以看到兩中數據結構的轉換使用過程。
源碼文件位置redis/src/t_zset.c
/**
* 當滿足 server.zset_max_ziplist_entries == 0 或者 server.zset_max_ziplist_value
* 小於要插入元素的字符串長度 時,則使用跳躍表結構,否則使用壓縮列表結構
*/
if (server.zset_max_ziplist_entries == 0 ||
server.zset_max_ziplist_value < sdslen(c->argv[scoreidx+1]->ptr))
{
zobj = createZsetObject(); // 創建跳躍表結構
} else {
zobj = createZsetZiplistObject(); //創建壓縮列表結構
}
/**
* 當zset 中元素個數大於 server.zset_max_ziplist_entries 或者 插入元素的字符串長度大於server.zset_max_ziplist_value 時,會將zset 的底層實現由壓縮列表轉爲跳躍表。
* 目前在zset 轉爲跳躍表之後,即時元素被刪除,也不會重新轉爲壓縮列表
*/
if (zzlLength(zobj->ptr) > server.zset_max_ziplist_entries ||
sdslen(ele) > server.zset_max_ziplist_value)
zsetConvert(zobj,OBJ_ENCODING_SKIPLIST);
4.源碼分析
1.跳躍表節點 zskiplistNode -源碼位置 redis/src/server.h
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
sds ele; // 存儲字符串類型數據
double score; // 存儲排序的分值
struct zskiplistNode *backward; //後退指針,指向當前節點最底層的前一個節點,頭節點指向NULL,從後向前遍歷跳躍表使用
struct zskiplistLevel {
struct zskiplistNode *forward; // 指向當前節點的下一個節點,最後一個節點指向NULL
unsigned long span; //forward 指向的節點與當前節點之間的元素個數。 值越大,跳過的節點個數越多。
} level[]; // 柔性數組。每個節點的數組長度不同,生成跳躍表節點時,隨機生成1~64 的值,值越大出現的概率越低
} zskiplistNode;
2.跳躍表結構 zskiplist -源碼位置 redis/src/server.h
//通過跳躍表結構我們可以在O(1)的時間複雜度快速知道跳躍表的頭節點、尾節點、高度、長度
typedef struct zskiplist {
struct zskiplistNode *header, /指向跳躍表頭節點,這個節點的level 數組元素個數是64 ,64個元素的forward 都指向NULL,ele 爲NULL ,score 爲0 ,不計入總長度
*tail; //指向跳躍表尾節點
unsigned long length; // 跳躍表的長度,是統計除頭節點以外的節點數
int level; //跳躍表的高度
} zskiplist;
3.zset 有序集合的新增 -源碼位置redis/src/t_zet.c
/* Insert a new node in the skiplist. Assumes the element does not already
* exist (up to the caller to enforce that). The skiplist takes ownership
* of the passed SDS string 'ele'. */
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
unsigned int rank[ZSKIPLIST_MAXLEVEL];
int i, level;
serverAssert(!isnan(score));
// update[] 是因爲在插入節點時,需要更新節點每層的前一個節點,需要將每層需要更新的節點記錄在此數組中
// rank[] 記錄當前層從header 節點到update[i] 節點所經歷的步長 += level[i].span
x = zsl->header;
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 &&
sdscmp(x->level[i].forward->ele,ele) < 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;
}
//插入節點,按照level 的層級循環插入
x = zslCreateNode(level,score,ele);
for (i = 0; i < level; 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;
}
/* increment span for untouched levels */
for (i = level; i < zsl->level; i++) {
update[i]->level[i].span++;
}
// 重新調整backward
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;
}
4..zset 有序集合的刪除 -源碼位置redis/src/t_zet.c
/* 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;
}
}
//調整backward
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--;
}
5.刪除跳躍表 -源碼位置redis/src/t_zet.c
/* Free a whole skiplist. */
void zslFree(zskiplist *zsl) {
zskiplistNode *node = zsl->header->level[0].forward, *next;
zfree(zsl->header);
//依次釋放節點內存
while(node) {
next = node->level[0].forward;
zslFreeNode(node);
node = next;
}
zfree(zsl);
}
參考書籍: 《Redis 5設計與源碼分析》