redis對象

redis對象

前面我們學習了redis各種數據結構,包括簡單動態字符串、鏈表、字典、哈希表、整數集合、壓縮列表,其實redis實際不是直接使用這些數據結構的,而是使用稱爲redis對象的數據結構:redisObject。

1. redis對象定義

redis對象的定義如下:

typedef struct redisObject {
    //對象類型
    unsigned type:4;
    //對象編碼
    unsigned encoding:4;
    //最後一次被訪問的時間
    unsigned lru:LRU_BITS; /* lru time (relative to server.lruclock) */
    //對象的引用計數
    int refcount;
    //底層數據結構
    void *ptr;
} robj;

1.1 對象類型-type

其中type表示的是具體數據類型,類型包括如下幾種:

類型常量 對象的名稱
REDIS_STRING 字符串對象
REDIS_LIST 列表對象
REDIS_HASH 哈希對象
REDIS_SET 集合對象
REDIS_ZSET 有序集合對象

redis中的鍵總是一個字符串對象,值可以是上述幾種對象的一種。

當我們對redis鍵使用type命令時,返回的結果是值的類型,看下例:

127.0.0.1:6379> set msg "hello world"
OK
127.0.0.1:6379> type msg
string
127.0.0.1:6379> hset jack age 23
(integer) 1
127.0.0.1:6379> type jack
hash

鍵msg對應的值對象是字符串對象、鍵jack對應的值對象是哈希對象,下表展示了不同對象類型type命令的輸出:

對象 對象type屬性的值 type命令的輸出
字符串對象 REDIS_STRING “string”
列表對象 REDIS_LIST “list”
哈希對象 REDIS_HASH “hash”
集合對象 REDIS_SET ”set”
有序集合對象 REDIS_ZSET “zset”

1.2 對象編碼-encoding

encoding記錄了對象的編碼,也就是這個對象使用了什麼樣的底層數據結構實現的,下表展示了encoding的可能取值:

編碼常量 編碼所對應的底層數據結構
REDIS_ENCODING_INT long類型的整數
REDIS_ENCODING_EMBSTR embstr編碼的簡單動態字符串
REDIS_ENCODING_RAW 簡單動態字符串
REDIS_ENCODING_HT 字典
REDIS_ENCODING_LINKEDLIST 雙端鏈表
REDIS_ENCODING_ZIPLIST 壓縮列表
REDIS_ENCODING_INTSET 整數集合
REDIS_ENCODING_SKIPLIST 跳錶和字典

下表展示了每種類型的對象使用的所有可能編碼:

類型 編碼 對象
REDIS_STRING REDIS_ENCODING_INT 使用整數實現的字符串對象
REDIS_STRING REDIS_ENCODING_EMBSTR 使用embstr編碼的簡單動態字符串實現的字符串對象
REDIS_STRING REDIS_ENCODING_RAW 使用簡單動態字符串實現的字符串對象
REDIS_LIST REDIS_ENCODING_ZIPLIST 使用壓縮列表實現的列表對象
REDIS_LIST REDIS_ENCODING_LINKEDLIST 使用雙端鏈表實現的列表對象
REDIS_HASH REDIS_ENCODING_ZIPLIST 使用壓縮列表實現的哈希對象
REDIS_HASH REDIS_ENCODING_HT 使用字典實現的哈希對象
REDIS_SET REDIS_ENCODING_INTSET 使用整數集合實現的集合對象
REDIS_SET REDIS_ENCODING_HT 使用字典實現的集合對象
REDIS_ZSET REDIS_ENCODING_ZIPLIST 使用壓縮列表實現的有序集合對象
REDIS_ZSET REDIS_ENCODING_SKIPLIST 使用跳錶和字典實現的有序集合對象

使用object encoding命令可以查看一個鍵的值對象的編碼,例如:

127.0.0.1:6379> set msg "hello redis"
OK
127.0.0.1:6379> type msg
string
127.0.0.1:6379> object encoding msg
"embstr"
127.0.0.1:6379> lpush languages chinese japanese english
(integer) 3
127.0.0.1:6379> type languages
list
127.0.0.1:6379> object encoding languages
"ziplist"

其中列表languages的編碼是ziplist。

redis每種類型的對象都關聯2到3種編碼方式,不同的場景使用不同的編碼方式,提高了內存使用率。後面會詳細介紹每種類型的對象底層不同的編碼方式,以及同一種類型的對象不同編碼方式的轉換和轉換條件。

1.3 對象引用計數-refcount

redis通過對象的引用計數字段(refcount)來實現內存的回收。隨着對象的使用,該字段也會動態變化

  • 創建一個新的對象時,refcount會被初始化爲1。
  • 當對象被新的程序引用時,refcount計數遞增1。
  • 當對象不再被一個程序使用時,refcount計數遞減1。
  • 當refcount等於0的時候,對象所佔用的內存會被釋放。

除了實現內存回收外,還可以通過引用計數實現對象的共享,例如鍵A已經創建了包含整數值100的字符串對象,此時若鍵B也需要整數值100的字符串對象,那麼完全可以將鍵B的值指針指向鍵A創建的整數值100的字符串對象,並將該對象的引用計數加1 。這種方式極大的提高了內存使用率。

在redis初始化時,會創建10000個從0到9999的字符串對象,這些對象就成爲了共享對象。看下例:

127.0.0.1:6379> set k1 123
OK
127.0.0.1:6379> object refcount k1
(integer) 2
127.0.0.1:6379> set k2 123
OK
127.0.0.1:6379> object refcount k2
(integer) 3

首先設置k1的值爲123,通過object refcount命令看到它的引用計數變爲2了。這是因爲當redis初始化創建該對象時就將該對象的refcount設置爲1,這裏又將鍵k1設置爲123,該對象的refcount增加1,變爲2。

接着又將k2設置爲123,該對象的引用計數加1,變爲3。

另外,redis只共享編碼爲整數的字符串對象。因爲只有在共享對象和想要創建的對象相同時,才能使用共享對象。判斷兩個對象是否相等,對於編碼爲整數的字符串對象來說,時間複雜度爲O(1);對於編碼爲字符串的字符串對象來說,時間複雜度爲O(N);對於包含多個值的對象來說,時間複雜度爲O(N^2^),所以爲了提高效率,redis只共享編碼爲整數的字符串對象。

1.4 對象的空轉時長-lru

redis對象的lru屬性記錄了該對象最後被程序訪問的時間,該屬性可以用來計算鍵的空轉時長,空轉時長就是通過將當前的時間減去對象的lru時間計算得到的。例如,通過object idle命令可以查看鍵的空轉時長是多少。

127.0.0.1:6379> object idletime k1
(integer) 700902

注意:命令object idletime比較特殊,它不會去修改對象的lru值。

另外,在內存回收的時候,空轉時間較長的鍵可能被優先回收。

1.5 對象的底層實現-ptr

redis對象的ptr字段指向了底層實現,這個底層實現是由encoding值決定的。

2. 字符串對象

字符串對象的類型(type)是string,編碼(encoding)是int、raw、embstr的一種。

2.1 int編碼的字符串對象

當字符串的值可以用long型整數來表示的時候,redis會將該字符串用int編碼。redis會將整數值保存在對象結構的ptr屬性裏面。此時ptr的類型由原來的void*變爲long*。例如,將k1設置爲123時,object encoding返回int。

127.0.0.1:6379> set k1 123
OK
127.0.0.1:6379> object encoding k1
"int"

該字符串對象如圖1示:

這裏寫圖片描述

圖1

2.2 raw編碼的字符串對象

當字符串對象保存的是一個字符串值,並且這個字符串值長度大於39字節,redis用簡單動態字符串來保存這個值,並將對象的編碼設置爲raw。看下例子:

這裏寫圖片描述

圖2

2.3 embstr編碼的字符串對象

當字符串對象保存的是一個字符串值,並且這個字符串值長度小於等於39字節,redis用embstr編碼的方式來保存這個字符串值。

embstr編碼是專門用來保存短字符串的一種優化編碼方式。embstr編碼和raw編碼一樣,都使用redisObject和sdshdr兩種數據結構來表示字符串對象。但是raw編碼會調用兩次內存分配函數分別爲redisObject和sdshdr分配內存空間,而embstr編碼只會調用一次內存分配函數爲redisObject和sdshdr分配一塊連續的內存空間。如下圖示,值爲”hello”的字符串對象:

這裏寫圖片描述

圖3

這種方式的好處有:

  • 分配內存次數降爲1次
  • 釋放內存次數降爲1次
  • 內存具有連續性,更好利用緩存

最後,double類型也可以通過字符串對象來保存,看下例子:

127.0.0.1:6379> set k3 1.234
OK
127.0.0.1:6379> object encoding k3
"embstr"

當在需要的時候,redis會在double和字符串之間正確轉換。

2.4 字符串對象各編碼之間的轉換

int編碼的和embstr編碼的字符串對象在滿足一定的條件下會轉換爲raw編碼來存儲。

  • 向一個int編碼的字符串對象追加字符時,會直接轉換成raw編碼
  • embstr編碼的字符串對象執行任何修改時,會直接轉換成raw編碼,可以認爲embstr編碼的字符串對象是隻讀的

3 列表對象

列表對象的編碼可以是ziplist和linkedlist。

3.1 ziplist編碼的列表對象

下圖是一個ziplist編碼的列表對象,其中節點元素是字節數組”hello”、整數值23、整數值35。

這裏寫圖片描述

圖4

3.2 linkedlist編碼的列表對象

linkedlist編碼的列表對象底層使用雙端鏈表實現,每個雙端鏈表節點都是一個字符串對象,下圖是一個linkedlist編碼的列表對象示意圖,其中鏈表包括兩個字符串對象(這裏的字符串對象簡化了)。

這裏寫圖片描述

圖5

3.3 列表對象各編碼之間的轉換

當列表對象同時滿足如下兩個條件時使用ziplist編碼,否則使用linkedlist編碼

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

當然,這兩個值是可以在配置文件中修改的。

4. 哈希對象

哈希對象的編碼可以是ziplist和hashtable

4.1 ziplist編碼的哈希對象

ziplist編碼的哈希對象,當有新的鍵值對需要插入到哈希對象時,首先會將保存鍵的壓縮列表節點保存到壓縮列表的表尾,再將保存值的壓縮列表節點保存到壓縮列表的表尾。因此同一個鍵值對總是會緊挨在一起,前一個是鍵,後一個是值。

下圖所示壓縮列表編碼的哈希對象:

這裏寫圖片描述

圖6

該哈希對象包括了兩個鍵值對age:23和name:jack,其中age:23是先添加的,name:jack是後添加的。

4.2 hashtable編碼的哈希對象

hashtable編碼的哈希對象使用字典作爲底層實現。

4.3 哈希對象各編碼之間的轉換

當哈希對象同時滿足以下兩個條件時,使用ziplist編碼,否則使用hashtable編碼

  • 哈希對象保存的鍵值對的鍵和值長度都小於64字節
  • 哈希對象保存的鍵值對數量小於512個

同樣地,這兩個數值可以通過配置文件進行配置。

5. 集合對象

集合的編碼是intset和hashtable。intset編碼的集合使用整數集合作爲底層實現。

5.1 intset編碼的集合對象

下面例子是一個包含三個整數的集合對象,使用了整數集合編碼。

這裏寫圖片描述

圖7

5.2 hashtable編碼的集合對象

hashtable編碼的集合對象底層是用字典實現的,其中鍵就是集合的元素(字符串對象),值都是NULL。

5.3 集合對象各編碼之間的轉換

當集合對象同時滿足以下兩個條件時,對象使用intset編碼,否則使用hashtable編碼

  • 集合對象保存的值都是整數
  • 集合對象保存元素數量不超過512個

其中512這個值是可以通過配製文件修改的。

6. 有序集合對象

有序集合對象的編碼可以是ziplist和skiplist。

6.1 ziplist編碼的有序集合對象

ziplist編碼的有序集合對象,底層使用壓縮列表實現,每個集合元素由兩部分組成,第一個保存元素的成員,第二個保存元素的分值。並且壓縮列表集合內的元素按照分值從小到大排列。

例如,下圖是一個壓縮列表編碼的有序集合對象的例子,其中元素都是按照分值從小到大排序的。

這裏寫圖片描述

圖8

6.2 skiplist實現的有序集合對象

skiplist實現的有序集合對象底層是通過跳錶和字典實現的。底層數據結構定義如下:

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

其中跳錶zsl按分值大小保存了有序集合的所有元素。

字典dict存儲了成員到分值的映射。

有序集合對象中的每個成員都是一個字符串對象,每個分值都是一個double類型的浮點數。zsl和dict會共用字符串和分值對象,不會產生額外的內存。

6.3 有序集合對象各編碼之間換轉換

當有序集合對象同時滿足以下兩個條件時,使用ziplist編碼,否則使用skiplist進行編碼

  • 有序集合保存的所有元素成員的長度都小於64字節
  • 有序集合保存的元素數量小於128個

同樣地,這兩個值也是可以通過配製文件配製的。

7. 類型檢查和命令多態

redis有很多命令,有些命令是通用的,可以對任何鍵執行。而有些命令只能對特定的鍵執行。

7.1 類型檢查

在執行一個命令前,會首先根據鍵去查找值對象,然後確定值對象redisObject的type類型是否是執行命令所需要的類型,如果是的話就執行命令,否則返回錯誤。

7.2 命令多態

除了進行類型檢查,redis還可以根據redisObject的編碼方式決定如何執行命令。

例如對於ziplist和linkedlist編碼的列表對象,當執行命令llen時,顯然會執行不同的方法獲取列表的長度。

參考:

  1. Redis設計與實現. 黃健宏著
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章