Redis源码探究系列—双向链表(adlist)源码实现解析

|----------------------------------------------|-------------------------------------------------------------------------------------------|
| 欢迎各位同学关注我哦~ 在这个 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;

dupfreematch三个函数指针实现了多态:链表本身不关心节点值的具体类型,由使用者在创建链表后设定这些回调。具体而言:

  • dup :在listDup中,若设定了dup回调,则逐节点调用copy->dup(node->value)复制值;否则仅浅拷贝指针(value = node->value)。这使得链表复制可以根据值类型选择深拷贝或浅拷贝。
  • free :在listReleaselistDelNode中,删除节点前调用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;
}

空链表的特殊处理:headtail都指向新节点,前后指针均为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->tailnode->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->nextnode->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)。

为什么默认比较指针而不是内容? 因为valuevoid*,链表不知道数据的实际类型和比较方式。如果使用者关心内容比较,就设置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;
}

在栈上创建迭代器(不分配堆内存),适合短生命周期的遍历场景。listSearchKeylistRotate都使用了这种方式。

五、高级操作

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的尾部。关键逻辑:

  1. 如果o有节点,先建立o->head->prevl->tail的连接(即使l->tail为NULL)
  2. 如果l非空,建立l->tail->nexto->head的连接;否则l->head直接指向o->head
  3. 更新l->tail和长度
  4. 拼接后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 喧嚣的时代 不忘初心,戒骄戒躁,认真沉淀 | |

相关推荐
一只小白00017 小时前
Redis 常用命令总结
数据库·redis·缓存
Han.miracle17 小时前
Redis 全套笔记:基础 API + 三大架构 + 缓存三大问题
java·windows·redis
风吹迎面入袖凉18 小时前
【Redis】Redis缓存击穿
数据库·redis·缓存
小飞鱼通达二开20 小时前
[学习笔记]Redis概述
redis
老毛肚21 小时前
Redis高级
java·数据库·redis
kiku18181 天前
NoSQL之Redis集群
数据库·redis·nosql
indexsunny1 天前
互联网大厂Java面试实录:微服务+Spring Boot在电商场景中的应用
java·spring boot·redis·微服务·eureka·kafka·spring security
卢傢蕊1 天前
NoSQL 之Redis 集群
数据库·redis·nosql
霸道流氓气质1 天前
Bat中实现简单运维脚本示例-启动redis、检测指定端口是否占用、占用则杀死进程、等待指定秒数、启动jar包
运维·redis·jar