Redis 原始碼解析之通用雙向連結串列(adlist)

楊領well發表於2023-04-08

Redis 原始碼解析之通用雙向連結串列(adlist)

概述

Redis原始碼中廣泛使用 adlist(A generic doubly linked list),作為一種通用的雙向連結串列,用於簡單的資料集合操作。adlist提供了基本的增刪改查能力,並支援使用者自定義深複製、釋放和匹配操作來維護資料集合中的泛化資料 value

adlist 的資料結構

  1. 連結串列節點 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;
  1. 連結串列迭代器 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
  1. 雙向連結串列結構 list。 其中, headtail 指標分別指向連結串列的首節點和尾節點。len 記錄當前連結串列的長度。函式指標 dup, freematch 分別代表業務註冊的對泛化型別 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 的基本操作

  1. 建立: listCreate 初始化相關欄位為零值。可以透過 listSetDupMethod, listSetFreeMethod, listSetMatchMethod來註冊該連結串列泛化型別 valuedup, freematch 函式。
/* 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))

image

  1. 在連結串列首插入新節點: listAddNodeHead
  • 在空連結串列插入新節點: 為 value 建立新節點,並讓 listheadtail 都指向新節點。
    image

  • 在非空連結串列插入新節點:
    (1) 將新節點的 next 指向當前首節點(當前首節點將成為第二節點, 將會是新節點的後繼節點)
    (2) 將當前節點的 prev 指向新節點, 新節點作為新的首節點將成為原首節點的前驅節點。
    (3) 將 head 從原本指向舊的首節點改為指向新節點, 將新節點作為連結串列首。
    (4) 連結串列總計數加一
    image

/* 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++;
}
  1. 在連結串列尾插入新節點: listAddNodeTail
  • 在空連結串列插入新節點: 邏輯與 listAddNodeHead 實現一致。
  • 在非空連結串列插入新節點:
    (1) 將新節點的 prev 指向當前首節點(當前尾節點將成為倒數第二節點, 將會是新節點的前驅節點)
    (2) 將當前節點的 next 指向新節點, 新節點作為新的尾節點將成為原尾節點的後繼節點。
    (3) 將 tail 從原本指向舊的尾節點改為指向新節點, 將新節點作為連結串列尾。
    (4) 連結串列總計數加一

image

/* 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++;
}
  1. 在連結串列指定位置插入 value: listInsertNode。如果 after 為非零, 則將新節點作為 old_node 後繼節點。否則,新節點作為 old_node 前驅節點。下圖以 after 為非零作為例子, 描述了這部分的程式碼邏輯。
    (1) 將新節點的 prev 指向 old_node(新節點插入在 old_node 之後);
    (2) 將新節點的 next 指向 old_node 的後繼節點(old_node 的後繼節點將成為新節點的後繼節點);
    (3) 將 old_nodenext 指向新節點;
    (4) 將新節點的後繼節點的 prev指向新節點(old_node的原後繼節點現在成為了新節點的後繼節點) 。
    (5) 連結串列總計數加一

image

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;
}
  1. 刪除連結串列指定節點: listDelNode。 下圖以刪除中間節點為例,展示了刪除的流程。
    (1) 待刪除節點的前驅節點的 next 指向待刪除節點的後繼節點;
    (2) 待刪除節點的後繼節點的 prev 指向待刪除節點的前驅節點;
    (3) 待刪除節點的 nextprev 都置為 NULL;
    (4) 連結串列總計數減一
    (5) 如果有註冊 free 函式,則用 free 函式釋放待刪除節點的 value。然後釋放待刪除節點。

image


/* 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) ltail 指向 otail(otail作為新連結串列的尾部);
(4) l 連結串列總計數加一;
(5) (6) 清空 o 連結串列的資訊;

image

/* 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;
}
  1. 其他函式: 其他函式實現較為簡單,這裡簡單羅列一下,感興趣的可以去看下原始碼。
// 獲取 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 原始碼細節和其他技術內容。 你的關注是我最大的動力。

image

相關文章