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)體里,先看一眼:

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

值得注意的是默認(rèn)的持久化策略。
然后是命令行參數(shù)解析:


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

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。

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

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

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

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

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

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

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

一共有三種情況: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的雙鏈表里。


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

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

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

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



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

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

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

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

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

完整的指令處理過程結(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ù)鏈表里刪除。

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

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

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

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

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

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

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

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

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

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


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

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