Redis服務(wù)器是一個事件驅(qū)動程序盐碱,服務(wù)器需要處理以下兩類事件:
- 文件事件(
file event
):Redis
服務(wù)器通過套接字與客戶端進(jìn)行連接,而文件事件就是服務(wù)器對套接字操作的抽象番甩。服務(wù)器和客戶端的通信會產(chǎn)生相應(yīng)的文件事件,而服務(wù)器則通過監(jiān)聽并處理這些事件來完成一系列的網(wǎng)路通信操作。 - 時間事件(
time event
):Redis
服務(wù)器中的一些操作(serverCron
函數(shù))需要在給定的時間點(diǎn)執(zhí)行,而時間事件就是服務(wù)器對這類定時操作的抽象著蛙。
12.1 文件事件
Redis
基于Reactor
模式開發(fā)了自己的網(wǎng)絡(luò)事件處理器:這個處理器被稱為文件事件處理器(file event handler
):
- 文件事件處理器使用
I/O
多路復(fù)用程序來同時監(jiān)聽多個套接字,并根據(jù)套接字目前執(zhí)行的任務(wù)來為套接字關(guān)聯(lián)不同的時間處理器耳贬。 - 當(dāng)被監(jiān)聽的套接字準(zhǔn)備好執(zhí)行連接應(yīng)答(
accept
)踏堡、讀取(read
)、寫入(write
)咒劲、關(guān)閉(close
)等操作時顷蟆,與操作相對應(yīng)的文件事件就會產(chǎn)生胖秒,這時文件事件處理器就會調(diào)用套接字之前關(guān)聯(lián)好的事件處理器來處理這些事件。
雖然文件事件處理器以單線程方式運(yùn)行慕的,但通過使用I/O多路復(fù)用程序來監(jiān)聽多個套接字阎肝,文件事件處理器既實現(xiàn)了高性能的網(wǎng)絡(luò)通信模型,又可以很好地與Redis服務(wù)器中其他同樣以單線程方式運(yùn)行的模塊進(jìn)行對接肮街,這保持了Redis內(nèi)部單線程設(shè)計的簡單性风题。
12.1.1 文件事件處理器的構(gòu)成
文件事件處理器有四個部分組成,分別是套接字嫉父、I/O
多路復(fù)用程序沛硅、文件事件分派器,以及事件處理器绕辖。
文件事件是對套接字操作的抽象摇肌,每當(dāng)一個套接字準(zhǔn)備好執(zhí)行連接應(yīng)答(accept
)、寫入仪际、讀取围小、關(guān)閉等操作時,就會產(chǎn)生一個文件事件树碱。因為一個服務(wù)器通常會連接多個套接字肯适,所以多個文件事件可能會并發(fā)地出現(xiàn)。
I/O
多路復(fù)用程序負(fù)責(zé)監(jiān)聽多個套接字成榜,并向文件事件分派器傳送那些產(chǎn)生了事件的套接字框舔。
盡管多個文件事件可能會并發(fā)地出現(xiàn),但I/O
多路復(fù)用程序總是會將所有產(chǎn)生事件的套接字都放到一個隊列里面赎婚,然后通過這個隊列刘绣,以有序、同步挣输、每次一個套接字的方式向文件事件分派器傳送套接字纬凤。當(dāng)上一個套接字產(chǎn)生的事件被處理完畢之后,I/O
多路復(fù)用程序才會繼續(xù)向文件事件分派器傳送下一個套接字歧焦。
文件事件分派器接受I/O
多路復(fù)用程序傳來的套接字移斩,并根據(jù)套接字產(chǎn)生的事件類型,調(diào)用相應(yīng)的事件處理器绢馍。
服務(wù)器會為執(zhí)行不同任務(wù)的套接字關(guān)聯(lián)不同的事件處理器向瓷,這些處理器是一個個函數(shù),它們定義了某個事件發(fā)生時舰涌,服務(wù)器應(yīng)該執(zhí)行的動作猖任。
12.1.2 I/O多路復(fù)用程序的實現(xiàn)
Redis
的I/O
多路復(fù)用程序的所有功能都是通過包裝創(chuàng)建的select
、epoll
瓷耙、evport
和kqueue
這些I/O
多路復(fù)用函數(shù)庫來實現(xiàn)的朱躺,每個I/O
多路復(fù)用函數(shù)庫在Redis
源碼中都對應(yīng)一個單獨(dú)的源文件刁赖。
因為Redis
為每個I/O
多路復(fù)用函數(shù)庫都實現(xiàn)了相同的API
,所以I/O
多路復(fù)用程序的底層實現(xiàn)時可以互換的长搀。
12.1.3 事件類型
I/O
多路復(fù)用程序可以監(jiān)聽多個套接字的ae.h/AE_READABLE
事件和ae.h/AE_WRITABLE
事件宇弛,這兩類事件和套接字操作之間的對應(yīng)關(guān)系如下:
- 當(dāng)套接字變得可讀時(客戶端對套接字執(zhí)行
write
操作,或者執(zhí)行close
操作)源请,或者有新的可應(yīng)答(acceptable
)套接字出現(xiàn)時(客戶端對服務(wù)器的監(jiān)聽套接字執(zhí)行connection
操作)枪芒,套接字產(chǎn)生AE_READABLE
事件。 - 當(dāng)套接字變得可寫時(客戶端對套接字執(zhí)行
read
操作)谁尸,套接字產(chǎn)生AE_WRITABLE
事件舅踪。
I/O
多路復(fù)用程序允許服務(wù)器同時監(jiān)聽套接字的AE_READABLE
事件和AE_WRITEABLE
事件,如果一個套接字同時產(chǎn)生了這兩種事件良蛮,那么文件事件分派器會有限處理AE_READABLE
事件抽碌,等到AE_READABLE
事件處理完之后,才處理AE_WRITEABLE
事件决瞳。
12.1.4 API
略(可以參考Netty和Linux)
12.1.5 文件事件處理器
Redis
為文件事件編寫了多個處理器货徙,這些事件處理器分別用于實現(xiàn)不同的網(wǎng)絡(luò)通信需要。
- 為了對連接服務(wù)器的各個客戶端進(jìn)行應(yīng)答瞒斩,服務(wù)器要為監(jiān)聽套接字關(guān)聯(lián)連接應(yīng)答服務(wù)器破婆。
- 為了接受客戶端傳來的命令請求涮总,服務(wù)器腰圍客戶端套接字關(guān)聯(lián)命令請求處理器
- 為了向客戶端返回命令的執(zhí)行結(jié)果胸囱,服務(wù)器要為客戶端套接字關(guān)聯(lián)命令回復(fù)處理器。
- 當(dāng)主服務(wù)器和從服務(wù)器進(jìn)行復(fù)制操作時瀑梗,主從服務(wù)器都需要關(guān)聯(lián)特別為復(fù)制功能編寫的復(fù)制處理器烹笔。
服務(wù)器最常用的是與客戶端進(jìn)行通信的連接應(yīng)答處理器、命令請求處理器和命令回復(fù)處理器抛丽。
1. 連接應(yīng)答處理器
networking.c/acceptTcpHandler
函數(shù)是Redis
的連接應(yīng)答處理器谤职,這個處理器用于對連接服務(wù)器監(jiān)聽套接字的客戶端進(jìn)行應(yīng)答错森,具體實現(xiàn)為sys/socket.h/accept
函數(shù)的包裝削咆。
當(dāng)Redis
服務(wù)器進(jìn)行初始化的時候室梅,程序會將這個連接應(yīng)答處理器和服務(wù)器監(jiān)聽套接字的AE_READABLE
事件關(guān)聯(lián)起來根灯,當(dāng)有客戶端用sys/socket.h/connect
函數(shù)連接服務(wù)器監(jiān)聽套接字的時候踏幻,套接字就會產(chǎn)生AE_READABLE
事件客扎,引發(fā)連接應(yīng)答處理執(zhí)行鞋喇,并執(zhí)行相應(yīng)的套接字應(yīng)答操作芒填。
2. 命令請求處理器
networking.c/readQueryFromClient
函數(shù)是Redis
的命令請求處理器垒探,這個處理器負(fù)責(zé)從套接字中讀入客戶端發(fā)送的命令請求內(nèi)容妓蛮,具體實現(xiàn)為unistd.h/read
函數(shù)的包裝。
當(dāng)一個客戶端通過連接應(yīng)答處理器成功連接到服務(wù)器之后圾叼,服務(wù)器會將客戶端套接字的AE_READABLE
事件和命令請求處理關(guān)聯(lián)起來蛤克,當(dāng)客戶端向服務(wù)器發(fā)送命令請求的時候捺癞,套接字就會產(chǎn)生AE_READABLE
事件,引發(fā)命令請求處理器執(zhí)行构挤,并執(zhí)行相應(yīng)的套接字讀入操作髓介。
在客戶端連接服務(wù)器的整個過程中,服務(wù)器都會一直為客戶端套接字的AE_READABLE
事件關(guān)聯(lián)命令請求處理器筋现。
3. 命令回復(fù)處理器
networking.c/sendReplyToClient
函數(shù)是Redis
的命令回復(fù)處理器版保,這個處理器負(fù)責(zé)將服務(wù)器執(zhí)行命令后得到的命令回復(fù)通過套接字返回給客戶端。具體實現(xiàn)為unistd.h/write
函數(shù)的包裝夫否。
當(dāng)服務(wù)器有命令回復(fù)需要傳送給客戶端的時候彻犁,服務(wù)器會將客戶端套接字的AE_WRITEABLE
事件和命令回復(fù)處理器關(guān)聯(lián)起來,當(dāng)客戶端準(zhǔn)備好接受服務(wù)器傳回的命令回復(fù)時凰慈,就會產(chǎn)生AE_WRITEABLE
事件汞幢,引發(fā)命令回復(fù)處理器執(zhí)行,并執(zhí)行相應(yīng)的套接字寫入操作微谓。
當(dāng)命令回復(fù)發(fā)送完畢之后森篷,服務(wù)器就會解除命令回復(fù)處理器與客戶端套接字的AE_WRITEABLE
事件之間的關(guān)聯(lián)。
4. 一次完整的客戶端與服務(wù)器連接事件示例
假設(shè)一個Redis
服務(wù)器正在運(yùn)作豺型,那么這個服務(wù)器的監(jiān)聽套接字的AE_READABLE
事件應(yīng)該正處于監(jiān)聽狀態(tài)之下仲智,而該事件所對應(yīng)的處理器為連接應(yīng)答處理器。
如果這時有一個Redis
客戶端向服務(wù)器發(fā)起連接姻氨,那么監(jiān)聽調(diào)節(jié)自將產(chǎn)生AE_READABLE
事件钓辆,觸發(fā)連接應(yīng)答處理器執(zhí)行。處理器會對客戶端的連接請求進(jìn)行應(yīng)答肴焊,然后創(chuàng)建客戶端套接字前联,以及客戶端狀態(tài),并將客戶端套接字的AE_READABLE
事件與命令請求處理器進(jìn)行關(guān)聯(lián)娶眷,是的客戶端可以向主服務(wù)器發(fā)送命令請求似嗤。
之后,客戶端向主服務(wù)器發(fā)送一個命令請求届宠,那么客戶端套接字將產(chǎn)生AE_READABLE
事件烁落,引發(fā)命令請求處理器執(zhí)行,處理器讀取客戶端的命令內(nèi)容豌注,然后傳給相關(guān)程序去執(zhí)行伤塌。
執(zhí)行命令將產(chǎn)生相應(yīng)的命令回復(fù),為了將這些命令回復(fù)傳送回客戶端幌羞,服務(wù)器會將客戶端套接字的AE_WRTIEABLE
事件與命令處理器進(jìn)行關(guān)聯(lián)寸谜,當(dāng)客戶端嘗試讀取命令回復(fù)的時候,客戶端套接字將產(chǎn)生AE_WRITEABLE
事件,觸發(fā)命令回復(fù)處理器執(zhí)行熊痴,當(dāng)命令回復(fù)處理器將命令回復(fù)全部寫入到套接字之后他爸,服務(wù)器就會接觸客戶端套接字的AE_WRITABLE
事件與命令回復(fù)處理器之間的關(guān)聯(lián)。
12.2 時間事件
Redis
的時間事件分為以下兩類:
- 定時事件:讓一段程序在指定的時間之后執(zhí)行一次果善。
- 周期性事件:讓一段程序每隔指定事件就執(zhí)行一次诊笤。
一個時間事件主要由以下三個屬性組成:
-id
:服務(wù)器為時間事件創(chuàng)建的全局唯一ID
(標(biāo)識號)。ID
號從小到大的順序遞增巾陕,新事件的ID
號比舊事件的ID
號要大讨跟。
-
when
:毫秒級別的UNIX
時間戳,記錄了時間事件的到達(dá)事件鄙煤。 -
timeProc
:時間事件處理器晾匠,一個函數(shù)。
一個時間事件是定時是事件還是周期性事件取決于時間事件處理器的返回值:
- 如果事件處理器返回
ae.h/AE_NOMORE
梯刚,那么這個事件為定時事件:該事件在達(dá)到一次之后就會被刪除凉馆,之后不在到達(dá)。 - 如果事件處理器返回一個非
AE_NOMORE
的整數(shù)值亡资,那么這個事件為周期性事件:當(dāng)一個時間事件到達(dá)之后澜共,服務(wù)器會根據(jù)事件處理器返回的值,對時間事件的when屬性進(jìn)行跟新锥腻,讓這個事件在一段時間之后再次到達(dá)嗦董,一直更新并運(yùn)行下去。
目前版本的Redis
只有用周期性事件瘦黑,而沒有使用定時事件京革。
12.2.1 實現(xiàn)
服務(wù)器將所有時間事件都放在一個無序鏈表中,每當(dāng)時間事件執(zhí)行器運(yùn)行時供璧,它就遍歷整個鏈表存崖,查找所有已到達(dá)的時間事件,并調(diào)用相應(yīng)的時間處理睡毒。
新的時間事件總是插入到鏈表的表頭,所以表頭的ID
最大冗栗,表尾的ID
最小演顾。
注意:保存時間事件的鏈表為無序鏈表,指的不是鏈表不按ID
排序隅居,而是說钠至,該鏈表不按when
屬性的大小排序。正因為鏈表沒有按when
屬性進(jìn)行排序胎源,所以當(dāng)時間事件執(zhí)行器運(yùn)行的時候棉钧,它必須遍歷鏈表中的所有時間事件,這樣才能確保服務(wù)器中所有已到達(dá)的時間事件都會被處理涕蚤。
12.2.2 API
略
12.2.3 時間事件應(yīng)用實例:serverCron函數(shù)
持續(xù)運(yùn)行的Redis
服務(wù)器需要定期對自身的資源和狀態(tài)進(jìn)行檢查和調(diào)整宪卿,從而確保服務(wù)器可以長期的诵、穩(wěn)定的運(yùn)行,這些定期操作由redis.c/serverCron
函數(shù)負(fù)責(zé)執(zhí)行佑钾,它的主要工作包括:
- 更新服務(wù)器的各類統(tǒng)計信息西疤,比如時間、內(nèi)次占用休溶、數(shù)據(jù)庫占用情況等代赁。
- 清理數(shù)據(jù)庫中的過期鍵值對。
- 關(guān)閉和清理連接失效的客戶端兽掰。
- 嘗試進(jìn)行
AOF
或RDB
持久化操作 - 如果服務(wù)器是主服務(wù)器芭碍,那么對從服務(wù)器進(jìn)行定期同步
- 如果處于集群模式,對集群進(jìn)行定期同步和連接測試孽尽。
Redi
s服務(wù)器以周期性事件的方式來運(yùn)行serverCron
函數(shù)豁跑,在服務(wù)器運(yùn)行期間,每個一段時間泻云,serverCron
就會執(zhí)行一次艇拍,知道服務(wù)器關(guān)閉為止。
默認(rèn)規(guī)定serverCron
每秒運(yùn)行10
次宠纯,平均每間隔100
毫秒運(yùn)行一次卸夕。
12.3 事件的調(diào)度與執(zhí)行
因為服務(wù)器中同時存在文件事件和時間事件兩種事件類型,所以服務(wù)器必須對這兩種事件進(jìn)行調(diào)度婆瓜,決定何時應(yīng)該處理文件事件快集,何時又應(yīng)該處理事件事件,以及花多少事件來處理他們廉白。
以下是事件的調(diào)度和執(zhí)行規(guī)則
-
aeApiPoll
函數(shù)的最大阻塞時間由到達(dá)時間最近當(dāng)前時間的時間事件決定个初,這個方法既可以避免服務(wù)器對時間事件進(jìn)行頻繁的輪詢,也可以確保aeApiPoll
函數(shù)不會阻塞過長的時間猴蹂。 - 因為文件事件是隨機(jī)出現(xiàn)的院溺,如果等待并處理完一次文件事件之后,仍未有任何時間事件到達(dá)磅轻,那么服務(wù)器將再次等待并處理文件事件珍逸。隨著文件事件的不斷執(zhí)行,事件會逐漸向時間事件所設(shè)置的到達(dá)時間逼近聋溜,并最終來到到達(dá)時間谆膳,這時服務(wù)器就可以開始處理到達(dá)的時間事件了。
- 對文件事件和時間事件的處理都是同步撮躁、有序漱病、原子的執(zhí)行的,服務(wù)器不會中途中斷事件處理,也不會對事件進(jìn)行搶占杨帽,因此漓穿,不管是文件事件的處理器,還是事件事件的處理器睦尽,他們都會盡可能地減少程序的阻塞事件器净,并在有需要的時候主動讓出執(zhí)行權(quán),從而降低造成事件饑餓的可能性当凡。另外山害,事件事件也會將非常耗時的持久化操作放到子線程或者子進(jìn)程中執(zhí)行。
- 因為時間事件在文件事件之后執(zhí)行沿量,并且事件之間不會出現(xiàn)搶占浪慌,所以時間事件的實際處理事件,通常會比時間事件設(shè)定的到達(dá)時間稍微晚一些朴则。