【深度解析】Redis 链表结构

本文详细介绍了Redis中的链表数据结构,包括其特点、双端链表节点和结构,以及部分API的实现。Redis链表支持存放所有数据类型,提供迭代器、深拷贝和自定义比较规则等功能。文章通过代码示例展示了如何创建迭代器、复制链表以及查找节点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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 设计与实现》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一朝英雄拔剑起

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值