Redis事件和服務(wù)器
事件
Redis是個單線程的,但是速度非???,其主要原因是因為它是基于事件的,是一個事件驅(qū)動程序,了解NIO的應(yīng)該都知道這種方式。
Redis服務(wù)器需要處理兩類事件。
- 文件事件(file event): Redis服務(wù)器通過套接字與客戶端進行連接,而文件事件就是服務(wù)器對套接字操作的抽象。服務(wù)器與客戶端的通信會產(chǎn)生相應(yīng)的文件事件,而服務(wù)器通過監(jiān)聽并處理這些事件來完成一系列網(wǎng)絡(luò)通信操作。
- 時間事件(file event): Redis服務(wù)器中的一些操作(比如serverCron函數(shù))需要在給定的時間點執(zhí)行,而時間事件就是服務(wù)器對這類定時操作的抽象。
文件事件
Redis基于Reactor模式開發(fā)了自己的網(wǎng)絡(luò)事件處理器,則個處理器被稱為文件時間處理器(file event handler):
- 文件事件處理器采用I/O多路復用(multiplexing)程序來同時監(jiān)聽多個套接字,并根據(jù)套接字目前嶄新的任務(wù)來為套接字關(guān)聯(lián)不同的事件處理器。
- 當被監(jiān)聽的套接字準備好連接應(yīng)答(accept),讀取(read),寫入(write),關(guān)閉(close)等操作時,與操作對應(yīng)的文件事件就會產(chǎn)生,這是文件時就處理器就會調(diào)用套接字之前關(guān)聯(lián)號的事件處理器來處理這些事件。
構(gòu)成
文件時間處理器由四個部分組成,分別是套接字。I/O多路復用程序,文件事件分發(fā)器(dispatcher)和事件處理器。

文件事件是對套接字操作的抽象,每當一個套接字準備好執(zhí)行連接應(yīng)答,寫入,讀取,關(guān)閉等操作時,就會產(chǎn)生一個文件時間。因為一個服務(wù)器會連接多個套接字,所有文件事件可能并發(fā)出現(xiàn)。
I/O多路復用程序負責監(jiān)聽多個套接字,并向文件事件分發(fā)器傳送那些產(chǎn)生了時間的套接字。I/O多路復用程序總是將所有產(chǎn)生時間的套接字放到一個隊列里面,然后通過這個隊列,以有序,同步,每次一個套接字的方式向文件時間分發(fā)器傳送套接字。

文件事件分發(fā)器接收I/O多路復用程序傳來的套接字,并根據(jù)套接字產(chǎn)生的時間類型,調(diào)用相應(yīng)的事件處理器。
服務(wù)器會為執(zhí)行不同的任務(wù)的套接字關(guān)聯(lián)不同的事件處理器,這些處理器是一個個函數(shù),它們定義某個時間發(fā)生時,服務(wù)器應(yīng)該執(zhí)行的操作。
Tips: Redis的I/O多路復用程序的所有功能都是通過包裝常見的select,epoll,evport和kqueue這些I/O多路復用函數(shù)庫來實現(xiàn)的,并且為每個多路復用函數(shù)庫都實現(xiàn)了相同的API,因此底層是可以互換的。在編譯的時候,會自動選擇系統(tǒng)中性能最高的I/O多路復用函數(shù)庫來作為底層實現(xiàn)。
客戶端發(fā)起請求示例:

時間事件
Redis的時間事件也有兩類:
- 定時事件: 讓一段程序在指定的時間之后執(zhí)行一次。
- 周期性事件: 讓一段程序每隔指定時間就執(zhí)行一次。
所有的時間事件都放在一個無序鏈表中,每當事件執(zhí)行器運行時,它就遍歷整個鏈表,查找所有已到達的時間事件,并調(diào)用相應(yīng)的事件處理器。(這里的無序是指事件到達時間無序)
serverCron函數(shù)
serverCron函數(shù)負責定期對Redis的資源和狀態(tài)進行檢查和調(diào)整,主要工作包括:
- 更新服務(wù)器的各類統(tǒng)計信息,比如時間,內(nèi)存占用,數(shù)據(jù)庫占用情況等。
- 清理數(shù)據(jù)庫中的過期鍵值對。
- 關(guān)閉和清理連接失效的客戶端。
- 嘗試進行AOF或RDB持久化操作。
- 如果是master,那么對從服務(wù)進行定期同步。
- 如果處于cluster,對集群進行定期同步和連接測試。
該函數(shù)默認執(zhí)行時間是每秒10次,可以通過調(diào)整配置hz的值來改變,這個值代表的是每秒執(zhí)行的次數(shù)哦!
hz 10
更新服務(wù)器時間緩存
Redis服務(wù)器中有不少功能要獲取系統(tǒng)當前時間,而每次獲取都需要執(zhí)行一次系統(tǒng)調(diào)用,為了減少系統(tǒng)調(diào)用次數(shù),服務(wù)器狀態(tài)中額unixtime屬性和mstime被用作當前時間緩存。
struct redisServer {
// ...
// 保存秒級進度的系統(tǒng)當前UNIX時間戳
time_t unixtime;
// 保存毫秒級進度的系統(tǒng)當前UNIX時間戳
long long mstime;
// ...
};
因為函數(shù)每秒運行10次,100毫秒一次,所以這兩個屬性的精確度不高。
- 服務(wù)器只會在打印日志,更細那服務(wù)器的LRU時鐘,決定好似否執(zhí)行持久化任務(wù)、計算服務(wù)器上線時間這類對時間精確度要求不高的功能上。
- 對于為鍵設(shè)置過期時間,添加慢查詢?nèi)罩具@種需要高精確度時間的功能來說,服務(wù)器還是會再次執(zhí)行系統(tǒng)調(diào)用,從而獲得最準確的系統(tǒng)當前時間。
更新LRU時鐘
服務(wù)器狀態(tài)中的lruclock屬性保存了服務(wù)器的LRU始終,這個屬性也是服務(wù)器時間緩存的一種,用來計算對象空轉(zhuǎn)時長。
struct redisServer {
// ...
// 默認每秒更新一次
unsigned lruclock:22;
//...
}
每個redis對象有l(wèi)ru屬性,這個屬性保存了對象最后一次被命令訪問的時間。
空轉(zhuǎn)時間=lruclock - lru.
命令:
OBJECT IDLETIME key: 查看對象空轉(zhuǎn)時長
INFO server: 可以輸出server中l(wèi)ruclock的值
更新服務(wù)器每秒執(zhí)行命令次數(shù)
redis> INFO stats
# Stats
...
instantaneous_ops_per_sec: 558
...
以上命令結(jié)果顯示,在最近一秒之內(nèi),服務(wù)器大概處理了558個命令。
更新服務(wù)器內(nèi)存峰值記錄
服務(wù)器stat_peak_memory屬性記錄了服務(wù)器內(nèi)存峰值大小。
redis> INFO memory
# Memory
...
used_memory_peak:6180016
used_memory_peak_human:5.89M
...
以上命令查看內(nèi)存峰值大小。
處理SIGTERM信號
啟動服務(wù)器時,Redis會為進程的SIGTERM信號關(guān)聯(lián)處理器(一個函數(shù)),這個信號處理器負責在服務(wù)器接收到SIGTERM信號時,打開服務(wù)器狀態(tài)的shutdown_asap標識。
每次serverCron函數(shù)運行時,程序都會對服務(wù)器狀態(tài)的shutdown_asap屬性進行檢查,如果值為1,則服務(wù)器會先進行RDB持久化操作,然后關(guān)閉服務(wù)器。
管理客戶端資源
主要是檢查客戶端:
- 如果客戶端與服務(wù)器之間連接已經(jīng)超時,則釋放這個客戶端。
- 如果客戶端在上一次執(zhí)行命令請求之后,輸入緩沖區(qū)的大小超過了一定長度,那么程序或釋放客戶端當前的輸入緩沖區(qū),并重新創(chuàng)建一個默認大小的輸入緩沖區(qū),從而防止客戶端你的輸入緩沖區(qū)耗費過多的內(nèi)存。
管理數(shù)據(jù)庫資源
這個部分就主要是檢查服務(wù)器中的一部分數(shù)據(jù)庫,刪除其中的過期鍵,并在有需要時,對字典(hash表)進行收縮操作。
執(zhí)行被延遲的BGREWRTEAOF
在服務(wù)器執(zhí)行BGSAVE命令期間,如果客戶端向服務(wù)器發(fā)來BGREWRITEAOF命令,那么服務(wù)器會將BGREWRITEAOF命令的執(zhí)行時間延遲到BGSAVE命令執(zhí)行完畢之后。
服務(wù)器的aof_rewrite_scheduled屬性記錄了是否延遲了BGREWRITEAOF命令。
struct redisServer {
// ...
// 值為1表示有BGREWRITEAOF命令被延遲了
int aof_rewrite_scheduled;
// ...
}
serverCron命令運行時都會檢查BGSAVE和BGREWRITEAOF命令是否在執(zhí)行,如果都沒有執(zhí)行,那么檢查服務(wù)器的aof_rewrite_scheduled屬性,如果值為1,那么就執(zhí)行延遲的BGREWRITEAOF命令.
檢查持久化操作的運行狀態(tài)
服務(wù)器狀態(tài)使用rdb_child_pid和aof_child_pid屬性記錄執(zhí)行BGSAVE命令和BGREWRITEAOF命令的子進程ID,這兩個屬性也可以用于檢查對應(yīng)命令是否正在執(zhí)行:
struct redisServer {
// ...
// 記錄執(zhí)行BGSAVE命令的子進程ID
// 如果沒有執(zhí)行,值為-1
pid_t rdb_child_pid;
// 記錄執(zhí)行BGREWRITEAOF命令的子進程ID
// 如果沒有執(zhí)行,值為-1
pid_t aof_child_pid;
// ...
}
serverCron函數(shù)每次執(zhí)行時,程序都會檢查這兩個屬性的值,只要其中一個屬性不為-1,程序就會檢查子進程是否有信號發(fā)來服務(wù)器進程(調(diào)用其他函數(shù))。
- 如果有信號到達,那么表示新的RDB文件已經(jīng)生成完畢或者AOF文件已經(jīng)重寫完畢,服務(wù)器需要進行后續(xù)操作,比如新的AOF替換舊的AOF文件。
- 如果沒有信號到達,表示持久化操作未完成,程序不做動作。
如果這兩個屬性的值都為-1,那么做三個檢查:
- 檢查是否有BGREWRITEAOF命令被延遲,如果有, 開始執(zhí)行命令。
- 檢查自動保存條件是否滿足,如果滿足,且服務(wù)器當前沒有執(zhí)行其他持久化操作,那么開始一次新的BGSAVE操作(因為上個檢查可能會引發(fā)一次新的BGREWRITEAOF,所有這次檢查中,程序會再次確認是否已經(jīng)在執(zhí)行持久化操作了)。
- 檢查服務(wù)器設(shè)置的AOF重寫條件是否滿足,如果條件滿足,并且服務(wù)器沒有執(zhí)行其他持久化操作,那么開始一次新的BGREWRITEAOF操作(也會挨次確認,因為上兩個檢查都可能會引發(fā)新的持久化操作)。
圖解:

將AOF緩沖區(qū)的內(nèi)容寫入AOF文件
如果服務(wù)器開啟了AOF持久化功能,并且AOF緩沖區(qū)里面還有待寫入的數(shù)據(jù),那么serverCron函數(shù)會調(diào)用相應(yīng)的程序,將AOF緩沖區(qū)中的內(nèi)容寫入到AOF文件。
關(guān)閉異步客戶端
這一步,服務(wù)器會關(guān)閉輸出緩沖區(qū)大小超出限制的客戶端。
客戶端的輸入緩沖區(qū)用于把醋你客戶端發(fā)送的命令請求:
typedef struct redisClient {
// ...
sds querybuf;
// ...
}
這個緩沖區(qū)中保存的是協(xié)議的值。
set mKey mValue
querybuf的值為
*3\r\n$3\r\nSET\r\n$4\r\nmKey\r\n$6\r\nmValue\r\n
輸入緩沖區(qū)的大小會根據(jù)內(nèi)容動態(tài)的縮小或者擴大,但它的值不能超過1G,否則服務(wù)器將關(guān)閉這個客戶端。
增加cronloops計數(shù)器的值
服務(wù)器狀態(tài)的cronloops屬性記錄了serverCron函數(shù)執(zhí)行的次數(shù)
struct redisServer {
// ...
// serverCron函數(shù)沒執(zhí)行一次,值就加1
int cronloops;
// ...
}
這個屬性目前在服務(wù)器中的唯一作用,就是在復制模塊中實現(xiàn)沒執(zhí)行N次就執(zhí)行一次指定代碼的功能。
if (cronloops % n == 0) {
// 指定代碼,這個指定的代碼應(yīng)該是服務(wù)器處理的一些事了,每隔多少次就處理一次
}
服務(wù)器
命令請求過程
命令請求的時候,在客戶端輸入命令的時候,客戶端會將這個命令轉(zhuǎn)換成協(xié)議,然后將協(xié)議內(nèi)容發(fā)送給服務(wù)器。
協(xié)議格式
*<參數(shù)數(shù)量> CR LF
$<參數(shù) 1 的字節(jié)數(shù)量> CR LF
<參數(shù) 1 的數(shù)據(jù)> CR LF
...
$<參數(shù) N 的字節(jié)數(shù)量> CR LF
<參數(shù) N 的數(shù)據(jù)> CR LF
注意:CRLF就是換行 \r\n
socket發(fā)送協(xié)議
我們先來試試:
cmd:
set mKey mValue
protocal:
*3\r\n$3\r\nSET\r\n$4\r\nmKey\r\n$6\r\nmValue\r\n
然后我們直接用java socket來發(fā)送協(xié)議給服務(wù)器。
public static void main(String[] args) throws Exception {
Socket socket = new Socket("192.168.2.11", 16379);
//獲取輸出流,向socket寫數(shù)據(jù)
OutputStream outputStream = socket.getOutputStream();
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.print("*3\r\n$3\r\nSET\r\n$4\r\nmKey\r\n$6\r\nmValue\r\n");
printWriter.flush();
//關(guān)閉輸出流
socket.shutdownOutput();
//讀取服務(wù)器返回的socket的數(shù)據(jù)
InputStream inputStream = socket.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String info = "";
String temp = null;
while ((temp = bufferedReader.readLine()) != null) {
info += temp;
System.out.println("客戶端接收服務(wù)端發(fā)送信息:" + info);
}
//關(guān)閉相對應(yīng)的資源
bufferedReader.close();
inputStream.close();
printWriter.close();
outputStream.close();
socket.close();
}
執(zhí)行結(jié)果:
客戶端接收服務(wù)端發(fā)送信息:+OK
Process finished with exit code 0
上服務(wù)器檢查是否有值:
redis> get mKey
"mValue"
服務(wù)器初始化過程
初始化服務(wù)器狀態(tài)
第一步就是創(chuàng)建一個strct redisServer類型的實例變量作為服務(wù)器狀態(tài),并為結(jié)構(gòu)中的各個屬性設(shè)置默認值。主要包括以下部分:
- 設(shè)置服務(wù)器的運行ID。
- 設(shè)置服務(wù)器的默認運行頻率。
- 設(shè)置服務(wù)器的默認配置文件路徑。
- 設(shè)置服務(wù)器的運行架構(gòu)。
- 設(shè)置服務(wù)器的默認端口號。
- 設(shè)置服務(wù)器的默認RDB持久化條件和AOF持久化條件。
- 初始化服務(wù)器的LRU時鐘。
- 創(chuàng)建命令表。
當初始化之后,就進入下一步,載入配置項。
載入配置項
在啟動服務(wù)器時,用戶可以通過給定配置參數(shù)或者指定配置文件來修改服務(wù)器的默認配置,比如
redis-server --port 16379
那么我們就通過給定配置參數(shù)的方式修改了服務(wù)器的運行端口號,當然也可以在配置文件中配置。
如果用戶為某些屬性設(shè)置了新值,那么服務(wù)器就使用用戶指定的值來更新相應(yīng)的屬性,如果沒有設(shè)置新值,那么就是用上一步初始化的默認值。
載入配置后,進入下一步,初始化服務(wù)器數(shù)據(jù)結(jié)構(gòu)。
初始化服務(wù)器數(shù)據(jù)結(jié)構(gòu)
在第一步的時候,服務(wù)器只創(chuàng)建了命令表一個數(shù)據(jù)結(jié)構(gòu),除了命令表之外,服務(wù)器還包括其他數(shù)據(jù)結(jié)構(gòu),比如server.clients鏈表,server.db數(shù)組等。
除了初始化這些數(shù)據(jù)結(jié)構(gòu)之外,還需要做一些重要的設(shè)置操作,包括:
- 為服務(wù)器設(shè)置進程信號處理器。
- 創(chuàng)建共享對象,比如包含"OK","ERR"回復的字符串對象。
- 打開服務(wù)器的監(jiān)聽端口,并未監(jiān)聽套接字關(guān)聯(lián)連接應(yīng)答處理器,等待服務(wù)器正式運行時接受客戶端的連接。
- 為serverCron函數(shù)創(chuàng)建時間事件,等待服務(wù)器正式運行時執(zhí)行serverCron函數(shù)。
- 如果AOF持久化功能打開,那么打開現(xiàn)有的AOF文件,如果不存在,那么創(chuàng)建一個新的AOF文件,并未AOF寫入做好準備。
- 初始化服務(wù)器的后臺I/O模塊,為將來的I/O操作做好準備。
這步操作完成之后,進入下一步,還原數(shù)據(jù)庫狀態(tài)。
還原數(shù)據(jù)庫狀態(tài)
在完成率server變量的初始化之后,服務(wù)器還需要載入RDB文件或者AOF文件,并根據(jù)文件記錄的內(nèi)容來還原服務(wù)器的數(shù)據(jù)庫狀態(tài)。RDB文件的優(yōu)先級沒有AOF文件高!有AOF文件的話RDB文件就不會被使用了。
- 如果啟用了AOF持久化功能,那么服務(wù)器使用AOF文件來還原數(shù)據(jù)庫狀態(tài)。
- 如果沒有啟用AOF持久化功能,那么使用RDB文件來還原數(shù)據(jù)庫狀態(tài)。
數(shù)據(jù)庫狀態(tài)還原之后,開始執(zhí)行最后一步,執(zhí)行事件循環(huán)。
執(zhí)行事件循環(huán)
這是初始化的最后一步,服務(wù)器開始執(zhí)行服務(wù)器的事件循環(huán)(loop),這個時候服務(wù)器的初始化就完成了,可以開始接受客戶端的連接請求,并處理客戶端發(fā)來的命令請求了。
over~~~