《Redis設計與實現》閱讀筆記8-客戶端
#12 客戶端
Redis伺服器是典型的一對多伺服器(一個伺服器與多個客服端建立連線),通過使用I/O多路複用技術實現檔案事件處理器,Redis使用單執行緒單程序的方式來處理命令請求,伺服器為客戶端建立redis.h/redisClient結構,這些結構儲存了客戶端當前的狀態資訊,及執行相關功能時需要用到的資料結構。
- 客戶端的套接字描述符
- 客戶端的名字
- 客戶端的標誌值
- 指向客戶端正在使用的資料庫的指標,以及該資料庫的號碼
- 客戶端當前要執行的命令,命令的引數,命令引數的個數,以及指向命令實現函式的指標
- 客戶端的輸入緩衝區和輸出緩衝區
- 客戶端的複製狀態資訊,以及進行復制所需的資料結構
- 客戶端執行BRPOP,BLPOP等列表阻塞命令時使用的資料結構
- 客戶端的事務狀態,以及執行WATCH命令時用到的資料結構
- 客戶端執行釋出與訂閱功能時用到的資料結構
- 客戶端的身份驗證標準
- 客戶端建立時間,與伺服器最後一次通訊時間,超出緩衝區大學的軟性限制時間
redisServer中有一個clients的連結串列結構,儲存所有與伺服器連線的客戶端的狀態
struct redisServer{
//...
//一個連結串列,儲存了所有客戶端的狀態
list *clients;
//...
}
##12.1 客戶端屬性
客戶端狀態包含的屬性可以分為兩類:
- 一類是比較通用的屬性,這些屬性很少與特定功能相關,無論客戶端執行的是什麼工作,都需要用到這些屬性
- 另外一類是和特定功能相關的屬性,比如操作資料庫時需要用到的db屬性和dictid屬性,執行事務用到的mstate屬性,以及執行WATCH命令時需要用到的watched_keys屬性等等。
####12.1.1 套接字描述符
typedef struct redisClient {
// ...
int fd;
// ...
} redisClient;
客戶端狀態的fd屬性記錄了客戶端正在使用的套接字描述符,根據客戶端的不同,fd的屬性的值可以是-1或者大於-1的整數。
-
偽客戶端的fd屬性的值為-1,偽客戶端處理的命令請求來源於AOF檔案或者Lua指令碼,而不是網路,所以這種客戶端不需要套接字連線,用到偽客戶端的地方為載入AOF檔案還原資料庫狀態或者執行Lua指令碼中包含的Redis命令。
-
普通客戶端的fd屬性的值大於-1,普通客戶端需要使用套接字與伺服器進行網路通訊,合法的套接字描述符不能是-1, 所以正常客戶端使用的是大於-1的整數。
####12.1.2 名字 typedef struct redisClient {
// ...
robj *name;
// ...
} redisClient; 在預設情況下,客戶端是沒有名字的,使用CLIENT list命令可以返回客戶端鏈中所有客戶端的資訊,此時可以發現名字這個地方是空的。
而使用CLIENT setname命令可以給客戶端指定一個名字,讓客戶端的身份變的更具體。
####12.1.3 標誌
typedef struct redisClient {
// ...
int flags;
// ...
} redisClient;
客戶端中的flags記錄客戶端的角色,及客戶端當前的狀態
flags可以是單個標誌: flags=<flag>
也可以是多個標誌的二進位制或: flags=<flag1>|<flag2>|...
每個標誌都使用一個常量表示,一部分標記記錄了客戶端的角色
- 在主從伺服器中,主伺服器使用REDIS_MASTER標誌,從伺服器使用REDIS_SLAVE標誌
- REDIS_PRE_PSYNC標誌代表客戶端是一個版本地獄Redis2.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 標誌, 否則傳送操作會被拒絕執行。
PUBSUB命令與SCRIPT LOAD命令的特殊性
通常Redis只會把對資料庫進行了修改的命令寫入到AOF檔案,並複製到各個從伺服器,只讀的命令不會被寫入AOF檔案。這個特點適用於大多數Redis命令,但PUBSUB命令與SCRIPT LOAD命令具體特殊性.PUBSUB命令不會修改資料庫,但它向頻道的所有訂閱者傳送訊息的行為帶有副作用,接收到訊息的所有客戶端狀態會因為這個命令發生改變,所以伺服器會使用REDIS_FORCE_AOF標誌,將這個語句寫入AOF,並在載入AOF檔案時,會再次產生相同的PUBSUB命令,併產生一樣的副作用。SCRIPT LOAD命令的情況類似PUBSUB命令,它不會修改資料庫,但它修改了伺服器的狀態,所以這是一個帶有副作用的命令,會使伺服器使用REDIS_FORCE_AOF標誌,強制寫入AOF檔案,並在載入AOF檔案時,再次執行,再次產生副作用。 另外,為了讓主伺服器與從伺服器都能成功載入SCRIPT LOAD命令指定的指令碼,伺服器需要使用REDIS_FORCE_REPL 標誌將命令強制複製給所有從伺服器。
####12.1.4 輸入緩衝區
客戶端狀態的輸入緩衝區用於儲存客戶端傳送的命令請求
typedef struct redisClient {
// ...
sds querybuf;
// ...
} redisClient;
輸入緩衝區的大小會根據輸入內容動態地縮小或者擴大, 但它的最大大小不能超過 1 GB , 否則伺服器將關閉這個客戶端。
####12.1.5 命令與命令引數
在伺服器將客戶端傳送的命令請求儲存到客戶端狀態的 querybuf 屬性之後, 伺服器將對命令請求的內容進行分析, 並將得出的命令引數以及命令引數的個數分別儲存到客戶端狀態的 argv 屬性和 argc 屬性:
typedef struct redisClient {
// ...
robj **argv;
int argc;
// ...
} redisClient;
-
argv 屬性是一個數組, 陣列中的每個項都是一個字串物件: 其中 argv[0] 是要執行的命令, 而之後的其他項則是傳給命令的引數。
-
argc 屬性則負責記錄 argv 陣列的長度。方便取的後面的引數。
####12.1.6 命令實現函式
當伺服器從協議內容中分析並得出 argv 屬性和 argc 屬性的值之後, 伺服器將根據項 argv[0] 的值, 在命令表中查詢命令所對應的命令實現函式。
當程式在命令表中成功找到 argv[0] 所對應的 redisCommand 結構時, 它會將客戶端狀態的 cmd 指標指向這個結構:
typedef struct redisClient {
// ...
//指向指定結構的指標
struct redisCommand *cmd;
// ...
} redisClient;
####12.1.7 輸出緩衝區
執行命令所得的命令回覆會被儲存在客戶端狀態的輸出緩衝區裡面, 每個客戶端都有兩個輸出緩衝區可用, 一個緩衝區的大小是固定的, 另一個緩衝區的大小是可變的:
- 固定大小的緩衝區用於儲存那些長度比較小的回覆, 比如 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 。
當 buf 陣列的空間已經用完, 或者回復因為太大而沒辦法放進 buf 數組裡面時, 伺服器就會開始使用可變大小緩衝區。 可變大小緩衝區由 reply 連結串列和一個或多個字串物件組成:
typedef struct redisClient {
// ...
list *reply;
// ...
} redisClient;
通過使用連結串列來連線多個字串物件, 伺服器可以為客戶端儲存一個非常長的命令回覆, 而不必受到固定大小緩衝區 16 KB 大小的限制。
####12.1.8 身份驗證
客戶端狀態的 authenticated 屬性用於記錄客戶端是否通過了身份驗證
typedef struct redisClient {
// ...
int authenticated;
// ...
} redisClient;
如果 authenticated 的值為 0 , 那麼表示客戶端未通過身份驗證; 如果 authenticated 的值為 1 , 那麼表示客戶端已經通過了身份驗證。當客戶端 authenticated 屬性的值為 0 時, 除了 AUTH 命令(身份驗證命令)之外, 客戶端傳送的所有其他命令都會被伺服器拒絕執行。當客戶端通過 AUTH 命令成功進行身份驗證之後, 客戶端狀態 authenticated 屬性的值就會從 0 變為 1 。
authenticated 屬性僅在伺服器啟用了身份驗證功能時使用: 如果伺服器沒有啟用身份驗證功能的話, 那麼即使 authenticated 屬性的值為 0 (這是預設值), 伺服器也不會拒絕執行客戶端傳送的命令請求。
配置可以檢視配置檔案中的requirepass選項。
####12.1.9 時間 客戶端還有幾個和時間有關的屬性
typedef struct redisClient {
// ...
time_t ctime;
time_t lastinteraction;
time_t obuf_soft_limit_reached_time;
// ...
} redisClient;
- ctime 屬性記錄了建立客戶端的時間, 這個時間可以用來計算客戶端與伺服器已經連線了多少秒 —— CLIENT_LIST 命令的 age 域記錄了這個秒數
- lastinteraction 屬性記錄了客戶端與伺服器最後一次進行互動(interaction)的時間, 這裡的互動可以是客戶端向伺服器傳送命令請求, 也可以是伺服器向客戶端傳送命令回覆。
- lastinteraction 屬性可以用來計算客戶端的空轉(idle)時間, 也即是, 距離客戶端與伺服器最後一次進行互動以來, 已經過去了多少秒 —— CLIENT_LIST 命令的 idle 域記錄了這個秒數
- obuf_soft_limit_reached_time 屬性記錄了輸出緩衝區第一次到達軟性限制(soft limit)的時間, 稍後介紹輸出緩衝區大小限制的時候會詳細說明這個屬性的作用。
12.2 客戶端的建立與關閉
伺服器會使用不同的方式來建立和關閉不同型別的客戶端
12.2.1 建立普通客戶端
如果客戶端是通過網路連線與伺服器連線的普通客戶端,伺服器會使用連線事件處理器為客戶端建立相應的客戶端狀態,並將新的客戶端狀態新增到伺服器資料結構的客戶端連結串列中。
12.2.2 關閉普通客戶端
一個普通客戶端被關閉有多種可能原因
- 如果客戶端程序退出或者被殺死,那麼客戶端與伺服器的網路連線將被關閉,造成客戶端被關閉
- 如果客服端向伺服器傳送了不符合協議格式的命令請求
- 客戶端成為CLIENT KILL命令的目標
- 如果使用者為伺服器設定了timeout配置選項,那麼空轉時間超過這個時間,客戶端會被關閉,例外:客戶端是主伺服器,從伺服器,被BLPOP等命令阻塞,或者正在執行SUBSCRIBE,PSUBSCRIBE等訂閱命令,空轉時間超過timeout也不會被關閉
- 客戶端傳送的命令請求大小超過了輸入緩衝區的限制大小
- 傳送給客戶端的命令回覆的大小超過了輸出緩衝區的限制大小,前面說的可變大小緩衝區用於接收命令回覆,由一個連結串列和任意多個字串物件組成,但避免給客戶端回覆過大,佔用過多的資源,會檢查輸出緩衝區的大小,並進行限制操作。
- 硬性限制:如果超過硬性限制要求的大小,那麼伺服器立即關閉客戶端
- 軟性限制:如果超過軟性限制的大小,沒超過硬性限制的大小。伺服器會使用客戶端狀態結構的obuf_soft_limit_reached_time屬性記錄下到達軟性限制的起始時間,之後會一直監視,如果處於超過軟性限制持續時間達到設定的時長,會關閉客戶端。若在指定時間內,達到軟性限制以下就不會關閉。
12.2.3 Lua指令碼的偽客戶端
伺服器會在初始化時建立負責執行Lua指令碼中包含Redis命令的偽客戶端,並將偽客戶端關聯在伺服器狀態結構的lua_client屬性中:
struct redisServer{
//...
redisClient *lua_client;
//...
}
lua_client偽客戶端在伺服器執行的整個生命週期中一直存在,只有伺服器被關閉時,這個客戶端才會被關閉。
12.2.4 AOF檔案的偽客戶端
伺服器在載入AOF檔案時,會建立用於執行AOF檔案包含的Redis命令的偽客戶端,並在載入完成以後,關閉這個偽客戶端