Redis源碼閱讀【8-命令處理生命週期-1】

Redis源碼閱讀【1-簡單動態字符串】

1、介紹

前面的內容大部分都是在介紹redis中的基本數據結構,從這裏開始主要介紹Redis服務端的請求接收,請求響應,接收到的命令解析,指向命令,返回命令響應等等。Redis是服務器是典型的事件驅動服務器,因此事件處理顯得格外重要,而Redis將事件分爲兩大類:

1、文件事件(socket的讀寫)
2、時間事件(週期性定時任務)

2、服務端對象redisObject

爲了更好的理解服務器與客戶端的交互,key只能是字符串value可以是字符串、列表、集合、有序集合和散列表 ,這5種數據類型用通用結構體 robj 表示,我們稱之爲Redis對象。結構體robjtype字段表示對象類型,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在不同情況下可能採用不同的數據結構存儲,結構體robjencoding字段表示當前對象底層存儲採用的數據結構,即對象的編碼,總共定義瞭如下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字段。可以看出,爲了創建一個字符串或者對象,必須分配兩次內存空間,robjsbs存儲空間;兩次內存分配效率低下,且數據分離存儲降低了計算機的高數緩存的效率。因此提出OBJ_ENCODING_EMBSTR編碼的字符,當字符串內容較短的時候,只分配一次內存,robjsbs連續存儲,以此提升內存分配效率與數據訪問效率。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根據用戶配置的緩存淘汰策略存儲不同數據,常用的策略就是LRULFU 。如果當前緩存策略是 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;

下面來介紹一下主要字段的含義:
idid爲當前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區間的數據都是需要返回給客戶端的。可以看到replybuf都用於緩存待返回給客戶端的命令回覆數據,至於爲什麼bufreply需要同時存在呢?後面的文章會進行解答。

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爲命令名稱,valueredisCommand對象,後面會進一步介紹。
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地址創建的socketipfd_count爲創建的socket文件描述符數目。
clients 和 current_client client是當前連接到Redis服務端的所有客戶端對象,current_client是當前正在執行命令的客戶端對象。
*delCommand, *multiCommand, *lpushCommand, *lpopCommand, *rpopCommand, *zpopminCommand, *zpopmaxCommand, *sremCommand, *execCommand, *expireCommand, *pexpireCommand, *xclaimCommand, *xgroupCommandRedis中常用的指令集合,在redisServer創建的時候就初始化好,方便快速調用相應的實現方法。
daemonize當前Redis是否以守護線程的方式運行。
aof_enabled是否開啓了AOF(後面AOF持久化會介紹)。
rdb_child_pid當前執行RDB存儲的子線程PID(後面RDB持久化會介紹)。
maxidletime最大空閒時間,可通過參數timeout配置,結合client對象的lastinteraction字段,當客戶端沒有與服務器交互的時間超過maxidletime的時候,會認爲客戶端超時並釋放該客戶端連接。
clients_pending_readclients_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當前的命令名稱,如getsetrpoplpop
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是命令名稱,valueredisCommand對象,函數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很好的實現了內部緩存的效果,實現了客戶端請求和響應的大吞吐量。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章