客戶端
Redis服務器是典型的一對多服務器程序:一個服務器可以與多個客戶端建立網絡連接,每個客戶端可以向服務器發送命令請求,而服務器則接收並處理客戶端發送的命令請求,並向客戶端返回命令回復。通過使用I/O多路復用技術實現的文件事件處理器,Redis服務器使用單線程單進程的方式來處理命令請求,並與多個客戶端進行網絡通信
對於每個與服務器進行連接的客戶端,服務器都為這些客戶端建立了相應的redis.h/redisClient結構(客戶端狀態),這個結構保存了客戶端當前的狀態信息,以及執行相關功能時需要用到的數據結構,其中包括:
- 客戶端的套接字描述符
- 客戶端的名字
- 客戶端的標志值(flag)
- 指向客戶端正在使用的數據庫指針,以及該數據庫的號碼
- 客戶端當前要執行的命令、命令的參數、命令參數的個數,以及指向命令實現函數的指針
- 客戶端的輸入緩沖區和輸出緩沖區
- 客戶端的復制狀態信息,以及進行復制所需的數據結構
- 客戶端執行BRPOP、BLPOP等列表阻塞命令時使用的數據結構
- 客戶端的事物狀態,以及執行WATCH命令時用到的數據結構
- 客戶端執行發布與訂閱功能時用到的數據結構
- 客戶端的身份驗證標識
- 客戶端的創建時間,客戶端和服務器最后一次通信時間,以及客戶端的輸出華沖區大小超出軟性限制的時間
Redis服務器狀態結構的clients屬性是一個鏈表,這個鏈表保存了所有與服務器連接的客戶端的狀態結構,對客戶端執行批量操作,或者查找某個指定的客戶端,都可以通過遍歷clients鏈表來完成:
redis.h
struct redisServer { …… //一個鏈表,保存了所有客戶端狀態 list *clients; …… };
圖1-1 客戶端與服務器
作為例子,圖1-1展示了一個與三個客戶端進行連接的服務器,而圖1-2則展示了這個服務器clients鏈表的例子
圖1-2 clients鏈表
客戶端屬性
客戶端狀態包含的屬性可以分為兩類:
- 一類是比較通用的屬性,這類屬性很少與特定功能相關,無論客戶端執行的是什么工作,它們都要用到這些屬性
- 另一類是和特定功能相關的屬性,比如操作數據庫時需要用到的db屬性和dictid屬性,執行事務時需要用到的master屬性,以及執行WATCH命令時需要用到的watched_keys屬性等
套接字描述符
客戶端狀態的fd屬性記錄了客戶端正在使用的套接字描述符
redis.h
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域顯示了服務器連接客戶端所使用的套接字描述符:
127.0.0.1:6379> CLIENT list id=3 addr=127.0.0.1:57522 fd=9 name= age=76021 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client
名字
在默認情況下,一個連接到服務器的客戶端是沒有名字的。比如上面的CLIENT list命令中,name域是空白的。不過我們可以使用CLIENT SETNAME命令為客戶端設置一個名字,讓客戶端的身份變得清晰。以下展示客戶端執行CLIENT SETNAME命令之后的客戶端列表
127.0.0.1:6379> CLIENT SETNAME message_quque OK 127.0.0.1:6379> CLIENT list id=3 addr=127.0.0.1:57522 fd=9 name=message_quque age=76399 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client
客戶端的名字記錄在客戶端狀態的name屬性中:
redis.h
typedef struct redisClient { …… robj *name; …… } redisClient;
如果客戶端沒有為自己設置名字,那么相應客戶端狀態的name屬性指向NULL指針;相反地,如果客戶端為自己設置了名字,那么name屬性將指向一個字符串對象,而對象則存放着客戶端的名字。圖1-3展示了客戶端狀態示例,根據name屬性顯示,客戶端名字為"message_queue"
圖1-3 name屬性示例
標志
客戶端的標志屬性flags記錄了客戶端的角色,以及客戶端目前所處的狀態
redis.h
typedef struct redisClient { …… int flags; …… } redisClient;
flags屬性的值可以是單個標志:flags = <flag>,也可以是多個標志的二進制:flags = <flags1> | <flags2> | ……
每個標志使用一個常量表示,一部分標志記錄了客戶端的角色:
- 在主從服務器進行復制操作時,主從服務器會成為從服務器的客戶端,而從服務器也會成為主服務器的客戶端。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標志
- 在主從服務器進行命令傳播期間,從服務器需要向主服務器發送REPLICATIONACK命令,在發送這個命令之前,從服務器必須打開主服務器對應的客戶端的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
輸入緩沖區
客戶端狀態的輸入緩沖區用於保存客戶端發送的命令請求
redis.h
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
圖1-4展示了這個SDS值以及querybuf屬性的樣子
圖1-4 querybuf屬性示例
命令與命令參數
在服務器將客戶端發送的命令請求保存到客戶端狀態的querybuf屬性之后,服務器將對命令請求的內容進行分析,並將得出的命令參數以及命令參數的個數分別保存到客戶端狀態的argv屬性和argc屬性:
redis.h
typedef struct redisClient { …… int argc; robj **argv; …… } redisClient;
argv屬性是一個數組,數組中的每個項都是一個字符串對象,其中argv[0]是要執行的命令,而之后的其他項則是傳給命令的參數。argc屬性負責記錄argv數組的長度
舉個栗子,對於圖1-4所示的querybuf屬性來說,服務器將分析並創建圖1-5所示的argv屬性和argc屬性
圖1-5 argv屬性和argc屬性示例
注意,圖1-5展示的客戶端狀態中,argc屬性的值為3,而不是2,因為命令的名字"SET"本身也是一個參數
命令的實現函數
當服務器從協議內容中分析並得出argv屬性和argc屬性的值之后,服務器將根據項argv[0]的值,在命令表中查找命令所對應的命令實現函數。圖1-6展示了一個命令表示例,該表是一個字典,字典的鍵是一個SDS結構體,保存了命令的名字,字典的值是命令所對應的redisCommand結構體,這個結構體保存了命令的實現函數、命令的標志、命令應該給定的參數個數、命令的總執行次數和總消耗時長等統計信息
圖1-6 命令表
當程序在命令表中成功找到argv[0]所對應的redisCommand結構體時,它會將客戶端狀態的cmd指針指向這個結構體:
redis.h
typedef struct redisClient { …… struct redisCommand *cmd; …… } redisClient;
之后,服務器就可以使用cmd屬性所指向的redisCommand結構體,以及argv、argc屬性中保存的命令參數信息,調用命令實現函數,執行客戶端指定的命令
圖1-7演示了服務器在argv[0]為"SET"時,查找命令表並將客戶端狀態的cmd指針指向目標redisCommand結構體的整個過程
圖1-7 查找命令並設置cmd屬性
針對命令表的查找操作不區分輸入字母的大小寫,所以無論argv[0]是"SET"、"set"或者是"SeT"等等,查找結果都是相同的
輸出緩沖區
執行命令所得的命令回復會被保存在客戶端狀態的輸出緩沖區里面,每個客戶端都有兩個輸出緩沖區可用,一個緩沖區的大小是固定的,另一個緩沖區的大小是可變的
- 固定大小的緩沖區用於保存那些長度比較小的回復,比如OK、簡短的字符串值、整數值、錯誤回復等等
- 可變大小的緩沖區用於保存那些長度比較大的回復,比如一個非常長的字符串值、一個由很多項組成的列表、一個包含了很多元素的集合等等
客戶端的固定大小緩沖區由buf和bufpos兩個屬性組合:
redis.h
typedef struct redisClient { …… int bufpos; char buf[REDIS_REPLY_CHUNK_BYTES]; } redisClient;
buf是一個大小為REDIS_REPLY_CHUNK_BYTES字節的字節數組,而bufpos屬性則記錄了buf數組目前已使用的字節數量。REDIS_REPLY_CHUNK_BYTES常量目前的默認值為16*1024,也即是說,buf數組的默認大小為16KB
圖1-8展示了一個使用固定大小緩沖區來保存返回值+OK\r\n的例子
圖1-8 固定大小緩沖區示例
當buf數組的空間已經用完,或者回復因為太大而無法裝進buf數組里面,服務器就會開始使用可變大小緩沖區。可變大小緩沖區由reply鏈表和一個或多個字符串對象組成:
redis.h
typedef struct redisClient { …… list *reply; …… } redisClient;
通過使用鏈表來連接多個字符串對象,服務器可以為客戶端保存一個非常長的命令回復,而不必受到固定大小緩沖區16KB大小的限制。圖1-9展示了一個包含三個字符串對象的reply鏈表
圖1-9 可變大小緩沖區示例
身份驗證
客戶端狀態的authenticated屬性用於記錄客戶端是否通過了身份驗證:
redis.h
typedef struct redisClient { …… int authenticated; /* when requirepass is non-NULL */ …… } redisClient;
如果authenticated的值為0,表示客戶端尚未通過身份認證;如果authenticated的值為1,表示客戶端已通過認認證。舉個栗子,對於一個尚未進程身份認證的客戶端來說,客戶端狀態的authenticated屬性如圖1-10所示
圖1-10 未驗證身份時的客戶端狀態
當客戶端authenticated屬性的值為0時,除了AUTH命令之外,客戶端發送的所有其他命令都會被服務器拒絕執行:
127.0.0.1:6379> PING (error) NOAUTH Authentication required. 127.0.0.1:6379> SET msg "hello world" (error) NOAUTH Authentication required.
當客戶端通過AUTH命令成功進行身份驗證之后,客戶端狀態authenticated屬性的值從0變為1,如圖1-11所示,這時客戶端就可以像往常一樣向服務器發送命令請求了:
127.0.0.1:6379> AUTH 123456 OK 127.0.0.1:6379> PING PONG 127.0.0.1:6379> SET msg "hello world" OK
圖1-11 已經通過身份驗證的客戶端狀態
authenticated屬性僅在服務器啟用了身份驗證功能時使用,如果服務器沒有啟用身份驗證功能的話,那么即使authenticated屬性的值為0(默認值),服務器也不會拒絕未驗證身份的客戶端發送的命令請求。關於服務器身份驗證的更多信息可以參考配置文件對requirepass選項的相關說明
時間
最后,客戶端還有幾個和時間相關的屬性:
redis.h
typedef struct redisClient { …… time_t ctime; time_t lastinteraction; time_t obuf_soft_limit_reached_time; …… } redisClient;
ctime屬性記錄了創建客戶端的時間,這個時間可以用來計算客戶端與服務器已經連接了多少秒,CLIENT list命令的age域記錄了這個秒數
127.0.0.1:6379> CLIENT list …… age=87315 …… …… age=535 ……
lastinteraction屬性記錄了客戶端與服務器最后一次進行互動(interaction)的時間,這里的互動可以是客戶端向服務器發送命令請求,也可以是服務器向客戶端發送命令回復。lastinteraction屬性可以用來計算客戶端的空轉時間,也即是,距離客戶端與服務器最后一次進行互動以來,已經過去多少秒,CLIENT list命令的idle域記錄了這個秒數
127.0.0.1:6379> CLIENT list …… idle=10916 …… …… idle=0 ……
obuf_soft_limit_reached_time屬性記錄了輸出緩沖區第一次達到軟性限制(soft time)的時間
客戶端的創建與關閉
服務器使用不同的方式來創建和關閉不同類型的客戶端,本節將介紹服務器創建和關閉客戶端的方法
創建普通客戶端
如果客戶端是通過網絡連接和服務器進行連接的普通客戶端,那么在客戶端使用connect函數連接到服務端時,服務器就會調用連接事件處理器,為客戶端創建相應的客戶端狀態,並將這個客戶端狀態添加到服務器狀態結構體中clients鏈表的末尾
舉個栗子,假設當前有c1和c2兩個普通客戶端正在連接服務器,那么當一個新的普通客戶端c3連接到服務器后,服務器會將c3所對應的客戶端狀態添加到clients鏈表的末尾,如圖1-12所示,其中用虛線包圍的就是服務器為c3新創建的客戶端狀態
圖1-12 服務器狀態結構體的clients鏈表
關閉普通客戶端
一個普通客戶端可以因為多種原因而被關閉:
- 如果客戶端進程退出或者被殺死,那么客戶端與服務器之間的網絡連接將被關閉,從而造成客戶端被關閉
- 如果客戶端向服務器發送了帶有不符合協議格式的命令請求,那么這個客戶端也會被服務器關閉
- 如果客戶端成為了CLIENTKILL命令的目標,那么它也會被關閉
- 如果用戶為服務器設置了timeout配置選項,那么當客戶端的空轉時間超過timeout選項設置的值時,客戶端將被關閉。不過timeout選項有一些例外情況:如果客戶端是主服務器(打開了REDIS_MASTER標志),從服務器(打開了REDIS_SLAVE標志),正在被BLPOP等命令阻塞(打開了REDIS_BLOCKED標志),或者正在執行SUBSCRIBE, PSUBSCRIBE等訂閱命令,那么即使客戶端的空轉時間超過了timeout選項的值,客戶端也不會被服務器關閉
- 如果客戶端發送的命令請求的大小超過了輸入緩沖區的限制大小(默認為1GB),那么這個客戶端會被服務器關閉
- 如果要發送給客戶端的命令回復的大小超過了輸出緩沖區的限制大小,那么這個客戶端會被服務器關閉
前面介紹輸出緩沖區的時候提到過,可變大小緩沖區由一個鏈表和任意多個字符串對象組成,理論上來說,這個緩沖區可以保存任意長度的命令回復。但為了避免客戶端回復過大,占用過多服務器資源,服務器會時刻檢查客戶端的輸出緩沖區的大小,並在緩沖區的大小超出范圍時,執行相應的限制操作。服務器使用兩種模式來限制客戶端輸出緩沖的大小:
- 硬性限制(hard limit):如果輸出緩沖區的大小超過了硬性限制所設置的大小,那么服務器立即關閉客戶端
- 軟性限制(soft limit):如果輸出緩沖區的大小超過了軟性限制所設置的大小,但還沒有超過硬性限制,那么服務器將使用服務器狀態結構的 obuf_soft_limit_reached_time 屬性記錄下客戶端到達軟性限制的起始時間,之后服務器會繼續監視客戶端,如果輸出緩沖區的大小一直超出軟性限制,並且持續時間超過服務器設定的時長,那么服務器就會關閉客戶端,相反地,如果客戶端在指 定時間內不再超出軟性限制,那么客戶端就不會被關閉,並且obuf_soft_limit_reached_time也會被清零
使用client-output-buffer-limit 可以為普通客戶端、從服務器客戶端、執行發布與訂閱功能的客戶端分別設置不同的軟性限制和硬性限制,格式為:
client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
以下是三個設置示例:
client-output-buffer-limit normal 0 0 0 #不限制輸出緩沖區大小 client-output-buffer-limit slave 256mb 64mb 60 client-output-buffer-limit pubsub 32mb 8mb 60 #執行發布和訂閱功能的客戶端硬性限制為32mb,軟性限制為8mb,軟性限制時長為60秒
第一行設置將普通客戶端的硬性限制和軟性限制都設置為0,表示不限制客戶端的輸出緩沖區大小
第二行設置將從服務器客戶端的硬性限制設置為256MB,而軟性限制設置為64MB,軟性限制的時長為60秒
第三行設置將執行發布與訂閱功能的客戶端的硬性限制設置為32MB,軟性限制設置為8MB,軟性限制的時長為60秒
關於client-output-buffer-limit選項的更多用法,可以參考示例配置文件redis.conf
Lua腳本的偽客戶端
服務器會在初始化時創建負責執行Lua腳本中包含的Redis命令的偽客戶端,並將這個偽客戶端關聯在服務器結構的lua_client屬性中:
redis.h
struct redisServer { …… redisClient *lua_client; …… };
lua_client偽客戶端在服務器運行的整個生命周期中會一直存在,只有服務器被關閉時,這個客戶端才會被關閉
AOF文件的偽客戶端
服務器在載入AOF文件時,會創建用於執行AOF文件包含的Redis命令的偽客戶端,並在載入完成之后,關閉這個偽客戶端