性能突出的 Redis 是咋使用 epoll 的?
作者 | 閃客
來源 | 低并發編程
我是個 redis 服務,我馬上就要啟動了
因為我的主人正在控制臺輸入:
./redis-server宏觀上看下我的流程
突然,主人按下了回車鍵,不得了了。
shell 程序把我的程序加載到了內存,開始執行我的 main 方法,一切就從這里開始了。
int?main(int?argc,?char?**argv)?{...initServer();...aeCreateFileEvent(fd, acceptHandler, ...);...aeMain();... }不要覺得我這里很復雜,其實主要就三大步。
第一步,我通過 listenToPort() 方法創建了一個 TCP 連接。
我的這個方法真是見名知意,而且如果展開看就更會發現沒什么神秘的,就是 socket bind listen 標準三步走,建立了一個 TCP 監聽,返回了一個文件描述符 fd。
第二步,我通過 aeCreateFileEvent() 方法,將上面那個創建了 TCP 連接返回的文件描述符 fd,加入到一個叫 aeFileEvent的鏈表中。
同時將這個文件描述符綁定一個函數 acceptHandler,這樣當有客戶端連接進來時,便會執行這個函數。
第三步,我通過 aeMain() 方法,將上面的 aeFileEvent 鏈表中的文件描述符,統統作為 select 的入參,這是 IO 多路復用模式。
好了,其實就是開啟了一個 TCP 監聽,然后如果有客戶端進來的話,讓他執行 acceptHandler 函數而已。
之后我就一直死等著客戶端連接了。
void?aeMain(aeEventLoop?*eventLoop) {eventLoop->stop?=?0;while?(!eventLoop->stop)aeProcessEvents(eventLoop,?AE_ALL_EVENTS); }展開體驗下我的具體工作
此時,另外一個人啟動了一個 redis-client,連接到了我。
redis-cli?-h?host?-p?port那么我頭上的 fd 就會感知有數據讀入,并執行 acceptHandler 方法。
static?void?acceptHandler(...)?{...cfd?=?anetAccept(...);...c?=?createClient(cfd))... }可以看到,當有新客戶端連接進來時,便會調用 createClient 創建一個專屬的 client 為其服務。
static?redisClient?*createClient(int?fd)?{...aeCreateFileEvent(c->fd,?readQueryFromClient, ...);... }這里又可以看到,所謂的專屬服務,其實仍然是這個 aeCreateFileEvent 函數。
這個上面說了,這個函數的功能就是把文件描述符掛在鏈表上,然后分配一個處理函數。
當然,這回的處理函數不再是處理新客戶端連接的 acceptHandler,而是處理具體客戶端傳來的 redis 命令的函數 readQueryFromClient。
不難想象,如果再來一個客戶端,又來一個客戶端... 那么不斷將新客戶端的文件描述符掛上去即可,而監聽新客戶端連接的,始終是最上面那個文件描述符。
好了,服務端開啟了監聽,客戶端也連上了服務端,此時我仍然在死等狀態,只不過等的不只是新客戶端連接到達,還在等待已經連接上的客戶端發來命令。
請注意,這里的死等,只有一個線程,循環調用 aeProcessEvents 函數,用 select 的方式監聽多個文件描述符。放上剛剛 main 方法的第三步,幫大家回憶一下。
void?aeMain(aeEventLoop?*eventLoop) {eventLoop->stop?=?0;while?(!eventLoop->stop)aeProcessEvents(eventLoop,?AE_ALL_EVENTS); }當有新客戶端建立連接時,會觸發 acceptHandler 函數執行,多出一個等待數據的描述符。
當有客戶端數據傳來時,會觸發 readQueryFromClient 函數執行,完成這個命令的操作。
注意,由于只有一個線程在監聽這些描述符,并做處理。所以即使客戶端并發地發送命令,后面仍然是依次取出命令,順序執行。
這也就是我們常常說的,redis 是單線程的,命令與命令之間是順序執行,無需考慮線程安全的問題。
為了方便大家吹牛,我來拔高一下
大家發現沒,我的啟動過程,其實就分成兩個大的部分。
一個是監聽客戶端的請求,就是用 IO 多路復用的方式,監聽多個文件描述符,就剛剛那個 aeMain() 方法干的事嘛。
一個是執行相應的函數去處理這個請求,具體執行什么函數就是出現多次的 aeCreateFileEvent() 方法去綁定的,這個相應的函數說得高大上一點,叫做事件處理器。
這里所謂的連接應答處理器,就是剛剛監聽連接的文件描述符所綁定的函數 acceptHandler。
所謂的命令請求處理器,就是監聽客戶端命令(讀事件)的文件描述符綁定的函數 readQueryFromClient。
所謂的命令回復處理器,就是后面要提到的,監聽客戶端響應(寫事件)的文件描述符綁定的函數 sendReplyToClient。
這種一個負責響應 IO 事件,一個負責交給相應的事件處理器去處理,就叫做 Reactor 模式。
Redis 正是基于 Reactor 模式開發了自己的文件事件處理器,實現了高性能的網絡通信模型,并且保持了 Redis 內部單線程設計的簡單性。
有點擔心這句話吹牛的逼格不夠,其實我是參考了《Redis 設計與實現》,截圖給大家。
具體怎么執行一個 Redis 命令
現在,我們通過一個已建立好連接的客戶端,發一個 redis 命令。
<client 6379> set?dibingfa?niubi此時 readQueryFromClient 函數將被執行。
這個函數會去一張表中尋找命令所對應的函數,這部分用的編碼技巧叫命令模式。
static?struct?redisCommand?cmdTable[]?=?{{"get",getCommand,2,REDIS_CMD_INLINE},{"set",setCommand,3,REDIS_CMD_BULK|REDIS_CMD_DENYOOM},{"setnx",setnxCommand,3,REDIS_CMD_BULK|REDIS_CMD_DENYOOM},{"del",delCommand,-2,REDIS_CMD_INLINE},{"exists",existsCommand,2,REDIS_CMD_INLINE},... }找到了 set 命令對應的函數就是 setCommand。
這個函數,最終就會一步步地將 key 和 value 分別存儲起來。
處理完命令后,就要發送響應給客戶端了。
static?void?setCommand(redisClient?*c)?{...addReply(c,?nx???shared.cone?:?shared.ok); }這個響應,并不是直接同步寫回去,當然也不是開啟一個線程去異步寫回去。
它仍然是調用那個萬惡的 aeCreateFileEvent 函數,將 sendReplyToClient 函數掛在需要響應的客戶端連接的文件描述符上。
static?void?addReply(redisClient?*c,?robj?*obj)?{...aeCreateFileEvent(server.el,?c->fd,?AE_WRITABLE,sendReplyToClient,?c,?NULL)?==?AE_ERR); }好了,這回上一小節挖的坑,終于補上了。
以上這些個破玩意,就是我的啟動過程啦,我是不是很可愛。
后記
整篇文章我好像沒講 Redis 為啥那么快,因為我感覺這個問題問得不好。
你可以從接收網絡請求的 IO 多路復用角度說起,也可以從事件處理器驅動的 Reactor 模式說起,還可以從具體處理命令時的數據結構說起,比如單單是字符串背后的 sds 其實就做了很多的巧妙設計。
如果我是面試官,我會具體讓面試者聊聊 Redis 的啟動流程,或者 Redis 處理命令的整個流程。
這里面可挖的點挺多的,如果能談笑風生,那自然是技術水平還不錯。
另外,你會發現本文出現的很多唬人的術語,比如 Reactor 模式,事件處理器等,看一遍 Redis 源碼后你會發現真的非常簡單。
毫不客氣地說,一切絲毫不談具體實現,和你堆砌一大堆唬人名詞的文章或者人,都是在耍流氓。
本文我參考的是 Redis3.0.0 源碼,但成文時用的講解代碼是 Redis1.0.0,整個網絡模塊的設計是完全一樣的。
往期推薦
Redis 緩存擊穿(失效)、緩存穿透、緩存雪崩怎么解決?
如果被問到分布式鎖,應該怎樣回答?
三分鐘教你用 Scarlet 寫一個 WebSocket App
Java 底層知識:什么是?“橋接方法”??
點分享
點收藏
點點贊
點在看
總結
以上是生活随笔為你收集整理的性能突出的 Redis 是咋使用 epoll 的?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 中国首部智能交通微纪录片正式发布 探讨交
- 下一篇: linux cmake编译源码,linu