Redis设计与实现(list链表)第一章

双端链表

链表作为数组之外的一种常用序列抽象,是大多数高级语言的基本数据类型,因为 C 语言本身 不支持链表类型,大部分 C 程序都会自己实现一种链表类型,Redis 也不例外——它实现了一 个双端链表结构。
双端链表作为一种常见的数据结构,在大部分的数据结构或者算法书里都有讲解,因此,这一 章关注的是 Redis 双端链表的具体实现,以及该实现的 API ,而对于双端链表本身,以及双端 链表所对应的算法,则不做任何解释。

双端链表还是 Redis 列表类型的底层实现之一,当对列表类型的键进行操作——比如执行 RPUSH 、LPOP 或 LLEN 等命令时,程序在底层操作的可能就是双端链表。

redis> RPUSH brands Apple Microsoft Google (integer) 3
redis> LPOP brands "Apple"
redis> LLEN brands (integer) 2
redis> LRANGE brands 0 -1 1) "Microsoft" 2) "Google"

Redis 列表使用两种数据结构作为底层实现:
1. 双端链表
2. 压缩列表
因为双端链表占用的内存比压缩列表要多,所以当创建新的列表键时,
列表会优先考虑使用压 缩列表作为底层实现,并且在有需要的时候,
才从压缩列表实现转换到双端链表实现。

Redis 自身功能的构建

除了实现列表类型以外,双端链表还被很多 Redis 内部模块所应用:
• 事务模块使用双端链表来按顺序保存输入的命令;
• 服务器模块使用双端链表来保存多个客户端;
• 订阅/发送模块使用双端链表来保存订阅模式的多个客户端;
• 事件模块使用双端链表来保存时间事件(time event);

1.2.2 双端链表的实现

双端链表的实现由 listNode 和 list 两个数据结构构成,下图展示了由这两个结构组成的一 个双端链表实例:

这里写图片描述

其中,listNode 是双端链表的节点: typedef struct listNode {
// 前驱节点 
struct listNode *prev;
// 后继节点 
struct listNode *next;
// 值 
void *value;
} listNode;
而 list 则是双端链表本身: 
typedef struct list {
// 表头指针
 listNode *head;
// 表尾指针 
listNode *tail;
// 节点数量 
unsigned long len;
// 复制函数 
void *(*dup)(void *ptr); 
// 释放函数 
void (*free)(void *ptr); 
// 比对函数 
int (*match)(void *ptr, void *key); 
} list;

list 类型保留了三个函数指针——dup 、free 和 match ,分别用于处理值的复制、释放和对比匹配。在对节点的值进行处 理时,如果有给定这些函数,那么它们就会被调用。
举个例子:当删除一个 listNode 时,如果包含这个节点的 list 的 list->free 函数不为空,
再执行余下的删除 操作(比如说,释放节点)。
另外,从这两个数据结构的定义上,也可以它们的一些行为和性能特征:

• listNode 带有 prev 和 next 两个指针,因此,对链表的遍历可以在两个方向上进行: 
从表头到表尾,或者从表尾到表头。
• list 保存了 head 和 tail 两个指针 ,LPUSH 、RPOP 、
RPOPLPUSH 等命令的关键。 
 • list 带有保存节点数量的 len 属性,所以计算链表长度的复杂度仅为 θ(1) ,
 这也保证 了 LLEN 命令不会成为性能瓶颈。 

以下是用于操作双端链表的 API ,它们的作用以及算法复杂度:

这里写图片描述

1.2.3 迭代器
Redis 为双端链表实现了一个迭代器 ,这个迭代器可以从两个方向对双端链表进行迭代: • 沿着节点的 next 指针前进,从表头向表尾迭代; • 沿着节点的 prev 指针前进,从表尾向表头迭代; 以下是迭代器的数据结构定义:

 typedef struct listIter {
// 下一节点 
listNode *next;
// 迭代方向 
int direction;
} listIter;

direction 记录迭代应该从那里开始: :
• 如果值为 adlist.h/AL_START_HEAD ,那么迭代器执行从表头到表尾的迭代;
• 如果值为 adlist.h/AL_START_TAIL ,那么迭代器执行从表尾到表头的迭代
这里写图片描述

1.2.4 小结

1)Redis 实现了自己的双端链表结构。 
2)  双端链表主要有两个作用:  
 – 作为 Redis 列表类型的底层实现之一;  
 – 作为通用数据结构,被其他功能模块所使用;   
3) 双端链表及其节点的性能特性如下: 
  – 节点带有前驱和后继指针,访问前驱节点和后继节点的复杂度为 O(1) , 
  并且对链表 的迭代可以在从表头到表尾和从表尾到表头两个方向进行; 
  – 链表带有指向表头和表尾的指针,因此对表头和表尾进行处理的复杂度为 O(1) ;  
  – 链表带有记录节点数量的属性,所以可以在 O(1) 复杂度内返回链表的节点数量(长 度);

重点API源码实现

listCreate

list *listCreate(void)
{
    struct list *list;

    // 申请空间,zmalloc函数是redis定义的空间配置函数,我们后面会分析其原理,现在就暂时把它等同于malloc
    if ((list = zmalloc(sizeof(*list))) == NULL)
        return NULL;
    // 进行“常规”的初始化,这些跟我们在《数据结构》中学习的过程基本一致,注意这里还把三个函数指针设为null
    list->head = list->tail = NULL;
    list->len = 0;
    list->dup = NULL;
    list->free = NULL;
    list->match = NULL;
    return list;
}

listRelease

 // 释放整个链表
void listRelease(list *list)
{
    // 先销毁链表中的所有节点
    unsigned long len;
    listNode *current, *next;

    current = list->head;
    len = list->len;
    while(len--) {
        next = current->next;
        // 通过list中的回调函数来释放每一节点数据域的内存空间
        if (list->free) list->free(current->value);
        // 销毁节点空间,zfree是redis定义的空间释放函数,和zmalloc对应
        zfree(current);
        current = next;
    }
    // 最后把链表也销毁
    zfree(list);
}

listGetIterator

//获取一个给定方向上的迭代器
listIter *listGetIterator(list *list, int direction)
{
    // 定义一个迭代器并为其分配空间
    listIter *iter;

    // 这里需要注意,调用该函数时分配了动态内存,使用完需要手动释放。这一点我们可以在后面代码中看到
    // listGetIterator和listReleaseIterator是成对出现的
    if ((iter = zmalloc(sizeof(*iter))) == NULL) return NULL;
    if (direction == AL_START_HEAD)
        // 如果该迭代器的方向是从前往后,则迭代器的next指针指向头结点
        iter->next = list->head;
    else
        // 如果该迭代器的方向是从后往前,则迭代器的next指针指向尾节点
        iter->next = list->tail;
    // 设置迭代器方向
    iter->direction = direction;
    return iter;
}

listAddNodeHead

// 在表头插入一个节点
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;
}

listAddNodeTail

// 在表尾插入一个节点
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;
}

listInsertNode

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;
}

listDelNode

// 删除一个节点

void listDelNode(list *list, listNode *node)
{
    // 删除链表中的一个节点要考虑被删除节点是否为头结点或尾节点

    if (node->prev)
        //  如果node->prev存在,说明该节点不是头结点,直接将prev节点的next指向被删除节点的下一个
        node->prev->next = node->next;
    else
        // 被删除节点是头结点,让被删除节点的下一个节点成为头结点
        list->head = node->next;

    if (node->next)
        // 如果node->next存在,说明该节点不是尾节点
        node->next->prev = node->prev;
    else
        // 被删除节点是尾节点
        list->tail = node->prev;

    // 回调free函数删除节点的数据域
    if (list->free) list->free(node->value);
    // 释放节点空间
    zfree(node);
    // 最后更新链表长度
    list->len--;
}

其他的实现请到:
https://blog.csdn.net/Xiejingfa/article/details/50938028

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值