Redis原始碼剖析--雙端連結串列Sdlist
阿新 • • 發佈:2019-02-12
> 請持續關注我的個人部落格:https://zcheng.ren
今天來分析Redis的一個基本資料結構–雙端連結串列,其定義和實現主要在sdlist.h和sdlist.c檔案中。其主要用在實現列表鍵、事務模組儲存輸入命令和伺服器模組,訂閱模組儲存多個客戶端等。
sdlist的資料結構
Redis為雙端連結串列的每一個節點定義瞭如下的結構體。
// 連結串列節點定義
typedef struct listNode {
struct listNode *prev; // 指向前一個節點
struct listNode *next; // 指向後一個節點
void *value; // 節點值
} listNode;
與一般的雙端連結串列無異,定義了連結串列節點的結構體之後,下面就定義連結串列的結構體,用來方便管理連結串列節點,其結構體定義如下:
typedef struct list {
listNode *head; // 指向連結串列頭節點
listNode *tail; // 指向連結串列尾節點
void *(*dup)(void *ptr); // 自定義節點值複製函式
void (*free)(void *ptr); // 自定義節點值釋放函式
int (*match)(void *ptr, void *key); // 自定義節點值匹配函式
unsigned long len; // 連結串列長度
} list;
Redis在實現連結串列的時候,定義其為雙端無環連結串列,其示意圖如下:
此外,Redis對其結構體提供了一系列的巨集定義函式,方便操作其結構體引數
#define listLength(l) ((l)->len) // 獲取list長度
#define listFirst(l) ((l)->head) // 獲取list頭節點指標
#define listLast(l) ((l)->tail) // 獲取list尾節點指標
#define listPrevNode(n) ((n)->prev) // 獲取當前節點前一個節點
#define listNextNode(n) ((n)->next) // 獲取當前節點後一個節點
#define listNodeValue(n) ((n)->value) // 獲取當前節點的值
#define listSetDupMethod(l,m) ((l)->dup = (m)) // 設定節點值複製函式
#define listSetFreeMethod(l,m) ((l)->free = (m)) // 設定節點值釋放函式
#define listSetMatchMethod(l,m) ((l)->match = (m)) // 設定節點值匹配函式
#define listGetDupMethod(l) ((l)->dup) // 獲取節點值賦值函式
#define listGetFree(l) ((l)->free) // 獲取節點值釋放函式
#define listGetMatchMethod(l) ((l)->match) // 獲取節點值匹配函式
sdlist迭代器結構
Redis為sdlist定義了一個迭代器結構,其能正序和逆序的訪問list結構。
typedef struct listIter {
listNode *next; // 指向下一個節點
int direction; // 方向引數,正序和逆序
} listIter;
對於direction引數,Redis提供了兩個巨集定義
#define AL_START_HEAD 0 // 從頭到尾
#define AL_START_TAIL 1 // 從尾到頭
sdlist基本操作
sdlist建立
sdlist提供了listCreate函式來建立一個空的連結串列。
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;
}
sdlist釋放
sdlist提供了listRelease函式來釋放整個連結串列
void listRelease(list *list)
{
unsigned long 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); // 釋放連結串列頭
}
插入節點
sdlist提供了三個函式來完成向list中插入一個節點的功能。
向頭部插入節點
// 該函式向list的頭部插入一個節點
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++; // 長度+1
return list;
}
向尾部新增節點
// 該函式可以在list的尾部新增一個節點
list *listAddNodeTail(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 = list->tail;
node->next = NULL;
list->tail->next = node;
list->tail = node;
}
list->len++; // 長度+1
return list;
}
向任意位置插入節點
// 向任意位置插入節點
// 其中,old_node為插入位置
// value為插入節點的值
// after為0時表示插在old_node前面,為1時表示插在old_node後面
list *listInsertNode(list *list, listNode *old_node, void *value, int after) {
listNode *node;
if ((node = zmalloc(sizeof(*node))) == NULL)
return NULL;
node->value = value;
if (after) { // 向後插入
node->prev = old_node;
node->next = old_node->next;
// 如果old_node為尾節點的話需要改變tail
if (list->tail == old_node) {
list->tail = node;
}
} else { // 向前插入
node->next = old_node;
node->prev = old_node->prev;
// 如果old_node為頭節點的話需要改變head
if (list->head == old_node) {
list->head = node;
}
}
if (node->prev != NULL) {
node->prev->next = node;
}
if (node->next != NULL) {
node->next->prev = node;
}
list->len++;
return list;
}
刪除節點
void listDelNode(list *list, listNode *node)
{
if (node->prev) // 刪除節點不為頭節點
node->prev->next = node->next;
else // 刪除節點為頭節點需要改變head的指向
list->head = node->next;
if (node->next) // 刪除節點不為尾節點
node->next->prev = node->prev;
else // 刪除節點為尾節點需要改變tail的指向
list->tail = node->prev;
if (list->free) list->free(node->value); // 釋放節點值
zfree(node); // 釋放節點
list->len--;
}
迭代器相關操作
sdlist為其迭代器提供了一些操作,用來完成獲取迭代器,釋放迭代器,重置迭代器,獲取下一個迭代器等操作,具體原始碼見如下分析。
獲取迭代器
listIter *listGetIterator(list *list, int direction)
{
listIter *iter; // 宣告迭代器
if ((iter = zmalloc(sizeof(*iter))) == NULL) return NULL;
// 根據迭代方向來初始化iter
if (direction == AL_START_HEAD)
iter->next = list->head;
else
iter->next = list->tail;
iter->direction = direction;
return iter;
}
釋放迭代器
void listReleaseIterator(listIter *iter) {
zfree(iter); // 直接呼叫zfree來釋放
}
重置迭代器
重置迭代器分為兩種,一種是重置正向迭代器,一種是重置為逆向迭代器
// 重置為正向迭代器
void listRewind(list *list, listIter *li) {
li->next = list->head;
li->direction = AL_START_HEAD;
}
// 重置為逆向迭代器
void listRewindTail(list *list, listIter *li) {
li->next = list->tail;
li->direction = AL_START_TAIL;
}
獲取下一個迭代器
// 根據direction屬性來獲取下一個迭代器
listNode *listNext(listIter *iter)
{
listNode *current = iter->next;
if (current != NULL) {
if (iter->direction == AL_START_HEAD)
iter->next = current->next;
else
iter->next = current->prev;
}
return current;
}
連結串列複製函式
sdlist提供了listDup函式,用於複製整個連結串列。
list *listDup(list *orig)
{
list *copy;
listIter iter;
listNode *node;
if ((copy = listCreate()) == NULL)
return NULL;
// 複製節點值操作函式
copy->dup = orig->dup;
copy->free = orig->free;
copy->match = orig->match;
// 重置迭代器
listRewind(orig, &iter);
while((node = listNext(&iter)) != NULL) {
void *value;
// 複製節點
// 如果定義了dup函式,則按照dup函式來複制節點值
if (copy->dup) {
value = copy->dup(node->value);
if (value == NULL) {
listRelease(copy);
return NULL;
}
} else // 如果沒有則直接賦值
value = node->value;
// 依次向尾部新增節點
if (listAddNodeTail(copy, value) == NULL) {
listRelease(copy);
return NULL;
}
}
return copy;
}
查詢函式
sdlist提供了兩種查詢函式。其一是根據給定節點值,在連結串列中查詢該節點
listNode *listSearchKey(list *list, void *key)
{
listIter iter;
listNode *node;
listRewind(list, &iter);
while((node = listNext(&iter)) != NULL) {
if (list->match) { // 如果定義了match匹配函式,則利用該函式進行節點匹配
if (list->match(node->value, key)) {
return node;
}
} else { // 如果沒有定義match,則直接比較節點值
if (key == node->value) { // 找到該節點
return node;
}
}
}
// 沒有找到就返回NULL
return NULL;
}
其二是根據序號來查詢節點
listNode *listIndex(list *list, long index) {
listNode *n;
if (index < 0) { // 序號為負,則倒序查詢
index = (-index)-1;
n = list->tail;
while(index-- && n) n = n->prev;
} else { // 正序查詢
n = list->head;
while(index-- && n) n = n->next;
}
return n;
}
連結串列旋轉函式
旋轉操作其實就是講表尾節點移除,然後插入到表頭,成為新的表頭
void listRotate(list *list) {
listNode *tail = list->tail;
if (listLength(list) <= 1) return;
// 取出表尾指標
list->tail = tail->prev;
list->tail->next = NULL;
// 將其移動到表頭併成為新的表頭指標
list->head->prev = tail;
tail->prev = NULL;
tail->next = list->head;
list->head = tail;
}
sdlist小結
分析完sdlist的原始碼,著實是把雙向連結串列的基本操作都複習了一遍,Redis的作者還真是喜歡造輪子,不愧是輪子界的鼻祖啊!雖然這些基本操作很簡單,但是可以學到一些優秀的設計,例如:sdlist迭代器的設計等,這些都對理解Redis的相關操作有著很大的幫助作用。