Redis源碼速覽
Introduction
人們常說寫代碼進步最快的方式之一是閱讀成熟開源項目的代碼,從中可以學習到許多良好的代碼風格、問題抽象實踐。今天我選擇了下面這個代碼質量被廣泛認可的開源項目源碼進行閱讀:
Redis是一個用c語言實現的key-value store。除了最基礎的基于字符串的鍵值對,redis還支持哈希、列表、集合、有序集合等數據結構,所以redis也常被稱為是一個data structure server。
我使用的redis源碼版本是redis 3.0.2。Redis的編譯、運行出乎意料的簡單。由于它將所有依賴項均以源碼方式加入項目中,在代碼根目錄下一句簡單的make就可以完成所有編譯任務,再來一句make test就可以完成所有測試。從代碼下載到完成測試,整個過程耗時竟然沒有超過5分鐘。
為了避免在茫茫代碼中走神,我決定以實現一條簡單命令的方式逐步閱讀redis的代碼。我打算實現的命令是randget,它接受一個列表名作為參數,并隨機返回列表中的一個元素。這的確是一個實際用處不大的命令,但應該能幫助我們更有目的性的閱讀代碼。
In Action
接下來我們正式開始實現這個隨機返回數組元素的randget命令。 Redis所支持的所有命令均存儲于redis.c文件開頭的redisCommandTable數組中:
struct redisCommand redisCommandTable[] = { {"get",getCommand,2,"rF",0,NULL,1,1,1,0,0}, {"randget",randgetCommand,2,"rF",0, NULL,1,1,1,0,0}, {"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0}, ...
數組中的每個元素是一個redisCommand結構體,結構體中記錄了關于一條命令的詳盡信息,可見于redis.h,以數組中記錄的第一條命令get命令為例,它說的是:
{"get",getCommand,2,"rF",0,NULL,1,1,1,0,0}
+ 命令名稱為get + 用于處理該命令的函數為getCommand + 命令參數個數為2 + 是一條只讀且復雜度為O(1)的命令 + 變量名在參數列表中的下標為1,如 `GET foo`
基于這個,我首先在數組中加入了這個條目:
{"randget",randgetCommand,2,"rF",0, NULL,1,1,1,0,0},
由于這是個和列表相關的命令,我決定把函數randgetCommand和其他列表相關的函數放在一起。首先在redis.h加入一行聲明:
void randgetCommand(redisClient *c);
在t_list.c中加入函數定義:
void randgetCommand(redisClient *c){ }
不妨編譯一下:
> make
能夠編譯通過,那我們來試一試命令,先啟動服務器端:
> src/redis-server
再在另外一個shell啟動客戶端
> src/redis-cli
并敲入:
127.0.0.1:6379> LPUSH mylist foo (integer) 1 127.0.0.1:6379> randget (error) ERR wrong number of arguments for 'randget' command 127.0.0.1:6379> randget mylist [hang up]
可以看到redis能正確識別randget應接收的參數個數。當參數個數正確時,redis識別了我們敲入的命令,但由于我們還沒有填入命令的實現,程序沒有正確進行回應而且程序掛起了。
接下來就是在函數體中填入我們的實現了,命令需要接收一個列表名作為輸入,并隨機返回列表中的一個元素,抽象來說就是:
void randgetCommand(redisClient *c){ // 讀入參數 // 使用該參數是否可取出某個列表? // 若否,返回空 // 讀入列表,得到列表長度 length // 若length為0 // 返回空 // 否則 // 隨機取出[0,length)中某個整數作為下標,取出對應元素 }
首先是獲取命令參數并進行檢查,由于各個關于列表的命令都需要使用類似的操作,我們參考了其他命令中的實現:
robj *o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk); if (o == NULL || checkType(c,o,REDIS_LIST)) return;
代碼首先使用c->argv[1]獲取列表名,并調用lookupKeyReadOrReply獲取這個鍵對應的值,若給定的鍵不存在則向客戶端返回空,且函數返回null。而在第二行將檢查o是否為null及o所存儲的值類型是否為列表,當o為空或類型不為列表時,將向客戶端返回空,并從這一命令中返回。當檢查順利通過時,o保存的就是我們感興趣的列表結構,我們首先獲取列表的長度,若長度為0則返回空,否則隨機得到一個下標并返回對應元素:
if (length == 0){ addReply(c,shared.nullbulk); } else { long index = random() % length; robj *value; ... }
Redis中列表的存儲方式有兩種,分別是Linked List和Zip List,Linked List即為我們熟悉的鏈表,而關于Zip List是一種為了節省內存消耗而特別設計的列表結構,它的詳細介紹可見于這篇文章。我們根據列表的不同存儲方式使用相應接口獲取下標為index的元素:
if (o->encoding == REDIS_ENCODING_ZIPLIST) { unsigned char *p; unsigned char *vstr; unsigned int vlen; long long vlong; p = ziplistIndex(o->ptr,index); if (ziplistGet(p,&vstr,&vlen,&vlong)) { if (vstr) { value = createStringObject((char*)vstr,vlen); } else { value = createStringObjectFromLongLong(vlong); } addReplyBulk(c,value); decrRefCount(value); } else { addReply(c,shared.nullbulk); } } else if (o->encoding == REDIS_ENCODING_LINKEDLIST) { listNode *ln = listIndex(o->ptr,index); if (ln != NULL) { value = listNodeValue(ln); addReplyBulk(c,value); } else { addReply(c,shared.nullbulk); } } else { redisPanic("Unknown list encoding"); }
這段代碼看起來有點復雜,所完成的工作就是以獲取列表中的元素,將所得元素的指針存儲于value中,并通過addReply或addReplyBulk將所得元素返回。至此命令所要完成的工作就完成了,函數的全貌是:
void randgetCommand(redisClient *c){ robj *o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk); if (o == NULL || checkType(c,o,REDIS_LIST)) return; long long length = listTypeLength(o); if (length == 0){ addReply(c,shared.nullbulk); } else { long index = random() % length; robj *value; if (o->encoding == REDIS_ENCODING_ZIPLIST) { unsigned char *p; unsigned char *vstr; unsigned int vlen; long long vlong; p = ziplistIndex(o->ptr,index); if (ziplistGet(p,&vstr,&vlen,&vlong)) { if (vstr) { value = createStringObject((char*)vstr,vlen); } else { value = createStringObjectFromLongLong(vlong); } addReplyBulk(c,value); decrRefCount(value); } else { addReply(c,shared.nullbulk); } } else if (o->encoding == REDIS_ENCODING_LINKEDLIST) { listNode *ln = listIndex(o->ptr,index); if (ln != NULL) { value = listNodeValue(ln); addReplyBulk(c,value); } else { addReply(c,shared.nullbulk); } } else { redisPanic("Unknown list encoding"); } } return; }
接下來我們重新編譯redis,并測試一下:
[client] > src/redis-cli # 初始化列表 127.0.0.1:6379> LPUSH list a (integer) 1 127.0.0.1:6379> LPUSH list b (integer) 2 127.0.0.1:6379> LPUSH list c (integer) 3 127.0.0.1:6379> LPUSH list d (integer) 4 127.0.0.1:6379> LPUSH list e (integer) 5 # 插入完畢,開始隨機獲取 127.0.0.1:6379> randget list "e" 127.0.0.1:6379> randget list "a" 127.0.0.1:6379> randget list "b" 127.0.0.1:6379> randget list "d" 127.0.0.1:6379> randget list "e" 127.0.0.1:6379> randget list "d" # 錯誤處理 127.0.0.1:6379> set foo bar OK 127.0.0.1:6379> randget (error) ERR wrong number of arguments for 'randget' command 127.0.0.1:6379> randget foo (error) WRONGTYPE Operation against a key holding the wrong kind of value 127.0.0.1:6379> randget unknown (nil) 127.0.0.1:6379>
可以看到命令能夠正常工作,并且能夠正確應對各種錯誤參數!
小結
今天我為redis添加了一條簡單的命令,并從中了解到了redis的內部抽象及處理命令的流程。不得不說redis代碼的易讀性及可擴展性做得非常非常好,我只需了解幾個文件就能夠輕松添加一條命令。另外這種 learn by hacking的方式的確要比漫無目的的通讀代碼來得高效,值得繼續。
來自:http://zhengqm.github.io/code/2015/06/20/Learn-by-hacking-redis-source-code/