redis中跳錶的運用及原始碼解析(一)
瞭解過Redis的都知道,Redis有一個非常有用的資料結構:SortSet,基於它,我們可以很輕鬆的實現一個Top N的應用。那麼,這個SortSet底層到怎麼樣實現的?怎麼樣實現才能既保證有序並提供插入,查詢的最優時間複雜度呢?這裡,便就是我將要給大家介紹的跳錶
跳錶的具體定義,請參考wikipedia跳錶定義,跳錶也是連結串列的一種,只不過它在連結串列的基礎上增加了跳躍功能,正是這個跳躍的功能,使得在查詢元素時,跳錶能夠提供O(log n)的時間複雜度。(通常,像紅黑樹等這樣的資料結構查詢的時間複雜度也是O(log n),但是正確實現一顆紅黑樹是比正確實現一個跳錶是要複雜很多很多的)
跳躍
那麼,這個跳躍的功能是怎麼實現的呢?為什麼能夠提供跟查詢樹一樣的O(log n)的時間複雜度呢?下邊我將藉助Redis中的程式碼來分析這些原理。(redis的完整程式碼,參看 redis)
redis程式碼中,跳錶主要實現在 t_zset.c,跳錶節點的定義,則在redis.h中,我們首先來看跳錶節點的定義:
typedef struct zskiplistNode {
robj *obj;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned int span;
} level[];
} zskiplistNode;
暫時先不關心robj *obj和unsigned int span這兩個屬性,robj是redis內部物件的定義, span是redis內部在計算節點的排名使用的。
在定義普通連結串列節點的時候(雙向連結串列)
struct ListNode
{
void *data;
ListNode *prev;
ListNode *next;
};
prev為前向指標,next為後向指標。
那麼我們再來看redis中跳錶節點的前向節點指標的定義,
struct zskiplistNode *backward;
backward指標表示其也是一個雙向連結串列,指向某個節點前向節點,這點跟普通連結串列的prev指標的含義是一樣的。
再看後向節點指標的定義:
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned int span;
} level[];
很奇怪是不是?後向節點指標變成了一個陣列,這正是跳錶可以實現跳躍的實質,這個陣列中的forward元素不僅可以指向這個節點的直接後繼元素,也可以指向後繼元素的後繼元素,或者是後繼後繼後繼元素,依此類推。這也就是說,從當前的這個節點,可以以O(1)的時間複雜度跨過其直接後繼節點查詢其後的某個節點,這樣就實現了跳躍。(普通連結串列需要查詢某個節點的話,必須要進行遍歷操作,這對於有查詢效能要求的應用場景來說,是不能接受的!)
O(log n)
現在我們再來看看跳錶為什麼能提供O(log n)的時間複雜度。
一般,我們會在插入元素的時候,就保持跳錶的有序排列。請看下圖:
可以看到,正式這樣一種結構,我們每次在進行查詢操作的時候,都會從最高一層開始找,由於連結串列是有序的,所以期望的時間複雜度是O(log n),當然最壞情況下的時間複雜度是o(n),不過這種情況應該不太會遇到。
需要指出的是,實際的儲存結構,並不是像上圖這樣有這麼多節點,跟普通連結串列相比,跳錶額外的儲存空間只有那個前向節點陣列花費的,當然,這是一種以空間換取時間的策略。與它提供的特性相比,這點額外的空間我們是可以接受的。
redis跳錶實現解析
首先我們看下redis實現中幾個我們可以自己拿出來用的api:
/** 建立一個skiplist */
zskiplist *zslCreate(void)
/** 建立一個skiplist節點 */
zskiplistNode *zslCreateNode(int level, double score, robj *obj);
/** 釋放一個節點的記憶體 */
void zslFreeNode(zskiplistNode *node);
/** 釋放整個skiplist */
void zslFree(zskiplist *zsl);
/** 插入一個節點 */
zskiplistNode *zslInsert(zskiplist *zsl, double score, robj *obj)
/** 刪除一個節點,根據score刪除 */
int zslDelete(zskiplist *zsl, double score, robj *obj);
/** 這個函式是查詢某個節點對應的排名,其實就是在跳錶中位置 */
unsigned long zslGetRank(zskiplist *zsl, double score, robj *o);
上邊這些使我們可以從redis原始碼中摳出來自己用的程式碼(當然還需要在封裝封裝),對於跳錶來說,重點需要關注的就是插入,查詢,刪除這幾個操作。
(下篇分析redis程式碼中skiplist插入,查詢,刪除的實現)
相關文章
- redis中跳錶的運用及原始碼解析(二)Redis原始碼
- Redis原始碼解析之跳躍表(一)Redis原始碼
- Redis原始碼解析之跳躍表(三)Redis原始碼
- 深入理解跳錶及其在Redis中的應用Redis
- Redis 為什麼用跳錶而不用平衡樹?Redis
- 聊聊Mysql索引和redis跳錶MySql索引Redis
- redis原始碼解析----epoll的使用Redis原始碼
- 走近原始碼:Redis跳躍列表究竟怎麼跳原始碼Redis
- Redis系列(十二):資料結構SortedSet跳躍表中基本操作命令和原始碼解析Redis資料結構原始碼
- Android中HandlerThread的使用及原始碼解析Androidthread原始碼
- Redis中的強大的資料結構跳躍表(skiplist)的內部詳解及實際運用Redis資料結構
- 代理模式與它在原始碼中的運用模式原始碼
- Redis(一):服務啟動及基礎請求處理流程原始碼解析Redis原始碼
- Redis radix tree原始碼解析Redis原始碼
- 原始碼深度解析 Handler 機制及應用原始碼
- Spring中AOP相關的API及原始碼解析SpringAPI原始碼
- Go執行指令碼命令用例及原始碼解析Go指令碼原始碼
- “單例”模式與它在原始碼中的運用單例模式原始碼
- GYHttpMock:使用及原始碼解析HTTPMock原始碼
- TextWatcher的使用及原始碼解析原始碼
- 「進擊Redis」六、Redis List運用場景、API解析RedisAPI
- Lru-k在Rust中的實現及原始碼解析Rust原始碼
- Lfu快取在Rust中的實現及原始碼解析快取Rust原始碼
- 實現一個簡單版本的Vue及原始碼解析(一)Vue原始碼
- JDK原始碼閱讀(十二) : 基於跳錶的併發容器——ConcurrentSkipListMapJDK原始碼
- 跳脫字元的運用字元
- Golang 實現 Redis(5): 使用跳錶實現 SortedSetGolangRedis
- ReentrantLock解析及原始碼分析ReentrantLock原始碼
- FastClick 填坑及原始碼解析AST原始碼
- 帶有ttl的Lru在Rust中的實現及原始碼解析Rust原始碼
- Redis(五):hash/hset/hget 命令原始碼解析Redis原始碼
- 如何獲得微信小遊戲跳一跳原始碼遊戲原始碼
- 實現一個簡單版本的vue及原始碼解析(二)Vue原始碼
- spark的基本運算元使用和原始碼解析Spark原始碼
- Promise的理解及react中的運用PromiseReact
- LinkedList 基本示例及原始碼解析原始碼
- CORS原理及@koa/cors原始碼解析CORS原始碼
- ReentrantLock介紹及原始碼解析ReentrantLock原始碼