Redis 的基礎數據結構(一) 可變字符串、鏈表、字典

原文地址:https://xilidou.com/2018/03/12/redis-data/java

這周開始學習 Redis,看看Redis是怎麼實現的。因此會寫一系列關於 Redis的文章。這篇文章關於 Redis 的基礎數據。閱讀這篇文章你能夠了解:redis

  • 動態字符串(SDS)
  • 鏈表
  • 字典

三個數據結構 Redis 是怎麼實現的。數據庫

SDS

SDS (Simple Dynamic String)是 Redis 最基礎的數據結構。直譯過來就是」簡單的動態字符串「。Redis 本身實現了一個動態的字符串,而不是直接使用了 C 語言中的字符串。數組

sds 的數據結構:安全

struct sdshdr {
    
    // buf 中已佔用空間的長度
    int len;

    // buf 中剩餘可用空間的長度
    int free;

    // 數據空間
    char buf[];
};

因此一個 SDS 的就以下圖:微信

sds

因此咱們看到,sds 包含3個參數。buf 的長度 len,buf 的剩餘長度,以及buf。數據結構

爲何這麼設計呢?函數

  • 能夠直接獲取字符串長度。
    C 語言中,獲取字符串的長度須要用指針遍歷字符串,時間複雜度爲 O(n),而 SDS 的長度,直接從len 獲取複雜度爲 O(1)。
  • 杜絕緩衝區溢出。
    因爲C 語言不記錄字符串長度,若是增長一個字符傳的長度,若是沒有注意就可能溢出,覆蓋了緊挨着這個字符的數據。對於SDS 而言增長字符串長度須要驗證 free的長度,若是free 不夠就會擴容整個 buf,防止溢出。
  • 減小修改字符串長度時形成的內存再次分配。
    redis 做爲高性能的內存數據庫,須要較高的相應速度。字符串也很大機率的頻繁修改。 SDS 經過未使用空間這個參數,將字符串的長度和底層buf的長度之間的額關係解除了。buf的長度也不是字符串的長度。基於這個分設計 SDS 實現了空間的預分配和惰性釋放。性能

    1. 預分配
若是對 SDS 修改後,若是 len 小於 1MB 那 len = 2 * len + 1byte。 這個 1 是用於保存空字節。
若是 SDS 修改後 len 大於 1MB 那麼 len = 1MB + len + 1byte。
2. 惰性釋放
若是縮短 SDS 的字符串長度,redis並非立刻減小 SDS 所佔內存。只是增長 free 的長度。同時向外提供 API 。真正須要釋放的時候,纔去從新縮小 SDS 所佔的內存
  • 二進制安全。
    C 語言中的字符串是以 」0「 做爲字符串的結束標記。而 SDS 是使用 len 的長度來標記字符串的結束。因此SDS 能夠存儲字符串以外的任意二進制流。由於有可能有的二進制流在流中就包含了」0「形成字符串提早結束。也就是說 SDS 不依賴 "0" 做爲結束的依據。
  • 兼容C語言
    SDS 按照慣例使用 」0「 做爲結尾的管理。部分普通C 語言的字符串 API 也可使用。

鏈表

C語言中並無鏈表這個數據結構因此 Redis 本身實現了一個。Redis 中的鏈表是:學習

typedef struct listNode {

    // 前置節點
    struct listNode *prev;

    // 後置節點
    struct listNode *next;

    // 節點的值
    void *value;

} listNode;

很是典型的雙向鏈表的數據結構。

同時爲雙向鏈表提供了以下操做的函數:

/*
 * 雙端鏈表迭代器
 */
typedef struct listIter {

    // 當前迭代到的節點
    listNode *next;

    // 迭代的方向
    int direction;

} listIter;

/*
 * 雙端鏈表結構
 */
typedef struct list {

    // 表頭節點
    listNode *head;

    // 表尾節點
    listNode *tail;

    // 節點值複製函數
    void *(*dup)(void *ptr);

    // 節點值釋放函數
    void (*free)(void *ptr);

    // 節點值對比函數
    int (*match)(void *ptr, void *key);

    // 鏈表所包含的節點數量
    unsigned long len;

} list;

鏈表的結構比較簡單,數據結構以下:

list

總結一下性質:

  • 雙向鏈表,某個節點尋找上一個或者下一個節點時間複雜度 O(1)。
  • list 記錄了 head 和 tail,尋找 head 和 tail 的時間複雜度爲 O(1)。
  • 獲取鏈表的長度 len 時間複雜度 O(1)。

字典

字典數據結構極其相似 java 中的 Hashmap。

Redis的字典由三個基礎的數據結構組成。最底層的單位是哈希表節點。結構以下:

typedef struct dictEntry {
    
    // 鍵
    void *key;

    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;

    // 指向下個哈希表節點,造成鏈表
    struct dictEntry *next;

} dictEntry;

實際上哈希表節點就是一個單項列表的節點。保存了一下下一個節點的指針。 key 就是節點的鍵,v是這個節點的值。這個 v 既能夠是一個指針,也能夠是一個 uint64_t或者 int64_t 整數。*next 指向下一個節點。

經過一個哈希表的數組把各個節點連接起來:

typedef struct dictht {
    
    // 哈希表數組
    dictEntry **table;

    // 哈希表大小
    unsigned long size;
    
    // 哈希表大小掩碼,用於計算索引值
    // 老是等於 size - 1
    unsigned long sizemask;

    // 該哈希表已有節點的數量
    unsigned long used;

} dictht;

dictht

經過圖示咱們觀察:

dictht.png

實際上,若是對java 的基本數據結構瞭解的同窗就會發現,這個數據結構和 java 中的 HashMap 是很相似的,就是數組加鏈表的結構。

字典的數據結構:

typedef struct dict {

    // 類型特定函數
    dictType *type;

    // 私有數據
    void *privdata;

    // 哈希表
    dictht ht[2];

    // rehash 索引
    // 當 rehash 不在進行時,值爲 -1
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */

    // 目前正在運行的安全迭代器的數量
    int iterators; /* number of iterators currently running */

} dict;

其中的dictType 是一組方法,代碼以下:

/*
 * 字典類型特定函數
 */
typedef struct dictType {

    // 計算哈希值的函數
    unsigned int (*hashFunction)(const void *key);

    // 複製鍵的函數
    void *(*keyDup)(void *privdata, const void *key);

    // 複製值的函數
    void *(*valDup)(void *privdata, const void *obj);

    // 對比鍵的函數
    int (*keyCompare)(void *privdata, const void *key1, const void *key2);

    // 銷燬鍵的函數
    void (*keyDestructor)(void *privdata, void *key);
    
    // 銷燬值的函數
    void (*valDestructor)(void *privdata, void *obj);

} dictType;

字典的數據結構以下圖:

dict

這裏咱們能夠看到一個dict 擁有兩個 dictht。通常來講只使用 ht[0],當擴容的時候發生了rehash的時候,ht[1]纔會被使用。

當咱們觀察或者研究一個hash結構的時候偶咱們首先要考慮的這個 dict 如何插入一個數據?

咱們梳理一下插入數據的邏輯。

  • 計算Key 的 hash 值。找到 hash 映射到 table 數組的位置。
  • 若是數據已經有一個 key 存在了。那就意味着發生了 hash 碰撞。新加入的節點,就會做爲鏈表的一個節點接到以前節點的 next 指針上。
  • 若是 key 發生了屢次碰撞,形成鏈表的長度愈來愈長。會使得字典的查詢速度降低。爲了維持正常的負載。Redis 會對 字典進行 rehash 操做。來增長 table 數組的長度。因此咱們要着重瞭解一下 Redis 的 rehash。步驟以下:

    1. 根據ht[0] 的數據和操做的類型(擴大或縮小),分配 ht[1] 的大小。
    2. 將 ht[0] 的數據 rehash 到 ht[1] 上。
    3. rehash 完成之後,將ht[1] 設置爲 ht[0],生成一個新的ht[1]備用。
  • 漸進式的 rehash 。

其實若是字典的 key 數量很大,達到千萬級以上,rehash 就會是一個相對較長的時間。因此爲了字典可以在 rehash 的時候可以繼續提供服務。Redis 提供了一個漸進式的 rehash 實現,rehash的步驟以下:

1. 分配 ht[1] 的空間,讓字典同時持有 ht[1] 和 ht[0]。
2. 在字典中維護一個 rehashidx,設置爲 0 ,表示字典正在 rehash。
3. 在rehash期間,每次對字典的操做除了進行指定的操做之外,都會根據 ht[0] 在 rehashidx 上對應的鍵值對 rehash 到 ht[1]上。
4. 隨着操做進行, ht[0] 的數據就會所有 rehash 到 ht[1] 。設置ht[0] 的 rehashidx 爲 -1,漸進的 rehash 結束。

這樣保證數據可以平滑的進行 rehash。防止 rehash 時間太久阻塞線程。

  • 在進行 rehash 的過程當中,若是進行了 delete 和 update 等操做,會在兩個哈希表上進行。若是是 find 的話優先在ht[0] 上進行,若是沒有找到,再去 ht[1] 中查找。若是是 insert 的話那就只會在 ht[1]中插入數據。這樣就會保證了 ht[1] 的數據只增不減,ht[0]的數據只減不增。

歡迎關注個人微信公衆號:
二維碼

相關文章
相關標籤/搜索