redis 基礎數據結構 之 鏈表

給新觀衆老爺的開場

大家好,我是弟弟!
最近讀了一遍 黃健宏大佬的 <<Redis 設計與實現>>,對Redis 3.0版本有了一些認識,該書作者有一版添加了註釋的 redis 3.0源碼。

網上說Redis代碼寫得很好,爲了加深印象和學習redis大佬的代碼寫作藝術,瞭解工作中使用的redis 命令背後的源碼邏輯,便有了從redis命令角度學習redis源碼的想法。
(全文提到的redis服務器,都指在 mac os 上啓動的一個默認配置的單機redis服務器)

redis的基礎數據結構之 鏈表

我以爲的鏈表是這樣的👇

typedef struct listNode {
    // 前置節點
    struct listNode *prev;
    // 後置節點
    struct listNode *next;
    // value的類型取決於具體的場景
    int value;
} listNode;

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;
/*
 * 雙端鏈表節點
 */
typedef struct listNode {
    // 前置節點
    struct listNode *prev;
    // 後置節點
    struct listNode *next;
    // 節點的值
    void *value;
} listNode;
/*
 * 雙端鏈表迭代器
 */
typedef struct listIter {
    // 當前迭代到的節點
    listNode *next;
    // 迭代的方向
    int direction;
} listIter;

觀衆老爺:“代碼看着費勁,來,開始你的表演”
弟弟 :“ 😅 ”

Q1: 爲什麼redis鏈表節點的value字段是void *類型

個人理解,這個體現 redis鏈表的高度抽象(對,又是抽象)。
鏈表就做鏈表的事情,把你們一個一個節點鏈接起來,有前/後指針就完事。
至於值,想放啥放啥。當然了,對於鏈表節點裏的值怎麼解讀,跟鏈表沒關係。
誰放的值誰負責解讀 🙃️

這樣一個鏈表裏就能放任意數據類型了,讓我們在源碼裏找一找證據。

redisServer裏的 list *Client與 list *slowlog

  1. 當一個redis客戶端連接上redis服務器後,會創建一個redisClient,並且該對象的指針被加入到了 redisServer->Clients
  2. 當一個命令被執行完畢,如果命令執行慢,將慢日誌寫入redisServer->slowlog裏

redisServer->Clients 與 redisServer->slowlog 都是list *類型,源碼如下👇

struct redisServer
{
	...
    // 一個鏈表,保存了所有客戶端狀態結構
    list *clients; /* List of active clients */
    ...
    // 保存了所有慢查詢日誌的鏈表
    list *slowlog; /* SLOWLOG list of commands */
	...
};

往一個list尾部裏添加一個元素函數是
list *listAddNodeTail(list *list, void *value)
可以看到 value的類型是void *

而在redisServer.Clients鏈表中加入redisClient時,傳入的value類型是redisClient *


/*
 * 創建一個新客戶端
 */
redisClient *createClient(int fd)
{
    // 分配空間
    redisClient *c = zmalloc(sizeof(redisClient));
    ...
    // 如果不是僞客戶端,那麼添加到服務器的客戶端鏈表中
    if (fd != -1)
        listAddNodeTail(server.clients, c);
    ...    
    // 返回客戶端
    return c;
}

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

從redisServer.Clients裏取出來的value,也是當成redisClient *用的

// 返回給定鏈表的表頭節點
#define listFirst(l) ((l)->head)
// 返回給定節點的值
#define listNodeValue(n) ((n)->value)
int clientsCronHandleTimeout(redisClient *c){
	...
}
void clientsCron(void)
{
	   ...
	   while(...){
	   ...
        head = listFirst(server.clients);
        c = listNodeValue(head);
        if (clientsCronHandleTimeout(c))
            continue;
        ...
       }
}

將慢日誌加入 redisServer.slowlog時,傳入的值的類型是slowlogEntry *

void slowlogPushEntryIfNeeded(robj **argv, int argc, long long duration) {
    ...
    // 如果執行時間超過服務器設置的上限,那麼將命令添加到慢查詢日誌
    if (duration >= server.slowlog_log_slower_than)
        // 新日誌添加到鏈表表頭
        listAddNodeHead(server.slowlog,slowlogCreateEntry(argv,argc,duration));
	...
}
slowlogEntry *slowlogCreateEntry(robj **argv, int argc, long long duration){
	...
}

Q2: list結構裏的函數指針是怎麼回事

雖然說list 可以不關心放進去的 value是什麼數據類型,
但是提供了下面三個函數定義來幫助處理各種不同的類型的 value

  1. 節點值複製函數 dup,
  2. 節點值釋放函數 free,
  3. 節點值對比函數 節點值對比函數 match

幫忙複製 list->dup函數

在複製鏈表時,如果dup不爲空,則會調用dup對value進行復制操作
否則僅複製value這個指針

/*
 * 複製整個鏈表。
 * 複製成功返回輸入鏈表的副本,
 * 如果因爲內存不足而造成複製失敗,返回 NULL 。
 * 如果鏈表有設置值複製函數 dup ,那麼對值的複製將使用複製函數進行,
 * 否則,新節點將和舊節點共享同一個指針。
 * 無論複製是成功還是失敗,輸入節點都不會修改。
 */
list *listDup(list *orig)
{
    ...
    // 設置節點值處理函數
    copy->dup = orig->dup;
    copy->free = orig->free;
    copy->match = orig->match;
    ...
    while ((node = listNext(iter)) != NULL)
    {
        void *value;
        // 複製節點值到新節點
        if (copy->dup)
        {
            value = copy->dup(node->value);
            if (value == NULL) {
                ...
                return NULL;
            }
        }
        else
            value = node->value;
        ...
    }
    ...
    return copy;
}

幫忙釋放 list->free函數

在刪除list中一個元素或者釋放整個list時,
如果free被賦值,則會調用free對value進行釋放

/*
 * 釋放整個鏈表,以及鏈表中所有節點
 */
void listRelease(list *list)
{
	...
    while (...)
    {
        next = current->next;
        // 如果有設置值釋放函數,那麼調用它
        if (list->free)
            list->free(current->value);
        // 釋放節點結構
        zfree(current);
        current = next;
    }
    // 釋放鏈表結構
    zfree(list);
}

/*
 * 從鏈表 list 中刪除給定節點 node 
 * 對節點私有值(private value of the node)的釋放工作由調用者進行。
 */
void listDelNode(list *list, listNode *node)
{
    ...
    // 釋放值
    if (list->free)
        list->free(node->value);
    // 釋放節點
    zfree(node);
    // 鏈表數減一
    list->len--;
}

幫忙找人 list->match函數

遍歷list時,如果match字段不爲空
將通過match指向的函數對比value與key是否匹配
否則直接判斷key與value是否相等

listNode *listSearchKey(list *list, void *key)
{
    listIter *iter;
    listNode *node;
    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的list,一個雙向鏈表。
list相關結構與函數定義放在了 adlist.h文件,實現則在adlist.c文件
貼一下 adlist.h

#ifndef __ADLIST_H__
#define __ADLIST_H__
/*
 * 雙端鏈表節點
 */
typedef struct listNode {
    // 前置節點
    struct listNode *prev;
    // 後置節點
    struct listNode *next;
    // 節點的值
    void *value;
} listNode;
/*
 * 雙端鏈表迭代器
 */
typedef struct listIter {
    // 當前迭代到的節點
    listNode *next;
    // 迭代的方向
    int direction;
} listIter;
/*
 * 雙端鏈表結構
 */
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;
/* Functions implemented as macros */
// 返回給定鏈表所包含的節點數量
// T = O(1)
#define listLength(l) ((l)->len)
// 返回給定鏈表的表頭節點
// T = O(1)
#define listFirst(l) ((l)->head)
// 返回給定鏈表的表尾節點
// T = O(1)
#define listLast(l) ((l)->tail)
// 返回給定節點的前置節點
// T = O(1)
#define listPrevNode(n) ((n)->prev)
// 返回給定節點的後置節點
// T = O(1)
#define listNextNode(n) ((n)->next)
// 返回給定節點的值
// T = O(1)
#define listNodeValue(n) ((n)->value)
// 將鏈表 l 的值複製函數設置爲 m
// T = O(1)
#define listSetDupMethod(l,m) ((l)->dup = (m))
// 將鏈表 l 的值釋放函數設置爲 m
// T = O(1)
#define listSetFreeMethod(l,m) ((l)->free = (m))
// 將鏈表的對比函數設置爲 m
// T = O(1)
#define listSetMatchMethod(l,m) ((l)->match = (m))
// 返回給定鏈表的值複製函數
// T = O(1)
#define listGetDupMethod(l) ((l)->dup)
// 返回給定鏈表的值釋放函數
// T = O(1)
#define listGetFree(l) ((l)->free)
// 返回給定鏈表的值對比函數
// T = O(1)
#define listGetMatchMethod(l) ((l)->match)
/* Prototypes */
list *listCreate(void);
void listRelease(list *list);
list *listAddNodeHead(list *list, void *value);
list *listAddNodeTail(list *list, void *value);
list *listInsertNode(list *list, listNode *old_node, void *value, int after);
void listDelNode(list *list, listNode *node);
listIter *listGetIterator(list *list, int direction);
listNode *listNext(listIter *iter);
void listReleaseIterator(listIter *iter);
list *listDup(list *orig);
listNode *listSearchKey(list *list, void *key);
listNode *listIndex(list *list, long index);
void listRewind(list *list, listIter *li);
void listRewindTail(list *list, listIter *li);
void listRotate(list *list);
/* Directions for iterators 
 *
 * 迭代器進行迭代的方向
 */
// 從表頭向表尾進行迭代
#define AL_START_HEAD 0
// 從表尾到表頭進行迭代
#define AL_START_TAIL 1
#endif /* __ADLIST_H__ */

小結

又到了小結時間,跑個題。
複製函數dup,和匹配函數match 如果忘了設置,在調試階段,比較容易發現。
list的free函數,如果value指向的結構需要釋放,但是忘了設置,寫完調試階段相對較難發現,容易會出現內存泄漏。

往期博客回顧

  1. redis服務器的部分啓動過程
  2. GET命令背後的源碼邏輯
  3. redis的基礎數據結構之 sds
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章