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
- 當一個redis客戶端連接上redis服務器後,會創建一個redisClient,並且該對象的指針被加入到了 redisServer->Clients
- 當一個命令被執行完畢,如果命令執行慢,將慢日誌寫入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
- 節點值複製函數
dup
, - 節點值釋放函數
free
, - 節點值對比函數 節點值對比函數
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指向的結構需要釋放,但是忘了設置,寫完調試階段相對較難發現,容易會出現內存泄漏。