Redis源码解析——双向链表
? ? ? ? 相對于之前介紹的字典和SDS字符串庫,Redis的雙向鏈表庫則是非常標準的、教科書般簡單的庫。但是作為Redis源碼的一部分,我決定還是要講一講的。(轉載請指明出于breaksoftware的csdn博客)
基本結構
? ? ? ? 首先我們看鏈表元素的結構。因為是雙向鏈表,所以其基本元素應該有一個指向前一個節點的指針和一個指向后一個節點的指針,還有一個記錄節點值的空間
typedef struct listNode {struct listNode *prev;struct listNode *next;void *value;
} listNode; 
? ? ? ? 因為雙向鏈表不僅可以從頭開始遍歷,還可以從尾開始遍歷,所以鏈表結構應該至少有頭和尾兩個節點的指針。
? ? ? ? 但是作為一個可以承載各種類型數據的鏈表,還需要鏈表使用者提供一些處理節點中數據的能力。因為這些數據可能是用戶自定義的,所以像復制、刪除、對比等操作都需要用戶來告訴框架。在《Redis源碼解析——字典結構》一文中,我們看到用戶創建字典時需要傳入的dictType結構體,就是一個承載數據的上述處理方法的載體。但是Redis在設計雙向鏈表時則沒有使用一個結構來承載這些方法,而是在鏈表結構中定義了
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; 
? ? ? ? 至于鏈表結構中為什么要存鏈表長度字段len,我覺得從必要性上來說是沒有必要的。有len字段的一個優點是不用每次計算鏈表長度時都要做一次遍歷操作,缺點便是導出需要維護這個變量。
創建和釋放鏈表
? ? ? ? 鏈表創建的過程比較簡單。只是申請了一個list結構體空間,然后各個字段設置為NULL
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;
} 
? ? ? ? 但是比較有意思的是,創建鏈表時沒有設定鏈表類型——沒有設置復制、釋放、對比等方法的指針。作者單獨提供了一些宏來設置,個人覺得這種割裂開的設計不是很好
#define listSetDupMethod(l,m) ((l)->dup = (m))
#define listSetFreeMethod(l,m) ((l)->free = (m))
#define listSetMatchMethod(l,m) ((l)->match = (m)) 
? ? ? ? 釋放鏈表的操作就是從頭部向尾部一個個釋放節點,迭代的方法是通過判斷不斷減小的鏈表長度字段len是否為0來進行。之前說過,len其實沒有必要性,只要判斷節點的next指針為空就知道到結尾了。
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);
} 
新增節點
? ? ? ? 新增節點分為三種:頭部新增、尾部新增和中間新增。頭部和尾部新增都很簡單,只是需要考慮一下新增之前鏈表是不是空的。如果是空的,要設置新增節點的指向前后指針為NULL,還要讓鏈表的頭尾指針都指向新增的節點
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;
}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++;return list;
} 
? ? ? ? 上述代碼還說明一個問題,新建節點的數據指針指向傳入的value內容,而沒有使用復制操作將value所指向的數據復制下來。于是插入鏈表中的數據,要保證在鏈表生存周期之內都要有效。
? ? ? ? 在鏈表中間插入節點時,可以指定插入到哪個節點前還是后。這個場景下則需要考慮作為坐標的節點是否為鏈表的頭結點或者尾節點;如果是,可能還要視新插入的節點的位置更新鏈表的頭尾節點指向。
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;if (list->tail == old_node) {list->tail = node;}} else {node->next = old_node;node->prev = old_node->prev;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;elselist->head = node->next;if (node->next)node->next->prev = node->prev;elselist->tail = node->prev;if (list->free) list->free(node->value);zfree(node);list->len--;
} 
創建和釋放迭代器
? ? ? ? 迭代器是一種輔助遍歷鏈表的結構,它分為向前迭代器和向后迭代器。我們在迭代器結構中可以發現方向類型變量
typedef struct listIter {listNode *next;int direction;
} listIter; 
? ? ? ? 創建一個迭代器,需要指定方向,從而可以讓迭代器的next指針指向鏈表的頭結點或者尾節點
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;elseiter->next = list->tail;iter->direction = direction;return iter;
} 
? ? ? ? 還可以通過下面兩個方法,讓迭代器類型發生轉變,比如可以讓一個向前的迭代器變成一個向后的迭代器。還可以讓這個迭代器指向另外一個鏈表,而非創建它時指向的鏈表。
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;
} 
? ? ? ? 因為通過listGetIterator創建的迭代器是在堆上動態分配的,所以不使用時要釋放
void listReleaseIterator(listIter *iter) {zfree(iter);
} 
迭代器遍歷
? ? ? ? 迭代器的遍歷其實就是簡單的通過節點向前向后指針訪問到下一個節點的過程
listNode *listNext(listIter *iter)
{listNode *current = iter->next;if (current != NULL) {if (iter->direction == AL_START_HEAD)iter->next = current->next;elseiter->next = current->prev;}return current;
} 
鏈表復制
? ? ? ? 鏈表的復制過程就是通過一個從頭向尾訪問的迭代器,將原鏈表中的數據復制到新建的鏈表中。但是這兒有個地方需要注意下,就是復制操作可能分為深拷貝和淺拷貝。如果我們通過listSetDupMethod設置了數據的復制方法,則使用該方法進行數據的復制,然后將復制出來的新數據放到新的鏈表中。如果沒有設置,則只是把老鏈表中元素的value字段賦值過去。
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;if (copy->dup) {value = copy->dup(node->value);if (value == NULL) {listRelease(copy);return NULL;}} elsevalue = node->value;if (listAddNodeTail(copy, value) == NULL) {listRelease(copy);return NULL;}}return copy;
} 
查找元素
? ? ? ? 查找元素同樣是通過迭代器遍歷整個鏈表,然后視用戶是否通過listSetMatchMethod設置對比方法來決定是使用用戶定義的方法去對比,還是直接使用value去對比。如果是使用value直接去對比,則是強對比,即要求對比的數據和鏈表的數據在內存中位置是一樣的。
listNode *listSearchKey(list *list, void *key)
{listIter iter;listNode *node;listRewind(list, &iter);while((node = listNext(&iter)) != NULL) {if (list->match) {if (list->match(node->value, key)) {return node;}} else {if (key == node->value) {return node;}}}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;
} 
結尾節點前移為頭結點
? ? ? ? 這個方法在Redis代碼中使用比較多。它將鏈表最后一個節點移動到鏈表頭部。我想設計這么一個方法是為了讓鏈表內容可以在無狀態記錄的情況下被均勻的輪詢到。
void listRotate(list *list) {listNode *tail = list->tail;if (listLength(list) <= 1) return;/* Detach current tail */list->tail = tail->prev;list->tail->next = NULL;/* Move it as head */list->head->prev = tail;tail->prev = NULL;tail->next = list->head;list->head = tail;
}
                            總結
以上是生活随笔為你收集整理的Redis源码解析——双向链表的全部內容,希望文章能夠幫你解決所遇到的問題。
                            
                        - 上一篇: Redis源码解析——字典遍历
 - 下一篇: Redis源码解析——有序整数集