Redis(二)--- Redis的底層數據結構

一、Redis的數據結構

Redis 的底層數據結構包含簡單的動態字符串(SDS)、鏈表、字典、壓縮列表、整數集合等等;五大數據類型(數據對象)都是由一種或幾種數結構構成。java

在命令行中可使用 OBJECT ENCODING key 來查看key的數據結構。redis

二、簡單動態字符串SDS

 redis是使用C語言編寫的,可是string數據類型並無使用C語言的字符串,而是從新編寫一個簡單的動態字符串(simple dynamic string,SDS)。算法

 1 /*
 2   * 保存字符串對象的結構
 3   */
 4 struct sdshdr {
 5   
 6     // buf 中已佔用空間的長度
 7     int len;
 8   
 9     // buf 中剩餘可用空間的長度
10     int free;
11  
12     // 數據空間
13     char buf[]
14 };

 

使用SDS保存字符串Redis,具體表示以下:數據庫

                                              圖片來自《Redis設計與實現》 黃健宏著數組

 

    • free 表示buf數組中剩餘的空間數量
    • len 記錄了buf數組中已存儲的字節長度
    • buf 數組是一個char類型的數據,記錄具體存儲的字符串,而且以 ‘\0’(空字符) 做爲結束標識符

 SDS定義較C語言的字符串幾乎相同,就是多出兩個屬性free,len;那爲什麼不直接使用C語言的字符串呢?安全

一、獲取字符串長度複雜度爲O(1)服務器

        因爲C語言沒有存儲字符串長度,每次獲取字符串長度多須要進行循環整個字符串計算,時間複雜度爲O(N);而SDS記錄了存儲的字符串的長度,獲取字符串長度時直接獲取len的屬性值便可,時間複雜度爲O(1);而SDS中設置和更新長度是API中自動完成,無需手動進行操做。網絡

二、杜絕緩衝區溢出數據結構

 C語言在進行兩個字符串拼接時,一旦沒有分配足夠的內存空間,就會形成溢出;而SDS在修改字符串時,會先根據len的值,檢查內存空間是否足夠,若是不足會先分配內存空間,再進行字符串修改,這樣就杜絕了緩衝區溢出。函數

三、減小修改字符串時帶來的內存從新分配次數

C語言不記錄字符串長度,因此當修改時,會從新分配內存;若是是正常字符串,內存空間不夠會產生溢出;若是是縮短字符串,不重重分配會產生泄露。

SDS採用空間預分配和惰性釋放空間兩種優化策略

空間預分配:對字符串進行增加操做,會分配出多餘的未使用空間,這樣若是之後的擴展,在必定程度上能夠減小內存從新分配的次數。

惰性釋放空間:對字符串通過縮短操做,並不會當即釋放這些空間,而是使用free來記錄這些空間的數量,當進行增加操做時,這些記錄的空間就能夠被從新利用;SDS提供了響應的API進行手動釋放空間,因此不會形成內存浪費。

四、二進制安全

C語言的字符串中不能包含空字符(由於C語言是以空字符判斷字符串結尾的),因此不能保存一些二進制文件(有可能包含空字符,如圖片);SDS則是以len來判斷字符串結尾,因此SDS結構能夠存儲圖片等,而且都是以二進制方式進行處理。

五、兼容部分C字符串函數

SDS結構中buf保存字符串一樣是以空字符結尾,因此能夠兼容C語言的部分字符串操做API。

總結:

                                             表來源:《Redis設計與實現》

 

三、鏈表

Redis使用C語言編寫,但並無內置鏈表這種數據結構,而是本身構建了鏈表的實現;構成鏈表結構的節點爲鏈表節點。

鏈表用的很是普遍,如列表鍵、發佈與訂閱、慢查詢、監視器等。

1 typedef struct listNode {
2     // 前置節點
3     struct listNode * prev;
4     // 後置節點
5     struct listNode * next;
6     // 節點的值
7     void * value;
8 }listNode;

多個listNode能夠經過prev和next指針構成雙端鏈表,使用list持有鏈表

 1 typedef struct list {
 2     // 表頭節點
 3     listNode * head;
 4     // 表尾節點
 5     listNode * tail;
 6     // 鏈表所包含的節點數量
 7     unsigned long len;
 8     // 節點值複製函數
 9     void *(*dup)(void *ptr);
10     // 節點值釋放函數
11     void (*free)(void *ptr);
12     // 節點值對比函數
13     int (*match)(void *ptr,void *key);
14 } list;
    • head 表頭指針
    • tail 表尾指針
    • len 鏈表長度計數器
    • dup、free、match 多態鏈表所需的類型特定的函數

Redis鏈表實現的特性以下:

一、雙端

鏈表節點帶有prev和next指針,能夠快速獲取前置和後置節點,時間複雜度都是O(1)。

二、無環

 頭節點prev指針和尾節點next指針都指向null,對鏈表訪問以NULL爲終點。

三、帶表頭指針和表尾指針

能夠快速的獲取表頭節點和表尾節點。

四、有鏈表長度計數器

能夠快速獲取鏈表長度。 

五、多態

鏈表能夠保存各類不一樣類型的值,經過list的dup,free,match三個屬性爲節點設計值類型特定的函數。

 

四、字典

字典又稱爲符號表(symbol table)、關聯數組(associative array)或者映射(map);字典中存儲key-value鍵值對,而且key不重複;

字典在Redis中普遍應用,如Redis數據庫就是使用字典做爲底層實現的。

Redis使用的C語言沒有內置這種結構,因此Redis構建了本身的字典實現。

字典使用哈希表做爲底層試下,一個哈希表包含多個哈希節點,每一個哈希節點保存一個鍵值對。

哈希表

 1 typedef struct dictht {
 2     // 哈希表數組
 3     dictEntry **table;
 4     // 哈希表大小
 5     unsigned long size;
 6     // 哈希表大小掩碼,用於計算索引值
 7     // 老是等於size-1
 8     unsigned long sizemask;
 9     // 該哈希表已有節點的數量
10     unsigned long used;
11 } dictht;

圖中是一個大小爲4的空哈希表

    • table是一個數組,數組元素是dictEntry結構的指針,每一個dictEntry保存一個鍵值對
    • size 記錄哈希表的大小
    • sizemask 值老是等於size-1,這個屬性和哈希值一塊兒決定一個鍵應該被方法table數組的哪一個索引上
    • used 記錄哈希表目前已有節點的數量

哈希表節點 

 1 typedef struct dictEntry {
 2     //
 3     void *key;
 4     //
 5     union{
 6         void *val;
 7         uint64_tu64;
 8         int64_ts64;
 9     } v;
10     // 指向下個哈希表節點,造成鏈表
11     struct dictEntry *next;
12 } dictEntry;
    • key屬性保存着鍵值對中的鍵,v屬性保存着鍵值對中的值
    • 鍵值對中的值可使指針val、一個uint64_t整數,或是一個int64_t整數
    • next是指向另外一個哈希表節點的指針,用以解決多個哈希值衝突問題

下圖爲將兩個索引值相同的鍵連在一塊兒

 

字典結構

 1 typedef struct dict {
 2     // 類型特定函數
 3     dictType *type;
 4     // 私有數據
 5     void *privdata;
 6     // 哈希表
 7     dictht ht[2];
 8     // rehash索引
 9     //當rehash不在進行時,值爲-1
10     in trehashidx; /* rehashing not in progress if rehashidx == -1 */
11 } dict;
12 
13 typedef struct dictType {
14     // 計算哈希值的函數
15     unsigned int (*hashFunction)(const void *key);
16     // 複製鍵的函數
17     void *(*keyDup)(void *privdata, const void *key);
18     // 複製值的函數
19     void *(*valDup)(void *privdata, const void *obj);
20     // 對比鍵的函數
21     int (*keyCompare)(void *privdata, const void *key1, const void *key2);
22     // 銷燬鍵的函數
23     void (*keyDestructor)(void *privdata, void *key);
24     // 銷燬值的函數
25     void (*valDestructor)(void *privdata, void *obj);
26 } dictType;
    • type 屬性是一個指向dictType結構的指針,每一個dictType機構保存了一簇用於操做特定類型鍵值對的函數,Redis貨位用途不一樣的字典設置不一樣的類型特定函數。
    • privdata 屬性保存了須要傳給那些類型特定函數的可選參數。
    • ht 屬性是一個長度爲2的數組,數組中的每一個元素都是一個哈希表,通常狀況下自字典只使用ht[0],ht[1]只會在進行rehash時使用.
    • trehashidx 屬性記錄了rehash目前的進度,若是沒有進行rehash則它的值爲-1。

下圖爲普通狀態下的字典結構

當一個新的鍵值對要添加到字典中去時,會涉及到一系列的操做,如計算索引、解決衝突、擴容等等,下面對這些操做進行描述。

一、哈希算法

添加鍵值對時,首先要根據鍵值對的鍵計算出哈希值和索引值,而後再根據索引值進行放入

1 #使用字典設置的哈希函數,計算鍵key的哈希值
2 hash = dict->type->hashFunction(key);
3 #使用哈希表的sizemask屬性和哈希值,計算出索引值
4 #根據狀況不一樣,ht[x]能夠是ht[0]或者ht[1]
5 index = hash & dict->ht[x].sizemask;

二、結局鍵衝突

當有兩個或以上數量的鍵值被分配到了哈希表數組的同一個索引上時,就發生了鍵衝突。

Redis的哈希表使用單向鏈表解決鍵衝突問題,每一個新的鍵老是添加到單項鍊表的表頭。

三、rehash(擴展或收縮)

哈希表具備負載因子(load factor),其始終須要保持在一個合理的範圍以內,當hashI表保存的鍵值對過多或過少時,就須要對哈希表進行rehash(從新散列)操做,步驟許下

(1) 爲字典的ht[1]分配空間,空間大小:若是是擴展操做則爲ht[0].used * 2 ,也就是擴展爲當前哈希表已使用空間的1倍;若是是收縮,則減少1倍。

(2) 將ht[0]內的數據從新計算哈希值和索引,並放到新分配的ht[1]空間上。

(3) 所有遷移完成後,將ht[1]設置爲ht[0],釋放ht[0]並建立一個空白的哈希表爲ht[1],爲下次rehash作準備。

四、哈希表的擴展與收縮觸發條件

(1) 服務器目前沒有在執行BGSAVE命令或者BGREWRITEAOF命令,而且哈希表的負載因子大於等等於1。

(2) 服務器目前正在執行BGSAVE命令或者BGREWRITEAOF命令,而且哈希表的負載因子大於等於5。

以上條件中任意一條被知足,程序自動開始對哈希表進行擴展;

負載因子算法:負載因子 = 哈希表以保存的節點數量 / 哈希表大小

當負載因子小於0.1時,程序自動進行收縮操做。

五、漸進式rehash

漸進式rehash就是,當ht[1]的鍵值對向ht[1]遷移的過程當中,若是數據量過大,則不能一次性遷移, 不然會對服務器性能形成影響,而是分紅屢次,漸進式的進行遷移。

在rehash期間,會維持一個索引計數器rehashidx,並把每次的遷移工做分配到了添加、刪除、查找、更新操做中,當rehash工做完成後rehashidx會增長1,這樣全部的ht[0]的值所有遷移完成後,程序會將rehashidx這是爲-1,標識最終的rehash完成。

六、漸進式rehash之情期間的表操做

因爲漸進式rehash期間,ht[0]和ht[1]中都有數據,當查找時,會先在ht[0]中進行,沒找到繼續到ht[1]中找;而添加操做一概會添加到ht[1]中。

 

字典總結: 

Redis字典底層機構實現與java(1.6以前) 中的hashmap很是相像,都是使用單項鍊表解決鍵衝突問題。

我的疑問:jdk1.8以上已是用紅黑樹解決多個鍵衝突問題,不知redis的鍵衝突是否也能夠用紅黑樹?

 

五、跳躍表

跳躍表(skiplist)數據結構特色是每一個節點中有多個指向其餘節點的指針,從而快速訪問節點。

跳躍表結構由跳躍表節點(zskiplistNode)和zskiplist兩個結構組成

跳躍表節點

 1 typedef struct zskiplistNode {
 2     //
 3     struct zskiplistLevel {
 4         // 前進指針
 5         struct zskiplistNode *forward;
 6         // 跨度
 7         unsigned int span;
 8     } level[];
 9     // 後退指針
10     struct zskiplistNode *backward;
11     // 分值
12     double score;
13     // 成員對象
14     robj *obj;
15 } zskiplistNode;
    • 層:爲一個數組,數組中的每一個數據都包含前進指針和跨度。
    • 前進指針:指向表尾方向的其餘節點的指針,用於從表頭方向到表尾方向快速訪問節點。
    • 跨度:記錄兩個節點之間的距離,跨度越大,兩個節點相聚越遠,全部指向NULL的前進指針的跨度都爲0。
    • 後退指針:用於從表尾節點向表頭節點訪問,每一個節點都有後退指針,而且每次只能後退一個節點。
    • 分值:節點的分值是一個double類型的浮點數,跳躍表中的說有分值按從小到大排列。
    • 成員對象:是一個指向字符串的指針,字符串則保存着一個SDS值。

跳躍表

1 typedef struct zskiplist {
2     // 表頭節點和表尾節點
3     structz skiplistNode *header, *tail;
4     // 表中節點的數量
5     unsigned long length;
6     // 表中層數最大的節點的層數
7     int level;
8 } zskiplist;

    • header 指向跳躍表的表頭節點,tail指向跳躍表的表尾節點,level記錄節點中的最大層數(不含表頭節點),length跳躍表包含節點數量(不含表頭節點)。
    • 跳躍表由不少層構成(L一、L2 ...),每一個層都帶有兩個屬性前進指針和跨度。
    • 每一個節點都包含成員對象(obj)、分值(score)、後退指針(backward),頭結點也包含這些屬性但不會被用到

在此處只是介紹跳躍表的結構相關,關於跳躍表的層的造成,對象的插入、刪除、查詢等操做的原理在此處不作詳解,另外會有文章進行說明。

 

六、整數集合

整數集合(intset)是集合鍵的底層實現之一,當一個集合只包含整數元素,而且元素的個數很少時,Redis就會使用整數集合做爲集合鍵的底層實現。

整數集合能夠保存int16_t、int32_t、int64_t的整數值,而且不會出現重複元素

1 typedef struct intset {
2     // 編碼方式
3     uint32_t encoding;
4     // 集合包含的元素數量
5     uint32_t length;
6     // 保存元素的數組
7     int8_t contents[];
8 } intset;
    • contents數組存儲的是集合中的每一個元素,他的類型是int8_t,但存儲數據的實際類型取決於編碼方式encoding
    • encoding編碼方式有三種INTSET_ENC_INT1六、INTSET_ENC_INT3二、INTSET_ENC_INT64分別對應的是int16_t、int32_t、int64_t類型
    • length記錄整數集合的元素數量,即contents數組的長度

整數集合的升級操做

整數集合中原來保存的是小類型(如:int16_t)的整數,當插入比其類型大(如:int_64_t)的整數時,會把整合集合裏的元素的數據類型都轉換成大的類型,這個過程稱爲升級

升級整數集合並添加新元素步驟以下:

(1)根據新元素的類型,擴展整數集合的底層數據的空間大小,併爲新元素分配空間。

(2)將現有的全部元素的類型轉換成與新元素相同的類型,保持原有數據有序性不變的狀況下,把轉換後的元素放在正確的位置上。

(3)將新元素添加到數組裏。

新元素引起升級,因此新元素要麼比全部元素都大,要麼比全部元素都小。

    • 當小於全部元素時,新元素放在底層數組的最開頭
    • 當大於全部元素時,新元素放在底層數據的最末尾

升級操做的好處

    • 提高整數的靈活性,能夠任意的向集合中放入3中不一樣類型的整數,而不用擔憂類型錯誤。
    • 節約內存,整數集合中只有大類型出現的時候纔會進行升級操做。

整數集合不支持降級操做

 

七、壓縮列表

壓縮列表(ziplist)是Redis爲了節約內存而開發,是一系列特殊編碼的連續內存塊組成的順序型數據結構。

一個壓縮列表能夠包含任意多個節點,每一個節點能夠保存一個字節數組或者一個整數值。

下圖爲壓縮列表的結構

每一個壓縮列表含有若干個節點,而每一個節點都由三部分構成,previous_entry_length、encoding、content,如圖:

 

    • previous_entry_length 存儲的是前一個節點的長度,因爲壓縮列表內存塊連續,使用此屬性值能夠計算前一個節點的地址,壓縮列表就是使用這一原理進行遍歷。
    • previous_entry_length 若是前一節點長度小於254字節,那麼previous_entry_length屬性自己長度爲1字節,存儲的指就是前一節點的長度;若是大於254個字節,那麼previous_entry_length屬性自己長度爲5個字節,前一個字節爲0xFE(十進制254),以後四個字節存儲前一節點的長度。
    • encoding 記錄本節點的content屬性所保存數據的類型及長度,其自己長度爲一字節、兩字節或五字節,值得最高位爲00、01或10的是字節數組的編碼,最高位以11開頭的是整數編碼。
    • content 保存節點的值,能夠是一個字節數組或者整數。

連鎖更新

當對壓縮列表進行添加節點或刪除節點時有可能會引起連鎖更新,因爲每一個節點的 previous_entry_length 存在兩種長度1字節或5字節,當全部節點previous_entry_length都爲1個字節時,有新節點的長度大於254個字節,那麼新的節點的後一個節點的previous_entry_length原來爲1個字節,沒法保存新節點的長度,這是就須要進行空間擴展previous_entry_length屬性由原來的1個字節增長4個字節變爲5個字節,若是增長後原節點的長度超過了254個字節則後續節點也要空間擴展,以此類推,最極端的狀況是一直擴展到最後一個節點完成;這種現象稱爲連鎖更新。在平常應用中所有連鎖更新的狀況屬於很是極端的,不常出現。

 

八、總結

Redis的底層數據結構共有六種,簡單動態字符串(SDS)、鏈表、字典、跳躍表、整數集合、壓縮列表。

Redis中的五大數據類型的底層就是由他們中的一種或幾種實現,數據的存儲結構最終也會落到他們上。

但是在redis命令下使用 OBJECT ENCODING 命令查看鍵值對象的編碼方式,也就是是以哪一種結構進行的底層編碼。

 

 參考:

《Redis設計與實現》黃健宏著,網上對Redis的詳解等

 

此博客爲筆者使用redis好久以後,參考網絡上各種文章總結性書寫,原創手打,若有錯誤歡迎指正。

相關文章
相關標籤/搜索