ADLIST 应用
链表在 Redis 里面的应用面很广,除了我们常用的 LPUSH
、RPUSH
和 RPOPLPUSH
等更多的命令,Redis 很多底层功能都用了链表,发布与订阅就是其中一员。所以我们今天来看看 Redis 的链表是如何实现的,尽管链表是一种很基础的数据结构。
ADLIST 相关设计
链表是由一个或者多个节点串联起来的数据结构,所以需要定义一个节点的结构体,对应 Redis 中的 struct listNode
。 成员包括了指向上一个节点的指针 listNode *prev
、指向下一个节点的指针 listNode *next
和保留节点保留的值的指针 void *value
。
不难发现 struct listNode
是一个双链表结构,通过单个节点我们不仅能够访问到下一个节点,还能访问到上一个节点。使用双链表而非单链表的目的是想增加结构的灵活性,因为只有经常对数据进行增加或者删除才会用到链表这种数据结构。无论是增加节点抑或删除节点,双链表都要比单链表更加简单高效。但提高灵活性的代价是需要更多的空间,但这个代价小到可以被我们忽略。
typedef struct listNode {
struct listNode *prev;
struct listNode *next;
void *value;
} listNode;
有了节点结构,接下来就可以组装一个链表结构 struct list
。当我第一看到这个结构体的时候,我觉得太有意思。因为我是一个 C++ 程序员,我对 C 语言的认识并没有我对 C++ 这门语言一样的深入。当我看到 Redis 把函数指针放在结构体里面的时候,我第一个想法就是,这不就是变相地把 C 语言当作面向对象编程语言使用嘛!!!
成员 *head
和 *tail
是头尾节点指针。 unsigned long len
记录了链表的总长度,但我们需要知道链表长度的时候,我们可以通过调用函数 listLength(l)
马上得到结果,而不需要把链表遍历一次。void *(*dup)(void *ptr)
用来拷贝链表,void (*free)(void *ptr)
用于释放链表资源,int (*match)(void *ptr, void *key)
用作判断某个节点是否匹配。
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;
再加上一堆宏定义函数,简直太妙了,这样一来 struct list
仿佛有了成员函数。
/* Functions implemented as macros */
#define listLength(l) ((l)->len)
#define listFirst(l) ((l)->head)
#define listLast(l) ((l)->tail)
#define listPrevNode(n) ((n)->prev)
#define listNextNode(n) ((n)->next)
#define listNodeValue(n) ((n)->value)
#define listSetDupMethod(l,m) ((l)->dup = (m))
#define listSetFreeMethod(l,m) ((l)->free = (m))
#define listSetMatchMethod(l,m) ((l)->match = (m))
#define listGetDupMethod(l) ((l)->dup)
#define listGetFreeMethod(l) ((l)->free)
#define listGetMatchMethod(l) ((l)->match)
不仅如此,Redis 还为 adlist 设计了简单的迭代器 。成员 listNode *next
虽然名叫 next ,但是它可以代表下一个节点,或者上一个节点,这要取决于另外一个成员 int direction
,它代表着迭代器的遍历方向。
typedef struct listIter {
listNode *next;
int direction;
} listIter;
ADLIST 操作
除了上面提及的 “成员函数” (比喻),还有众多 struct list
操作函数。这里我不打算对所有函数进行分析,只挑选出一部分比较重要的函数。
/* Prototypes */
list *listCreate(void);
void listRelease(list *list);
void listEmpty(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 listRotateTailToHead(list *list);
void listRotateHeadToTail(list *list);
void listJoin(list *l, list *o);
listCreate
第一要介绍的函数是 list *listCreate(void)
,因为一切都始于这个函数。Redis 的 struct list
的初始化实现跟我平常的双链表的做法不太一样。我不会把 *head
跟 *tail
用于存放真实数据,我一般都会把它们当作哨兵节点去使用。使用哨兵节点的好处就是,不用检验头尾节点的合法性,因为它们永远都不为 NULL
,并且在最开始的时候,首尾相连,头尾指针互相指向对方。
而 Redis 的做法是,在链表创建之初,*head
和 *tail
都为 NULL
,因为像我之前所说 Redis 没有哨兵节点这一玩法。
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;
}
listInsertNode
我们看一下 Redis 是如何插入一个链表节点。参数 after
用于判断新的节点是插在 *old_node
之前还是之后。参数 void *value
是新的节点所需保存的值,所以并不需要自己分配内存去创建一个新的节点再传入函数 listInsertNode
。
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;
}
Redis 的 LINSERT 命令大概就是调用了 llistInsertNode
。
LINSERT fruits BEFORE "orange" "apple"
listSearchKey
链表有它自身的优势,同时它也有不足之处。想要在链表中寻找某个特定的节点,我们需要把链表从头到尾遍历一次直到命中目标节点为止。如果某个搜索节点不存在于链表之中,这是一件很浪费时间的事情。为了遍历链表,Redis 设计的迭代器派上用场。函数 void listRewind(list *list, listIter *li)
用来创建一个新的迭代器,并且指向链表得 *head
节点。然后通过调用 listNode *listNext(listIter *iter)
拿到下一个节点的指针。如果最终没有找到目标节点,那么返回 NULL
。
listNode *listSearchKey(list *list, void *key)
{
listIter iter;
listNode *node;
listRewind(list, &iter);
while((node = listNext(&iter)) != NULL) {
if (list->match) {
if (list->match(node->value, key)) {
return node;
}
} else {
if (key == node->value) {
return node;
}
}
}
return NULL;
}
结语
链表虽然是一种很普通常见的数据结构,但链表的操作可以其实是可以变得十分复杂。不过总体上链表的设计不会有太多变化,Redis 的设计实在值得让我学习。