作者推荐 |【Redis技术进阶之路】「底层源码解析」揭秘高效存储模型与数据结构底层实现(链表)(二)

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 作者推荐 |【Redis技术进阶之路】「底层源码解析」揭秘高效存储模型与数据结构底层实现(链表)

作者推荐 |【Redis技术进阶之路】「底层源码解析」揭秘高效存储模型与数据结构底层实现(链表)(一)https://developer.aliyun.com/article/1471148


核心方法源码分析(adlist.c)
头文件引入

在C语言的代码中,预处理器指令部分起到了至关重要的作用。这部分代码完成了对三个头文件的引入,分别是:

c

复制代码

#include <stdlib.h>
#include "adlist.h"
#include "zmalloc.h"
  1. <stdlib.h>:这是C语言的标准库头文件,它包含了诸如内存管理、程序控制以及类型转换等功能的函数和宏定义。通过引入这个头文件,我们可以方便地使用mallocfreeexit等标准库函数,为程序提供基础且强大的支持。
  2. "adlist.h":通过引入这个头文件,可以利用其中定义的数据结构、函数原型或宏,从而简化列表相关操作的实现。
  3. "zmalloc.h":引入这个头文件后,可以使用其中提供的特定内存分配和释放机制,这些机制可能具有更好的错误处理、内存跟踪或其他高级功能,从而增强程序的健壮性和性能。
listCreate:创建一个新的空链表
  • 时间复杂度:O(1)

下面的源码是创建新链表的函数listCreate的实现。该函数返回list指针类型,不接受任何参数。以下是对源码的详细注释和解释,帮助您清晰地了解源码的含义和实现原理。

c

复制代码

// 创建新链表的函数,返回链表结构体的指针
list *listCreate(void)
{
    // 声明了一个list结构体的指针list。struct list 应该是在adlist.h头文件中定义的,它定义了链表的结构。
    struct list *list;
   // 使用 zmalloc 函数为 list 结构体分配内存。zmalloc是Redis提供的内存分配函数,它在内存分配失败时会返回 NULL。sizeof(*list) 计算 list 指针所指向的结构体的大小。
    if ((list = zmalloc(sizeof(*list))) == NULL)
        return NULL; //如果内存分配失败,函数会返回 NULL。    
    // 这里初始化了链表的头指针 head 和尾指针 tail 为 NULL,并设置链表的长度 len 为 0。这表示新创建的链表是空的。    
    list->head = list->tail = NULL;
    list->len = 0; // 初始长度为0
    // 设置复制、释放和匹配函数为 NULL
    list->dup = NULL;
    list->free = NULL;
    list->match = NULL;
    return list;
}

总结一下,listCreate 函数的主要功能是创建一个新的、空的链表,并返回其指针。如果内存分配失败,它会返回 NULL。当不再需要已创建的列表时,应使用listRelease()函数来释放其占用的内存。在调用listRelease()之前,列表中每个节点的私有值需要由用户自行释放,或者可以通过listSetFreeMethod函数来设定一个自动释放这些私有值的方法。

listRelease:释放整个列表
  • 时间复杂度:O(N) ,N为链表长度

listRelease 函数具有高度的可靠性,它确保成功释放给定的链表及其所包含的所有节点,不会出现失败的情况。通过调用该函数,可以有效地回收链表占用的内存资源,确保程序的稳定运行。



c

复制代码

/*  
 * Free the whole list.
 * This function can't fail. 
 */
void listRelease(list *list)
{
    //传入的list指针是否为NULL。如果是NULL,函数会直接返回,不执行任何操作。
    if (!list) // 避免了在空指针上执行操作,这可能会导致未定义行为或程序崩溃。
        return;
    // 调用listEmpty函数来释放链表中的所有节点,逐个释放每个节点所占用的内存。
    listEmpty(list);
    // 使用zfree函数来释放list结构体本身所占用的内存
    zfree(list);
}
listEmpty:移除所有的元素,但是不释放自己本身
  • 时间复杂度:O(1)

在深入探讨listRelease方法的实现时,我们注意到其中调用了listEmpty方法。为了更全面地理解整个链表的内存释放过程,接下来我们将仔细研究并分析listEmpty方法的实现原理和运行流程。下面的源码就是针对于实现原理和流程进行分析,有了对应的源码和注释,相信你可以从底层更加清晰的认识listEmpty的实现原理。

c

复制代码

/* Remove all the elements from the list without destroying the list itself. */
void listEmpty(list *list)  
{  
    // 获取链表的长度  
    unsigned long len;  
    // 定义当前节点和下一个节点的指针  
    listNode *current, *next;  
      
    // 将当前节点指针指向链表的头节点  
    current = list->head;  
      
    // 将链表长度赋值给len变量,方便后续遍历  
    len = list->len;  
      
    // 循环遍历链表,直到所有节点都被处理  
    while(len--) {  
        // 将下一个节点指针指向当前节点的下一个节点  
        next = current->next;  
          
        // 如果链表定义了释放节点值的函数,则调用该函数释放当前节点的值  
        if (list->free) list->free(current->value);  
          
        // 释放当前节点所占用的内存  
        zfree(current);  
          
        // 将当前节点指针指向下一个节点,准备处理下一个节点  
        current = next;  
    }  
      
    // 将链表的头节点和尾节点指针置为NULL,表示链表为空  
    list->head = list->tail = NULL;  
      
    // 将链表长度置为0,表示链表没有节点  
    list->len = 0;  
}
listAddNodeHead:插入新节点到表头
  • 时间复杂度:O(1)

函数的目的通过插入新节点到表头,我们根据需要修改链表的结构和内容。无论是将新节点添加到链表的开头,还是在特定节点的前后位置插入,都能轻松实现。同时,通过返回更新后的链表头节点指针,我们可以方便地访问和操作整个链表。

  • 参数:
  • list:指向列表的指针
  • value:要添加到新节点的值(作为指针)
  • 返回值:
  • 成功时返回更新后的列表指针
  • 出错时返回NULL

下面便是源码的分析和解释说明:

c

复制代码

// 定义一个名为 listAddNodeHead 的函数,该函数接受一个指向 list 结构的指针和一个 void 类型的指针作为参数。  
// 函数返回一个指向 list 结构的指针。  
list *listAddNodeHead(list *list, void *value)  
{  
    // 定义一个指向 listNode 结构的指针 node。  
    listNode *node;  
  
    // 使用 zmalloc 函数为 listNode 分配内存,并将返回的地址赋值给 node。  
    // 如果内存分配失败,zmalloc 会返回 NULL。  
    if ((node = zmalloc(sizeof(*node))) == NULL)  
        // 如果内存分配失败,则返回 NULL。  
        return NULL;  
  
    // 将传入的 value 赋值给新节点的 value 字段。  
    node->value = value;  
  
    // 调用 listLinkNodeHead 函数,将新节点添加到链表的头部。  
    listLinkNodeHead(list, node);  
  
    // 返回更新后的链表指针。  
    return list;  
}

我们需要向给定的列表头部添加一个新节点,该节点包含特定的“值”。在此过程中,指针将作为关键参数进行传递。若在执行过程中遇到任何错误,函数将立即返回NULL,且不会执行任何对原列表的修改操作,以确保列表的完整性。一旦操作成功,函数将返回传递给它的原始“列表”指针,以便调用者能够继续利用更新后的列表。

listAddNodeTail:插入新节点到表尾
  • 时间复杂度:O(1)

函数的目的通过插入新节点到表尾,我们根据需要修改链表的结构和内容。下面便是源码的分析和解释说明:

  • 参数:
  • list:指向列表的指针
  • value:要添加到新节点的值(作为指针)
  • 返回值:
  • 成功时返回更新后的列表指针
  • 出错时返回NULL

c

复制代码

// 函数定义:将一个新节点添加到列表的尾部,并返回更新后的列表指针。  
list *listAddNodeTail(list *list, void *value)  {  
    // 声明一个指向listNode结构的指针node,用于存储新节点的地址  
    listNode *node;  
    // 使用zmalloc函数为新节点分配内存,并将返回的地址赋值给node  
    // 如果内存分配失败,zmalloc将返回NULL  
    if ((node = zmalloc(sizeof(*node))) == NULL)  
        // 如果内存分配失败,则返回NULL,不执行任何操作  
        return NULL;  
    // 将传入的value赋值给新节点的value字段  
    node->value = value;  
    // 调用listLinkNodeTail函数,将新节点添加到列表的尾部  
    listLinkNodeTail(list, node);  
    // 返回更新后的列表指针  
    return list;  
}

在列表中的尾部添加一个全新的节点,这个新节点应持有指定的“Value”指针作为其内容。如果在执行过程中出现任何错误,函数将立即返回NULL,并且不会进行任何对原列表的修改,以确保列表的完整性。一旦操作成功完成,函数将返回传递给它的原始“list”指针,以便调用者能够继续利用更新后的列表。

listLinkNodeHead:将新节点添加到链表
  • 时间复杂度:O(1)

将预先分配好的节点添加至列表的起始位置,实现节点的头部插入操作。

c

复制代码

// 函数用于将一个节点链接到链表的头部  
void listLinkNodeHead(list* list, listNode *node) {  
    // 如果链表为空(即长度为0)  
    if (list->len == 0) {  
        // 将新节点设置为链表的头节点和尾节点  
        list->head = list->tail = node;  
        // 因为链表只有一个节点,所以该节点的prev和next都指向NULL  
        node->prev = node->next = NULL;  
    } else {  
        // 如果链表不为空,那么将新节点的prev指向NULL(因为它是头部节点)  
        node->prev = NULL;  
        // 将新节点的next指向当前的头节点  
        node->next = list->head;  
        // 将当前头节点的prev指向新节点,形成双向链表的链接  
        list->head->prev = node;  
        // 将链表的头节点更新为新节点  
        list->head = node;  
    }  
    // 无论链表是否为空,都将链表的长度加1  
    list->len++;  
}
  1. 链表为空的情况
  • 如果链表为空(list->len == 0),说明之前没有节点,新节点不仅会成为头节点,也会成为尾节点。
  • list->headlist->tail都指向新节点node
  • 由于链表只有一个节点,该节点的prevnext指针都应该指向NULL
  1. 链表不为空的情况
  • 初始化新节点的prevNULL,因为它即将成为新的头节点。
  • 将新节点的next指向当前的头节点list->head
  • 更新当前头节点的prev指针,使其指向新节点,确保双向链表的连接正确。
  • 更新链表的头节点指针list->head,使其指向新节点。
  1. 链表长度更新
  • 无论链表之前是否为空,在插入新节点后,链表的长度都会增加1,因此list->len需要加1。
listDelNode:从链表中删除给定的节点
  • 时间复杂度:O(N),N为链表长度

从给定的列表中移除特定的节点,并释放该节点的内存。  若提供了自由回调函数,则利用该函数来释放节点所持有的值。  此函数确保执行成功,不会出现失败的情况。

c

复制代码

// 函数定义:从列表中删除一个节点,并释放相关内存  
void listDelNode(list *list, listNode *node)  
{  
    // 调用listUnlinkNode函数,从列表中解除该节点的链接  
    listUnlinkNode(list, node);  
    // 如果列表的free字段不为空(即存在一个用于释放节点值的回调函数)  
    if (list->free)   
        // 调用该回调函数,释放节点中存储的值所占用的内存  
        list->free(node->value);  
      
    // 调用zfree函数,释放节点本身所占用的内存  
    zfree(node);  
}
listUnlinkNode:列表中解除该节点的链接
  • 时间复杂度:O(1)

从给定的列表中移除特定的节点,但保留其内存不被释放。这意味着节点仍然存在于内存中,只是不再与列表相关联。

c

复制代码

// 函数定义:从双向链表中移除一个节点,但不释放其内存  
void listUnlinkNode(list *list, listNode *node) {  
    // 如果该节点有前一个节点  
    if (node->prev) {  
        // 将前一个节点的next指针指向当前节点的下一个节点  
        node->prev->next = node->next;  
    } else {  
        // 否则,当前节点是头节点,将列表的头指针指向当前节点的下一个节点  
        list->head = node->next;  
    }  
      
    // 如果该节点有下一个节点  
    if (node->next) {  
        // 将下一个节点的prev指针指向当前节点的前一个节点  
        node->next->prev = node->prev;  
    } else {  
        // 否则,当前节点是尾节点,将列表的尾指针指向当前节点的前一个节点  
        list->tail = node->prev;  
    }  
      
    // 将当前节点的next和prev指针都设置为NULL,表示它已经从链表中移除  
    node->next = NULL;  
    node->prev = NULL;  
      
    // 减少列表的长度计数  
    list->len--;  
}

通过执行此操作,您可以重新利用该节点,或者将其添加到其他列表中,而无需再次分配内存。此功能确保从列表中安全地移除节点,同时保持其内存完整性。

最后总结

由于篇幅过程,因此其他的函数就不一一列举了,在这里主要给大家介绍了以上这几个函数,因为它们属于较为核心以及重要的方法。如果大家希望作者完善和继续完成其他的函数还希望大家在评论区沟通和说明,我会努力完成和完善的,文章和学习不容易,希望大家多多支持、点赞和鼓励!

源码来源

链表是Redis实现各种功能的核心组件,诸如列表键的操作、发布订阅机制、慢查询跟踪以及监视器功能,都依赖链表的支持。

链表节点

链表节点的表示通过精心设计的listNode结构体来实现。每个节点不仅包含数据,还具备指向前一个节点和后一个节点的指针,使得Redis的链表成为双向链表,提供了更加灵活的遍历和操作方式。



列表结构

整个链表由list结构体代表,包含指向链表头节点和尾节点的指针,以及记录链表长度的信息。这种结构化的管理方式使得链表的操作更加高效和便捷。


注意,Redis的链表实现特别注重安全性。链表头节点的前置指针和链表尾节点的后置指针都指向NULL,这种设计有效避免了链表中的环形结构,确保了链表操作的安全性和稳定性。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
1月前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
172 9
|
17天前
|
机器学习/深度学习 人工智能 PyTorch
Transformer模型变长序列优化:解析PyTorch上的FlashAttention2与xFormers
本文探讨了Transformer模型中变长输入序列的优化策略,旨在解决深度学习中常见的计算效率问题。文章首先介绍了批处理变长输入的技术挑战,特别是填充方法导致的资源浪费。随后,提出了多种优化技术,包括动态填充、PyTorch NestedTensors、FlashAttention2和XFormers的memory_efficient_attention。这些技术通过减少冗余计算、优化内存管理和改进计算模式,显著提升了模型的性能。实验结果显示,使用FlashAttention2和无填充策略的组合可以将步骤时间减少至323毫秒,相比未优化版本提升了约2.5倍。
34 3
Transformer模型变长序列优化:解析PyTorch上的FlashAttention2与xFormers
|
17天前
|
存储 算法 C语言
【C语言】深入浅出:C语言链表的全面解析
链表是一种重要的基础数据结构,适用于频繁的插入和删除操作。通过本篇详细讲解了单链表、双向链表和循环链表的概念和实现,以及各类常用操作的示例代码。掌握链表的使用对于理解更复杂的数据结构和算法具有重要意义。
127 6
|
17天前
|
存储 消息中间件 缓存
Redis 5 种基础数据结构?
Redis的五种基础数据结构——字符串、哈希、列表、集合和有序集合——提供了丰富的功能来满足各种应用需求。理解并灵活运用这些数据结构,可以极大地提高应用程序的性能和可扩展性。
25 2
|
29天前
|
缓存 NoSQL PHP
Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出
本文深入探讨了Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出。文章还介绍了Redis在页面缓存、数据缓存和会话缓存等应用场景中的使用,并强调了缓存数据一致性、过期时间设置、容量控制和安全问题的重要性。
39 5
|
1月前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
74 16
|
1月前
|
存储 网络协议 安全
30 道初级网络工程师面试题,涵盖 OSI 模型、TCP/IP 协议栈、IP 地址、子网掩码、VLAN、STP、DHCP、DNS、防火墙、NAT、VPN 等基础知识和技术,帮助小白们充分准备面试,顺利踏入职场
本文精选了 30 道初级网络工程师面试题,涵盖 OSI 模型、TCP/IP 协议栈、IP 地址、子网掩码、VLAN、STP、DHCP、DNS、防火墙、NAT、VPN 等基础知识和技术,帮助小白们充分准备面试,顺利踏入职场。
83 2
|
1月前
|
存储 安全 Linux
Golang的GMP调度模型与源码解析
【11月更文挑战第11天】GMP 调度模型是 Go 语言运行时系统的核心部分,用于高效管理和调度大量协程(goroutine)。它通过少量的操作系统线程(M)和逻辑处理器(P)来调度大量的轻量级协程(G),从而实现高性能的并发处理。GMP 模型通过本地队列和全局队列来减少锁竞争,提高调度效率。在 Go 源码中,`runtime.h` 文件定义了关键数据结构,`schedule()` 和 `findrunnable()` 函数实现了核心调度逻辑。通过深入研究 GMP 模型,可以更好地理解 Go 语言的并发机制。
|
1月前
|
C语言
【数据结构】二叉树(c语言)(附源码)
本文介绍了如何使用链式结构实现二叉树的基本功能,包括前序、中序、后序和层序遍历,统计节点个数和树的高度,查找节点,判断是否为完全二叉树,以及销毁二叉树。通过手动创建一棵二叉树,详细讲解了每个功能的实现方法和代码示例,帮助读者深入理解递归和数据结构的应用。
127 8
|
23天前
|
机器学习/深度学习 人工智能 自然语言处理
探索深度学习与自然语言处理的前沿技术:Transformer模型的深度解析
探索深度学习与自然语言处理的前沿技术:Transformer模型的深度解析
72 0

推荐镜像

更多