向優秀代碼學習: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命令的定義:
- {"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。
在命令表的底部加上我們的命令:
- {"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的結構體,作為命令的參數同時也用來向實際的客戶端發送響應。
- void randCommand(redisClient *c);
這個原型應該放在src/redis.h中與其他所有命令的原型一起。搜索下面的一行:
- /* Commands prototypes */
這將幫你找到正確的位置。
我們在src/redis.c中加一個空實現:
- void randCommand(redisClient *c) {
- }
我將它加在了infoCommand定義的旁邊。現在,我們執行make命令。
- make
然后,啟動我們剛剛編譯成共的Redis服務(如果你已經有一個Redis服務在本地運行,你應該停掉它):
- > src/redis-server
接著我們在另外的終端中運行Redis客戶端,并試著運行我們的命令:
- >redis-cli
首先,我們試一試我們的異常處理:
- redis 127.0.0.1:6379> rand
- (error) ERR wrong number of arguments for 'rand' command
很好,參數數量限制檢查是正常的。這一次我們指定一個參數:
- redis 127.0.0.1:6379> rand 1
Redis卡住了。這正是我預期的,因為我在randCommand函數中沒有任何響應。將服務停掉,我們接著回去看代碼。
- void randCommand(redisClient *c) {
- addReplyLongLong(c,3);
- }
然后,我們在make一次,并測試命令:
- 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限制的隨機數:
- 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函數。
getLongFromObjectOrReply函數接收一個redisClient實例作參數,并檢查它的第二個參數是否是一個長整型, 如果是則將第二個參數的指針賦給第三個參數(這個參數是一個指針類型),并且返回REDIS_OK。如果第二個參數不是長整型(或溢出了),函數返回 REDIS_ERR。這個方法的美麗之處在于:如果我們從我們的randCommand函數得到的返回值是REDIS_ERR,所有必須的錯誤響應已經被 發送給客戶端了。我們再試一下我們的命令:
- 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或者其他的開源的代碼庫。
原文鏈接:http://www.heychinaski.com/blog/2013/10/14/a-look-at-the-redis-source-code