文章目錄
1、介紹
前面的內容大部分都是在介紹redis中的基本數據結構,從這裏開始主要介紹Redis服務端的請求接收,請求響應,接收到的命令解析,指向命令,返回命令響應等等。Redis是服務器是典型的事件驅動服務器,因此事件處理顯得格外重要,而Redis將事件分爲兩大類:
1、
文件事件
(socket的讀寫)
2、時間事件
(週期性定時任務)
2、服務端對象redisObject
爲了更好的理解服務器與客戶端的交互,key只能是字符串
,value可以是字符串、列表、集合、有序集合和散列表
,這5種數據類型用通用結構體 robj
表示,我們稱之爲Redis對象。結構體robj
的type
字段表示對象類型,5種對象在server.h
文件中定義:
/* 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. */
針對某一種類型的對象,Redis在不同情況下可能採用不同的數據結構存儲,結構體robj
的encoding
字段表示當前對象底層存儲採用的數據結構,即對象的編碼,總共定義瞭如下11種encoding
常量:
encoding常量 | 數據結構 | 可存儲對象類型 |
---|---|---|
OBJ_ENCODING_RAW | 簡單動態字符(sds) | 字符串 |
OBJ_ENCODING_INT | 整數 | 字符串 |
OBJ_ENCODING_HT | 字典 | 集合,散列表,有序集合 |
OBJ_ENCODING_ZIPMAP | 未使用 | 未使用 |
OBJ_ENCODING_LINKEDLIST | 廢棄(使用quicklist替代) | 廢棄(使用quicklist替代) |
OBJ_ENCODING_ZIPLIST | 壓縮列表 | 散列表、有序集合 |
OBJ_ENCODING_INTSET | 整數集合 | 集合 |
OBJ_ENCODING_SKIPLIST | 跳躍表 | 有序集合 |
OBJ_ENCODING_EMBSTR | 簡單動態字符串(sds) | 字符串 |
OBJ_ENCODING_QUICKLIST | 快速鏈表(quicklist) | 列表 |
OBJ_ENCODING_STREAM | stream | stream |
對象的整個生命週期中,編碼不是一成不變的,比如集合對象。當集合中所有元素都可以用整數表示時,底層數據結構採用整數集合;當執行sadd命令向集合中添加元素時,Redis總會校驗待添加元素是否可以解析爲整數,如果解析失敗,則會將集合存儲結構轉換爲字典。代碼如下:
else if (subject->encoding == OBJ_ENCODING_INTSET) {
if (isSdsRepresentableAsLongLong(value,&llval) == C_OK) {
uint8_t success = 0;
subject->ptr = intsetAdd(subject->ptr,llval,&success);
if (success) {
if (intsetLen(subject->ptr) > server.set_max_intset_entries)
//編碼轉換
setTypeConvert(subject,OBJ_ENCODING_HT);
return 1;
}
} else {
//編碼轉換
setTypeConvert(subject,OBJ_ENCODING_HT);
serverAssert(dictAdd(subject->ptr,sdsdup(value),NULL) == DICT_OK);
return 1;
}
對象在不同的情況下可能採用不同的數據結構存儲,比如字典與跳躍表在不同的命令下各有優勢 (例如:跳躍表擅長 zrange 與zrank ,字典的查詢時間複雜度爲O(1) ) ,Redis會同時採用字典與跳躍表存儲有序集合。例如有序集合定義如下:
typedef struct zset {
dict *dict; //字典
zskiplist *zsl; //跳躍表
} zset;
2.1、redisObject 結構
robj定義代碼如下:
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). */
//lru 緩存淘汰使用 (16位最後訪問時間-時間戳 + 8位-調用次數)
int refcount; //當前被引用的計數
void *ptr; //指向數據結構的指針
} robj;
⭐ptr
:是void類型的指針,指向實際存儲的某一種數據結構,但是當robj存儲的數據可以使用long
類型表示的時候,數據直接存儲在ptr
字段。可以看出,爲了創建一個字符串或者對象,必須分配兩次內存空間,robj與sbs存儲空間;兩次內存分配效率低下,且數據分離存儲降低了計算機的高數緩存的效率。因此提出OBJ_ENCODING_EMBSTR
編碼的字符,當字符串內容較短的時候,只分配一次內存,robj與 sbs連續存儲,以此提升內存分配效率與數據訪問效率。OBJ_ENCODING_EMBSTR
編碼字符串內存結構如下如:
⭐refcount
存儲當前對象的引用次數,用於實現對象共享。當共享對象的時候refcount
加 1;刪除對象時候refcount
減 1,當refconut
爲0 的時候,釋放其對象內存空間。刪除對象的代碼如下:
//減少refcount
void decrRefCount(robj *o) {
//判斷是否釋放共享對象
if (o->refcount == 1) {
switch(o->type) {
case OBJ_STRING: freeStringObject(o); break; //釋放字符
case OBJ_LIST: freeListObject(o); break; //釋放鏈表
case OBJ_SET: freeSetObject(o); break;//釋放集合
case OBJ_ZSET: freeZsetObject(o); break; //釋放有序集合
case OBJ_HASH: freeHashObject(o); break; //釋放hash表
case OBJ_MODULE: freeModuleObject(o); break;
case OBJ_STREAM: freeStreamObject(o); break;
default: serverPanic("Unknown object type"); break;
}
zfree(o); //回收空間
} else {
if (o->refcount <= 0) serverPanic("decrRefCount against refcount <= 0");
if (o->refcount != OBJ_SHARED_REFCOUNT) o->refcount--; //計數器自減
}
}
⭐lru
字段佔24bit,用於實現緩存淘汰策略,可以在配置文件中使用maxmemory-policy
配置已用內存達到最大內存限制時的緩存淘汰策略。lru
根據用戶配置的緩存淘汰策略存儲不同數據,常用的策略就是LRU 和 LFU 。如果當前緩存策略是 LRU 那麼lru
上面存儲的就是上次對象被訪問的時間
,如果策略是 LFU,那麼lru
上面存儲的是上次訪問時間
與訪問次數
。使用如get
命令訪問數據時,會執行如下代碼來更新對象的lru
,代碼如下:
robj *lookupKey(redisDb *db, robj *key, int flags) {
dictEntry *de = dictFind(db->dict,key->ptr);
if (de) {
robj *val = dictGetVal(de);
//hasActiveChildProcess 保證當前沒有子進程在訪問中
if (!hasActiveChildProcess() && !(flags & LOOKUP_NOTOUCH)){
//判斷當前策略是LRU還是LFU
if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
updateLFU(val); //更新時間和訪問次數
} else {
val->lru = LRU_CLOCK(); //更新時間
}
}
return val;
} else {
return NULL;
}
}
LRU_CLOCK
函數用來獲取當前時間,這裏的時間並不是實時獲取的,Redis會在一秒的週期內去獲取系統的精確時間,緩存在全局變量server.lruclock
中,LRU_CLOCK
獲取的只是該緩存時間。
updateLFU
函數用於更新對象的上次訪問時間和訪問次數,函數實現如下:
void updateLFU(robj *val) {
unsigned long counter = LFUDecrAndReturn(val); //具有隨時間衰減效果的計數器
counter = LFULogIncr(counter); //訪問次數自增
val->lru = (LFUGetTimeInMinutes()<<8) | counter; //將訪問次數放入到lru的前8位
}
注意: lru
一共佔用24
個字節,可以從上面看出低8
位存儲的是對象的訪問次數,高16
位存儲的是對象的上次訪問時間,以分鐘爲單位;此外LFUDecrAndReturn
函數的返回值是counter訪問次數
,對象的訪問次數在counter
上面累加。爲了保證存活較長時間的對象長,其訪問次數可能會很大,但是訪問頻率很小,導致無法被淘汰的情況。LFU雖然是基於訪問次數的,但是對於訪問頻率較小的對象也要被淘汰。LFUDecrAndReturn
就是實現了訪問次數隨着時間的推移遞減的效果,代碼如下:
unsigned long LFUDecrAndReturn(robj *o) {
unsigned long ldt = o->lru >> 8;//獲取最近一次訪問時間
unsigned long counter = o->lru & 255;//獲取訪問次數
//num_periods 是當前理應衰減的數量
unsigned long num_periods = server.lfu_decay_time ? LFUTimeElapsed(ldt) / server.lfu_decay_time : 0;
//判斷釋放衰減到0
if (num_periods)
counter = (num_periods > counter) ? 0 : counter - num_periods;
return counter;
}
衰減的具體公式= (當前時間-最近一次訪問時間)/ 指定的衰減週期
,其中衰減週期server.lfu_decay_time可以在配置文件中配置
3、數據庫對象redisDb
Redis爲了方便使用,將衆多redisObject
封裝在redisDb
裏面並提供給外界使用,總所周知Redis實例中不止一個數據庫,默認是有16
個庫,那麼這16個庫是怎麼來的呢?下面我們將介紹一下,首先我們來看看redisDb
的定義是怎麼樣的,代碼如下:
typedef struct redisDb {
dict *dict; //當前數據庫所有的鍵值對字典
dict *expires; //每個鍵值對對於的淘汰策略字典
dict *blocking_keys; //用於阻塞client場景的key操作
dict *ready_keys; //當前正在被阻塞的key
dict *watched_keys; //用於事務的,監控key
int id; //當前數據庫的ID
long long avg_ttl; //平均ttl
} redisDb;
下面來解釋一下各個字段的含義
⭐id
爲數據庫序號,默認情況下Redis有16個數據庫,id序號爲0~15,可以通過配置文件屬性修改數據庫數量,通過redisDb[]
數組的方式可以快速切換到不同的數據庫。
⭐dict
存儲當前數據庫所有的鍵值對。
⭐expires
存儲鍵的過期時間。
⭐avg_ttl
存儲數據庫對象的平均TTL,用於統計。
⭐blocking_keys 和 ready_keys
當使用BLPOP命令阻塞的獲取列表原始的時候,如果鏈表爲空,會阻塞當前客戶端,同時將此列表的鍵記錄在blocking_keys
;當使用命令PUSH向列表添加元素的時候,會從字典blocking_keys
中查找該key,如果找到說明當前有客戶端正在被該key阻塞中,於是將此列表的鍵記錄到ready_keys
裏面,以便後續響應。
⭐watched_keys
Redis支持事務, 使用multi
開啓事務,命令exec
執行事務,但是當開啓事務到執行事務期間數據有可能會被修改,Redis採用了類似於樂觀鎖的方式防止這種情況發生。當開啓事務的時候同時會使用watch key
命令監控關心的數據鍵,而watched_keys
就是當前被watch命令監控的所有數據鍵的字典,其中key-value分別是數據鍵與客戶端對象。當Redis服務器接收到寫命令時,會從字典watched_keys
中查找該key,如果找到說明當前有客戶端正在監控該數據鍵,於是將標記客戶端對象爲dirty
;待Redis服務器接收到客戶端exec
命令時,如果客戶端帶有dirty
標記,則會拒絕事務執行。
當然redisDb除了以上的幾個屬性外,還有其它屬性,這裏就不展開了歡迎大家直接下載源碼閱讀,大致的功能,完整的定義如下:
typedef struct redisDb {
dict *dict; //當前數據庫所有的鍵值對字典
dict *expires; //每個鍵值對對於的淘汰策略字典
dict *blocking_keys; //用於阻塞client場景的key操作
dict *ready_keys; //當前正在被阻塞的key
dict *watched_keys; //用於事務的,監控key
int id; //當前數據庫的ID
long long avg_ttl; /* Average TTL, just for stats */
unsigned long expires_cursor; /* Cursor of the active expire cycle. */
list *defrag_later; /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;
4、客戶端對象client
Redis是典型的客戶端----服務端結構,客戶通過socket與服務端建立網絡連接併發送命令請求,服務端處理命令請求並回復。Redis使用結構體client存儲客戶端連接的所有信息,包括但不限於客戶端名稱
,客戶端連接的套接字描述符
,客戶端當前選擇的數據庫ID
,客戶端輸入緩衝區
,客戶端輸出緩衝區
等等。主要結構體定義如下:
typedef struct client {
uint64_t id; //該客戶端ID
connection *conn; //客戶端連接
int resp; //客戶端使用的RESP協議版本(爲了兼容舊版)
redisDb *db; //當前選擇的DB
robj *name; //客戶端名稱
time_t lastinteraction; //客戶端上次與服務端交互時間,用於實現超時處理
sds querybuf; //用於積累查詢的緩衝區
size_t qb_pos; //當前在查詢緩衝區讀取到的位置
int argc; //輸入緩衝區的命令請求是按照Redis協議格式(RESP協議)編碼的字符串,需要解析出所有的參數,其中參數的個數就存儲在argc中
robj **argv; //當前命令的參數
sds pending_querybuf; //由於主從複製的緩衝區
struct redisCommand *cmd, *lastcmd; //最後一次執行的命令集合
list *reply; //需要響應對象的鏈表
unsigned long long reply_bytes; //需要響應對象的總大小
size_t sentlen; //表示已經返回給客戶端的字節數
/* Response buffer */
int bufpos;
char buf[PROTO_REPLY_CHUNK_BYTES];
} client;
下面來介紹一下主要字段的含義:
⭐id
id爲當前client的全局唯一ID,通過全局變量server.next_client_id實現。
⭐conn
爲當前客戶端socket連接
⭐resp
當前客戶端使用的resp協議版本,Redis是向下兼容,需要兼容低版本的客戶端。
⭐db
爲客戶端當前select
的數據庫對象redisDb。
⭐name
客戶端名稱,可以使用命令CLIENT SETNAME
進行設置。
⭐lastinteraction
客戶端上次與服務端交互時間,用於超時處理。
⭐querybuf
輸入緩衝區,recv
函數接收的客戶端命令請求會暫時存放在這裏。
⭐qb_pos
當前緩衝區讀取到的位置指針。
⭐argc
輸入緩衝區的命令請求是按照Redis協議格式 (resp協議) 編碼的字符串,需要解析出所有的參數,其中參數的個數就存儲在argc
中。
⭐argv
當前正在執行的命令的參數,已經被解析成redisObject。
⭐pending_querybuf
用於主從複製的緩衝區。
⭐cmd
待執行的客戶端命令;解析命令請求後,會根據命令名稱查找該命令對應的命令對象,存儲在cmd
字段中,對應的命令類型可以參考命令對象redisCommand
,後面會進一步講解。
⭐reply
輸出鏈表,存儲待返回給客戶端的命令回覆數據。
⭐reply_bytes
輸出鏈表中所有節點的存儲空間總大小。
⭐sentlen
表示已經返回給客戶端的字節數。
⭐buf 和 bufpos
輸出的緩衝區,存儲待返回給客戶端的命令回覆數據,bufpos
表示輸出緩衝區中數據的最大字節位置,顯然sentlen
~bufpos
區間的數據都是需要返回給客戶端的。可以看到reply
和buf
都用於緩存待返回給客戶端的命令回覆數據,至於爲什麼buf
和reply
需要同時存在呢?後面的文章會進行解答。
5、服務端對象redisServer
結構體redisServer存儲的是Redis服務器的所有信息,包括單不限於:數據庫
,配置參數
,命令表
,監聽端口與地址
,客戶端列表
,若干統計信息
,RDB與AOF持久化相關信息
,主從複製相關信息
,集羣相關信息
等等。由於redisServer涉及的屬性非常多,這裏我們也介紹一下經常接觸到的屬性內容,其餘的也歡迎讀者自行研究就不做過多的概述,定義代碼如下server.h
:
struct redisServer {
/* General */
pid_t pid; //當前Redis進程PID
char *configfile; //當前Redis配置文件
............................【中間省略】..............................
redisDb *db; //當前的Redis所有的Db指針
int dbnum; //當前數據庫總數
dict *commands; //當前Redis對應的命令字典
aeEventLoop *el; //10k的處理方式
..........................【中間省略】...............................
/* Networking */
int port; //當前監聽的端口
char *bindaddr[CONFIG_BINDADDR_MAX]; //綁定的所有IP地址
int bindaddr_count; //綁定所有IP地址的數量
int ipfd[CONFIG_BINDADDR_MAX]; //針對bindaddr字段的所有IP地址創建的socket文件描述符
int ipfd_count; //創建socket文件描述符數目
list *clients; //當前連接到Redis服務器所有客戶端
client *current_client; //當前執行命令的客戶端
list *clients_pending_write; //等待寫的client
list *clients_pending_read; //等待讀的client
.........................【中間省略】....................................
//常用指令指針緩存
struct redisCommand *delCommand, *multiCommand, *lpushCommand,
*lpopCommand, *rpopCommand, *zpopminCommand,
*zpopmaxCommand, *sremCommand, *execCommand,
*expireCommand, *pexpireCommand, *xclaimCommand,
*xgroupCommand;
.........................【中間省略】.....................................
int daemonize; //是否守護線程運行
int aof_enabled; //是否開啓aof
pid_t rdb_child_pid; //RDB保存的子進程PID
.........................【中間省略】....................................
int maxidletime; //最大空閒時間
};
字段定義如下:
⭐pid
當前Redis進程PID
⭐configfile
配置文件絕對路徑。
⭐db
數據庫數組,數組的每個元素都是redisDb類型。
⭐dbnum
數據庫數目,可以通過databases配置,默認16。
⭐commands
命令字典,Redis支持的所有命令都存儲在這個字典中,key
爲命令名稱,value
爲redisCommand對象,後面會進一步介紹。
⭐el
Redis是典型的事件驅動模型,el
代表Redis的事件循環,類型爲aeEventLoop (默認使用epoll模式),後面會介紹。
⭐port
服務端監聽端口號,可以通過參數port進行配置,默認端口是6379。
⭐bindaddr 和 bindaddr_count
綁定的所有IP地址,可以通過參數bind配置多個,例如:bind 192.168.1.11 192.168.1.12,bindaddr_count
爲用戶配置的IP地址數目;CONFIG_BINDADDR_MAX
常量爲16,即最多綁定16個IP地址;Redis默認會綁定到當前機器所有可用的IP地址。
⭐ipfd 和 ipfd_count
針對bindaddr
字段的所有IP地址創建的socket,ipfd_count
爲創建的socket文件描述符數目。
⭐clients 和 current_client
client
是當前連接到Redis服務端的所有客戶端對象,current_client
是當前正在執行命令的客戶端對象。
⭐*delCommand, *multiCommand, *lpushCommand, *lpopCommand, *rpopCommand, *zpopminCommand, *zpopmaxCommand, *sremCommand, *execCommand, *expireCommand, *pexpireCommand, *xclaimCommand, *xgroupCommand
Redis中常用的指令集合,在redisServer創建的時候就初始化好,方便快速調用相應的實現方法。
⭐daemonize
當前Redis是否以守護線程的方式運行。
⭐aof_enabled
是否開啓了AOF(後面AOF持久化會介紹)。
⭐rdb_child_pid
當前執行RDB存儲的子線程PID(後面RDB持久化會介紹)。
⭐maxidletime
最大空閒時間,可通過參數timeout
配置,結合client對象的lastinteraction
字段,當客戶端沒有與服務器交互的時間超過maxidletime
的時候,會認爲客戶端超時並釋放該客戶端連接。
⭐clients_pending_read
和 clients_pending_write
等待讀和寫的client鏈表,和多線程有關,後續會介紹
6、命令結構體redisCommand
Redis支持的所有命令初始都存儲在字典commands
中,對應的類型爲redisCommand,定義初始化代碼如下:
struct redisCommand redisCommandTable[] = {
{"get",getCommand,2,"read-only fast @string",0,NULL,1,1,1,0,0,0},
{"set",setCommand,-3,"write use-memory @string",0,NULL,1,1,1,0,0,0},
{"setnx",setnxCommand,3,"write use-memory fast @string",0,NULL,1,1,1,0,0,0},
{"setex",setexCommand,4,"write use-memory @string",0,NULL,1,1,1,0,0,0},
{"psetex",psetexCommand,4,"write use-memory @string",0,NULL,1,1,1,0,0,0},
{"strlen",strlenCommand,2,"read-only fast @string",0,NULL,1,1,1,0,0,0},
{"del",delCommand,-2,"write @keyspace",0,NULL,1,-1,1,0,0,0},
{"rpush",rpushCommand,-3,"write use-memory fast @list",0,NULL,1,1,1,0,0,0},
{"lpush",lpushCommand,-3,"write use-memory fast @list",0,NULL,1,1,1,0,0,0},
{"rpushx",rpushxCommand,-3,"write use-memory fast @list",0,NULL,1,1,1,0,0,0},
{"lpushx",lpushxCommand,-3,"write use-memory fast @list",0,NULL,1,1,1,0,0,0},
{"linsert",linsertCommand,5,"write use-memory @list",0,NULL,1,1,1,0,0,0},
{"rpop",rpopCommand,2,"write fast @list",0,NULL,1,1,1,0,0,0},
{"lpop",lpopCommand,2,"write fast @list",0,NULL,1,1,1,0,0,0},
.......................................後面還有很多命令就不列出來了
redisCommand結構相對比較簡單,主要定義了命令的名稱
,命令處理函數
,命令標誌
等等。其結構體定義如下server.h
:
struct redisCommand {
char *name; //命令名稱
redisCommandProc *proc; //命令對應的處理函數指針
int arity; //命令參數數量
char *sflags; // 命令標誌
uint64_t flags; //命令二進制標誌
redisGetKeysProc *getkeys_proc; //獲取鍵的處理函數
long long microseconds, calls; //當前命令的總執行次數和總執行時間
int id; //命令對應的ID ,運行的時候動態分配的,用於ACL鑑權指定命令使用權限
};
各個字段定義如下;
⭐name
當前的命令名稱,如get
,set
,rpop
,lpop
。
⭐proc
命令處理函數的指針。
⭐arity
命令參數數目,用於校驗命令的請求格式是否正確,當arity
小於0時,表示命令參數數目大於等於arity
;當arity
大於0時,表示命令參數數目必須爲arity
;注意命令請求中,命令的名稱本身也是一個參數,如get
命令參數數目爲2,命令請求格式爲get key
。
⭐sflags
命令標誌,例如標識命令是讀命令還是寫命令。
⭐flags
命令的二進制標誌,服務器啓動時解析sflags
字段生成。
⭐calls
表示從服務器運行開始到現在,命令被調用次數。
⭐microseconds
表示從服務器運行開始到現在,命令總執行時間,通過microseconds/calls
可以計算出該命令的平均處理時間,用於統計。
下面是字符標誌和二進制標誌對應的含義和命令表:
字符標識 | 二進制標識 | 含義 | 相關命令 |
---|---|---|---|
w | CMD_WEITE | 寫命令 | set,del,incr,lpush |
r | CMD_READONLY | 讀命令 | get,exists,llen |
m | CMD_DENYOOM | 內存不足時,拒絕執行此類命令 | set,append,lpush |
a | CMD_ADMIN | 管理命令 | save,shutdown,slaveof |
p | CMD_PUBSUB | 發佈訂閱相關命令 | subscribe,unsubscribe |
s | CMD_NOSCRIPT | 命令不可在Lua腳本使用 | auth,save,brpop |
R | CMD_RANDOM | 隨機命令,即使命令請求參數完全相同,返回結果也可能不同 | srandmember,scan,time |
S | CMD_SORT_FOR_SCRIPT | 當在Lua腳本使用此類命令時,需要對輸出結果做排序 | sinter,save,brpop |
l | CMD_LOADING | 服務器啓動載入過程中,只能執行此類命令 | sinter,sunion,sdiff |
t | CMD_STALE | 當服務器與主服務器斷開連接,且從服務器配置slave-serve-stale-data no的時候,從服務器只能執行此類命令 | auth,shutdown,info |
M | CMD_SKIP_MONITOR | 此類命令不會傳播給監視器 | exec |
k | CMD_ASKING | 集羣槽(slot)遷移時使用 | restore-asking |
F | CMD_FAST | 命令執行時間超過限制,會記錄延遲事件,此標記用於區分延遲事件類型,F表示fast-command | get,setnx,strlen,exists |
爲了提升命令執行的效率,redisCommandTable[]
轉換爲redisServer裏面的commands
字典,其中key
是命令名稱,value
是redisCommand對象,函數populateCommandTable
函數實現這個數組到字典的轉換,代碼如下:
void populateCommandTable(void) {
int j;
//統計需要放入的命令總數
int numcommands = sizeof(redisCommandTable)/sizeof(struct redisCommand);
for (j = 0; j < numcommands; j++) {
//通過偏移獲取對應的redisCommand對象位置
struct redisCommand *c = redisCommandTable+j;
int retval1, retval2;
//解析 sflags 生成 flags
if (populateCommandTableParseFlags(c,c->sflags) == C_ERR)
serverPanic("Unsupported command flag");
//初始化命令ID,用於ACL
c->id = ACLGetCommandID(c->name); /* Assign the ID used for ACL. */
//添加到字典中
retval1 = dictAdd(server.commands, sdsnew(c->name), c);
serverAssert(retval1 == DICT_OK && retval2 == DICT_OK);
}
}
//解析 sflags 生成 flags
int populateCommandTableParseFlags(struct redisCommand *c, char *strflags) {
int argc;
sds *argv;
argv = sdssplitargs(strflags,&argc);
if (argv == NULL) return C_ERR;
for (int j = 0; j < argc; j++) {
char *flag = argv[j];
if (!strcasecmp(flag,"write")) {
c->flags |= CMD_WRITE|CMD_CATEGORY_WRITE;
} else if (!strcasecmp(flag,"read-only")) {
c->flags |= CMD_READONLY|CMD_CATEGORY_READ;
} else if (!strcasecmp(flag,"use-memory")) {
c->flags |= CMD_DENYOOM;
} else if (!strcasecmp(flag,"admin")) {
c->flags |= CMD_ADMIN|CMD_CATEGORY_ADMIN|CMD_CATEGORY_DANGEROUS;
} else if (!strcasecmp(flag,"pub-sub")) {
c->flags |= CMD_PUBSUB|CMD_CATEGORY_PUBSUB;
} else if (!strcasecmp(flag,"no-script")) {
c->flags |= CMD_NOSCRIPT;
} else if (!strcasecmp(flag,"random")) {
c->flags |= CMD_RANDOM;
} else if (!strcasecmp(flag,"to-sort")) {
c->flags |= CMD_SORT_FOR_SCRIPT;
} else if (!strcasecmp(flag,"ok-loading")) {
c->flags |= CMD_LOADING;
} else if (!strcasecmp(flag,"ok-stale")) {
c->flags |= CMD_STALE;
} else if (!strcasecmp(flag,"no-monitor")) {
c->flags |= CMD_SKIP_MONITOR;
} else if (!strcasecmp(flag,"no-slowlog")) {
c->flags |= CMD_SKIP_SLOWLOG;
} else if (!strcasecmp(flag,"cluster-asking")) {
c->flags |= CMD_ASKING;
} else if (!strcasecmp(flag,"fast")) {
c->flags |= CMD_FAST | CMD_CATEGORY_FAST;
} else {
/* Parse ACL categories here if the flag name starts with @. */
uint64_t catflag;
if (flag[0] == '@' &&
(catflag = ACLGetCommandCategoryFlagByName(flag+1)) != 0)
{
c->flags |= catflag;
} else {
sdsfreesplitres(argv,argc);
return C_ERR;
}
}
}
/* If it's not @fast is @slow in this binary world. */
if (!(c->flags & CMD_CATEGORY_FAST)) c->flags |= CMD_CATEGORY_SLOW;
sdsfreesplitres(argv,argc);
return C_OK;
}
此外對於經常使用的命令,redisServer也直接緩存下來:
//常用指令指針緩存
struct redisCommand *delCommand, *multiCommand, *lpushCommand,
*lpopCommand, *rpopCommand, *zpopminCommand,
*zpopmaxCommand, *sremCommand, *execCommand,
*expireCommand, *pexpireCommand, *xclaimCommand,
*xgroupCommand;
7、總結
從上面的文章內容我們大概可以得出這樣的一個關係圖 (雖然不是很準確):
可以看出來Redis有很好的封裝,將命令執行
,客戶端
和實際存儲的數據庫
分離,體現了較好的解耦合的效果,其次aeEventLoop
也隔離的事件驅動的本身,無論是使用select/NIO/epoll還是時間事件 (下一篇文章介紹) ,能很靈活的切換。
此外:
通過redisCommand的隔離,讓Redis後續的命令拓展變的更加方便,也是較好的解耦合,redisServer內部的client
很好的實現了內部緩存的效果,實現了客戶端請求和響應的大吞吐量。