Redis 的源码分析
https://opentalk.upyun.com/277.html
Redis 這個(gè)東西很簡單,懂 C 語言的同學(xué)花一個(gè)下午,可以把它的來龍去脈都研究懂。但是,它麻雀雖小五臟俱全。一個(gè)常見的軟件,比如 Redis,跑起來該用的東西可能都用一些,如果我們把 Redis 搞懂了,要分析一款其他的軟件,思路可能也是差不多的,所以我借這個(gè)機(jī)會,跟大家分享一下我們解剖一個(gè)軟件的過程。
和大家分享 Redis,主要通過以下幾個(gè)步驟。
啟動過程
首先,看一下 Redis 的一個(gè)啟動過程。任何一款軟件,它的很多C語言實(shí)現(xiàn)的過程,都是從 main 函數(shù)這個(gè)漏斗開始的。一般任何軟件設(shè)計(jì)的時(shí)候,不管是 Redis,還是阿帕奇,或者亂七八糟的東西,一般 Main 函數(shù)都定義在跟它軟件名字一樣的. C 文件里面,里面 main 函數(shù)執(zhí)行的過程分以下幾步:
第一步,Redis 會設(shè)置一些回調(diào)函數(shù),當(dāng)前時(shí)間,隨機(jī)數(shù)的種子。回調(diào)函數(shù)實(shí)際上什么?舉個(gè)例子,比如 Q/3 要給 Redis 發(fā)送一個(gè)關(guān)閉的命令,讓它去做一些優(yōu)雅的關(guān)閉,做一些掃尾清楚的工作,這個(gè)工作如果不設(shè)計(jì)回調(diào)函數(shù),它其實(shí)什么都不會干。其實(shí) C 語言的程序跑在操作系統(tǒng)之上,Linux 操作系統(tǒng)本身就是提供給我們事件機(jī)制的回調(diào)注冊功能,所以它會設(shè)計(jì)這個(gè)回調(diào)函數(shù),讓你注冊上,關(guān)閉的時(shí)候優(yōu)雅的關(guān)閉,然后它在后面可以做一些業(yè)務(wù)邏輯。
第二步,不管任何軟件,肯定有一份配置文件需要配置。首先在服務(wù)器端會把它默認(rèn)的一份配置做一個(gè)初始化。
第三步,Redis 在 3.0 版本正式發(fā)布之前其實(shí)已經(jīng)有篩選這個(gè)模式了,但是這個(gè)模式,我很少在生產(chǎn)環(huán)境在用。Redis 可以初始化這個(gè)模式,比較復(fù)雜。
第四步,解析啟動的參數(shù)。其實(shí)不管什么軟件,它在初始化的過程當(dāng)中,配置都是由兩部分組成的。第一部分,靜態(tài)的配置文件;第二部分,動態(tài)啟動的時(shí)候,main,就是參數(shù)給它的時(shí)候進(jìn)去配置。
第五步,把服務(wù)端的東西拿過來,裝載 Config 配置文件,loadServerConfig。
第六步,初始化服務(wù)器,initServer。
第七步,從磁盤裝載數(shù)據(jù)。
第八步,有一個(gè)主循環(huán)程序開始干活,用來處理客戶端的請求,并且把這個(gè)請求轉(zhuǎn)到后端的業(yè)務(wù)邏輯,幫你完成命令執(zhí)行,然后吐數(shù)據(jù),這么一個(gè)過程。
服務(wù)器的模型
接下來看一下 Redis 服務(wù)器的模型。Redis 實(shí)現(xiàn)的過程當(dāng)中,基于不動的操作系統(tǒng),封裝了不同的模型。舉個(gè)例子,它在 Linux 上面是基于 epoll 做了一個(gè)封裝,不管怎么樣,它都是以 ae_epoll.c 封裝的。封裝過程當(dāng)中有三個(gè)步驟,我們用原生調(diào)用 epoll 的時(shí)候也是三個(gè)步驟完成。第一個(gè)步驟,aeApiCreate,就是 epoll 的一個(gè)池子,先創(chuàng)建了一個(gè)池子的東西。第二、通過 ApiAddEvent 調(diào)用 epoll 這個(gè)函數(shù),可以往 epoll 池子里面注冊事件。第三、ApiPoll,通過 epoll_wait 來獲取已經(jīng)響應(yīng)的事件。
Redis 在服務(wù)端初始化 epoll
首先,在 main 函數(shù)初始化過程當(dāng)中調(diào)用了 innitServer,其實(shí)就是調(diào)用剛才講的 aeCreateEvent ,創(chuàng)建了 epoll 池子。然后調(diào)用函數(shù),設(shè)定 EVENTLOOP_FDSET_INCR。然后設(shè)置回調(diào)函數(shù),注冊的事件響應(yīng)之后要干活,這是一個(gè)循環(huán)調(diào)用的過程。怎么調(diào)呢?我們把 aeCreateEvent 這個(gè)函數(shù)展開,里面有兩個(gè)過程,Event如果這個(gè)死循環(huán)在調(diào)用的過程當(dāng)中,可以跟兩類事件發(fā)生交道。第一類事件,aeflieEvent。第二類事件,aeTimeEvent。因?yàn)?Redis 針對 epoll 再做一次封裝的時(shí)候,它實(shí)現(xiàn)了一個(gè)定時(shí)器,這個(gè)定時(shí)器可以把你想要注冊到這個(gè)定時(shí)器里面的一些事件注冊進(jìn)去。舉個(gè)例子,比如內(nèi)存淘汰的時(shí)候,是一個(gè) LRU 的一個(gè)算法,你注冊到這個(gè)定時(shí)器,比如內(nèi)存達(dá)到某個(gè)大小,比如限制兩兆,當(dāng)它大于兩兆的時(shí)候要淘汰,這個(gè)時(shí)候定時(shí)器在這個(gè)場景下面就會發(fā)生作用。
主循環(huán)的實(shí)現(xiàn)原理
Redis 真正的主循環(huán)的原理,大致可以分成三步:
第一步,查找一些優(yōu)先要處理的事件。什么叫優(yōu)先要處理?你在調(diào)用API的時(shí)候,這個(gè) API 可能作為 Redis 的使用者不會去關(guān)注。但是作為 Redis 的開發(fā)者他可能會關(guān)注到。你首先要讓 Redis 執(zhí)行一個(gè)東西,它這個(gè)時(shí)候會優(yōu)先去做處理。
第二步,假如說沒有優(yōu)先處理實(shí)踐,則執(zhí)?aeApiPoll 來處理 epoll 中的就緒事件。
最后,處理定時(shí)器任務(wù)。
服務(wù)器整體架構(gòu)圖
我們可以通過這張圖回顧一下它整體服務(wù)器的架構(gòu),其實(shí)就是這么一回事。最中間圓圈,代表了一個(gè)死循環(huán)。死循環(huán)要跑的時(shí)候,要干哪些活?我們把邏輯注冊到某個(gè)池子里面,比如注冊到 epoll 的池子里面,或者注冊到定時(shí)器當(dāng)中。它都是通過一些回調(diào)函數(shù)注冊的。比如 TCP 的時(shí)間要響應(yīng),就不停的執(zhí)行,這么一個(gè)過程,Redis 本身實(shí)現(xiàn)也不是太復(fù)雜。
當(dāng)你啟動 Redis 的時(shí)候,它本身就是一個(gè)單進(jìn)程,單線程的模式。所以,我們在事件處理過程當(dāng)中,要做到非常小心,精確的做一些控制,因?yàn)槟愕氖录坏┻M(jìn)到 Redis 里面,比如我們簡單的讓 Redis 做一個(gè)技術(shù)器加法運(yùn)算,如果加法運(yùn)算時(shí)間花的很多,后面的規(guī)模可能就一直等在那里,執(zhí)行不下去了,因?yàn)樗菃尉€程,單進(jìn)程的。所以說,如果你讓 Redis 同步在執(zhí)行的過程當(dāng)中,它必然是 CPU 密集型的運(yùn)算,而且能很快計(jì)算完畢,把結(jié)果推送給你。
請求協(xié)議
其實(shí)請求的協(xié)議,在前面 main 函數(shù)執(zhí)行過程當(dāng)中會 initSever,在 initSever 過程當(dāng)中我們會注冊一個(gè) acceptTcpHandler 回調(diào)函數(shù),然后這個(gè)函數(shù)就會被調(diào)用了。Redis 請求協(xié)議分稱兩種,第一、inline 協(xié)議,第二、multibulk 協(xié)議,如果不是各*開頭,就是 inline 協(xié)議。
首先,看 inline 的協(xié)議,調(diào)用 processInline 這個(gè)函數(shù)比較簡單,當(dāng)你把數(shù)據(jù)發(fā)送給服務(wù)端,任何的軟件都會把這個(gè)數(shù)據(jù)丟到一個(gè)緩存區(qū),Redis 里面有一個(gè) querybuf 結(jié)構(gòu),執(zhí)行到緩存區(qū),然后存入到 client 的 arg 數(shù)組,argc 代表了參數(shù)的格式。processMultibulkBuffer 協(xié)議,我們這里有三個(gè)參數(shù)的數(shù)量,比如 3,指的是長度 3,具體就是這么一個(gè)過程。
當(dāng)我們把這數(shù)據(jù)完全解析完之后,這個(gè)時(shí)候就知道它是什么命令了。比如剛才 Set 命令已經(jīng)解析完,我們知道它是一個(gè) Set 命令,并且知道它的參數(shù)是什么。這時(shí)候我們會調(diào)用 processcommand 這個(gè)函數(shù),執(zhí)行的過程分成 12 個(gè)步驟:
- 第一、假如命令當(dāng)中包含了 quit,后面的指令將不會被執(zhí)行,直接會返回退出來。
- 第二、如果不包含 quit,它有一個(gè) cmd 的結(jié)構(gòu)數(shù)組,會到里面查找現(xiàn)在命令到底是哪一個(gè),把具體要執(zhí)行命令的函數(shù)執(zhí)政找到。
- 第三、檢測命令的參數(shù)個(gè)數(shù)。
- 第四、如果服務(wù)器配置需要密碼檢驗(yàn)功能,調(diào)用的命令必須是 authCommand。
- 第五、如果服務(wù)器有最大內(nèi)存限制,必須限制性一下 freeMemorylfNeed 這個(gè)過程。
- 第六、如果服務(wù)器狀態(tài)出現(xiàn)了問題,那么停止執(zhí)行命令。
- 第七、如果服務(wù)器設(shè)置了最小的 slave 數(shù)量限制,當(dāng) slave 數(shù)量小于最小 slave 數(shù)量的時(shí)候,停止執(zhí)行命令。
- 第八、如果服務(wù)器為 slave,則不接受 write 命令。
- 第九、只能支持 pub/sub 相關(guān)的命令了。
- 第十、當(dāng) slave 和 mater 的連接已經(jīng)斷開,并且設(shè)置了跟 mater 斷開后不再提供服務(wù),那么停止執(zhí)行命令。
- 第十一、如果服務(wù)器正在裝載數(shù)據(jù)中,則不接受命令。
- 第十二、如果 lua 腳本執(zhí)行速度太慢了,也會停止執(zhí)行命令。
在命令真正的執(zhí)行過程當(dāng)中,Redis 分成了兩個(gè)步驟。第一種,假如已經(jīng)用了剛才講的事務(wù)處理模式,Redis 會把命令在 Q 里面存起來。所以,真正到 EXEC 之前,打開事務(wù)模式,把丟過來的命令先在 Q 存起來,真正執(zhí)行的時(shí)候再執(zhí)行。第二種,假如不是事務(wù)模式,這個(gè)時(shí)候它就會去真正調(diào)用這個(gè) proc 函數(shù),把 Redis 命令真正在后臺執(zhí)行。比如,剛才提到的事務(wù)模式,通過 MULTI 關(guān)鍵詞輸入,后面就起到命令模式,如果后面不調(diào)用,它就不會真正執(zhí)行。
命令執(zhí)行過程
剛才事務(wù)執(zhí)行時(shí)候的命令過程,會把隊(duì)列里面的命令一個(gè)一個(gè)拿出來,然后去執(zhí)行的過程。一個(gè)正常命令的執(zhí)行過程,主要是分成幾個(gè)步驟:
- 第一,假如有監(jiān)視器狀態(tài)的客戶端,首先會把命令發(fā)送給客戶端。什么叫監(jiān)視器?舉個(gè)例子,我是mater slave機(jī)制的,首先要把這個(gè)機(jī)制告訴slave,你要去執(zhí)行這條命令。
- 第二、真正執(zhí)行。
- 第三、開啟慢查詢。
- 第四、監(jiān)視就是監(jiān)視器的命令,哪條命令要執(zhí)行了,什么日志,什么參數(shù)都會發(fā)送給我,這是第一步要執(zhí)行的,只有真正執(zhí)行完,才會把這個(gè)工作發(fā)送給AOF和Slave,這樣才符合邏輯。
AddReply 會注冊寫事件到 epoll 里面去,通過 prepareClientToWrite。第二、會調(diào)用 _addReplyToBuffer 數(shù)據(jù)寫到 buf 中。下一次執(zhí)行的時(shí)候才會循環(huán)這個(gè)動作,這樣每次做的時(shí)候,TPS 在單線程,單進(jìn)程的情況下還能達(dá)到理想的狀況。第三、假如 buf 為不夠大,會添加到鏈表里面去。
其實(shí) RedisDb 最最核心的實(shí)現(xiàn)就是一個(gè)置頂?shù)膶?shí)現(xiàn),比如有存數(shù)據(jù)的置頂,就是要不要過期,其實(shí)也是存在置頂里面。舉個(gè)例子,有些請求它其實(shí)會阻塞的,阻塞到哪里?有一個(gè)阻塞器置頂。當(dāng)阻塞已經(jīng)就緒了,有一個(gè)就緒的 1 K的置頂,還可以堅(jiān)持某個(gè) K。置頂?shù)木唧w實(shí)現(xiàn),就不再講了。
核心數(shù)據(jù)結(jié)構(gòu)
因?yàn)槲覀冏罱K服務(wù)器其實(shí)都跟核心的數(shù)據(jù)結(jié)構(gòu)操作相關(guān)。首先,看 string 這個(gè)東西,其實(shí) string 就是一個(gè) struct 指針,可以描述長度,還剩余多少等等這些東西。看一下 struct 指針到底怎么指的,它會把 sdshdr 放到內(nèi)存的前面,把 buf 放到內(nèi)存的后面。Redis 檢索怎么查找到 sdshdr 這個(gè)區(qū)域,一般通過目前 buf 最前置的指針減去 sdshdr 這個(gè)長度,就知道 sdshdr 在哪里。
我們知道字符串其實(shí)就是一個(gè) struct 結(jié)構(gòu),接下來看一下 hash 結(jié)構(gòu)怎么實(shí)現(xiàn)的。hash 本質(zhì)是基于 ziplist 的實(shí)現(xiàn),關(guān)于 ziplist 的實(shí)現(xiàn),ziplist 通過文本定義了一個(gè)數(shù)據(jù)結(jié)構(gòu)。其實(shí) ziplist 可以認(rèn)為里面是一個(gè)一個(gè)的元素。我們理解 hash_max 的時(shí)候,有一個(gè) hash_max_ziplist_value 的結(jié)構(gòu),就是通過這張圖描述的這種方式把里面的東西撈出來了。當(dāng)然,ziplist 在存儲 hash 的時(shí)候,hash 通過兩種方式存的。第一、ziplist 這種結(jié)構(gòu)。因?yàn)?ziplist 具體的長度是可以設(shè)置的,當(dāng)你的長度超過了某個(gè)數(shù)值之后,它就會轉(zhuǎn)成 dict 的這個(gè)結(jié)構(gòu),最最原始的 dict 的結(jié)構(gòu),這樣它存儲的時(shí)候都存到 dict 的結(jié)構(gòu)體里面去了。
list 其實(shí)就是我們通常用的比較經(jīng)典的這種雙向鏈表,頭指針,尾指針,定義了 list。接下來還有一個(gè) set。其實(shí) Redis set 還是存在 dict 這樣的結(jié)構(gòu)里面的,因?yàn)?list 只有 Velue 沒有 Key。Redis 還有一個(gè)數(shù)據(jù)結(jié)構(gòu)叫 Sorted Sets,它是為了加速檢索的過程,用到以空間換時(shí)間的方式。舉個(gè)例子,可能有些場景用搜索引擎構(gòu)建的時(shí)候,覺得太麻煩,會建幾張表做索引,其實(shí) Sorted Sets 也是一樣的,就是通過 span 結(jié)構(gòu)實(shí)現(xiàn)了多級索引查詢的過程。可以在這個(gè) Velue 之上通過多級指針進(jìn)行檢索。Redis里面有一個(gè) pub/sub_channels 這么一個(gè)屬性,當(dāng)有什么東西要給客戶端的時(shí)候,會到這個(gè)隊(duì)列里面查看有沒有注冊上來的客戶端。
事務(wù)處理當(dāng)中,可能還要注意幾點(diǎn):
- 首先,假如客戶端的 flag 是 DIRTY_CAS 或者是 DIRTY_EXEC,就放棄執(zhí)行事務(wù)了。
- 第二、在事務(wù)執(zhí)行期間,取消對 key 的 Watch。
- 第三、遍歷執(zhí)行隊(duì)列中的命令。
- 第四、通過 ReplicationFeedmonitors 服務(wù)器同步給 Monitors 客戶端進(jìn)程。
持久化 rdb 的過程,其實(shí) Redis 服務(wù)器分成兩個(gè)步驟,第一、rdb 的持久化,第二、AOF 的持久化,基于 rdb 的持久化方式,服務(wù)器啟動的時(shí)候,首先會調(diào)用 serverparamslen 的函數(shù),然后 rdb 的工作會把內(nèi)存里面存的數(shù)據(jù),原封不動的拷貝,存儲到本地磁盤當(dāng)中去。rdbSave 不是讓組件程序看這個(gè)活,我們需要 fork 一個(gè)子進(jìn)程專門做 rdeSave 的數(shù)。
- 1、創(chuàng)建臨時(shí)文件:temp-%d 為 rdb
- 2、調(diào)用 rdbSaveRio 將 db 中的數(shù)據(jù)獬入到臨時(shí)文件。
- 3、調(diào)用 fflush,fsync 將緩存中的數(shù)據(jù)刷新到磁盤。
- 4、將 temp 文件重命名為正式的rdb文件,后面就是這些描述,這些描述跟前面講的 Redis 的數(shù)據(jù)結(jié)構(gòu)其實(shí)是對應(yīng)起來的,然后以這種方式存到這個(gè)里面去。
aof 存儲的格式和剛才我們請求協(xié)議里面講到的協(xié)議是一模一樣的,就是純文本的,比如 set 什么東西,就是一模一樣的東西存在這個(gè)文件里面。假如開啟了 aof 這個(gè)功能,會把你歷史執(zhí)行的命令記錄原封不動都存在里面,這樣這個(gè)文件會越來越大。當(dāng)然,Redis 提供給我們一個(gè)功能,可以把 aof 命令壓縮。在每次 Redis 重啟之后,如果開啟了 aof 功能,就會重載 aof 文件中的數(shù)據(jù)執(zhí)行命令。然后 Redis 提供了 rewriteaof 定期壓縮的功能,其實(shí)就是把 db 中的數(shù)據(jù)重新生成一份新的 aof。
Redis 的內(nèi)存分配還是比較簡單,不像 memorycash。Redis 通過調(diào)用原生的函數(shù)直接向操作系統(tǒng)申請內(nèi)存。當(dāng)內(nèi)存不停的申請,在使用一段時(shí)間之后,Redis 會處罰一些淘汰的策略。這個(gè)淘汰分成兩種,一種是主動淘汰,舉個(gè)例子,當(dāng)我們在調(diào)用 RandomKey 等這些函數(shù)的時(shí)候,首先會主動的淘汰一些內(nèi)存,這個(gè)就叫主動淘汰。還有一種淘汰是 lru 的淘汰,當(dāng)你在執(zhí)行的過程當(dāng)中,如果內(nèi)存不夠,就會處罰 lru 的淘汰算法。另外,還有被動淘汰,前面講到因?yàn)槲覀冊?main 函數(shù)調(diào)用真正的 epoll 死循環(huán)的前置有一個(gè) beforeSleep,beforeSleep 函數(shù)里面會在 databasesCron 定時(shí)器都調(diào)用 activeExpireCycle。
Replication 機(jī)制
RedisReplication 的機(jī)制,分為客戶端請求和服務(wù)器的處理。我們啟動客戶端的時(shí)候,main 函數(shù)里面會調(diào)用 serverCron,在 serverCron 里又會調(diào)用ReplicationCron 這個(gè)函數(shù),每隔一秒鐘會觸發(fā)這個(gè)函數(shù)。
Replication 機(jī)制的工作原理。假如說,我們支持 psync 這個(gè)協(xié)議,服務(wù)端會發(fā)送我現(xiàn)在的 runid 和 offset。相當(dāng)于 rdb 同步到哪個(gè)地方了,會把 offset 發(fā)送給客戶端,每個(gè)客戶端都會保持一個(gè) cashed_master 節(jié)點(diǎn),就是長鏈接斷掉之后,還會有一個(gè) cashed_master 在。假如不支持 psync 協(xié)議,則發(fā)送 sync 協(xié)議。
服務(wù)器端的實(shí)現(xiàn),主要由syncCommand實(shí)現(xiàn),它主要的執(zhí)行過程是這樣的。
- 第一、psync這種模式,首先會進(jìn)行runid和offset的校驗(yàn),并發(fā)送新的給客戶端。
- 第二、psync最后會把現(xiàn)在內(nèi)存里面增量的數(shù)據(jù)發(fā)送給客戶端。
- 第三、如果全量同步,首先會觸發(fā)一個(gè)bgsave,把內(nèi)存里面的數(shù)據(jù),本地保存一份,再推給客戶端。如果我們沒有定制過的Redis服務(wù)器,直接從Redis那個(gè)網(wǎng)站上下載的Redis服務(wù)器,如果在全量同步的時(shí)候,客戶端連接太多,調(diào)用的時(shí)候就會斷掉。
- 第四、觸發(fā)sync的過程。如果是全量,先rdb保存一份,再把全量的數(shù)據(jù)托管。
定制開發(fā) Redis
首先,在 Redis.c 文件找到 RedisCommandTable,添加命令,比如添加“test”,testCommannd,-5 的函數(shù)。
第二、添加命令處理函數(shù)。完了我們要修改這個(gè) makefile 文件,最終編譯打包。其實(shí)真正做的時(shí)候沒有那么簡單,因?yàn)?Redis 在內(nèi)部,你在調(diào)用過程當(dāng)中,會用到它很多內(nèi)部的函數(shù)。所以,你要真正的完整開發(fā)定制一個(gè) Redis,步驟是這樣,但是需要把這些函數(shù)從頭到尾學(xué)習(xí)一遍,如果你自己又去開發(fā)函數(shù),會把 Redis 搞得亂七八糟,很糟糕,可能不一定能跑的很好。
轉(zhuǎn)載于:https://www.cnblogs.com/davidwang456/articles/9294912.html
總結(jié)
以上是生活随笔為你收集整理的Redis 的源码分析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 使用mspaint查看图片像素
- 下一篇: 如何优雅的分析 Redis 里存了啥?