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

欢迎各位同学关注我哦~
在这个 AI 喧嚣的时代
不忘初心,戒骄戒躁,认真沉淀
Deepincode

在上一篇文章中,我们深入分析了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

// adlist.h:39-43
typedef struct listNode {
    struct listNode *prev;  // 前驱节点指针
    struct listNode *next;  // 后继节点指针
    void *value;            // 节点值,void*实现泛型
} listNode;

2.2 链表迭代器 — listIter

// adlist.h:51-55
typedef struct listIter {
    listNode *next;  // 下一个要访问的节点
    int direction;   // 迭代方向,AL_START_HEAD或AL_START_TAIL
} listIter;

再Redis中,链表和迭代器是解耦的,这样的好处就是一个链表可以同时拥有多个不同方向的迭代器,互不干扰。

2.3 链表 — list

// 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

// 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

// adlist.c:75-79
void listRelease(list *list)
{
    listEmpty(list);
    zfree(list);
}

释放链表分两步:先调用listEmpty清空所有节点,再释放链表结构本身。listEmpty会遍历所有节点,调用free回调释放value,然后释放节点。时间复杂度O(n)。

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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语言实现多态的经典手法:

// 比较的两种模式
if (list->match) {
    // 有 match 回调:按内容比较
    if (list->match(node->value, key)) return node;
} else {
    // 无 match 回调:按指针比较
    if (key == node->value) return node;
}

6.2 Redis中的使用示例

// server.c:2572 
// 慢查询日志使用adlist
listSetFreeMethod(server.slowlog, slowlogFreeEntry);

// pubsub.c:445
// 发布订阅维护客户端列表
// 不设 free,因为客户端结构由其他模块管理
clients = listCreate();

七、复杂度汇总

操作函数时间复杂度
创建链表listCreateO(1)
释放链表listReleaseO(n)
头部插入listAddNodeHeadO(1)
尾部插入listAddNodeTailO(1)
指定位置插入listInsertNodeO(1)
删除节点listDelNodeO(1)
按值查找listSearchKeyO(n)
按索引访问listIndexO(n)
旋转listRotateO(1)
拼接listJoinO(1)
复制listDupO(n)
获取长度listLengthO(1)
获取头/尾节点listFirst/LastO(1)

九、adlist在Redis中的使用场景

场景字段/变量说明
列表键底层实现robj->ptr编码为OBJ_ENCODING_LINKEDLIST
发布订阅频道列表client->pubsub_channels客户端订阅的频道
客户端列表server.clients所有已连接客户端
从节点列表server.slaves所有从节点
慢查询日志server.slowlog慢查询条目列表
AOF重写差异缓冲server.aof_rewrite_buf_blocksAOF重写期间的增量命令缓冲
监视器客户端列表server.monitors所有MONITOR客户端

Redis 3.2之后,列表键的底层实现从adlist + ziplist改为quicklist(双向链表 + 压缩列表的混合体)。但adlist仍然在上述其他场景中广泛使用。下一篇,我们将深入字典的源码实现,看看Redis是如何设计与实现一个高性能哈希表的。

欢迎各位同学关注我哦~
在这个 AI 喧嚣的时代
不忘初心,戒骄戒躁,认真沉淀
Deepincode
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值