這七年以來,主要是在寫Java和Scala程式碼,我的C語言技能都退化了。事實上,它可能已經完全沒有了。除了偶爾會用來hack,大學畢業以後我基本上都沒有用到C了。大家都說,閱讀他人的程式碼是非常好的學習方法,特別是程式碼庫的作者是專家或者它質量有很高的評價時。因此,我準備閱讀一個這樣的程式碼庫:Redis。
(相關閱讀:《閱讀優秀程式碼是提高開發人員修為的一種捷徑》)
Redis是一個用ANSI C 編寫的開源資料結構伺服器。“資料結構伺服器”只是對靈巧的key-value儲存服務的另外一種稱謂。你不僅僅可以儲存簡單的字串,還可以儲存包括hash(或者map,甚至dicts),list,set,sorted set。我們在Top10 中大量應用了Redis,大部分為了根據使用者搜尋的日期和酒店的空房情況和價格建立索引。我發現Redis的程式碼非常容易讀懂,甚至是對於像我這樣的新手。程式碼寫的很整潔,並且程式碼量相對較小(4.5萬行左右),大部分都是單執行緒的,依賴也很少。所有的依賴都跟原始碼放在一起了,這中做法讓編譯它變得非常簡單:clone它的庫,然後輸入make即可。
我決定通過為它增加一條命令來深入程式碼。而這簡單的事情可以讓我知道Redis怎麼處理一條命令並排程響應它。命令rand,接收一個整型值作為max,並隨機返回0到max(不包含max)之間的一個整數。這不是使用鍵值儲存的思路,但是實現它將會很有啟發性。而我也肯定不會提交一個pull request。
免責宣告:如我之前所說,我絕對不是一個C語言的專家,因此這裡所有的程式碼和其解釋都符合這個條款。而且,我連結了Redis的一個不穩定分支,所以它是不穩定的。如果你自己去獲取Redis原始碼,用你喜歡的編輯器來檢視時,你將發現更多本文的不同,特別是如果你編譯並執行時會發現不同。
命令表在src/redis.c檔案的靠頂部的位置。它是一個陣列,陣列的元素型別是redisCommand
結構體。redisCommand是在src/redis.h中定義的。在
redisCommandTable的上方有一塊比較詳細的註釋,對它的每一個field做了解釋。下面是get命令的定義:
1 |
{"get",getCommand,2,"r",0,NULL,1,1,1,0,0}, |
第一個field是命令的名字“get”。第二個field是一個函式指標,指向這個命令的具體實現(你可以檢視實現細節t_string.c)。
第三個field是命令的引數數量限制(命令接收的引數個數)。指定這個,意味著在呼叫函式指標之前,查詢和執行命令的程式碼可以做一個預先驗證。這種做法減少了在每個命令函式必須的錯誤處理程式碼。引數的個數算上了命令名字本身,所以它只接受兩個引數:它自己的名字,key的名字(我們要獲取它的值)。
第四個field,被設為”r”,用來指明這個命令是隻讀的,不能修改這個key的value或狀態。有一大堆的字母標誌,你都可以用在這個位置。而且在附近的註釋塊中,每個字母標誌都有詳細的解釋。緊跟這個field的field總是被設定為0,後面會用來計算。它只是第四個field的字串包含資訊的位掩碼。
第六個field是NULL,因為它只有在你要用複雜的邏輯去告訴Redis哪個引數才是真正的key的時候才需要。一個key指向一個儲存在Redis中的值的引用,對應簡單的引數,例如我們的max引數。這種機制,允許Redis在呼叫命令的實現之前,提取key的值(並且校驗key是否存在)。如果這個field被設定了值,那麼它將會是一個函式指標,指向的函式會返回一個引數索引的整型陣列(db.c中的zunionInterGetKeys是一個示例)。在get命令(其他大部分命令)的場景下,這個陣列的資訊傳達的資訊跟後面三個field的一樣。get命令只有一個引數,而它就是key。因此,第一個引數(key)在位置1上,最後一個引數(也是key)在位置1上,從第一個引數到最後一個引數的增量也是1(譯者注:原始碼註釋是:intkeystep;/* The step between first and last key */)。
redisCommand
的最後兩個field是命令的度量項,由Redis來設定,並且總是初始化為0。
在命令表的底部加上我們的命令:
1 |
{"rand",randCommand,2,"rRl",0,NULL,0,0,0,0,0} |
命令的名字是“rand”,randCommand指向實現的指標(還未實現),它接收2個引數(命令名字和max)。至於標誌,它是隻讀的(r),返回隨機的,不確定的輸出(R),而且它可以在Redis還在載入資料的時候使用(l)。它沒有關鍵引數。
下一步是在src/redis.h中增加randCommand的函式原型。Redis命令的函式接收一個引數,一個redisClient的結構體,作為命令的引數同時也用來向實際的客戶端傳送響應。
1 |
void randCommand(redisClient *c); |
這個原型應該放在src/redis.h中與其他所有命令的原型一起。搜尋下面的一行:
1 |
/* Commands prototypes */ |
這將幫你找到正確的位置。
我們在src/redis.c中加一個空實現:
1 2 3 |
void randCommand(redisClient *c) { } |
我將它加在了infoCommand定義的旁邊。現在,我們執行make命令。
1 |
make |
然後,啟動我們剛剛編譯成共的Redis服務(如果你已經有一個Redis服務在本地執行,你應該停掉它):
1 |
> src/redis-server |
接著我們在另外的終端中執行Redis客戶端,並試著執行我們的命令:
1 |
>redis-cli |
首先,我們試一試我們的異常處理:
1 2 |
redis 127.0.0.1:6379> rand (error) ERR wrong number of arguments for 'rand' command |
很好,引數數量限制檢查是正常的。這一次我們指定一個引數:
1 |
redis 127.0.0.1:6379> rand 1 |
Redis卡住了。這正是我預期的,因為我在randCommand函式中沒有任何響應。將服務停掉,我們接著回去看程式碼。
1 2 3 |
void randCommand(redisClient *c) { addReplyLongLong(c,3); } |
然後,我們在make一次,並測試命令:
1 2 3 4 5 6 7 8 |
redis 127.0.0.1:6379> rand 1 (integer) 3 redis 127.0.0.1:6379> rand 2 (integer) 3 redis 127.0.0.1:6379> rand 3 (integer) 3 |
好吧,結果不是太隨機,但這只是個開始。我們從命令裡獲取引數max,並返回一個由max限制的隨機數:
1 2 3 4 5 6 7 8 |
void randCommand(redisClient *c) { long max; if (getLongFromObjectOrReply(c,c->argv[1],&max,NULL) != REDIS_OK) return; addReplyLongLong(c,random() % max); } |
儘管Redis在整個程式碼庫中都用原始型別和C型字串,但它同時也擁有自己的以更通用的方式存在的內部物件系統,用來表示字串,長整型和更復雜的型別。一個利用這種型別的例子就是:每個命令的引數。每一個命令的引數都作為一個Redis物件被存在redisClient例項c的field,陣列argv裡。(譯註:在原始碼src/redis.c裡面redisClient是一個結構體,argv是一個redisObject指標的指標)。在src/t_string.c裡面有一個從Redis物件獲取長整型的例子:getrangeCommand,它呼叫了src/object.c中的getLongFromObjectOrReply函式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
redis 127.0.0.1:6379> rand 10 (integer) 9 redis 127.0.0.1:6379> rand notanumber (error) ERR value is not an integer or out of range redis 127.0.0.1:6379> rand 10 (integer) 3 redis 127.0.0.1:6379> rand 10 (integer) 1 redis 127.0.0.1:6379> rand 100 (integer) 43 redis 127.0.0.1:6379> rand 100 (integer) 55 redis 127.0.0.1:6379> rand 100 (integer) 86 |
看起來不錯!rand看起來是一個沒有多少意義的命令,但是從實現它的過程中學到很多關於Redis的東西,我希望你跟著做下來也同樣學到很多。請在評論裡告訴我這篇文章裡是否明顯的錯誤。我也很高興知道這篇文章對你很有用或者你很喜歡它。我考慮寫一些類似的東西,關於Redis或者其他的開源的程式碼庫。
注: forenroll 首發於他的個人部落格:http://forenroll.iteye.com/blog/1967696