本文主要介紹Redis的事件驅(qū)動(dòng),說(shuō)明其內(nèi)部實(shí)現(xiàn)機(jī)制灵汪。
I败许、上帝視角看Redis的事件驅(qū)動(dòng)
Redis服務(wù)器是一個(gè)事件驅(qū)動(dòng)程序,需要處理兩類事件:
1砰嘁、文件事件:Redis服務(wù)器通過(guò)socket與客戶端連接件炉,而文件事件就是服務(wù)器對(duì)socket操作的抽象勘究。服務(wù)器與客戶端的通信會(huì)產(chǎn)生相應(yīng)的文件事件,而服務(wù)器則通過(guò)監(jiān)聽(tīng)并處理這些事件來(lái)完成一系列網(wǎng)絡(luò)操作斟冕。
2口糕、時(shí)間事件:Redis服務(wù)器中的一些操作(如ServerCron
)需要在給定的時(shí)間點(diǎn)執(zhí)行,而時(shí)間事件就是服務(wù)器對(duì)這類定時(shí)操作的抽象磕蛇。
II景描、文件事件
Redis基于Reactor模式開(kāi)發(fā)了自己的網(wǎng)絡(luò)事件處理器,這個(gè)處理器被稱為文件事件處理器秀撇。
· 文件事件處理器使用I/O多路復(fù)用程序來(lái)同時(shí)監(jiān)聽(tīng)多個(gè)socket超棺,并根據(jù)socket目前執(zhí)行的任務(wù)來(lái)為socket關(guān)聯(lián)不同的事件處理器。
· 當(dāng)被監(jiān)聽(tīng)的socket準(zhǔn)備好執(zhí)行連接應(yīng)答(accept)呵燕,讀忍幕妗(read),寫(xiě)入(write)再扭,關(guān)閉(close)等操作時(shí)氧苍,與操作相對(duì)應(yīng)的文件事件就會(huì)產(chǎn)生,這時(shí)文件事件處理器就會(huì)調(diào)用socket之前關(guān)聯(lián)好的事件處理器來(lái)處理這些事件霍衫。
2.1 文件事件處理器的構(gòu)成
下圖展示了文件事件處理器的四個(gè)組成部分:socket候引,I/O多路復(fù)用程序,文件事件分派器敦跌,以及事件處理器澄干。
1、文件事件是對(duì)socket操作的抽象柠傍,當(dāng)一個(gè)socket準(zhǔn)備好執(zhí)行accept麸俘, read,等操作時(shí)惧笛,就會(huì)產(chǎn)生一個(gè)文件事件从媚。
2、I/O多路復(fù)用程序負(fù)責(zé)監(jiān)聽(tīng)多個(gè)socket患整,并向文件事件分派器傳送產(chǎn)生事件的socket拜效。
3、盡管多個(gè)文件事件可能會(huì)并發(fā)的出現(xiàn)各谚,但I(xiàn)/O多路復(fù)用程序總是會(huì)將所有產(chǎn)生的事件socket都放到一個(gè)隊(duì)列中紧憾,然后通過(guò)這個(gè)隊(duì)列,以有序昌渤,同步赴穗,每次一個(gè)socket的方式向文件事件分派器傳送。當(dāng)上一個(gè)socket處理完畢后(該socket為事件所關(guān)聯(lián)的事件處理器執(zhí)行完畢),I/O多路復(fù)用程序才會(huì)繼續(xù)向文件事件分派器傳送下一個(gè)socket:
4般眉、文件事件分派器接收I/O多路復(fù)用程序傳來(lái)的socket了赵,并根據(jù)socket產(chǎn)生的事件類型,調(diào)用相應(yīng)的事件處理器(不同的函數(shù))甸赃。
2.2 I/O多路復(fù)用程序的實(shí)現(xiàn)
1柿汛、Redis的I/O多路復(fù)用程序的所有功能都是通過(guò)包裝select
,epoll
等函數(shù)庫(kù)實(shí)現(xiàn)的辑奈。
2苛茂、Redis為每個(gè)I/O多路復(fù)用函數(shù)庫(kù)都實(shí)現(xiàn)了相同的API已烤,所以I/O多路復(fù)用程序的底層實(shí)現(xiàn)是可以互換的鸠窗。
3、Redis為I/O多路復(fù)用程序定義了相應(yīng)的規(guī)則胯究,程序會(huì)在編譯時(shí)自動(dòng)選擇系統(tǒng)中性能最高的函數(shù)庫(kù)來(lái)作為其底層實(shí)現(xiàn)稍计。
2.3 事件的類型
I/O多路復(fù)用程序允許服務(wù)器同時(shí)監(jiān)聽(tīng)socket的AE_READABLE
事件和AE_WRITABLE
事件,如果一個(gè)套接字同時(shí)產(chǎn)生了這兩種事件裕循,則文件事件分派器會(huì)優(yōu)先處理可讀事件臣嚣,后處理可寫(xiě)事件。
2.4 文件事件的處理器
Redis為文件事件編寫(xiě)了多個(gè)處理器剥哑,這些處理器分別用于實(shí)現(xiàn)不同的網(wǎng)絡(luò)通信需求硅则。
1、連接應(yīng)答處理器
networking.c/acceptTcpHandler
函數(shù)是連接應(yīng)答處理器株婴,這個(gè)處理器用于對(duì)連接服務(wù)器監(jiān)聽(tīng)socket的客戶端進(jìn)行應(yīng)答怎虫,具體為sys/socket.h/accept
函數(shù)的封裝。
2困介、命令請(qǐng)求處理器
networking.c/readQueryFromClient
函數(shù)是命令請(qǐng)求處理器大审,負(fù)責(zé)從socket中讀取客戶端發(fā)送的命令請(qǐng)求內(nèi)容,具體為對(duì)unistd.h/read
函數(shù)的封裝座哩。
3徒扶、命令回復(fù)處理器
networking.c/sendReplyToClient
函數(shù)是Redis命令的回復(fù)處理器,這個(gè)處理器負(fù)責(zé)將服務(wù)器執(zhí)行命令后得到的結(jié)果通過(guò)socket返回客戶端根穷,具體為unistd.h/write
函數(shù)的封裝姜骡。
4、一次完整的連接事件示例
Redis服務(wù)器正在運(yùn)作屿良,這個(gè)服務(wù)器的監(jiān)聽(tīng)socket的
AE_READABLE
事件處于監(jiān)聽(tīng)狀態(tài)之下圈澈,而該事件所對(duì)應(yīng)的處理器為連接應(yīng)答處理器。
如果這時(shí)有一個(gè)客戶端向服務(wù)器發(fā)起連接管引,則監(jiān)聽(tīng)socket會(huì)產(chǎn)生AE_READABLE
事件士败,觸發(fā)連接應(yīng)答處理器執(zhí)行。處理器對(duì)客戶端的連接請(qǐng)求進(jìn)行應(yīng)答,創(chuàng)建客戶端socket谅将,以及客戶端狀態(tài)句灌,并將客戶端socket的AE_READABLE
事件與命令請(qǐng)求處理器進(jìn)行關(guān)聯(lián),使得客戶端可以向服務(wù)器發(fā)送命令請(qǐng)求票唆。客戶端發(fā)送命令請(qǐng)求茅信,客戶端socket會(huì)產(chǎn)生
AE_READABLE
事件,引發(fā)命令請(qǐng)求處理器執(zhí)行隅熙,處理命令內(nèi)容稽煤。執(zhí)行命令所產(chǎn)生的回復(fù),服務(wù)器將客戶端socket的
AE_WRITEABLE
事件與命令回復(fù)處理器進(jìn)行關(guān)聯(lián)囚戚。當(dāng)客戶端嘗試讀取命令回復(fù)的時(shí)候酵熙,客戶端socket將產(chǎn)生AE_WRITEABLE
事件,觸發(fā)命令回復(fù)處理器執(zhí)行驰坊,將回復(fù)命令寫(xiě)入客戶端socket匾二。
III、時(shí)間事件
1拳芙、Redis的時(shí)間事件分為兩類: 定時(shí)事件察藐,周期性事件。
2舟扎、一個(gè)時(shí)間事件主要由以下三個(gè)屬性組成:
· id:服務(wù)器為時(shí)間事件創(chuàng)建的全局唯一ID分飞。ID號(hào)按從小到大的順序遞增,新事件的ID號(hào)比舊事件的ID號(hào)要大睹限。
· when:毫秒精度的UNIX時(shí)間戳譬猫,記錄了時(shí)間事件的到達(dá)時(shí)間。
· timeProc:時(shí)間事件處理器邦泄,一個(gè)函數(shù)删窒。當(dāng)時(shí)間事件到達(dá)時(shí),服務(wù)器就會(huì)調(diào)用相應(yīng)的處理器來(lái)處理事件顺囊。
3肌索、一個(gè)時(shí)間事件是定時(shí)時(shí)間還是周期性事件取決于時(shí)間事件處理器的返回值:
· 如果返回值為ae.h/AE_NOMORE
,則這個(gè)事件為定時(shí)事件:該時(shí)間在達(dá)到一次之后就會(huì)被刪除特碳。
· 如果事件處理器返回一個(gè)非AE_NOMORE
的整數(shù)值诚亚,則返回值就是周期性時(shí)間。
目前Redis版本中午乓,只使用周期性事件站宗,沒(méi)有使用定時(shí)事件。
3.1 實(shí)現(xiàn)
服務(wù)器將所有時(shí)間事件都存放到一個(gè)無(wú)序鏈表中益愈,每當(dāng)時(shí)間事件執(zhí)行運(yùn)行時(shí)梢灭,就遍歷整個(gè)鏈表夷家,查找所有已到達(dá)的時(shí)間事件,并調(diào)用相應(yīng)的時(shí)間處理器:
目前版本中敏释,正常模式下的Redis服務(wù)器只使用serverCron
一個(gè)時(shí)間事件库快,在這種情況下,服務(wù)器幾乎是將無(wú)序鏈表退化成一個(gè)指針來(lái)使用钥顽,所以义屏,以鏈表的方式存儲(chǔ)并不會(huì)影響其性能。
IV蜂大、事件的調(diào)度與執(zhí)行
以下是事件的調(diào)度和執(zhí)行規(guī)則:
1闽铐、aeApiPoll
(等待文件事件產(chǎn)生)函數(shù)的最大阻塞時(shí)間由到達(dá)時(shí)間最接近當(dāng)前時(shí)間的時(shí)間事件決定,這個(gè)方法既可以避免服務(wù)器對(duì)時(shí)間事件進(jìn)行頻繁的輪詢(忙等待)奶浦,也可以保證aeApiPoll
函數(shù)不會(huì)阻塞過(guò)程時(shí)間兄墅。
2、因?yàn)槲募录请S機(jī)出現(xiàn)的财喳,如果等待并處理完一次文件事件之后察迟,仍未有任何時(shí)間事件到達(dá),那么服務(wù)器將再次等待并處理文件事件耳高。隨著文件事件的不斷執(zhí)行,時(shí)間會(huì)逐漸向時(shí)間事件設(shè)置的到達(dá)時(shí)間逼近所踊,并最終來(lái)到到達(dá)時(shí)間泌枪,這是服務(wù)器就可以開(kāi)始處理到達(dá)的時(shí)間事件了。
3秕岛、對(duì)文件事件和時(shí)間事件的處理都是同步碌燕、有序、原子的執(zhí)行的继薛,服務(wù)器不會(huì)中途中斷事件處理修壕,也不會(huì)對(duì)事件進(jìn)行搶占,因此遏考,不管是文件事件的處理器慈鸠,還是時(shí)間事件的處理器,它們都會(huì)盡可能減少程序的阻塞時(shí)間灌具,并在有需要的時(shí)候主動(dòng)讓出執(zhí)行權(quán)青团,從而降低造成事件饑餓的可能性。
例如咖楣,在命令回復(fù)處理器將一個(gè)命令回復(fù)寫(xiě)入到客戶端socket時(shí)督笆,如果寫(xiě)入字節(jié)數(shù)超過(guò)了一個(gè)預(yù)設(shè)常量的話,命令回復(fù)處理器會(huì)主動(dòng)用break跳出寫(xiě)入循環(huán)诱贿,將余下的數(shù)據(jù)流到下次再寫(xiě)娃肿;另外,時(shí)間事件也會(huì)將非常好使的持久化操作放到子進(jìn)程中執(zhí)行。
4料扰、因?yàn)闀r(shí)間事件在文件事件執(zhí)行之后執(zhí)行锨阿,并不會(huì)對(duì)文件事件進(jìn)行搶占,所以通常時(shí)間事件的實(shí)際處理時(shí)間记罚,會(huì)比所設(shè)定的時(shí)間稍晚一些墅诡。
【參考】
[1] 《Redis設(shè)計(jì)與實(shí)現(xiàn)》
歡迎轉(zhuǎn)載,轉(zhuǎn)載請(qǐng)注明出處wenmingxing Redis之事件