redis請求處理流程

1, 編譯:redis源碼是基于makefile構(gòu)建的,在ide里調(diào)試很麻煩,不能符號跳轉(zhuǎn),所以就根據(jù)makefile里描述的編譯過程,用cmake重新寫一遍,導(dǎo)入到clion里調(diào)試分析。
編譯c / c++,不管怎么構(gòu)建,不管用什么ide (eclipse cdt,visual studio,xcode,clion),都要設(shè)置幾個關(guān)鍵的東西:頭文件路徑,依賴的lib路徑,lib名,編譯器選項,源碼中編譯相關(guān)的宏。
編譯redis server的CMakeLists.txt

編譯redis-server

aux_source_directory(. DIR_REDIS_SERVER_SRCS)
add_executable(redis-server ${DIR_REDIS_SERVER_SRCS})
target_include_directories(redis-server PRIVATE
./include
../lua
../hiredis
../jemalloc)
target_link_libraries(redis-server
m dl hiredis lua jemalloc)

編譯redis-cli

set(REDIS_CLI_SRCS anet.c adlist.c redis-cli/redis-cli.c zmalloc.c release.c anet.c ae.c crc64.c)
add_executable(redis-cli ${REDIS_CLI_SRCS})
target_include_directories(redis-cli PRIVATE
.
../hiredis
../linenoise)
target_link_libraries(redis-cli
m dl hiredis linenoise jemalloc)

//target_compile_options() 添加編譯選項 -Wall之類的
//target_compile_definitions() 添加預(yù)定義宏
//PRIVATE依賴:頭文件不依賴,源文件依賴
//INTERFACE:頭文件依賴 源文件不依賴
//PUBLIC 頭文件和源文件都依賴

2,c/c++指令的項目,直接搜索main方法。
redis-server的main方法在server.c文件里。下面開始描述server啟動的整個過程。
整個server的主要狀態(tài)都保存在redisServer server這個結(jié)構(gòu)體里,先看一眼:


redisServer.png

首先初始化server,c語言中變量不初始化,值是不確定的。


initServerConfig.png

值得注意的是默認(rèn)的持久化策略。

然后是命令行參數(shù)解析:


命令行參數(shù)解析.png
參數(shù)解析.png

redis-server的第一個參數(shù)如果不是以一個或兩個減號開頭就表示配置文件路徑,
后續(xù)的可選參數(shù)會跟讀到內(nèi)存中的配置文件內(nèi)容合到一起解析。然后就是一行一行的處理配置文件了。


加載配置文件.png

server狀態(tài)加載完畢,可以初始化socket準(zhǔn)備接收client連接了,毫無疑問使用IO多播,但是跟netty又不太一樣,netty是使用jdk里的nio,jdk為了可移植,使用了所有系統(tǒng)都支持且行為一致的select/poll。但是select/poll由于每次都要把監(jiān)控的fd和關(guān)聯(lián)event傳入內(nèi)核,然后內(nèi)核再把IO可用的fd和event返回給調(diào)用方,性能較差,select比poll性能更差,select只能指定最大fd,poll可以精確指定fd。所以像redis和nginx這種支持超高并發(fā)的系統(tǒng)都會使用各個系統(tǒng)特定的性能更高的api。linux上使用epoll,osx上使用kqueue,unix上使用/dev/poll。
先解釋一下fd這個東西:file descriptor,文件描述符,linux/unix系統(tǒng)中號稱一切都是文件,每個文件都有一個fd,類似windows上的Handle,打開文件時獲取fd,操作文件時指定fd。linux系統(tǒng)上有一個配置,指定了fd_set的大小。fd_set表示一個進程打開的所有fd,順序增加,0,1,2分別表示stdin/stdout/stderr。每次打開文件,從0開始查找fd_set中第一個未被占用的fd作為新打開文件的fd。fd_set默認(rèn)大小1024。所以第一步是根據(jù)設(shè)置的client連接數(shù)最大值調(diào)整fd_set。


修改fd_set.png

之后是調(diào)用kqueue系統(tǒng)api初始化aeEventLoop結(jié)構(gòu)體,綁定到指定端口。aeEventLoop中主要有兩類字段:timeEventNextId,lastTime,timeEventHead跟定時任務(wù)有關(guān),其他的跟IO多播有關(guān)。


EventLoop.png

之后是初始化redis的16個db。dict是hash table結(jié)構(gòu),之后統(tǒng)一描述各種數(shù)據(jù)結(jié)構(gòu)。


16db.png

之后會把一個非常重要的定時任務(wù)添加到EventLoop上,然后把server socket的fd注冊到kqueue的監(jiān)控列表里。


添加定時任務(wù).png

之后是加載動態(tài)模塊和從aof/rdb里恢復(fù)狀態(tài)。redis有一套擴展用的api,server啟動時可以加載實現(xiàn)里這些api的動態(tài)庫。有一個很明顯很有用的擴展點:通過實現(xiàn)api往EventLoop里添加定時任務(wù)。


恢復(fù)狀態(tài).png

最后進入主循環(huán):


主循環(huán).png

主循環(huán)的核心流程如下:
先計算多少毫秒之后要觸發(fā)最早的定時任務(wù),利用定時任務(wù)觸發(fā)的間隙檢查是否用io可用的fd,定時任務(wù)都在EventLoop的單鏈表里。如果沒有定時任務(wù)要執(zhí)行,直接阻塞直到有fd可用。


查找最早的定時任務(wù).png

使用kqueue在指定時間段內(nèi)檢查可用的fd,調(diào)用關(guān)聯(lián)的函數(shù)


處理可用的fd.png

一共有三種情況:accept client連接;client請求可讀,client可以接收響應(yīng)。

accept client是之前綁定到指定端口的server socket注冊的,觸發(fā)時會把收到的client連接注冊到kqueue里。client結(jié)構(gòu)體保存每個client相關(guān)的狀態(tài),所有的client都保存在server.clients的雙鏈表里。


accept.png
createClient.png

保護模式下,只有跟server ip相同的client才能連接。


保護模式.png

client發(fā)送請求fd觸發(fā)時:
先把請求緩存在client的querybuf里。


緩存請求到buffer.png

redis client協(xié)議:先按換行符拆分,每行是一個指令,每個指令再按空格拆分,保存到client里


拆分指令.png

然后就是根據(jù)指令列表里每個指令關(guān)聯(lián)的函數(shù),執(zhí)行指令。


查找指令.png
各種檢查.png
執(zhí)行指令.png

以簡單的get key為例分析響應(yīng)過程:


從db里查找key.png

響應(yīng)信息放入client的buf,放不下里放入replay鏈表.


響應(yīng)信息放入client的buf和replay鏈表.png

有響應(yīng)信息要返回的client,加入server.clients_pending_write鏈表
加入clients_pending_write鏈表.png

每次調(diào)用kqueue等待IO之前都會先調(diào)用beforeSleep,beforeSleep會檢查上個循環(huán)有響應(yīng)信息待處理的client,注冊write事件。


注冊write事件.png

當(dāng)本次事件循環(huán),發(fā)現(xiàn)write可用時,


write.png

完整的指令處理過程結(jié)束了,可以發(fā)現(xiàn)目前只有一個線程,一個線程干了很多活,避免了多線程中線程切換和鎖相關(guān)的開銷,這也是另一個redis高性能的原因。

redis除了響應(yīng)client請求外,還有很多其他的要做:最明顯的就是緩存持久化,過期key清除等。這些任務(wù)都集中在server初始化時注冊到EventLoop上的定時任務(wù)里。之前說過請求處理任務(wù)是在定時任務(wù)觸發(fā)的間隙中處理的。

現(xiàn)在分析一下定時任務(wù)都干了啥:很簡單,循環(huán)EventLoop上定時任務(wù)單鏈表,觸發(fā)時間過期的任務(wù),調(diào)用對應(yīng)的定時任務(wù)函數(shù),如果timeProc不返回-1,表示定時任務(wù)還需要觸發(fā),計算好下次觸發(fā)時間,否則id設(shè)置為-1,等待下次從定時任務(wù)鏈表里刪除。


定時任務(wù).png

目前redis只有一個定時任務(wù):serverCron,根據(jù)配置文件里server.hz的值,決定serverCron每秒執(zhí)行 1000 / server.hz 個周期
serverCron的子任務(wù)如下:

首先是100個周期執(zhí)行一次的統(tǒng)計任務(wù):


統(tǒng)計任務(wù).png

然后是記錄內(nèi)存占用的峰值,如果收到了shutdown指令,關(guān)閉子進程


shutdown.png

每5000個周期打印一次狀態(tài)log


狀態(tài)log.png

檢查很久不發(fā)指令的client,關(guān)閉連接,檢查阻塞在b開頭的指令的client,阻塞指定時間后返回timeout錯誤信息。


關(guān)閉超時的client.png

如果client太多,一次全部檢查一遍很阻塞很長時間,所以分散壓力,每次只執(zhí)行一部分。一秒之內(nèi)全部檢查一遍,其實這個時間是非常不精確又無法保證的。


分散壓力.png

檢查過期key,如果key很多,一個一個檢查肯定會阻塞很久,所以每次都花定量的時間隨機刪除過期的key,如果多次隨機都沒有遇到過期key,密度很小,停止任務(wù)。


檢查過期key.png

刪除過期key竟然還有異步刪除,原來是漏掉了一個地方:redis除了主線程,還有三個后臺任務(wù)線程,用于處理各種異步操作。


異步刪除key.png

異步任務(wù)只有三種:異步關(guān)閉連接,異步同步aof,異步刪除。


異步任務(wù)類型.png

使用pthread創(chuàng)建線程,等待條件變量通知。


異步任務(wù)線程.png

然后會fork兩個子進程,分別用于處理aof和rdb持久化。

aof持久化.png
rdb持久化.png

然后是一些跟集群和主從相關(guān)的任務(wù),等待下回分解。


集群和主從.png

本節(jié)完?。?!。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容