redisObject 以及對抽象的理解
給新觀衆老爺的開場
大家好,我是弟弟!
最近讀了一遍 黃健宏大佬的 <<Redis 設計與實現>>,對Redis 3.0版本有了一些認識
該書作者有一版添加了註釋的 redis 3.0源碼
👉官方redis的github傳送門。
👉黃健宏大佬添加了註釋的 redis 3.0源碼傳送門
網上說Redis代碼寫得很好,爲了加深印象和學習redis大佬的代碼寫作藝術,瞭解工作中使用的redis 命令背後的源碼邏輯,便有了寫博客記錄學習redis源碼過程的想法。
redisObject
看過前幾期的觀衆老爺,都會常常見到一個叫 robj 的結構體
比如在k/v空間中查詢k/v, key是robj類型的指針,返回的value 也是robj類型的指針👇
/*
* 從數據庫 db 中取出鍵 key 的值(對象)
*
* 如果 key 的值存在,那麼返回該值;否則,返回 NULL 。
*/
robj *lookupKey(redisDb *db, robj *key)
{
// 查找鍵空間
dictEntry *de = dictFind(db->dict, key->ptr);
// 節點存在
if (de)
{
// 取出值
robj *val = dictGetVal(de);
...
// 返回值
return val;
}
// 節點不存在
return NULL;
}
redis的k/v空間裏不是有好多種類型的數據結構嗎?
如果說key都是字符串可以用一個類型來表示,
那鏈表,集合,有序集合,hash表呢?
我們是不是應該對不同的數據結構都寫一套 存取k/v的邏輯呢?
顯然是隻寫一套是最簡單省事的哈 🐶
如果每種數據結構都來一套對應的 存取k/v邏輯,
不僅增加了開發量,而且引入了非常多的冗餘邏輯,
不僅不方便人閱讀,而且系統的混亂程度也會升高,
做新功能擴展一類的開發,代碼會越寫越屎💩
抽象出 redisObject 的好處,拿存取k/v來舉例
-
抽象層面相同邏輯的代碼一套就夠了。 比如存取k/v的代碼
針對不同數據結構來一套特定的crud代碼,這個是沒辦法避免的事情。
但是對於 k/v 在redis中的存取操作,這是可以抽象出一套代碼的,
因爲不同數據類型的對象都可以用一個指針來指向他們,
對於k/v的存取最需要關心的是 key的指針是哪個,value的指針是哪個,
k/v 的指針指向的結構類型是什麼(類型變量,只是一個數字),編碼方式是什麼。
而對於 k/v指針指向的具體結構到底長啥樣,對於存取k/v的指針這個操作來說 實際上是不關心的。
誰要讀 就 根據讀出來的 指針、類型字段和實現方式 自己解析就好了。🙃️ -
代碼邏輯精簡,清晰明瞭
便於開發者自己閱讀,也便於後來的其他開發者閱讀,賞心悅目。 -
降低系統混亂程度(墒減),系統各個部分都簡單明瞭時,系統會比較健壯,容易擴展
因爲簡單嘛,所以相對複雜來說不容易出錯,就比較健壯。是否在工作中有這樣一種體驗? 修了一個bug,結果多了好幾個bug😂
這種問題一部分是因爲系統太混亂,對於修改造成的影響,人在短時間之內沒辦法快速評估/發現所有被影響的地方。只有等上線、搞出一口大鍋之後才發現原來沒改對😂人腦有智慧,但接受/處理/輸出 信息的速度,相比於電腦來說差太遠了。
金字塔原理一書中提到 人大概同一時間接收超過7個信息時,就不太能記全所有信息了。
如果系統太過複雜,謹慎修bug😂 -
因結構簡單帶來了 可以組合/嵌套出複雜結構的能力。
這種能力要是不通過抽象獲得,無腦寫代碼將是一場噩夢。一個hash表裏,可以存放各種類型的value, 甚至可以hash表裏嵌套一個hash表。
沒錯redis的哈希數據結構就是放在全局的k/v字典裏的,這個全局k/v字典實際上就是一個hash表 -
對象引用計數,方便對象的複用與銷燬,節省空間
-
記錄對象的訪問信息,以便於內存不足時進行內存淘汰
redisObject的結構定義
redisObject一共有5個字段
- 指向實際對象的指針
- 實際對象的數據結構類型
- 實際對象在一個具體數據結構類型下的編碼類型
- 引用計數
- lru字段
結構定義如下👇
typedef struct redisObject
{
// 類型
unsigned type : 4;
// 編碼
unsigned encoding : 4;
// 對象最後一次被訪問的時間
unsigned lru : REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
// 引用計數
int refcount;
// 指向實際值的指針
void *ptr;
} robj;
redisObject 涵蓋的數據類型有哪些
在redis3.0裏redisObject一共有5中數據類型
/* Object types */
// 對象類型
#define REDIS_STRING 0
#define REDIS_LIST 1
#define REDIS_SET 2
#define REDIS_ZSET 3
#define REDIS_HASH 4
在較新一些的redis6.0裏,redisObject一共有7種類型。
每一種數據類型,在redis內部都有2種及以上的 編碼方式,也可以理解爲實現方式。
爲什麼要這麼搞呢?
主要是考慮到redis作爲一個內存數據庫,在key對應的value比較小時,使用更節省內存的方式存儲。
當key對應的value元素個數或單個元素過大時,會轉換成更爲通用的實現方式。
在之前學習各個數據結構時,會經常發現這樣的情況,再創建各個數據類型時,默認會先以節省內存的編碼方式創建,當滿足一定條件時,會轉換爲更爲通用的編碼方式。
所有的編碼類型如下👇
/* Objects encoding. Some kind of objects like Strings and Hashes can be
* internally represented in multiple ways. The 'encoding' field of the object
* is set to one of this fields for this object. */
// 對象編碼
#define REDIS_ENCODING_RAW 0 (REDIS_STRING)
#define REDIS_ENCODING_INT 1 (REDIS_STRING)
#define REDIS_ENCODING_HT 2 (REDIS_SET,REDIS_HASH)
#define REDIS_ENCODING_ZIPMAP 3 /* Encoded as zipmap */
#define REDIS_ENCODING_LINKEDLIST 4 (REDIS_LIST)
#define REDIS_ENCODING_ZIPLIST 5 (REDIS_LIST,REDIS_HASH,REDIS_ZSET)
#define REDIS_ENCODING_INTSET 6 (REDIS_SET)
#define REDIS_ENCODING_SKIPLIST 7 (REDIS_ZSET)
#define REDIS_ENCODING_EMBSTR 8 (REDIS_STRING)
數據結構類型判斷
前面說,當從redis的db中存取k/v時,使用了redisObject結構,沒有關心value的具體結構。
那麼當value從redis的db中取出來後,再做相應邏輯之前還是要判斷一下value的數據結構類型,
否則是會有問題的。
比如hget 命令對應的 hgetCommand命令,通過key從db中取出value後,判斷了value是否是hash對象
源碼邏輯如下👇
void hgetCommand(redisClient *c) {
robj *o;
if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL ||
checkType(c,o,REDIS_HASH)) return;
// 取出並返回域的值
addHashFieldToReply(c, o, c->argv[2]);
}
/*
* 檢查對象 o 的類型是否和 type 相同:
*
* - 相同返回 0
*
* - 不相同返回 1 ,並向客戶端回覆一個錯誤
*/
int checkType(redisClient *c, robj *o, int type) {
if (o->type != type) {
addReply(c,shared.wrongtypeerr);
return 1;
}
return 0;
}
/*
* 爲執行讀取操作而從數據庫中查找返回 key 的值。
*
* 如果 key 存在,那麼返回 key 的值對象。
*
* 如果 key 不存在,那麼向客戶端發送 reply 參數中的信息,並返回 NULL 。
*/
robj *lookupKeyReadOrReply(redisClient *c, robj *key, robj *reply) {
// 查找
robj *o = lookupKeyRead(c->db, key);
// 決定是否發送信息
if (!o) addReply(c,reply);
return o;
}
實際上這個 用key在redis的db中取value並判斷類型的操作,在各種數據類型上都是有的
比如 取string
void getCommand(redisClient *c) {
getGenericCommand(c);
}
int getGenericCommand(redisClient *c) {
robj *o;
// 嘗試從數據庫中取出鍵 c->argv[1] 對應的值對象
// 如果鍵不存在時,向客戶端發送回覆信息,並返回 NULL
if ((o = lookupKeyReadOrReply(c, c->argv[1], shared.nullbulk)) == NULL) {
return REDIS_OK;
}
// 值對象存在,檢查它的類型
if (o->type != REDIS_STRING) {
// 類型錯誤
addReply(c, shared.wrongtypeerr);
return REDIS_ERR;
}
...
}
取list
void lindexCommand(redisClient *c) {
robj *o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk);
if (o == NULL || checkType(c,o,REDIS_LIST)) return;
...
}
其餘類型…省略
將公有邏輯抽象出來,只寫一套代碼。不同的實現方式的具體邏輯再寫不同的代碼。
似乎還不錯🤔
對象引用計數
C語言是沒有GC的,自己申請的內存還得自己釋放。
比如B函數通過A函數得到了一個robj*類型的變量C ,B函數用完之後理所應當的要釋放掉C。
但此時問題就來了,這個C別人有沒有在用呢?如果只是B自己在用那釋放掉就釋放了。
如果有別人在用,這個C還不能馬上釋放。
redis作者搞了一個簡單的對象引用計數,來解決內存複用與銷燬的問題。
- 誰要"拷貝"這個redisObject,引用計數就+1
- 誰用完了這個redisObject,引用計數就-1
引用計數爲0時,free掉redisObject佔用的內存空間
舉個源碼例子
比如判斷hash表中的key是否與輸入的key相等
因爲字符串類型底層有多種不同的實現方式,在比較的時候需要統一的一種形式。
getDecodedObject 會返回給定 robj的 REDIS_ENCODING_RAW 編碼形式。
- 如果robj 的編碼就是 REDIS_ENCODING_RAW 那麼可以複用該內存,僅增加引用計數即可
- 否則,創建一個新的 REDIS_ENCODING_RAW編碼的robj 字符串,引用計數初始爲1
- 比對完之後對 getDecodedObject 返回的結構體 減1個引用計數,若引用計數減爲0,則釋放該結構體
非字符串類型的robj會崩潰
int dictEncObjKeyCompare(void *privdata, const void *key1,
const void *key2)
{
robj *o1 = (robj*) key1, *o2 = (robj*) key2;
int cmp;
if (o1->encoding == REDIS_ENCODING_INT &&
o2->encoding == REDIS_ENCODING_INT)
return o1->ptr == o2->ptr;
o1 = getDecodedObject(o1);
o2 = getDecodedObject(o2);
cmp = dictSdsKeyCompare(privdata,o1->ptr,o2->ptr);
decrRefCount(o1);
decrRefCount(o2);
return cmp;
}
/* Get a decoded version of an encoded object (returned as a new object).
*
* 以新對象的形式,返回一個輸入對象的解碼版本(RAW 編碼)。
*
* If the object is already raw-encoded just increment the ref count.
*
* 如果對象已經是 RAW 編碼的,那麼對輸入對象的引用計數增一,
* 然後返回輸入對象。
*/
robj *getDecodedObject(robj *o) {
robj *dec;
if (sdsEncodedObject(o)) {
incrRefCount(o);
return o;
}
// 解碼對象,將對象的值從整數轉換爲字符串
if (o->type == REDIS_STRING && o->encoding == REDIS_ENCODING_INT) {
char buf[32];
ll2string(buf,32,(long)o->ptr);
dec = createStringObject(buf,strlen(buf));
return dec;
} else {
redisPanic("Unknown encoding type");
}
}
/*
* 爲對象的引用計數增一
*/
void incrRefCount(robj *o) {
o->refcount++;
}
/*
* 爲對象的引用計數減一
*
* 當對象的引用計數降爲 0 時,釋放對象。
*/
void decrRefCount(robj *o) {
if (o->refcount <= 0) redisPanic("decrRefCount against refcount <= 0");
// 釋放對象
if (o->refcount == 1) {
switch(o->type) {
case REDIS_STRING: freeStringObject(o); break;
case REDIS_LIST: freeListObject(o); break;
case REDIS_SET: freeSetObject(o); break;
case REDIS_ZSET: freeZsetObject(o); break;
case REDIS_HASH: freeHashObject(o); break;
default: redisPanic("Unknown object type"); break;
}
zfree(o);
// 減少計數
} else {
o->refcount--;
}
}
lru字段 與 內存淘汰
redisObject中的lru字段記錄了k/v被訪問的 時間 or 頻率 相關信息。
這個lru字段 只佔了24位空間
在內存不足時作爲參考數據,根據淘汰策略進行數據淘汰,以釋放空間。
這個redis的內存淘汰策略有多種方式,可以單獨寫一篇博客了,在這裏先不細說了。🐶
小結
- 寫代碼的時候最先想到的就是具體如何實現,這個想法並沒有什麼問題。
但這是局部視角,並不完整,缺少站在系統整體層面的思考。
所寫的代碼模塊,在整個系統中是一個什麼定位,與系統中其他模塊有什麼關係。
所寫代碼模塊本身是否有更好的組織方式。
系統層面的整體思考 跟 具體功能如何實現 一樣重要。(跑題了🐶)