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時,顯然會執行不同的方法獲取列表的長度。
參考:
- Redis設計與實現. 黃健宏著