Redis【1】- 如何閱讀 Redis 原始碼

秦怀杂货店發表於2024-11-27

1 Redis 的簡介

Redis 實際上是簡稱,全稱為 Remote Dictionary Server (遠端字典伺服器),由 Salvatore Sanfilippo 寫的高效能 key-value 儲存系統,其完全開源免費,遵守 BSD 協議。Redis 與其他 key-value 快取產品(如 memcache)有以下幾個特點。 

  • 資料持久化:可以將記憶體中的資料儲存在磁碟中,重啟的時候可以再次載入進行使用。 
  • 資料結構簡單豐富:既有簡單的 key-value 型別的資料,同時還提供 list,set,zset,hash 等資料結構的儲存。 
  • 高可用:支援主從、哨兵、叢集等模式,可以有效提高可用性。

Redis 也是一種 分散式快取,其程式碼是 c 語言寫的,那我們該如何閱讀呢?

2 環境搭建

環境依賴,先看看 gcc 、cc、g++ 有沒有安裝

whereis gcc
whereis cc
whereis g++

安裝gcc

xcode-select --install  
brew install gcc  
brew install pkg-config

檢視 gcc 的版本:

$ gcc --version
Apple clang version 14.0.0 (clang-1400.0.29.202)
Target: x86_64-apple-darwin22.1.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin

我使用 CLion 2022.3.1 ,這個版本可以支援 Makefile 的專案,我們可以檢查一下環境是不是有問題, 如果有問題,這裡會有錯誤資訊,我的之前報錯是因為 Clion 的版本版本太低了,升級之後就好了。

下載Redis原始碼:

git clone https://github.com/redis/redis.git

切換到指定的版本

git checkout 7.0

File => New CMake Project from Sources, 開啟原始碼專案, 會自動生成根目錄下的 CMakeList.txt 檔案:
Clion 匯入專案的時候選擇已有的 MakeFile 檔案,如果有是否 clean 專案,選擇 clean 即可,之後可以點開 MakeFile 檔案:

如果需要禁止編譯器最佳化,可以使用下面命令:

make CFLAGS="-g -O0" MALLOC=jemalloc

執行完之後, Src 檔案下就會出現可執行檔案:

然後可以看到這些可執行的選項,繼而配置Edit configuration 執行配置:

選擇 debug 進行啟動,啟動成功,然後可以進行除錯了:

可以使用 Redis Desktop Manager 來進行連線:
image.png

或者命令列連線(沒有密碼就可以不需要 -a 12345):

redis-cli -h 127.0.0.1 -p 6379 -a 12345

如果標頭檔案引入報紅色下劃線,那就試試重新載入一下

3 Redis原始碼閱讀技巧

3.1 Redis 的目錄結構

Redis 的目錄:

  • deps: Redis 所依賴的第三方程式碼庫

    • hdr_histogram:用於生成命令的延遲追蹤直方圖
    • hiredis:官方c語言客戶端
    • Jemalloc:記憶體分配器,預設情況下選擇該記憶體分配器來代替 Linux 系統的 libc-malloc,libc-malloc 效能不高,且碎片化嚴重。
    • linenoise:一種讀線替換。它由 Redis 的 同一作者開發,但作為一個單獨的專案進行管理,並根據需要進行更新。
    • lua:lua 指令碼相關的功能。
  • src:原始碼

    • commons:都是 json 檔案,放著每個指令的原資訊。
    • modules:實現 Redis Module 的示例程式碼。
    • 其他檔案均是原始碼
  • test:測試程式碼

    • cluster,Redis Cluster 功能測試。
    • sentinel,哨兵叢集功能測試。
    • unit,單元測試。
    • integration,主從複製功能測試。
  • utils:工具類
  • Makefile:編譯檔案
  • redis.conf : redis 啟動的配置檔案
  • sentinel.conf:哨兵配置

3.2 Redis 原始碼閱讀順序

網上的原始碼閱讀順序(引自網上):

  • 自底向上:從耦合關係最小的模組開始讀,然後逐漸過度到關係緊密的模組。就好像寫程式的測試一樣,先從單元測試開始,然後才到功能測試。
  • 從功能入手:透過檔名(模組名)和函式名,快速定位到一個功能的具體實現,然後追蹤整個實現的運作流程,從而瞭解該功能的實現方式。
  • 自頂向下:從程式的 main() 函式,或者某個特別大的呼叫者函式為入口,以深度優先或者廣度優先的方式閱讀它的原始碼。

從大方向來說,學習 Redis 會有兩種路徑:

  • 先從資料機構入手,直接手撕資料結構

    • 好處:學著踏實,知根知底
    • 壞處:容易從入門到放棄
  • 先從啟動 Redis 開始,跟著啟動順序讀原始碼,跟著具體的操作讀原始碼

    • 好處:比較符合人的認知路線,知道 Redis 啟動做了哪些操作,執行命令時做了哪些操作。
    • 壞處:容易迷路,前期看哪一句,都不知道在幹嘛,畢竟 RDB,AOF,叢集,哨兵這些原始碼,如果實操過才相對容易理解一點。

個人建議是先學習如何啟動 Redis,抓大放小(大致知道哪個類啟動,讀那些配置檔案,大概是做什麼用的),學習 Redis 到底能幹什麼,大致知道 Redis 的一些用法之後,再去了解 Redis 的常用的資料結構,到底怎麼實現的,這個時候對 Redis 的一些資料結構大致有印象,之後可以跟著 Redis 啟動,執行命令去看具體功能執行的路徑。
在 Debug 的過程中,可以加深影響,更加了解資料結構的設計,程式碼的呼叫關係。

4 C語言的知識

4.1 \#define的基本用法

在C語言中,常量是使用頻率很高的一個量。常量是指在程式執行過程中,其值不能被改變的量。常量常使用 #define來定義。
使用#define定義的常量也稱為符號常量,可以提高程式的執行效率,Redis 的原始碼中有比較多的地方都使用該方式。

一般有以下兩種用法:

#define 宏名 宏值
#define 宏名(引數列表) 表示式

第一種就是定義常量,比如:

#define N 100

此後直到 #undef N之前, N的值都是100。當遇到#undef N,其後如果再出現 N,則 N 需要重新定義之後才可以使用。

第二種語法常用來定義符號函式。
例如:

#define AREA(x,y) (x)*(y)

表示用來求長和寬分別是x和y的矩形的面積。
需要注意的是,在表示式(x) * (y)中,x和y都要使用“()”括起來,這是因為符號函式在編譯時時進行符號形式替換。如果不加()則可能會發生意想不到的錯誤,例如:

#define AREA(x,y)  x*y
...
A = AREA( 2+3, 1+2 );

此處預期的結果是15,但是實際的結果卻是7,這是因為該段程式碼在編譯進行了簡單的符號替換而得到的實際表示式是:
A = 2+3 * 1+2;

根據運算子的優先順序,先進行乘法運算,然後才是加法,這就導致了錯誤。
而如果使用

#define AREA(x,y)  (x)*(y)
...
A = AREA( 2+3, 1+2 );

則在編譯時替換的結果是:

A = (2+3) * (1+2);
#include"stdio.h"  
#define AREA(x,y)  (x)*(y)  
int main()  
{  
    int a = AREA(2+3, 1+2);  
   printf( " %d\n", a);  
   return 0;  
}

4.2 標頭檔案

Redis 是使用 c 語言寫的,裡面有很多標頭檔案:

#include "server.h"  
#include "monotonic.h"  
#include "cluster.h"  
#include "slowlog.h"  
#include "bio.h"  
#include "latency.h"  
#include "atomicvar.h"  
#include "mt19937-64.h"  
#include "functions.h"  
#include "syscheck.h"  
  
#include <time.h>

< 開頭的,比如 #include <time.h> 是標準庫的標頭檔案,會在系統指定路徑下查詢,對應到 Java裡面可以理解為 官方的 jdk 裡面的類,而類似 #include "server.h" 則是工程裡面自定義的。

我沒怎麼寫過 c 語言的程式碼, 一般 .c 檔案是寫實現的程式碼邏輯的,那如何在 a 檔案裡面寫一個方法,讓 b 檔案也能用呢?

透過標頭檔案的機制,類似 Java 裡面的 介面, publicprivate 的概念,Java 中 一般希望對外暴露的方法,會設定為 public ,,如果不希望暴露,則設定為private。c 語言裡面如果希望暴露,則可以在標頭檔案裡面定義,否則不用定義。(雖然c語言是程序導向的,但是Redis確實在裡面實踐一些物件導向的思想)。

比如計算兩數之和 與 兩數之差 的乘積 test.c

long long mul(int a,int b) {  
    return a*b;  
}  
  
  
long long calculate(int a,int b) {  
    return mul(a+b,a-b);  
}

暴露出去的標頭檔案test.h

long long calculate(int a,int b);

執行的程式碼 main.c ,可以正常計算結果為 -3:

#include "stdio.h"  
#include "test.h"  
int main(){  
    printf("結果:%lld",calculate(1,2));  
    return 0;  
}

但是如果直接引用 sum() 方法,則會報錯,無法使用:

如果我們多次引用標頭檔案會怎麼樣?結果是正常執行:

4.3 ifndef

Redis 裡面有挺多的地方定義標頭檔案的時候總是來一句 #isdef 或者 ifndef

#ifdef __linux__  
#include <sys/mman.h>  
#endif
#ifndef __ADLIST_H__  
#define __ADLIST_H__
...
#endif /* __ADLIST_H__ */

如果加了 #ifndef ,則會判斷只有沒有定義這個宏的時候,才會定義它,第二次再次遇到 include 的時候,發現這個宏已經被定義過了,就會直接跳過,這樣可以保證多次 include 也不會被解析多次,有且只有一次。

解析多次的壞處是什麼?

  1. 如果在.h 檔案裡面定義了全域性變數,會導致變數重複定義。這個基本不太會,公司編碼規範一般都會禁止,這樣寫是不人道的。
  2. 浪費編譯時間。

既然禁止了在 .h 檔案裡面定義全域性變數,那全域性變數在哪裡定義呢?當然是 .c 檔案,比如 Redis 裡面的全域性變數:

那其他的檔案怎麼使用?這個 sever 可是全域性唯一的,維護了 redis 的全部狀態資料,那當然是暴露出去,在哪裡暴露出去,在 .h 檔案,使用關鍵字 extern

5 小結一下

閱讀原始碼,是一件長期的事情,但是我們每次跟讀程式碼的時候,一定要帶著問題去閱讀,否則效率會下降挺多。前期瞭解資料結構模型的時候,可以在網上找一些簡單易懂的部落格,最好是有圖片的,書籍比較推薦《Redis 設計與實現》。有一定了解之後,會有些疑問,不用擔心,此時再透過讀原始碼去驗證我們的想法,可能不少小夥伴沒學過 c 語言,也不必擔心,語言之間都是相通的,其次即使有關鍵字不會,可以透過搜尋也可以快速瞭解其作用。
希望我們都能從全域性看功能 --> 實踐 --> 抓大放小 --> 帶疑問看原始碼 --> 重構知識圖譜 --> 關聯知識 --> 跳出細節俯瞰全域性,最終完成 Redis 相關的知識學習,並形成一套自己的方法論。

作者:秦懷

相關文章