Redis物件與redisObject超詳細分析原始碼層
以下內容是基於Redis 6.2.6 版本整理總結
一、物件
前面幾篇文章,我們介紹了Redis用到的主要的資料結構,如:sds、list、dict、ziplist、skiplist、inset等。
但是,Redis並沒有直接使用這些資料結構來實現key-value資料庫,而是基於這些資料結構構建了一個物件系統。包括字串物件、列表物件、雜湊物件、集合物件和有序集合物件五種型別的物件。每種物件都使用了至少一種前面提到的資料結構。
通過對物件的區分,Redis可以在執行命令前判斷該物件是否能夠執行該條命令。為物件設定不同的資料結構實現,只要是為了提高效率。
二、物件的型別及編碼
Redis使用物件來表示資料中的key和value,每當我們在Redis資料庫中建立一個新的鍵值對時,至少會建立兩個物件,一個作用域key,另一個作用於value。
舉個栗子:set msg “hello world” 表示分別建立了一個字串物件儲存“msg”,另一個字串物件儲存“hello world”:
redisObject 結構體
Redis中的每個物件由 redisObject 結構體來描述,物件的型別、編碼、記憶體回收、共用物件都需要redisObject的支援,redisObject 結構體定義如下:
#define LRU_BITS 24 typedef struct redisObject { unsigned type:4; // 型別 unsigned encoding:4; // 編碼 unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or * LFU data (least significant 8 bits frequency * and most significant 16 bits access time). */ int refcount; void *ptr; } robj;
下面我們來看看每個欄位的含義:
(1)type: 佔4個位元位,表示物件的型別,有五種型別。當我們執行type命令時,便是通過type欄位獲取物件的型別。
/* The actual Redis Object */ #define OBJ_STRING 0 /* String object. */ #define OBJ_LIST 1 /* List object. */ #define OBJ_SET 2 /* Set object. */ #define OBJ_ZSET 3 /* Sorted set object. */ #define OBJ_HASH 4 /* Hash object. */
type命令使用範例:
(2)encoding: 佔4個位元位,表示物件使用哪種編碼,redis會根據不同的場景使用不同的編碼,大大提高了redis的靈活性和效率。
字串物件不同編碼型別範例:
(3)lru: 佔 24 個位元位,記錄該物件最後一次被存取的時間。千萬別以為這隻能在LRU淘汰策略中才用,LFU也是複用的個欄位。當使用LRU時,它儲存的上次讀寫的24位元unix時間戳(秒級);使用LFU時,24位元會被分為兩個部分,16位元的分鐘級時間戳和8位元特殊計數器,這裡就不展開了,詳細可以注意我後續的文章。
(4)refcount: 物件的參照計數,類似於shared_ptr 智慧指標的參照計數,當refcount為0時,釋放該物件。
(5)ptr: 指向物件具體的底層實現的資料結構。
三、不同物件編碼規則
四、redisObject結構各欄位使用範例
Redis中操作key的命令大致可以分為兩類:一種是可以操作任何型別的key,如:del type object等等,另外一種是針對特定型別的key只能使用特定的命令。如:LLEN只能用來獲取列表物件的長度。
4.1 型別檢查(type欄位)
比如對於LLEN命令,Redis伺服器在執行命令之前會先檢查輸入的key對應的的value物件是否為列表型別,即檢查該value物件的type型別是不是OBJ_LIST,如果是才會執行LLEN命令。否則就拒絕執行命令並返回操作型別錯誤。
4.2 多型命令的實現(encoding)
Redis除了會根據value物件的型別來判斷對應key能否執行執行命令外,還會根據value物件的**編碼方式(encoding欄位)**選擇正確的方式來執行命令。比如:列表物件的編碼方式有quicklist 和 ziplist兩種,Redis伺服器除了判斷對應value物件的型別為列表物件還要根據具體的編碼選擇正確的LLEN執行。
借用物件導向的術語來說,可以認為LLEN命令是多型的。只要執行LLEN命令的列表鍵,無論value物件的編碼是哪種方式,LLEN命令都可以正常執行。實際上del type 等也是多型命令。他們和LLEN的區別在於,前者是基於型別的多型,後者是基於編碼的多型。
4.3 記憶體回收和共用物件(refcount)
C語言不具備自動回收功能,Redis就通過參照計數實現了自己的記憶體回收機制。具體是由redisObject結構中的refcount欄位記錄。物件的參照計數會隨著物件的使用狀態而不斷變化。
建立一個新物件時,refcount會被初始化為1,;當物件被另一個新程式使用時 refcount加1;不被一個程式使用時減1;當refcount==0時,該物件所佔的空間會被回收。
參照計數除了被用來實現記憶體回收外,還被用來實現物件共用。比如:
上面我們建立可一個value為100的key A,並使用object refcount來檢視key A的參照計數,會看到此時的refcount為2,這是為什麼呢?此時有兩個地方參照了這個value物件,一個是持有該物件的伺服器程式,另外一個是共用該value物件的key A。如果,我們再建立一個value為100 的 key B,那麼鍵B也會指向這個value物件,使得該物件的參照計數變為3。由此,可以看到,共用value物件的鍵越多,節約的記憶體就越多。
在建立鍵B的時候,伺服器考到鍵B要建立的物件是int編碼的字串物件100,而剛好有個value為100的共用物件匹配,就直接將鍵B指向該共用物件。因為是整數的字串物件,直接比較即可,如果共用物件是字串值的物件,要從頭到尾比較每個字元,時間複雜度O(n)。
簡單來說就是,要能使用共用物件,需要先驗證該共用物件和要建立的目標物件是不是完全一致,如果共用物件儲存的值越複雜,消耗的CPU也就越多,所以Redis值對整數型別的字串物件做了共用。沒有共用儲存字串值的字串物件。
Redis在初始化伺服器是,就建立了一萬個字串物件,這些物件包含了0~9999的所有整數值。當你建立了這個範圍內的 字串物件時,伺服器就會使用這些共用物件,而不是建立新物件,以節約記憶體。
4.4 物件的空轉時長(lru)
redisObject結構中的lru欄位儲存,該物件最後一次被存取的時間。 使用object idletime 來檢視,注意這個命令不會修改物件的lru屬性。
當Redis開啟最大記憶體限制,一般為機器記憶體的一半,如果redis使用的記憶體達到這個值,且記憶體淘汰策略使用的是volatile-lru 或者 allkeys-lru,空轉時長較高的那些鍵會被優先釋放。
使用object idletime 檢視鍵的空間時間,單位:秒:
127.0.0.1:6379[1]> keys *
1) "msg"
2) "teacher"
127.0.0.1:6379[1]> object idletime msg
(integer) 71166
127.0.0.1:6379[1]>
五、物件在原始碼中的使用
5.1 字串物件
5.1.1字串物件建立
// code location: src/object.c #define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44 // 建立 strintg 物件 robj *createStringObject(const char *ptr, size_t len) { // 如果待儲存的字串的長度小於等於44,使用 embstr 編碼 if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) return createEmbeddedStringObject(ptr,len); else // 否則使用 raw 編碼 return createRawStringObject(ptr,len); } robj *createEmbeddedStringObject(const char *ptr, size_t len) { // 申請 robj + sdshdr + data + 1 的空間 robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr8)+len+1); struct sdshdr8 *sh = (void*)(o+1); o->type = OBJ_STRING; // 設定型別 o->encoding = OBJ_ENCODING_EMBSTR; // 設定編碼 o->ptr = sh+1; o->refcount = 1; if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) { o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL; } else { o->lru = LRU_CLOCK(); } sh->len = len; sh->alloc = len; sh->flags = SDS_TYPE_8; if (ptr == SDS_NOINIT) sh->buf[len] = ' '; else if (ptr) { memcpy(sh->buf,ptr,len); sh->buf[len] = ' '; } else { memset(sh->buf,0,len+1); } return o; }
從 createEmbeddedStringObject 函數可以看到,該物件是robj和sds的結合體,將sds直接放入到robj裡,這也是嵌入式編碼embstr的由來。
為什麼要限制44位元組呢?因為robj結構體佔16個位元組,sdshdr結構體佔3個位元組,最後結尾的‘