redis源碼分析—基本數據結構

redis中使用的數據結構有:

  1. dict 字典,就是個哈希表,實現和HashMap類似,不做闡述;不同的是在哈希表resize()的時候是分步執行的,後續篇幅再說明。
  2. sds 很多項目都對自己的字符串進行了封裝,作用類似於leveldb的slice。
  3. linkedlist 雙端鏈表,迭代器的實現是通過鏈表的pre和next實現的,是個BidirctionalIterator。代碼中只實現了ForwardIterator的功能。
  4. zipmap 已不再使用了
  5. inset 一個緊緻的有序整型數組。
  6. ziplist 也是一個鏈表,但是它的內存是連續的,在數據量小的時候使用,增長到一定的大小會轉化成list或者skiplist
  7. skiplist 跳躍表,實現在t_zset.c中

本文只闡述inset,ziplist,skiplist三種數據結構。
1.ziplist
直接借用代碼註釋上ziplist的存儲結構:
【加羣】:857565362
1) zlbytes 記錄了整個ziplist佔用的內存大小;
2) zltail 記錄尾節點的相對於ziplist實例指針的偏移量;
3) zllen 是整個list的節點數量的無符號數,佔2個字節,可以最大表示2**16-1,實際使用保留了0xffff ffff ,即大於等於這個數的時候,已經不能通過此值來取得節點的數量了;
4) zlend 固定爲0xff。
5) entry 的結構並不是代碼中給的結構體zlentry。而是如下結構:
【加羣】:857565362
這是個變長的結構,pre節點小於254則pre_entry_len佔用1字節,否則5字節,爲什麼不是255字節呢?255會和zlend產生衝突;cur_entry_len中包含了編碼信息(以字符串編碼還是數字,數字編碼能減少內存長度)和本節點的長度信息,數字格式編碼固定佔用1字節;字符串格式編碼數據小於127字節佔1字節,小於0x3fff 佔用2字節,否則5個字節。
因此ziplist有如下特點:
1)內存連續,數據緊湊。
2)查找效率o(n)
3)雙端查詢,可前後遍歷,類似於nodelist。
瞭解了數據結構,思考下如何對其增加一個節點p,p的大小爲sp。
1.ziplist長度發生變化,zlbytes要增加entry的內存大小;
2.zltail也要做相應改變,如果插入的是尾節點,則zltail等於ziplist到新節點p的距離,否則是原值加上新節點p的長度。
3.內存大小發生改變,需要realloc個更大的內存。
4.對於在新節點以後的節點,後移sp個單位。接下來要更新p以後的節點內容:
4.1如果p的前一個節點的長度和p節點的長度一樣長,那麼皆大歡喜。只需要更新p節點後一個節點的pre_entry_len值;
4.2如果比p節點的長度大(5字節對1字節),也ok,存下來,浪費4個字節內存。
4.3如果比p節點的長度小(1字節對5字節),意味着p以後的節點長度會發生變化,增加4個字節來存儲p的長度,zltail和ziplist也要同時加4;同時由於p->next節點長度發生變化,p->next->next也要去做一次判斷,需要重複步驟4,直到不滿足4.3的情況爲止。經過以上步驟,就可以大概還原redis中的代碼實現了,也可以看出來ziplist的增刪代價是很大的,改變一個節點的複雜度最差是(n^2);一個節點改動,可能牽一髮而動全身,所以ziplist是不適合大數據量的存儲的。
【加羣】:857565362
2.inset
inset 實際是一個整數數組,結構如下:

typedef struct intset {
        uint32_t encoding;
     uint32_t length;
     int8_t contents[];
} intset;

其中encoding是編碼方式,有16位,32位,64位三種取值;length是inset集合中保存的元素個數,contents爲保存的數據。下面是inset的特點和實現方式.
1.初始化的inset是16位編碼的,如果inset裏面只保存了16位的數字,那麼這個編碼方式將保持不變。
2.inset是有序不重複的,從小到大排列,每次插入/刪除會觸發inset的大小調整,length+1。
3.如果插入了比當前編碼範圍更大的數字,會觸發intsetUpgradeAndAdd(),下面以16位-> 32位擴展爲例。
3.1 重新調整contents大小,大小爲原(size+1)*2,更新encoding的值爲INTSET_ENC_INT32;
3.2 新數字肯定是位於數組開頭或者結尾,如果是在結尾,將原數組的每個數從原位置pos移到pos2的位置,否則是(pos+1)2;
3.3 插入新的數據,intsetUpgradeAndAdd()過程結束
4.刪除不改變inset的編碼方式,即如果原inset爲32位,刪光了inset裏的16位不能表示的數字以後,inset也不會恢復到16位,代價太大,即intsetUpgradeAndAdd()的過程是不可逆的。
5.在inset中查找一個數採用二分查找。
根據這些特點,寫出inset的實現,想必不難,inset的特點是在存儲的數據爲小值的時候,佔用的內存較小,但是隻要插入了一個64位的數據,那麼inset就沒有優勢了,插入和刪除的時間複雜度都是o(n),因此不適合做大數量的存儲。
3.skiplist
跳錶是一個特殊的有序鏈表。
跳錶結構 【加羣】:857565362
結構如下

typedef struct zskiplist {

    // 表頭節點和表尾節點
    struct zskiplistNode *header, *tail;

    // 表中節點的數量
    unsigned long length;

    // 表中層數最大的節點的層數
    int level;

} zskiplist;

節點的結構

typedef struct zskiplistNode {

    // 成員對象
    robj *obj;

    // 分值
    double score;

    // 後退指針
    struct zskiplistNode *backward;

    // 層
    struct zskiplistLevel {

        // 前進指針
        struct zskiplistNode *forward;

        // 跨度
        unsigned int span;

    } level[];

} zskiplistNode;

簡述下跳錶的特點

  • 跳錶是一個排序鏈表,redis中以score的大小做排序,增刪改查的效率均是o(logn),由於其實現更易於理解和實現而被廣泛使用。
  • 跳錶是不穩定的,即同樣是創建一個跳錶,插入若干相同的值,內部的數據結構可能是不一致的。
  • 每個節點的大小不固定,不固定的原因是節點的擁有的forward指針個數不定,這個數值是在節點創建的時候隨機選取的一個正整數,redis實現默認最大層數是32。
  • redis中的跳錶增加了一個指向前節點的backward來往前遍歷;一個span,用於計算forward節點和本節點中間的跨度,以方便zcount等命令來計算範圍內score的數量。
    如何插入一個數據?
    redis排序順序是從頭到尾,按照score從小到大。如果要增加一個值,首先需要確定插入後的順序,假設跳錶爲skl ,新節點爲n。
  • 找到每一層比新節點score小,而且距離最近的節點,保存爲update[]。
  • 隨機產生新節點的層高,對每一層的forward節點複製,令n->level[i]->forward = update[i]->level[i]->forward,給新節點的所有forward指針賦值。令update[i]->forward = n使新節點和其所有前置節點關聯;實際上和雙向鏈表操作類似。
    如何查找一個節點p?
    場景1.根據score範圍查找內容,對應zrange命令。查找左界和右界,中間的範圍及所求。左界和右界算法類似,從head的頂層forward節點開始,如果比p的score小,那麼讓head = forward 繼續此步驟.如果等於score那麼返回。如果小於那麼代表需要找的左界在head和forward之間,跳到下一層重複此步驟。
    場景2.根據內容查找score,對應zscore命令。redis不查找skiplist,而是額外使用了個hash表來達到o(1)的效率。
    迭代器如何設計?
    可知n->level[0]->forward的步長是1,循環forward可以遍歷整個跳錶。
    綜上,redis的數據結構分析告一段落。
    我這兒整理了比較全面的JAVA相關的面試資料,
    需要領取面試資料的同學,請加羣:473984645

    【加羣】:857565362
    獲取更多學習資料,可以加羣:473984645或掃描下方二維碼
    在這裏插入圖片描述
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章