對象
文章目錄
幾個概念澄清。Redis並不直接使用之前學習的數據結構,而是使用對象
1. 對象分類
5大對象
類型常量 | 對象的名稱 |
---|---|
REDIS_STRING | 字符串對象 |
REDIS_LIST | 列表對象 |
REDIS_HASH | 哈希對象 |
REDIS_SET | 集合對象 |
REDIS_ZSET | 有序集合對象 |
Redis的數據存儲總是分爲key-value的形式。而key和value分別是以對象存儲的。其中對於key來說,永遠是字符串對象。而值則是以上任意。
因此只要是在Redis中談到的對象類型時候,都是在談值的對象類型。因爲key的對象是固定死了的。
2. 數據結構
typedef struct redisObject{
// 類型
unsigned type:4;
// 編碼
unsigned encoding:4;
// 對象最後一次被訪問的時間
unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
// 引用計數
int refcount;
// data
void *ptr;
2.1 類型
類型即指對象類型
2.2 編碼
編碼指的是底層數據結構的實現。對於同一種對象,可能會有不同的底層數據結構實現。
目的是爲了增加靈活性和效率。
3. 接下來的目標
到目前爲止,我們已經完成了Redis底層數據結構的閱讀。接下來我們就需要進行編碼部分的閱讀。看看Redis是如何利用這些底層的數據結構來具體實現這5種對象的。
5. 各種對象
5.1 創建一個新 robj 對象
robj *createObject(int type, void *ptr) {
robj *o = zmalloc(sizeof(*o));
o->type = type;
o->encoding = REDIS_ENCODING_RAW;
o->ptr = ptr;
o->refcount = 1;
/* Set the LRU to the current lruclock (minutes resolution). */
o->lru = LRU_CLOCK();
return o;
}
我們可以看到,在創建一個robj對象時
- 設置了引用計數
- 設置了時鐘,後期在淘汰key對象的時候會被用到。
接下來在各種對象的創建時,可能會用到這裏。
5.2 字符串對象
5.2.1 編碼
字符串對象的編碼可以是
REDIS_ENCODING_INT
REDIS_ENCODING_EMBSTR
REDIS_ENCODING_RAW
分別對應如下命令
redis> SET number 10086
redis> SET sds "string"
redis> SET story "long,long,long ago, there lived a king ... this is a very long string larger than 39 bytes"
對應規則如下。
- 當字符串對象的值可以用long類型來表示。那麼
void*
就可以用來被long存儲。此時編碼設置爲INT - 當字符串長度大於39字節的時候,就會被設置爲RAW。
根據《Redis的設計與實現》談到,embstr和raw的區別時。
相同點
- embstr和raw的數據結構時一模一樣的。
不同點
- embstr是一次分配內存,因此刪除也是一次。而raw需要2次。
- 因爲使一次分配內存,所以embstr的內存分佈是連續的。
5.2.2 embstr的創建
let’s show code
robj *createEmbeddedStringObject(char *ptr, size_t len) {
robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr)+len+1);
// 1次分配內存, len+1爲sds.buff長度和結束符的存放
struct sdshdr *sh = (void*)(o+1);
// o是robj類型,因此o+1,移動sizeof(robj)個字節
o->type = REDIS_STRING;
o->encoding = REDIS_ENCODING_EMBSTR;
o->ptr = sh+1;
// sh移動sizeof(sds)字節,指向buff,這樣纔可以使用SDS API
o->refcount = 1;
o->lru = LRU_CLOCK();
sh->len = len;
sh->free = 0;
if (ptr) {
memcpy(sh->buf,ptr,len);
sh->buf[len] = '\0';
} else {
memset(sh->buf,0,len+1);
}
return o;
}
這裏需要對指針的+1理解充分,且需要理解底層數據結構。
簡單例子
int *a;
struct B *b;
char *c;
//a+1;
//b+1;
//c+1;
int aMove = reinterpret_cast<char*>(a+1) - reinterpret_cast<char*>(a);
int bMove = reinterpret_cast<char*>(b+1) - reinterpret_cast<char*>(b);
int cMove = reinterpret_cast<char*>(c+1) - reinterpret_cast<char*>(c);
這裏a會移動4個字節,b移動sizeof(b)個字節,c移動1個字節。
5.3 列表對象
5.3.1 編碼
列表對象的編碼使用ziplist或者linkedlist
比如使用
redis>RPUSH numbers 1 "three" 5
ziplist的底層實現就是簡單地將這些轉爲ziplistNode然後存放。
而對於linkedlist來說,則全部存放爲字符串對象
,這實際上是一種對象中嵌套對象的做法。
5.4 哈希對象
5.4.1 編碼
哈希對象的編碼可以是ziplist或者hashtable
對於ziplist來說,實際上就是簡單的將鍵值對放在一起,然後ziplist實際上像是一個連續空間的鏈表。顯然,只有在元素很少的時候,才能使用。
5.5 集合對象
5.5.1 編碼
對於無序的集合對象來說,使用的是intset和hashtable。
我們重點來看有序的集合對象,其編碼使用的是ziplist和skiplist。
5.5.2 zset
爲什麼有序集合zset中既使用跳錶,又使用哈希表來做數據結構?
zset雖然是有序集合,實際上也可以看做是value = score的鍵值對。
- 想要哈希表的O(1)根據成員名獲得分值的特性。
- 同時哈希表的底層是無序的,不能通過使用ZRANK來獲得指定範圍內的元素。如果只有哈希表,就需要通過排序來得到其有序結構O(nlogN),而且隨着新插入元素,還需要重新排序。所以不如在底層就維護好一個有序結構。