1. 程式人生 > >redis原始碼學習之工作流程初探

redis原始碼學習之工作流程初探

[toc] ## 背景 redis是當下比較流行的KV資料庫之一,是抵禦高併發的一把利器,本著知其然還要知其所以然的目的,我決定花一點時間來研究其原始碼,希望最後能向自己解釋清楚“redis為什麼這麼快”這個疑惑,第一篇主要介紹環境搭建和redis工作流程初探,後期會陸續獻上其他有意思的章節。 ## 環境準備 我自己的電腦是win10系統,所以我會準備一套適合windows系統的環境來供自己學習,這樣方便除錯分析。 ### 下載redis原始碼 redis本身是不支援windows系統的,但是微軟的工程師針對windows平臺做了支援,原始碼放在了github上,有需要的可以自己去下載,我這裡下載的是v2.8.9這個tag的原始碼,下載地址[https://github.com/microsoftarchive/redis]()。這裡扯個題外話,學習一個開源軟體的時候不要一上來就下載最新版本的原始碼看,經過的迭代太多程式碼量就上來了,對於新手來說容易暈,先下一個早期的穩定版本瞭解其體系結構和工作流程,等待熟悉了以後再循序漸進。 ### 下載Visual Studio 其他軟體我沒嘗試過,這個是官方推薦的ide,一定一定一定下載 Visual Studio 2013 update5這個版本的,否則編譯的時候各種報錯,下載地址 [http://download.microsoft.com/download/9/3/E/93EA27FF-DB02-4822-8771-DCA0238957E9/vs2013.5_ult_chs.iso]()。這裡再扯個題外話,我剛開始下載的是最新版本的Visual Studio,結果編譯的時候各種報錯,然後就去網路上一頓查一頓試,折騰半天還是沒好,最後下載了 Visual Studio 2013 update5這個版本,結果一把就成功,有些牛角尖一定得鑽,但是有些牛角尖不值得鑽。 ### Visual Studio開啟redis原始碼 按照下圖方式開啟下載的redis原始碼 ![](https://img2020.cnblogs.com/blog/846817/202006/846817-20200629112618248-839154375.png) ![](https://img2020.cnblogs.com/blog/846817/202006/846817-20200629112842724-1977658603.png) ![](https://img2020.cnblogs.com/blog/846817/202006/846817-20200629113052729-777881781.png) c程式的入口是main方法,redis main方法的位置在redis.c檔案中,下面我們通過main方法來逐步瞭解redis的工作流程。 ## 啟動過程分析 跟著main方法順序看下去,大概有以下幾個關鍵步驟(略過了sentinel相關邏輯): 1.設定隨機數種子、獲取當前時間等; 2.初始化服務配置資訊,設定預設值(initServerConfig); 3.解析配置檔案(loadServerConfig); 4.初始化server物件(initServer);  4.1建立eventLoop物件;  4.2建立serverSocket,監聽埠;  4.3新增定時事件到eventLoop物件中;  4.4將serverSocket檔案描述符新增到監視集中,這裡藉助IO多路複用框架的能力(windows平臺使用IOCP,其他平臺使用select、epoll、evport等); 5.從磁碟載入資料到記憶體中(loadDataFromDisk); 6.執行事件迴圈邏輯(aeMain),這是redis真正揮灑汗水的地方,下一節會單獨講述這塊內容。 ### 呼叫關係圖 ![](https://img2020.cnblogs.com/blog/846817/202006/846817-20200629180602437-640092097.png) ## 事件迴圈分析 我們都知道redis是單執行緒執行客戶端命令的,那究竟是怎樣一種設計才能支援高併發的讀寫呢。 ### 工作模型 1.server啟動,建立serverSocket監聽埠,將serverSocket對應的FD(檔案描述符)簡稱為FD-Server新增到IO多路複用框架的監視集當中,註冊AE_READABLE事件(可讀),關聯的事件處理器是acceptTcpHandler; 2.client連線server; 3.事件迴圈開始輪詢IO多路複用框架介面aeApiPoll,會得到就緒的FD,執行對應的事件處理器; 4.由第3步事件迴圈觸發FD-Server AE_READABLE事件對應的事件處理器acceptTcpHandler;  4.1呼叫accept獲得clientSocket對應的FD簡稱為FD-Client;  4.2將FD-Client新增到IO多路複用框架的監視集當中,註冊AE_READABLE事件(可讀),關聯的事件處理器是readQueryFromClient; 5.client傳送redis命令; 6.由第3步事件迴圈觸發FD-Clien AE_READABLE事件對應的事件處理器readQueryFromClient;  6.1解析客戶端發來的redis命令,找到命令對應的redisCommandProc(命令對應的處理函式);  6.2執行redisCommandProc;  6.3prepareClientToWrite準備回寫響應資訊,為FD-Client註冊AE_WRITEABLE事件(可寫),關聯的事件處理器是sendReplyToClient; 7.執行redis中的定時任務; 8.由第3步事件迴圈觸發FD-Clien AE_WRITEABLE事件對應的事件處理器sendReplyToClient,傳送響應內容給client; ![](https://img2020.cnblogs.com/blog/846817/202006/846817-20200630175238449-573543879.png) ### 程式碼分析 server啟動,建立serverSocket並註冊AE_READABLE事件,設定事件處理器為acceptTcpHandler ``` void initServer() { //省略部分程式碼 //初始化eventLoop物件,eventLoop物件裡面儲存了所有的事件 server.el = aeCreateEventLoop(server.maxclients+REDIS_EVENTLOOP_FDSET_INCR); //建立serverSocket,監聽埠 if (server.port != 0 && listenToPort(server.port,server.ipfd,&server.ipfd_count) == REDIS_ERR) exit(1); //新增定時任務到eventLoop中 if(aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) { } //將serverSocket對應的檔案描述符新增到監視集中,關聯的事件處理器是acceptTcpHandler for (j = 0; j < server.ipfd_count; j++) { if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE, acceptTcpHandler,NULL) == AE_ERR) } } ``` acceptTcpHandler當有連線過來的時候被觸發,呼叫accept得到client socket對應的FD,並將FD新增到監視集中,關聯的事件處理器是readQueryFromClient ``` void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) { int cport, cfd; //呼叫accept獲得clientSocket對應的FD cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport); //將clientSocket對應的FD新增到監視集中 acceptCommonHandler(cfd,0); } static void acceptCommonHandler(int fd, int flags) { redisClient *c; //呼叫createClient新增 if ((c = createClient(fd)) == NULL) { } } redisClient *createClient(int fd) { redisClient *c = zmalloc(sizeof(redisClient)); if (fd != -1) { anetNonBlock(NULL,fd); anetEnableTcpNoDelay(NULL,fd); if (server.tcpkeepalive) anetKeepAlive(NULL,fd,server.tcpkeepalive); //將fd新增到監視集中,關聯的事件處理器是readQueryFromClient if (aeCreateFileEvent(server.el,fd,AE_READABLE, readQueryFromClient, c) == AE_ERR) { } } } ``` aeMain就是跑一個迴圈,一直去呼叫aeProcessEvents ``` void aeMain(aeEventLoop *eventLoop) { eventLoop->stop = 0; while (!eventLoop->stop) { if (eventLoop->beforesleep != NULL) eventLoop->beforesleep(eventLoop); aeProcessEvents(eventLoop, AE_ALL_EVENTS); } } ``` aeProcessEvents會呼叫aeApiPoll方法來獲得就緒的檔案描述符,然後執行檔案描述符關聯的的事件處理器 ``` int aeProcessEvents(aeEventLoop *eventLoop, int flags) { int processed = 0, numevents; #ifdef _WIN32 if (ServiceStopIssued() == TRUE) aeStop(eventLoop); #endif /* Nothing to do? return ASAP */ if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0; /* Note that we want call select() even if there are no * file events to process as long as we want to process time * events, in order to sleep until the next time event is ready * to fire. */ if (eventLoop->maxfd != -1 || ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) { int j; aeTimeEvent *shortest = NULL; struct timeval tv, *tvp; if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT)) shortest = aeSearchNearestTimer(eventLoop); if (shortest) { long now_sec, now_ms; /* Calculate the time missing for the nearest * timer to fire. */ aeGetTime(&now_sec, &now_ms); tvp = &tv; tvp->tv_sec = shortest->when_sec - now_sec; if (shortest->when_ms < now_ms) { tvp->tv_usec = ((shortest->when_ms+1000) - now_ms)*1000; tvp->tv_sec --; } else { tvp->tv_usec = (shortest->when_ms - now_ms)*1000; } if (tvp->tv_sec < 0) tvp->tv_sec = 0; if (tvp->tv_usec < 0) tvp->tv_usec = 0; } else { /* If we have to check for events but need to return * ASAP because of AE_DONT_WAIT we need to set the timeout * to zero */ if (flags & AE_DONT_WAIT) { tv.tv_sec = tv.tv_usec = 0; tvp = &tv; } else { /* Otherwise we can block */ tvp = NULL; /* wait forever */ } } numevents = aeApiPoll(eventLoop, tvp); for (j = 0; j < numevents; j++) { aeFileEvent *fe; int mask = eventLoop->fired[j].mask; int fd = eventLoop->fired[j].fd; int rfired = 0; fe = &eventLoop->events[eventLoop->fired[j].fd]; /* note the fe->mask & mask & ... code: maybe an already processed * event removed an element that fired and we still didn't * processed, so we check if the event is still valid. */ if (fe->mask & mask & AE_READABLE) { rfired = 1; fe->rfileProc(eventLoop,fd,fe->clientData,mask); } if (fe->mask & mask & AE_WRITABLE) { if (!rfired || fe->wfileProc != fe->rfileProc) fe->wfileProc(eventLoop,fd,fe->clientData,mask); } processed++; } } /* Check time events */ if (flags & AE_TIME_EVENTS) processed += processTimeEvents(eventLoop);//處理延遲任務 return processed; /* return the number of processed file/time events */ } ``` ###動畫演示 做了一個動畫幫助理解工作過程(redis啟動之後使用命令列telnet到6379埠,然後執行keys *命令,最終拿到結果) ![](https://img2020.cnblogs.com/blog/846817/202006/846817-20200630185918129-1968514217.gif) ##網路模組 ###IO多路複用 這部分內容網路上精彩的內容太多,這裡把我認為比較經典的一些內容貼出來供大家品讀(建議從上往下順序閱讀) [The C10K problem](http://www.kegel.com/c10k.html) [socket阻塞非阻塞等頭疼問題解釋](https://www.cnblogs.com/junneyang/p/6126635.html) [LINUX – IO MULTIPLEXING – SELECT VS POLL VS EPOLL](https://devarea.com/linux-io-multiplexing-select-vs-poll-vs-epoll/#.XvB2n8gzaks) [poll vs select vs event-based](https://daniel.haxx.se/docs/poll-vs-select.html) [redis事件驅動](https://redis.io/topics/internals-ev