Redis底层链表的源码分析:

一.链表结点的结构(单个结点):

// listNode 双端链表节点
typedef struct listNode { // 前置节点
struct listNode *prev; // 后置节点
struct listNode *next; // 节点的值
void *value; } listNode;

该链表为双向链表,由多个listNode结点组成的链表结构图如下:

二.双端链表的结构:

// list 双端链表
typedef struct list { // 在c语言中,用结构体的方式来模拟对象是一种常见的手法 // 表头节点
listNode *head; // 表尾节点
listNode *tail; // 节点值复制函数
void *(*dup)(void *ptr); // 节点值释放函数
void(*free)(void *ptr); // 节点值对比函数
int(*match)(void *ptr, void *key); // 链表所包含的节点数量
unsigned long len; } list;

这种链表的封装实现方法可以说说极具特色了,由lsit结构和listNode结点组成的链表结构图如下:

其中封装了3个内置函数

1.dup函数:复制链表结点所保存的值

2.free函数:释放链表结点所保存的值

3.match函数:对比链表结点所保存的值和另一个输入值是否相等

这三个函数是用于实现多态链表所需的类型特定函数

三.Redis链表的特性:

1.双端:获取某个结点的前驱和后继结点都是O(1)

2.无环:表头的prev指针和表尾的next指针都指向NULL,对链表的访问都是以NULL为终点

3.带表头指针和表尾指针:获取表头和表尾的复杂度都是O(1)

4.带链表长度计数器:len属性记录,获取链表长度O(1)

5.多态:链表结点使用void*指针来保存结点的值,并且可以通过链表结构的三个函数为结点值设置类型特定函数,所以链表可以保存各种不同类型的值

四.链表和链表结点的API

ps:下面这些API中出现的zmalloc函数为内存分配模块函数,暂不探究

先讲一下根据list结构宏定义实现的函数:

/* 作为宏实现的函数 */
//获取长度
#define listLength(l) ((l)->len)
//获取头节点
#define listFirst(l) ((l)->head)
//获取尾结点
#define listLast(l) ((l)->tail)
//获取前一个结点
#define listPrevNode(n) ((n)->prev)
//获取后一个结点
#define listNextNode(n) ((n)->next)
//获取结点的值 是一个void类型指针
#define listNodeValue(n) ((n)->value) /* 下面3个函数主要用来设置list结构中3个函数指针,参数m为method的意思 */
#define listSetDupMethod(l,m) ((l)->dup = (m))
#define listSetFreeMethod(l,m) ((l)->free = (m))
#define listSetMatchMethod(l,m) ((l)->match = (m)) /* 下面3个函数主要用来获取list结构的单个函数指针 */
#define listGetDupMethod(l) ((l)->dup)
#define listGetFree(l) ((l)->free)
#define listGetMatchMethod(l) ((l)->match)

ok,继续讲具体的API

1)listCreate函数:创建一个不包含任何结点的新链表

/*
* listCreate 创建一个新的链表
*
* 创建成功返回链表,失败返回 NULL 。
*
* T = O(1)
*/
list *listCreate(void)
{
struct list *list; // 分配内存
if ((list = zmalloc(sizeof(*list))) == NULL)
return NULL;//内存分配失败则返回NULL // 初始化属性
list->head = list->tail = NULL;//空链表
list->len = ;
list->dup = NULL;
list->free = NULL;
list->match = NULL; return list;
}

注意创建的是不含任何结点的空链表,内存分配失败返回NULL

2)listAddNodeHead函数:将一个包含给定值的新结点添加到给定链表的表头

/*
* listAddNodeHead 将一个包含有给定值指针 value 的新节点添加到链表的表头
*
* 如果为新节点分配内存出错,那么不执行任何动作,仅返回 NULL
*
* 如果执行成功,返回传入的链表指针
*
* T = O(1)
*/
list *listAddNodeHead(list *list, void *value)
{
listNode *node; // 为节点分配内存
if ((node = zmalloc(sizeof(*node))) == NULL)
return NULL; // 保存值指针
node->value = value; // 添加节点到空链表
if (list->len == ) {
list->head = list->tail = node;
//该结点的前驱和后继都为NULL
node->prev = node->next = NULL;
}
else { // 添加节点到非空链表
node->prev = NULL;
node->next = list->head;
list->head->prev = node;
list->head = node;
} // 更新链表节点数
list->len++; return list;
}

就是将一个给定的结点插入到指定链表的表头

3)listAddNodeTail函数:将一个包含给定值的新结点插入到给定链表的表尾

/*
* listAddNodeTail 将一个包含有给定值指针 value 的新节点添加到链表的表尾
*
* 如果为新节点分配内存出错,那么不执行任何动作,仅返回 NULL
*
* 如果执行成功,返回传入的链表指针
*
* T = O(1)
*/
list *listAddNodeTail(list *list, void *value)
{
listNode *node; // 为新节点分配内存
if ((node = zmalloc(sizeof(*node))) == NULL)
return NULL; // 保存值指针
node->value = value; // 目标链表为空
if (list->len == ) {
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;
}

4)listInsertNode函数:将一个给定值的新结点插入到给定结点之前或者之后

/*
* listInsertNode 创建一个包含值 value 的新节点,并将它插入到 old_node 的之前或之后
*
* 如果 after 为 0 ,将新节点插入到 old_node 之前。
* 如果 after 为 1 ,将新节点插入到 old_node 之后。
*
* T = O(1)
*/
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;
}

5)listDelNode函数:从指定的list中删除给定的结点

/*
* listDelNode 从链表 list 中删除给定节点 node
*
* 对节点私有值(private value of the node)的释放工作由调用者进行。该函数一定会成功.
*
* T = O(1)
*/
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--;
}

6)listRelease函数:释放给定链表以及链表中所有结点

/*
* listRelease 释放整个链表,以及链表中所有节点, 这个函数不可能会失败.
*
* T = O(N)
*/
void listRelease(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;
} // 释放链表结构
zfree(list);
}

该函数不仅释放了表结点的内存还释放了表结构的内存!

7)listGetIterator函数:为给定链表创建一个迭代器

在讲这个函数之前,我们应该先看看链表迭代器的结构:

// listIter 双端链表迭代器
typedef struct listIter { // 当前迭代到的节点
listNode *next; // 迭代的方向
int direction; } listIter;

迭起器只有两个重要的属性:当前迭代到的结点,迭代的方向

下面再看看链表的迭代器创建函数

/*
* listGetIterator 为给定链表创建一个迭代器,
* 之后每次对这个迭代器调用 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;
}

8)listReleaseIterator函数:释放指定的迭代器

/*
* listReleaseIterator 释放迭代器
*
* T = O(1)
*/
void listReleaseIterator(listIter *iter) {
zfree(iter);
}

9)listRewind函数和listRewindTail函数:迭代器重新指向表头或者表尾的函数

/*
* 将迭代器的方向设置为 AL_START_HEAD,
* 并将迭代指针重新指向表头节点。
*
* T = O(1)
*/
void listRewind(list *list, listIter *li) {
li->next = list->head;
li->direction = AL_START_HEAD;
} /*
* 将迭代器的方向设置为 AL_START_TAIL,
* 并将迭代指针重新指向表尾节点。
*
* T = O(1)
*/
void listRewindTail(list *list, listIter *li) {
li->next = list->tail;
li->direction = AL_START_TAIL;
}

10)listNext函数:返回当前迭代器指向的结点

/*
* 返回迭代器当前所指向的节点。
*
* 删除当前节点是允许的,但不能修改链表里的其他节点。
*
* 函数要么返回一个节点,要么返回 NULL,常见的用法是:
*
* iter = listGetIterator(list,<direction>);
* while ((node = listNext(iter)) != NULL) {
* doSomethingWith(listNodeValue(node));
* }
*
* T = O(1)
*/
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;
}

该函数保持了当前结点的下一个结点,避免了当前结点被删除而迭代器无法继续迭代的尴尬情况

11)listDup函数:复制整个链表,返回副本

/*
* 复制整个链表。
*
* 复制成功返回输入链表的副本,
* 如果因为内存不足而造成复制失败,返回 NULL 。
*
* 如果链表有设置值复制函数 dup ,那么对值的复制将使用复制函数进行,
* 否则,新节点将和旧节点共享同一个指针。
*
* 无论复制是成功还是失败,输入节点都不会修改。
*
* T = O(N)
*/
list *listDup(list *orig)
{
list *copy;//链表副本
listIter *iter;//链表迭代器
listNode *node;//链表结点 // 创建新的空链表
if ((copy = listCreate()) == NULL)
return NULL;//创建空的链表失败则返回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;
}

如果复制失败则要注意释放副本链表和迭代器,避免内存泄漏

12)listSearchKey函数:查找list中值和key匹配的结点

/*
* 查找链表 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;
}
 
13)listIndex函数:返回链表在给定索引上的值
/*
* 返回链表在给定索引上的值。
*
* 索引以 0 为起始,也可以是负数, -1 表示链表最后一个节点,诸如此类。
*
* 如果索引超出范围(out of range),返回 NULL 。
*
* T = O(N)
*/
listNode *listIndex(list *list, long index) { listNode *n;//链表结点 /* n不用设置成NULL的原因:
如果索引超出范围,
那肯定是找到表头或者表尾没有找到,
表头的前驱和表尾的后继都是NULL,
所以这里n不用设置为NULL,直接设置也可以*/ // 如果索引为负数,从表尾开始查找
if (index < ) { //变成正数,方便索引
index = (-index) - ; //从尾部开始找
n = list->tail; //寻找 因为从尾部开始找,所以是前驱
while (index-- && n) n = n->prev; } // 如果索引为正数,从表头开始查找
else { //从头部开始找
n = list->head; //寻找 因为从头部开始找,所以是后继
while (index-- && n) n = n->next;
} return n;
}

14)listRotate函数:取出链表的表尾结点放到表头,成为新的表头结点

/*
* 取出链表的表尾节点,并将它移动到表头,成为新的表头节点。
*
* T = O(1)
*/
void listRotate(list *list) { //表尾结点
listNode *tail = list->tail; //如果链表中只有一个元素,那么表头就是表尾,可以直接返回
if (listLength(list) <= ) return; // 重新设置表尾节点
list->tail = tail->prev;
list->tail->next = NULL; // 插入到表头
list->head->prev = tail;
tail->prev = NULL;
tail->next = list->head;
list->head = tail;
}

总结:Redis内置的链表实现还是很简单的,代码也不多,但是功能都有,值得学习

参考:《Redis设计与实现》

Redis学习之底层链表源码分析的更多相关文章

  1. Redis学习之对象系统源码分析

    背景知识: Redis并没有直接使用sds,双端链表,字典,压缩列表,跳表等这些数据结构来直接实现键值对数据库,而是基于这些对象创建了一个对象系统,这个对象系统包含5个对象:字符串对象,列表对象,哈希 ...

  2. memcached学习笔记——存储命令源码分析下篇

    上一篇回顾:<memcached学习笔记——存储命令源码分析上篇>通过分析memcached的存储命令源码的过程,了解了memcached如何解析文本命令和mencached的内存管理机制 ...

  3. memcached学习笔记——存储命令源码分析上篇

    原创文章,转载请标明,谢谢. 上一篇分析过memcached的连接模型,了解memcached是如何高效处理客户端连接,这一篇分析memcached源码中的process_update_command ...

  4. 图解Janusgraph系列-图数据底层序列化源码分析(Data Serialize)

    图解Janusgraph系列-图数据底层序列化源码分析(Data Serialize) 大家好,我是洋仔,JanusGraph图解系列文章,实时更新~ 图数据库文章总目录: 整理所有图相关文章,请移步 ...

  5. Redis 专栏(使用介绍、源码分析、常见问题...)

    一.介绍相关 说Redis : 介绍Redis特性,使用场景,使用Jedis操作Redis等. 二.源码分析 1. 数据结构 Redis源码分析(sds):Redis自己封装的C语言字符串类型. Re ...

  6. tornado 学习笔记6 Application 源码分析

    Application 是Tornado重要的模块之一,主要是配置访问路由表及其他应用参数的设置. 源代码位于虚拟运行环境文件夹下(我的是env),具体位置为env > lib>sit-p ...

  7. EasyUI学习总结(三)——easyloader源码分析(转载)

    声明:这一篇文章是转载过来的,转载地址忘记了,原作者如果看到了,希望能够告知一声,我好加上去! easyloader模块是用来加载jquery easyui的js和css文件的,而且它可以分析模块的依 ...

  8. 正式学习React(五) react-redux源码分析

    磨刀不误砍柴工,咱先把react-redux里的工具函数分析一下: 源码点这里  shallowEqual.js export default function shallowEqual(objA, ...

  9. 正式学习React (七) react-router 源码分析

    学习react已经有10来天了,对于react redux react-redux 的使用流程和原理,也已经有一定的了解,在我上一篇的实战项目里,我用到了react-route,其实对它还只是 停留在 ...

随机推荐

  1. JMeter Dubbo请求插件jmeter-plugin-dubbo.jar

    JMeter Dubbo请求插件jmeter-plugin-dubbo.jar   by:授客 QQ:1033553122 测试环境 apache-jmeter-3.2 Dubbo  2.6.2 声明 ...

  2. c++函数集锦

    1.标准C++库字符串类std::string的用法 begin       得到指向字符串开头的Iterator end       得到指向字符串结尾的Iterator rbegin        ...

  3. mysql的数据类型和字段属性

    本文内容: 数据类型 数值类型 整数型 浮点型 定点型 日期时间类型 字符串类型 补充: 显示宽度与zerofll 记录长度 字段属性 空\不为空值:NULL.NOT NULL 主键:primary ...

  4. Docker Data Center系列(四)- 离线安装UCP和DTR

    本系列文章演示如何搭建一个mini的云平台和DevOps实践环境. 基于这套实践环境,可以部署微服务架构的应用栈,演练提升DevOps实践能力. 1 离线安装UCP 1.1 可用版本 Version ...

  5. Microsoft Teams 集成 (协作, 沟通 和 行为)

    Microsoft Teams 集成 (协作, 沟通 和 行为) 概述 Microsoft Teams是在Office 365中以chat为中心的工作空间.软件开发团队可以快速获得在一个专门的团队协作 ...

  6. Spark操作parquet文件

    package code.parquet import java.net.URI import org.apache.hadoop.conf.Configuration import org.apac ...

  7. Linux 防火墙

    目录 iptables配置 1. iptables 控制类型 2. 链表规则 3. iptables表 CentOS和RedHat 6.x CentOS和RedHat 7.x ufw ubuntu u ...

  8. python第一百零八天---Django 3 session 操作

    上节内容回顾: 1.请求周期 url> 路由 > 函数或类 > 返回字符串或者模板语言? Form表单提交: 提交 -> url > 函数或类中的方法 - .... Ht ...

  9. f.lux 自动调节显示器色温

    我的环境 f.lux 我的使用感受是让屏幕看起来舒服一些,因为我有近视,所以需要保护眼睛. f.lux官网:https://justgetflux.com/ f.lux v4.47 windows 1 ...

  10. 修改linux 默认SHELL

    首先你得查看可以用的shell: 1.命令:chsh -l ,结果如下: /bin/sh/bin/bash/sbin/nologin/usr/bin/sh/usr/bin/bash/usr/sbin/ ...