溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

探索Redis設計與實現(xiàn)1:Redis 的基礎數(shù)據(jù)結構概覽

發(fā)布時間:2020-08-10 18:52:28 來源:ITPUB博客 閱讀:165 作者:Java技術江湖 欄目:關系型數(shù)據(jù)庫

本文轉自互聯(lián)網

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫里查看

https://github.com/h3pl/Java-Tutorial

喜歡的話麻煩點下Star哈

文章首發(fā)于我的個人博客:

www.how2playlife.com

本文是微信公眾號【Java技術江湖】的《探索Redis設計與實現(xiàn)》其中一篇,本文部分內容來源于網絡,為了把本文主題講得清晰透徹,也整合了很多我認為不錯的技術博客內容,引用其中了一些比較好的博客文章,如有侵權,請聯(lián)系作者。

該系列博文會告訴你如何從入門到進階,Redis基本的使用方法,Redis的基本數(shù)據(jù)結構,以及一些進階的使用方法,同時也需要進一步了解Redis的底層數(shù)據(jù)結構,再接著,還會帶來Redis主從復制、集群、分布式鎖等方面的相關內容,以及作為緩存的一些使用方法和注意事項,以便讓你更完整地了解整個Redis相關的技術體系,形成自己的知識框架。

如果對本系列文章有什么建議,或者是有什么疑問的話,也可以關注公眾號【Java技術江湖】聯(lián)系作者,歡迎你參與本系列博文的創(chuàng)作和修訂。

這周開始學習 Redis,看看Redis是怎么實現(xiàn)的。所以會寫一系列關于 Redis的文章。這篇文章關于 Redis 的基礎數(shù)據(jù)。閱讀這篇文章你可以了解:

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

三個數(shù)據(jù)結構 Redis 是怎么實現(xiàn)的。

SDS

SDS (Simple Dynamic String)是 Redis 最基礎的數(shù)據(jù)結構。直譯過來就是”簡單的動態(tài)字符串“。Redis 自己實現(xiàn)了一個動態(tài)的字符串,而不是直接使用了 C 語言中的字符串。

sds 的數(shù)據(jù)結構:

struct sdshdr {   
// buf 中已占用空間的長度 int len; 
// buf 中剩余可用空間的長度 int free; 
// 數(shù)據(jù)空間 
char buf[];
}

所以一個 SDS 的就如下圖:

探索Redis設計與實現(xiàn)1:Redis 的基礎數(shù)據(jù)結構概覽

所以我們看到,sds 包含3個參數(shù)。buf 的長度 len,buf 的剩余長度,以及buf。

為什么這么設計呢?

  • 可以直接獲取字符串長度。
    C 語言中,獲取字符串的長度需要用指針遍歷字符串,時間復雜度為 O(n),而 SDS 的長度,直接從len 獲取復雜度為 O(1)。

  • 杜絕緩沖區(qū)溢出。
    由于C 語言不記錄字符串長度,如果增加一個字符傳的長度,如果沒有注意就可能溢出,覆蓋了緊挨著這個字符的數(shù)據(jù)。對于SDS 而言增加字符串長度需要驗證 free的長度,如果free 不夠就會擴容整個 buf,防止溢出。

  • 減少修改字符串長度時造成的內存再次分配。
    redis 作為高性能的內存數(shù)據(jù)庫,需要較高的相應速度。字符串也很大概率的頻繁修改。 SDS 通過未使用空間這個參數(shù),將字符串的長度和底層buf的長度之間的額關系解除了。buf的長度也不是字符串的長度?;谶@個分設計 SDS 實現(xiàn)了空間的預分配和惰性釋放。

    1. 預分配
      如果對 SDS 修改后,如果 len 小于 1MB 那 len = 2 * len + 1byte。 這個 1 是用于保存空字節(jié)。
      如果 SDS 修改后 len 大于 1MB 那么 len = 1MB + len + 1byte。
    2. 惰性釋放
      如果縮短 SDS 的字符串長度,redis并不是馬上減少 SDS 所占內存。只是增加 free 的長度。同時向外提供 API 。真正需要釋放的時候,才去重新縮小 SDS 所占的內存
  • 二進制安全。
    C 語言中的字符串是以 ”\0“ 作為字符串的結束標記。而 SDS 是使用 len 的長度來標記字符串的結束。所以SDS 可以存儲字符串之外的任意二進制流。因為有可能有的二進制流在流中就包含了”\0“造成字符串提前結束。也就是說 SDS 不依賴 “\0” 作為結束的依據(jù)。

  • 兼容C語言
    SDS 按照慣例使用 ”\0“ 作為結尾的管理。部分普通C 語言的字符串 API 也可以使用。

鏈表

C語言中并沒有鏈表這個數(shù)據(jù)結構所以 Redis 自己實現(xiàn)了一個。Redis 中的鏈表是:

typedef struct listNode { 
// 前置節(jié)點 struct listNode *prev; 
// 后置節(jié)點 struct listNode *next; 
// 節(jié)點的值 void *value;} listNode;

非常典型的雙向鏈表的數(shù)據(jù)結構。

同時為雙向鏈表提供了如下操作的函數(shù):

/* * 雙端鏈表迭代器 */typedef struct listIter { 
// 當前迭代到的節(jié)點 listNode *next; 
// 迭代的方向 int direction;} listIter;
/* * 雙端鏈表結構 
*/typedef struct list { 
// 表頭節(jié)點 listNode *head; 
// 表尾節(jié)點 listNode *tail; 
// 節(jié)點值復制函數(shù) void *(*dup)(void *ptr); 
// 節(jié)點值釋放函數(shù) void (*free)(void *ptr); 
// 節(jié)點值對比函數(shù) int (*match)(void *ptr, void *key); 
// 鏈表所包含的節(jié)點數(shù)量 unsigned long len;} list;

鏈表的結構比較簡單,數(shù)據(jù)結構如下:

探索Redis設計與實現(xiàn)1:Redis 的基礎數(shù)據(jù)結構概覽

總結一下性質:

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

字典

字典數(shù)據(jù)結構極其類似 java 中的 Hashmap。

Redis的字典由三個基礎的數(shù)據(jù)結構組成。最底層的單位是哈希表節(jié)點。結構如下:

typedef struct dictEntry {
    // 鍵
    void *key;
    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;
    // 指向下個哈希表節(jié)點,形成鏈表
    struct dictEntry *next;
} dictEntry;

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

通過一個哈希表的數(shù)組把各個節(jié)點鏈接起來:
typedef struct dictht {

    // 哈希表數(shù)組
    dictEntry **table;
    // 哈希表大小
    unsigned long size;
    // 哈希表大小掩碼,用于計算索引值
    // 總是等于 size - 1
    unsigned long sizemask;
    // 該哈希表已有節(jié)點的數(shù)量
    unsigned long used;
} dictht;

dictht

通過圖示我們觀察:

探索Redis設計與實現(xiàn)1:Redis 的基礎數(shù)據(jù)結構概覽

實際上,如果對java 的基本數(shù)據(jù)結構了解的同學就會發(fā)現(xiàn),這個數(shù)據(jù)結構和 java 中的 HashMap 是很類似的,就是數(shù)組加鏈表的結構。

字典的數(shù)據(jù)結構:

typedef struct dict {
    // 類型特定函數(shù)
    dictType *type;
    // 私有數(shù)據(jù)
    void *privdata;
    // 哈希表
    dictht ht[2];
    // rehash 索引
    // 當 rehash 不在進行時,值為 -1
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */
    // 目前正在運行的安全迭代器的數(shù)量
    int iterators; /* number of iterators currently running */
} dict;

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

/*
 * 字典類型特定函數(shù)
 */
typedef struct dictType {
    // 計算哈希值的函數(shù)
    unsigned int (*hashFunction)(const void *key);
    // 復制鍵的函數(shù)
    void *(*keyDup)(void *privdata, const void *key);
    // 復制值的函數(shù)
    void *(*valDup)(void *privdata, const void *obj);
    // 對比鍵的函數(shù)
    int (*keyCompare)(void *privdata, const void *key1, const void *key2);
    // 銷毀鍵的函數(shù)
    void (*keyDestructor)(void *privdata, void *key);
    // 銷毀值的函數(shù)
    void (*valDestructor)(void *privdata, void *obj);
} dictType;

字典的數(shù)據(jù)結構如下圖:

探索Redis設計與實現(xiàn)1:Redis 的基礎數(shù)據(jù)結構概覽

這里我們可以看到一個dict 擁有兩個 dictht。一般來說只使用 ht[0],當擴容的時候發(fā)生了rehash的時候,ht[1]才會被使用。

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

我們梳理一下插入數(shù)據(jù)的邏輯。

  • 計算Key 的 hash 值。找到 hash 映射到 table 數(shù)組的位置。

  • 如果數(shù)據(jù)已經有一個 key 存在了。那就意味著發(fā)生了 hash 碰撞。新加入的節(jié)點,就會作為鏈表的一個節(jié)點接到之前節(jié)點的 next 指針上。

  • 如果 key 發(fā)生了多次碰撞,造成鏈表的長度越來越長。會使得字典的查詢速度下降。為了維持正常的負載。Redis 會對 字典進行 rehash 操作。來增加 table 數(shù)組的長度。所以我們要著重了解一下 Redis 的 rehash。步驟如下:

    1. 根據(jù)ht[0] 的數(shù)據(jù)和操作的類型(擴大或縮?。?,分配 ht[1] 的大小。
    2. 將 ht[0] 的數(shù)據(jù) rehash 到 ht[1] 上。
    3. rehash 完成以后,將ht[1] 設置為 ht[0],生成一個新的ht[1]備用。
  • 漸進式的 rehash 。
    其實如果字典的 key 數(shù)量很大,達到千萬級以上,rehash 就會是一個相對較長的時間。所以為了字典能夠在 rehash 的時候能夠繼續(xù)提供服務。Redis 提供了一個漸進式的 rehash 實現(xiàn),rehash的步驟如下:

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

這樣保證數(shù)據(jù)能夠平滑的進行 rehash。防止 rehash 時間過久阻塞線程。

  • 在進行 rehash 的過程中,如果進行了 delete 和 update 等操作,會在兩個哈希表上進行。如果是 find 的話優(yōu)先在ht[0] 上進行,如果沒有找到,再去 ht[1] 中查找。如果是 insert 的話那就只會在 ht[1]中插入數(shù)據(jù)。這樣就會保證了 ht[1] 的數(shù)據(jù)只增不減,ht[0]的數(shù)據(jù)只減不增。
向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經查實,將立刻刪除涉嫌侵權內容。

AI