Redis底層數據結構之 zset


zsetRedis提供的一個非常特別的數據結構,常用作排行榜等功能,以用戶idvalue,關注時間或者分數作為score進行排序。與其他數據結構相似,zset也有兩種不同的實現,分別是zipListskipListzipList前面我們已經介紹過了,這里就不再介紹了。具體使用哪種結構進行存儲,規則如下:

  • zipList:滿足以下兩個條件
    • [score,value]鍵值對數量少於128個;
    • 每個元素的長度小於64字節;
  • skipList:不滿足以上兩個條件時使用跳表、組合了hashskipList
    • hash用來存儲valuescore的映射,這樣就可以在O(1)時間內找到value對應的分數;
    • skipList按照從小到大的順序存儲分數
    • skipList每個元素的值都是[socre,value]

使用zipList的示意圖如下所示:

使用跳表時的示意圖:

跳表 skipList

跳表skipListRedis中的運用場景只有一個,那就是作為有序列表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

https://juejin.im/post/5bd7cbce51882576c65a4a1b#heading-4

https://juejin.im/post/5ed5eec16fb9a047995842f2#heading-17


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM