一個命令請求從發送到獲得回復的過程中, 客戶端和服務器需要完成一系列操作。
舉個例子, 如果我們使用客戶端執行以下命令:
~~~
redis> SET KEY VALUE
OK
~~~
那么從客戶端發送?`SET?KEY?VALUE`?命令到獲得回復?`OK`?期間, 客戶端和服務器共需要執行以下操作:
1. 客戶端向服務器發送命令請求?`SET?KEY?VALUE`?。
2. 服務器接收并處理客戶端發來的命令請求?`SET?KEY?VALUE`?, 在數據庫中進行設置操作, 并產生命令回復?`OK`?。
3. 服務器將命令回復?`OK`?發送給客戶端。
4. 客戶端接收服務器返回的命令回復?`OK`?, 并將這個回復打印給用戶觀看。
本節接下來的內容將對這些操作的執行細節進行補充, 詳細地說明客戶端和服務器在執行命令請求時所做的各種工作。
## 發送命令請求
Redis 服務器的命令請求來自 Redis 客戶端, 當用戶在客戶端中鍵入一個命令請求時, 客戶端會將這個命令請求轉換成協議格式, 然后通過連接到服務器的套接字, 將協議格式的命令請求發送給服務器, 如圖 14-1 所示。

舉個例子, 假設客戶端執行命令:
~~~
SET KEY VALUE
~~~
那么客戶端會將這個命令轉換成協議:
~~~
*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n
~~~
然后將這段協議內容發送給服務器。
## 讀取命令請求
當客戶端與服務器之間的連接套接字因為客戶端的寫入而變得可讀時, 服務器將調用命令請求處理器來執行以下操作:
1. 讀取套接字中協議格式的命令請求, 并將其保存到客戶端狀態的輸入緩沖區里面。
2. 對輸入緩沖區中的命令請求進行分析, 提取出命令請求中包含的命令參數, 以及命令參數的個數, 然后分別將參數和參數個數保存到客戶端狀態的?`argv`?屬性和?`argc`?屬性里面。
3. 調用命令執行器, 執行客戶端指定的命令。
繼續用上一個小節的?SET?命令為例子, 圖 14-2 展示了程序將命令請求保存到客戶端狀態的輸入緩沖區之后, 客戶端狀態的樣子。

之后, 分析程序將對輸入緩沖區中的協議:
~~~
*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n
~~~
進行分析, 并將得出的分析結果保存到客戶端狀態的?`argv`?屬性和?`argc`?屬性里面, 如圖 14-3 所示。

之后, 服務器將通過調用命令執行器來完成執行命令所需的余下步驟, 以下幾個小節將分別介紹命令執行器所執行的工作。
## 命令執行器(1):查找命令實現
命令執行器要做的第一件事就是根據客戶端狀態的?`argv[0]`?參數, 在命令表(command table)中查找參數所指定的命令, 并將找到的命令保存到客戶端狀態的?`cmd`?屬性里面。
命令表是一個字典, 字典的鍵是一個個命令名字,比如?`"set"`?、?`"get"`?、?`"del"`?,等等; 而字典的值則是一個個?`redisCommand`?結構, 每個`redisCommand`?結構記錄了一個 Redis 命令的實現信息, 表 14-1 記錄了這個結構的各個主要屬性的類型和作用。
* * *
表 14-1?`redisCommand`?結構的主要屬性
| 屬性名 | 類型 | 作用 |
| --- | --- | --- |
| `name` | `char?*` | 命令的名字,比如?`"set"`?。 |
| `proc` | `redisCommandProc?*` | 函數指針,指向命令的實現函數,比如?`setCommand`?。?`redisCommandProc`?類型的定義為`typedef?void?redisCommandProc(redisClient?*c);`?。 |
| `arity` | `int` | 命令參數的個數,用于檢查命令請求的格式是否正確。 如果這個值為負數?`-N`?,那么表示參數的數量大于等于?`N`?。 注意命令的名字本身也是一個參數, 比如說?`SET?msg?"helloworld"`?命令的參數是?`"SET"`?、?`"msg"`?、?`"hello?world"`?, 而不僅僅是?`"msg"`?和?`"helloworld"`?。 |
| `sflags` | `char?*` | 字符串形式的標識值, 這個值記錄了命令的屬性, 比如這個命令是寫命令還是讀命令, 這個命令是否允許在載入數據時使用, 這個命令是否允許在 Lua 腳本中使用, 等等。 |
| `flags` | `int` | 對?`sflags`?標識進行分析得出的二進制標識, 由程序自動生成。 服務器對命令標識進行檢查時使用的都是?`flags`?屬性而不是?`sflags`?屬性, 因為對二進制標識的檢查可以方便地通過?`&`?、?`^`?、?`~`?等操作來完成。 |
| `calls` | `long?long` | 服務器總共執行了多少次這個命令。 |
| `milliseconds` | `long?long` | 服務器執行這個命令所耗費的總時長。 |
* * *
表 14-2 列出了?`sflags`?屬性可以使用的標識值, 以及這些標識的意義。
* * *
表 14-2?`sflags`?屬性的標識
| 標識 | 意義 | 帶有這個標識的命令 |
| --- | --- | --- |
| `w` | 這是一個寫入命令,可能會修改數據庫。 | SET?、?RPUSH?、?DEL?,等等。 |
| `r` | 這是一個只讀命令,不會修改數據庫。 | GET?、?STRLEN?、?EXISTS?,等等。 |
| `m` | 這個命令可能會占用大量內存, 執行之前需要先檢查服務器的內存使用情況, 如果內存緊缺的話就禁止執行這個命令。 | SET?、?APPEND?、?RPUSH?、?LPUSH?、?SADD?、SINTERSTORE?,等等。 |
| `a` | 這是一個管理命令。 | SAVE?、?BGSAVE?、?SHUTDOWN?,等等。 |
| `p` | 這是一個發布與訂閱功能方面的命令。 | PUBLISH?、?SUBSCRIBE?、?PUBSUB?,等等。 |
| `s` | 這個命令不可以在 Lua 腳本中使用。 | BRPOP?、?BLPOP?、?BRPOPLPUSH?、?SPOP?,等等。 |
| `R` | 這是一個隨機命令, 對于相同的數據集和相同的參數, 命令返回的結果可能不同。 | SPOP?、?SRANDMEMBER?、?SSCAN?、?RANDOMKEY?,等等。 |
| `S` | 當在 Lua 腳本中使用這個命令時, 對這個命令的輸出結果進行一次排序, 使得命令的結果有序。 | SINTER?、?SUNION?、?SDIFF?、?SMEMBERS?、KEYS?,等等。 |
| `l` | 這個命令可以在服務器載入數據的過程中使用。 | INFO?、?SHUTDOWN?、?PUBLISH?,等等。 |
| `t` | 這是一個允許從服務器在帶有過期數據時使用的命令。 | SLAVEOF?、?PING?、?INFO?,等等。 |
| `M` | 這個命令在監視器(monitor)模式下不會自動被傳播(propagate)。 | EXEC |
* * *
圖 14-4 展示了命令表的樣子, 并且以?SET?命令和?GET?命令作為例子, 展示了?`redisCommand`?結構:
* SET?命令的名字為?`"set"`?, 實現函數為?`setCommand`?; 命令的參數個數為?`-3`?, 表示命令接受三個或以上數量的參數; 命令的標識為`"wm"`?, 表示?SET?命令是一個寫入命令, 并且在執行這個命令之前, 服務器應該對占用內存狀況進行檢查, 因為這個命令可能會占用大量內存。
* GET?命令的名字為?`"get"`?, 實現函數為?`getCommand`?函數; 命令的參數個數為?`2`?, 表示命令只接受兩個參數; 命令的標識為?`"r"`?, 表示這是一個只讀命令。

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

命令名字的大小寫不影響命令表的查找結果
因為命令表使用的是大小寫無關的查找算法, 無論輸入的命令名字是大寫、小寫或者混合大小寫, 只要命令的名字是正確的, 就能找到相應的?`redisCommand`?結構。
比如說, 無論用戶輸入的命令名字是?`"SET"`?、?`"set"`?、?`"SeT"`?又或者?`"sEt"`?, 命令表返回的都是同一個?`redisCommand`?結構。
這也是 Redis 客戶端可以發送不同大小寫的命令, 并且獲得相同執行結果的原因:
~~~
# 以下四個命令的執行效果完全一樣
redis> SET msg "hello world"
OK
redis> set msg "hello world"
OK
redis> SeT msg "hello world"
OK
redis> sEt msg "hello world"
OK
~~~
## 命令執行器(2):執行預備操作
到目前為止, 服務器已經將執行命令所需的命令實現函數(保存在客戶端狀態的?`cmd`?屬性)、參數(保存在客戶端狀態的?`argv`?屬性)、參數個數(保存在客戶端狀態的?`argc`?屬性)都收集齊了, 但是在真正執行命令之前, 程序還需要進行一些預備操作, 從而確保命令可以正確、順利地被執行, 這些操作包括:
* 檢查客戶端狀態的?`cmd`?指針是否指向?`NULL`?, 如果是的話, 那么說明用戶輸入的命令名字找不到相應的命令實現, 服務器不再執行后續步驟, 并向客戶端返回一個錯誤。
* 根據客戶端?`cmd`?屬性指向的?`redisCommand`?結構的?`arity`?屬性, 檢查命令請求所給定的參數個數是否正確, 當參數個數不正確時, 不再執行后續步驟, 直接向客戶端返回一個錯誤。 比如說, 如果?`redisCommand`?結構的?`arity`?屬性的值為?`-3`?, 那么用戶輸入的命令參數個數必須大于等于?`3`?個才行。
* 檢查客戶端是否已經通過了身份驗證, 未通過身份驗證的客戶端只能執行?AUTH?命令, 如果未通過身份驗證的客戶端試圖執行除?AUTH命令之外的其他命令, 那么服務器將向客戶端返回一個錯誤。
* 如果服務器打開了?`maxmemory`?功能, 那么在執行命令之前, 先檢查服務器的內存占用情況, 并在有需要時進行內存回收, 從而使得接下來的命令可以順利執行。 如果內存回收失敗, 那么不再執行后續步驟, 向客戶端返回一個錯誤。
* 如果服務器上一次執行?BGSAVE?命令時出錯, 并且服務器打開了?`stop-writes-on-bgsave-error`?功能, 而且服務器即將要執行的命令是一個寫命令, 那么服務器將拒絕執行這個命令, 并向客戶端返回一個錯誤。
* 如果客戶端當前正在用?SUBSCRIBE?命令訂閱頻道, 或者正在用?PSUBSCRIBE?命令訂閱模式, 那么服務器只會執行客戶端發來的SUBSCRIBE?、?PSUBSCRIBE?、?UNSUBSCRIBE?、?PUNSUBSCRIBE?四個命令, 其他別的命令都會被服務器拒絕。
* 如果服務器正在進行數據載入, 那么客戶端發送的命令必須帶有?`l`?標識(比如?INFO?、?SHUTDOWN?、?PUBLISH?,等等)才會被服務器執行, 其他別的命令都會被服務器拒絕。
* 如果服務器因為執行 Lua 腳本而超時并進入阻塞狀態, 那么服務器只會執行客戶端發來的?SHUTDOWN nosave?命令和?SCRIPT KILL?命令, 其他別的命令都會被服務器拒絕。
* 如果客戶端正在執行事務, 那么服務器只會執行客戶端發來的?EXEC?、?DISCARD?、?MULTI?、?WATCH?四個命令, 其他命令都會被放進事務隊列中。
* 如果服務器打開了監視器功能, 那么服務器會將要執行的命令和參數等信息發送給監視器。
當完成了以上預備操作之后, 服務器就可以開始真正執行命令了。
注意
以上只列出了服務器在單機模式下執行命令時的檢查操作, 當服務器在復制或者集群模式下執行命令時, 預備操作還會更多一些。
## 命令執行器(3):調用命令的實現函數
在前面的操作中, 服務器已經將要執行命令的實現保存到了客戶端狀態的?`cmd`?屬性里面, 并將命令的參數和參數個數分別保存到了客戶端狀態的?`argv`?屬性和?`argc`?屬性里面, 當服務器決定要執行命令時, 它只要執行以下語句就可以了:
~~~
// client 是指向客戶端狀態的指針
client->cmd->proc(client);
~~~
因為執行命令所需的實際參數都已經保存到客戶端狀態的?`argv`?屬性里面了, 所以命令的實現函數只需要一個指向客戶端狀態的指針作為參數即可。
繼續以之前的?SET?命令為例子, 圖 14-6 展示了客戶端包含了命令實現、參數和參數個數的樣子。

對于這個例子來說, 執行語句:
~~~
client->cmd->proc(client);
~~~
等于執行語句:
~~~
setCommand(client);
~~~
被調用的命令實現函數會執行指定的操作, 并產生相應的命令回復, 這些回復會被保存在客戶端狀態的輸出緩沖區里面(`buf`?屬性和?`reply`屬性), 之后實現函數還會為客戶端的套接字關聯命令回復處理器, 這個處理器負責將命令回復返回給客戶端。
對于前面?SET?命令的例子來說, 函數調用?`setCommand(client);`?將產生一個?`"+OK\r\n"`?回復, 這個回復會被保存到客戶端狀態的?`buf`?屬性里面, 如圖 14-7 所示。

## 命令執行器(4):執行后續工作
在執行完實現函數之后, 服務器還需要執行一些后續工作:
* 如果服務器開啟了慢查詢日志功能, 那么慢查詢日志模塊會檢查是否需要為剛剛執行完的命令請求添加一條新的慢查詢日志。
* 根據剛剛執行命令所耗費的時長, 更新被執行命令的?`redisCommand`?結構的?`milliseconds`?屬性, 并將命令的?`redisCommand`?結構的?`calls`計數器的值增一。
* 如果服務器開啟了 AOF 持久化功能, 那么 AOF 持久化模塊會將剛剛執行的命令請求寫入到 AOF 緩沖區里面。
* 如果有其他從服務器正在復制當前這個服務器, 那么服務器會將剛剛執行的命令傳播給所有從服務器。
當以上操作都執行完了之后, 服務器對于當前命令的執行到此就告一段落了, 之后服務器就可以繼續從文件事件處理器中取出并處理下一個命令請求了。
## 將命令回復發送給客戶端
前面說過, 命令實現函數會將命令回復保存到客戶端的輸出緩沖區里面, 并為客戶端的套接字關聯命令回復處理器, 當客戶端套接字變為可寫狀態時, 服務器就會執行命令回復處理器, 將保存在客戶端輸出緩沖區中的命令回復發送給客戶端。
當命令回復發送完畢之后, 回復處理器會清空客戶端狀態的輸出緩沖區, 為處理下一個命令請求做好準備。
以圖 14-7 所示的客戶端狀態為例子, 當客戶端的套接字變為可寫狀態時, 命令回復處理器會將協議格式的命令回復?`"+OK\r\n"`?發送給客戶端。
## 客戶端接收并打印命令回復
當客戶端接收到協議格式的命令回復之后, 它會將這些回復轉換成人類可讀的格式, 并打印給用戶觀看(假設我們使用的是 Redis 自帶的`redis-cli`?客戶端), 如圖 14-8 所示。

繼續以之前的?SET?命令為例子, 當客戶端接到服務器發來的?`"+OK\r\n"`?協議回復時, 它會將這個回復轉換成?`"OK\n"`?, 然后打印給用戶看:
~~~
redis> SET KEY VALUE
OK
~~~
以上就是 Redis 客戶端和服務器執行命令請求的整個過程了。
- 介紹
- 前言
- 致謝
- 簡介
- 第一部分:數據結構與對象
- 簡單動態字符串
- SDS 的定義
- SDS 與 C 字符串的區別
- SDS API
- 重點回顧
- 參考資料
- 鏈表
- 鏈表和鏈表節點的實現
- 鏈表和鏈表節點的 API
- 重點回顧
- 字典
- 字典的實現
- 哈希算法
- 解決鍵沖突
- rehash
- 漸進式 rehash
- 字典 API
- 重點回顧
- 跳躍表
- 跳躍表的實現
- 跳躍表 API
- 重點回顧
- 整數集合
- 整數集合的實現
- 升級
- 升級的好處
- 降級
- 整數集合 API
- 重點回顧
- 壓縮列表
- 壓縮列表的構成
- 壓縮列表節點的構成
- 連鎖更新
- 壓縮列表 API
- 重點回顧
- 對象
- 對象的類型與編碼
- 字符串對象
- 列表對象
- 哈希對象
- 集合對象
- 有序集合對象
- 類型檢查與命令多態
- 內存回收
- 對象共享
- 對象的空轉時長
- 重點回顧
- 第二部分:單機數據庫的實現
- 數據庫
- 數據庫鍵空間
- 重點回顧
- RDB 持久化
- RDB 文件結構
- 重點回顧
- AOF 持久化
- AOF 持久化的實現
- 重點回顧
- 事件
- 文件事件
- 重點回顧
- 參考資料
- 客戶端
- 客戶端屬性
- 重點回顧
- 服務器
- 命令請求的執行過程
- 重點回顧
- 第三部分:多機數據庫的實現
- 復制
- 舊版復制功能的實現
- 重點回顧
- Sentinel
- 啟動并初始化 Sentinel
- 重點回顧
- 參考資料
- 集群
- 節點
- 重點回顧
- 第四部分:獨立功能的實現
- 發布與訂閱
- 頻道的訂閱與退訂
- 重點回顧
- 參考資料
- 事務
- 事務的實現
- 重點回顧
- Lua 腳本
- 創建并修改 Lua 環境
- 重點回顧
- 排序
- SORT <key> 命令的實現
- 重點回顧
- 二進制位數組
- GETBIT 命令的實現
- 重點回顧
- 慢查詢日志
- 慢查詢記錄的保存
- 慢查詢日志的閱覽和刪除
- 添加新日志
- 重點回顧
- 監視器
- 成為監視器
- 向監視器發送命令信息
- 重點回顧
- 源碼、相關資源和勘誤