zset
是Redis
提供的一個非常特別的數據結構,常用作排行榜等功能,以用戶id
為value
,關注時間或者分數作為score
進行排序。與其他數據結構相似,zset
也有兩種不同的實現,分別是zipList
和skipList
。zipList
前面我們已經介紹過了,這里就不再介紹了。具體使用哪種結構進行存儲,規則如下:
zipList
:滿足以下兩個條件[score,value]
鍵值對數量少於128個;- 每個元素的長度小於64字節;
skipList
:不滿足以上兩個條件時使用跳表、組合了hash
和skipList
hash
用來存儲value
到score
的映射,這樣就可以在O(1)
時間內找到value
對應的分數;skipList
按照從小到大的順序存儲分數skipList
每個元素的值都是[socre,value]
對
使用zipList
的示意圖如下所示:
使用跳表時的示意圖:
跳表 skipList
跳表skipList
在Redis
中的運用場景只有一個,那就是作為有序列表zset
的底層實現。跳表可以保證增、刪、查等操作時的時間復雜度為O(logN)
,這個性能可以與平衡樹相媲美,但實現方式上卻更加簡單,唯一美中不足的就是跳表占用的空間比較大,其實就是一種空間換時間的思想。跳表的結構如下所示:
Redis
中跳表一個節點最高可以達到64層,一個跳表中最多可以存儲2^64個元素。跳表中,每個節點都是一個skiplistNode
,
每個跳表的節點也都會維護着一個score
值,這個值在跳表中是按照從小到大的順序排列好的。
跳表的結構定義如下所示:
typedf struct zskiplist{
//頭節點
struct zskiplistNode *header;
//尾節點
struct zskiplistNode *tail;
// 跳表中元素個數
unsigned long length;
//目前表內節點的最大層數
int level;
}zskiplist;
header:指向跳表的頭節點,通過這個指針可以直接找到表頭,時間復雜度為O(1)
;
tail:指向跳表的尾節點,通過這個指針可以直接找到表尾,時間復雜度為o(1)
;
length:記錄跳表的長度,即不包括頭節點,整個跳表中有多少個元素;
level:記錄當前跳表內,所有節點中層數最大的level
;
zskiplist
的示意圖如下所示:
zskiplistNode
的結構定義如下:
typedf struct zskiplistNode{
sds ele;// 具體的數據
double score;// 分數
struct zskiplistNode *backward;//后退指針
struct zskiplistLevel{
struct zskiplistNode *forward;//前進指針forward
unsigned int span;//跨度span
}level[];//層級數組 最大32
}zskiplistNode;
ele:真正的數據,每個節點的數據都是唯一的,但節點的分數score
可以是一樣的。兩個相同分數score
的節點是按照元素的字典序進行排列的;
score:各個節點中的數字是節點所保存的分數score
,在跳表中,節點按照各自所保存的分數從小到大排列;
backward:用於從表尾向表頭遍歷,每個節點只有一個后退指針,即每次只能后退一步;
層級數組:這個數組中的每個節點都有兩個屬性,forward
指向下一個節點,span
跨度用來計算當前節點在跳表中的一個排名,這就為zset
提供了一個查看排名的方法。數組中的每個節點中用1、2、3等字樣標記節點的各個層,L1
代表第一層,L2
代表第二層,L3
代表第三層;,以此類推;
skiplistNode
的示意圖如下所示:
增刪改查
以下圖為例,講解一下skiplist
的增刪改查過程。
查
假設現在要查找7這個節點,步驟如下:
- 從
head
開始遍歷,指針指向4這個節點,由於4<7,且同層的下一個指針指向NULL
,所以下級一層; - 跳到6節點所在的層,同理,6<7,且同層的下一個指針指向
NULL
,再下降一層; - 此時到了第一層,第一層是一個雙向鏈表,由於6<7,所以開始向后遍歷,查找到7就返回,不然就返回
NULL
;
刪
刪除的過程前期與查找相似,先定位到元素所在的位置,再進行刪除,最后更新一下指針、更新一下最高的層數。
改
先是判斷這個 value 是否存在,如果存在就是更新的過程,如果不存在就是插入過程。在更新的過程是,如果找到了Value,先刪除掉,再新增,這樣的弊端是會做兩次的搜索,在性能上來講就比較慢了,在 Redis 5.0 版本中,Redis 的作者 Antirez 優化了這個更新的過程,目前的更新過程是如果判斷這個 value是否存在,如果存在的話就直接更新,然后再調整整個跳躍表的 score 排序,這樣就不需要兩次的搜索過程。
增
比如要插入的值為 6
- 從 head 節點開始,先是在 head 開始降層來查找到最后一個比 6 小的節點;
- 等到查到最后一個比 6 小的節點的時候(假設為 5 );
- 然后需要引入一個隨機層數算法來為這個節點隨機地建立層數;
- 把這個節點插入進去以后,同時更新一遍最高的層數即可;
隨機層數算法
int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level<ZSKIPLIST_MAXLEVEL)
? level : ZSKIPLIST_MAXLEVEL;
}
#define ZSKIPLIST_MAXLEVEL 32
#define ZSKIPLIST_P 0.25
總結
參考
https://juejin.im/post/5ec4f57af265da76c1132837#comment
https://juejin.im/post/5e1ac4d95188254c457786ee
https://juejin.im/post/5e1ae93d6fb9a030073b43bc