跟著大彬讀原始碼 - Redis 3 - 伺服器如何響應客戶端請求?(下)

北國丶風光發表於2019-07-15

繼續我們上一節的討論。伺服器啟動了,客戶端也傳送命令了。接下來,就要到伺服器“表演”的時刻了。

1 伺服器處理

伺服器讀取到命令請求後,會進行一系列的處理。

1.1 讀取命令請求

當客戶端與伺服器之間的套接字因客戶端的寫入變得可讀時,伺服器將呼叫命令請求處理器執行以下操作:

  1. 讀取套接字中的命令請求,並將其儲存到客戶端狀態的輸入緩衝區。
  2. 對輸入緩衝區的命令請求進行分析,提取出命令請求中包含的命令引數及引數個數,然後分別將引數和引數個數儲存到客戶端狀態的 argv 屬性和 argc 屬性裡。
  3. 呼叫命令執行器,執行客戶端指定的命令。

上面的 SET 命令儲存到客戶端狀態的輸入快取區之後,客戶端狀態如圖 4。

圖 4 - 客戶端狀態中的命令請求

之後,分析程式將對輸入緩衝區中的協議進行分析,並將得出的結果儲存的客戶端的 argv 和 argc 屬性中,如圖 5 所示:

圖 5 - 客戶端狀態中的 argv 和 argc 屬性

之後,伺服器將通過呼叫命令執行器來完成執行命令的餘下步驟。

1.2 查詢命令實現

命令執行器要做的第一件事就是根據 argv[0] 引數,在命令表(commandtable)中查詢引數所指定的命令,並將找到的命令儲存到 cmd 屬性中。

命令表是一個字典,字典的鍵是一個個命令名稱,比如 "SET"、"GET" 等。而字典的值則是一個個 redisCommand 結構,每個 redisCommand 結構記錄了 Redis 命令的實現資訊。原始碼如下:

# server.h/redisCommand
struct redisCommand {
    char *name;   // 命令名稱。如 "SET"
    redisCommandProc *proc; // 對應函式指標,指向命令的實現函式。比如 SET 對應的 setCommand 函式
    int arity;    // 命令引數的格個數。用來檢查命令請求的格式是否合法。
                        // 要注意的命令的名稱也是一個引數。像我們上面的 SET KEY VALUE 命令,實際上有三個引數。
    char *sflags; // 字串形式的標識值。記錄了命令的屬性。
    int flags;    // 對 sflags 標識分析得出的二進位制標識,由程式自動生成。檢查命令時,實際上使用的是此欄位
    redisGetKeysProc *getkeys_proc; // 指標函式,通過此方法來指定 key 的位置。
    int firstkey; // 第一個 key 的位置
    int lastkey;  // 最後一個 key 的位置
    int keystep;  // key 之間的間距
    long long microseconds, calls; // 命令的總呼叫時間及呼叫次數
};

另外,對於 sflags 屬性,可使用的標識值及含義如下表:

標識 意義 帶有此標識的命令
w 這是一個寫入命令,可能會修改資料庫 SET、RPUSH、DEL 等
r 這是一個只讀命令,不會修改資料庫 GET、STRLEN 等
m 此命令可能會佔用大量記憶體,執行器需先檢查記憶體使用情況,如果記憶體緊缺就禁止執行此命令 SET、APPEND、RPUSH、SADD 等
a 這是一個管理命令 SAVE、BGSAVE 等
p 這是一個釋出與訂閱功能的命令 PUBLISH、SUBSRIBE 等
s 這個命令不可以在 lua 腳步中使用 BPOP、BLPOP 等
R 這是一個隨機命令。對於相同的資料集和相同的引數,返回結果可能不同 SPOP、SRANDMEMBER 等
S 當在 lua 腳步中使用此命令時,對返回結果進行排序,使得結果有序 SINTER、SUNION 等
l 這個命令可以在伺服器載入資料的過程中使用 INFO、PUBLISH 等
t 這個命令允許在從庫有過期資料時使用 SLAVEOF、PING 等
M 這個命令在監視模式下,不會被自動傳播 EXEC
k 叢集模式下,如果對應槽點標記位“匯入”,則接受此命令 restore-asking
F 這個命令在程式執行時應該立刻執行 SETNX、GET 等

命令表結構如圖 6:

圖 6 - 命令表

對於我們上面的 SET KEY VALUE 命令,當程式以圖 5 中的 argv[0] 作為輸入,在命令表中進行查詢時,命令表返回 "set" 鍵對於的 redisCommand 結構,客戶端狀態的 cmd 指標會指向這個 redisCommand 結構。如圖 7 所示:

圖 7 - 設定客戶端狀態的 cmd 指標

要注意的是,對於 Redis 而言,命令名字的大小寫不影響命令表的查詢結果,也就是命令名稱不區分大小寫。執行 SET 和 set、Set 將獲得相同結果。

1.3 執行預備操作

到目前為止,伺服器已經將執行命令所需要的命令實現函式(客戶端 cmd 屬性)、引數(客戶端 argv 屬性)、引數個數(客戶端 argc 屬性)都初始化完畢。但在真正執行命令之前,程式還會進行一些預備操作,保證命令可以正確、順利的被執行。預備操作包括:

  1. 檢查客戶端的 cmd 指標是否指向 NULL,如果是的話,說明使用者輸入的命令名稱沒有對應的函式,伺服器將不再執行後續操作,並向客戶端返回一個錯誤。
  2. 根據客戶端 cmd 屬性指向的 redisCommand 結果的 arity 屬性,檢查命令請求所給定的引數個數是否正確。
  3. 檢查客戶端是否已經通過了身份驗證。未通過身份驗證的客戶端只能執行 AUTH 命令。否則,將會向客戶端返回一個錯誤。
  4. 如果伺服器開啟了 maxmemory 功能,在執行命令之前,會先檢查伺服器的記憶體佔用情況,並在有需要時進行記憶體回收,從而使得接下來的命令可以順利執行。如果記憶體回收失敗,將不再執行後續步驟,向客戶端返回一個錯誤。
  5. 如果伺服器上一次執行 BGSAVE 命令時出錯,並且伺服器開啟了 stop-writes-on-bgsave-error 功能,而將要執行的命令是一個寫命令,那麼伺服器將拒絕執行這個鞋命令,並向客戶端返回一個錯誤。
  6. 如果客戶端正在用 SUBSCRIBEPSUBSCRIBE 命令訂閱頻道或模式,那麼伺服器只會執行客戶端發來的 SUBSCRIBEPSUBSCRIBEUNSUBSCRIBEPUNSUBSCRIBE 四個命令,其它命令都會被拒絕。
  7. 如果伺服器正在進行資料載入,那麼客戶端傳送是命令必須帶有 l 標識才會被伺服器執行。
  8. 如果客戶端正在執行事務,那麼伺服器只會執行 EXECDISCARDMULTIWATCH 四個命令,其他命令都會被放進事務佇列中。
  9. 如果伺服器開啟了監視器功能,那麼伺服器會將要執行的命令和引數等資訊傳送給監視器。

當完成了以上預備操作之後,伺服器就開始真正的執行命令了。

要注意的是,上面列出的預備操作只是伺服器在單機模式下的檢查操作。如果在複製或者叢集模式下,預備操作還會更多。

1.4 呼叫命令的實現函式

在前面的操作中 ,伺服器已經將要執行的命令實現、引數、引數個數儲存在客戶端結構中。

對於我們上面的 SET KEY VALUE 命令,圖 8 包含了命令實現、引數和引數個數結構:

圖 8 - 客戶端狀態

當伺服器決定要執行命令時,只要執行以下語句即可:

// client 是指向客戶端狀態的指標。server.c/call()
client->cmd->proc(client);

上面的執行語句實際上就是呼叫 setCommand 函式(t_string.c)。

被呼叫的命令實現函式會執行指定的操作,併產生相應的命令回覆,這些回覆會被儲存在客戶端狀態的輸出緩衝區中(bug 屬性 和 reply 屬性),之後實現函式會為客戶端的套接字關聯命令回覆處理器,由命令回覆處理器返回給客戶端。

回到我們的示例,setCommand(client) 將產生一個 "+OK\r\n" 回覆,這個回覆被儲存在客戶端的 buf 屬性中。如圖 9 所示:

圖 9 - 儲存了命令回覆的客戶端狀態

1.5 執行後續工作

實現函式執行完後,伺服器還會執行一些後續工作,主要包括:

  1. 如果伺服器開啟了 slow-log 功能,那麼慢查詢日誌模組將會檢查是否需要將剛執行的命令新增到慢查詢日誌。
  2. 更新 redisCommand 結構的 milliseconds 和 calls 屬性。
  3. 如果伺服器開啟了 AOF 持久化功能,那麼 AOF 持久化模組會將剛剛執行的命令請求寫入到 AOF 緩衝區中。
  4. 如果有其它伺服器正在複製當前這個伺服器,那麼伺服器將會把剛剛執行的命令傳播給所有從伺服器。

以上後續操作執行完畢後,一條執行命令也就執行完成了。伺服器可以繼續處理後續的命令。

1.6 將命令回覆傳送給客戶端

上面過程中,命令實現函式會將命令回覆儲存到客戶端的輸出緩衝區中,併為客戶端的套接字關聯命令回覆處理器。當客戶端套接字變為可寫狀態時,伺服器就會執行命令回覆處理器,將命令回覆傳送給客戶端。

當命令回覆傳送完畢後,回覆處理器會情況客戶端的輸出緩衝區,為處理下一個命令請求做好準備。

以圖 9 所示的客戶端狀態為例,當客戶端的套接字變為可寫狀態時,命令回覆處理器會將協議格式的命令回覆 "+OK\r\n" 傳送給客戶端。

1.7 原始碼解讀

命令處理請求,函式呼叫堆疊資訊如圖 3-7-1:

圖 3-7-1:命令執行過程函式堆疊資訊

命令回覆,函式呼叫堆疊資訊如圖 3-7-2:
圖 3-7-2:命令回覆函式堆疊資訊

2 客戶端接收並列印回覆

客戶端接收到命令回覆之後,會將回復轉換成我們可讀的格式,並列印在螢幕上(對於 redis-cli 客戶端),如圖 10 所示。

圖 10 客戶端接收並列印命令回覆的過程

至此,我們走完了從發起一個命令請求,到收到回覆的所有過程。對於我們最開始提的問題,伺服器如何響應客戶端請求,你有答案了嗎?

總結

  1. 伺服器通過 networking.c/readQueryFromClient() 讀取和執行對應命令。
  2. 伺服器通過 networking.c/writeToClient() 將命令回覆傳送給客戶端。

相關文章