Redis的數據結構介紹

咱們都知道Redis是用C語言編寫的內存數據庫。可是因爲C幾乎沒有提供任何數據結構的封裝,因此Redis爲了實現更快,更安全的操做,本身在內部封裝了一系列的數據結構。 其中包括了簡單動態字符串、鏈表、字典、跳躍表、整數集合、壓縮列表,下面來一一介紹(畫的圖有點醜。。)。redis

簡單動態字符串(SDS)

SDS定義

在redis中,只有字符串字面量纔會用C字符串來表示(好比打印日誌),其它都使用SDS來表示(好比鍵值對的鍵都是用SDS表示的字符串)。算法

SDS的結構:

struct sdshdr {
  // 記錄buf數組已使用的字節數,也就是SDS字符串的長度
  int len;
  // 記錄buf數組中未使用字節的數量
  int free;
  // 字節數組,用於保存字符串
  char buf[];
}
複製代碼

SDS爲了能夠重用C字符串函數庫裏的函數,因此遵循了用空字符結尾,但這個空字符不計入len屬性中。數據庫

SDS的特色

  1. 常數複雜度獲取字符串長度。C字符串若是要獲取字符串長度,必須從頭至尾遍歷整個字符串,因此致使複雜度爲O(N)。可是SDS自己在屬性中記錄了長度,因此獲取SDS長度的複雜度爲O(1)。
  2. 杜絕緩衝區溢出。C字符串若是在拼接字符串操做時,已分配的內存空間不足以放下拼接後的字符串,那麼將會形成緩衝區溢出。可是SDS會根據所需空間和自身空間來動態擴展空間大小。
  3. 經過未使用空間減小了內存重分配次數。C字符串在每次拼接或截斷操做時,都要從新分配內存空間以防止緩衝區溢出或內存泄漏。而SDS經過未使用空間實現了空間預分配和惰性空間釋放兩種優化策略來減小內存重分配次數。
    1. 空間預分配:若是SDS修改以後,長度將小於1MB,那麼將會分配和SDS長度一樣大小的未使用空間。若是長度將大於1MB,那麼將直接分配1MB的未使用空間。
    2. 惰性空間釋放:若是SDS的長度縮短時,多餘的空間並不會被當即釋放,而是用未使用空間將他們留在SDS中,未之後可能的增長預留空間。固然,SDS也能夠經過手動調用API來釋放未使用空間,以避免形成內存泄漏。
  4. 二進制安全 。因爲C字符串會將遇到的第一個空字符判斷爲字符串結尾,因此致使C字符串只能保存文本,而不能保存像圖片、視頻等二進制數據,因此C字符串被稱爲字符數組。而SDS會以處理二進制的方式來處理SDS存放再buf數組裏的數據,SDS不是以空字符判斷結尾的,而是經過len屬性的值來判斷字符串是否結束。因此SDS的API是二進制安全的,能夠存放各類數據,因此SDS被稱爲字節數組。
  5. 兼容部分C字符串的函數。由於SDS與C字符串同樣遵循以空字符結尾,因此可讓那些保存文本數據的SDS重用一部分C字符串函數庫的函數。

鏈表

當一個列表鍵包含了數量比較多的元素,又或者列表中包含的元素都是比較長的字符串時,Redis就會使用鏈表做爲列表鍵的底層實現。同時,在發佈與訂閱、慢查詢、監視器等功能也用到了鏈表。數組

鏈表和鏈表節點的實現

鏈表結構

typedef struct list {
  // 表頭節點
  listNode *head;
  // 表尾節點
  listNode *tail;
  // 鏈表所包含的節點數量
  unsigned long len;
  // 節點值複製函數
  void *(*dup)(void *ptr);
  // 節點值釋放函數
  void (*free)(void *ptr);
  // 節點值對比函數
  void (*match)(void *ptr, void *key);
} list;
複製代碼

鏈表結構爲鏈表提供了表頭指針head、表尾指針tail,以及鏈表長度計數器len。而dup、free和match則是用於實現多臺所需的類型特定函數,從而實現能夠保存各類不一樣類型的值。安全

鏈表節點結構

typedef struct listNode {
  // 前置節點
  struct listNode *prev;
  // 後置節點
  struct listNode *next;
  // 節點的值
  void *value;
}listNode;
複製代碼

多個listNode能夠經過prev和next指針組成雙端鏈表。可是無環,由於表頭節點的prev指針和表尾節點的next指針都指向NULL,因此對鏈表的訪問以NULL爲終點。 服務器

在這裏插入圖片描述

字典

Redis的數據庫就是使用字典做爲底層來實現的。能夠把數據庫中全部的對象都看做是鍵值對,而這個鍵值對就是保存在表明數據庫的字典裏的。另外,哈希鍵的底層也是經過字典實現的。數據結構

字典的實現

字典結構

typedef struct dict {
  // 類型特定函數(我以爲這個應該是至關於Java中的泛型)
  dictType *type;
  // 私有數據
  void *privdata;
  // 哈希表數組,字典存儲使用ht[0],ht[1]在rehash遷移字典數據時使用
  dictht ht[2];
  // rehash索引,當rehash不在進行時,值爲-1
  int trehashidx;
} dict;
複製代碼

type屬性和privdata屬性是針對不一樣類型的鍵值對,爲建立多態字典而設置的。函數

哈希表結構

typedef struct dictht {
  // 哈希表節點數組
  dictEntry **table;
  // 哈希表大小
  unsigned long size;
  // 哈希表大小掩碼,用於計算索引值,老是等於size - 1
  unsigned long sizemask;
  // 該哈希表已有節點的數量
  unsigned long used;
} dictht;
複製代碼

sizemask屬性和哈希值一塊兒決定一個鍵應該被放到table數組的哪一個索引上面。性能

哈希表節點結構

typedef struct dictEntry {
  // 鍵
  void *key;
  // 值,用union結構存儲數據,用於壓縮空間
  union {
    void *val;
    uint64_t u64;
    int64_t s64;
  } v;
  // 指向下個哈希表節點,造成鏈表(拉鍊法解決哈希衝突)
  struct dictEntry *next;
} dictEntry;
複製代碼

在這裏插入圖片描述

哈希算法

當要將一個新的鍵值對添加到字典裏面時,程序須要先根據鍵值對的鍵計算出哈希值,再根據哈希表的sizemask和哈希值計算出索引值,而後再根據索引值,將包含新鍵值對的哈希表節點放到哈希表數組的指定索引上面。 Redis使用MurmurHash2算法來計算鍵的哈希值,這種算法的優勢在於,即便輸入的鍵是有規律的,算法仍能給出一個很好的隨機分佈性,而且算法的計算速度也很是快。優化

rehash

擴展和收縮哈希表的工做能夠經過執行rehash(從新散列)操做來完成,Redis對字典的哈希表執行rehash的步驟以下:

  1. 爲字典的ht[1]哈希表分配空間,這個哈希表的空間大小取決於要執行的操做,以及ht[0]當前包含的鍵值對數量(也就是ht[0].used屬性的值)
    1. 若是要執行的是擴展操做,那麼ht[1] 的大小爲第一個大於等於ht[0].used乘以2的2的n次方冪。好比ht[0].used的值爲4,那麼4乘以2等於8又等於2的三次方冪,因此ht[1]的大小將被分配爲8.
    2. 若是要執行的是收縮操做,那麼ht[1] 的大小爲第一個大於等於ht[0].used的2的n次方冪。好比ht[0].used的值爲4,那麼4等於2的2次方冪,因此ht[1]的大小將被分配爲2.
  2. 將保存在ht[0]中的全部鍵值對rehash到ht[1]上面,rehash指的是從新計算鍵的哈希值和索引值,而後將鍵值對按照索引值放到ht[1]對應的位置上。
  3. 當ht[0]中的全部鍵值對都遷移到了ht[1]以後,ht[0]的空間將會被釋放,而後將ht[1]設置爲ht[0],並再建立一個ht[1]空表,爲下一次rehash作準備。

漸進式rehash

爲了不rehash對服務器性能形成影響,服務器並非一次性將ht[0]裏面的全部鍵值對所有rehash到ht[1],而是分屢次、漸進式的將ht[0]裏面的鍵值對慢慢的rehash到ht[1]。這裏就用到了rehashidx屬性,當程序處理rehash期間時,rehashidx值被設置爲0,當rehash操做完成時,又將它設置爲-1.
漸進式rehash的好處在於它採起分而治之的方式,將rehash鍵值對所需的計算工做均攤到對字典的每一個增刪改查操做上,從而避免了集中式rehash帶來的龐大計算量。
另外,在rehash期間,字典的刪除、查找、更新操做會在兩個哈希表上進行,若是在ht[0]沒有找到的話,就回去ht[1]找。而添加操做則所有在ht[1]進行,即全部新添加的鍵值對都會存到ht[1]裏面。

跳躍表

跳躍表是一種有序數據結構,它經過在每一個節點中維持多個指向其它節點的指針,從而達到快速訪問節點的目的。
跳躍表支持平均O(logN),最壞O(N)複雜度的節點查找,還能夠經過順序行操做來批量處理節點。在Redis中用跳躍表來做爲有序集合的底層實現之一。

跳躍表的實現

跳躍表結構(zskiplist)

typedef struct zskiplist {
  // 表頭節點和表尾節點
  struct zskiplistNode *header, *tail;
  // 表中節點的數量
  unsigned long length;
  // 表中層數最大的節點的層數
  int level;
} zskiplist;
複製代碼

level屬性用於在O(1)複雜度內獲取跳躍表中層數最高的那個節點的層數,注意,表頭節點的層高並不能算在裏面。

跳躍表節點

typedef struct zskiplistNode {
  // 後退指針
  struct zskiplistNode *backward;
  // 分值
  double score;
  // 成員對象
  robj *obj;
  // 層
  struct zskiplistLevel {
    // 前進指針
    struct zskiplistNode * forward;
    // 跨度
    unsigned int span;
  } level[];
} zskiplistNode;
複製代碼

\[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-kdIgrhl3-1573733502227)(https://i.loli.net/2019/11/14/cfYdF2s7x8jLBIC.png)\]

  1. 層:每次建立一個新跳躍表節點的時候,程序都根據冪次定律(越大的數出現的機率越小)隨機生成一個介於1和32之間的值做爲level數組的大小,這個大小就是層的高度。
  2. 前進指針:每一個層都有一個指向表尾方向的前進指針,用於從表頭向表尾方向訪問節點,當程序遍歷跳躍表的時候,就是根據每一個層的前進指針來移動的。
  3. 跨度:層的跨度用於記錄兩個節點之間的距離,這個是用於來計算節點的排位的。在查找某個節點的過程當中,將沿途訪過的全部層的跨度累加起來,獲得的就是當前節點在跳躍表中的排位。
  4. 後退指針:節點的後退指針用於從表尾向表頭方向訪問節點,但後退指針每次只能後退至前一個節點,而不能跳躍多個節點。
  5. 分值和成員:節點的分值是一個double類型的浮點數,也就是表明着節點的排位。跳躍表中的全部節點都按分值從小到大排序。節點的成員對象是一個指針,它指向一個字符串對象,而字符串對象則保存着一個SDS值。在一個跳躍表中,各個節點的成員對象必須是惟一的,可是分值能夠相同。分值相同的節點按照成員對象在字典序中的大小來排序,成員對象較小的節點會排在前面(靠近表頭的方向)。

整數集合

整數集合是集合鍵的底層實現之一,當一個集合只包含整數值元素,且元素數量很少時,將會使用整數集合做爲集合的底層實現。

整數集合的實現

typedef struct intset {
  // 編碼方式
  uint32_t encoding;
  // 集合包含的元素數量
  uint32_t length;
  // 保存元素的數組
  int8_t contents[];
} intset;
複製代碼

encoding的類型能夠是int16_t,int32_t或者int64_t。其中雖然contents被聲明爲int8_t,但實際上contents數組中不會保存int8_t類型的值,真正的類型仍是取決於encoding屬性的值。注意,若是contents數組中包含了不一樣整數類型的值,那麼encoding將被設置爲佔用空間最大的那個類型。同時,其餘值也將被升級編碼爲該類型。

升級

當咱們要將一個新元素添加到整數集合裏時,而且新元素的類型比整數集合現有元素的類型都要長時,咱們將須要先將整數集合進行升級,才能將新元素添加進去。
升級整數集合並添加新元素分三步進行:

  1. 根據新元素的類型,擴展整數集合底層數組的空間大小,併爲新元素分配空間。
  2. 將底層數組的其餘元素都轉換爲新類型,並保存與原來相同的順序放置。
  3. 最後再將新元素添加到數組中。

壓縮列表

壓縮列表是列表建和哈希鍵的底層實現之一。當列表鍵或哈希鍵中的元素較少時,將會使用壓縮列表來做爲他們的底層實現。

壓縮列表的實現

壓縮列表是Redis爲了節約內存而開發的,是由一系列特殊編碼的連續內存塊組成的順序性數據結構。一個壓縮列表能夠包含任意多個節點,一個節點能夠保存一個SDS或一個整數值。

\[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-GKvIvNi0-1573733502233)(https://i.loli.net/2019/11/14/7hqXRxWIcN4QMwb.png)\]

  1. zlbytes:記錄整個壓縮列表佔用的內存字節數,在對壓縮列表進行內存重分配或計算zlend的位置時使用。
  2. zltail:記錄壓縮列表表尾節點距離壓縮列表的起始地址有多少個字節,經過zltail能夠經過O(1)複雜度肯定表尾節點的地址。
  3. zlen:記錄壓縮列表的節點數量,當這個值等於UINT16_MAX時,節點的真實數量須要遍歷整個壓縮列表才能獲得。
  4. entryX:壓縮列表包含的各個節點。
  5. zlend:特殊值0xFF,用於標記壓縮列表的結尾。

壓縮列表節點的實現

每一個壓縮列表節點能夠保存一個字節數組或者一個整數值。壓縮列表節點由三部分組成。

在這裏插入圖片描述

previous_entry_length

記錄了壓縮列表中前一個節點的長度。previous_entry_length屬性自身的長度能夠是1字節或5字節。

  • 若是前一個節點的長度小於254字節,那麼previous_entry_length屬性的長度爲1字節,前一節點的長度就保存在這1字節裏。
  • 若是前一個字節的長度大於等於254字節,那麼previous_entry_length屬性的長度爲5字節。其中1字節將被設置爲oxFE,而其它4字節用於保存前一節點的長度。 程序能夠經過指針運算,根據當前節點的起始地址來計算出前一個節點的起始地址。壓縮列表的從表尾向表頭的遍歷操做就是利用這一原理實現的。

encoding

  • 記錄了節點的content屬性所保存數據的類型以及長度。一字節、兩字節或五字節長、值的最高位爲00、0一、10的表示節點的content屬性保存着字節數組,數組的長度爲去掉encoding的最高兩位以後的位記錄。
  • 一字節長,值的最高位以11開頭的是整數編碼:這種編碼表示節點的content屬性保存着的是整數值。

content

負責保存節點的值,節點值能夠是字節數組或整數,具體由encoding決定。

連鎖更新

若是當前壓縮列表的節點長度都小於254字節,那麼用於記錄前一個字節長度的屬性previous_entry_length只須要用一個字節保存,可是如今要新加一個字節長度大於254字節的節點到壓縮列表中來,那麼將會形成連鎖更新,由於新加節點的後一個節點保存了這個節點的長度,須要將previous_entry_length擴展爲5字節的,而後繼續相似的擴展直到最後一個節點。

參考

Redis的設計與實現 黃建宏 著

相關文章
相關標籤/搜索