date: 2020-10-15 14:58:00
updated: 2020-10-19 17:58:00
Redis中的跳表
redis 數據類型 zset 實現有序集合,底層使用的數據結構是跳表。
源碼在 src/t_zset.c 文件中,相關數據結構的定義在 src/server.h 文件中。(4.0版本)
元素有序的時候,如果是數組,可以通過二分查找來提速;如果是鏈表,如何提速? => 跳表,插入/刪除/搜索 都是O(logn)
第一層索引 n/2 個節點,第二層 n/4 個節點,第三層 n/8,第K層 n/(2^k)
假設第K層有2個節點,即 n/(2^k) = 2 => k = log2(n) - 1
typedef struct zskiplist {
// 頭節點,尾節點
struct zskiplistNode *header, *tail;
// 節點數量
unsigned long length;
// 目前表內節點的最大層數
int level;
} zskiplist;
typedef struct zskiplistNode {
// member 對象
robj *obj;
// 分值
double score;
// 后退指針
struct zskiplistNode *backward;
// 層
struct zskiplistLevel {
// 前進指針
struct zskiplistNode *forward;
// 這個層跨越的節點數量
unsigned int span;
} level[];
} zskiplistNode;
1. 跳表 SkipList
在 java.util.concurrent.ConcurrentSkipListMap/ConcurrentSkipListSet 類中也有實現
查詢鏈表時會從頭到尾的遍歷鏈表,最壞的時間復雜度是O(N),這是一次比較一個值,如果跳着1個元素來進行比較(比較下標為2n+1的元素),那么就相當於一次性比較2個元素,效率就會提高 => 跳表

跳表是犧牲空間來換取時間,除了最底層是最原始的數據外,其他的每一層,其實都相當於是一個索引,最理想的是按照第一層1級跳,第二層2級跳,第三層4級跳,第四層8級跳。。。但是考慮到有插入,如果插入的時候還要保證這個遞增關系,那么就要調整當前的數據結構,時間太長,所以是否插入會有一個25%概率比較

插入有一個地方需要注意,最底層肯定是要插入數據的,然后產生一個隨機數,根據冪次定律,越大的值生成的幾率越小。
2. 如何確定層數
int zslRandomLevel(void){
int level = 1;
while((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
retrun (level < ZSKIPLST_MAXLAVEL) ? level : ZSKIPLST_MAXLAVEL;
}
// 0xFFFF 65535
// ZSKIPLIST_P 默認為 0.25
// define ZSKIPLST_MAXLAVEL 64 常量值為64
3. 為什么使用跳躍表,而不是平衡樹等用來做有序元素的查找
- 跳躍表的時間復雜度和紅黑樹是一樣的,而且實現簡單
- 在並發的情況下,紅黑樹在插入刪除的時候可能需要做rebalance的操作,這樣的操作可能會涉及到整個樹的其他部分;而鏈表的操作就會相對局部,只需要關注插入刪除的位置即可,只要多個線程操作的地方不一樣,就不會產生沖突
開發者的解釋:
There are a few reasons:
- They are not very memory intensive. It’s up to you basically. Changing parameters about the probability of a node to have a given number of levels will make then less memory intensive than btrees. => 並不是非常耗費內存。控制好ZSKIPLIST_P的值,內存消耗和平衡樹差不多
- A sorted set is often target of many ZRANGE or ZREVRANGE operations, that is, traversing the skip list as a linked list. With this operation the cache locality of skip lists is at least as good as with other kind of balanced trees. => 有序集合經常會進行 zrange 或 zrevrange 這樣的范圍查找,跳表里的雙向鏈表可以十分方便的進行這操作
- They are simpler to implement, debug, and so forth. For instance thanks to the skip list simplicity I received a patch (already in Redis master) with augmented skip lists implementing ZRANK in O(log(N)). It required little changes to the code.
About the Append Only durability & speed, I don’t think it is a good idea to optimize Redis at cost of more code and more complexity for a use case that IMHO should be rare for the Redis target (fsync() at every command). Almost no one is using this feature even with ACID SQL databases, as the performance hint is big anyway. => 實現簡單,zrank 還能達到O(logn)的時間復雜度
About threads: our experience shows that Redis is mostly I/O bound. I’m using threads to serve things from Virtual Memory. The long term solution to exploit all the cores, assuming your link is so fast that you can saturate a single core, is running multiple instances of Redis (no locks, almost fully scalable linearly with number of cores), and using the “Redis Cluster” solution that I plan to develop in the future.
在 src/t_zset.c 文件中,主要有兩個結構,zskiplist 和 zskiplistNode,前者保存跳躍表信息(如表頭節點、表尾節點、長度),而 zskiplistNode 用於保存節點
另外,跳躍表中的節點按照分值大小進行排序, 當分值相同時, 節點按照成員對象的大小進行排序。這一點也是redis針對跳表這個結構做出的優化之一。具體的優化點為:
- 允許重復分值,即多個節點允許相同的分值,但是每個節點的成員對象必須是唯一的
- 比較的時候不僅僅是分值,還有整個對象,即分值相同,按照成員對象大小排序
- 在第一層有一個back指針,適用於 ZREVRANGE 方法,允許從尾部到頭部來遍歷列表
