1. 程式人生 > >Redis 的底層資料結構(物件)

Redis 的底層資料結構(物件)

目前為止,我們介紹了 redis 中非常典型的五種資料結構,從 SDS 到 壓縮列表,這都是 redis 最底層、最常用的資料結構,相信你也掌握的不錯。

但 redis 實際儲存鍵值對的時候,是基於物件這個基本單位的,並且往往一個物件下面對對應不同的底層資料結構實現以便於在不同的場景下切換底層實現提升效率。例如列表物件在元素不多情況話會使用壓縮列表來實現以壓縮記憶體,而在元素比較多的時候常規的雙端連結串列進行實現。

下面我們就具體來看看 redis 中都有哪些物件,底層又對應哪些可供選擇的資料結構。

一、Redis 物件結構定義

redis 為每個物件定義為如下資料結構:

typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; 
    int refcount;
    void *ptr;
} robj;

type 記錄的是當前的物件型別,有以下幾種型別:

#define OBJ_STRING 0   /*字串物件*/

#define OBJ_LIST 1    /*列表物件*/

#define OBJ_SET 2    /*集合物件*/

#define OBJ_ZSET 3   /*有序集合物件*/

#define OBJ_HASH 4   /*雜湊物件*/

encoding 記錄的是當前物件使用的哪種底層資料結構實現的,有以下型別可供選擇:

#define OBJ_ENCODING_RAW 0     /* SDS 字串 */

#define OBJ_ENCODING_INT 1     /* 整數 */

#define OBJ_ENCODING_HT 2      /* 字典結構 */

#define OBJ_ENCODING_ZIPMAP 3  /* 壓縮map,已經廢棄 */

#define OBJ_ENCODING_LINKEDLIST 4 /* LinkedList 雙端連結串列,廢棄了 */

#define OBJ_ENCODING_ZIPLIST 5 /* 壓縮列表 */

#define OBJ_ENCODING_INTSET 6  /* 整數集合 */

#define OBJ_ENCODING_SKIPLIST 7  /* 跳躍表 */

#define OBJ_ENCODING_EMBSTR 8  /* 短字串 */

#define OBJ_ENCODING_QUICKLIST 9 /* 壓縮連結串列和雙向連結串列組成的快速列表 */

8 和 9 我們遇到時在介紹,這裡暫時不做介紹。

lru 記錄的是上一個當前物件例項被訪問的時間,它用作計算物件空轉時長,空轉時長過大的物件會被 redis 優先釋放記憶體。

refcount 記錄的是物件的引用計數,引用計數演算法是很多程式語言中管理物件是否應該被銷燬的依據,和它類似的典型的 Java 中可達性分析演算法,都是用於計數當前物件是否依然被使用,以便釋放記憶體。

ptr 指標指向的是實際實現當前物件的資料結構首地址。

以上就是 redisObject 資料結構的基本解釋,下面我們看具體的物件分別會在什麼情況下切換不同的底層實現。

二、字串物件

字串物件有三種 encoding 值,也就是隻有這三種情況,redis 才會使用字串物件儲存資料。

  1. 字串(raw):普通的字串
  2. 整數(int):long 型別的整數值
  3. 短字串(embstr ):短字串

如果判定使用 raw 編碼,那麼 redis 的 ptr 指標將會指向一個 SDS 結構,如果確定使用 int 編碼,那麼會將 redisObject 中 ptr 型別由 void* 變成 long,繼而分配 robj 記憶體。

當字串的長度小於 39 個位元組時,會採用 embstr 這種編碼,embstr 其實也是使用 SDS 進行儲存,區別於 raw 編碼的是,後者會將 robj 和 ptr 指向的 SDS 分配在連續的記憶體塊,唯一的好處是分配和釋放記憶體都只需要一次操作即可完成,再一個是因為資料相鄰,有可能一次載入 robj 的時候,CPU 將後面的 embstr 也載入進快取,等到訪問的時候就可以直接從快取中訪問。

但是,我們看一個例子:

hello 原本是以 int 編碼儲存的,但是我們執行 append 命令添加了字串之後,它變成了 raw 編碼。

這其實是 redis 的一種編碼換換,當 hello 不再適合使用 int 編碼繼續儲存的時候,會進行一個編碼轉換。

三、列表物件

列表物件有兩種編碼,壓縮列表 ziplist 和 linkedlist。我們之前說過壓縮列表的推薦應用場景,少量整數或字串的時候可以用壓縮列表來節省記憶體空間,而大資料量的節點則推薦使用普通的雙端連結串列進行實現。

但是實際上,redis 的較新版本已經使用一種叫 quicklist 的快速列表整合 ziplist 和 linkedlist 作為列表物件的實現了。它將所有的節點分段拆分,每一份又使用壓縮列表進行壓縮,不同段之間使用雙向指標連線。

四、集合物件

集合物件也有兩種編碼,整數集合 intset 和 字典 hashtable。預設情況下,當集合中有且僅有整型資料,且不超過 512 個,那麼 redis 會使用整數集合 intset 進行集合儲存,其餘情況 redis 則構建字典進行集合資料儲存。

順便給大家複習下 intset 的無重複性、順序性的特性,重複的元素是插入不進去的,因為插入之前會通過二分查詢查詢是否存在該元素,如果存在則拒絕插入操作。

當然了,如果集合中元素個數超過 512,那麼 redis 就轉而使用字典結構進行資料儲存,具體例項就不再演示了。

五、有序集合物件

有序集合物件同樣使用兩種編碼 ziplist 和 skiplist,可能你又見到壓縮列表的身影了,足以見得,壓縮列表是一個非常優秀的資料結構。

同樣,當有序集合中包含少量元素的時候,redis 會優先使用壓縮列表進行儲存,反之選擇跳躍表。

sadd 命令的標準語法是:

ZADD KEY_NAME SCORE1 VALUE1.. SCOREN VALUEN

每一個元素都會對應一個分值,skiplist 本身的實現就需要這個分值進行元素的儲存排序,有的時候有序集合會使用壓縮列表進行實現,那麼也需要這個分值來有序的壓縮元素,這也是壓縮列表頁可以實現有序集合的原因。

這裡補充一下,雖然說 redis 的有序集合是跳錶實現的,這句話不錯,但有失偏駁。

typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;

準確來說,redis 中的有序集合是由我們之前介紹過的字典加上跳錶(組合起來就是zset)實現的,字典中儲存的資料和分數 score 的對映關係,每次插入資料會從字典中查詢,如果已經存在了,就不再插入,有序集合中是不允許重複資料。

六、雜湊物件

雜湊物件的編碼可以是 ziplist 或者 hashtable,沒什麼特殊的,不再贅述。

以上,我們總結了 redis 中五大物件結構,以及他們可選的底層實現資料結構,相信你也理解的不錯,這將非常有助於我們後面的學習。

下節開始,我們向 redis 資料庫邁進~


關注公眾不迷路,一個愛分享的程式設計師。

公眾號回覆「1024」加作者微信一起探討學習!

每篇文章用到的所有案例程式碼素材都會上傳我個人 github

https://github.com/SingleYam/overview_java

歡迎來踩!