前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Redis | 源码阅读 —— 链表

Redis | 源码阅读 —— 链表

作者头像
码农UP2U
发布2020-09-22 15:27:46
4100
发布2020-09-22 15:27:46
举报
文章被收录于专栏:码农UP2U码农UP2U

本文是参考 黄建宏 先生所写的 《Redis 设计与实现》 一书而来的,在此感谢 黄建宏 先生能写出这么优秀的书籍。本次来整理关于链表相关的数据结构。

链表不但是一种存储结构(数据结构的存储不外乎是顺序存储和链式存储两种),也是一种数据结构,它和数组相比,它的优点是方便节点的插入与节点的删除。在 Redis 中也存在链表这种数据结构,关于链表数据结构的定义和操作包含在 adlist.h 和 adlist.c 两个文件中。

链表相关数据结构

在 Redis 的源码中,链表的数据结构和相关的操作都包含在 adlist.h 和 adlist.c 两个文件当中,这两个文件是 Redis 中对底层链表的所有实现。在 adlist.h 文件中,有两个关于链表的重要数据结构,一个是双向链表的数据结构,另外一个是用于管理双向链表的数据结构。

双向链表的数据结构定义如下:

代码语言:javascript
复制
/**
 * 双向链表
 */
typedef struct listNode {
    // 前驱节点指针
    struct listNode *prev;
    // 后继节点指针
    struct listNode *next;
    // 数据域指针
    // 数据域的指针是void类型的指针,说明该链表可以保存各种类型的数据
    void *value;
} listNode;

除了定义了基本的 listNode 数据结构外,还有一个用于管理双向链表的数据结构,定义如下:

代码语言:javascript
复制
/**
 * 对listNode节点的管理
 */
typedef struct list {
    // 指向listNode的头节点
    listNode *head;
    // 指针listNode的尾节点
    listNode *tail;
    // 节点复制函数
    void *(*dup)(void *ptr);
    // 节点释放函数
    void (*free)(void *ptr);
    // 节点比对函数
    int (*match)(void *ptr, void *key);
    // 管理的listNode节点的数量
    unsigned long len;
} list;

双向链表的定义是 listNode,其中包含 prev 和 next 两个指针,分别用来保存其前驱节点与后继节点,而它的节点值 val 是一个 void * 的类型,void * 这种类型在 C 语言当中算是一个 “万能” 类型,使用强制类型转换,可以将 void * 这种类型转换为任意的类型,也就是 listNode 数据结构的节点值可以保存任意的数据类型。

Redis 除了 listNode 还定义了 list 的数据结构,该数据结构是用于管理双向链表的数据结构,该数据结构中保存了所管理的 listNode 链的头节点位置、尾节点位置和管理的 listNode 链节点的数量,通过 list 数据结构可以快速定位到链表的表头、表尾。对于 Redis 的 list(列表) 这种数据类型(这里说的是我们操作 Redis 时的 list 数据类型,此 list 非数据结构的 list,文中混用较多,请根据上下文理解),就是使用 链表 这种数据结构,而 list 可以通过 正负索引 来定位其中的元素,那么通过 list 数据结构的 head 和 tail 就可以方便的定位到 list(列表) 数据类型的开始和结尾了。

了解过数据结构的同学都知道,链表的长度需要通过遍历链表的每个节点可以累加得到,这样它的时间复杂度为O(n),而 Redis 的 list 中通过 len 保存了 listNode 链的节点的数量,这样将链表长度计算的时间复杂度从 O(n) 变为了 O(1)。

在 list 数据结构中保存着三个 函数指针,分别是 dup、free 和 match,它们的作用是用来保存链表的节点复制函数、节点释放函数和节点比对函数的地址。前面已经说过,listNode 的值是 void* 类型,也就是可以保存不同的数据类型,比如 char*、int、double,甚至可以是复杂的数据结构,这样不同的数据类型,在进行节点复制、节点释放和节点比较时,肯定使用不同的方式进行比较,因此 dup、free 和 match 根据不同的数据类型存放不同的数据类型的复制、释放和比对函数的地址。

链表管理操作的宏定义

链表的很多管理函数都是使用宏定义完成的,比如对 list 数据结构中成员的设置和获取,宏定义如下:

代码语言:javascript
复制
/* 返回list的长度 */
#define listLength(l) ((l)->len)
/* 返回list指向listNode的头指针 */
#define listFirst(l) ((l)->head)
/* 返回list指向listNode的尾指针 */
#define listLast(l) ((l)->tail)
/* 返回list指向的当前节点的前一个节点 */
#define listPrevNode(n) ((n)->prev)
/* 返回list指向的当前节点的后一个节点 */
#define listNextNode(n) ((n)->next)
/* 返回list指向的当前节点的值 */
#define listNodeValue(n) ((n)->value)

再比如对 dup、free 和 match 的 getter 和 setter 的函数也是相关的宏定义,如下所示:

代码语言:javascript
复制
/* 将指定函数设置为list的赋值函数 */
#define listSetDupMethod(l,m) ((l)->dup = (m))
/* 将指定函数设置为list的释放函数 */
#define listSetFreeMethod(l,m) ((l)->free = (m))
/* 将指定函数设置为list的比对函数 */
#define listSetMatchMethod(l,m) ((l)->match = (m))


/* 返回list的赋值函数 */
#define listGetDupMethod(l) ((l)->dup)
/* 返回list的释放函数 */
#define listGetFree(l) ((l)->free)
/* 返回list的比对函数 */
#define listGetMatchMethod(l) ((l)->match)

上面的函数都是针对 list 结构体都操作,而 list 数据结构是用于管理 nodeList 数据结构的,真正用于存储 list(列表)数据的是 nodeList 数据结构,而关于操作 listNode 数据结构的函数是真正的链表操作函数。

操作 nodeList 数据结构的函数

操作 nodeList 数据结构的函数是一些 C 语言的函数,而不再是宏定义,这些函数基本上完成了关于链表增删改查的全部操作。

代码语言:javascript
复制
/* 创建一个不包含任何节点的链表 */
list *listCreate(void);
/* 释放指定的list */
void listRelease(list *list);
/* 将值插入到链表的头部 */
list *listAddNodeHead(list *list, void *value);
/* 将值插入到链表的尾部 */
list *listAddNodeTail(list *list, void *value);
/* 将新的listNode插入到list中指定节点前面或后面 */
list *listInsertNode(list *list, listNode *old_node, void *value, int after);
/* 删除list中的指定listNode */
void listDelNode(list *list, listNode *node);
/* 对指定链表创建副本 */
list *listDup(list *orig);
/* 查找指定的key,并返回listNode的指针 */
listNode *listSearchKey(list *list, void *key);
/* 返回指定索引的listNode的指针 */
listNode *listIndex(list *list, long index);
/* 尾节点变为新的头节点 */
void listRotate(list *list);

以上的函数包含了链表的大部分操作,我们来具体看其中几个函数。

无环链表

Redis 的链表是无环的双向链表,这点可以通过 Redis 插入头节点和插入尾节点的函数看出,两个函数代码如下:

代码语言:javascript
复制
/**
 * 将值插入到链表的头部
 */
list *listAddNodeHead(list *list, void *value)
{
    listNode *node;

    if ((node = zmalloc(sizeof(*node))) == NULL)
        return NULL;
    node->value = value;
    if (list->len == 0) {
        // list的头尾都指向node
        list->head = list->tail = node;
        // node的前驱和后继都为空
        node->prev = node->next = NULL;
    } else {
        // 头节点没有前驱
        node->prev = NULL;
        // 头节点的后继为当前list的头节点
        node->next = list->head;
        // 当前头节点的前驱为新节点
        list->head->prev = node;
        // 修正新的头节点为新的node
        list->head = node;
    }
    // 增加list的长度
    list->len++;
    return list;
}
/**
 * 将值插入到链表的尾部
 */
list *listAddNodeTail(list *list, void *value)
{
    listNode *node;

    if ((node = zmalloc(sizeof(*node))) == NULL)
        return NULL;
    node->value = value;
    if (list->len == 0) {
        // list的头尾追昂node
        list->head = list->tail = node;
        // node的前驱和后继都为空
        node->prev = node->next = NULL;
    } else {
        // 当前节点的前驱为list的尾节点
        node->prev = list->tail;
        // 当前节点的后继为空
        node->next = NULL;
        // list的尾节点的后继为新节点
        list->tail->next = node;
        // 修正新的尾节点为新的node
        list->tail = node;
    }
    // 增加list的长度
    list->len++;
    return list;
}

可以看到,头节点的前驱节点是 NULL,而尾节点的后继节点也是 NULL,这说明 Redis 的链表是无环的链表,也就是首尾没有相连。

迭代器

链表节点的复制、搜索都会涉及到链表的遍历,链表的遍历使用了迭代器的数据结构。迭代器的数据结构如下:

代码语言:javascript
复制
/**
 * 迭代器
 */
typedef struct listIter {
    // 下一个节点的指针
    listNode *next;
    // 控制方向
    int direction;
} listIter;

迭代器数据结构中的 next 保存了下一个节点的指针,因为它是双向链表,因此这里的 next 可能保存的是 listNode 中的 prev 或者 next 的指针。具体是 prev 还是 next 是由 direction 的来控制着。看代码,如下:

代码语言:javascript
复制
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;
}

获取迭代器的指针中会对 direction 进行判断,direction 的值由两个宏进行定义,定义如下:

代码语言:javascript
复制
/* Directions for iterators */
#define AL_START_HEAD 0
#define AL_START_TAIL 1

关于迭代器的代码,就不在进行列举了。

最后

上面就是关于 Redis 中链表实现的代码了。Redis 的链表会用在包括但不限于 list(列表)的场景,比如发布订阅、慢查询等也会使用列表的数据结构。

喝水不忘打井人,没有黄老师的书籍做我 Redis 学习的指路明灯,恐怕对于学习 Redis 的源码我会艰难万分。再次感谢黄老师写的关于 Redis 的书籍,能够让好的技术遍地开花。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-09-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码农UP2U 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档