通過學習Redis服務端設計這篇文章,相信大家對redis的整體架構有了最基本的認識,下面來學習Redis的基本結構,大家知道redis效能很高的原因其中之一便是資料結構簡單。redis的資料結構支援多種型別,在記憶體使用方面處理的可謂是“錙銖必較”。那麼redis是如何設計的呢?
現在通過服務接收客戶端請求及處理流程為例進行分析redis的基礎結構 以“set lemon coder”為例:
通過上圖,可以看到服務在處理客戶端請求的時候 主要為兩個步驟 1. 處理輸入流(解析請求) 2. 處理命令(將請求儲存db,以寫命令為例)
下面通過原始碼來分析 服務端實現的整體流程,本文原始碼基於Redis版本5.0.5
第一個需要了解的是客戶端物件結構,每一個客戶端連線,服務端都會建立一個client來與其對應。
src/server.h
/* 服務端建立的客戶端物件,只保留了本文需要的欄位 */
typedef struct client {
sds querybuf; /* 輸入緩衝區 */
int argc; /* 引數的個數 */
robj **argv; /* 具體引數 */
struct redisCommand *cmd, *lastcmd; /* 命令處理方法 */
} client;
複製程式碼
下面根據這幾個欄位的處理來分析redis服務如何處理輸入流的
解析客戶端請求
實現方法為processInlineBuffer()
原始碼1. 將輸入流querybuf轉為argv的過程
querybuf為sds型別
argv為robj型別
src/networking.c
processInlineBuffer(){
/* 使用\r\n分割引數 */
querylen = newline-(c->querybuf+c->qb_pos);
aux = sdsnewlen(c->querybuf+c->qb_pos,querylen);
// 將入參進行拆分為引數
argv = sdssplitargs(aux,&argc);
/* 為所有的引數建立redisObject物件 */
for (c->argc = 0, j = 0; j < argc; j++) {
if (sdslen(argv[j])) {
// 將引數進行轉換為robj
c->argv[c->argc] = createObject(OBJ_STRING,argv[j]);
c->argc++;
} else {
sdsfree(argv[j]);
}
}
}
複製程式碼
原始碼2. createObject的處理
src/object.c
robj *createObject(int type, void *ptr) {
robj *o = zmalloc(sizeof(*o));
o->type = type;
o->encoding = OBJ_ENCODING_RAW;
o->ptr = ptr;
o->refcount = 1;
/* 設定lru資訊*/
if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL;
} else {
o->lru = LRU_CLOCK();
}
return o;
}
複製程式碼
處理命令 processCommand()
查詢命令對應的處理函式,比如set會尋找到setCommand
對key進行robj+sds型別轉換處理
key儲存到全域性字典dict中
value放入dict中
原始碼1. 查詢命令,執行命令
src/server.c
int processCommand(client *c) {
/* 當前檔案下的 setCommand */
c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);
/* 執行命令 */
call(c,CMD_CALL_FULL);
return C_OK;
}
複製程式碼
原始碼2. 執行命令setCommand
setCommand為set指令的處理方法,如果指令為hset,那麼對應的方法就是hsetCommand
src/t_string.c
/* SET key value [NX] [XX] [EX <seconds>] [PX <milliseconds>] */
void setCommand(client *c) {
/* 判斷過期時間設定等邏輯*/
.....
/* 嘗試型別encoding */
c->argv[2] = tryObjectEncoding(c->argv[2]);
/* 執行基本命令 */
setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}
複製程式碼
原始碼3. 基本命令的處理
設定key value到字典中
如果有過期屬性,設定過期資訊
void setGenericCommand(client *c, int flags, robj *key, robj *val, robj *expire, int unit, robj *ok_reply, robj *abort_reply) {
/* 設定key val 到dict中*/
setKey(c->db,key,val);
/* 如果有過期屬性,設定過期資訊 */
if (expire) setExpire(c,c->db,key,mstime()+milliseconds);
addReply(c, ok_reply ? ok_reply : shared.ok);
}
複製程式碼
原始碼4. 儲存字典
此處為原始碼3中setKey的實現,流程為 1. 插入kv(如果key已經存在覆蓋設值),2. 移除過期資訊,其中插入字典的方法為dbAdd,其中包含sdsup方法即選擇合適的結構。
src/db.c
void setKey(redisDb *db, robj *key, robj *val) {
if (lookupKeyWrite(db,key) == NULL) {
dbAdd(db,key,val);
} else {
dbOverwrite(db,key,val);
}
incrRefCount(val);
removeExpire(db,key);
signalModifiedKey(db,key);
}
src/db.c
void dbAdd(redisDb *db, robj *key, robj *val) {
sds copy = sdsdup(key->ptr);
int retval = dictAdd(db->dict, copy, val);
}
複製程式碼
原始碼5. 型別處理
- 將key轉化為合適的結構
- 將kv插入字典中,此時value為空
- 將value轉換為合適的結構
- 將value插入字典中
src/db.c
void dbAdd(redisDb *db, robj *key, robj *val) {
sds copy = sdsdup(key->ptr);
int retval = dictAdd(db->dict, copy, val);
serverAssertWithInfo(NULL,key,retval == DICT_OK);
if (val->type == OBJ_LIST ||
val->type == OBJ_ZSET)
signalKeyAsReady(db, key);
if (server.cluster_enabled) slotToKeyAdd(key);
}
src/sds.c
/* 對sds型別進行轉換為合適的結構 */
sds sdsdup(const sds s) {
return sdsnewlen(s, sdslen(s));
}
src/dict.c
/* 新增一個元素到字典中 */
int dictAdd(dict *d, void *key, void *val)
{
dictEntry *entry = dictAddRaw(d,key,NULL);
if (!entry) return DICT_ERR;
dictSetVal(d, entry, val);
return DICT_OK;
}
src/dict.h
/* 對val轉換為合適的結構 */
define dictSetVal(d, entry, _val_)
do {
if ((d)->type->valDup)
(entry)->v.val = (d)->type->valDup((d)->privdata, _val_);
else
(entry)->v.val = (_val_);
} while(0)
複製程式碼
以上就是服務處理請求的流程,主要包含:處理輸入流(解析引數)、儲存(確認型別、插入全域性字典)。
注意點:
通過上面的對過期時間的設定,會對過期時間進行removeExpire(db,key)操作,操作完成之後會檢測是否配置了過期時間,如果沒有設定則不過期。看個示例:
127.0.0.1:6379> set lemon coder ex 100
OK
127.0.0.1:6379> pttl lemon
(integer) 95426
127.0.0.1:6379> set lemon coder
OK
127.0.0.1:6379> pttl lemon
(integer) -1
複製程式碼
結構編碼關係
通過上面的針對字串的setCommand命令的分析,可以看到查詢命令過程,不同的指令會對應不同的處理流程,分別建立不同的型別,並根據值的大小及個數選擇合適的資料結構。下面來看下redis有哪些型別和資料結構,以及他們之間的關係。
型別
src/server.h:492
#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. */
複製程式碼
資料結構(內部編碼)
/* 內部編碼*/
#define OBJ_ENCODING_RAW 0 /* Raw 字串 */
#define OBJ_ENCODING_INT 1 /* int 字串(全部為數字的情況) */
#define OBJ_ENCODING_HT 2 /* 雜湊表 */
#define OBJ_ENCODING_ZIPMAP 3 /* zipmap */
#define OBJ_ENCODING_LINKEDLIST 4 /* linkedlist */
#define OBJ_ENCODING_ZIPLIST 5 /* 壓縮列表ziplist */
#define OBJ_ENCODING_INTSET 6 /* intset */
#define OBJ_ENCODING_SKIPLIST 7 /* 跳躍列表skiplist */
#define OBJ_ENCODING_EMBSTR 8 /* 字串 */
#define OBJ_ENCODING_QUICKLIST 9 /* 快速列表 quickList(linkedList+ziplist) */
#define OBJ_ENCODING_STREAM 10 /* stream */
複製程式碼
對應關係
上圖為redis的結構及內部編碼,後續作者的文章將詳細介紹每一種結構,以及每一種結構如何進行編碼的轉換和擴容及縮容。
以上就是本篇文章的全部內容,感謝您的閱讀,如果您在其中發現任何不正確或者不嚴謹的描述,歡迎指正。
原文地址:Redis資料結構概覽(原始碼分析)
歡迎關注作者公眾號: