小白也能看懂的Redis教學基礎篇——redis基礎資料結構
各位看官大大們,週末好!
作為一個Java後端開發,要想獲得比較可觀的工資,Redis基本上是必會的(不要問我為什麼知道,問就是被問過無數次)。
那麼Redis是什麼,它到底擁有什麼神祕的力量,能獲得眾多公司的青睞?接下來就由小編我帶大家來揭祕Redis的五種基本資料結構。
Redis是C語音編寫的基於記憶體的資料結構儲存系統。它可以當作資料庫、快取和訊息中介軟體。 它支援多種型別的資料結構,如 字串(strings),
列表(lists), 字典(dictht),集合(sets), 有序集合(sorted sets)。通常我們在專案中可以用它來做快取、記錄簽到資料、分散式鎖等等。
要使用Redis首先我們來了解一下它的五種基礎資料結構。
1.字串(strings)
Redis擁有兩種字串表述方式,其一是C語言傳統的字串表述方式,常用Redis程式碼中字串常量等一些無需對字串進行修改的地方。
第二種是自己封裝了一種名為簡單動態字串(simple dynamic string)簡稱SDS的抽象型別,這個才是我們儲存字串時所使用的物件,等同於java中的StringBuilder。
SDS的結構如下:
struct sdshdr{ //記錄字元陣列中已經使用的位元組數量 即是字串的長度 int len; //記錄字元陣列中未使用的位元組數 int free; //字元陣列 用於儲存字串 char buf[]; }
結構如下圖所示:
至於Redis為什麼要使用這樣的結構,其實和java使用StringBuilder的思維是大相徑庭。為了方便修改和提升效能。比如C的字串獲取字串長度時要遍歷整個字元陣列。
其時間複雜度是O(n),而SDS則可以直接獲取len,時間複雜度為O(1)。修改字串N次字串並且字串和以前的長度不一致時,C普通字串長度必然需要執行N次記憶體重分配。
而SDS存在預擴容,所以最多需要執行N次記憶體分配。
注:與擴容其本質和list類似,在需要的長度大於現在陣列的長度時,會觸發字串擴容,當資料小於1M時,字元陣列每次擴容都是其原來容量的2倍。1M後每次擴容新增1M容量。
2.列表
Redis中的列表相當於java中的LinkedList,它是一個雙向連結串列,插入和刪除都擁有極好的效能,時間複雜度為O(1),但是隨機查詢比較慢,時間複雜度為O(n)。雖然可以將列表
當成一個LinkedList,但是在Redis內部列表並不是一個簡單的雙向連結串列的實現。在列表儲存元素個數小於512個且每個元素長度小於64位元組的時候為了節省記憶體其底層實現是一塊
連續記憶體來儲存,稱之為ziplist壓縮列表。當不滿足之前的兩個條件時則改用quicklist快速列表來儲存原元素。
ziplist壓縮列表:
壓縮列表是Redis為了節約記憶體而開發的,是由一系列特殊編碼的連續記憶體塊組成的順序型資料結構。一個壓縮列表可以包含任意多個節點,每個節點儲存一個位元組陣列或者一個整數值。
struct ziplist<T>{ int32 zlbytes; int32 zltail_offset; int16 zllemhth; T[] entries; int8 zlend; }
其結構如下圖所示:
節點結構如下:
struct entry{ int<var> previous_entry_length;//前一個原數的位元組長度 int<var> encoding;//元數型別編碼 optional byte[] content;//元素內容 }
這裡有一個點要注意,如果entryX+1和起身後的節點的長度都都在250~253個位元組之間的話,如果entryX長度變成了254個位元組。那麼entryX+1中的previous_entry_length將擴容成5個位元組,
這將導致entryX+1的整體長度也會大於254個位元組,引起entryX+2個位元組中的previous_entry_length也發生擴容,使得entryX+2的整體長度也超過254。並對後面的節點造成連鎖影響這個就叫連鎖更新。
將會對效能造成一定的影響。
quicklist快速列表:
快速列表是ziplist和linkedlist的混合體。它將linkedlist按段切分,每一段使用ziplist讓記憶體緊湊,多個ziplist之間使用雙向指標串接起來。為了進一步節省空間。Redis還會對ziplist進行壓縮,
使用LZF演算法壓縮。可以選擇壓縮的深度,預設的壓縮深度是0既不壓縮。有時候為了節省空間,但是又不想因為壓縮而影響取出和放入的效能,可以選著壓縮深度為1或者2。
既首尾的第一個或者首尾的第一個和第二個不壓縮。
struct quicklist{ quicklistNode* head;//頭節點 quicklistNode* tail;//尾節點 long count;//元素總數 int nodes;//ziplist 節點數量 int compressDepth;//LZF 演算法壓縮深度 }; struct quicklistNode{ quicklistNode* prev;//前一個節點 quicklistNode* next;//下一個節點 ziplist* zl;//指向壓縮列表的指標 int32 size;//壓縮列表的位元組總數 int16 count;//壓縮列表中的元素個數 int2 encoding;//儲存形式 2bit 是原生位元組陣列還是被壓縮過的 };
注:LZF演算法是蘋果開源的一種無失真壓縮演算法,有興趣的看官們可以自行去了解下,這裡不做過多的贅述。
3.字典(dictht)
字典又稱之為hash,或者對映(map),也可以理解為redis自己實現的JDK1.7版本的HashMap。是一種用於儲存鍵值對的抽象資料結構。在字典中,一個鍵(Key)可以和一個值(value)進行關聯,成為一個鍵值對。
字典中每個鍵都是唯一的。程式可以在字典中根據鍵查詢與之關聯的值,或者通過鍵來跟新或者刪除值。接下來的內容將詳細介紹Redis中字典的兩種底層實現。
第一種:ziplist
當字典中的元素滿足以下兩個條件時,字典的底層將會使用ziplist來報錯鍵值對。
1.字典物件儲存的所有鍵值對的鍵和值的字串長度都小於64個位元組。
2.欄位物件儲存的鍵值對數量小於512個。
看到這裡也許有的看官會不明白了。在上面我們剛剛學過ziplist壓縮列表,大家都知道這其實就是一個數組。前面的列表可以用陣列來儲存,但是這裡是鍵值對啊,一個map,怎麼用陣列來儲存?
各位看官先莫慌,按照慣例先上圖(畢竟無圖無真相啊):
第二種:hash表
hash表顧名思義,其本質就是一個HashMap。下面我帶各位看官們看看他的結構
typedef struct dict{ dictType *type;//型別特定函式 void *privdata;//私有函式 dictht ht[2];//hash表 int trehashidx;//擴容索引 當不在擴容的時候 為-1 }; typedef struct dictht{ dictEntry **table;//雜湊表陣列 unsigned long size;//雜湊表大小 unsigned long sizemask;//雜湊表槽位取模基準引數 總是等於size - 1 unsigned long used;//已有節點數量 } typedef struct dictEntry{ void *key;//鍵 //值 這裡三個屬性是因為 值可能是一個物件引用也可能是 一個uint64_t或者int64_t整數值 union{ void *val; uint64_t u64; int64_t s64; } v; //下一個節點 多個槽位相同的值 串聯成一個連結串列 struct dictEntry *next; }
結構示意圖:
漸進式rehash :
字典在擴容的過程中會在 ht[1] 建立一個新的雜湊表,而且它並不會一次性將所有的資料都轉移到新的雜湊表之中。而是分而治之,像螞蟻搬家一樣,一部分一部分的遷移,我們稱之為漸進式rehash。
因為篇幅原因,後面會寫一篇專門的文章來詳細說明漸進式rehash和在遷移過程中獲取元素的方式,這裡就簡略的介紹一下。
4.集合(sets)
Redis集合中的Set集合,相當與java中的HashSet,它內部的鍵值對是無序的,唯一的。在Redis中Set集合底層也存在兩種實現方式。
第一種,當一個集合只包含整數值元素,並且這個集合的元素數量不超過512時,Redis就會使用整數集合作為集合鍵的底層實現。
typedef struct intset{ //編碼方式 uint32_t encoding; //集合包含的元素數量 uint32_t length; //儲存元素的陣列 int8_t contents[]; };
contents陣列是整數集合的底層實現:整數集合的每個元素都是contents陣列的一個數組項(item),各個項在陣列中按值的大小從小到大有序地排列,並且陣列中不包含任何重複項。
length屬性記錄了整數集合包含的元素數量,也即是contents陣列的長度。雖然intset結構將contents陣列宣告為int8_t型別的陣列。但實際上contents陣列的真正型別取決於encoding;
如果encoding型別為INTSET_ENC_INT16,那麼contents就是一個int16_t型別的陣列。
如果encoding型別為INTSET_ENC_INT32,那麼contents就是一個int32_t型別的陣列。
如果encoding型別為INTSET_ENC_INT64,那麼contents就是一個int64_t型別的陣列。
整數陣列的升級:
當我們要將一個新的元素新增到集合中,並且新元素的型別比整數集合現有的所有元素型別都要長時。整數集合現有先進行升級,然後才能將新元素新增到整數集合裡。
比如向一個包含1,2,3 的陣列中插入一個65535;
第二種使用字典實現,字典的每個鍵都是一個字串物件,而值則全部被設定為Null;
5.有序集合(ZSet)
ZSet在Redis底層也存在兩種實現,一種是簡單實現通過Ziplist儲存元素成員。
結構如下圖所示:
還一種是複雜模型,它內部儲存有一個跳錶和一個字典,通過字典來實現O(1)時間複雜度的元素查詢,通過跳錶來完成高效能的zrank、zrange等範圍命令。如果單純的字典,要完成zrange命令,
要先將所有資料排序,時間複雜度為O(nlogn),而且還需要長度為N的陣列來儲存排序完成的資料。如果單純使用跳錶,查詢的時間複雜度為O(logn)。
結構如下圖所示:
總結:
這五種只是最常用的五種資料結構,在Redis中還有其他型別的資料結構或者實現。比如緊湊列表listpack,基數樹rax等還等待著我們去探索。
由於篇幅有限,這期就先到這裡,預知後事如何,請聽下集分解...
參考書籍:
《Reids設計與實現》
《Redis深度歷險——核心原理與應用實踐》
&n