redis中跳錶的運用及原始碼解析(二)
上篇中(click here), 我對跳錶的一些基本的概念及其在redis原始碼中的定義形式做了一些分析,同時也對跳錶為什麼能實現“跳躍”功能做了一番介紹。在這篇中,我將重點針對redis中跳錶的實現做詳細的分析;關於跳錶相關的定義以及程式碼位置,在上篇中已有說明。
我們接著上篇末尾列出的那幾個函式定義開始,這幾個函式包含了最基本的幾種操作,建立、插入、刪除、查詢;首先,我們看redis中是如何實現跳錶的建立的:
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
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;
}
首先是跳錶結構的定義
struct zskiplistNode *header, *tail
頭尾指標,表名是雙向連結串列。
unsigned long length;
length是表示連結串列中節點的個數。
int level;
表示跳錶的層數。
然後我們在看zslCreate函式的定義,其中最關鍵的就是裡邊的那個迴圈,
ZSKIPLIST_MAXLEVEL
巨集定義,用於表示這個跳錶最多有多少層。在迴圈內,將指向後邊節點的forward指標置為NULL。 zslCreate呼叫完成後,其實就是建立了跳錶的頭結點(我們知道所有的連結串列實現中通常也是需要一個頭結點的)。
然後我們再來看如何給跳錶插入一個元素,插入元素程式碼請看t_zset.c,第107行zslInsert函式, 這裡不貼出完整的程式碼,我只取其中比較重要的部分分析。
插入一個元素到跳錶中,首先需要確定的是插入元素的位置,插入元素的時候需要保證跳錶元素的排列順序是升序排列;然後確定待插入節點的層數;最後是將節點各層指向正確的後續節點,然後插入元素。
首先請看下述程式碼段:
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
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 &&
compareStringObjects(x->level[i].forward->obj,obj) < 0))) {
rank[i] += x->level[i].span;
x = x->level[i].forward;
}
update[i] = x;
}
這段程式碼主要的作用就是查詢節點所要插入的位置。首先請看下邊這幅圖:
通常,在跳錶中查詢元素的時候,不像在連結串列中查詢元素那樣需要遍歷,而是會從頭結點(頭結點的下一節點才是元素節點)的最頂層開始,即上述程式碼的for迴圈,如果level陣列的forward指標指向的節點的score值大於要插入的score,那麼就下降一層;否則,就把x前進一個節點,指向到下一個節點,繼續比較,即上述程式碼中while迴圈所做的工作。最後,當結束for迴圈時,update陣列中儲存的節點的forward就是將要插入的節點的level陣列中的forward需要的值,並且待插入的節點一定是位於update[0]這個指標所指節點的後邊。
當新節點需要插入的位置找到後,就需要確定新節點的層數:
level = zslRandomLevel();
我們知道,跳躍列表是對有序的連結串列增加上附加的前進連結,增加是以隨機化的方式進行的,所以節點層數的確定當然也是隨機的,以上這行程式碼便是確定節點的層數。
因為在前邊查詢的時候,是從當前跳錶的最高一層開始查詢的,如果新節點的層數大於跳錶當前的層數,則需要更新跳錶的層數並擴充套件之前的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;
}
這裡需要指出的是為什麼新加的層節點直接用zsl->header賦值呢? 原因是這樣的,因為新加入的這層與zsl->header直接肯定是沒有其他節點的層的;而下邊我們在給初始化新插入節點的level陣列的時候,是把update的每一個元素當作其前一個跳躍節點的。
x = zslCreateNode(level,score,obj);
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;
}
綜合這幾段程式碼,我們便可以知道update這個陣列的作用了;然後便是給backward賦值,以及修改zsl的tail指標:
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++;
到這裡,基本跳錶的建立以及插入元素都分析完畢了,下邊,我們看看如何刪除一個元素。刪除元素的程式碼位於t_zset.c 第185行zslDelete定義
同插入節點類似,刪除節點的時候,同樣也是需要先找到需要刪除節點的位置,程式碼如下:
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;
}
這裡,需要特別注意的是,到for迴圈結束時,update[0]處的指標所指向的是要刪除的節點的前一個節點,到這裡為止,要刪除的節點是否存在並不知道。
x = x->level[0].forward;
這裡便是將節點往後前進一個位置,便到了我們要尋找的節點的位置,到這裡為止,要刪除的節點是否存在仍然不知道,下邊便是做節點的比較:
if (x && score == x->score && equalStringObjects(x->obj,obj)) {
zslDeleteNode(zsl, x, update);
zslFreeNode(x);
return 1;
} else {
return 0; /* not found */
}
只有當score值與節點元素的值全部相等時,才說明要刪除的節點是存在的,否則就是不存在的。
最後,節點元素的查詢跟之前插入跟刪除節點的查詢過程是一樣的,具體請看redis的實現 t_zset.c unsigned long zslGetRank(zskiplist *zsl, double score, robj *o) 這個函式。
相關文章
- 深入理解跳錶及其在Redis中的應用Redis
- Redis原始碼解析之跳躍表(一)Redis原始碼
- Redis原始碼解析之跳躍表(三)Redis原始碼
- 聊聊Mysql索引和redis跳錶MySql索引Redis
- redis原始碼解析----epoll的使用Redis原始碼
- 走近原始碼:Redis跳躍列表究竟怎麼跳原始碼Redis
- Redis系列(十二):資料結構SortedSet跳躍表中基本操作命令和原始碼解析Redis資料結構原始碼
- Redis radix tree原始碼解析Redis原始碼
- Spring中AOP相關的API及原始碼解析SpringAPI原始碼
- ThreadPoolExecutor原始碼解析(二)thread原始碼
- TextWatcher的使用及原始碼解析原始碼
- JDK原始碼閱讀(十二) : 基於跳錶的併發容器——ConcurrentSkipListMapJDK原始碼
- Spring原始碼解析之基礎應用(二)Spring原始碼
- Redis中的強大的資料結構跳躍表(skiplist)的內部詳解及實際運用Redis資料結構
- 原始碼深度解析 Handler 機制及應用原始碼
- 跳錶
- 代理模式與它在原始碼中的運用模式原始碼
- Go執行指令碼命令用例及原始碼解析Go指令碼原始碼
- 實現一個簡單版本的vue及原始碼解析(二)Vue原始碼
- 連結串列中的跳錶小結
- Mobx 原始碼解析 二(autorun)原始碼
- Picasso-原始碼解析(二)原始碼
- FaceBook POP原始碼解析二原始碼
- Glide原始碼解析二---into方法IDE原始碼
- React Hook原始碼解析(二)ReactHook原始碼
- Flutter aspectd (二)原始碼解析Flutter原始碼
- GYHttpMock:使用及原始碼解析HTTPMock原始碼
- ReentrantLock解析及原始碼分析ReentrantLock原始碼
- “單例”模式與它在原始碼中的運用單例模式原始碼
- Golang 實現 Redis(5): 使用跳錶實現 SortedSetGolangRedis
- Lru-k在Rust中的實現及原始碼解析Rust原始碼
- Lfu快取在Rust中的實現及原始碼解析快取Rust原始碼
- 「進擊Redis」六、Redis List運用場景、API解析RedisAPI
- 認真的 Netty 原始碼解析(二)Netty原始碼
- mybatis通用mapper原始碼解析(二)MyBatisAPP原始碼
- RxDownload2 原始碼解析(二)原始碼
- Spring原始碼解析之ConfigurationClassPostProcessor(二)Spring原始碼
- Spring原始碼解析之BeanFactoryPostProcessor(二)Spring原始碼Bean
- LinkedList 基本示例及原始碼解析原始碼