事件是 Redis 服務(wù)器的核心,它處理兩項重要的任務(wù):
本文以下內(nèi)容就來介紹這兩種事件,以及它們背后的運(yùn)作模式。
Redis 服務(wù)器通過在多個客戶端之間進(jìn)行多路復(fù)用,從而實現(xiàn)高效的命令請求處理:多個客戶端通過套接字連接到 Redis 服務(wù)器中,但只有在套接字可以無阻塞地進(jìn)行讀或者寫時,服務(wù)器才會和這些客戶端進(jìn)行交互。
Redis 將這類因為對套接字進(jìn)行多路復(fù)用而產(chǎn)生的事件稱為文件事件(file event),文件事件可以分為讀事件和寫事件兩類。
讀事件標(biāo)志著客戶端命令請求的發(fā)送狀態(tài)。
當(dāng)一個新的客戶端連接到服務(wù)器時,服務(wù)器會給為該客戶端綁定讀事件,直到客戶端斷開連接之后,這個讀事件才會被移除。
讀事件在整個網(wǎng)絡(luò)連接的生命期內(nèi),都會在等待和就緒兩種狀態(tài)之間切換:
作為例子,下圖展示了三個已連接到服務(wù)器、但并沒有發(fā)送命令的客戶端:
server [dir=none, style=dotted, label="等待命令請求"]; cy -> server [dir=none, style=dotted, label="等待命令請求"]; cz -> server [dir=none, style=dotted, label="等待命令請求"];}" />
這三個客戶端的狀態(tài)如下表:
客戶端 | 讀事件狀態(tài) | 命令發(fā)送狀態(tài) |
---|---|---|
客戶端 X | 等待 | 未發(fā)送 |
客戶端 Y | 等待 | 未發(fā)送 |
客戶端 Z | 等待 | 未發(fā)送 |
之后,當(dāng)客戶端 X 向服務(wù)器發(fā)送命令請求,并且命令請求已到達(dá)時,客戶端 X 的讀事件狀態(tài)變?yōu)榫途w:
server [style= "dashed, bold" , label="發(fā)送命令請求", color = "#B22222"]; cy -> server [dir=none, style=dotted, label="等待命令請求"]; cz -> server [dir=none, style=dotted, label="等待命令請求"];}" />
這時,三個客戶端的狀態(tài)如下表(只有客戶端 X 的狀態(tài)被更新了):
客戶端 | 讀事件狀態(tài) | 命令發(fā)送狀態(tài) |
---|---|---|
客戶端 X | 就緒 | 已發(fā)送,并且已到達(dá) |
客戶端 Y | 等待 | 未發(fā)送 |
客戶端 Z | 等待 | 未發(fā)送 |
當(dāng)事件處理器被執(zhí)行時,就緒的文件事件會被識別到,相應(yīng)的命令請求會被發(fā)送到命令執(zhí)行器,并對命令進(jìn)行求值。
寫事件標(biāo)志著客戶端對命令結(jié)果的接收狀態(tài)。
和客戶端自始至終都關(guān)聯(lián)著讀事件不同,服務(wù)器只會在有命令結(jié)果要傳回給客戶端時,才會為客戶端關(guān)聯(lián)寫事件,并且在命令結(jié)果傳送完畢之后,客戶端和寫事件的關(guān)聯(lián)就會被移除。
一個寫事件會在兩種狀態(tài)之間切換:
當(dāng)客戶端向服務(wù)器發(fā)送命令請求,并且請求被接受并執(zhí)行之后,服務(wù)器就需要將保存在緩存內(nèi)的命令執(zhí)行結(jié)果返回給客戶端,這時服務(wù)器就會為客戶端關(guān)聯(lián)寫事件。
作為例子,下圖展示了三個連接到服務(wù)器的客戶端,其中服務(wù)器正等待客戶端 X 變得可寫,從而將命令的執(zhí)行結(jié)果返回給它:
server [dir=none, style=dotted, label="等待將命令結(jié)果返回\n等待命令請求"]; cy -> server [dir=none, style=dotted, label="等待命令請求"]; cz -> server [dir=none, style=dotted, label="等待命令請求"];}" />
此時三個客戶端的事件狀態(tài)分別如下表:
客戶端 | 讀事件狀態(tài) | 寫事件狀態(tài) |
---|---|---|
客戶端 X | 等待 | 等待 |
客戶端 Y | 等待 | 無 |
客戶端 Z | 等待 | 無 |
當(dāng)客戶端 X 的套接字可以進(jìn)行無阻塞寫操作時,寫事件就緒,服務(wù)器將保存在緩存內(nèi)的命令執(zhí)行結(jié)果返回給客戶端:
server [dir=back, style="dashed, bold", label="返回命令執(zhí)行結(jié)果\n等待命令請求", color = "#B22222"]; cy -> server [dir=none, style=dotted, label="等待命令請求"]; cz -> server [dir=none, style=dotted, label="等待命令請求"];}" />
此時三個客戶端的事件狀態(tài)分別如下表(只有客戶端 X 的狀態(tài)被更新了):
客戶端 | 讀事件狀態(tài) | 寫事件狀態(tài) |
---|---|---|
客戶端 X | 等待 | 已就緒 |
客戶端 Y | 等待 | 無 |
客戶端 Z | 等待 | 無 |
當(dāng)命令執(zhí)行結(jié)果被傳送回客戶端之后,客戶端和寫事件之間的關(guān)聯(lián)會被解除(只剩下讀事件),至此,返回命令執(zhí)行結(jié)果的動作執(zhí)行完畢:
server [dir=none, style=dotted, label="等待命令請求"]; cy -> server [dir=none, style=dotted, label="等待命令請求"]; cz -> server [dir=none, style=dotted, label="等待命令請求"];}" />
Note
同時關(guān)聯(lián)寫事件和讀事件
前面提到過,讀事件只有在客戶端斷開和服務(wù)器的連接時,才會被移除。
這也就是說,當(dāng)客戶端關(guān)聯(lián)寫事件的時候,實際上它在同時關(guān)聯(lián)讀/寫兩種事件。
因為在同一次文件事件處理器的調(diào)用中,單個客戶端只能執(zhí)行其中一種事件(要么讀,要么寫,但不能又讀又寫),當(dāng)出現(xiàn)讀事件和寫事件同時就緒的情況時,事件處理器優(yōu)先處理讀事件。
這也就是說,當(dāng)服務(wù)器有命令結(jié)果要返回客戶端,而客戶端又有新命令請求進(jìn)入時,服務(wù)器先處理新命令請求。
時間事件記錄著那些要在指定時間點(diǎn)運(yùn)行的事件,多個時間事件以無序鏈表的形式保存在服務(wù)器狀態(tài)中。
每個時間事件主要由三個屬性組成:
when
:以毫秒格式的 UNIX 時間戳為單位,記錄了應(yīng)該在什么時間點(diǎn)執(zhí)行事件處理函數(shù)。timeProc
:事件處理函數(shù)。next
指向下一個時間事件,形成鏈表。根據(jù) timeProc
函數(shù)的返回值,可以將時間事件劃分為兩類:
ae.h/AE_NOMORE
,那么這個事件為單次執(zhí)行事件:該事件會在指定的時間被處理一次,之后該事件就會被刪除,不再執(zhí)行。AE_NOMORE
的整數(shù)值,那么這個事件為循環(huán)執(zhí)行事件:該事件會在指定的時間被處理,之后它會按照事件處理函數(shù)的返回值,更新事件的 when
屬性,讓這個事件在之后的某個時間點(diǎn)再次運(yùn)行,并以這種方式一直更新并運(yùn)行下去。可以用偽代碼來表示這兩種事件的處理方式:
def handle_time_event(server, time_event):
# 執(zhí)行事件處理器,并獲取返回值
# 返回值可以是 AE_NOMORE ,或者一個表示毫秒數(shù)的非符整數(shù)值
retval = time_event.timeProc()
if retval == AE_NOMORE:
# 如果返回 AE_NOMORE ,那么將事件從鏈表中刪除,不再執(zhí)行
server.time_event_linked_list.delete(time_event)
else:
# 否則,更新事件的 when 屬性
# 讓它在當(dāng)前時間之后的 retval 毫秒之后再次運(yùn)行
time_event.when = unix_ts_in_ms() + retval
當(dāng)時間事件處理器被執(zhí)行時,它遍歷所有鏈表中的時間事件,檢查它們的到達(dá)事件(when
屬性),并執(zhí)行其中的已到達(dá)事件:
def process_time_event(server):
# 遍歷時間事件鏈表
for time_event in server.time_event_linked_list:
# 檢查事件是否已經(jīng)到達(dá)
if time_event.when <= unix_ts_in_ms():
# 處理已到達(dá)事件
handle_time_event(server, time_event)
Note
無序鏈表并不影響時間事件處理器的性能
在目前的版本中,正常模式下的 Redis 只帶有 serverCron
一個時間事件,而在 benchmark 模式下,Redis 也只使用兩個時間事件。
在這種情況下,程序幾乎是將無序鏈表退化成一個指針來使用,所以使用無序鏈表來保存時間事件,并不影響事件處理器的性能。
對于持續(xù)運(yùn)行的服務(wù)器來說,服務(wù)器需要定期對自身的資源和狀態(tài)進(jìn)行必要的檢查和整理,從而讓服務(wù)器維持在一個健康穩(wěn)定的狀態(tài),這類操作被統(tǒng)稱為常規(guī)操作(cron job)。
在 Redis 中,常規(guī)操作由 redis.c/serverCron
實現(xiàn),它主要執(zhí)行以下操作:
Redis 將 serverCron
作為時間事件來運(yùn)行,從而確保它每隔一段時間就會自動運(yùn)行一次,又因為 serverCron
需要在 Redis 服務(wù)器運(yùn)行期間一直定期運(yùn)行,所以它是一個循環(huán)時間事件:serverCron
會一直定期執(zhí)行,直到服務(wù)器關(guān)閉為止。
在 Redis 2.6 版本中,程序規(guī)定 serverCron
每秒運(yùn)行 10
次,平均每 100
毫秒運(yùn)行一次。從 Redis 2.8 開始,用戶可以通過修改 hz
選項來調(diào)整 serverCron
的每秒執(zhí)行次數(shù),具體信息請參考 redis.conf
文件中關(guān)于 hz
選項的說明。
既然 Redis 里面既有文件事件,又有時間事件,那么如何調(diào)度這兩種事件就成了一個關(guān)鍵問題。
簡單地說,Redis 里面的兩種事件呈合作關(guān)系,它們之間包含以下三種屬性:
serverCron
)poll
函數(shù)的最大阻塞時間),由距離到達(dá)時間最短的時間事件決定。這些屬性表明,實際處理時間事件的時間,通常會比時間事件所預(yù)定的時間要晚,至于延遲的時間有多長,取決于時間事件執(zhí)行之前,執(zhí)行文件事件所消耗的時間。
比如說,以下圖表就展示了,雖然時間事件 TE 1
預(yù)定在 t1
時間執(zhí)行,但因為文件事件 FE 1
正在運(yùn)行,所以 TE 1
的執(zhí)行被延遲了:
t1
|
V
time -----------------+------------------->|
| FE 1 | TE 1 |
|<------>|
TE 1
delay
time
另外,對于像 serverCron
這類循環(huán)執(zhí)行的時間事件來說,如果事件處理器的返回值是 t
,那么 Redis 只保證:
t
, 那么這個時間事件至少會被處理一次。t
時間, 就一定要執(zhí)行一次事件 —— 這對于不使用搶占調(diào)度的 Redis 事件處理器來說,也是不可能做到的舉個例子,雖然 serverCron
(sC
)設(shè)定的間隔為 10
毫秒,但它并不是像如下那樣每隔 10
毫秒就運(yùn)行一次:
time ----------------------------------------------------->|
|<---- 10 ms ---->|<---- 10 ms ---->|<---- 10 ms ---->|
| FE 1 | FE 2 | sC 1 | FE 3 | sC 2 | FE 4 |
^ ^ ^ ^ ^
| | | | |
file event time event | time event |
handler handler | handler |
run run | run |
file event file event
handler handler
run run
在實際中,serverCron
的運(yùn)行方式更可能是這樣子的:
time ----------------------------------------------------------------------->|
|<---- 10 ms ---->|<---- 10 ms ---->|<---- 10 ms ---->|<---- 10 ms ---->|
| FE 1 | FE 2 | sC 1 | FE 3 | FE 4 | FE 5 | sC 2 |
|<-------- 15 ms -------->| |<------- 12 ms ------->|
>= 10 ms >= 10 ms
^ ^ ^ ^
| | | |
file event time event | time event
handler handler | handler
run run | run
file event
handler
run
根據(jù)情況,如果處理文件事件耗費(fèi)了非常多的時間,serverCron
被推遲到一兩秒之后才能執(zhí)行,也是有可能的。
整個事件處理器程序可以用以下偽代碼描述:
def process_event():
# 獲取執(zhí)行時間最接近現(xiàn)在的一個時間事件
te = get_nearest_time_event(server.time_event_linked_list)
# 檢查該事件的執(zhí)行時間和現(xiàn)在時間之差
# 如果值 <= 0 ,那么說明至少有一個時間事件已到達(dá)
# 如果值 > 0 ,那么說明目前沒有任何時間事件到達(dá)
nearest_te_remaind_ms = te.when - now_in_ms()
if nearest_te_remaind_ms <= 0:
# 如果有時間事件已經(jīng)到達(dá)
# 那么調(diào)用不阻塞的文件事件等待函數(shù)
poll(timeout=None)
else:
# 如果時間事件還沒到達(dá)
# 那么阻塞的最大時間不超過 te 的到達(dá)時間
poll(timeout=nearest_te_remaind_ms)
# 處理已就緒文件事件
process_file_events()
# 處理已到達(dá)時間事件
process_time_event()
通過這段代碼,可以清晰地看出:
poll
的最大阻塞時長。將這個事件處理函數(shù)置于一個循環(huán)中,加上初始化和清理函數(shù),這就構(gòu)成了 Redis 服務(wù)器的主函數(shù)調(diào)用:
def redis_main():
# 初始化服務(wù)器
init_server()
# 一直處理事件,直到服務(wù)器關(guān)閉為止
while server_is_not_shutdown():
process_event()
# 清理服務(wù)器
clean_server()
serverCron
就是循環(huán)事件。
更多建議: