文章原創於公眾號:程式猿周先森。本平臺不定時更新,喜歡我的文章,歡迎關注我的微信公眾號。
上一篇文章有提到,Redis中使用最頻繁的有5種資料型別:String、List、Hash、Set、SortSet。上一篇文章只是單純介紹了下這5種資料型別使用到的指令以及常用場景,本篇文章會談談5種資料型別的底層資料結構以及各自常用的操作命令來分別進行解析。Redis作為目前最流行的Key-Value型記憶體資料庫,不僅資料庫操作在記憶體中進行,並且可定期的將資料持久化到磁碟中,所以效能相對普通資料庫高很多,而在Redis中,每個Value實際上都是以一個redisObject結構來表示:typedef struct redisObject{unsigned type:4;unsigned encoding:4;void *ptr;int refCount;unsigned lru:}我們可以看看這幾個引數分別的含義:
- type:物件的資料型別,一般情況就是5大資料型別。
- encode:redisObject物件底層編碼實現,主要編碼型別有簡單動態字串,連結串列,字典,跳躍表,整數集合及壓縮列表。
- *ptr:指向底層實現資料結構的指標。
- refCount:計數器,當引用計數值為0將會釋放物件。
- lru:最後一次訪問本物件的時間。
**String資料型別**複製程式碼
String 資料結構是簡單的 Key-Value 型別,是Redis中最常用的一種資料型別,Value 可以是string或者數字。String資料型別實際上可以儲存字串、整數、浮點數三種不同型別的值,Redis是如何做到自動識別字串、整數、浮點數三種不同型別的值。Redis是使用C實現的,但是並未使用C中的字串,實際上Redis自己實現了一個結構體SDS來替代String型別:struct sdshdr{//記錄buf陣列中已使用位元組的長度int len;//記錄buf陣列中剩餘空間的長度int free;//位元組陣列,用於儲存字串char buf[];};
我們可以看到free引數是用來判斷剩餘可使用空間的長度,len表示字串的長度,buf儲存字串的每一個字元以及結尾的'0'。為什麼Redis要自己實現SDS結構體呢?因為SDS結構體有幾個優點:
- 由於len儲存了當前字串的實際長度,所以獲取長度時間複雜度為O(1)。
- SDS在拼接之前會對當前字串的空間進行自動調整和擴充套件,防止當前字串資料溢位。
- 減少記憶體分配次數,SDS拼接字串發生時,如果此時的字串長度len小於1M,則SDS會分配和len大小相同的未使用空間給free,如果此時的字串長度len大於1M,則SDS會分配和1M的未使用空間給free,當字串縮短時,縮短的空間會疊加到free中,用於後續的拼接使用。
String資料型別常用命令:
- 常用命令:set、get、decr、incr、mget 等。
String資料型別適用場景:
- 分散式鎖
- 分散式session:將分散式應用session儲存到Redis中
- 商品秒殺
- 常規計數:部落格數,閱讀數
List資料型別List資料結構是用來儲存多個有序的字串,List中的每個字串成為元素,List提供了節點重排和節點順序訪問的能力,在Redis中,List可以在兩端push和pop元素,還可以獲取指定範圍的元素列表,獲取指定索引下標的元素等,List資料結構主要有zipList(壓縮連結串列)和LinkedList(雙向連結串列)兩種實現方式。首先我們可以先看看LinkedList的結構:type struct list{//表頭節點listNode *head;//表尾節點listNode *tail;//包含的節點總數unsigned long len;};
可以看到每個LinkedList中都會包含一個表頭節點head和一個表尾結點tail,在LinkedList中每個節點都會有一個prev指向前一個元素,同時還有一個next指向後一個元素,每個節點的value就是節點的值。從而實現雙向連結串列,理解起來實際上和C中的雙向連結串列有很大程度的相似性。而另一種實現方式zipList是基於連續記憶體實現,有點類似於陣列方式,但是和陣列有點不一致的是zipList的每一個entry的大小可能不一致,需要特殊方法去控制解決,但是在執行push,pop操作時會有資料的遷移,時間複雜度為O(n), 所以一般只有在元素較少時才會使用zipList,我們可以看看zipList的結構:
type struct ziplist{//整個壓縮列表的位元組數uint32_t zlbytes;//記錄壓縮列表尾節點到頭結點的位元組數,直接可以求節點的地址uint32t zltailoffset;//記錄了節點數,有多種型別,預設如下uint16_t zllength;//節點List entryX;}
zipList中每個節點都會有以下幾個引數資訊:
- previousentrylength:記錄前一個節點的位元組長度
- content:節點所儲存的內容,可以是一個位元組陣列或者整數
- encoding:記錄content屬性中所儲存的資料型別以及長度
* List資料型別適用場景
在渲染文章列表時可以使用List資料型別,一般情況下每個使用者都會有自己釋出的文章列表,如果需要展示文章列表,就可以使用List資料型別,不但可以有序而且可以按照索引範圍去查詢文章列表。
Set資料型別
Set資料型別和List資料型別有點類似,也可以用來儲存多個元素,但最大的一點區別在於Set資料型別不允許出現重複的元素,並且Set中的元素是無序的,所以沒辦法和List一樣通過索引下標獲取元素,但是Set型別支援多個Set集合取交集、並集、差集,所以合理使用Set資料型別,可以在實際專案開發中解決很多問題。Set資料型別有兩種資料結構:IntSet和HashTable。首先我們來看看IntSet的結構:
typedef struct intset {// 編碼方式uint32_t enconding;// 集合包含的元素數量uint32_t length;// 儲存元素的陣列int8_t contents[];} intset;
當Set集合中所有元素都為整型時,Redis才會使用IntSet資料結構。有一點需要格外注意的是:IntSet資料結構是有序的。因為為了減輕效能的消耗,Redis在Set集合元素都為整型時,會使用一種基於動態陣列的結構體,同時在push元素的時候控制元素的大小順序,這樣就可以使用二分查詢演算法來對元素進行push及pop操作,這樣時間複雜度僅為O(logN)。在Set集合中元素存在非整型資料時,Redis這時會自動採用HashTable資料結構來存放資料,在HashTable中,存放的只有key值而沒有value值,所以說在HashTable中,鍵值永遠為null。我們可以看下HashTable的結構:
typedef struct dict{//型別特定函式dictType *type;//雜湊表 兩個,一個用於實時儲存,一個用於rehashdictht ht[2];//rehash索引 資料遷移時使用unsigned rehashidx;}
Set資料型別使用場景:
- 記錄唯一值:比如登入ip,身份證號
- 新增標籤:可以通過標籤的交併集計算使用者喜好程度等資料。
Hash資料型別在Redis中雜湊型別是指鍵本身又是一種鍵值對結構,也就是我們所說的物件,所以Hash資料型別用來儲存物件是最合適的資料型別。Hash資料型別的編碼可以是zipList或HashTable。當雜湊物件儲存的所有鍵值對長度小於64位元組並且元素數量少於512時使用zipList,否則使用HashTable。zipList與剛才List資料型別中講到的zipList實際上基本一致,唯一區別在於Hash儲存entry數量成對增加,所以長度一定為2的整數倍。當然,使用zipList剛才已經說過push和pop時間複雜度為O(n),所以只能在資料量少的情況下才允許使用。而HashTable其實有點類似於Java中的HashTable,HashTTable主要依賴於三個結構:dict、dictht、entry。三個結構的關係可以表示為如下這幅圖:
Hash資料型別適用場景:
- 儲存物件資料。
- 結合Json描述物件集合。
SortSet資料型別
有序集合是在Set集合的基礎上,保留了Set集合中不能存在重複元素的特性,但是不同的是,SortSet集合中元素是可以排序的,SortSet排序和List排序都可以使用索引下標作為排序依據,所以說SortSet實現了資料有序且鍵值對唯一的集合,SortSet的資料結構有兩種:zipList和skipList + HashTable,zipList都不用多少了,是用於資料量較少的情況,預設排序為元素從小到大。而採用skipList + HashTable的資料結構,skipList會在保證集合有序的情況下優化範圍查詢的時間複雜性,而HashTable剛才已經提到過它可以優化push和pop元素時的時間複雜性。skipList基於有序連結串列,可以建立多層索引,實現以空間複雜度來換取時間複雜度的做法,最終實現時間複雜度為O(logN)的元素查詢過程,當需要push或者pop元素時,則使用HashTable實現時間複雜度僅為O(1).
SortSet資料型別適用場景
- 積分排行榜:根據積分排序從小到大
- 獲取某個範圍的資料:考試80-100分的資料
歡迎關注公眾號:程式設計師周先森