客戶端狀態包含的屬性可以分為兩類:
* 一類是比較通用的屬性, 這些屬性很少與特定功能相關, 無論客戶端執行的是什么工作, 它們都要用到這些屬性。
* 另外一類是和特定功能相關的屬性, 比如操作數據庫時需要用到的?`db`?屬性和?`dictid`?屬性, 執行事務時需要用到的?`mstate`?屬性, 以及執行?WATCH?命令時需要用到的?`watched_keys`?屬性, 等等。
本章將對客戶端狀態中比較通用的那部分屬性進行介紹, 至于那些和特定功能相關的屬性, 則會在相應的章節進行介紹。
## 套接字描述符
客戶端狀態的?`fd`?屬性記錄了客戶端正在使用的套接字描述符:
~~~
typedef struct redisClient {
// ...
int fd;
// ...
} redisClient;
~~~
根據客戶端類型的不同,?`fd`?屬性的值可以是?`-1`?或者是大于?`-1`?的整數:
* 偽客戶端(fake client)的?`fd`?屬性的值為?`-1`?: 偽客戶端處理的命令請求來源于 AOF 文件或者 Lua 腳本, 而不是網絡, 所以這種客戶端不需要套接字連接, 自然也不需要記錄套接字描述符。 目前 Redis 服務器會在兩個地方用到偽客戶端, 一個用于載入 AOF 文件并還原數據庫狀態, 而另一個則用于執行 Lua 腳本中包含的 Redis 命令。
* 普通客戶端的?`fd`?屬性的值為大于?`-1`?的整數: 普通客戶端使用套接字來與服務器進行通訊, 所以服務器會用?`fd`?屬性來記錄客戶端套接字的描述符。 因為合法的套接字描述符不能是?`-1`?, 所以普通客戶端的套接字描述符的值必然是大于?`-1`?的整數。
執行?CLIENT_LIST?命令可以列出目前所有連接到服務器的普通客戶端, 命令輸出中的?`fd`?域顯示了服務器連接客戶端所使用的套接字描述符:
~~~
redis> CLIENT list
addr=127.0.0.1:53428 fd=6 name= age=1242 idle=0 ...
addr=127.0.0.1:53469 fd=7 name= age=4 idle=4 ...
~~~
## 名字
在默認情況下, 一個連接到服務器的客戶端是沒有名字的。
比如在下面展示的?CLIENT_LIST?命令示例中, 兩個客戶端的?`name`?域都是空白的:
~~~
redis> CLIENT list
addr=127.0.0.1:53428 fd=6 name= age=1242 idle=0 ...
addr=127.0.0.1:53469 fd=7 name= age=4 idle=4 ...
~~~
使用?CLIENT_SETNAME?命令可以為客戶端設置一個名字, 讓客戶端的身份變得更清晰。
以下展示的是客戶端執行?CLIENT_SETNAME?命令之后的客戶端列表:
~~~
redis> CLIENT list
addr=127.0.0.1:53428 fd=6 name=message_queue age=2093 idle=0 ...
addr=127.0.0.1:53469 fd=7 name=user_relationship age=855 idle=2 ...
~~~
其中, 第一個客戶端的名字是?`message_queue`?, 我們可以猜測它是負責處理消息隊列的客戶端; 第二個客戶端的名字是?`user_relationship`?, 我們可以猜測它為負責處理用戶關系的客戶端。
客戶端的名字記錄在客戶端狀態的?`name`?屬性里面:
~~~
typedef struct redisClient {
// ...
robj *name;
// ...
} redisClient;
~~~
如果客戶端沒有為自己設置名字, 那么相應客戶端狀態的?`name`?屬性指向?`NULL`?指針; 相反地, 如果客戶端為自己設置了名字, 那么?`name`屬性將指向一個字符串對象, 而該對象就保存著客戶端的名字。
圖 13-3 展示了一個客戶端狀態示例, 根據?`name`?屬性顯示, 客戶端的名字為?`"message_queue"`?。

## 標志
客戶端的標志屬性?`flags`?記錄了客戶端的角色(role), 以及客戶端目前所處的狀態:
~~~
typedef struct redisClient {
// ...
int flags;
// ...
} redisClient;
~~~
`flags`?屬性的值可以是單個標志:
~~~
flags = <flag>
~~~
也可以是多個標志的二進制或, 比如:
~~~
flags = <flag1> | <flag2> | ...
~~~
每個標志使用一個常量表示, 一部分標志記錄了客戶端的角色:
* 在主從服務器進行復制操作時, 主服務器會成為從服務器的客戶端, 而從服務器也會成為主服務器的客戶端。?`REDIS_MASTER`?標志表示客戶端代表的是一個主服務器,?`REDIS_SLAVE`?標志表示客戶端代表的是一個從服務器。
* `REDIS_PRE_PSYNC`?標志表示客戶端代表的是一個版本低于 Redis 2.8 的從服務器, 主服務器不能使用?PSYNC?命令與這個從服務器進行同步。 這個標志只能在?`REDIS_SLAVE`?標志處于打開狀態時使用。
* `REDIS_LUA_CLIENT`?標識表示客戶端是專門用于處理 Lua 腳本里面包含的 Redis 命令的偽客戶端。
而另外一部分標志則記錄了客戶端目前所處的狀態:
* `REDIS_MONITOR`?標志表示客戶端正在執行?MONITOR?命令。
* `REDIS_UNIX_SOCKET`?標志表示服務器使用 UNIX 套接字來連接客戶端。
* `REDIS_BLOCKED`?標志表示客戶端正在被?BRPOP?、?BLPOP?等命令阻塞。
* `REDIS_UNBLOCKED`?標志表示客戶端已經從?`REDIS_BLOCKED`?標志所表示的阻塞狀態中脫離出來, 不再阻塞。?`REDIS_UNBLOCKED`?標志只能在`REDIS_BLOCKED`?標志已經打開的情況下使用。
* `REDIS_MULTI`?標志表示客戶端正在執行事務。
* `REDIS_DIRTY_CAS`?標志表示事務使用?WATCH?命令監視的數據庫鍵已經被修改,?`REDIS_DIRTY_EXEC`?標志表示事務在命令入隊時出現了錯誤, 以上兩個標志都表示事務的安全性已經被破壞, 只要這兩個標記中的任意一個被打開,?EXEC?命令必然會執行失敗。 這兩個標志只能在客戶端打開了?`REDIS_MULTI`?標志的情況下使用。
* `REDIS_CLOSE_ASAP`?標志表示客戶端的輸出緩沖區大小超出了服務器允許的范圍, 服務器會在下一次執行?`serverCron`?函數時關閉這個客戶端, 以免服務器的穩定性受到這個客戶端影響。 積存在輸出緩沖區中的所有內容會直接被釋放, 不會返回給客戶端。
* `REDIS_CLOSE_AFTER_REPLY`?標志表示有用戶對這個客戶端執行了?CLIENT_KILL?命令, 或者客戶端發送給服務器的命令請求中包含了錯誤的協議內容。 服務器會將客戶端積存在輸出緩沖區中的所有內容發送給客戶端, 然后關閉客戶端。
* `REDIS_ASKING`?標志表示客戶端向集群節點(運行在集群模式下的服務器)發送了?ASKING?命令。
* `REDIS_FORCE_AOF`?標志強制服務器將當前執行的命令寫入到 AOF 文件里面,?`REDIS_FORCE_REPL`?標志強制主服務器將當前執行的命令復制給所有從服務器。 執行?PUBSUB?命令會使客戶端打開?`REDIS_FORCE_AOF`?標志, 執行?SCRIPT_LOAD?命令會使客戶端打開?`REDIS_FORCE_AOF`?標志和?`REDIS_FORCE_REPL`?標志。
* 在主從服務器進行命令傳播期間, 從服務器需要向主服務器發送?REPLICATION ACK?命令, 在發送這個命令之前, 從服務器必須打開主服務器對應的客戶端的?`REDIS_MASTER_FORCE_REPLY`?標志, 否則發送操作會被拒絕執行。
以上提到的所有標志都定義在?`redis.h`?文件里面。
`PUBSUB`?命令和?`SCRIPT?LOAD`?命令的特殊性
通常情況下, Redis 只會將那些對數據庫進行了修改的命令寫入到 AOF 文件, 并復制到各個從服務器: 如果一個命令沒有對數據庫進行任何修改, 那么它就會被認為是只讀命令, 這個命令不會被寫入到 AOF 文件, 也不會被復制到從服務器。
以上規則適用于絕大部分 Redis 命令, 但?PUBSUB?命令和?SCRIPT_LOAD?命令是其中的例外。
PUBSUB?命令雖然沒有修改數據庫, 但?PUBSUB?命令向頻道的所有訂閱者發送消息這一行為帶有副作用, 接收到消息的所有客戶端的狀態都會因為這個命令而改變。 因此, 服務器需要使用?`REDIS_FORCE_AOF`?標志, 強制將這個命令寫入 AOF 文件, 這樣在將來載入 AOF 文件時, 服務器就可以再次執行相同的?PUBSUB?命令, 并產生相同的副作用。
SCRIPT_LOAD?命令的情況與?PUBSUB?命令類似: 雖然?SCRIPT_LOAD?命令沒有修改數據庫, 但它修改了服務器狀態, 所以它是一個帶有副作用的命令, 服務器需要使用?`REDIS_FORCE_AOF`?標志, 強制將這個命令寫入 AOF 文件, 使得將來在載入 AOF 文件時, 服務器可以產生相同的副作用。
另外, 為了讓主服務器和從服務器都可以正確地載入?SCRIPT_LOAD?命令指定的腳本, 服務器需要使用?`REDIS_FORCE_REPL`?標志, 強制將SCRIPT_LOAD?命令復制給所有從服務器。
以下是一些?`flags`?屬性的例子:
~~~
# 客戶端是一個主服務器
REDIS_MASTER
# 客戶端正在被列表命令阻塞
REDIS_BLOCKED
# 客戶端正在執行事務,但事務的安全性已被破壞
REDIS_MULTI | REDIS_DIRTY_CAS
# 客戶端是一個從服務器,并且版本低于 Redis 2.8
REDIS_SLAVE | REDIS_PRE_PSYNC
# 這是專門用于執行 Lua 腳本包含的 Redis 命令的偽客戶端
# 它強制服務器將當前執行的命令寫入 AOF 文件,并復制給從服務器
REDIS_LUA_CLIENT | REDIS_FORCE_AOF | REDIS_FORCE_REPL
~~~
## 輸入緩沖區
客戶端狀態的輸入緩沖區用于保存客戶端發送的命令請求:
~~~
typedef struct redisClient {
// ...
sds querybuf;
// ...
} redisClient;
~~~
舉個例子, 如果客戶端向服務器發送了以下命令請求:
~~~
SET key value
~~~
那么客戶端狀態的?`querybuf`?屬性將是一個包含以下內容的 SDS 值:
~~~
*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n
~~~
圖 13-4 展示了這個 SDS 值以及?`querybuf`?屬性的樣子:

輸入緩沖區的大小會根據輸入內容動態地縮小或者擴大, 但它的最大大小不能超過 1 GB , 否則服務器將關閉這個客戶端。
## 命令與命令參數
在服務器將客戶端發送的命令請求保存到客戶端狀態的?`querybuf`?屬性之后, 服務器將對命令請求的內容進行分析, 并將得出的命令參數以及命令參數的個數分別保存到客戶端狀態的?`argv`?屬性和?`argc`?屬性:
~~~
typedef struct redisClient {
// ...
robj **argv;
int argc;
// ...
} redisClient;
~~~
`argv`?屬性是一個數組, 數組中的每個項都是一個字符串對象: 其中?`argv[0]`?是要執行的命令, 而之后的其他項則是傳給命令的參數。
`argc`?屬性則負責記錄?`argv`?數組的長度。
舉個例子, 對于圖 13-4 所示的?`querybuf`?屬性來說, 服務器將分析并創建圖 13-5 所示的?`argv`?屬性和?`argc`?屬性。

注意, 在圖 13-5 展示的客戶端狀態中,?`argc`?屬性的值為?`3`?, 而不是?`2`?, 因為命令的名字?`"SET"`?本身也是一個參數。
## 命令的實現函數
當服務器從協議內容中分析并得出?`argv`?屬性和?`argc`?屬性的值之后, 服務器將根據項?`argv[0]`?的值, 在命令表中查找命令所對應的命令實現函數。

圖 13-6 展示了一個命令表示例, 該表是一個字典, 字典的鍵是一個 SDS 結構, 保存了命令的名字, 字典的值是命令所對應的`redisCommand`?結構, 這個結構保存了命令的實現函數、 命令的標志、 命令應該給定的參數個數、 命令的總執行次數和總消耗時長等統計信息。
當程序在命令表中成功找到?`argv[0]`?所對應的?`redisCommand`?結構時, 它會將客戶端狀態的?`cmd`?指針指向這個結構:
~~~
typedef struct redisClient {
// ...
struct redisCommand *cmd;
// ...
} redisClient;
~~~
之后, 服務器就可以使用?`cmd`?屬性所指向的?`redisCommand`?結構, 以及?`argv`?、?`argc`?屬性中保存的命令參數信息, 調用命令實現函數, 執行客戶端指定的命令。
圖 13-7 演示了服務器在?`argv[0]`?為?`"SET"`?時, 查找命令表并將客戶端狀態的?`cmd`?指針指向目標?`redisCommand`?結構的整個過程。

針對命令表的查找操作不區分輸入字母的大小寫, 所以無論?`argv[0]`?是?`"SET"`?、?`"set"`?、或者?`"SeT`?, 等等, 查找的結果都是相同的。
## 輸出緩沖區
執行命令所得的命令回復會被保存在客戶端狀態的輸出緩沖區里面, 每個客戶端都有兩個輸出緩沖區可用, 一個緩沖區的大小是固定的, 另一個緩沖區的大小是可變的:
* 固定大小的緩沖區用于保存那些長度比較小的回復, 比如?`OK`?、簡短的字符串值、整數值、錯誤回復,等等。
* 可變大小的緩沖區用于保存那些長度比較大的回復, 比如一個非常長的字符串值, 一個由很多項組成的列表, 一個包含了很多元素的集合, 等等。
客戶端的固定大小緩沖區由?`buf`?和?`bufpos`?兩個屬性組成:
~~~
typedef struct redisClient {
// ...
char buf[REDIS_REPLY_CHUNK_BYTES];
int bufpos;
// ...
} redisClient;
~~~
`buf`?是一個大小為?`REDIS_REPLY_CHUNK_BYTES`?字節的字節數組, 而?`bufpos`?屬性則記錄了?`buf`?數組目前已使用的字節數量。
`REDIS_REPLY_CHUNK_BYTES`?常量目前的默認值為?`16*1024`?, 也即是說,?`buf`?數組的默認大小為 16 KB 。
圖 13-8 展示了一個使用固定大小緩沖區來保存返回值?`+OK\r\n`?的例子。

當?`buf`?數組的空間已經用完, 或者回復因為太大而沒辦法放進?`buf`?數組里面時, 服務器就會開始使用可變大小緩沖區。
可變大小緩沖區由?`reply`?鏈表和一個或多個字符串對象組成:
~~~
typedef struct redisClient {
// ...
list *reply;
// ...
} redisClient;
~~~
通過使用鏈表來連接多個字符串對象, 服務器可以為客戶端保存一個非常長的命令回復, 而不必受到固定大小緩沖區 16 KB 大小的限制。
圖 13-9 展示了一個包含三個字符串對象的?`reply`?鏈表。

## 身份驗證
客戶端狀態的?`authenticated`?屬性用于記錄客戶端是否通過了身份驗證:
~~~
typedef struct redisClient {
// ...
int authenticated;
// ...
} redisClient;
~~~
如果?`authenticated`?的值為?`0`?, 那么表示客戶端未通過身份驗證; 如果?`authenticated`?的值為?`1`?, 那么表示客戶端已經通過了身份驗證。
舉個例子, 對于一個尚未進行身份驗證的客戶端來說, 客戶端狀態的?`authenticated`?屬性將如圖 13-10 所示。

當客戶端?`authenticated`?屬性的值為?`0`?時, 除了?AUTH?命令之外, 客戶端發送的所有其他命令都會被服務器拒絕執行:
~~~
redis> PING
(error) NOAUTH Authentication required.
redis> SET msg "hello world"
(error) NOAUTH Authentication required.
~~~

當客戶端通過?AUTH?命令成功進行身份驗證之后, 客戶端狀態?`authenticated`?屬性的值就會從?`0`?變為?`1`?, 如圖 13-11 所示, 這時客戶端就可以像往常一樣向服務器發送命令請求了:
~~~
# authenticated 屬性的值從 0 變為 1
redis> AUTH 123321
OK
redis> PING
PONG
redis> SET msg "hello world"
OK
~~~
`authenticated`?屬性僅在服務器啟用了身份驗證功能時使用: 如果服務器沒有啟用身份驗證功能的話, 那么即使?`authenticated`?屬性的值為?`0`(這是默認值), 服務器也不會拒絕執行客戶端發送的命令請求。
關于服務器身份驗證的更多信息可以參考示例配置文件對?`requirepass`?選項的相關說明。
## 時間
最后, 客戶端還有幾個和時間有關的屬性:
~~~
typedef struct redisClient {
// ...
time_t ctime;
time_t lastinteraction;
time_t obuf_soft_limit_reached_time;
// ...
} redisClient;
~~~
`ctime`?屬性記錄了創建客戶端的時間, 這個時間可以用來計算客戶端與服務器已經連接了多少秒 ——?CLIENT_LIST?命令的?`age`?域記錄了這個秒數:
~~~
redis> CLIENT list
addr=127.0.0.1:53428 ... age=1242 ...
~~~
`lastinteraction`?屬性記錄了客戶端與服務器最后一次進行互動(interaction)的時間, 這里的互動可以是客戶端向服務器發送命令請求, 也可以是服務器向客戶端發送命令回復。
`lastinteraction`?屬性可以用來計算客戶端的空轉(idle)時間, 也即是, 距離客戶端與服務器最后一次進行互動以來, 已經過去了多少秒 ——?CLIENT_LIST?命令的?`idle`?域記錄了這個秒數:
~~~
redis> CLIENT list
addr=127.0.0.1:53428 ... idle=12 ...
~~~
`obuf_soft_limit_reached_time`?屬性記錄了輸出緩沖區第一次到達軟性限制(soft limit)的時間, 稍后介紹輸出緩沖區大小限制的時候會詳細說明這個屬性的作用。
- 介紹
- 前言
- 致謝
- 簡介
- 第一部分:數據結構與對象
- 簡單動態字符串
- SDS 的定義
- SDS 與 C 字符串的區別
- SDS API
- 重點回顧
- 參考資料
- 鏈表
- 鏈表和鏈表節點的實現
- 鏈表和鏈表節點的 API
- 重點回顧
- 字典
- 字典的實現
- 哈希算法
- 解決鍵沖突
- rehash
- 漸進式 rehash
- 字典 API
- 重點回顧
- 跳躍表
- 跳躍表的實現
- 跳躍表 API
- 重點回顧
- 整數集合
- 整數集合的實現
- 升級
- 升級的好處
- 降級
- 整數集合 API
- 重點回顧
- 壓縮列表
- 壓縮列表的構成
- 壓縮列表節點的構成
- 連鎖更新
- 壓縮列表 API
- 重點回顧
- 對象
- 對象的類型與編碼
- 字符串對象
- 列表對象
- 哈希對象
- 集合對象
- 有序集合對象
- 類型檢查與命令多態
- 內存回收
- 對象共享
- 對象的空轉時長
- 重點回顧
- 第二部分:單機數據庫的實現
- 數據庫
- 數據庫鍵空間
- 重點回顧
- RDB 持久化
- RDB 文件結構
- 重點回顧
- AOF 持久化
- AOF 持久化的實現
- 重點回顧
- 事件
- 文件事件
- 重點回顧
- 參考資料
- 客戶端
- 客戶端屬性
- 重點回顧
- 服務器
- 命令請求的執行過程
- 重點回顧
- 第三部分:多機數據庫的實現
- 復制
- 舊版復制功能的實現
- 重點回顧
- Sentinel
- 啟動并初始化 Sentinel
- 重點回顧
- 參考資料
- 集群
- 節點
- 重點回顧
- 第四部分:獨立功能的實現
- 發布與訂閱
- 頻道的訂閱與退訂
- 重點回顧
- 參考資料
- 事務
- 事務的實現
- 重點回顧
- Lua 腳本
- 創建并修改 Lua 環境
- 重點回顧
- 排序
- SORT <key> 命令的實現
- 重點回顧
- 二進制位數組
- GETBIT 命令的實現
- 重點回顧
- 慢查詢日志
- 慢查詢記錄的保存
- 慢查詢日志的閱覽和刪除
- 添加新日志
- 重點回顧
- 監視器
- 成為監視器
- 向監視器發送命令信息
- 重點回顧
- 源碼、相關資源和勘誤