Redis 的基礎數據結構(三)對象

前兩篇文章介紹了 Redis 的基本數據結構動態字符串,鏈表,字典,跳躍表,壓縮鏈表,整數集合,但是使用過 Redis 的同學會發現,平時根本沒有使用過這些數據結構。 平時使用的數據結構,包括字符串,列表,哈希,集合,還有有序集合。 其實 Redis 的實現是將底層的一種或者幾種數據結構進行結合成我們使用的數據結構。

所以今天這篇文章就是要解釋 Redis 是怎麼實現符串,列表,哈希,集合,還有有序集合的。

對象

對於 Redis 來說使用了 redisObject 來對所有的對象進行了封裝:

typedef struct redisObject {
    // 對象類型  
    unsigned type:4;
    // 編碼
    unsigned encoding:4;
    // 對象最後一次被訪問的時間
    unsigned lru:REDIS_LRU_BITS; 
    /* lru time (relative to server.lruclock) */
    // 引用計數    
    int refcount;
    // 指向實際值的指針
    void *ptr;
} robj;

我們先關注兩個參數

typeencoding :

/* Object types */// 對象類型

#define REDIS_STRING 0
#define REDIS_LIST 1
#define REDIS_SET 2
#define REDIS_ZSET 3
#define REDIS_HASH 4

/* Objects encoding. Some kind of objects like Strings and Hashes can be
 * internally represented in multiple ways. The 'encoding' field of the object
 * is set to one of this fields for this object. */


// 對象編碼

#define REDIS_ENCODING_RAW 0     /* Raw representation */
#define REDIS_ENCODING_INT 1     /* Encoded as integer */
#define REDIS_ENCODING_HT 2      /* Encoded as hash table */
#define REDIS_ENCODING_ZIPMAP 3  /* Encoded as zipmap */
#define REDIS_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */
#define REDIS_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define REDIS_ENCODING_INTSET 6  /* E  dncoded as intset */
#define REDIS_ENCODING_SKIPLIST 7  /* Encoded as skiplist */
#define REDIS_ENCODING_EMBSTR 8  /* Embedded sds string encoding */

所以通過這段代碼我們可以知道 Redis 支持的數據類型如下:

type

類型

REDIS_STRING

字符串

REDIS_LIST

列表

REDIS_SET

集合

REDIS_ZSET

有序集合

REDIS_HASH

哈希表

Redis 的 Object 通過 ptr 指向具體的底層數據。Redis 的底層數據:

編碼

類型

REDIS_ENCODING_RAW

SDS 實現的動態字符串對象

REDIS_ENCODING_INT

整數實現的動態字符串對象

REDIS_ENCODING_HT

字典實現的 hash 對象

REDIS_ENCODING_ZIPMAP

壓縮map實現對對象,(3.0)版本未使用

REDIS_ENCODING_LINKEDLIST

雙向鏈表實現的對象

REDIS_ENCODING_ZIPLIST

壓縮列表實現的對象

REDIS_ENCODING_INTSET

整數集合實現的對象

REDIS_ENCODING_SKIPLIST

跳躍表實現的對象

REDIS_ENCODING_EMBSTR

使用 embstr 實現的動態字符串的對象

PS:下文會解釋 RAW 和 EMBSTR 的區別。

我就按照類型的順序看看 Redis 是怎麼利用底層的數據結構實現不同的對象類型的。

REDIS_STRING (字符串)

Redis 的字符串 String,主要由 int、raw 和 emstr 底層數據實現的。 Redis 遵循以下的原則來決定使用底層數據結構的使用。

  • 如果數據是可以用 long 表示的整數,那就直接使用將ptr 的類型設置爲long。將RedisObject 的 encoding 設置爲 REDIS_ENCODING_INT。
  • 如果是一個字符串,那就需要考察字符串的字節數。如果字節數小於 39 就是使用 emstr,encoding 就使用 REDIS_ENCODING_EMBSTR,底層依然是我們之前介紹的 SDS 。
  • 如果字符串的長度超過 39 那就使用 raw,encoding 就是 REDIS_ENCODING_RAW。

問題來了:

  1. 爲什麼是 39 個字符? 我們所String對象是由一個 RedisObject 和 sdshdr 組成的。所以我們如下公式在 在64位的系統中,一個 emstr 最大佔用 64bite。 RedisObject(16b) + sds header(8b) + emstr + “\0”(1b) <= 64 簡單的 四則運算 emstr <= 39。
  2. 一直都是 39 麼? 在 3.2 的版本的時候,作者對 sdshdr 做了修改,從 39 改成了 44。爲什麼? 之前我們說過一個 sdshdr 包含三個參數,lenfree 還有 buf,在3.2之前 len 和 free 的數據類型都是 unsigned int。 這個就是爲什麼上面的公式 sds header 是 8個字節了。新版本的 sdshdr 變成了 sdshdr8, sdshdr16 和 sdshdr32還有 sdshdr64。優化的地方就在於如果 buf 小,使用更小位數的數據類型來描述 len 和 free 減少他們佔用的內存,同時增加了一個char flags。emstr使用了最小的 sdshdr8。 這個時候 sds header 就變成了(len(1b) + free(1b) + flags(1b)) 3個字節, 比之前的實現少了5個字節。 所以新版本的 emstr 的最大字節變成了 44。 還是那句話 Redis 對內存真是 “斤斤計較”
  3. SDS 是動態的爲什麼要區分 emstr 和 raw? 區別在於生產 raw 的時候,會有兩步操作,分別產生 redisObject 和 sdshdr。而 emstr 一次成型,同時生成 redisObject 和 sdshdr 。就是爲了高效。同時注意 emstr 是不可變的。
  4. 他們之間是什麼關係? 如果不能用 long 表示的數據,double 也是使用 raw 或者 emstr 來保存的。 按照 Redis 的套路這三個底層數據在條件滿足的是是會發生裝換的。REDIS_ENCODING_INT 的數據如果不是整數了,那就會變成 raw 或者 emstr。emstr 發生了變化就會變成 raw。

REDIS_LIST 列表

Reids 的列表,底層是一個 ziplist 或者 linkedlist。

  • 當列表對象保存的字符串元素的長度都小於64字節。
  • 保存的元素數量小於512個。

兩個條件都滿足使用ziplist編碼,兩個條件任意一個不滿足時,ziplist會變爲linkedlist。

3.2 以後使用 quicklist 保存。這個數據結構之前沒有講解過。

實際上 quicklist 是 ziplist 和雙向鏈表結合的產物。我們這樣理解,每個雙向鏈表的節點上是一個ziplist。之所以這麼設計,應該是空間和時間之間的取捨或者一個折中的方案。 具體的實現我會在以後的文章裏面具體分析。

REDIS_SET (集合)

Redis 的集合底層是一個 intset 或者 一個字典(hashtable)。

這個比較容易理解:

  • 當集合都是整數且不超過512個的時候,就使用intset。
  • 剩下都是用字典。

使用字典的時候,字典的每一個 key 就是集合的一個元素,對應的 value 就是一個 null。

REDIS_ZSET (有序集合)

Redis 的有序集合使用 ziplist 或者 skiplist 實現的。

  • 元素小於 128 個
  • 每個元素長度 小於 64 字節。

同時滿足以上條件使用ziplist,否則使用skiplist。

對於 ziplist 的實現,redis 使用相鄰的兩個 entity 分別保存對象以及對象的排序因子。這樣對於插入和查詢的複雜度都是 O(n) 的。直接看圖:

元素開發工程師,排序的因子就是月薪。(好吧php是世界上最好的語言)。

對於skiplist 的實現:

typedef struct zset{

zskiplist *zsl;
dict *dict

}zset;

skiplist 的有序鏈表的實現不只是只有一個 skiplist ,還有一個字典存儲對象的key 和 排序因子的映射,這個是爲了保證按照key 查詢的時候時間負責度爲 O(1)。同時有序性依賴 skiplist 維護。大家可以看我之前的教程。所以直接看圖:

REDIS_HASH (hash表)

Redis 的 hash 表 使用 ziplist 和 字典 實現的。

  • 鍵值對的鍵和值都小於 64 個字節
  • 鍵值對的數量小於 512。

都滿足的時候使用 ziplist,否則使用字典。

ziplist 的實現類似,類似 zset 的實現。兩個entity成對出現。一個存儲key,另一個存儲 velue。

還是可以使用上面使用過的圖。這個時候 entity 不用排序。key 是職位名稱,velue 是對應的月薪。(好吧php還是世界上最好的語言)。與zset實現的區別就是查詢是 O(n) 的,插入直接往tail後面插入就行時間複雜度O(1)。

使用字典實現一個 hash表。好像沒有什麼可以多說的。

int refcount(引用計數器)

這個參數是引用計數。Redis 自己管理內存,所以就使用了最簡單的內存管理方式—引用計數。

  • 創建對象的時候計數器爲1
  • 每被一個地方引用,計數器加一
  • 每被取消引用,計數器減一
  • 計數器爲0的時候,就說明沒有地方需要這個對象了。內存就會被 Redis 回收。

unsigned lru:REDIS_LRU_BITS

這個參數記錄了對象的最後一次活躍時間。

如果 Redis 開啓了淘汰策略,且淘汰的方式是 LRU 的時候,這個參數就派上了用場。Redis 會優先回收 lru 最久的對象。

總結

至此 Redis 的數據結構就介紹完了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章