【Redis學習筆記】2018-06-21 redis命令執行過程 SET

LNMPR原始碼研究發表於2018-06-27

順風車運營研發團隊 李樂
1.命令執行過程

1.1命令請求格式

當使用者在客戶端鍵入一條命令請求時,客戶端會將其按照特定協議轉換為字串,傳送給伺服器;伺服器解析字串,獲取命令請求;

例如,當使用者執行 set key value 時,轉換後的字串為 *3rn3rnset3rnkey$5rnvaluern

其中,*3表示當前命令請求引數數目(set命令也是一個引數);rn用於分隔每個引數;3、5等表示引數字串長度;

1.2 服務端讀取命令請求

1.2.1客戶端處理函式簡介:

伺服器啟動時,會監聽socket,並建立對應檔案事件,監聽此fd上的可讀事件;(server.c/initServer)

//監聽socket
if (server.port != 0 &&
    listenToPort(server.port,server.ipfd,&server.ipfd_count) == C_ERR)
    exit(1);
 
//為所有監聽的socket建立檔案事件,監聽可讀事件;事件處理函式為acceptTcpHandler
for (j = 0; j < server.ipfd_count; j++) {
    if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
        acceptTcpHandler,NULL) == AE_ERR)
        {
     
        }
}

當客戶端連線到伺服器時,會呼叫acceptTcpHandler處理函式,伺服器會為每個連結建立一個client物件,並建立相應檔案事件,監聽此連結fd的可讀事件,並指定事件處理函式

//接收客戶端連結請求
cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
if (cfd == ANET_ERR) {
}
 
//建立客戶端
if ((c = createClient(fd)) == NULL) {
    close(fd); /* May be already closed, just ignore errors */
    return;
}
 
 
client *createClient(int fd) {
    client *c = zmalloc(sizeof(client));
 
    //設定fd為非阻塞;非延遲(epoll等堅挺的額socket必須是非阻塞;延遲的話傳送的資料會先儲存在tcp緩衝區,等到一定事件或資料量大時才會傳送)
    //建立檔案事件,監聽可讀事件,事件處理函式為readQueryFromClient
    if (fd != -1) {
        anetNonBlock(NULL,fd);
        anetEnableTcpNoDelay(NULL,fd);
        if (server.tcpkeepalive)
            anetKeepAlive(NULL,fd,server.tcpkeepalive);
        if (aeCreateFileEvent(server.el,fd,AE_READABLE,
            readQueryFromClient, c) == AE_ERR)
        {
            close(fd);
            zfree(c);
            return NULL;
        }
    }
    ……………………//初始化client結構體各欄位
}

1.2.2 讀取命令請求到輸入緩衝區

命令請求字串會先讀入client的輸入緩衝區中

void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
    client *c = (client*) privdata;
 
    …………
 
    qblen = sdslen(c->querybuf);
 
    //統計
    if (c->querybuf_peak < qblen) c->querybuf_peak = qblen;
    c->querybuf = sdsMakeRoomFor(c->querybuf, readlen);
    //讀取
    nread = read(fd, c->querybuf+qblen, readlen);
 
    //處理輸入緩衝區
    processInputBuffer(c);
 
    …………
}

1.2.3 解析輸入緩衝區資料

首先呼叫processMultibulkBuffer:解析*3獲取行數,迴圈獲取每一行引數(會先解析$3獲取引數長度),構造為一個redisObject物件,儲存在客戶端結構體的argv和argc欄位

其次呼叫processCommand處理命令請求

 void processInputBuffer(client *c) {
     
    while(sdslen(c->querybuf)) {
        
        //判斷命令請求型別;telnet傳送的命令和redis-cli傳送的命令請求格式不同
        if (!c->reqtype) {
            if (c->querybuf[0] == `*`) {
                c->reqtype = PROTO_REQ_MULTIBULK;
            } else {
                c->reqtype = PROTO_REQ_INLINE;
            }
        }
 
        if (c->reqtype == PROTO_REQ_INLINE) {
            if (processInlineBuffer(c) != C_OK) break;
        } else if (c->reqtype == PROTO_REQ_MULTIBULK) {
            if (processMultibulkBuffer(c) != C_OK) break;
        } else {
            serverPanic("Unknown request type");
        }
 
        //引數個數為0時
        if (c->argc == 0) {
            resetClient(c);
        } else {
            //處理命令
            if (processCommand(c) == C_OK) {
                if (c->flags & CLIENT_MASTER && !(c->flags & CLIENT_MULTI)) {
                 
                if (!(c->flags & CLIENT_BLOCKED) || c->btype != BLOCKED_MODULE)
                    resetClient(c);
            }
             
            if (server.current_client == NULL) break;
        }
    }
}
 
//resetClient()函式會釋放client結構體arg欄位中的各引數,重置argc為0

解析緩衝區字串邏輯如下:

int processMultibulkBuffer(client *c) {
    char *newline = NULL;
    long pos = 0;
    int ok;
    long long ll;
 
    if (c->multibulklen == 0) {
 
       //定位到第一行結束
        newline = strchr(c->querybuf,`
`);
       
        //解析行數,即引數個數
        ok = string2ll(c->querybuf+1,newline-(c->querybuf+1),&ll);
       
        c->multibulklen = ll;
 
        //argv村春命令引數,解析之前先清空
        if (c->argv) zfree(c->argv);
        c->argv = zmalloc(sizeof(robj*)*c->multibulklen);
    }
 
    //迴圈解析所有引數
    while(c->multibulklen) {
         
        //讀取$3字串,解析數值3
        if (c->bulklen == -1) {
            newline = strchr(c->querybuf+pos,`
`);
             
            if (c->querybuf[pos] != `$`) {
                return C_ERR;
            }
 
            ok = string2ll(c->querybuf+pos+1,newline-(c->querybuf+pos+1),&ll);
             
            c->bulklen = ll;
        }
 
        //讀取引數
        c->argv[c->argc++] =
        createStringObject(c->querybuf+pos,c->bulklen);
        pos += c->bulklen+2;
             
        c->bulklen = -1;
        c->multibulklen--;
         
    }
 
    /* We`re done when c->multibulk == 0 */
    if (c->multibulklen == 0) return C_OK;
 
    /* Still not ready to process the command */
    return C_ERR;
}

1.2.4 補充:什麼是redisObject

redis底層有多種資料結構:sds,intset,ziplist,linkedlist,skiplist,dict等;redis對外提供的資料型別有字串、列表、雜湊表、有需集合、集合;

redis會根據實際情況選擇合適的資料結構來儲存某一種資料型別;而同一種資料型別可能使用不同的資料結構儲存;

redisObject是對底層多種資料結構的進一步封裝;看看結構體:

typedef struct redisObject {
    unsigned type:4; //資料型別:字串、列表、雜湊表、集合、有序集合
    unsigned encoding:4; //儲存資料型別使用哪種資料結構,如列表的實現可能是ziplist或linkedlist
    unsigned lru:LRU_BITS; //lru,資料淘汰
    int refcount; //引用計數
    void *ptr;    //指向具體的資料結構
} robj;

1.2.5 處理命令

processInputBuffer解析輸入緩衝區中命令請求字串,將各個引數轉換為redisObject儲存在client結構體的argv欄位中,argc儲存引數個數,下一步呼叫processCommand處理命令

1.2.5.1命令結構體redisCommand

struct redisCommand {
    char *name; //命令名稱
    redisCommandProc *proc; //命令處理函式指標
    int arity; //命令引數個數,用於檢查命令請求引數是否正確;當取值-N時,表示引數個數大於等於N
    char *sflags; //標識命令屬性;如讀命令或寫命令等
    int flags;    //sflags的二進位制表示
     
    //沒搞明白
    redisGetKeysProc *getkeys_proc;
    int firstkey;
    int lastkey; 
    int keystep;
 
    //命令執行總耗時和執行次數
    long long microseconds, calls;
};

1.2.5.2命令執行前校驗

int processCommand(client *c) {
     
    //quit命令直接返回
    if (!strcasecmp(c->argv[0]->ptr,"quit")) {
        addReply(c,shared.ok);
        c->flags |= CLIENT_CLOSE_AFTER_REPLY;
        return C_ERR;
    }
 
    //命令字典查詢指定命令;所有的命令都儲存在命令字典中 struct redisCommand redisCommandTable[]={}
    c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);
 
    if (!c->cmd) { //沒有查詢到命令
         
    } else if ((c->cmd->arity > 0 && c->cmd->arity != c->argc) || (c->argc < -c->cmd->arity)) { //命令請求引數個數錯誤
     
    }
 
    //是否通過認證;沒有通過且必須認證時,只接受認證命令
    if (server.requirepass && !c->authenticated && c->cmd->proc != authCommand){
     
    }
     
    //開啟最大記憶體限制
    if (server.maxmemory) {
    //釋放記憶體
    int retval = freeMemoryIfNeeded();
     
    //CMD_DENYOOM表示記憶體不夠時,禁止執行此命令
    if ((c->cmd->flags & CMD_DENYOOM) && retval == C_ERR) {
         
    }
 
    //當此伺服器是master時:aof持久化失敗時,或上一次bgsave執行錯誤,且配置bgsave引數和stop_writes_on_bgsave_err;禁止執行寫命令
    if (((server.stop_writes_on_bgsave_err &&
        server.saveparamslen > 0 &&
        server.lastbgsave_status == C_ERR) ||
        server.aof_last_write_status == C_ERR) &&
        server.masterhost == NULL &&
        (c->cmd->flags & CMD_WRITE ||
        c->cmd->proc == pingCommand)){
    
    }
 
    //當此伺服器時master時:如果配置了repl_min_slaves_to_write,當slave數目小於時,禁止執行寫命令
    if (server.masterhost == NULL &&
        server.repl_min_slaves_to_write &&
        server.repl_min_slaves_max_lag &&
        c->cmd->flags & CMD_WRITE &&
        server.repl_good_slaves_count < server.repl_min_slaves_to_write){
     
    }
 
    //當此伺服器是slave,且配置了只讀時,如果客戶端不是master,則拒絕執行寫命令
    if (server.masterhost && server.repl_slave_ro &&
        !(c->flags & CLIENT_MASTER) &&
        c->cmd->flags & CMD_WRITE){
     
    }
 
    //當客戶端正在訂閱頻道時,只會執行以下命令
    if (c->flags & CLIENT_PUBSUB &&
        c->cmd->proc != pingCommand &&
        c->cmd->proc != subscribeCommand &&
        c->cmd->proc != unsubscribeCommand &&
        c->cmd->proc != psubscribeCommand &&
        c->cmd->proc != punsubscribeCommand) {
     
    }
 
    //伺服器為slave,但沒有正確連線master時,只會執行帶有CMD_STALE標誌的命令,如info等
    if (server.masterhost && server.repl_state != REPL_STATE_CONNECTED &&
        server.repl_serve_stale_data == 0 &&
        !(c->cmd->flags & CMD_STALE)){
    
    }
 
    //正在載入資料庫時,只會執行帶有CMD_LOADING標誌的命令,其餘都會被拒絕
    if (server.loading && !(c->cmd->flags & CMD_LOADING)) {
    
    }
 
    //當伺服器因為執行lua指令碼阻塞時,只會執行以下幾個命令,其餘都會拒絕
    if (server.lua_timedout &&
        c->cmd->proc != authCommand &&
        c->cmd->proc != replconfCommand &&
        !(c->cmd->proc == shutdownCommand &&
        c->argc == 2 &&
        tolower(((char*)c->argv[1]->ptr)[0]) == `n`) &&
        !(c->cmd->proc == scriptCommand &&
        c->argc == 2 &&
        tolower(((char*)c->argv[1]->ptr)[0]) == `k`)){
     
    }
 
    //執行命令
    if (c->flags & CLIENT_MULTI &&
        c->cmd->proc != execCommand && c->cmd->proc != discardCommand &&
        c->cmd->proc != multiCommand && c->cmd->proc != watchCommand){
        //開啟了事務,命令只會入佇列;
        queueMultiCommand(c);
        addReply(c,shared.queued);
    } else {
        //直接執行命令
        call(c,CMD_CALL_FULL);
    
    }
}  

1.2.5.3 命令執行

執行命令時,會需要做很多額外的操作,統計,記錄慢查詢日誌,傳播命令道monitor、slave,aof持久化等

//flags=CMD_CALL_FULL=(CMD_CALL_SLOWLOG | CMD_CALL_STATS | CMD_CALL_PROPAGATE)
//表示需要記錄慢查詢日誌,統計,廣播命令
void call(client *c, int flags) {
 
    //dirty記錄資料庫修改次數;start記錄命令開始執行時間us;duration記錄命令執行花費時間
    long long dirty, start, duration;
    int client_old_flags = c->flags;
 
    //有監視器的話,需要將命令傳送給監視器
    if (listLength(server.monitors) &&
        !server.loading &&
        !(c->cmd->flags & (CMD_SKIP_MONITOR|CMD_ADMIN)))
    {
        replicationFeedMonitors(c,server.monitors,c->db->id,c->argv,c->argc);
    }
 
    //處理命令,呼叫命令處理函式
    dirty = server.dirty;
    start = ustime();
    c->cmd->proc(c);
    duration = ustime()-start;
    dirty = server.dirty-dirty;
    if (dirty < 0) dirty = 0;
 
    //記錄慢查詢日誌
    if (flags & CMD_CALL_SLOWLOG && c->cmd->proc != execCommand) {
        char *latency_event = (c->cmd->flags & CMD_FAST) ?
                              "fast-command" : "command";
        latencyAddSampleIfNeeded(latency_event,duration/1000);
        slowlogPushEntryIfNeeded(c,c->argv,c->argc,duration);
    }
    //統計
    if (flags & CMD_CALL_STATS) {
        c->lastcmd->microseconds += duration;
        c->lastcmd->calls++;
    }
 
    //廣播命令
    if (flags & CMD_CALL_PROPAGATE &&
        (c->flags & CLIENT_PREVENT_PROP) != CLIENT_PREVENT_PROP)
    {
        int propagate_flags = PROPAGATE_NONE;
 
        //dirty大於0時,需要廣播命令給slave和aof
        if (dirty) propagate_flags |= (PROPAGATE_AOF|PROPAGATE_REPL);
 
        
        if (c->flags & CLIENT_FORCE_REPL) propagate_flags |= PROPAGATE_REPL;
        if (c->flags & CLIENT_FORCE_AOF) propagate_flags |= PROPAGATE_AOF;
 
   
        if (c->flags & CLIENT_PREVENT_REPL_PROP ||
            !(flags & CMD_CALL_PROPAGATE_REPL))
                propagate_flags &= ~PROPAGATE_REPL;
        if (c->flags & CLIENT_PREVENT_AOF_PROP ||
            !(flags & CMD_CALL_PROPAGATE_AOF))
                propagate_flags &= ~PROPAGATE_AOF;
 
        //廣播命令,寫如aof,傳送命令到slave
        if (propagate_flags != PROPAGATE_NONE && !(c->cmd->flags & CMD_MODULE))
            propagate(c->cmd,c->db->id,c->argv,c->argc,propagate_flags);
    }
}

2.命令表

redis有個命令表redisCommandTable儲存每個命令的詳細屬性

//命令名稱,命令處理函式,命令引數(-3表示引數數目大於等於3個),命令屬性標誌,…………
struct redisCommand redisCommandTable[] = {
    {"get",getCommand,2,"rF",0,NULL,1,1,1,0,0},
    {"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0},
    {"setnx",setnxCommand,3,"wmF",0,NULL,1,1,1,0,0},
    {"setex",setexCommand,4,"wm",0,NULL,1,1,1,0,0},
    {"psetex",psetexCommand,4,"wm",0,NULL,1,1,1,0,0},
    {"append",appendCommand,3,"wm",0,NULL,1,1,1,0,0},
    {"strlen",strlenCommand,2,"rF",0,NULL,1,1,1,0,0},
    {"del",delCommand,-2,"w",0,NULL,1,-1,1,0,0},
    {"unlink",unlinkCommand,-2,"wF",0,NULL,1,-1,1,0,0},
    {"exists",existsCommand,-2,"rF",0,NULL,1,-1,1,0,0},
    …………
}

3.set命令執行

3.1 set命令介紹

SET key value [EX seconds] [PX milliseconds] [NX|XX]

將字串值 value 關聯到 key 。

如果 key 已經持有其他值, SET 就覆寫舊值,無視型別。

對於某個原本帶有生存時間(TTL)的鍵來說, 當 SET 命令成功在這個鍵上執行時, 這個鍵原有的 TTL 將被清除。

EX、PX、NX/XX可選引數含義如下:

  • EX second :設定鍵的過期時間為 second 秒。 SET key value EX second 效果等同於 SETEX key second value 。
  • PX millisecond :設定鍵的過期時間為 millisecond 毫秒。 SET key value PX millisecond 效果等同於 PSETEX keymillisecond value 。
  • NX :只在鍵不存在時,才對鍵進行設定操作。 SET key value NX 效果等同於 SETNX key value 。
  • XX :只在鍵已經存在時,才對鍵進行設定操作。

3.2 set命令執行函式

3.2.1 解析set命令請求

/* SET key value [NX] [XX] [EX <seconds>] [PX <milliseconds>] */
void setCommand(client *c) {
    int j;
    robj *expire = NULL;        //過期時間
    int unit = UNIT_SECONDS;    //時間單位
    int flags = OBJ_SET_NO_FLAGS; //標誌命令是否攜帶nx、xx、ex、px可選引數
 
    for (j = 3; j < c->argc; j++) {
        char *a = c->argv[j]->ptr;
        robj *next = (j == c->argc-1) ? NULL : c->argv[j+1]; //最後一個引數可能是過期時間
 
        if ((a[0] == `n` || a[0] == `N`) &&
            (a[1] == `x` || a[1] == `X`) && a[2] == ` ` &&
            !(flags & OBJ_SET_XX))
        {
            flags |= OBJ_SET_NX;   //NX標誌
        } else if ((a[0] == `x` || a[0] == `X`) &&
                   (a[1] == `x` || a[1] == `X`) && a[2] == ` ` &&
                   !(flags & OBJ_SET_NX))
        {
            flags |= OBJ_SET_XX;  //XX標誌
        } else if ((a[0] == `e` || a[0] == `E`) &&
                   (a[1] == `x` || a[1] == `X`) && a[2] == ` ` &&
                   !(flags & OBJ_SET_PX) && next)
        {
            flags |= OBJ_SET_EX;   //EX標誌
            unit = UNIT_SECONDS;
            expire = next;
            j++;
        } else if ((a[0] == `p` || a[0] == `P`) &&
                   (a[1] == `x` || a[1] == `X`) && a[2] == ` ` &&
                   !(flags & OBJ_SET_EX) && next)
        {
            flags |= OBJ_SET_PX;   //PX標誌
            unit = UNIT_MILLISECONDS;
            expire = next;
            j++;
        } else {
            addReply(c,shared.syntaxerr);
            return;
        }
    }
     
    c->argv[2] = tryObjectEncoding(c->argv[2]);
    setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL); //處理命令
}

3.2.2 命令處理

void setGenericCommand(client *c, int flags, robj *key, robj *val, robj *expire, int unit, robj *ok_reply, robj *abort_reply) {
    long long milliseconds = 0; /* initialized to avoid any harmness warning */
 
    //設定了過期時間;expire是robj型別,獲取整數值
    if (expire) {
        if (getLongLongFromObjectOrReply(c, expire, &milliseconds, NULL) != C_OK)
            return;
        if (milliseconds <= 0) {
            addReplyErrorFormat(c,"invalid expire time in %s",c->cmd->name);
            return;
        }
        if (unit == UNIT_SECONDS) milliseconds *= 1000;
    }
 
    //NX,key存在時直接返回;XX,key不存在時直接返回
    if ((flags & OBJ_SET_NX && lookupKeyWrite(c->db,key) != NULL) ||
        (flags & OBJ_SET_XX && lookupKeyWrite(c->db,key) == NULL))
    {
        addReply(c, abort_reply ? abort_reply : shared.nullbulk);
        return;
    }
 
    //新增都資料庫字典
    setKey(c->db,key,val);
    server.dirty++;
 
    //過期時間新增到過期字典
    if (expire) setExpire(c,c->db,key,mstime()+milliseconds);
 
    //鍵空間通知
    notifyKeyspaceEvent(NOTIFY_STRING,"set",key,c->db->id);
    if (expire) notifyKeyspaceEvent(NOTIFY_GENERIC,
        "expire",key,c->db->id);
    addReply(c, ok_reply ? ok_reply : shared.ok);
}

相關文章