9.redis學習筆記-客戶端&伺服器.md
12. 客戶端
12.1. 客戶端屬性
客戶端狀態包含的屬性可以分為兩類:
- 通用屬性,很少與特定功能相關,無論客戶端執行什麼工作,都要用到這些屬性
- 與特定功能相關的屬性,比如操作資料庫要用到的db屬性和dictid屬性
12.1.1. 套接字描述
客戶端狀態的fd屬性記錄客戶端正在使用的套接字描述符:
typedef struct redisClient{
// ...
int fd;
// ...
}redisClient;
fd屬性的值根據客戶端型別不同而取不同的值:
- -1 :偽客戶端,偽客戶端處理的命令請求來源於AOF檔案或者Lua指令碼,而不是網路,所以這種客戶端不需要套接字連線,所以不需要記錄套接字描述符。-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 ...
12.1.2. 名字
預設情況下,連線到伺服器的客戶端是沒有名字的。
比如上面的執行CLIENT list
例子上,兩個客戶端的name域
都是空白的
使用CLIENT setname
客戶端的名字記錄在客戶端狀態的name屬性
中:
typedef struct redisClient{
// ...
robj *name;
// ...
}redisClient;
如果客戶端沒有為自己設定名字,客戶端狀態的name屬性
指向NULL
,設定了,就指向一個字串物件。
12.1.3. 標誌
客戶端的標誌屬性flags
記錄了客戶端的角色,以及客戶端目前所處的狀態:
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
標誌, 否則傳送操作會被拒絕執行。
12.1.4. 輸入緩衝區
輸入緩衝區用於儲存客戶端傳送的命令請求:
typedef struct redsiClient{
// ...
sds querybuf;
// ...
}redisClient;
輸入緩衝區的大小會根據內容動態地縮小或者擴大,但它的最大大小不能超過1GB,否則伺服器將關閉這個客戶端。
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]
的值,在命令表中查詢命令所對應的命令實現函式。
命令表是一個字典,鍵時SDS結構,儲存命令名字,值是所對應的redisCommand
結構,這個結構儲存了命令的實現函式、命令的標誌、命令應該給定的引數個數、命令的總執行次數和總消耗次數等資訊。
當程式在命令表中成功找到 argv[0]
所對應的 redisCommand
結構時, 它會將客戶端狀態的 cmd
指標指向這個結構:
typedef struct redisClient {
// ...
struct redisCommand *cmd;
// ...
} redisClient;
之後, 伺服器就可以使用 cmd
屬性所指向的 redisCommand
結構, 以及 argv
、 argc
屬性中儲存的命令引數資訊, 呼叫命令實現函式, 執行客戶端指定的命令。
12.1.7. 輸出緩衝區
執行命令後得到的命令回覆會被儲存到客戶端狀態的輸出緩衝區裡,每個客戶端都有兩個緩衝區可用,一個固定大小緩衝區(儲存長度較小的回覆),一個可變大小緩衝區(板寸長度較大的回覆)。
固定大小緩衝區:
typedef struct redisClient{
// ...
// REDIS_REPLY_CHUNK_BYTES 預設大小為16*1024 也就是16K
char bug[REDIS_REPLY_CHUNK_BYTES];
int bufpos;//記錄目前buf陣列已使用的位元組數量
// ...
}redisClient;
可變大小緩衝區:
typedef struct redisClient{
// ...
// 通過連結串列連線多個字串物件,用於儲存長的命令回覆
list *reply;
// ...
}redisClient;
12.1.8. 身份驗證
客戶端狀態的 authenticated
屬性記錄客戶單是否通過了身份驗證:
typedef struct redisClient{
// ...
int authenticated;
// ...
}redisClient;
-
0:未通過驗證
除了
AUTH
命令之外,客戶端傳送的所有命令都會被伺服器拒絕 -
1:通過驗證
12.1.9. 時間
客戶端有幾個和時間相關的屬性:
typedef struct redisClient{
// ...
time_t ctime;
time_t lastinteraction;
time_t obuf_soft_limit_reached_time;
// ...
}redisClient;
ctime
屬性記錄了建立客戶端的時間,用於計算客戶端與伺服器已經連線了多少秒,CILENT list
命令的age
域記錄了這個秒數。lastinteraction
屬性記錄客戶端與伺服器最後一次互動的時間(互動指的是客戶端向伺服器傳送命令請求,或者伺服器向客戶端傳送命令回覆)obuf_soft_limit_reached_time
屬性記錄了輸出緩衝區第一次到達軟體限制的時間
12.2. 客戶端的建立和關閉
12.2.1. 建立普通客戶端
通過網路連線與伺服器進行連線的客戶端是普通客戶端,使用connect
函式連線伺服器的時候,伺服器會呼叫事件處理器,為客戶端建立相應的客戶端狀態,並將這個新的客戶端狀態新增到伺服器狀態結構clients
連結串列的末尾。
例子:
c1,c2正在連線伺服器,c3是一個新的普通客戶端,連線到伺服器:
12.2.2. 關閉普通客戶端
普通客戶端關閉的原因:
-
客戶端程序退出或被殺死,導致客戶端伺服器之間的網路連線關閉
-
客戶端向傳送了不符合協議格式的命令請求
-
客戶端是
CLIENT KILL
命令的目標 -
使用者為伺服器設定了
timeout
選項,當客戶端的空轉時間超過timeout
選項設定的值時,客戶端將會被關閉例外:客戶端是主伺服器(打開了
REDIS_MASTER
標誌),從伺服器(打開了REDIS_SLAVE
標誌),正在被BLPOP
等命令阻塞(打開了REDIS_BLOCKED
標誌)或者正在執行SUBSCRIBR
、PSUBSCRIBE
等訂閱命令,那麼即使客戶端的timeout
時間,客戶端也不會別伺服器關閉 -
客戶端傳送的命令請求的代銷超過了輸入緩衝區的顯示大小(預設1GB)
-
命令回覆超過了輸出緩衝區的限制大小
注:學習輸出緩衝區時,講到了可變大小緩衝區,原則上是可以儲存任意長的命令回覆,但是為了避免客戶端的回覆過大,佔用過多的伺服器資源,所以伺服器會在緩衝區大小超出範圍之後,執行相應的限制操作:
-
硬性限制
如果輸出緩衝區超過了硬性限制,立馬關閉客戶端
-
軟性限制
輸出緩衝區超過了軟性限制,而沒超過硬性限制,那麼伺服器將使用客戶端狀態結構的
obuf_soft_limit_reached_time
屬性記錄客戶端達到軟性限制的起始時間,監視客戶端,如果輸出緩衝區一直超過軟性限制,並且持續時間超過伺服器設定的時長,那麼伺服器將關閉客戶端,相反,在指定時間內,不再超過軟性限制,那麼客戶單不會被關閉,並且obuf_soft_limit_reached_time
的值會被清零。
使用client-output-buffer-limit
選項為普通、從伺服器、執行釋出與訂閱功能的客戶單分別設定不同的軟性或者硬性限制。
命令格式:client-output-buffer-limit <class> <hard limt> <soft limit< <soft seconds>
12.2.3. Lua指令碼的偽客戶端
伺服器會在初始化建立負責執行Lua指令碼中包含的Redis命令的偽客戶端,並將這個偽客戶端關聯在伺服器狀態結構的lua_client
屬性中。
lua_client
偽客戶端會在伺服器執行的整個生命週期中一直存在,當伺服器關閉時,關閉。
12.2.4. AOF檔案的偽客戶端
在載入AOF完成之後,關閉這個偽客戶端
13. 伺服器
Redis的伺服器負責與多個客戶端建立網路連線,處理客戶端傳送的命令請求,在資料庫中儲存客戶端執行命令所產生的資料,並通過資源管理來維持伺服器自身的運轉。
13.1. 命令請求的執行過程
13.1.1. 傳送命令請求
當用戶在客戶端輸入一個命令請求時,客戶端會將這個命令請求轉換成協議格式,然後通過連線到伺服器的套接字,將協議格式的命令請求傳送給伺服器:
13.1.2. 讀取命令請求
當客戶端與伺服器之間的連線套接字因為客戶單的寫入變成可讀是:伺服器會呼叫命令請求處理器執行:
- 讀取套接字中協議格式的命令請求,並儲存到客戶端狀態的輸入緩衝區中
- 對輸入緩衝區中的命令請求進行分析,提取出命令請求中包含的命令引數,以及命令引數的個數,然後分別將引數和引數個數儲存到客戶端狀態的
argv
和argc
屬性中去 - 呼叫命令執行器,執行客戶端指定的命令
13.1.3. 命令執行器(1):查詢命令實現
命令執行器的第一件事是根據客戶端狀態的argv[0]
引數,在命令表中查詢引數所指定的命令,並將找到哦啊的命令儲存到客戶端的cmd
屬性中。
命令表是一個字典,鍵是命令的名字,值是redisCommand
結構,每個redisCommand
記錄了一個Redis命令的實現資訊。
下表 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" 和 "hello world" 。 |
sflags |
char * |
字串形式的標識值, 這個值記錄了命令的屬性, 比如這個命令是寫命令還是讀命令, 這個命令是否允許在載入資料時使用, 這個命令是否允許在 Lua 指令碼中使用, 等等。 |
flags |
int |
對 sflags 標識進行分析得出的二進位制標識, 由程式自動生成。 伺服器對命令標識進行檢查時使用的都是 flags 屬性而不是 sflags 屬性, 因為對二進位制標識的檢查可以方便地通過 & 、 ^ 、 ~ 等操作來完成。 |
calls |
long long |
伺服器總共執行了多少次這個命令。 |
milliseconds |
long long |
伺服器執行這個命令所耗費的總時長。 |
下表 列出了 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 |
注:命令名字不區分大小寫
13.1.4. 命令執行器(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 四個命令, 其他命令都會被放進事務佇列中。
- 如果伺服器打開了監視器功能, 那麼伺服器會將要執行的命令和引數等資訊傳送給監視器。
當完成了以上預備操作之後, 伺服器就可以開始真正執行命令了。
13.1.5. 命令執行器(3):呼叫命令的實現函式
當伺服器要執行命令時,它執行以下語句:
client->cmd->proc(client);
13.1.6. 命令執行器(4):執行後續操作
在執行完實現函式之後, 伺服器還需要執行一些後續工作:
- 如果伺服器開啟了慢查詢日誌功能, 那麼慢查詢日誌模組會檢查是否需要為剛剛執行完的命令請求新增一條新的慢查詢日誌。
- 根據剛剛執行命令所耗費的時長, 更新被執行命令的
redisCommand
結構的milliseconds
屬性, 並將命令的redisCommand
結構的calls
計數器的值增一。 - 如果伺服器開啟了 AOF 持久化功能, 那麼 AOF 持久化模組會將剛剛執行的命令請求寫入到 AOF 緩衝區裡面。
- 如果有其他從伺服器正在複製當前這個伺服器, 那麼伺服器會將剛剛執行的命令傳播給所有從伺服器。
13.1.7. 將命令回覆傳送給客戶端
13.1.8. 客戶端接收並列印命令回覆
13.2. serverCron
函式
serverCron
函式每100毫秒執行一次,負責管理伺服器資源,並保持伺服器自身的良好運轉。
13.2.1. 更新伺服器時間快取
Redis 伺服器狀態中unixtime
屬性和mstime
屬性被用作當前時間的快取,serverCron
函式每100毫秒更新一次這個資料。
13.2.2. 更新LRU時鐘
伺服器狀態中的lruclock
屬性儲存伺服器的LRU時鐘,是伺服器時間快取之一。每個物件都有,儲存了物件最後一次被命令訪問的時間。當伺服器要計算一個數據庫鍵的空轉時間,程式會用伺服器的lruclock
屬性的值減去物件的lru
屬性的值,得出的結果就是這個空轉時間。
serverCron
每10秒更新一次lruclock
的值
13.2.3. 更新伺服器每秒執行命令次數
13.2.4. 更新伺服器記憶體峰值記錄
13.2.5. 處理SIGTERM
訊號
13.2.6. 管理客戶端資源
13.2.7. 管理資料庫資源
13.2.8. 執行被延遲的BGREWRITEAOF
13.2.9. 檢查持久化操作的執行狀態
13.2.10. 將AOF緩衝區的內容寫入到AOF檔案
13.2.11. 關閉非同步客戶端
13.2.12. 增加cronloops
計數器的值
13.3. 初始化伺服器
13.3.1. 初始化伺服器狀態結構
第一步就是建立一個struct redisServer
型別的例項變數server
作為伺服器的狀態,併為結構中的各個屬性設定預設值。
初始化server
變數的工作由redis.c/initServerConfig
函式完成,部分程式碼如下:
void initServerConfig(void){
// 設定伺服器的執行id
getRandomHexChars(server.runid,REDIS_RUN_ID_SIZE);
// 為執行id加上結尾字元
server.runid[REDIS_RUN_ID_SIZE] = '\0';
// 設定預設配置檔案路徑
server.configfile = NULL;
// 設定預設伺服器頻率
server.hz = REDIS_DEFAULT_HZ;
// 設定伺服器執行架構
server.arch_bits = (sizeof(long) == 8) ? 64 : 32;
// 設定預設伺服器埠
server.port = REDIS_SERVERPORT;
// ...
}
除了上面的工作,還有部分工作是:
- 設定伺服器的預設RDB持久化條件和AOF持久化條件
- 初始化伺服器的LRU始終
- 建立命令表
13.3.2. 載入配置選項
啟動伺服器的時候,使用者可以通過給定配置引數或者指定配置檔案來修改伺服器的預設配置。
13.3.3. 初始化伺服器資料結構
執行了initServerConfig
函式初始化server
結構時,程式只建立了命令表這一個資料結構。
當前面步驟執行完後,執行到這個步驟時,伺服器將呼叫initServer
函式,為其餘相應的資料結構分配記憶體,如有需要為這些資料結構設定或者關聯初始化值。
之所有現在才初始化這些資料結構,是因為伺服器必須先載入使用者指定的配置選項,才能正確地對這些資料結構進行初始化。
13.3.4. 還原資料庫狀態
初始化完server
變數之後,伺服器會載入RDB檔案或者AOF檔案,並根據檔案內容來還原伺服器的資料庫狀態。
根據伺服器是否啟用了AOF持久化功能,伺服器載入資料所使用的目標檔案有所不同:
- 如果開啟了,那麼載入AOF檔案來還原資料庫狀態
- 如果沒開啟,那麼伺服器使用RDB檔案來還原資料庫狀態