破玩意 | Redis 為什麼那麼快

ITPUB社群發表於2022-11-30



我是個 redis 服務,我馬上就要啟動了
因為我的主人正在控制檯輸入:
./redis-server


宏觀上看下我的流程

突然,主人按下了Enter鍵,不得了了。
shell 程式把我的程式載入到了記憶體,開始執行我的 main 方法,一切就從這裡開始了。
int main(int argc, char **argv) {
   ...
   initServer();
    ...
   aeCreateFileEvent(fd, acceptHandler, ...);
   ...
   aeMain();
   ...
}
不要覺得我這裡很複雜,其實主要就三大步。
第一步,我透過 listenToPort() 方法建立了一個 TCP 連線。

破玩意 | Redis 為什麼那麼快

我的這個方法真是見名知意,而且如果展開看就更會發現沒什麼神秘的,就是 socket bind listen 標準三步走,建立了一個 TCP 監聽,返回了一個檔案描述符 fd。
第二步,我透過 aeCreateFileEvent() 方法,將上面那個建立了 TCP 連線返回的檔案描述符 fd,加入到一個叫 aeFileEvent的連結串列中。

破玩意 | Redis 為什麼那麼快

同時將這個檔案描述符繫結一個函式 acceptHandler,這樣當有客戶端連線進來時,便會執行這個函式。

破玩意 | Redis 為什麼那麼快

第三步,我透過 aeMain() 方法,將上面的 aeFileEvent 連結串列中的檔案描述符,統統作為 select 的入參,這是 IO 多路複用模式,如果不太瞭解的同學請閱讀,《你管這破玩意叫 IO 多路複用?》。

破玩意 | Redis 為什麼那麼快

好了,其實就是開啟了一個 TCP 監聽,然後如果有客戶端進來的話,讓他執行 acceptHandler 函式而已。
之後我就一直死等著客戶端連線了。
void aeMain(aeEventLoop *eventLoop)
{
    eventLoop->stop = 0;
    while (!eventLoop->stop)
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}

破玩意 | Redis 為什麼那麼快

展開體驗下我的具體工作

此時,另外一個人啟動了一個 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

破玩意 | Redis 為什麼那麼快

不難想象,如果再來一個客戶端,又來一個客戶端... 那麼不斷將新客戶端的檔案描述符掛上去即可,而監聽新客戶端連線的,始終是最上面那個檔案描述符。

破玩意 | Redis 為什麼那麼快

好了,服務端開啟了監聽,客戶端也連上了服務端,此時我仍然在死等狀態,只不過等的不只是新客戶端連線到達,還在等待已經連線上的客戶端發來命令。

破玩意 | Redis 為什麼那麼快

請注意,這裡的死等,只有一個執行緒,迴圈呼叫 aeProcessEvents 函式,用 select 的方式監聽多個檔案描述符。放上剛剛 main 方法的第三步,幫大家回憶一下。
void aeMain(aeEventLoop *eventLoop)
{
    eventLoop->stop = 0;
    while (!eventLoop->stop)
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
當有新客戶端建立連線時,會觸發 acceptHandler 函式執行,多出一個等待資料的描述符。

破玩意 | Redis 為什麼那麼快

當有客戶端資料傳來時,會觸發 readQueryFromClient 函式執行,完成這個命令的操作。

破玩意 | Redis 為什麼那麼快

注意,由於只有一個執行緒在監聽這些描述符,並做處理。所以即使客戶端併發地傳送命令,後面仍然是依次取出命令,順序執行

破玩意 | Redis 為什麼那麼快

這也就是我們常常說的,redis 是單執行緒的,命令與命令之間是順序執行,無需考慮執行緒安全的問題。

為了方便大家吹牛,我來拔高一下

大家發現沒,我的啟動過程,其實就分成兩個大的部分。
一個是監聽客戶端的請求,就是用 IO 多路複用的方式,監聽多個檔案描述符,就剛剛那個 aeMain() 方法乾的事嘛。
一個是執行相應的函式去處理這個請求,具體執行什麼函式就是出現多次的 aeCreateFileEvent() 方法去繫結的,這個相應的函式說得高大上一點,叫做事件處理器

破玩意 | Redis 為什麼那麼快

這裡所謂的連線應答處理器,就是剛剛監聽連線的檔案描述符所繫結的函式 acceptHandler
所謂的命令請求處理器,就是監聽客戶端命令(讀事件)的檔案描述符繫結的函式 readQueryFromClient
所謂的命令回覆處理器,就是後面要提到的,監聽客戶端響應(寫事件)的檔案描述符繫結的函式 sendReplyToClient
這種一個負責響應 IO 事件,一個負責交給相應的事件處理器去處理,就叫做 Reactor 模式
Redis 正是基於 Reactor 模式開發了自己的檔案事件處理器,實現了高效能的網路通訊模型,並且保持了 Redis 內部單執行緒設計的簡單性
有點擔心這句話吹牛的逼格不夠,其實我是參考了《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 分別儲存起來,這部分的原始碼細節,可以閱讀我之前的文章,《Redis 資料結構之字串的那些騷操作》。
而關於 redis 資料結構與底層對應的編碼結構,可以閱讀我之前的文章,《面試官問我 redis 的資料型別,我回答了 8 種》。
處理完命令後,就要傳送響應給客戶端了。
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 為什麼那麼快

後記

整篇文章我好像沒講 Redis 為啥那麼快,因為我感覺這個問題問得不好。

你可以從接收網路請求的 IO 多路複用角度說起,也可以從事件處理器驅動的 Reactor 模式說起,還可以從具體處理命令時的資料結構說起,比如單單是字串背後的 sds 其實就做了很多的巧妙設計。

如果我是面試官,我會具體讓面試者聊聊 Redis 的啟動流程,或者 Redis 處理命令的整個流程。

這裡面可挖的點挺多的,如果能談笑風生,那自然是技術水平還不錯。

另外,你會發現本文出現的很多唬人的術語,比如 Reactor 模式,事件處理器等,看一遍 Redis 原始碼後你會發現真的非常簡單。

毫不客氣地說,一切絲毫不談具體實現,和你堆砌一大堆唬人名詞的文章或者人,都是在耍流氓。

本文我參考的是 Redis3.0.0 原始碼,但成文時用的講解程式碼是 Redis1.0.0,整個網路模組的設計是完全一樣的。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024420/viewspace-2925892/,如需轉載,請註明出處,否則將追究法律責任。

相關文章