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