1. 程式人生 > >Redis底層資料結構

Redis底層資料結構

字串(簡單動態字串 simple dynamic string)

SDS 資料結構
每個 sds.h/sdshdr 結構表示一個 SDS 值:

struct sdshdr {
    // 記錄 buf 陣列中已使用位元組的數量
    // 等於 SDS 所儲存字串的長度
    int len;
    // 記錄 buf 陣列中未使用位元組的數量
    int free;
    // 位元組陣列,用於儲存字串
    char buf[];
};

SDS與C字串的區別

  1. 常數複雜度獲取字串長度

    因為 C 字串並不記錄自身的長度資訊, 所以為了獲取一個 C 字串的長度, 程式必須遍歷整個字串, 對遇到的每個字元進行計數, 直到遇到代表字串結尾的空字元為止, 這個操作的複雜度為 O(N)
    和 C 字串不同, 因為 SDS 在 len 屬性中記錄了 SDS 本身的長度, 所以獲取一個 SDS 長度的複雜度僅為O(1)

  2. 杜絕緩衝區溢位

    因為C字串不記錄本身長度,所以進行字串拼接的時候若沒有分配足夠的記憶體空間,新的字串會溢位到其他記憶體空間,導致其他內容被修改;而當SDS API對SDS進行修改時,API會先檢查SDS的空間是否滿足修改所需的要求,如果不滿足,API會自動將SDS的空間擴充套件至執行修改所需大小,然後再執行修改操作。

  3. 二進位制安全

    C字串除了字串末尾有空格外,字串中不能出現空字串,否者會被程式任務是字串結尾 – 這些限制了C字串只能儲存文字資料,不能儲存圖片,視訊,音訊,壓縮檔案這樣的二進位制資料;所有SDS API 都是以處理二進位制的方式來處理SDS存放字buff陣列中的資料,不會做限制和過濾

· SDS 空間預分配

優化SDS字串增長的操作,避免頻繁的記憶體分配。程式不僅會分配修改必要的空間外,還會為SDS分配額外的未使用的空間。
分配公式:小於1M 程式會分配和len屬性同樣大小的未使用空間,這是len屬性的值和free相相同;大於1M 程式會分配1M的未使用空間。

· 惰性空間釋放
優化SDS字串縮短的操作。當SDS API 需要縮短SDS儲存的字串時,程式不會立即使用記憶體分配來回收縮短後多出來的位元組,而是使用free屬性將這些位元組的數量記錄起來,並等待將來使用。(與此同時,SDS也提供了API來真正釋放SDS裡面未使用的空間,不會造成記憶體浪費)

連結串列

  • 連結串列被廣泛用於實現 Redis 的各種功能, 比如列表鍵, 釋出與訂閱, 慢查詢, 監視器, 等等。
  • 每個連結串列節點由一個 listNode 結構來表示, 每個節點都有一個指向前置節點和後置節點的指標, 所以 Redis
    的連結串列實現是雙端連結串列。
  • 每個連結串列使用一個 list 結構來表示, 這個結構帶有表頭節點指標、表尾節點指標、以及連結串列長度等資訊。
  • 因為連結串列表頭節點的前置節點和表尾節點的後置節點都指向 NULL , 所以 Redis 的連結串列實現是無環連結串列。
  • 通過為連結串列設定不同的型別特定函式, Redis 的連結串列可以用於儲存各種不同型別的值。

字典

字典是雜湊鍵的底層實現之一,當一個雜湊鍵包含的鍵值對比較多的時候,又或者鍵值對中的元素都是比較長的字串時 ,redis就會使用字典作為雜湊鍵的底層實現。

雜湊

使用MurmurHash2演算法
rehash
rehashid:它記錄了rehash當前的進度,如果目前沒有進行rehash,值為-1。
什麼時候進行rehash?(負載因子 = 雜湊表已儲存節點數量 ÷ 雜湊表大小) (load_factor = ht[0].used / ht[0].size)

  • 伺服器目前沒有執行BGSAVE(background save)或BGREWRITEAOF命令時,並且雜湊表的負載因子大於等於1;

  • 伺服器目前正在執行BGSAVE或BGREWRITEAOF命令時,並且負載因子大於等於5;

當redis進行BGSAVE或BGREWRITEAOF命令的過程中,redis需要建立當前伺服器程序的子程序,大多數作業系統都是採用寫時複製(copy-on-write)技術來優化子程序的使用效率,所以在子程序(BGSAVE或BGREWRITEAOF)存在期間,伺服器會提高執行擴充套件操作所需的負載因子,避免在子程序(BGSAVE或BGREWRITEAOF)存在的期間進行雜湊表的擴充套件操作,這可以避免不必要的記憶體寫入操作,最大程度的節約記憶體。

如何rehash?

  1. 為字典的ht[1]雜湊表分配空間,這個表的空間大小取決於執行的操作(擴充套件/收縮),以及ht[0]包含的鍵值對數量(ht[0].uesd)

    擴充套件操作,ht[1]的大小為:第一個大於等於ht[0].uesd * 2的 2的n次方;
    收縮操作,ht[1]的大小為:第一個大於等於ht[0].uesd 的2的n次方;

  2. 將ht[0]上所有的鍵值對rehash到ht[1]上。這裡的rehash指的是重新計算鍵的雜湊值和索引值,然後將鍵值對放置在ht[1]指定的位置上。
  3. 當ht[0]上所有的鍵值對都遷移到ht[1]之後(ht[0]變為空表),釋放ht[0],將ht[1]設定為ht[0],並在ht[1]上新建一個空白雜湊表,為下次rehash做準備。
    漸進式rehash
    rehash動作並不是一次集中完成的,而是分多次、漸進式完成的。rehash期間字典的urd操作會在ht[0] ht[1]兩個表上同時進行(新增操作會在ht[1])。

跳躍表(skiplist)

有序的資料結構,通過在每個節點維持多個指向其他節點的指標,達到快速查詢的目的。
查詢的時間複雜度O(logN) 最壞O(N)。
大部分情況下,跳躍表的效率可以和平衡樹相媲美,而且實現比平衡樹簡單。
redis使用跳躍表最為有序集合的底層實現之一:
● 如果一個有序集合包含的元素比較多
● 有序集合中元素的成員是比較長的字串時。
·redis 的跳躍表由zskiplist和zskipnode兩個結構實現,zskiplist儲存跳躍表資訊(比如:表頭節點,表尾節點,長度);
·每個跳躍表的層高都是1到32之間的;
·再同一個跳躍表中,多個節點可以包含相同的分值,但每個幾點的物件必須唯一;
·跳躍表的節點按分值大小進行排序,當分值相同時,節點按照成員物件大小進行排序;

整數集合

整數集合(intset)是集合鍵的底層實現之一:當一個集合只包含整數元素,並且這個集合的元素數量不多的時候。
升級
每當我們將一個新的新元素新增到整數集合裡面時,並且新元素的型別比現有整數集合所有元素的型別要長的時候
升級需要三步:
1. 根據新元素的型別,擴充套件整數集合底層陣列的空間大小,並且為新元素分配空間;
2. 將底層陣列的所有元素都轉換成與新元素相同的型別,將轉換後的元素放置到正確的位置上,而且放置的過程中,需要繼續維持底層陣列的有序性質不變;
3. 將新元素新增到底層數組裡面。
升級的好處:

  1. 提高靈活性

    因為c語言是靜態型別,為了避免型別錯誤,我們通常不會將不同的型別值放在同一個結構中。但是因為整數集合可以自動升級底層陣列來適應新元素,不用擔心型別錯誤

  2. 節約記憶體

當然,如果讓一個數組同時儲存int16、int32、int64,簡單的做法就是直接使用int64最為底層陣列的實現。

參考文獻:《redis設計與實現》