Redis是一個進行中中的開源索引值資料庫專案,作者是antirez。redis與一般索引值資料庫相比,其最大特色是索引值對中的值支援鏈表、集合、排序集等複合結構。這裡我們來看看antirez是怎樣實現通用鏈表結構的。
如果你熟悉Linux,應該知道Linux核心也實現了通用鏈表結構,它使用了GCC中超越了ANSI C規範的特性。但是Redis沒有這樣做,而是使用了類似C++ STL的做法。Redis實現的是雙向非迴圈鏈表,牽涉到三種資料結構:結點listNode、迭代器listIter、鏈表本身list。代碼如下。
typedef struct listNode {
void *value;
struct listNode *prev;
struct listNode *next;
}listNode;
value指向具體的資料,從後面兩個指標可以看出這是個雙向鏈表,至於是否迴圈要從其他代碼判斷。
typedef struct listIter {
struct listNode *next;
int direction;
}listIter;
這是個迭代器,當然需要一個指標來指向結點咯,就是next;direction表示該迭代器的前進方向,AL_START_HEAD表示從頭開始,AL_START_TAIL表示從尾開始。
好,鏈表結構是時候出來了
adlist.h
typedef struct list {
listNode *head;
listNode *tail;
void* (*dup)(void *ptr);
void (*free)(void *ptr);
int (*match)(void *ptr, void*key);
unsigned int len;
}list;
head、tail和len很好理解,中間三個變數是幹嘛的呢,它們是函數變數,dup是複製函數,你可以自己定義複製策略,free是釋放策略,match是如何在鏈表中查詢內容的策略。
與鏈表相關的資料結構就是這三個,下面來看看此鏈表的API是如何?的,API詳情可查看adlist.h檔案。
list* listCreate(void); 毫無疑問這是鏈表的建構函式
adlist.c
list* listCreate(void)
{
struct list *list;
if ((list = zmalloc(sizeof(*list))) == NULL)
return NULL;
list->head = list->tail = NULL;
list->len = 0;
list->dup = NULL;
list->free = NULL;
list->match = NULL;
return list;
}
沒什麼好說的,從堆中動態分配一個list結構,初始化成員變數,然後返回list的指標,這樣我們就能控制堆中的鏈表了。注意zmalloc是Redis自己封裝的記憶體配置函數,如果感興趣可以查看zmalloc.c檔案
有建構函式,就自然有解構函式了,如下:
void listRelease(list *list);
void listRelease(list *list)
{
unsigned int len;
listNode *current, *next;
current = list->head;
len = list->len;
while (len--) {
next = current->next;
if (list->free) list->free(current->value);
zfree(current);
current = next;
}
zfree(list);
}
zfree一樣是Redis封裝的函數,這裡要注意的是list->free,它是用來釋放結點中的資料的,說明如果你的listNode中的value是指向堆中的記憶體的話,就一定要為鏈表定義釋放函數,否則會造成記憶體流失。
現在來看看怎樣為鏈表增加結點,Redis只提供了在鏈表頭部或尾部添加結點的方法,可能這些對Redis已經夠用了吧。
list* listAddNodeHead(list* list, void *value);
adlist.c
list *listAddNodeHead(list *list, void *value)
{
listNode *node;
if ((node = zmalloc(sizeof(*node))) == NULL)
return NULL;
node->value = value;
if (list->len == 0) {
list->head = list->tail = node;
node->prev = node->next = NULL;
} else {
node->prev = NULL;
node->next = list->head;
list->head->prev = node;
list->head = node;
}
list->len++;
return list;
}
我們只需要為鏈表提供真正的資料value即可,該函數會自己建立listNode結構。這個函數的聲明有點兒意思,它返回的是list指標,通常我們會返回int,用0表示成功,-1表示失敗。而Redis中如果鏈表插入失敗會返回NULL,原來的鏈表不會受影響;如果插入成功,那麼就返回改變後的鏈表。listAddNodeTail類似。
有插入自然就有刪除咯,插入時我們只提供了listNode中value,但刪除就要提供listNode的地址了。與listRelease一樣,如果value指向的記憶體在堆中,就要提供你自己定義的free函數,否則會有記憶體流失。
void listDelNode(list *list, listNode *node);
adlist.c
這個函數為什麼不像插入函數一樣返回list指標呢,原始碼中說這個函數不可能失敗,所以沒必要傳回值。不過我認為返回list指標也好啊,可以保持介面的一致性。不知道antirez是怎麼想的,難道是考慮效能。
Redis中是通過迭代器訪問結點的,同STL一樣,那麼必須要有個為鏈表產生迭代器的函數
listIter* listGetIter(list *list, int direction);
adlist.c
listIter *listGetIterator(list *list, int direction)
{
listIter *iter;
if ((iter = zmalloc(sizeof(*iter))) == NULL) return NULL;
if (direction == AL_START_HEAD)
iter->next = list->head;
else
iter->next = list->tail;
iter->direction = direction;
return iter;
}
迭代器不是鏈表的一部分,而是協助程式員操作鏈結表結點的,所以它需要一個單獨的解構函式
void listReleaseIterator(listIter *iter);
void listReleaseIterator(listIter *iter) {
zfree(iter);
}
現在我們要通過迭代器訪問鏈表結點,這個函數或許是理解鏈表最重要的吧,他會返回迭代器當前所指的結點,並且根據direction前進或後退
listNode* listNext(listIter *iter);adlist.c
對於direction==AL_START_HEAD而言,當迭代器已經指向了最後一個結點時,如果你再調用此函數,他就會返回NULL。所以我們遍曆鏈表的架構如下:
iter = listGetIterator(list);
while ( (node=listNext(iter))!=NULL ) {
doSomethingWith(listNodeValue(node)); //listNodeValue是adlist.h中的宏,從結點中取出值
}
我覺得這個架構有點兒彆扭,與STL好像不太一樣,關於這個介面的設計我會在總結中談到。
總結:
通用鏈表結構的基本API已經講述完畢,想要透徹瞭解最好去看Redis的源碼。但是從listDelNode開始,我嗅到了一絲絲的不和諧,listDelNode需要我們傳一個listNode型的指標,但是在一個好的通用鏈表中使用者的概念中應該只有三種資料結構:鏈表,迭代器,和與具體應用相關的value的類型。在使用者的概念之中value應該就是鏈表中的結點,而沒有必要知道value其實是listNode中的一個指標,所以在使用者介面中listNode根本就不應該出現。基於這樣的理念,我們可以更改兩個介面:
void listDelNode(listNode *node) 應改為 void listDelNode(listIter *iter)
listNode* listNext(listIter *iter)應改為 listIter* listNext(listIter *iter)
這樣遍曆架構就成了
iter = listGetIter(list, direction);
while ( !listIterNull(iter) ) {
doSomethingWith(listIterValue(iter));
listNext(iter);
}
看,這樣遍曆架構是不是順眼多了。我們還要實現架構中的listIterNull和listIterValue,都很簡單
#define listIterNull(iter) (iter->next)
#define listIterValue(iter) (iter->next->value)
OK,告一段落,你有沒有從中領悟到一些通用資料結構設計的思想呢?