文章目录
1 Redis链表特点
链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点 来灵活地调整链表的长度。
作为一种常用数据结构,链表内置在很多高级的编程语言里面,因为Redis使用的C语言并没有内置这种数据结构,所以Redis构建了自己的链表实现。 链表在Redis中的应用非常广泛,比如列表键的底层实现之一就是链表。
当一个列表键包含了数量比较多的元素,又或者列表中包含的元素都是比较长的字符串时,Redis就会使用链表作为列表键的底层实现。
Redis链表主要特点:
- 支持存放所有的数据类型。
- 支持迭代器。
- 支持深拷贝(需自行传入对应拷贝函数)。
- 支持内存自动回收(需自行传入释放函数)。
- 支持自定义元素比较规则(需自行传入对应比较函数)。
2 Redis链表结构
2.1 双端链表节点
typedef struct listNode {
// 前置节点
struct listNode *prev;
// 后置节点
struct listNode *next;
// 节点的值
void *value;
} listNode;
Redis的链表是双端链表,有一个前置节点和一个后置节点。每个节点的值的类型是void*,可以说是最简单的泛型实现了好吧。大道至简,优雅永不过时!
2.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;
相较于普通链表,Redis的链表结构多了很多东西。为了兼容所有的数据类型,以及后续一系列操作,Redis的链表结构里,有三个函数指针,分别是:节点复制函数dup,节点值释放函数free,节点值比较函数match。
2.3 双端链表迭代器
typedef struct listIter {
// 当前迭代到的节点
listNode *next;
// 迭代的方向
int direction;
} listIter;
Redis还提供了一个迭代器结构,用于遍历链表。
3 部分API实现
由于Redis的双端链表共有15个API,在这里仅列出个人觉得有特点的API。
3.1 获取迭代器
/*
* 为给定链表创建一个迭代器,
* 之后每次对这个迭代器调用 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;
}
此函数创建了一个链表的迭代器。Redis有自己的内存分配函数zmalloc。
3.2 复制整个链表
/*
* 复制整个链表。
*
* 复制成功返回输入链表的副本,
* 如果因为内存不足而造成复制失败,返回 NULL 。
*
* 如果链表有设置值复制函数 dup ,那么对值的复制将使用复制函数进行,
* 否则,新节点将和旧节点共享同一个指针。
*
* 无论复制是成功还是失败,输入节点都不会修改。
*
* T = O(N)
*/
list *listDup(list *orig)
{
list *copy;
listIter *iter;
listNode *node;
// 创建新链表
if ((copy = listCreate()) == NULL)
return NULL;
// 设置节点值处理函数
copy->dup = orig->dup;
copy->free = orig->free;
copy->match = orig->match;
// 迭代整个输入链表
iter = listGetIterator(orig, AL_START_HEAD);
while((node = listNext(iter)) != NULL) {
void *value;
// 复制节点值到新节点
if (copy->dup) {
value = copy->dup(node->value);
if (value == NULL) {
listRelease(copy);
listReleaseIterator(iter);
return NULL;
}
} else
value = node->value;
// 将节点添加到链表
if (listAddNodeTail(copy, value) == NULL) {
listRelease(copy);
listReleaseIterator(iter);
return NULL;
}
}
// 释放迭代器
listReleaseIterator(iter);
// 返回副本
return copy;
}
Redis的链表支持拷贝,这依赖于该链表的节点复制函数dup。它会遍历原链表,调用dup函数复制一份节点的值,然后将其链接到新的链表上。如果没有传入dup函数,则是直接将原链表的void*类型的value,赋值给新的链表。这种行为非常危险,因为两个链表的节点指向同一个地址,可能一个节点修改了内容,而另一个链表不知道。
所以,在使用Redis链表的时候,应尽量传入合适的函数指针。
3.3 查找
/*
* 查找链表 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;
}
查找函数使用节点值比较函数match来判断节点的值是否与key相同。如果没有传入match函数,则通过判断节点value的指向和key的指向来节点是否相同(节点的value是void*类型的)。
4 参考资料
《Redis 设计与实现》