跟着大彬讀源碼 - Redis 3 - 服務器如何響應客戶端請求?(下)

繼續咱們上一節的討論。服務器啓動了,客戶端也發送命令了。接下來,就要到服務器「表演」的時刻了。數據庫

1 服務器處理

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

1.1 讀取命令請求

當客戶端與服務器之間的套接字因客戶端的寫入變得可讀時,服務器將調用命令請求處理器執行如下操做:服務器

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

上面的 SET 命令保存到客戶端狀態的輸入緩存區以後,客戶端狀態如圖 4。函數

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

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

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

以後,服務器將經過調用命令執行器來完成執行命令的餘下步驟。3d

1.2 查找命令實現

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

命令表是一個字典,字典的鍵是一個個命令名稱,好比 "SET"、"GET" 等。而字典的值則是一個個 redisCommand 結構,每一個 redisCommand 結構記錄了 Redis 命令的實現信息。源碼以下:rest

# 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() 將命令回覆發送給客戶端。
相關文章
相關標籤/搜索