1. 程式人生 > 其它 >Redis使用及原始碼剖析-3.Redis連結串列-2021-1-17

Redis使用及原始碼剖析-3.Redis連結串列-2021-1-17

技術標籤:redisredis資料庫

文章目錄


前言

本文對Redis的底層資料結構連結串列做了簡要介紹,涉及的檔案是adlist.h和adlist.c。


一、連結串列簡介

連結串列是一種非常常用的資料結構,在很多高階語言中都有實現。Redis 使用的 C 語言並沒有內建這種資料結構, 所以 Redis 構建了自己的連結串列實現。
連結串列在 Redis 中的應用非常廣泛, 比如列表鍵的底層實現之一就是連結串列: 當一個列表鍵包含了數量比較多的元素, 又或者列表中包含的元素都是比較長的字串時, Redis 就會使用連結串列作為列表鍵的底層實現。

除了連結串列鍵之外, 釋出與訂閱、慢查詢、監視器等功能也用到了連結串列, Redis 伺服器本身還使用連結串列來儲存多個客戶端的狀態資訊, 以及使用連結串列來構建客戶端輸出緩衝區(output buffer),後續的部分文章將陸續對這些連結串列應用進行介紹。

二、連結串列實現

1.連結串列節點實現

在adlist.h中定義了listNode結構代表連結串列節點,如下所示:

/*
 * 雙端連結串列節點
 */
typedef struct listNode {

    // 前置節點
    struct listNode *prev;

    // 後置節點
    struct listNode *
next; // 節點的值 void *value; } listNode;

可以看出連結串列節點是雙向的,並且通過void *指標可以存各種型別的值,通過節點的prev和next指標我們就可以連接出一個雙向連結串列如下圖所示:
在這裡插入圖片描述

2.連結串列實現

雖然可以直接用節點來構造雙向連結串列,但是實際連結串列實現時還會再定義一個連結串列結構。如下所示:

/*
 * 雙端連結串列結構
 */
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;

可以看出通過定義list結構體,就可以常數時間內獲得連結串列頭尾節點以及連結串列長度,這些都是隻用連結串列節點來表示連結串列時不具備的。
此外list中還定義了節點值的複製、釋放和對比函式,dup 函式用於複製連結串列節點所儲存的值;free 函式用於釋放連結串列節點所儲存的值;match 函式則用於對比連結串列節點所儲存的值和另一個輸入值是否相等。通過這些函式的定義使得連結串列的特性更加的複雜多樣。

3.連結串列迭代器實現

Redis為了便於訪問連結串列元素,還定義了連結串列的迭代器程式碼如下:

/* Directions for iterators 
 *
 * 迭代器進行迭代的方向
 */
// 從表頭向表尾進行迭代
#define AL_START_HEAD 0
// 從表尾到表頭進行迭代
#define AL_START_TAIL 1
/*
 * 雙端連結串列迭代器
 */
typedef struct listIter {

    // 當前迭代到的節點
    listNode *next;

    // 迭代的方向
    int direction;

} listIter;

可以看出list的迭代器可以支援兩個方向的遍歷。

4.連結串列API

選取了部分連結串列API進行剖析。建立一個空連結串列的程式碼如下所示:

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;
}

在指定節點之前或者之後插入節點的程式碼如下所示:

/*
 * 建立一個包含值 value 的新節點,並將它插入到 old_node 的之前或之後
 *
 * 如果 after 為 0 ,將新節點插入到 old_node 之前。
 * 如果 after 為 1 ,將新節點插入到 old_node 之後。
 *
 * T = O(1)
 */
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;
}

建立和釋放一個迭代器的程式碼如下所示:

/*
 * 為給定連結串列建立一個迭代器,
 * 之後每次對這個迭代器呼叫 listNext 都返回被迭代到的連結串列節點
 *
 * direction 引數決定了迭代器的迭代方向:
 *  AL_START_HEAD :從表頭向表尾迭代
 *  AL_START_TAIL :從表尾想表頭迭代
 *
 * T = O(1)
 */
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;
}

/* Release the iterator memory */
/*
 * 釋放迭代器
 *
 * T = O(1)
 */
void listReleaseIterator(listIter *iter) {
    zfree(iter);
}

連結串列中查詢指定元素函式如下所示:

/* 
 * 查詢連結串列 list 中值和 key 匹配的節點。
 * 
 * 對比操作由連結串列的 match 函式負責進行,
 * 如果沒有設定 match 函式,
 * 那麼直接通過對比值的指標來決定是否匹配。
 *
 * 如果匹配成功,那麼第一個匹配的節點會被返回。
 * 如果沒有匹配任何節點,那麼返回 NULL 。
 *
 * T = O(N)
 */
listNode *listSearchKey(list *list, void *key)
{
    listIter *iter;
    listNode *node;

    // 迭代整個連結串列
    iter = listGetIterator(list, AL_START_HEAD);
    while((node = listNext(iter)) != NULL) {
        
        // 對比
        if (list->match) {
            if (list->match(node->value, key)) {
                listReleaseIterator(iter);
                // 找到
                return node;
            }
        } else {
            if (key == node->value) {
                listReleaseIterator(iter);
                // 找到
                return node;
            }
        }
    }
    
    listReleaseIterator(iter);

    // 未找到
    return NULL;
}

總結

本文對Redis的連結串列結構進行了簡單介紹,如有不當之處,請指正。