Redis 命令請求的執(zhí)行過程

2018-08-02 14:54 更新

一個命令請求從發(fā)送到獲得回復的過程中, 客戶端和服務器需要完成一系列操作。

舉個例子, 如果我們使用客戶端執(zhí)行以下命令:

redis> SET KEY VALUE
OK

那么從客戶端發(fā)送 SET KEY VALUE 命令到獲得回復 OK 期間, 客戶端和服務器共需要執(zhí)行以下操作:

  1. 客戶端向服務器發(fā)送命令請求 SET KEY VALUE 。
  2. 服務器接收并處理客戶端發(fā)來的命令請求 SET KEY VALUE , 在數(shù)據(jù)庫中進行設(shè)置操作, 并產(chǎn)生命令回復 OK 。
  3. 服務器將命令回復 OK 發(fā)送給客戶端。
  4. 客戶端接收服務器返回的命令回復 OK , 并將這個回復打印給用戶觀看。

本節(jié)接下來的內(nèi)容將對這些操作的執(zhí)行細節(jié)進行補充, 詳細地說明客戶端和服務器在執(zhí)行命令請求時所做的各種工作。

發(fā)送命令請求

Redis 服務器的命令請求來自 Redis 客戶端, 當用戶在客戶端中鍵入一個命令請求時, 客戶端會將這個命令請求轉(zhuǎn)換成協(xié)議格式, 然后通過連接到服務器的套接字, 將協(xié)議格式的命令請求發(fā)送給服務器, 如圖 14-1 所示。

舉個例子, 假設(shè)客戶端執(zhí)行命令:

SET KEY VALUE

那么客戶端會將這個命令轉(zhuǎn)換成協(xié)議:

*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n

然后將這段協(xié)議內(nèi)容發(fā)送給服務器。

讀取命令請求

當客戶端與服務器之間的連接套接字因為客戶端的寫入而變得可讀時, 服務器將調(diào)用命令請求處理器來執(zhí)行以下操作:

  1. 讀取套接字中協(xié)議格式的命令請求, 并將其保存到客戶端狀態(tài)的輸入緩沖區(qū)里面。
  2. 對輸入緩沖區(qū)中的命令請求進行分析, 提取出命令請求中包含的命令參數(shù), 以及命令參數(shù)的個數(shù), 然后分別將參數(shù)和參數(shù)個數(shù)保存到客戶端狀態(tài)的 argv 屬性和 argc 屬性里面。
  3. 調(diào)用命令執(zhí)行器, 執(zhí)行客戶端指定的命令。

繼續(xù)用上一個小節(jié)的 SET 命令為例子, 圖 14-2 展示了程序?qū)⒚钫埱蟊4娴娇蛻舳藸顟B(tài)的輸入緩沖區(qū)之后, 客戶端狀態(tài)的樣子。

之后, 分析程序?qū)斎刖彌_區(qū)中的協(xié)議:

*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n

進行分析, 并將得出的分析結(jié)果保存到客戶端狀態(tài)的 argv 屬性和 argc 屬性里面, 如圖 14-3 所示。

之后, 服務器將通過調(diào)用命令執(zhí)行器來完成執(zhí)行命令所需的余下步驟, 以下幾個小節(jié)將分別介紹命令執(zhí)行器所執(zhí)行的工作。

命令執(zhí)行器(1):查找命令實現(xiàn)

命令執(zhí)行器要做的第一件事就是根據(jù)客戶端狀態(tài)的 argv[0] 參數(shù), 在命令表(command table)中查找參數(shù)所指定的命令, 并將找到的命令保存到客戶端狀態(tài)的 cmd 屬性里面。

命令表是一個字典, 字典的鍵是一個個命令名字,比如 "set" 、 "get" 、 "del" ,等等; 而字典的值則是一個個 redisCommand 結(jié)構(gòu), 每個redisCommand 結(jié)構(gòu)記錄了一個 Redis 命令的實現(xiàn)信息, 表 14-1 記錄了這個結(jié)構(gòu)的各個主要屬性的類型和作用。


表 14-1 redisCommand 結(jié)構(gòu)的主要屬性

屬性名 類型 作用
name char * 命令的名字,比如 "set" 。
proc redisCommandProc * 函數(shù)指針,指向命令的實現(xiàn)函數(shù),比如 setCommand 。 redisCommandProc 類型的定義為typedef void redisCommandProc(redisClient *c); 。
arity int 命令參數(shù)的個數(shù),用于檢查命令請求的格式是否正確。 如果這個值為負數(shù) -N ,那么表示參數(shù)的數(shù)量大于等于 N 。 注意命令的名字本身也是一個參數(shù), 比如說 SET msg "helloworld" 命令的參數(shù)是 "SET" 、 "msg" 、 "hello world" , 而不僅僅是 "msg" 和 "helloworld" 。
sflags char * 字符串形式的標識值, 這個值記錄了命令的屬性, 比如這個命令是寫命令還是讀命令, 這個命令是否允許在載入數(shù)據(jù)時使用, 這個命令是否允許在 Lua 腳本中使用, 等等。
flags int 對 sflags 標識進行分析得出的二進制標識, 由程序自動生成。 服務器對命令標識進行檢查時使用的都是 flags 屬性而不是 sflags 屬性, 因為對二進制標識的檢查可以方便地通過 & 、 ^ 、 ~ 等操作來完成。
calls long long 服務器總共執(zhí)行了多少次這個命令。
milliseconds long long 服務器執(zhí)行這個命令所耗費的總時長。

表 14-2 列出了 sflags 屬性可以使用的標識值, 以及這些標識的意義。


表 14-2 sflags 屬性的標識

標識 意義 帶有這個標識的命令
w 這是一個寫入命令,可能會修改數(shù)據(jù)庫。 SET 、 RPUSH 、 DEL ,等等。
r 這是一個只讀命令,不會修改數(shù)據(jù)庫。 GET 、 STRLEN 、 EXISTS ,等等。
m 這個命令可能會占用大量內(nèi)存, 執(zhí)行之前需要先檢查服務器的內(nèi)存使用情況, 如果內(nèi)存緊缺的話就禁止執(zhí)行這個命令。 SET 、 APPEND 、 RPUSH 、 LPUSH 、 SADD 、SINTERSTORE ,等等。
a 這是一個管理命令。 SAVE 、 BGSAVE 、 SHUTDOWN ,等等。
p 這是一個發(fā)布與訂閱功能方面的命令。 PUBLISH 、 SUBSCRIBE 、 PUBSUB ,等等。
s 這個命令不可以在 Lua 腳本中使用。 BRPOP 、 BLPOP 、 BRPOPLPUSH 、 SPOP ,等等。
R 這是一個隨機命令, 對于相同的數(shù)據(jù)集和相同的參數(shù), 命令返回的結(jié)果可能不同。 SPOP 、 SRANDMEMBER 、 SSCAN 、 RANDOMKEY ,等等。
S 當在 Lua 腳本中使用這個命令時, 對這個命令的輸出結(jié)果進行一次排序, 使得命令的結(jié)果有序。 SINTER 、 SUNION 、 SDIFF 、 SMEMBERS 、KEYS ,等等。
l 這個命令可以在服務器載入數(shù)據(jù)的過程中使用。 INFO 、 SHUTDOWN 、 PUBLISH ,等等。
t 這是一個允許從服務器在帶有過期數(shù)據(jù)時使用的命令。 SLAVEOF 、 PING 、 INFO ,等等。
M 這個命令在監(jiān)視器(monitor)模式下不會自動被傳播(propagate)。 EXEC

圖 14-4 展示了命令表的樣子, 并且以 SET 命令和 GET 命令作為例子, 展示了 redisCommand 結(jié)構(gòu):

  • SET 命令的名字為 "set" , 實現(xiàn)函數(shù)為 setCommand ; 命令的參數(shù)個數(shù)為 -3 , 表示命令接受三個或以上數(shù)量的參數(shù); 命令的標識為"wm" , 表示 SET 命令是一個寫入命令, 并且在執(zhí)行這個命令之前, 服務器應該對占用內(nèi)存狀況進行檢查, 因為這個命令可能會占用大量內(nèi)存。
  • GET 命令的名字為 "get" , 實現(xiàn)函數(shù)為 getCommand 函數(shù); 命令的參數(shù)個數(shù)為 2 , 表示命令只接受兩個參數(shù); 命令的標識為 "r" , 表示這是一個只讀命令。

繼續(xù)之前 SET 命令的例子, 當程序以圖 14-3 中的 argv[0] 作為輸入, 在命令表中進行查找時, 命令表將返回 "set" 鍵所對應的redisCommand 結(jié)構(gòu), 客戶端狀態(tài)的 cmd 指針會指向這個 redisCommand 結(jié)構(gòu), 如圖 14-5 所示。

命令名字的大小寫不影響命令表的查找結(jié)果

因為命令表使用的是大小寫無關(guān)的查找算法, 無論輸入的命令名字是大寫、小寫或者混合大小寫, 只要命令的名字是正確的, 就能找到相應的 redisCommand 結(jié)構(gòu)。

比如說, 無論用戶輸入的命令名字是 "SET" 、 "set" 、 "SeT" 又或者 "sEt" , 命令表返回的都是同一個 redisCommand 結(jié)構(gòu)。

這也是 Redis 客戶端可以發(fā)送不同大小寫的命令, 并且獲得相同執(zhí)行結(jié)果的原因:

# 以下四個命令的執(zhí)行效果完全一樣

redis> SET msg "hello world"
OK

redis> set msg "hello world"
OK

redis> SeT msg "hello world"
OK

redis> sEt msg "hello world"
OK

命令執(zhí)行器(2):執(zhí)行預備操作

到目前為止, 服務器已經(jīng)將執(zhí)行命令所需的命令實現(xiàn)函數(shù)(保存在客戶端狀態(tài)的 cmd 屬性)、參數(shù)(保存在客戶端狀態(tài)的 argv 屬性)、參數(shù)個數(shù)(保存在客戶端狀態(tài)的 argc 屬性)都收集齊了, 但是在真正執(zhí)行命令之前, 程序還需要進行一些預備操作, 從而確保命令可以正確、順利地被執(zhí)行, 這些操作包括:

  • 檢查客戶端狀態(tài)的 cmd 指針是否指向 NULL , 如果是的話, 那么說明用戶輸入的命令名字找不到相應的命令實現(xiàn), 服務器不再執(zhí)行后續(xù)步驟, 并向客戶端返回一個錯誤。
  • 根據(jù)客戶端 cmd 屬性指向的 redisCommand 結(jié)構(gòu)的 arity 屬性, 檢查命令請求所給定的參數(shù)個數(shù)是否正確, 當參數(shù)個數(shù)不正確時, 不再執(zhí)行后續(xù)步驟, 直接向客戶端返回一個錯誤。 比如說, 如果 redisCommand 結(jié)構(gòu)的 arity 屬性的值為 -3 , 那么用戶輸入的命令參數(shù)個數(shù)必須大于等于 3 個才行。
  • 檢查客戶端是否已經(jīng)通過了身份驗證, 未通過身份驗證的客戶端只能執(zhí)行 AUTH 命令, 如果未通過身份驗證的客戶端試圖執(zhí)行除 AUTH命令之外的其他命令, 那么服務器將向客戶端返回一個錯誤。
  • 如果服務器打開了 maxmemory 功能, 那么在執(zhí)行命令之前, 先檢查服務器的內(nèi)存占用情況, 并在有需要時進行內(nèi)存回收, 從而使得接下來的命令可以順利執(zhí)行。 如果內(nèi)存回收失敗, 那么不再執(zhí)行后續(xù)步驟, 向客戶端返回一個錯誤。
  • 如果服務器上一次執(zhí)行 BGSAVE 命令時出錯, 并且服務器打開了 stop-writes-on-bgsave-error 功能, 而且服務器即將要執(zhí)行的命令是一個寫命令, 那么服務器將拒絕執(zhí)行這個命令, 并向客戶端返回一個錯誤。
  • 如果客戶端當前正在用 SUBSCRIBE 命令訂閱頻道, 或者正在用 PSUBSCRIBE 命令訂閱模式, 那么服務器只會執(zhí)行客戶端發(fā)來的SUBSCRIBE 、 PSUBSCRIBE 、 UNSUBSCRIBE 、 PUNSUBSCRIBE 四個命令, 其他別的命令都會被服務器拒絕。
  • 如果服務器正在進行數(shù)據(jù)載入, 那么客戶端發(fā)送的命令必須帶有 l 標識(比如 INFO 、 SHUTDOWN 、 PUBLISH ,等等)才會被服務器執(zhí)行, 其他別的命令都會被服務器拒絕。
  • 如果服務器因為執(zhí)行 Lua 腳本而超時并進入阻塞狀態(tài), 那么服務器只會執(zhí)行客戶端發(fā)來的 SHUTDOWN nosave 命令和 SCRIPT KILL 命令, 其他別的命令都會被服務器拒絕。
  • 如果客戶端正在執(zhí)行事務, 那么服務器只會執(zhí)行客戶端發(fā)來的 EXEC 、 DISCARD 、 MULTI 、 WATCH 四個命令, 其他命令都會被放進事務隊列中。
  • 如果服務器打開了監(jiān)視器功能, 那么服務器會將要執(zhí)行的命令和參數(shù)等信息發(fā)送給監(jiān)視器。

當完成了以上預備操作之后, 服務器就可以開始真正執(zhí)行命令了。

注意

以上只列出了服務器在單機模式下執(zhí)行命令時的檢查操作, 當服務器在復制或者集群模式下執(zhí)行命令時, 預備操作還會更多一些。

命令執(zhí)行器(3):調(diào)用命令的實現(xiàn)函數(shù)

在前面的操作中, 服務器已經(jīng)將要執(zhí)行命令的實現(xiàn)保存到了客戶端狀態(tài)的 cmd 屬性里面, 并將命令的參數(shù)和參數(shù)個數(shù)分別保存到了客戶端狀態(tài)的 argv 屬性和 argc 屬性里面, 當服務器決定要執(zhí)行命令時, 它只要執(zhí)行以下語句就可以了:

// client 是指向客戶端狀態(tài)的指針

client->cmd->proc(client);

因為執(zhí)行命令所需的實際參數(shù)都已經(jīng)保存到客戶端狀態(tài)的 argv 屬性里面了, 所以命令的實現(xiàn)函數(shù)只需要一個指向客戶端狀態(tài)的指針作為參數(shù)即可。

繼續(xù)以之前的 SET 命令為例子, 圖 14-6 展示了客戶端包含了命令實現(xiàn)、參數(shù)和參數(shù)個數(shù)的樣子。

對于這個例子來說, 執(zhí)行語句:

client->cmd->proc(client);

等于執(zhí)行語句:

setCommand(client);

被調(diào)用的命令實現(xiàn)函數(shù)會執(zhí)行指定的操作, 并產(chǎn)生相應的命令回復, 這些回復會被保存在客戶端狀態(tài)的輸出緩沖區(qū)里面(buf 屬性和 reply屬性), 之后實現(xiàn)函數(shù)還會為客戶端的套接字關(guān)聯(lián)命令回復處理器, 這個處理器負責將命令回復返回給客戶端。

對于前面 SET 命令的例子來說, 函數(shù)調(diào)用 setCommand(client); 將產(chǎn)生一個 "+OK\r\n" 回復, 這個回復會被保存到客戶端狀態(tài)的 buf 屬性里面, 如圖 14-7 所示。

命令執(zhí)行器(4):執(zhí)行后續(xù)工作

在執(zhí)行完實現(xiàn)函數(shù)之后, 服務器還需要執(zhí)行一些后續(xù)工作:

  • 如果服務器開啟了慢查詢?nèi)罩竟δ埽?那么慢查詢?nèi)罩灸K會檢查是否需要為剛剛執(zhí)行完的命令請求添加一條新的慢查詢?nèi)罩尽?/li>
  • 根據(jù)剛剛執(zhí)行命令所耗費的時長, 更新被執(zhí)行命令的 redisCommand 結(jié)構(gòu)的 milliseconds 屬性, 并將命令的 redisCommand 結(jié)構(gòu)的 calls計數(shù)器的值增一。
  • 如果服務器開啟了 AOF 持久化功能, 那么 AOF 持久化模塊會將剛剛執(zhí)行的命令請求寫入到 AOF 緩沖區(qū)里面。
  • 如果有其他從服務器正在復制當前這個服務器, 那么服務器會將剛剛執(zhí)行的命令傳播給所有從服務器。

當以上操作都執(zhí)行完了之后, 服務器對于當前命令的執(zhí)行到此就告一段落了, 之后服務器就可以繼續(xù)從文件事件處理器中取出并處理下一個命令請求了。

將命令回復發(fā)送給客戶端

前面說過, 命令實現(xiàn)函數(shù)會將命令回復保存到客戶端的輸出緩沖區(qū)里面, 并為客戶端的套接字關(guān)聯(lián)命令回復處理器, 當客戶端套接字變?yōu)榭蓪憼顟B(tài)時, 服務器就會執(zhí)行命令回復處理器, 將保存在客戶端輸出緩沖區(qū)中的命令回復發(fā)送給客戶端。

當命令回復發(fā)送完畢之后, 回復處理器會清空客戶端狀態(tài)的輸出緩沖區(qū), 為處理下一個命令請求做好準備。

以圖 14-7 所示的客戶端狀態(tài)為例子, 當客戶端的套接字變?yōu)榭蓪憼顟B(tài)時, 命令回復處理器會將協(xié)議格式的命令回復 "+OK\r\n" 發(fā)送給客戶端。

客戶端接收并打印命令回復

當客戶端接收到協(xié)議格式的命令回復之后, 它會將這些回復轉(zhuǎn)換成人類可讀的格式, 并打印給用戶觀看(假設(shè)我們使用的是 Redis 自帶的redis-cli 客戶端), 如圖 14-8 所示。

繼續(xù)以之前的 SET 命令為例子, 當客戶端接到服務器發(fā)來的 "+OK\r\n" 協(xié)議回復時, 它會將這個回復轉(zhuǎn)換成 "OK\n" , 然后打印給用戶看:

redis> SET KEY VALUE
OK

以上就是 Redis 客戶端和服務器執(zhí)行命令請求的整個過程了。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號