Redis 命令的執行過程

犀利豆發表於2018-03-30

原文地址:www.xilidou.com/2018/03/30/…

之前寫了一系列文章,已經很深入的探討了 Redis 的資料結構,資料庫的實現,key的過期策略以及 Redis 是怎麼處理事件的。所以距離 Redis 的單機實現只差最後一步了,就是 Redis 是怎麼處理 client 發來的命令並返回結果的,所以我們就仔細討論一下 Redis 是怎麼執行命令的。

閱讀這篇文章你將會了解到:

  • Redis 是怎麼執行遠端客戶端發來的命令的

Redis client(客戶端)

Redis 是單執行緒應用,它是如何與多個客戶端簡歷網路連結並處理命令的? 由於 Redis 是基於 I/O 多路複用技術,為了能夠處理多個客戶端的請求,Redis 在本地為每一個連結到 Redis 伺服器的客戶端建立了一個 redisClient 的資料結構,這個資料結構包含了每個客戶端各自的狀態和執行的命令。 Redis 伺服器使用一個連結串列來維護多個 redisClient 資料結構。

在伺服器端用一個連結串列來管理所有的 redisClient。


struct redisServer {

    //...
    list *clients;              /* List of active clients */
    //...
}
複製程式碼

所以我就看看 redisClient 包含的資料結構和重要引數:

typedef struct redisClient {

    // 客戶端狀態標誌
    int flags;              /* REDIS_SLAVE | REDIS_MONITOR | REDIS_MULTI ... */
    
    // 套接字描述符
    int fd;

    // 當前正在使用的資料庫
    redisDb *db;

    // 當前正在使用的資料庫的 id (號碼)
    int dictid;

    // 客戶端的名字
    robj *name;             /* As set by CLIENT SETNAME */

    // 查詢緩衝區
    sds querybuf;

    // 查詢緩衝區長度峰值
    size_t querybuf_peak;   /* Recent (100ms or more) peak of querybuf size */

    // 引數數量
    int argc;

    // 引數物件陣列
    robj **argv;

    // 記錄被客戶端執行的命令
    struct redisCommand *cmd, *lastcmd;

    // 請求的型別:內聯命令還是多條命令
    int reqtype;

    // 剩餘未讀取的命令內容數量
    int multibulklen;       /* number of multi bulk arguments left to read */

    // 命令內容的長度
    long bulklen;           /* length of bulk argument in multi bulk request */

    // 回覆連結串列
    list *reply;

    // 回覆連結串列中物件的總大小
    unsigned long reply_bytes; /* Tot bytes of objects in reply list */

    // 已傳送位元組,處理 short write 用
    int sentlen;            /* Amount of bytes already sent in the current
                               buffer or object being sent. */

    // 回覆偏移量
    int bufpos;
    // 回覆緩衝區
    char buf[REDIS_REPLY_CHUNK_BYTES];
    // ...
}

複製程式碼

這裡需要特別的注意,redisClient 並非指遠端的客戶端,而是一個 Redis 服務本地的資料結構,我們可以理解這個 redisClient 是遠端客戶端的一個對映或者代理。

flags

flags 表示了目前客戶端的角色,以及目前所處的狀態。他比較特殊可以單獨表示一個狀態或者多個狀態。

querybuf

querybuf 是一個 sds 動態字串型別,所謂 buf 說明是它只是一個緩衝區,用於儲存沒有被解析的命令。

argc & argv

上文的 querybuf 是一個沒有處理過的命令,當 Redis 將 querybuf 命令解析以後,會將得出的引數個數和以及引數分別儲存在 argc 和 argv 中。argv 是一個 redisObject 的陣列。

cmd

Redis 使用一個字典儲存了所有的 redisCommand。key 是 redisCommand 的名字,值就是一個 redisCommand 結構,這個結構儲存了命令的實現函式,命令的標誌,命令應該給定的引數個數,命令的執行次數和總消耗時長等統計資訊,cmd 是一個 redisCommand。

當 Redis 解析出 argv 和 argc 後,會根據陣列 argv[0],到字典中查詢出對應的 redisCommand。上文的例子中 Redis 就會去字典去查詢 SET 這個命令對應的 redisCommand。redis 會執行 redisCommand 中命令的實現函式。

buf & bufpos & reply

buf 是一個長度為 REDIS_REPLY_CHUNK_BYTES 的陣列。Redis 執行相應的操作以後,就會將需要返回的返回的資料儲存到 buf 中,bufpos 用於記錄 buf 中已用的位元組數數量,當需要恢復的資料大於 REDIS_REPLY_CHUNK_BYTES 時,redis 就會是用 reply 這個連結串列來儲存資料。

其他引數

其他引數大家看註釋就能明白,就是字面的意思。省略的引數基本上涉及 Redis 叢集管理的引數,在之後的文章中會繼續講解。

客戶端的連結和斷開

上文說過 redisServer 是用一個連結串列來維護所有的 redisClient 狀態,每當有一個客戶端發起連結以後,就會在 Redis 中生成一個對應的 redisClient 資料結構,增加到clients這個連結串列之後。

一個客戶端很可能被多種原因斷開。

總體分為幾種型別:

  • 客戶端主動退出或者被 kill。
  • timeout 超時。
  • Redis 為了自我保護,會斷開發的資料超過限制大小的客戶端。
  • Redis 為了自我保護,會斷需要返回的資料超過限制大小的客戶端。

呼叫總結

當客戶端和伺服器端的巢狀字變得可讀的時候,伺服器將會呼叫命令請求處理器來執行以下操作:

  1. 讀取巢狀字中的資料,寫入 querybuf。
  2. 解析 querybuf 中的命令,記錄到 argc 和 argv 中。
  3. 根據 argv[0] 查詢對應的 recommand。
  4. 執行 recommand 對應的實現函式。
  5. 執行以後將結果存入 buf & bufpos & reply 中,返回給呼叫方。

Redis Server (服務端)

上文是從 redisClient 的角度來觀察命令的執行,文章接下來的部分將會從 Redis 的程式碼層面,微觀的觀察 Redis 是怎麼實現命令的執行的。

redisServer 的啟動

在瞭解redisServer 的工作機制的工作機制之前,需要了解 redisServer 的啟動做了什麼:

可以繼續觀察 Redis 的 main() 函式。

int main(int argc, char **argv) {

    //...

    // 建立並初始化伺服器資料結構
    initServer();

    //...
}
複製程式碼

我們只關注 initServer() 這個函式,他負責初始化伺服器的資料結構。繼續跟蹤程式碼:


void initServer() {

    //...

    //建立eventLoop
    server.el = aeCreateEventLoop(server.maxclients+REDIS_EVENTLOOP_FDSET_INCR);

    /* Create an event handler for accepting new connections in TCP and Unix
     * domain sockets. */
    // 為 TCP 連線關聯連線應答(accept)處理器
    // 用於接受並應答客戶端的 connect() 呼叫
    for (j = 0; j < server.ipfd_count; j++) {
        if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
            acceptTcpHandler,NULL) == AE_ERR)
            {
                redisPanic(
                    "Unrecoverable error creating server.ipfd file event.");
            }
    }

    // 為本地套接字關聯應答處理器
    if (server.sofd > 0 && aeCreateFileEvent(server.el,server.sofd,AE_READABLE,
        acceptUnixHandler,NULL) == AE_ERR) redisPanic("Unrecoverable error creating server.sofd file event.");

    //...

}

複製程式碼

篇幅限制,我們省略了很多與本編文章無關的程式碼,保留了核心邏輯程式碼。

在上一篇文章中 《Redis 中的事件驅動模型》 我們講解過,redis 使用不同的事件處理器,處理不同的事件。

在這段程式碼裡面:

  • 初始化了事件處理器的 eventLoop
  • 向 eventLoop 中註冊了兩個事件處理器 acceptTcpHandleracceptUnixHandler,分別處理遠端的連結和本地連結。

redisClient 的建立

當有一個遠端客戶端連線到 Redis 的伺服器,會觸發 acceptTcpHandler 事件處理器.

acceptTcpHandler 事件處理器,會建立一個連結。然後繼續呼叫 acceptCommonHandler

acceptCommonHandler 事件處理器的作用是:

  • 呼叫 createClient() 方法建立 redisClient
  • 檢查已經建立的 redisClient 是否超過 server 允許的數量的上限
  • 如果超過上限就拒絕遠端連線
  • 否則建立 redisClient 建立成功
  • 並更新連線的統計次數,更新 redisClinet 的 flags 欄位

這個時候 Redis 在服務端建立了 redisClient 資料結構,這個時候遠端的客戶端就在 redisServer 中建立了一個代理。遠端的客戶端就與 Redis 伺服器建立了聯絡,就可以向伺服器傳送命令了。

處理命令

createClient() 行數中:

// 繫結讀事件到事件 loop (開始接收命令請求)
if (aeCreateFileEvent(server.el,fd,AE_READABLE,readQueryFromClient, c) == AE_ERR)
複製程式碼

向 eventLoop 中註冊了 readQueryFromClientreadQueryFromClient 的作用就是從client中讀取客戶端的查詢緩衝區內容。

然後呼叫函式 processInputBuffer 來處理客戶端的請求。在 processInputBuffer 中有幾個核心函式:

  • processInlineBufferprocessMultibulkBuffer 解析 querybuf 中的命令,記錄到 argc 和 argv 中。
  • processCommand 根據 argv[0] 查詢對應的 recommen,執行 recommend 對應的執行函式。在執行之前還會驗證命令的正確性。將結果存入 buf & bufpos & reply 中

返回資料

萬事具備了,執行完了命令就需要把資料返回給遠端的呼叫方。呼叫鏈如下

processCommand -> addReply -> prepareClientToWrite

prepareClientToWrite 中我們有見到了熟悉的程式碼:

aeCreateFileEvent(server.el, c->fd, AE_WRITABLE,sendReplyToClient, c) == AE_ERR) return REDIS_ERR;
複製程式碼

向 eventloop 繫結了 sendReplyToClient 事件處理器。

sendReplyToClient 中觀察程式碼發現,如果 bufpos 大於 0,將會把 buf 傳送給遠端的客戶端,如果連結串列 reply 的長度大於0,就會將遍歷連結串列 reply,傳送給遠端的客戶端,這裡需要注意的是,為了避免 reply 資料量過大,就會過度的佔用資源引起 Redis 相應慢。為了解決這個問題,當寫入的總數量大於 REDIS_MAX_WRITE_PER_EVENT 時,Redis 將會臨時中斷寫入,記錄操作的進度,將處理時間讓給其他操作,剩餘的內容等下次繼續。這樣的套路我們一路走來看過太多了。

總結

  1. 遠端客戶端連線到 redis 後,redis服務端會為遠端客戶端建立一個 redisClient 作為代理。
  2. redis 會讀取巢狀字中的資料,寫入 querybuf 中。
  3. 解析 querybuf 中的命令,記錄到 argc 和 argv 中。
  4. 根據 argv[0] 查詢對應的 recommand。
  5. 執行 recommend 對應的執行函式。
  6. 執行以後將結果存入 buf & bufpos & reply 中。
  7. 返回給呼叫方。返回資料的時候,會控制寫入資料量的大小,如果過大會分成若干次。保證 redis 的相應時間。

Redis 作為單執行緒應用,一直貫徹的思想就是,每個步驟的執行都有一個上限(包括執行時間的上限或者檔案尺寸的上限)一旦達到上限,就會記錄下當前的執行進度,下次再執行。保證了 Redis 能夠及時響應不發生阻塞。

大家還可以閱讀我的 Redis 相關的文章:

Redis 的基礎資料結構(一) 可變字串、連結串列、字典

Redis 的基礎資料結構(二) 整數集合、跳躍表、壓縮列表

Redis 的基礎資料結構(三)物件

Redis 資料庫、鍵過期的實現

Redis 中的事件驅動模型

歡迎關注我的微信公眾號:

二維碼

相關文章