河貍家:Redis 源碼的深度剖析 | UPYUN 技術現場

jopen 9年前發布 | 11K 次閱讀 Redis NoSQL數據庫

原文鏈接: http://t.cn/RyuxZQJ

Redis 這個東西很簡單,懂 C 語言的同學花一個下午,可以把它的來龍去脈都研究懂。但是,它麻雀雖小五臟俱全。一個常見的軟件,比如 Redis,跑起來該用的東西可能都用一些,如果我們把 Redis 搞懂了,要分析一款其他的軟件,思路可能也是差不多的,所以我借這個機會,跟大家分享一下我們解剖一個軟件的過程。

分享 Redis,主要通過以下幾個步驟。

啟動過程

河貍家:Redis 源碼的深度剖析 | UPYUN 技術現場

首先,看一下 Redis 的一個啟動過程。任何一款軟件,它的很多C語言實現的過程,都是從 main 函數這個漏斗開始的。一般任何軟件設計的時候,不管是 Redis,還是阿帕奇,或者亂七八糟的東西,一般 Main 函數都定義在跟它軟件名字一樣的. C 文件里面,里面 main 函數執行的過程分以下幾步:

第一步,Redis 會設置一些回調函數,當前時間,隨機數的種子。回調函數實際上什么?舉個例子,比如 Q/3 要給 Redis 發送一個關閉的命令,讓它去做一些優雅的關閉,做一些掃尾清楚的工作,這個工作如果不設計回調函數,它其實什么都不會干。其實 C 語言的程序跑在操作系統之上,Linux 操作系統本身就是提供給我們事件機制的回調注冊功能,所以它會設計這個回調函數,讓你注冊上,關閉的時候優雅的關閉,然后它在后面可以做一些業務邏輯。

第二步,不管任何軟件,肯定有一份配置文件需要配置。首先在服務器端會把它默認的一份配置做一個初始化。

第三步,Redis 在 3.0 版本正式發布之前其實已經有篩選這個模式了,但是這個模式,我很少在生產環境在用。Redis 可以初始化這個模式,比較復雜。

第四步,解析啟動的參數。其實不管什么軟件,它在初始化的過程當中,配置都是由兩部分組成的。第一部分,靜態的配置文件;第二部分,動態啟動的時候,main,就是參數給它的時候進去配置。

第五步,把服務端的東西拿過來,裝載 Config 配置文件,loadServerConfig。

第六步,初始化服務器,initServer。

第七步,從磁盤裝載數據。

第八步,有一個主循環程序開始干活,用來處理客戶端的請求,并且把這個請求轉到后端的業務邏輯,幫你完成命令執行,然后吐數據,這么一個過程。

服務器的模型

河貍家:Redis 源碼的深度剖析 | UPYUN 技術現場

接下來看一下 Redis 服務器的模型。Redis 實現的過程當中,基于不動的操作系統,封裝了不同的模型。舉個例子,它在 Linux 上面是基于 epoll 做了一個封裝,不管怎么樣,它都是以 ae_epoll.c 封裝的。封裝過程當中有三個步驟,我們用原生調用 epoll 的時候也是三個步驟完成。第一個步驟,aeApiCreate,就是 epoll 的一個池子,先創建了一個池子的東西。第二、通過 ApiAddEvent 調用 epoll 這個函數,可以往 epoll 池子里面注冊事件。第三、ApiPoll,通過 epoll_wait 來獲取已經響應的事件。

Redis 在服務端初始化 epoll

河貍家:Redis 源碼的深度剖析 | UPYUN 技術現場

首先,在 main 函數初始化過程當中調用了 innitServer,其實就是調用剛才講的 aeCreateEvent ,創建了 epoll 池子。然后調用函數,設定 EVENTLOOP_FDSET_INCR。然后設置回調函數,注冊的事件響應之后要干活,這是一個循環調用的過程。怎么調呢?我們把 aeCreateEvent 這個函數展開,里面有兩個過程,Event如果這個死循環在調用的過程當中,可以跟兩類事件發生交道。第一類事件,aeflieEvent。第二類事 件,aeTimeEvent。因為 Redis 針對 epoll 再做一次封裝的時候,它實現了一個定時器,這個定時器可以把你想要注冊到這個定時器里面的一些事件注冊進去。舉個例子,比如內存淘汰的時候,是一個 LRU 的一個算法,你注冊到這個定時器,比如內存達到某個大小,比如限制兩兆,當它大于兩兆的時候要淘汰,這個時候定時器在這個場景下面就會發生作用。

主循環的實現原理

河貍家:Redis 源碼的深度剖析 | UPYUN 技術現場

Redis 真正的主循環的原理,大致可以分成三步:

第一步,查找一些優先要處理的事件。什么叫優先要處理?你在調用API的時候,這個 API 可能作為 Redis 的使用者不會去關注。但是作為 Redis 的開發者他可能會關注到。你首先要讓 Redis 執行一個東西,它這個時候會優先去做處理。

第二步,假如說沒有優先處理實踐,則執?aeApiPoll 來處理 epoll 中的就緒事件。

最后,處理定時器任務。

服務器整體架構圖

河貍家:Redis 源碼的深度剖析 | UPYUN 技術現場

我們可以通過這張圖回顧一下它整體服務器的架構,其實就是這么一回事。最中間圓圈,代表了一個死循環。死循環要跑的時候,要干哪些活?我們把邏輯 注冊到某個池子里面,比如注冊到 epoll 的池子里面,或者注冊到定時器當中。它都是通過一些回調函數注冊的。比如 TCP 的時間要響應,就不停的執行,這么一個過程,Redis 本身實現也不是太復雜。

當你啟動 Redis 的時候,它本身就是一個單進程,單線程的模式。所以,我們在事件處理過程當中,要做到非常小心,精確的做一些控制,因為你的事件一旦進到 Redis 里面,比如我們簡單的讓 Redis 做一個技術器加法運算,如果加法運算時間花的很多,后面的規模可能就一直等在那里,執行不下去了,因為它是單線程,單進程的。所以說,如果你讓 Redis 同步在執行的過程當中,它必然是 CPU 密集型的運算,而且能很快計算完畢,把結果推送給你。

請求協議

河貍家:Redis 源碼的深度剖析 | UPYUN 技術現場

其實請求的協議,在前面 main 函數執行過程當中會 initSever,在 initSever 過程當中我們會注冊一個 acceptTcpHandler 回調函數,然后這個函數就會被調用了。Redis 請求協議分稱兩種,第一、inline 協議,第二、multibulk 協議,如果不是各*開頭,就是 inline 協議。

首先,看 inline 的協議,調用 processInline 這個函數比較簡單,當你把數據發送給服務端,任何的軟件都會把這個數據丟到一個緩存區,Redis 里面有一個 querybuf 結構,執行到緩存區,然后存入到 client 的 arg 數組,argc 代表了參數的格式。processMultibulkBuffer 協議,我們這里有三個參數的數量,比如 3,指的是長度 3,具體就是這么一個過程。

當我們把這數據完全解析完之后,這個時候就知道它是什么命令了。比如剛才 Set 命令已經解析完,我們知道它是一個 Set 命令,并且知道它的參數是什么。這時候我們會調用 processcommand 這個函數,執行的過程分成 12 個步驟:

第一、假如命令當中包含了 quit,后面的指令將不會被執行,直接會返回退出來。

第二、如果不包含 quit,它有一個 cmd 的結構數組,會到里面查找現在命令到底是哪一個,把具體要執行命令的函數執政找到。

第三、檢測命令的參數個數。

第四、如果服務器配置需要密碼檢驗功能,調用的命令必須是 authCommand。

第五、如果服務器有最大內存限制,必須限制性一下 freeMemorylfNeed 這個過程。

第六、如果服務器狀態出現了問題,那么停止執行命令。

第七、如果服務器設置了最小的 slave 數量限制,當 slave 數量小于最小 slave 數量的時候,停止執行命令。

第八、如果服務器為 slave,則不接受 write 命令。

第九、只能支持 pub/sub 相關的命令了。

第十、當 slave 和 mater 的連接已經斷開,并且設置了跟 mater 斷開后不再提供服務,那么停止執行命令。

第十一、如果服務器正在裝載數據中,則不接受命令。

?

?

第十二、如果 lua 腳本執行速度太慢了,也會停止執行命令。

</div>

在命令真正的執行過程當中,Redis 分成了兩個步驟。第一種,假如已經用了剛才講的事務處理模式,Redis 會把命令在 Q 里面存起來。所以,真正到 EXEC 之前,打開事務模式,把丟過來的命令先在 Q 存起來,真正執行的時候再執行。第二種,假如不是事務模式,這個時候它就會去真正調用這個 proc 函數,把 Redis 命令真正在后臺執行。比如,剛才提到的事務模式,通過 MULTI 關鍵詞輸入,后面就起到命令模式,如果后面不調用,它就不會真正執行。

命令執行過程

河貍家:Redis 源碼的深度剖析 | UPYUN 技術現場

剛才事務執行時候的命令過程,會把隊列里面的命令一個一個拿出來,然后去執行的過程。一個正常命令的執行過程,主要是分成幾個步驟:

第一,假如有監視器狀態的客戶端,首先會把命令發送給客戶端。什么叫監視器?舉個例子,我是mater slave機制的,首先要把這個機制告訴slave,你要去執行這條命令。

第二、真正執行。

第三、開啟慢查詢。

第四、監視就是監視器的命令,哪條命令要執行了,什么日志,什么參數都會發送給我,這是第一步要執行的,只有真正執行完,才會把這個工作發送給AOF和Slave,這樣才符合邏輯。

AddReply 會注冊寫事件到 epoll 里面去,通過 prepareClientToWrite。第二、會調用 _addReplyToBuffer 數據寫到 buf 中。下一次執行的時候才會循環這個動作,這樣每次做的時候,TPS 在單線程,單進程的情況下還能達到理想的狀況。第三、假如 buf 為不夠大,會添加到鏈表里面去。

其實 RedisDb 最最核心的實現就是一個置頂的實現,比如有存數據的置頂,就是要不要過期,其實也是存在置頂里面。舉個例子,有些請求它其實會阻塞的,阻塞到哪里?有一個 阻塞器置頂。當阻塞已經就緒了,有一個就緒的 1 K的置頂,還可以堅持某個 K。置頂的具體實現,就不再講了。

核心數據結構

河貍家:Redis 源碼的深度剖析 | UPYUN 技術現場

因為我們最終服務器其實都跟核心的數據結構操作相關。首先,看 string 這個東西,其實 string 就是一個 struct 指針,可以描述長度,還剩余多少等等這些東西。看一下 struct 指針到底怎么指的,它會把 sdshdr 放到內存的前面,把 buf 放到內存的后面。Redis 檢索怎么查找到 sdshdr 這個區域,一般通過目前 buf 最前置的指針減去 sdshdr 這個長度,就知道 sdshdr 在哪里。

河貍家:Redis 源碼的深度剖析 | UPYUN 技術現場

我們知道字符串其實就是一個 struct 結構,接下來看一下 hash 結構怎么實現的。hash 本質是基于 ziplist 的實現,關于 ziplist 的實現,ziplist 通過文本定義了一個數據結構。其實 ziplist 可以認為里面是一個一個的元素。我們理解 hash_max 的時候,有一個 hash_max_ziplist_value 的結構,就是通過這張圖描述的這種方式把里面的東西撈出來了。當然,ziplist 在存儲 hash 的時候,hash 通過兩種方式存的。第一、ziplist 這種結構。因為 ziplist 具體的長度是可以設置的,當你的長度超過了某個數值之后,它就會轉成 dict 的這個結構,最最原始的 dict 的結構,這樣它存儲的時候都存到 dict 的結構體里面去了。

河貍家:Redis 源碼的深度剖析 | UPYUN 技術現場

list 其實就是我們通常用的比較經典的這種雙向鏈表,頭指針,尾指針,定義了 list。接下來還有一個 set。其實 Redis set 還是存在 dict 這樣的結構里面的,因為 list 只有 Velue 沒有 Key。Redis 還有一個數據結構叫 Sorted Sets,它是為了加速檢索的過程,用到以空間換時間的方式。舉個例子,可能有些場景用搜索引擎構建的時候,覺得太麻煩,會建幾張表做索引,其實 Sorted Sets 也是一樣的,就是通過 span 結構實現了多級索引查詢的過程。可以在這個 Velue 之上通過多級指針進行檢索。Redis里面有一個 pub/sub_channels 這么一個屬性,當有什么東西要給客戶端的時候,會到這個隊列里面查看有沒有注冊上來的客戶端。

事務處理當中,可能還要注意幾點:

首先,假如客戶端的 flag 是 DIRTY_CAS 或者是 DIRTY_EXEC,就放棄執行事務了。

第二、在事務執行期間,取消對 key 的 Watch。

第三、遍歷執行隊列中的命令。

第四、通過 ReplicationFeedmonitors 服務器同步給 Monitors 客戶端進程。

持久化 rdb 的過程,其實 Redis 服務器分成兩個步驟,第一、rdb 的持久化,第二、AOF 的持久化,基于 rdb 的持久化方式,服務器啟動的時候,首先會調用 serverparamslen 的函數,然后 rdb 的工作會把內存里面存的數據,原封不動的拷貝,存儲到本地磁盤當中去。rdbSave 不是讓組件程序看這個活,我們需要 fork 一個子進程專門做 rdeSave 的數。

1、創建臨時文件:temp-%d 為 rdb

2、調用 rdbSaveRio 將 db 中的數據獬入到臨時文件。

3、調用 fflush,fsync 將緩存中的數據刷新到磁盤。

4、將 temp 文件重命名為正式的rdb文件,后面就是這些描述,這些描述跟前面講的 Redis 的數據結構其實是對應起來的,然后以這種方式存到這個里面去。

aof 存儲的格式和剛才我們請求協議里面講到的協議是一模一樣的,就是純文本的,比如 set 什么東西,就是一模一樣的東西存在這個文件里面。假如開啟了 aof 這個功能,會把你歷史執行的命令記錄原封不動都存在里面,這樣這個文件會越來越大。當然,Redis 提供給我們一個功能,可以把 aof 命令壓縮。在每次 Redis 重啟之后,如果開啟了 aof 功能,就會重載 aof 文件中的數據執行命令。然后 Redis 提供了 rewriteaof 定期壓縮的功能,其實就是把 db 中的數據重新生成一份新的 aof。

Redis 的內存分配還是比較簡單,不像 memorycash。Redis 通過調用原生的函數直接向操作系統申請內存。當內存不停的申請,在使用一段時間之后,Redis 會處罰一些淘汰的策略。這個淘汰分成兩種,一種是主動淘汰,舉個例子,當我們在調用 RandomKey 等這些函數的時候,首先會主動的淘汰一些內存,這個就叫主動淘汰。還有一種淘汰是 lru 的淘汰,當你在執行的過程當中,如果內存不夠,就會處罰 lru 的淘汰算法。另外,還有被動淘汰,前面講到因為我們在 main 函數調用真正的 epoll 死循環的前置有一個 beforeSleep,beforeSleep 函數里面會在 databasesCron 定時器都調用 activeExpireCycle。

Replication 機制

河貍家:Redis 源碼的深度剖析 | UPYUN 技術現場

RedisReplication 的機制,分為客戶端請求和服務器的處理。我們啟動客戶端的時候,main 函數里面會調用 serverCron,在 serverCron 里又會調用ReplicationCron 這個函數,每隔一秒鐘會觸發這個函數。

Replication 機制的工作原理。假如說,我們支持 psync 這個協議,服務端會發送我現在的 runid 和 offset。相當于 rdb 同步到哪個地方了,會把 offset 發送給客戶端,每個客戶端都會保持一個 cashed_master 節點,就是長鏈接斷掉之后,還會有一個 cashed_master 在。假如不支持 psync 協議,則發送 sync 協議。

服務器端的實現,主要由syncCommand實現,它主要的執行過程是這樣的。

第一、psync這種模式,首先會進行runid和offset的校驗,并發送新的給客戶端。

第二、psync最后會把現在內存里面增量的數據發送給客戶端。

第三、如果全量同步,首先會觸發一個bgsave,把內存里面的數據,本地保存一份,再推給客戶端。如果我們沒有定制過的Redis服務器,直接從Redis那個網站上下載的Redis服務器,如果在全量同步的時候,客戶端連接太多,調用的時候就會斷掉。

第四、觸發sync的過程。如果是全量,先rdb保存一份,再把全量的數據托管。

定制開發 Redis

河貍家:Redis 源碼的深度剖析 | UPYUN 技術現場

首先,在 Redis.c 文件找到 RedisCommandTable,添加命令,比如添加“test”,testCommannd,-5 的函數。

第二、添加命令處理函數。完了我們要修改這個 makefile 文件,最終編譯打包。其實真正做的時候沒有那么簡單,因為 Redis 在內部,你在調用過程當中,會用到它很多內部的函數。所以,你要真正的完整開發定制一個 Redis,步驟是這樣,但是需要把這些函數從頭到尾學習一遍,如果你自己又去開發函數,會把 Redis 搞得亂七八糟,很糟糕,可能不一定能跑的很好。

  • 本文整理自 UPYUN Open Talk 主題技術沙龍第十四期的講師現場分享內容。

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!