|----------------------------------------------|-------------------------------------------------------------------------------------------|
| 欢迎各位同学关注我哦~ 在这个 AI 喧嚣的时代 不忘初心,戒骄戒躁,认真沉淀 |
|
在上一篇文章中,我们深入分析了SDS简单动态字符串------Redis最基础的数据结构之一。SDS解决了C字符串在二进制安全、动态扩容方面的不足,为Redis的键值存储打下了基础。但Redis的数据结构体系不止于此。从本文开始,我们进入链表和字典的部分,首先来看双向链表(adlist)。
链表和字典是Redis中列表键、哈希键等高级数据类型的底层支撑。与SDS一样,C语言同样没有内置链表,Redis选择自己实现一套简洁高效的双向链表。接下来,我们就来一起看一下Redis的底层是如何自己实现链表的。
一、为什么Redis需要自己实现链表?
C语言没有内置链表数据结构。虽然可以用指针和结构体手动构建,但每次都从零写一遍既繁琐又容易出错。Redis需要在多处使用链表:
- 列表键的底层实现之一(当列表元素较多或元素为长字符串时)
- 发布与订阅模块中维护客户端列表
- 慢查询日志的存储
- 监视器功能的客户端列表
- AOF缓冲区中的回复缓冲
Redis没有使用某种通用链表库,而是在adlist.h / adlist.c中实现了一套简洁高效的双向链表,代码量不到400行,却涵盖了链表的所有常用操作。
二、数据结构定义
2.1 链表节点 --- listNode
c
// adlist.h:39-43
typedef struct listNode {
struct listNode *prev; // 前驱节点指针
struct listNode *next; // 后继节点指针
void *value; // 节点值,void*实现泛型
} listNode;
2.2 链表迭代器 --- listIter
c
// adlist.h:51-55
typedef struct listIter {
listNode *next; // 下一个要访问的节点
int direction; // 迭代方向,AL_START_HEAD或AL_START_TAIL
} listIter;
再Redis中,链表和迭代器是解耦的,这样的好处就是一个链表可以同时拥有多个不同方向的迭代器,互不干扰。
2.3 链表 --- list
c
// adlist.h:57-67
typedef struct list {
listNode *head; // 头节点
listNode *tail; // 尾节点
void *(*dup)(void *ptr); // 节点值复制函数
void (*free)(void *ptr); // 节点值释放函数
int (*match)(void *ptr, void *key); // 节点值比较函数
unsigned long len; // 链表长度, O(1)获取
} list;
dup、free、match三个函数指针实现了多态:链表本身不关心节点值的具体类型,由使用者在创建链表后设定这些回调。具体而言:
dup:在listDup中,若设定了dup回调,则逐节点调用copy->dup(node->value)复制值;否则仅浅拷贝指针(value = node->value)。这使得链表复制可以根据值类型选择深拷贝或浅拷贝。free:在listRelease和listDelNode中,删除节点前调用list->free(node->value)释放值所占资源。若未设定,则不释放值------适用于值由外部管理或为栈分配的情形。match:在listSearchKey中,若设定了match回调,则调用list->match(node->value, key)比较;否则直接比较指针(key == node->value)。这使得链表可以按内容语义查找,而非仅按指针地址匹配。
2.4 内存布局
list
┌─────────┬─────────┬──────────┬──────────┬──────────┬───────┐
│ head │ tail │ dup │ free │ match │ len │
└────┬────┴────┬────┴──────────┴──────────┴──────────┴───────┘
│ │
▼ ▼
NULL NULL (空链表)
list (非空)
┌─────────┬─────────┬───────┬──────────┬──────────┬───────┐
│ head │ tail │ dup │ free │ match │ len │
└────┬────┴────┬────┴───────┴──────────┴──────────┴───────┘
│ │
▼ ▼
┌──────┐ ┌──────┐
│prev=N│ │next=N│
│next──┼─►│prev──┼─►NULL
│value │ │value │
└──────┘ └──────┘
head tail
三、核心API源码分析
3.1 创建链表 --- listCreate
c
// adlist.c:34-44
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;
}
创建空链表,三个函数指针初始化为NULL。这意味着如果使用者不设置free回调,删除节点时不会释放value指向的内存------这是使用者的责任。
3.2 释放链表 --- listRelease
c
// adlist.c:75-79
void listRelease(list *list)
{
listEmpty(list);
zfree(list);
}
释放链表分两步:先调用listEmpty清空所有节点,再释放链表结构本身。listEmpty会遍历所有节点,调用free回调释放value,然后释放节点。时间复杂度O(n)。
c
// adlist.c:55-68
void listEmpty(list *list)
{
unsigned long len;
listNode *current, *next;
current = list->head;
len = list->len;
while(len--) {
next = current->next;
if (list->free) list->free(current->value);
zfree(current);
current = next;
}
list->head = list->tail = NULL;
list->len = 0;
}
listEmpty移除所有元素但不销毁链表结构本身,清空后链表恢复到初始状态(空链表)。
3.3 头插法 --- listAddNodeHead
c
// adlist.c:67-86
list *listAddNodeHead(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 = NULL;
node->next = list->head;
list->head->prev = node;
list->head = node;
}
list->len++;
return list;
}
空链表的特殊处理:head和tail都指向新节点,前后指针均为NULL。非空时只需调整4个指针。时间复杂度O(1)。
3.4 尾插法 --- listAddNodeTail
c
// adlist.c:88-107
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;
}
结构与头插法对称------将list->tail和node->prev的关系做对称操作即可。头插和尾插都只需修改常量个指针(至多4个),无需遍历链表,因此时间复杂度均为O(1)。这正是双向链表相比单链表的核心优势:单链表的尾插需要O(N)遍历到尾部,而双向链表通过list->tail指针可以直接定位尾节点,使得头尾两端插入同样高效。
3.5 指定位置插入 --- listInsertNode
c
// adlist.c:109-145
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;
}
after参数控制插入在old_node之后还是之前:当after为真时,新节点成为old_node的后继,若old_node恰好是尾节点则需同步更新tail;当after为假时,新节点成为old_node的前驱,若old_node恰好是头节点则需同步更新head。无论哪种情况,前后邻居的指针更新都通过node->prev->next和node->next->prev统一处理,避免了重复代码。
3.6 删除节点 --- listDelNode
c
// adlist.c:147-167
void listDelNode(list *list, listNode *node) {
if (node->prev)
node->prev->next = node->next;
else
list->head = node->next;
if (node->next)
node->next->prev = node->prev;
else
list->tail = node->prev;
if (list->free) list->free(node->value);
zfree(node);
list->len--;
}
通过判断prev/next是否为NULL来区分是否是头/尾节点,分别处理。删除后调free回调释放value,再释放节点本身。时间复杂度O(1)------不需要遍历查找,因为调用者已经持有节点指针。
3.7 查找节点 --- listSearchKey
c
// adlist.c:219-238
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;
}
从头遍历查找。如果有match回调,用回调比较;否则直接比较指针地址。时间复杂度O(n)。
为什么默认比较指针而不是内容? 因为value是void*,链表不知道数据的实际类型和比较方式。如果使用者关心内容比较,就设置match回调。
3.8 按索引获取 --- listIndex
c
// adlist.c:240-254
listNode *listIndex(list *list, long index) {
listNode *n;
if (index < 0) {
index = (-index) - 1;
n = list->tail;
while (index-- && n) n = n->prev;
} else {
n = list->head;
while (index-- && n) n = n->next;
}
return n;
}
支持正负索引:正数从头部开始,负数从尾部开始(-1表示尾节点)。这是Redis列表命令LINDEX的底层支撑。时间复杂度O(n)。
四、迭代器API
4.1 获取迭代器 --- listGetIterator
c
// adlist.c:169-180
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;
}
根据方向设置迭代起点:从头开始则next指向head,从尾开始则指向tail。
4.2 获取下一个节点 --- listNext
c
// adlist.c:189-203
listNode *listNext(listIter *iter) {
listNode *current = iter->next;
if (current != NULL) {
if (iter->direction == AL_START_HEAD)
iter->next = current->next;
else
iter->next = current->prev;
}
return current;
}
返回当前节点,并将迭代器推进到下一个。方向决定推进方式:正向推next,逆向推prev。
4.3 栈上迭代器 --- listRewind / listRewindTail
c
// adlist.c:182-187
void listRewind(list *list, listIter *li) {
li->next = list->head;
li->direction = AL_START_HEAD;
}
void listRewindTail(list *list, listIter *li) {
li->next = list->tail;
li->direction = AL_START_TAIL;
}
在栈上创建迭代器(不分配堆内存),适合短生命周期的遍历场景。listSearchKey和listRotate都使用了这种方式。
五、高级操作
5.1 链表旋转 --- listRotate
c
// adlist.c:256-272
void listRotate(list *list) {
listNode *tail = list->tail;
if (listLength(list) <= 1) return;
list->tail = tail->prev;
list->tail->next = NULL;
list->head->prev = tail;
tail->prev = NULL;
tail->next = list->head;
list->head = tail;
}
将尾节点移到头部。4个指针调整,O(1)完成。这是Redis RPOPLPUSH命令的底层操作之一。
5.2 链表拼接 --- listJoin
c
// adlist.c:345-360
void listJoin(list *l, list *o) {
if (o->head)
o->head->prev = l->tail;
if (l->tail)
l->tail->next = o->head;
else
l->head = o->head;
if (o->tail) l->tail = o->tail;
l->len += o->len;
/* Setup other as an empty list. */
o->head = o->tail = NULL;
o->len = 0;
}
将o的所有节点拼接到l的尾部。关键逻辑:
- 如果
o有节点,先建立o->head->prev到l->tail的连接(即使l->tail为NULL) - 如果
l非空,建立l->tail->next到o->head的连接;否则l->head直接指向o->head - 更新
l->tail和长度 - 拼接后
o变成空链表,但o本身不被释放,由调用者来决定是否释放o
O(1)时间完成,不用一个一个遍历所有的节点。
5.3 链表复制 --- listDup
c
// adlist.c:205-217
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;
listRewind(orig, &iter);
while ((node = listNext(&iter)) != NULL) {
void *value;
if (copy->dup) {
value = copy->dup(node->value);
if (value == NULL) {
listRelease(copy);
return NULL;
}
} else {
value = node->value;
}
if (listAddNodeTail(copy, value) == NULL) {
listRelease(copy);
return NULL;
}
}
return copy;
}
深拷贝vs浅拷贝由dup回调决定:有dup则深拷贝,无dup则共享value指针。任一节点复制失败则回滚释放整个副本。
六、多态机制详解
adlist的多态完全依靠三个函数指针 + void*实现:
listSetDupMethod(list, myDup);
listSetFreeMethod(list, myFree);
listSetMatchMethod(list, myMatch);
6.1 为什么不用C++ 的模板或继承?
Redis用纯C编写,没有模板和继承。void* + 函数指针是C语言实现多态的经典手法:
c
// 比较的两种模式
if (list->match) {
// 有 match 回调:按内容比较
if (list->match(node->value, key)) return node;
} else {
// 无 match 回调:按指针比较
if (key == node->value) return node;
}
6.2 Redis中的使用示例
c
// server.c:2572
// 慢查询日志使用adlist
listSetFreeMethod(server.slowlog, slowlogFreeEntry);
// pubsub.c:445
// 发布订阅维护客户端列表
// 不设 free,因为客户端结构由其他模块管理
clients = listCreate();
七、复杂度汇总
| 操作 | 函数 | 时间复杂度 |
|---|---|---|
| 创建链表 | listCreate |
O(1) |
| 释放链表 | listRelease |
O(n) |
| 头部插入 | listAddNodeHead |
O(1) |
| 尾部插入 | listAddNodeTail |
O(1) |
| 指定位置插入 | listInsertNode |
O(1) |
| 删除节点 | listDelNode |
O(1) |
| 按值查找 | listSearchKey |
O(n) |
| 按索引访问 | listIndex |
O(n) |
| 旋转 | listRotate |
O(1) |
| 拼接 | listJoin |
O(1) |
| 复制 | listDup |
O(n) |
| 获取长度 | listLength |
O(1) |
| 获取头/尾节点 | listFirst/Last |
O(1) |
九、adlist在Redis中的使用场景
| 场景 | 字段/变量 | 说明 |
|---|---|---|
| 列表键底层实现 | robj->ptr |
编码为OBJ_ENCODING_LINKEDLIST时 |
| 发布订阅频道列表 | client->pubsub_channels |
客户端订阅的频道 |
| 客户端列表 | server.clients |
所有已连接客户端 |
| 从节点列表 | server.slaves |
所有从节点 |
| 慢查询日志 | server.slowlog |
慢查询条目列表 |
| AOF重写差异缓冲 | server.aof_rewrite_buf_blocks |
AOF重写期间的增量命令缓冲 |
| 监视器客户端列表 | server.monitors |
所有MONITOR客户端 |
Redis 3.2之后,列表键的底层实现从adlist + ziplist改为quicklist(双向链表 + 压缩列表的混合体)。但adlist仍然在上述其他场景中广泛使用。下一篇,我们将深入字典的源码实现,看看Redis是如何设计与实现一个高性能哈希表的。
|----------------------------------------------|-------------------------------------------------------------------------------------------|
| 欢迎各位同学关注我哦~ 在这个 AI 喧嚣的时代 不忘初心,戒骄戒躁,认真沉淀 |
|