圖解redis五種數據結構底層實現(動圖哦)

redis有五種基本數據結構:字符串、hash、set、zset、list。可是你知道構成這五種結構的底層數據結構是怎樣的嗎? 今天咱們來花費五分鐘的時間瞭解一下。 (目前redis版本爲3.0.6)redis

動態字符串SDS

SDS是"simple dynamic string"的縮寫。 redis中全部場景中出現的字符串,基本都是由SDS來實現的數據庫

  • 全部非數字的key。例如set msg "hello world" 中的key msg.
  • 字符串數據類型的值。例如`` set msg "hello world"中的msg的值"hello wolrd"
  • 非字符串數據類型中的「字符串值」。例如RPUSH fruits "apple" "banana" "cherry"中的"apple" "banana" "cherry"

SDS長這樣:

free:還剩多少空間 len:字符串長度 buf:存放的字符數組數組

空間預分配

爲減小修改字符串帶來的內存重分配次數,sds採用了「一次管夠」的策略:服務器

  • 若修改以後sds長度小於1MB,則多分配現有len長度的空間
  • 若修改以後sds長度大於等於1MB,則擴充除了知足修改以後的長度外,額外多1MB空間

惰性空間釋放

爲避免縮短字符串時候的內存重分配操做,sds在數據減小時,並不馬上釋放空間。微信

int

就是redis中存放的各類數字 包括一下這種,故意加引號「」的數據結構

雙向鏈表

長這樣:app

分兩部分,一部分是「統籌部分」:橘黃色,一部分是「具體實施方「:藍色。函數

主體」統籌部分「:ui

  • head指向具體雙向鏈表的頭
  • tail指向具體雙向鏈表的尾
  • len雙向鏈表的長度

具體"實施方":一目瞭然的雙向鏈表結構,有前驅pre有後繼nextthis

listlistNode兩個數據結構構成。

ziplist

壓縮列表。 redis的列表鍵和哈希鍵的底層實現之一。此數據結構是爲了節約內存而開發的。和各類語言的數組相似,它是由連續的內存塊組成的,這樣一來,因爲內存是連續的,就減小了不少內存碎片和指針的內存佔用,進而節約了內存。

而後文中的entry的結構是這樣的:

元素的遍歷

先找到列表尾部元素:

而後再根據ziplist節點元素中的previous_entry_length屬性,來逐個遍歷:

連鎖更新

再次看看entry元素的結構,有一個previous_entry_length字段,他的長度要麼都是1個字節,要麼都是5個字節:

  • 前一節點的長度小於254字節,則previous_entry_length長度爲1字節
  • 前一節點的長度大於254字節,則previous_entry_length長度爲5字節

假設如今存在一組壓縮列表,長度都在250字節至253字節之間,忽然新增一新節點new, 長度大於等於254字節,會出現:

程序須要不斷的對壓縮列表進行空間重分配工做,直到結束。

除了增長操做,刪除操做也有可能帶來「連鎖更新」。 請看下圖,ziplist中全部entry節點的長度都在250字節至253字節之間,big節點長度大於254字節,small節點小於254字節。

哈希表

哈希表略微有點複雜。哈希表的製做方法通常有兩種,一種是:開放尋址法,一種是拉鍊法。redis的哈希表的製做使用的是拉鍊法

總體結構以下圖:

也是分爲兩部分:左邊橘黃色部分和右邊藍色部分,一樣,也是」統籌「和」實施「的關係。 具體哈希表的實現,都是在藍色部分實現的。 先來看看藍色部分:

這也分爲左右兩邊「統籌」和「實施」的兩部分。

右邊部分很容易理解:就是一般拉鍊表實現的哈希表的樣式;數組就是bucket,通常不一樣的key首先會定位到不一樣的bucket,若key重複,就用鏈表把衝突的key串起來。

新建key的過程:

假如重複了:

rehash

再來看看哈希表整體圖中左邊橘黃色的「統籌」部分,其中有兩個關鍵的屬性:htrehashidxht是一個數組,有且只有倆元素ht[0]和ht[1];其中,ht[0]存放的是redis中使用的哈希表,而ht[1]和rehashidx和哈希表的rehash有關。

rehash指的是從新計算鍵的哈希值和索引值,而後將鍵值對重排的過程。

加載因子(load factor) = ht[0].used / ht[0].size

擴容和收縮標準

擴容:

  • 沒有執行BGSAVE和BGREWRITEAOF指令的狀況下,哈希表的加載因子大於等於1。
  • 正在執行BGSAVE和BGREWRITEAOF指令的狀況下,哈希表的加載因子大於等於5。

收縮:

  • 加載因子小於0.1時,程序自動開始對哈希表進行收縮操做。

擴容和收縮的數量

擴容:

  • 第一個大於等於ht[0].used * 22^n(2的n次方冪)。

收縮:

  • 第一個大於等於ht[0].used2^n(2的n次方冪)。

(如下部分屬於細節分析,能夠跳過直接看擴容步驟) 對於收縮,我當時陷入了疑慮:收縮標準是加載因子小於0.1的時候,也就是說假如哈希表中有4個元素的話,哈希表的長度只要大於40,就會進行收縮,假若有一個長度大於40,可是存在的元素爲4即(ht[0].used爲4)的哈希表,進行收縮,那收縮後的值爲多少?

我想了一下:按照前文所講的內容,應該是4。 可是,假如是4,存在和收縮後的長度相等,是否是又該擴容? 翻開源碼看看:

收縮具體函數:

int dictResize(dict *d)     //縮小字典d
{
    int minimal;

    //若是dict_can_resize被設置成0,表示不能進行rehash,或正在進行rehash,返回出錯標誌DICT_ERR
    if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;

    minimal = d->ht[0].used;            //得到已經有的節點數量做爲最小限度minimal
    if (minimal < DICT_HT_INITIAL_SIZE)//可是minimal不能小於最低值DICT_HT_INITIAL_SIZE(4)
        minimal = DICT_HT_INITIAL_SIZE;
    return dictExpand(d, minimal);      //用minimal調整字典d的大小
} 
複製代碼
int dictExpand(dict *d, unsigned long size)     //根據size調整或建立字典d的哈希表
{
    dictht n; 
    unsigned long realsize = _dictNextPower(size);  //得到一個最接近2^n的realsize

    if (dictIsRehashing(d) || d->ht[0].used > size) //正在rehash或size不夠大返回出錯標誌
        return DICT_ERR;

    if (realsize == d->ht[0].size) return DICT_ERR; //若是新的realsize和本來的size同樣則返回出錯標誌
    /* Allocate the new hash table and initialize all pointers to NULL */
    //初始化新的哈希表的成員
    n.size = realsize;
    n.sizemask = realsize-1;
    n.table = zcalloc(realsize*sizeof(dictEntry*));
    n.used = 0;

    /* Is this the first initialization? If so it's not really a rehashing * we just set the first hash table so that it can accept keys. */
    if (d->ht[0].table == NULL) {   //若是ht[0]哈希表爲空,則將新的哈希表n設置爲ht[0]
        d->ht[0] = n;
        return DICT_OK;
    }

    d->ht[1] = n;           //若是ht[0]非空,則須要rehash
    d->rehashidx = 0;       //設置rehash標誌位爲0,開始漸進式rehash(incremental rehashing)
    return DICT_OK;
} 
複製代碼
static unsigned long _dictNextPower(unsigned long size)
{
    unsigned long i = DICT_HT_INITIAL_SIZE; //DICT_HT_INITIAL_SIZE 爲 4

    if (size >= LONG_MAX) return LONG_MAX + 1LU;
    while(1) {
        if (i >= size)
            return i;
        i *= 2;
    }
}

複製代碼

由代碼咱們能夠看到,假如收縮後長度爲4,不只不會收縮,甚至還會報錯。(😝)

咱們回過頭來再看看設定:題目可能成立嗎? 哈希表的擴容都是2倍增加的,最小是4, 4 ===》 8 ====》 16 =====》 32 ======》 64 ====》 128

也就是說:不存在長度爲 40多的狀況,只能是64。可是若是是64的話,64 X 0.1(收縮界限)= 6.4 ,也就是說在減小到6的時候,哈希表就會收縮,會縮小到多少呢?是8。此時,再繼續減小到4,也不會再收縮了。因此,根本不存在一個長度大於40,可是存在的元素爲4的哈希表的。

擴容步驟

收縮步驟

漸進式refresh

在"擴容步驟"和"收縮步驟" 兩幅動圖中每幅圖的第四步驟「將ht[0]中的數據利用哈希函數從新計算,rehash到ht[1]」,並非一步完成的,而是分紅N多步,按部就班的完成的。 由於hash中有可能存放幾千萬甚至上億個key,畢竟Redis中每一個hash中能夠存2^32 - 1 鍵值對(40多億),假如一次性將這些鍵值rehash的話,可能會致使服務器在一段時間內中止服務,畢竟哈希函數就得計算一陣子呢((#^.^#))。

哈希表的refresh是分屢次、漸進式進行的。

漸進式refresh和下圖中左邊橘黃色的「統籌」部分中的rehashidx密切相關:

  • rehashidx 的數值就是如今rehash的元素位置
  • rehashidx 等於 -1 的時候說明沒有在進行refresh

甚至在進行期間,每次對哈希表的增刪改查操做,除了正常執行以外,還會順帶將ht[0]哈希表相關鍵值對rehash到ht[1]。

以擴容步驟爲例:

intset

整數集合是集合鍵的底層實現方式之一。

跳錶

跳錶這種數據結構長這樣:

redis中把跳錶抽象成以下所示:

看這個圖,左邊「統籌」,右邊實現。 統籌部分有如下幾點說明:

  • header: 跳錶表頭
  • tail:跳錶表尾
  • level:層數最大的那個節點的層數
  • length:跳錶的長度

實現部分有如下幾點說明:

  • 表頭:是鏈表的哨兵節點,不記錄主體數據。
  • 是個雙向鏈表
  • 分值是有順序的
  • o一、o二、o3是節點所保存的成員,是一個指針,能夠指向一個SDS值。
  • 層級高度最高是32。沒每次建立一個新的節點的時候,程序都會隨機生成一個介於1和32之間的值做爲level數組的大小,這個大小就是「高度」

redis五種數據結構的實現

redis對象

redis中並無直接使用以上所說的各類數據結構來實現鍵值數據庫,而是基於一種對象,對象底層再間接的引用上文所說的具體的數據結構。

結構以下圖:

字符串

其中:embstr和raw都是由SDS動態字符串構成的。惟一區別是:raw是分配內存的時候,redisobject和 sds 各分配一塊內存,而embstr是redisobject和raw在一起內存中。

列表

hash

set

zset

更多精彩內容,請關注個人微信公衆號 互聯網技術窩 或者加微信共同探討交流:

參考文獻

相關文章
相關標籤/搜索