Redis 原始碼解析之通用雙向連結串列(adlist)
概述
Redis原始碼中廣泛使用 adlist(A generic doubly linked list),作為一種通用的雙向連結串列,用於簡單的資料集合操作。adlist提供了基本的增刪改查能力,並支援使用者自定義深複製、釋放和匹配操作來維護資料集合中的泛化資料 value
。
adlist 的資料結構
- 連結串列節點
listNode
, 作為雙向連結串列,prev
,next
指標分別指向前序和後序節點。void*
指標型別的value
用於存放泛化的資料型別(如果資料型別的 size 小於sizeof(void*)
, 則可直接存放在value
中。 否則value
存放指向該泛化型別的指標)。
// in adlist.h
typedef struct listNode {
struct listNode *prev;
struct listNode *next;
void *value;
} listNode;
- 連結串列迭代器
listIter
, 其中next
指標指向下一次訪問的連結串列節點。direction
標識當前迭代器的方向是AL_START_HEAD(從頭到尾遍歷)
還是AL_START_TAIL(從尾到頭遍歷)
。
// in adlist.h
typedef struct listIter {
listNode *next;
int direction;
} listIter;
/* Directions for iterators */
#define AL_START_HEAD 0
#define AL_START_TAIL 1
- 雙向連結串列結構
list
。 其中,head
和tail
指標分別指向連結串列的首節點和尾節點。len
記錄當前連結串列的長度。函式指標dup
,free
和match
分別代表業務註冊的對泛化型別value
進行深複製,釋放和匹配操作的函式。(如果沒有註冊dup
, 則預設進行淺複製。 如果沒有註冊free
, 則不對value
進行釋放。如果沒有註冊match
則直接比較value
的字面值)
// in adlist.h
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;
adlist 的基本操作
- 建立:
listCreate
初始化相關欄位為零值。可以透過listSetDupMethod,
listSetFreeMethod
,listSetMatchMethod
來註冊該連結串列泛化型別value
的dup
,free
和match
函式。
/* Create a new list. The created list can be freed with
* listRelease(), but private value of every node need to be freed
* by the user before to call listRelease(), or by setting a free method using
* listSetFreeMethod.
*
* On error, NULL is returned. Otherwise the pointer to the new list. */
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))
- 在連結串列首插入新節點:
listAddNodeHead
-
在空連結串列插入新節點: 為
value
建立新節點,並讓list
的head
和tail
都指向新節點。
-
在非空連結串列插入新節點:
(1) 將新節點的next
指向當前首節點(當前首節點將成為第二節點, 將會是新節點的後繼節點)
(2) 將當前節點的prev
指向新節點, 新節點作為新的首節點將成為原首節點的前驅節點。
(3) 將head
從原本指向舊的首節點改為指向新節點, 將新節點作為連結串列首。
(4) 連結串列總計數加一
/* Add a new node to the list, to head, containing the specified 'value'
* pointer as value.
*
* On error, NULL is returned and no operation is performed (i.e. the
* list remains unaltered).
* On success the 'list' pointer you pass to the function is returned. */
list *listAddNodeHead(list *list, void *value)
{
listNode *node;
if ((node = zmalloc(sizeof(*node))) == NULL)
return NULL;
node->value = value;
listLinkNodeHead(list, node);
return list;
}
/*
* Add a node that has already been allocated to the head of list
*/
void listLinkNodeHead(list* list, listNode *node) {
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++;
}
- 在連結串列尾插入新節點:
listAddNodeTail
- 在空連結串列插入新節點: 邏輯與
listAddNodeHead
實現一致。 - 在非空連結串列插入新節點:
(1) 將新節點的prev
指向當前首節點(當前尾節點將成為倒數第二節點, 將會是新節點的前驅節點)
(2) 將當前節點的next
指向新節點, 新節點作為新的尾節點將成為原尾節點的後繼節點。
(3) 將tail
從原本指向舊的尾節點改為指向新節點, 將新節點作為連結串列尾。
(4) 連結串列總計數加一
/* Add a new node to the list, to tail, containing the specified 'value'
* pointer as value.
*
* On error, NULL is returned and no operation is performed (i.e. the
* list remains unaltered).
* On success the 'list' pointer you pass to the function is returned. */
list *listAddNodeTail(list *list, void *value)
{
listNode *node;
if ((node = zmalloc(sizeof(*node))) == NULL)
return NULL;
node->value = value;
listLinkNodeTail(list, node);
return list;
}
/*
* Add a node that has already been allocated to the tail of list
*/
void listLinkNodeTail(list *list, listNode *node) {
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++;
}
- 在連結串列指定位置插入
value
:listInsertNode
。如果after
為非零, 則將新節點作為old_node
後繼節點。否則,新節點作為old_node
前驅節點。下圖以after
為非零作為例子, 描述了這部分的程式碼邏輯。
(1) 將新節點的prev
指向old_node
(新節點插入在old_node
之後);
(2) 將新節點的next
指向old_node
的後繼節點(old_node
的後繼節點將成為新節點的後繼節點);
(3) 將old_node
的next
指向新節點;
(4) 將新節點的後繼節點的prev
指向新節點(old_node
的原後繼節點現在成為了新節點的後繼節點) 。
(5) 連結串列總計數加一
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;
}
- 刪除連結串列指定節點:
listDelNode
。 下圖以刪除中間節點為例,展示了刪除的流程。
(1) 待刪除節點的前驅節點的next
指向待刪除節點的後繼節點;
(2) 待刪除節點的後繼節點的prev
指向待刪除節點的前驅節點;
(3) 待刪除節點的next
和prev
都置為NULL
;
(4) 連結串列總計數減一
(5) 如果有註冊free
函式,則用free
函式釋放待刪除節點的value
。然後釋放待刪除節點。
/* Remove the specified node from the specified list.
* The node is freed. If free callback is provided the value is freed as well.
*
* This function can't fail. */
void listDelNode(list *list, listNode *node)
{
listUnlinkNode(list, node);
if (list->free) list->free(node->value);
zfree(node);
}
/*
* Remove the specified node from the list without freeing it.
*/
void listUnlinkNode(list *list, listNode *node) {
if (node->prev)
node->prev->next = node->next;
else
list->head = node->next;
if (node->next)
node->next->prev = node->prev;
else
list->tail = node->prev;
node->next = NULL;
node->prev = NULL;
list->len--;
}
5.連結串列的 Join 操作: listJoin
在連結串列l
的末尾新增列表o
的所有元素。 下圖以兩個連結串列都不為 NULL
的場景為例。
(1) o
的首部節點的 prev
指向 l
的尾部節點;
(2) l
的尾部節點的 next
指向 o
的首部節點(1,2 步將兩個連結串列連結起來);
(3) l
的 tail
指向 o
的 tail
(o
的 tail
作為新連結串列的尾部);
(4) l
連結串列總計數加一;
(5) (6) 清空 o
連結串列的資訊;
/* Add all the elements of the list 'o' at the end of the
* list 'l'. The list 'other' remains empty but otherwise valid. */
void listJoin(list *l, list *o) {
if (o->len == 0) return;
o->head->prev = l->tail;
if (l->tail)
l->tail->next = o->head;
else
l->head = o->head;
l->tail = o->tail;
l->len += o->len;
/* Setup other as an empty list. */
o->head = o->tail = NULL;
o->len = 0;
}
- 其他函式: 其他函式實現較為簡單,這裡簡單羅列一下,感興趣的可以去看下原始碼。
// 獲取 list 的迭代器
listIter *listGetIterator(list *list, int direction);
// 返回迭代器的下一個元素,並將迭代器移動一位。如果已遍歷完成, 則返回 NULL
listNode *listNext(listIter *iter);
// 釋放迭代器資源
void listReleaseIterator(listIter *iter);
// 複製連結串列
list *listDup(list *orig);
// 在連結串列中查詢與 key 匹配的 value 所在的第一個節點。
// 如果不存在,則返回 NULL。
// 匹配操作由 list->match 函式提供。
// 如果沒有註冊 match 函式, 則直接比較 key 是否與 value 相等。
listNode *listSearchKey(list *list, void *key);
// 返回指定的索引的元素。 如果超過了連結串列範圍, 則返回 NULL。
// 正整數表示從首部開始計算。
// 0 表示第一個元素, 1 表示第二個元素, 以此類推。
// 負整數表示從尾部開始計算。
// -1 表示倒數第一個元素, -2 表示倒數第二個元素,以此類推。
listNode *listIndex(list *list, long index);
// 返回連結串列初始化的正向迭代器
void listRewind(list *list, listIter *li);
// 返回連結串列初始化的反向迭代器
void listRewindTail(list *list, listIter *li);
// 將連結串列尾部節點移到首部
void listRotateTailToHead(list *list);
// 將連結串列首部節點移到尾部
void listRotateHeadToTail(list *list);
// 用 value 初始化節點
void listInitNode(listNode *node, void *value);
adlist 的使用 demo
git@github.com:younglionwell/redis-adlist-example.git
關注公眾號瞭解更多 redis 原始碼細節和其他技術內容。 你的關注是我最大的動力。