Redis 是一個客戶端服務端的程序,服務端提供數據存儲等等服務,客戶端連接服務端並通過向服務端發送命令,讀取或寫入數據,簡單來說,客戶端就是某種工具,我們通過它與 Redis 服務端進行通訊並完成數據操作。
客戶端並不是 Redis 的核心,Redis 的核心是它的服務端程序,服務端程序才是完成數據存、取,持久化等等我們使用頻繁的各種操作的執行者。但也不是說客戶端就沒什么作用,客戶端在整個 Redis 服務體系中也是非常重要的一環。
Redis服務器是一個典型的一對多服務器程序:一個服務器可以與多個客戶端建立網絡連接,每個客戶端可以向服務器發送命令請求,而服務器則接受並處理客戶端發送的命令請求,並向客戶端返回命令回復。通過使用由I/O多路復用技術實現的文件事件處理器,Redis服務器使用單線程單進程的方式來處理命令請求,並與多個客戶端進行網絡通信。
服務器狀態結構的clients屬性是一個鏈表,這個鏈表保存了所有與服務器連接的客戶端的狀態結構,對客戶端執行批量操作,或者查找某個指定的客戶端,都可以通過遍歷clients鏈表來完成:
struct redisServer{ //,., // 一個鏈表,保存了所有客戶端狀態 list * clients; }

一、客戶端的基本屬性
redis 中為客戶端抽象的數據結構是,server.h/client 結構,我這里是 redis-4.0.x 版本,不同版本或許稍有不同,每一個 redis 客戶端成功的連接上服務端之后,服務端就會創建一個 client 結構實例,並以鏈表的形式鏈接所有連接成功的客戶端。
這個結構最主要作用就是存儲當前客戶端的大量屬性,套接字、名字、標志,狀態等等信息,這些信息非常的重要,當服務端為客戶端服務時,很多的信息例如當前要執行的命令、參數都會從這里獲取。我們一個一個來了解。
1、客戶端名稱
默認情況下,所有連接成功的客戶端都是沒有名字的,這一點你可以通過向服務發送 client list 命令驗證,它會返回當前服務端成功建立的客戶端以及他們的基本信息。例如:

可以看到,name 字段默認是空,如果你想讓你的客戶端辨識度更高,你可以向服務端發送 client setname 為你的客戶端命名,這里我就不做演示了,客戶端名稱這個信息保存在 client 結構中的 name 字段里。
typedef struct client { ......... robj *name; /* As set by CLIENT SETNAME. */ ......... } client;
2、套接字
客戶端套接字由客戶端狀態的 fd 屬性記錄
當 fd 屬性值為-1 時,表示這個客戶端是偽客戶端。偽客戶端的請求命令不是來源於網絡的,而是來源於 Lua 腳本或 AOF 文件(后續詳細介紹)的,所以偽客戶端不需要套接字連接,它也沒有套接字描述符。當我們執行的 Lua 腳本中含有 Redis 命令,或者使用 AOF 文件來還原數據庫狀態時,就會用到偽客戶端。
當 fd 屬性值是大於-1 的整數時,表示這個客戶端是普通客戶端。普通客戶端采用相關套接字來實現與服務器的通信,因此服務器會利用 fd 屬性來記錄客戶端套接字的描述符。
3、標志屬性
標志用於描述當前 redis 客戶端的一些狀態或者角色,對應的到數據結構中就是一個整型字段。
typedef struct client { ......... int flags; /* Client flags: CLIENT_* macros. */ ......... } client;
Redis 中定義了很多的客戶端標志,客戶端的標志屬性 flags 用來記錄客戶端的角色(Role)及客戶端目前所處的狀態。
flags 屬性的取值可以是單個標志,也可以是多個二進制或的組合標志,具體如下。
單個標志:flags=<flag>
組合標志:flags=<flag1>|<flag2>|<flag3>|…
標志使用常量來表示。Redis 所具有的所有標志都定義在 redis.h 文件中。
記錄客戶端角色的標志有如下幾個:
- 在利用 Redis 主從服務器實現復制時,主從服務器會相互成為對方的客戶端,也就是從服務器是主服務器的客戶端,同時主服務器也是從服務器的客戶端。Redis 使用REDIS_MASTER 標志來表示這個客戶端是主服務器,而使用 REDIS_SLAVE 標志來表示另一個客戶端是從服務器。
- Redis 使用 REDIS_LUA_CLIENT 標志來表示該客戶端是一個專門用於處理 Lua 腳本的偽客戶端,它主要用於執行 Lua 腳本中包含的 Redis 命令。
- Redis 使用 REDIS_PRE_PSYNC 標志來表示該客戶端是一個低於 Redis 2.8 版本的從服務器,此時,對應的主服務器不能使用 PSYNC 命令實現與從服務器的數據同步。只有當 REDIS_SLAVE 標志處於打開狀態時,才能使用 REDIS_PRE_PSYNC 標志。
記錄客戶端當前狀態的標志有如下幾個:
- REDIS_ASKING 標志表示客戶端向運行在集群模式下的服務器節點發送了 ASKING 命令。
- REDIS_CLOSE_ASAP 標志表示客戶端的輸出緩沖區過大,超出了服務器所允許的范圍。當服務器在下一次執行 serverCron 函數時,會關閉這個輸出緩沖區過大的客戶端,以此來保證服務器的穩定性不受這個客戶端影響。在關閉的時候,存儲在這個緩沖區中的數據也會被刪除,並且不會給客戶端返回任何信息。
- REDIS_CLOSE_AFTER_REPLY 標志表示客戶端給服務器發送的命令請求中有錯誤的協議內容,或者用戶在客戶端中執行了 CLIENT kill 命令。此時服務器會將客戶端輸出緩沖區中存儲的所有數據內容發送給客戶端,然后關閉這個客戶端。
- REDIS_DIRTY_CAS 標志表示事務使用 WATCH 命令監視的數據庫鍵已經被修改。
- REDIS_DIRTY_EXEC 標志表示事務在命令入隊時出現錯誤。
REDIS_DIRTY_CAS 和 REDIS_DIRTY_EXEC 標志的出現都表示 Redis 事務的安全性已被破壞。只要這兩個標志中的任何一個被打開,EXEC 命令都會執行失敗。而只有在客戶端打開了 REDIS_MULTI 標志的情況下,才能使用這兩個標志。
- REDIS_MULTI 標志表示客戶端正處於執行事務的狀態中。
- REDIS_MONITOR 標志表示客戶端正處於執行 MONITOR 命令的狀態中。
- REDIS_FORCE_AOF 標志表示讓服務器將當前正在執行的命令強制寫入 AOF 文件中。在執行 PUBSUB 命令時,會使客戶端打開 REDIS_FORCE_AOF 標志。
- REDIS_FORCE_REPL 標志表示強制讓主服務器將當前正在執行的命令復制給所有與它連接的從服務器。當執行 SCRIPT LOAD 命令時,會使客戶端同時開啟 REDIS_FORCE_AOF 和 REDIS_FORCE_REPL 標志。如果要實現主從服務器可以正確地載入 SCRIPT LOAD 命令指定的腳本,那么服務器必須使用 REDIS_FORCE_REPL 標志,讓主服務器強制將 SCRIPT LOAD 命令分發給相應的從服務器。
- REDIS_UNIX_SOCKET 標志表示服務器連接客戶端使用的是 UNIX 套接字。
- REDIS_BLOCKED 標志表示客戶端正處於被 BRPOP、BLPOP 等命令阻塞的狀態中。
- REDIS_UNBLOCKED 標志表示客戶端不再阻塞,它從 REDIS_BLOCKED 標志的阻塞狀態中脫離出來。只有在 REDIS_BLOCKED 標志被打開的情況下,才能使用 REDIS_UNBLOCKED 標志。
- REDIS_MASTER_FORCE_REPLY 標志:在主從服務器進行命令交互的過程中,從服務器需要向主服務器發送 REPLICATION ACK 命令。但是,在發送此命令之前,從服務器必須開啟主服務器對應的客戶端的 REDIS_MASTER_FORCE_REPLY 標志;否則主服務器會拒絕執行從服務器發送的 REPLCATION ACK 命令。
當然了,上面那個 flages 的值只是舉了個例子,描述了當前客戶端是一個主節點的 server(當進行主從節點復制的時候,主節點會作為客戶端連接從節點發送 RDB 文件給客戶端),又正在執行 MONITOR 命令。前者描述了客戶端角色,后者描述客戶端狀態。
總而言之,redis 客戶端 flags 字段可以描述當前客戶端的角色,也可以記錄當前客戶端各種狀態信息,是服務端了解客戶端信息的一個非常重要的字段。
4、輸入/輸出緩沖區
redis 服務端收到客戶端發來的命令請求需要很多步驟來處理和調用相關命令的實現,並最終將數據返回給客戶端,那么輸入緩沖區其實就是一小塊內存,用於存儲客戶端發送過來的命令,包括參數,這塊內存空間默認不能超過 1GB,否則 redis 服務端就會強制關閉與該客戶端的連接。
typedef struct client { ......... sds querybuf; /* Buffer we use to accumulate client queries. */ ......... } client;
querybuf 就是客戶端緩沖區,它是一個 SDS 類型的字段,那么說明這是一個可以動態擴充輸入緩沖區。
當然我們也可以通過 client list 看看當前客戶端的的 querybuf 分配和使用情況。

其中 qbuf 和 qbuf-free 用於描述客戶端輸入緩沖區狀態。我這里的這個沒有寫入過大的命令,所以這里的 querybuf 只分配了 32768 個字節。
ps:盡量不要使用過大的 KEY,這樣會導致客戶端 querybuf 占用過多內存,這樣會導致 redis 服務端程序占用過高內存,如果超過 maxmemory 限制,會觸發 KEY 的 LRU 淘汰或程序異常。
除此之外,redis 客戶端還有一個輸出緩沖區,用於緩存服務端響應的回復。
輸出緩沖區有兩種,一種是固定大小的,用於存儲服務端簡單的響應,例如:OK,錯誤信息等。還有一種是非固定長度的緩沖區,它的長度是可動態擴展的,用於存儲一些較長的響應內容。
typedef struct client { ......... /* Response buffer */ int bufpos; char buf[PROTO_REPLY_CHUNK_BYTES]; ......... } client;
PROTO_REPLY_CHUNK_BYTES 等於 16*1024,也就是默認固定輸出緩沖區只有 16K,bufpos 記錄當前固定緩沖區已經使用的字節數。
typedef struct client { ......... list *reply; /* List of reply objects to send to the client. */ ......... } client;
動態緩沖區用鏈表實現,可以為我們返回較大的 key,例如一些 set、list 集合等等。我們可以通過 client list 命令查看輸出緩沖區的使用情況。

obl 表示固定緩沖區長度,oll 代表動態緩沖區長度,omem 表示固定緩沖區和動態緩沖區總共占用了多少字節。
ps:輸出緩沖區可以通過配置 client-output-buffer-limit 限制最大內存上限,同樣如果濫用,一樣會導致 redis 服務器內存飆升,建議盡量配置小一點的輸出緩存區大小。
5、命令與命令參數
在服務器將客戶端發送的命令請求保存到客戶端狀態的queybuf屬性之后,服務器將對命令請求的內容進行分析,並將得出的命令參數以及命令參數的個數分別保存到客戶端狀態的argv屬性和argc屬性:
int argc; /* Num of arguments of current command. */ robj **argv; /* Arguments of current command. */
argv 屬性是個數組,數組中的每一項都是字符串對象,其中 argv[0] 是要執行的命令,而之后的其他則是傳給命令的參數。argc記錄了argv數組的長度。
6、命令的實現函數
當服務器從協議內容中分析得出argv 和 argc屬性后,服務器會根據項argv[0]的值,在命令表中查找命令對應的命令實現函數。如圖,命令表實際上是個字典,鍵是SDS,保存命令的名字;
值是redisCommand結構,這個結構保存了命令的實現函數、命令的標志、命令應該給定的參數個數、命令的總執行次數和總耗時。當程序在命令表中成功找到argv[0]所對應的redisCommand結構時,它會將客戶端狀態的cmd指針指向這個結構。之后服務器可以使用cmd屬性指向的redisCommand結構,以及argv、argc屬性中的命令參數信息,調用命令實現函數,執行客戶端指定的命令。
7、身份驗證
客戶端狀態的authenticated 屬性用於記錄客戶端是否通過了身份驗證:如果為0,那么表示客戶端未通過身份驗證,如果值為1,那么表示客戶端已經通過了身份驗證。該屬性只有在配置文件對requirepass 選項打開的情況下使用。
當 authenticated 屬性值為 0 時,服務器除執行 AUTH 命令之外,將會拒絕執行客戶端發送過來的其他所有命令。
8、時間
客戶端還有幾個和時間有關的屬性:
- ctime屬性記錄了創建客戶端的時間,用來記錄客戶端與服務端已經連接了多少秒;
- lastinteraction 屬性記錄了客戶端與服務器最后一次進行互動的時間,這里的互動可以是客戶端向服務端發送命令請求,也可以是服務端向客戶端發送命令回復。 lastinteraction 屬性可以計算出客戶端的空轉時間,也就是在進行最后一次交互之前過去了多少時間,單位為秒。CLIENT list 命令返回的 idle 域記錄了這個時間。當 idle 的值為 0 時,表示空轉時間為 0 秒。
- obuf_soft_limit_reached_time屬性記錄了輸出緩沖區第一次到達軟性限制(soft limit)的時間,下面說這個屬性的用途。
二、客戶端的創建和關閉
2.1、創建普通客戶端
如果客戶端是通過網絡連接與服務器進行連接的普通客戶端,那么在客戶端使用connect函數連接到服務器時,服務器就會調用連接事件處理器,為客戶端創建相應的客戶端狀態,並將這個新的客戶端狀態添加到服務器狀態結構clients鏈表的末尾。

2.2、關閉客戶端
普通客戶端被關閉的幾種方式:
● 當客戶端執行了 CLIENT kill 命令時,客戶端會被關閉。
● 當客戶端進程被殺死時,客戶端將會斷開與服務器的連接,從而客戶端被關閉。
● 當客戶端向服務器發送的命令是錯誤協議格式時,客戶端會被關閉。
● 當客戶端發送的命令請求的大小超過了輸入緩沖區的限制大小時,客戶端會被關閉。
● 當發送給客戶端的命令執行后返回結果的大小超過了輸出緩沖區的限制大小時,客戶端也會被關閉。
● 當為服務器設置了 timeout 參數值,同時客戶端的空轉時間又超過了 timeout 參數值時,客戶端將會被關閉。而如果這個客戶端是主服務器,而從服務器被 BLPOP、BRPOP 等相關命令阻塞,或者從服務器正在執行與訂閱發布相關的命令,此時就算客戶端的空轉時間超過了 timeout 參數值,這個客戶端也不會被關閉。
服務器使用兩種模式來限制客戶端輸出緩沖區的大小:
- 硬性限制(hard limit):如果輸出緩沖區的大小超過了硬性限制所設置的大小,那么服務器立即關閉客戶端。
- 軟性限制(soft limit):如果輸出緩沖區的大小超過了軟性限制所設置的大小,但還沒有超過硬性限制,那么服務器將使用服務器狀態結構的 obuf_soft_limit_reached_time 屬性記錄下客戶端
到達軟性限制的起始時間,之后服務器會繼續監視客戶端,如果輸出緩沖區的大小一直超出軟性限制,並且持續時間超過服務器設定的時長,那么服務器就會關閉客戶端,相反地,如果客戶端在指定時間內不再超出軟性限制,那么客戶端就不會被關閉,並且obuf_soft_limit_reached_time也會被清零。
2.3、Lua腳本的偽客戶端
服務器會在初始化時創建負責執行Lua腳本中包含的Redis命令的偽客戶端,並將這個偽客戶端關聯在服務器狀態結構的lua_client屬性中。lua_client偽客戶端在服務器運行的整個生命期中一直存在,只有服務器被關閉時,這個客戶端才會被關閉。
2.4、AOF文件的偽客戶端
服務器在載入AOF文件時,會創建用於執行AOF文件包含的Redis命令的偽客戶端,並在載入完成之后,關閉這個偽客戶端。
參考《Redis設計與實現》
https://www.cnblogs.com/undefined22/p/12580818.html
https://blog.csdn.net/sinat_30333853/article/details/80917044
