Redis 的基礎資料結構(三)物件

犀利豆發表於2018-03-17

原文地址:xilidou.com/2018/03/15/…

前兩篇文章介紹了 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。

  1. 一直都是 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 對記憶體真是 “斤斤計較”

  1. SDS 是動態的為什麼要區分 emstr 和 raw?

區別在於生產 raw 的時候,會有兩步操作,分別產生 redisObject 和 sdshdr。而 emstr 一次成型,同時生成 redisObject 和 sdshdr 。就是為了高效。同時注意 emstr 是不可變的。

  1. 他們之間是什麼關係?

如果不能用 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) 的。直接看圖:

ziplist

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

對於skiplist 的實現:


typedef struct zset{

    zskiplist *zsl;
    
    dict *dict

}zset;

複製程式碼

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

zset

REDIS_HASH (hash表)

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

  • 鍵值對的鍵和值都小於 64 個位元組
  • 鍵值對的數量小於 512。

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

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

ziplist

還是可以使用上面使用過的圖。這個時候 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 的資料結構就介紹完了。

大家可以閱讀之前的文章:

Redis 的基礎資料結構(一) 可變字串、連結串列、字典

Redis 的基礎資料結構(二) 整數集合、跳躍表、壓縮列表

歡迎關注我的微信公眾號:

二維碼

相關文章