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

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 作者推荐 |【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
目录
打赏
0
1
1
0
382
分享
相关文章
Redis 核心知识与项目实践解析
本文围绕 Redis 展开,涵盖其在项目中的应用(热点数据缓存、存储业务数据、实现分布式锁)、基础数据类型(string 等 5 种)、持久化策略(RDB、AOF 及混合持久化)、过期策略(惰性 + 定期删除)、淘汰策略(8 种分类)。 还介绍了集群方案(主从复制、哨兵、Cluster 分片)及主从同步机制,分片集群数据存储的哈希槽算法。对比了 Redis 与 Memcached 的区别,说明了内存用完的情况及与 MySQL 数据一致性的保证方案。 此外,详解了缓存穿透、击穿、雪崩的概念及解决办法,如何保证 Redis 中是热点数据,Redis 分布式锁的实现及问题解决,以及项目中分布式锁
Redis 实操要点:Java 最新技术栈的实战解析
本文介绍了基于Spring Boot 3、Redis 7和Lettuce客户端的Redis高级应用实践。内容包括:1)现代Java项目集成Redis的配置方法;2)使用Redisson实现分布式可重入锁与公平锁;3)缓存模式解决方案,包括布隆过滤器防穿透和随机过期时间防雪崩;4)Redis数据结构的高级应用,如HyperLogLog统计UV和GeoHash处理地理位置。文章提供了详细的代码示例,涵盖Redis在分布式系统中的核心应用场景,特别适合需要处理高并发、分布式锁等问题的开发场景。
157 38
Java Redis 面试题集锦 常见高频面试题目及解析
本文总结了Redis在Java中的核心面试题,包括数据类型操作、单线程高性能原理、键过期策略及分布式锁实现等关键内容。通过Jedis代码示例展示了String、List等数据类型的操作方法,讲解了惰性删除和定期删除相结合的过期策略,并提供了Spring Boot配置Redis过期时间的方案。文章还探讨了缓存穿透、雪崩等问题解决方案,以及基于Redis的分布式锁实现,帮助开发者全面掌握Redis在Java应用中的实践要点。
106 6
Java 集合面试题从数据结构到 HashMap 源码剖析详解及长尾考点梳理
本文深入解析Java集合框架,涵盖基础概念、常见集合类型及HashMap的底层数据结构与源码实现。从Collection、Map到Iterator接口,逐一剖析其特性与应用场景。重点解读HashMap在JDK1.7与1.8中的数据结构演变,包括数组+链表+红黑树优化,以及put方法和扩容机制的实现细节。结合订单管理与用户权限管理等实际案例,展示集合框架的应用价值,助你全面掌握相关知识,轻松应对面试与开发需求。
131 3
Redis中的常用命令-get&set&keys&exists&expire&ttl&type的详细解析
总的来说,这些Redis命令提供了处理存储在内存中的键值对的便捷方式。通过理解和运用它们,你可以更有效地在Redis中操作数据,使其更好地服务于你的应用。
248 17
Redis设计与实现——数据结构与对象
Redis 是一个高性能的键值存储系统,其数据结构设计精妙且高效。主要包括以下几种核心数据结构:SDS、链表、字典、跳跃表、整数集合、压缩列表。此外,Redis 对象通过类型和编码方式动态转换,优化内存使用,并支持引用计数、共享对象和淘汰策略(如 LRU/LFU)。这些特性共同确保 Redis 在性能与灵活性之间的平衡。
解析局域网内控制电脑机制:基于 Go 语言链表算法的隐秘通信技术探究
数字化办公与物联网蓬勃发展的时代背景下,局域网内计算机控制已成为提升工作效率、达成设备协同管理的重要途径。无论是企业远程办公时的设备统一调度,还是智能家居系统中多设备间的联动控制,高效的数据传输与管理机制均构成实现局域网内计算机控制功能的核心要素。本文将深入探究 Go 语言中的链表数据结构,剖析其在局域网内计算机控制过程中,如何达成数据的有序存储与高效传输,并通过完整的 Go 语言代码示例展示其应用流程。
78 0
|
3月前
|
【LeetCode 热题100】23:合并 K 个升序链表(详细解析)(Go语言版)
本文详细解析了 LeetCode 热题 23——合并 K 个升序链表的两种解法:优先队列(最小堆)和分治合并。题目要求将多个已排序链表合并为一个升序链表。最小堆方法通过维护节点优先级快速选择最小值,;分治合并则采用归并思想两两合并链表。文章提供了 Go 语言实现代码,并对比分析两种方法的适用场景,帮助读者深入理解链表操作与算法设计。
123 10
|
4月前
|
【数据结构进阶】红黑树超详解 + 实现(附源码)
本文深入探讨了红黑树的实现原理与特性。红黑树是一种自平衡二叉搜索树,通过节点着色(红/黑)和特定规则,确保树的高度接近平衡,从而实现高效的插入、删除和查找操作。相比AVL树,红黑树允许一定程度的不平衡,减少了旋转调整次数,提升了动态操作性能。文章详细解析了红黑树的性质、插入时的平衡调整(变色与旋转)、查找逻辑以及合法性检查,并提供了完整的C++代码实现。红黑树在操作系统和数据库中广泛应用,其设计兼顾效率与复杂性的平衡。
571 3
【数据结构进阶】AVL树深度剖析 + 实现(附源码)
在深入探讨了AVL树的原理和实现后,我们不难发现,这种数据结构不仅优雅地解决了传统二叉搜索树可能面临的性能退化问题,还通过其独特的平衡机制,确保了在任何情况下都能提供稳定且高效的查找、插入和删除操作。
324 19

推荐镜像

更多
  • DNS
  • AI助理

    你好,我是AI助理

    可以解答问题、推荐解决方案等

    登录插画

    登录以查看您的控制台资源

    管理云资源
    状态一览
    快捷访问