1.事件處理庫html
Redis 最初發佈於 2009 年,它最牛逼的一件事情大概就是它的速度 —— 它可以處理大量的併發客戶端鏈接。須要特別指出的是,它是用一個單線程來完成的,並且還不對保存在內存中的數據使用任何複雜的鎖或者同步機制。linux
Redis 之因此如此牛逼是由於,它在給定的系統上使用了其可用的最快的事件循環,並將它們封裝成由它實現的事件循環庫(在 Linux 上是 epoll,在 BSD 上是 kqueue,等等)。這個庫的名字叫作 ae。ae 使得編寫一個快速服務器變得很容易,只要在它內部沒有阻塞便可,而 Redis 則保證 注1 了這一點。redis
在這裏,咱們的興趣點主要是它對文件事件的支持 —— 當文件描述符(如網絡套接字)有一些有趣的未決事情時將調用註冊的回調函數。與 libuv 相似,ae 支持多路事件循環(參閱本系列的第三節和第四節)和不該該感到意外的 aeCreateFileEvent 信號:
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
aeFileProc *proc, void *clientData);
它在 fd 上使用一個給定的事件循環,爲新的文件事件註冊一個回調(proc)函數。當使用的是 epoll 時,它將調用 epoll_ctl 在文件描述符上添加一個事件(多是 EPOLLIN、EPOLLOUT、也或許二者都有,取決於 mask 參數)。ae 的 aeProcessEvents 功能是 「運行事件循環和發送回調函數」,它在底層調用了 epoll_wait。數據庫
2.處理客戶端請求服務器
咱們經過跟蹤 Redis 服務器代碼來看一下,ae 如何爲客戶端事件註冊回調函數的。initServer 啓動時,經過註冊一個回調函數來讀取正在監聽的套接字上的事件,經過使用回調函數 acceptTcpHandler 來調用 aeCreateFileEvent。當新的鏈接可用時,這個回調函數被調用。它調用 accept 注2 ,接下來是 acceptCommonHandler,它轉而去調用 createClient 以初始化新客戶端鏈接所須要的數據結構。網絡
createClient 的工做是去監聽來自客戶端的入站數據。它將套接字設置爲非阻塞模式(一個異步事件循環中的關鍵因素)並使用 aeCreateFileEvent 去註冊另一個文件事件回調函數以讀取事件 —— readQueryFromClient。每當客戶端發送數據,這個函數將被事件循環調用。數據結構
readQueryFromClient 就讓咱們指望的那樣 —— 解析客戶端命令和動做,並經過查詢和/或操做數據來回復。由於客戶端套接字是非阻塞的,因此這個函數必須可以處理 EAGAIN,以及部分數據;從客戶端中讀取的數據是累積在客戶端專用的緩衝區中,而完整的查詢可能被分割在回調函數的多個調用當中。多線程
3.將數據發送回客戶端併發
在前面的內容中,我說到了 readQueryFromClient 結束了發送給客戶端的回覆。這在邏輯上是正確的,由於 readQueryFromClient 準備要發送回覆,但它不真正去作實質的發送 —— 由於這裏並不能保證客戶端套接字已經準備好寫入/發送數據。咱們必須爲此使用事件循環機制。異步
Redis 是這樣作的,它註冊一個 beforeSleep 函數,每次事件循環即將進入休眠時,調用它去等待套接字變得能夠讀取/寫入。beforeSleep 作的其中一件事情就是調用 handleClientsWithPendingWrites。它的做用是經過調用 writeToClient 去嘗試當即發送全部可用的回覆;若是一些套接字不可用時,那麼當套接字可用時,它將註冊一個事件循環去調用 sendReplyToClient。這能夠被看做爲一種優化 —— 若是套接字可用於當即發送數據(通常是 TCP 套接字),這時並不須要註冊事件 ——直接發送數據。由於套接字是非阻塞的,它從不會去阻塞循環。
4.Redis中的多線程
在 Redis 的絕大多數歷史中,它都是一個徹徹底底的單線程的東西。一些人以爲這太難以想象了,有這種想法徹底能夠理解。Redis 本質上是受網絡束縛的 —— 只要數據庫大小合理,對於任何給定的客戶端請求,其大部分延時都是浪費在網絡等待上,而不是在 Redis 的數據結構上。
然而,如今事情已經再也不那麼簡單了。Redis 如今有幾個新功能都用到了線程:
1.「惰性」 內存釋放。
2.在後臺線程中使用 fsync 調用寫一個 持久化日誌。
3.運行須要執行一個長週期運行的操做的用戶定義模塊。
對於前兩個特性,Redis 使用它本身的一個簡單的 bio(它是 「Background I/O" 的首字母縮寫)庫。這個庫是根據 Redis 的須要進行了硬編碼,它不能用到其它的地方 —— 它運行預設數量的線程,每一個 Redis 後臺做業類型須要一個線程。
而對於第三個特性,Redis 模塊 能夠定義新的 Redis 命令,而且遵循與普通 Redis 命令相同的標準,包括不阻塞主線程。若是在模塊中自定義的一個 Redis 命令,但願去執行一個長週期運行的操做,這將建立一個線程在後臺去運行它。在 Redis 源碼樹中的 src/modules/helloblock.c 提供了這樣的一個示例。
有了這些特性,Redis 使用線程將一個事件循環結合起來,在通常的案例中,Redis 具備了更快的速度和彈性,這有點相似於在本系統文章中 第四節 討論的工做隊列。