文:河狸家 架構師 陳科算法
原文連接:http://t.cn/RyuxZQJ後端
Redis 這個東西很簡單,懂 C 語言的同窗花一個下午,能夠把它的前因後果都研究懂。可是,它麻雀雖小五臟俱全。一個常見的軟件,好比 Redis,跑起來該用的東西可能都用一些,若是咱們把 Redis 搞懂了,要分析一款其餘的軟件,思路可能也是差很少的,因此我借這個機會,跟你們分享一下咱們解剖一個軟件的過程。數組
分享 Redis,主要經過如下幾個步驟。緩存
首先,看一下 Redis 的一個啓動過程。任何一款軟件,它的不少C語言實現的過程,都是從 main 函數這個漏斗開始的。通常任何軟件設計的時候,無論是 Redis,仍是阿帕奇,或者亂七八糟的東西,通常 Main 函數都定義在跟它軟件名字同樣的. C 文件裏面,裏面 main 函數執行的過程分如下幾步:服務器
第一步,Redis 會設置一些回調函數,當前時間,隨機數的種子。回調函數實際上什麼?舉個例子,好比 Q/3 要給 Redis 發送一個關閉的命令,讓它去作一些優雅的關閉,作一些掃尾清楚的工做,這個工做若是不設計回調函數,它其實什麼都不會幹。其實 C 語言的程序跑在操做系統之上,Linux 操做系統自己就是提供給咱們事件機制的回調註冊功能,因此它會設計這個回調函數,讓你註冊上,關閉的時候優雅的關閉,而後它在後面能夠作一些業務邏輯。微信
第二步,無論任何軟件,確定有一份配置文件須要配置。首先在服務器端會把它默認的一份配置作一個初始化。數據結構
第三步,Redis 在 3.0 版本正式發佈以前其實已經有篩選這個模式了,可是這個模式,我不多在生產環境在用。Redis 能夠初始化這個模式,比較複雜。架構
第四步,解析啓動的參數。其實無論什麼軟件,它在初始化的過程中,配置都是由兩部分組成的。第一部分,靜態的配置文件;第二部分,動態啓動的時候,main,就是參數給它的時候進去配置。併發
第五步,把服務端的東西拿過來,裝載 Config 配置文件,loadServerConfig。dom
第六步,初始化服務器,initServer。
第七步,從磁盤裝載數據。
第八步,有一個主循環程序開始幹活,用來處理客戶端的請求,而且把這個請求轉到後端的業務邏輯,幫你完成命令執行,而後吐數據,這麼一個過程。
接下來看一下 Redis 服務器的模型。Redis 實現的過程中,基於不動的操做系統,封裝了不一樣的模型。舉個例子,它在 Linux 上面是基於 epoll 作了一個封裝,無論怎麼樣,它都是以 ae_epoll.c 封裝的。封裝過程中有三個步驟,咱們用原生調用 epoll 的時候也是三個步驟完成。第一個步驟,aeApiCreate,就是 epoll 的一個池子,先建立了一個池子的東西。第2、經過 ApiAddEvent 調用 epoll 這個函數,能夠往 epoll 池子裏面註冊事件。第3、ApiPoll,經過 epoll_wait 來獲取已經響應的事件。
首先,在 main 函數初始化過程中調用了 innitServer,其實就是調用剛纔講的 aeCreateEvent ,建立了 epoll 池子。而後調用函數,設定 EVENTLOOP_FDSET_INCR。而後設置回調函數,註冊的事件響應以後要幹活,這是一個循環調用的過程。怎麼調呢?咱們把 aeCreateEvent 這個函數展開,裏面有兩個過程,Event若是這個死循環在調用的過程中,能夠跟兩類事件發生交道。第一類事件,aeflieEvent。第二類事件,aeTimeEvent。由於 Redis 針對 epoll 再作一次封裝的時候,它實現了一個定時器,這個定時器能夠把你想要註冊到這個定時器裏面的一些事件註冊進去。舉個例子,好比內存淘汰的時候,是一個 LRU 的一個算法,你註冊到這個定時器,好比內存達到某個大小,好比限制兩兆,當它大於兩兆的時候要淘汰,這個時候定時器在這個場景下面就會發生做用。
Redis 真正的主循環的原理,大體能夠分紅三步:
第一步,查找一些優先要處理的事件。什麼叫優先要處理?你在調用API的時候,這個 API 可能做爲 Redis 的使用者不會去關注。可是做爲 Redis 的開發者他可能會關注到。你首先要讓 Redis 執行一個東西,它這個時候會優先去作處理。
第二步,假如說沒有優先處理實踐,則執⾏aeApiPoll 來處理 epoll 中的就緒事件。
最後,處理定時器任務。
咱們能夠經過這張圖回顧一下它總體服務器的架構,其實就是這麼一回事。最中間圓圈,表明了一個死循環。死循環要跑的時候,要幹哪些活?咱們把邏輯註冊到某個池子裏面,好比註冊到 epoll 的池子裏面,或者註冊到定時器當中。它都是經過一些回調函數註冊的。好比 TCP 的時間要響應,就不停的執行,這麼一個過程,Redis 自己實現也不是太複雜。
當你啓動 Redis 的時候,它自己就是一個單進程,單線程的模式。因此,咱們在事件處理過程中,要作到很是當心,精確的作一些控制,由於你的事件一旦進到 Redis 裏面,好比咱們簡單的讓 Redis 作一個技術器加法運算,若是加法運算時間花的不少,後面的規模可能就一直等在那裏,執行不下去了,由於它是單線程,單進程的。因此說,若是你讓 Redis 同步在執行的過程中,它必然是 CPU 密集型的運算,並且能很快計算完畢,把結果推送給你。
其實請求的協議,在前面 main 函數執行過程中會 initSever,在 initSever 過程中咱們會註冊一個 acceptTcpHandler 回調函數,而後這個函數就會被調用了。Redis 請求協議分稱兩種,第1、inline 協議,第2、multibulk 協議,若是不是各*開頭,就是 inline 協議。
首先,看 inline 的協議,調用 processInline 這個函數比較簡單,當你把數據發送給服務端,任何的軟件都會把這個數據丟到一個緩存區,Redis 裏面有一個 querybuf 結構,執行到緩存區,而後存入到 client 的 arg 數組,argc 表明了參數的格式。processMultibulkBuffer 協議,咱們這裏有三個參數的數量,好比 3,指的是長度 3,具體就是這麼一個過程。
當咱們把這數據徹底解析完以後,這個時候就知道它是什麼命令了。好比剛纔 Set 命令已經解析完,咱們知道它是一個 Set 命令,而且知道它的參數是什麼。這時候咱們會調用 processcommand 這個函數,執行的過程分紅 12 個步驟:
第1、假如命令當中包含了 quit,後面的指令將不會被執行,直接會返回退出來。
第2、若是不包含 quit,它有一個 cmd 的結構數組,會到裏面查找如今命令究竟是哪個,把具體要執行命令的函數執政找到。
第3、檢測命令的參數個數。
第4、若是服務器配置須要密碼檢驗功能,調用的命令必須是 authCommand。
第5、若是服務器有最大內存限制,必須限制性一下 freeMemorylfNeed 這個過程。
第6、若是服務器狀態出現了問題,那麼中止執行命令。
第7、若是服務器設置了最小的 slave 數量限制,當 slave 數量小於最小 slave 數量的時候,中止執行命令。
第8、若是服務器爲 slave,則不接受 write 命令。
第9、只能支持 pub/sub 相關的命令了。
第10、當 slave 和 mater 的鏈接已經斷開,而且設置了跟 mater 斷開後再也不提供服務,那麼中止執行命令。
第11、若是服務器正在裝載數據中,則不接受命令。
第12、若是 lua 腳本執行速度太慢了,也會中止執行命令。
在命令真正的執行過程中,Redis 分紅了兩個步驟。第一種,假如已經用了剛纔講的事務處理模式,Redis 會把命令在 Q 裏面存起來。因此,真正到 EXEC 以前,打開事務模式,把丟過來的命令先在 Q 存起來,真正執行的時候再執行。第二種,假如不是事務模式,這個時候它就會去真正調用這個 proc 函數,把 Redis 命令真正在後臺執行。好比,剛纔提到的事務模式,經過 MULTI 關鍵詞輸入,後面就起到命令模式,若是後面不調用,它就不會真正執行。
剛纔事務執行時候的命令過程,會把隊列裏面的命令一個一個拿出來,而後去執行的過程。一個正常命令的執行過程,主要是分紅幾個步驟:
第一,假若有監視器狀態的客戶端,首先會把命令發送給客戶端。什麼叫監視器?舉個例子,我是mater slave機制的,首先要把這個機制告訴slave,你要去執行這條命令。
第2、真正執行。
第3、開啓慢查詢。
第4、監視就是監視器的命令,哪條命令要執行了,什麼日誌,什麼參數都會發送給我,這是第一步要執行的,只有真正執行完,纔會把這個工做發送給AOF和Slave,這樣才符合邏輯。
AddReply 會註冊寫事件到 epoll 裏面去,經過 prepareClientToWrite。第2、會調用 _addReplyToBuffer 數據寫到 buf 中。下一次執行的時候纔會循環這個動做,這樣每次作的時候,TPS 在單線程,單進程的狀況下還能達到理想的情況。第3、假如 buf 爲不夠大,會添加到鏈表裏面去。
其實 RedisDb 最最核心的實現就是一個置頂的實現,好比有存數據的置頂,就是要不要過時,其實也是存在置頂裏面。舉個例子,有些請求它其實會阻塞的,阻塞到哪裏?有一個阻塞器置頂。當阻塞已經就緒了,有一個就緒的 1 K的置頂,還能夠堅持某個 K。置頂的具體實現,就再也不講了。
由於咱們最終服務器其實都跟核心的數據結構操做相關。首先,看 string 這個東西,其實 string 就是一個 struct 指針,能夠描述長度,還剩餘多少等等這些東西。看一下 struct 指針到底怎麼指的,它會把 sdshdr 放到內存的前面,把 buf 放到內存的後面。Redis 檢索怎麼查找到 sdshdr 這個區域,通常經過目前 buf 最前置的指針減去 sdshdr 這個長度,就知道 sdshdr 在哪裏。
咱們知道字符串其實就是一個 struct 結構,接下來看一下 hash 結構怎麼實現的。hash 本質是基於 ziplist 的實現,關於 ziplist 的實現,ziplist 經過文本定義了一個數據結構。其實 ziplist 能夠認爲裏面是一個一個的元素。咱們理解 hash_max 的時候,有一個 hash_max_ziplist_value 的結構,就是經過這張圖描述的這種方式把裏面的東西撈出來了。固然,ziplist 在存儲 hash 的時候,hash 經過兩種方式存的。第1、ziplist 這種結構。由於 ziplist 具體的長度是能夠設置的,當你的長度超過了某個數值以後,它就會轉成 dict 的這個結構,最最原始的 dict 的結構,這樣它存儲的時候都存到 dict 的結構體裏面去了。
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,就放棄執行事務了。
第2、在事務執行期間,取消對 key 的 Watch。
第3、遍歷執行隊列中的命令。
第4、經過 ReplicationFeedmonitors 服務器同步給 Monitors 客戶端進程。
持久化 rdb 的過程,其實 Redis 服務器分紅兩個步驟,第1、rdb 的持久化,第2、AOF 的持久化,基於 rdb 的持久化方式,服務器啓動的時候,首先會調用 serverparamslen 的函數,而後 rdb 的工做會把內存裏面存的數據,原封不動的拷貝,存儲到本地磁盤當中去。rdbSave 不是讓組件程序看這個活,咱們須要 fork 一個子進程專門作 rdeSave 的數。
一、建立臨時文件:temp-%d 爲 rdb
二、調用 rdbSaveRio 將 db 中的數據獬入到臨時文件。
三、調用 fflush,fsync 將緩存中的數據刷新到磁盤。
四、將 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。
RedisReplication 的機制,分爲客戶端請求和服務器的處理。咱們啓動客戶端的時候,main 函數裏面會調用 serverCron,在 serverCron 裏又會調用ReplicationCron 這個函數,每隔一秒鐘會觸發這個函數。
Replication 機制的工做原理。假如說,咱們支持 psync 這個協議,服務端會發送我如今的 runid 和 offset。至關於 rdb 同步到哪一個地方了,會把 offset 發送給客戶端,每一個客戶端都會保持一個 cashed_master 節點,就是長連接斷掉以後,還會有一個 cashed_master 在。假如不支持 psync 協議,則發送 sync 協議。
服務器端的實現,主要由syncCommand實現,它主要的執行過程是這樣的。
第1、psync這種模式,首先會進行runid和offset的校驗,併發送新的給客戶端。
第2、psync最後會把如今內存裏面增量的數據發送給客戶端。
第3、若是全量同步,首先會觸發一個bgsave,把內存裏面的數據,本地保存一份,再推給客戶端。若是咱們沒有定製過的Redis服務器,直接從Redis那個網站上下載的Redis服務器,若是在全量同步的時候,客戶端鏈接太多,調用的時候就會斷掉。
第4、觸發sync的過程。若是是全量,先rdb保存一份,再把全量的數據託管。
首先,在 Redis.c 文件找到 RedisCommandTable,添加命令,好比添加「test」,testCommannd,-5 的函數。
第2、添加命令處理函數。完了咱們要修改這個 makefile 文件,最終編譯打包。其實真正作的時候沒有那麼簡單,由於 Redis 在內部,你在調用過程中,會用到它不少內部的函數。因此,你要真正的完整開發定製一個 Redis,步驟是這樣,可是須要把這些函數從頭至尾學習一遍,若是你本身又去開發函數,會把 Redis 搞得亂七八糟,很糟糕,可能不必定能跑的很好。
本文整理自 UPYUN Open Talk 主題技術沙龍第十四期的講師現場分享內容。
查看 & 下載講師課件及現場視頻、瞭解更多該活動產生的技術分享內容,請關注 UPYUN 微信公衆號(upaiyun)