楔子
這里我們需要先解釋一下,為什么要閱讀 Redis 源碼。平常我們在基於 Redis 做應用開發時,可能只是將 Redis 作為一個緩存系統或是數據庫來存取數據,並不會接觸到源碼層面的東西。比如,我們在做社交應用開發時,會將用戶數據、關注信息等緩存在 Redis 中;在開發存儲系統軟件時,也會用 Redis 保存系統元數據。
但是在使用或運維 Redis 時,我們經常會面臨 Redis 性能變差、Redis 實例故障等問題,而這些問題都會影響到業務應用的運行。如果你不了解 Redis 源碼層面的實現原理,那么你在實際開發中排查問題的時候就會收到阻礙。舉個簡單的例子:Redis 在運行過程中,隨着保存數據的增加,會進行 rehash 操作,而 rehash 操作會對 Redis 的性能造成一定影響。如果我們想定位當前性能問題是否由 rehash 引起,我們就需要了解 rehash 的具體觸發時機,這就包括 rehash 的觸發條件有哪些,以及在哪些操作過程中會對這些觸發條件進行判斷。
可是當我們只是了解 rehash 的基本原理時,我們就只是知道當哈希表的負載因子大於預設閾值后,就會開始執行 rehash。但是具體到 Redis 來說,我們還需要進一步了解:
哈希表的負載因子是怎么算的?知道了這一點,我們可以推算 Redis 的負載壓力除了負載因子這一條件,是否還有其他觸發條件?了解這一點,可以幫助我們結合 Redis 運行情況,推斷當前是否發生 rehashrehash 觸發條件的判斷會在哪些函數中進行調用?了解這一點很有用,可以讓我們知道在哪些操作執行過程中,會判斷 rehash 觸發條件,進而執行 rehash
你看,雖然從原理上說這是一個 rehash 操作,但一旦落到實際的性能問題排查時,我們卻會面臨很多的具體問題。而要想解決這些問題,最好的辦法就是閱讀和學習 Redis 源碼,通過學習源碼,我們能進一步掌握 Redis 的實現細節,這帶來的最明顯收益就是,能了解 Redis 運行過程中要判斷和處理的各種條件。這些細節正對應了我們在排查 Redis 性能、故障問題時的排查思路,可以幫助我們有章法、高效地解決問題。
當然學習 Redis 源代碼除了能更加深刻地理解 Redis 以及在使用 Redis 出現故障時能夠更好地定位問題之外,最關鍵的是我們也能提升我們的編程水平、設計思想。因為 Redis 是一個非常經典的內存數據庫,涉及到了軟件設計的方方面面,從單體設計到分布式,以及各種的網絡、內存相關的優化。

可以看到包含的內容相當之多,因此學習 Redis 源碼對我們是有極大好處的,里面的知識點完全可以用在其它的任何項目上。
Redis 源代碼目錄結構
在學習 Redis 源代碼之前,我們需要對 Redis 代碼的整體架構有一個了解,因為一旦掌握了 Redis 代碼的整體架構,就相當於給 Redis 代碼畫了張全景圖。有了這張圖,我們再去學習 Redis 不同功能模塊的設計與實現時,就可以從圖上快速查找和定位這些功能模塊對應的代碼文件。而且有了代碼的全景圖之后,我們還可以對 Redis 各方面的功能特性有個全面了解,這樣也便於更加全面地掌握 Redis 的功能,而不會遺漏某一特性。
而想了解一個項目的架構,看它的源代碼目錄是一個行之有效的方法。對於 Redis 而言,它的源碼總目錄下,一共包含了 deps、src、tests、utils 四個子目錄,這四個子目錄分別對應了 Redis 中發揮不同作用的代碼。

我們介紹這些目錄的作用。
deps 目錄
這個目錄主要包含了 Redis 依賴的第三方代碼庫,包括 Redis 的 C 語言版本客戶端代碼 hiredis、jemalloc 內存分配器代碼、readline 功能的替代代碼 linenoise,以及 lua 腳本代碼。這部分代碼的一個顯著特點就是它們可以獨立於 Redis src 目錄下的功能源碼進行編譯,也就是說它們可以獨立於 Redis 存在和發展。

那么為什么在 Redis 源碼結構中會有第三方代碼庫目錄呢?其實主要有兩方面的原因。
一方面,Redis 作為一個用 C 語言寫的用戶態程序,它的不少功能是依賴於標准的 glibc 庫提供的,比如內存分配、行讀寫(readline)、文件讀寫、子進程 / 線程創建等。但是 glibc 庫提供的某些功能實現,效率並不高。
舉個簡單的例子,glibc 庫中實現的內存分配器的性能就不是很高,它的內存碎片化情況也比較嚴重。因此為了避免對系統性能產生影響,Redis 使用了 jemalloc 庫替換了 glibc 庫的內存分配器。可是,jemalloc 庫本身又不屬於 Redis 系統自身的功能,把它和 Redis 功能源碼放在一個目錄下並不合適,所以 Redis 使用了專門的 deps 目錄來保存這部分代碼。
另一方面,有些功能是 Redis 運行所需要的,但是這部分功能又會獨立於 Redis 進行開發和演進。這種類型最為典型的功能代碼,就是 Redis 的客戶端代碼。Redis 作為 Client-Server 架構的系統,訪問 Redis 離不開客戶端的支撐。此外,Redis 自身功能中的命令行 redis-cli、基准測試程序 redis-benchmark 以及哨兵,都需要用到客戶端來訪問 Redis 實例。
不過你應該也清楚,針對客戶端的開發,只要保證客戶端和服務端實例交互的過程滿足 RESP 協議就行,客戶端和服務端的功能可以各自迭代演進。所以在 Redis 源碼結構中,C 語言版本的客戶端 hiredis,就被放到了 deps 目錄中,以便開發人員自行開發和改進客戶端功能。
總而言之,對於 deps 目錄來說,你只需要記住它主要存放了三類代碼:一是 Redis 依賴的、實現更加高效的功能庫,如內存分配;二是獨立於 Redis 開發演進的代碼,如客戶端;三是 lua 腳本代碼。后續在學習這些功能的設計實現時,就可以在 deps 目錄找到它們。
src 目錄
這個目錄里面包含了 Redis 所有功能模塊的代碼文件,也是 Redis 源碼的重要組成部分,同樣我們先來看下 src 目錄下的子目錄結構。

我們會發現,src 目錄下只有一個 modules 子目錄,其中包含了一個實現 Redis module 的示例代碼。剩余的源碼文件都是在 src 目錄下,沒有再分下一級子目錄。
因為 Redis 的功能模塊實現是典型的 C 語言風格,不同功能模塊之間不再設置目錄分隔,而是通過頭文件包含來相互調用。這樣的代碼風格在基於 C 語言開發的系統軟件中也比較常見,比如 Memcached 的源碼文件也是在同一級目錄下。
所以當你使用 C 語言來開發軟件系統時,就可以參考 Redis 的功能源碼結構,用一個扁平的目錄組織所有的源碼文件,這樣模塊相互間的引用也會很方便。當然了,如果代碼量比較大,還是分層級比較合適,不過說實話我們一般也很少會用 C 語言去開發一個比 Redis 規模還大的項目。
tests 目錄
在軟件產品的開發過程中,除了第三方依賴庫和功能模塊源碼以外,我們通常還需要在系統源碼中,添加用於功能模塊測試和單元測試的代碼。而在 Redis 的代碼目錄中,就將這部分代碼用一個 tests 目錄統一管理了起來。
Redis 實現的測試代碼可以分成四部分,分別是單元測試(對應 unit 子目錄),Redis Cluster 功能測試(對應 cluster 子目錄)、哨兵功能測試(對應 sentinel 子目錄)、主從復制功能測試(對應 integration 子目錄)。這些子目錄中的測試代碼使用了 Tcl 語言(通用的腳本語言)進行編寫,主要目的就是方便進行測試。
另外,每一部分的測試都是一個測試集合,覆蓋了相應功能模塊中的多項子功能測試。比如在單元測試的目錄中,我們可以看到有針對過期 key 的測試(expire.tcl)、惰性刪除的測試(lazyfree.tcl),以及不同數據類型操作的測試(type 子目錄)等。而在 Redis Cluster 功能測試的目錄中,我們可以看到有針對故障切換的測試(failover.tcl)、副本遷移的測試(replica-migration.tcl)等。
不過在 tests 目錄中,除了有針對特定功能模塊的測試代碼外,還有一些代碼是用來支撐測試功能的,這些代碼在 assets、helpers、modules、support 四個目錄中。

utils 子目錄
在 Redis 開發過程中,還有一些功能屬於輔助性功能,包括用於創建 Redis Cluster 的腳本、用於測試 LRU 算法效果的程序,以及可視化 rehash 過程的程序。在 Redis 代碼結構中,這些功能代碼都被歸類到了 utils 目錄中統一管理。

所以當我們在開發系統時,就可以學習 Redis 的代碼結構,也把和系統相關的輔助性功能划歸到 utils 目錄中統一管理。
除了 deps、src、tests、utils 四個子目錄以外,Redis 源碼總目錄下其實還包含了兩個重要的配置文件,一個是 Redis 實例的配置文件 redis.conf,另一個是哨兵的配置文件 sentinel.conf。當你需要查找或修改 Redis 實例或哨兵的配置時,就可以直接定位到源碼主目錄下。
在了解了 Redis 的代碼目錄和層次以后,接下來,我們還需要重點學習下功能模塊的源碼文件(即 src 目錄下的文件內容),這有助於我們在后續課程中學習 Redis 的相關設計思想時,能夠快速找到對應的源碼文件。
Redis 功能模塊相關源碼文件
Redis 代碼結構中的 src 目錄,包含了實現功能模塊的 123 個代碼文件,Redis 服務端的所有功能實現都在這里面。在這 123 個代碼文件中,對於某個功能來說,一般包括了實現該功能的源文件(.c 文件) 和對應的頭文件(.h 文件),比如 dict.c 和 dict.h 就是用於實現哈希表的 C 文件和頭文件。
這里我們 Redis 源碼版本是 5.0.8
那么,我們該如何將這 123 個文件和 Redis 的主要功能對應上呢?其實,Redis 代碼文件的命名非常規范,文件名中就體現了該文件實現的主要功能。比如,對於 rdb.h 和 rdb.c 這兩個代碼文件來說,從文件名上,你就可以看出來它們是實現內存快照 RDB 的對應代碼。
所以這里為了能快速定位源碼,我們分別按照 Redis 的服務器實例、數據庫操作、可靠性和可擴展性保證、輔助功能四個維度,把 Redis 功能源碼梳理成了四條代碼路徑。你可以根據自己想要了解的功能維度,對應地學習相關代碼。
服務器實例
首先我們知道,Redis 在運行時是一個網絡服務器實例(也就是 redis-server 啟動之后對應的進程),因此相應地就需要有代碼實現服務器實例的初始化和主體控制流程,而這是由 server.h / server.c 實現的,Redis 整個代碼的 main 入口函數也是在 server.c 中。如果你想了解 Redis 是如何開始運行的,那么就可以從 server.c 的 main 函數開始看起。
當然,對於一個網絡服務器來說,它還需要提供網絡通信功能。Redis 使用了基於事件驅動機制的網絡通信框架,涉及的代碼文件包括 ae.h / ae.c,ae_epoll.c,ae_evport.c,ae_kqueue.c,ae_select.c。關於事件驅動框架的具體設計思路與實現方法,我們在后面會詳細介紹。
而除了事件驅動網絡框架以外,與網絡通信相關的功能還包括底層 TCP 網絡通信和客戶端實現。Redis 對 TCP 網絡通信的 Socket 連接、設置等操作進行了封裝,這些封裝后的函數實現在 anet.h/anet.c 中。這些函數在 Redis Cluster 創建和主從復制的過程中,會被調用並用於建立 TCP 連接。
除此之外,客戶端在 Redis 的運行過程中也會被廣泛使用,比如實例返回讀取的數據、主從復制時在主從庫間傳輸數據、Redis Cluster 的切片實例通信等,都會用到客戶端。Redis 將客戶端的創建、消息回復等功能,實現在了 networking.c 文件中,如果你想了解客戶端的設計與實現,可以重點看下這個代碼文件。

那么,在了解了 Redis 服務器實例的主要功能代碼之后,我們再從 Redis 內存數據庫這一特性維度,來梳理下與它相關的代碼文件。
數據庫數據類型與操作
Redis 數據庫提供了豐富的鍵值對類型,其中包括了 String、List、Hash、Set 和 Sorted Set 這五種基本鍵值類型。此外,Redis 還支持位圖、HyperLogLog、Geo 等擴展數據類型。而為了支持這些數據類型,Redis 就使用了多種數據結構來作為這些類型的底層結構,比如 String 類型的底層數據結構是 SDS,而 Hash 類型的底層數據結構包括哈希表和壓縮列表。
不過,因為 Redis 實現的底層數據結構非常多,所以這里我把這些底層結構和它們對應的鍵值對類型,以及相應的代碼文件列在了下表中,你可以用這張表來快速定位代碼文件。

除了實現了諸多的數據類型以外,Redis 作為數據庫,還實現了對鍵值對的新增、查詢、修改和刪除等操作接口,這部分功能是在 db.c 文件實現的。當然,Redis 作為內存數據庫,其保存的數據量受限於內存大小。因此,內存的高效使用對於 Redis 來說就非常重要。
那么你可能就要問了:Redis 是如何優化內存使用的呢?實際上,Redis 是從三個方面來優化內存使用的,分別是內存分配、內存回收,以及數據替換。
首先,在內存分配方面 Redis 支持使用不同的內存分配器,包括 glibc 庫提供的默認分配器 tcmalloc、第三方庫提供的 jemalloc,Redis 把對內存分配器的封裝實現在了 zmalloc.h/zmalloc.c 中。
其次,在內存回收上 Redis 支持設置過期 key,並針對過期 key 可以使用不同刪除策略,這部分代碼實現在 expire.c 文件中。同時為了避免大量 key 刪除回收內存,會對系統性能產生影響,Redis 在 lazyfree.c 中實現了異步刪除的功能,所以我們就可以使用后台 IO 線程來完成刪除,以避免對 Redis 主線程的影響。
最后,針對數據替換,如果內存滿了,Redis 還會按照一定規則清除不需要的數據,這也是 Redis 可以作為緩存使用的原因。Redis 實現的數據替換策略有很多種,包括 LRU、LFU 等經典算法,這部分的代碼實現在了 evict.c 中。
高可靠性和高可擴展性
首先,雖然 Redis 一般是作為內存數據庫來使用的,但是它也提供了可靠性保證,這主要體現在 Redis 可以對數據做持久化保存,並且它還實現了主從復制機制,從而可以提供故障恢復的功能。這部分的代碼實現比較集中,主要包括以下兩個部分。
數據持久化實現
Redis 的數據持久化實現有兩種方式:內存快照 RDB 和 AOF 日志,分別實現在了 rdb.h/rdb.c 和 aof.c 中。
注意,在使用 RDB 或 AOF 對數據庫進行恢復時,RDB 和 AOF 文件可能會因為 Redis 實例所在服務器宕機,而未能完整保存,進而會影響到數據庫恢復。因此針對這一問題,Redis 還實現了對這兩類文件的檢查功能,對應的代碼文件分別是 redis-check-rdb.c 和 redis-check-aof.c。
主從復制實現
Redis 把主從復制功能實現在了 replication.c 文件中,另外你還需要知道的是,Redis 的主從集群在進行恢復時,主要是依賴於哨兵機制,而這部分功能則直接實現在了 sentinel.c 文件中。其次,與 Redis 實現高可靠性保證的功能類似,Redis 高可擴展性保證的功能,是通過 Redis Cluster 來實現的,這部分代碼也非常集中,就是在 cluster.h/cluster.c 代碼文件中。所以這樣,我們在學習 Redis Cluster 的設計與實現時,就會非常方便,不用在不同的文件之間來回跳轉了。
輔助功能
Redis 還實現了一些用於支持系統運維的輔助功能,比如為了便於運維人員查看分析不同操作的延遲產生來源,Redis 在 latency.h/latency.c 中實現了操作延遲監控的功能;為了便於運維人員查找運行過慢的操作命令,Redis 在 slowlog.h/slowlog.c 中實現了慢命令的記錄功能等等。
此外運維人員有時還需要了解 Redis 的性能表現,為了支持這一目標,Redis 實現了對系統進行性能評測的功能,這部分代碼在 redis-benchmark.c 中。如果你想要了解如何對 Redis 開展性能測試,這個代碼文件也值得一讀。
小結
以上就是 Redis 源代碼的架構,理解代碼結構,可以為我們提供 Redis 功能模塊的全景圖,並方便我們快速查找和定位某個具體功能模塊的實現源碼,這樣也有助於提升代碼閱讀的效率。當然,通過學習 Redis 的目錄結構,我們也學到了一個重要的編程規范:在開發系統軟件時,使用不同的目錄對代碼進行划分。
常見的目錄包括保存第三方庫的 deps 目錄、保存測試用例的 tests 目錄,以及輔助功能和工具的常用目錄 utils 目錄,還有最重要的功能模塊的 src 目錄。按照這個規范來組織你的代碼,就可以提升代碼的可讀性和可維護性。
當然我們上面在介紹源碼文件的時候,沒有覆蓋到 Redis 的全部功能,比如發布訂閱(pubsub.c),Redis 在 4.0 的時候提供的后台異步執行任務,如異步刪除數據(bio.c)等等。可以自己實際查看一下 Redis 源碼目錄,看看每個文件都是做什么的。
