Redis 5.0数据结构之双端链表linkedlist源码解析

本文详细介绍了Redis 5.0中双端链表linkedlist的数据结构和主要函数,包括listNode节点、list结构、链表创建、回收、迭代器操作、添加、删除、查找及复制等。通过对源码的解析,阐述了Redis如何实现高效的操作。
摘要由CSDN通过智能技术生成

概述

链表可以提供高效节点重排和顺序性节点访问的方式,并且可以通过增删节点灵活的调整链表长度。Redis基于C语言开发,C语言并未内置链表,因此Redis自己实现了一系列链表,分别是双端链表linkedlist、压缩链表ziplist、快速链表quicklist等,本篇将从源码角度展现双端链表linkedlist。

Redis 中的list实现

众所周知Redis有五种数据类型,链表是其中的一种,在Redis在3.2版本,修改了list的底层实现,在Redis 3.2之前链表是由双端链表linkedlist和压缩链表ziplist共同实现的,Redis3.2版本之后主要实现优化为了快速链表quicklist。

Redis3.2之前,当同时满足以下两个条件时,list的实现为ziplist,其余情况均使用linkedlist

  • 哈希对象保存的所有键值的字符串长度小于64字节;
  • 哈希对象保存的键值对数量小于512个;

本篇使用源码版本均为Redis 5.0,主要介绍的双端链表linkedlist源码均来自adlist.h和adlist.c两个文件。

linkedlist数据结构

linkedlist结构源码

对于双端链表中的节点,使用adlist.h/listNode结构来进行表示,源码如下:

typedef struct listNode {
	// 前置节点
    struct listNode *prev;
    // 后置节点
    struct listNode *next;
    // 值
    void *value;
} listNode;

可以很明显的看到每个节点都可以直接访问到其前或后位的节点,故Redis里list的实现是双端链表。

由若干listNode节点组成的双端链表示意图如下:
在这里插入图片描述
————————————————————————————————————————

对于双端链表,使用adlist.h/list结构来表示,源码如下:

typedef struct list {
	// 头节点
    listNode *head;
    // 尾节点
    listNode *tail;
    // 节点值复制函数
    void *(*dup)(void *ptr);
    // 节点值释放函数
    void (*free)(void *ptr);
    // 节点值对比函数
    int (*match)(void *ptr, void *key);
    // 链表所包含节点数量
    unsigned long len;
} list;

list结构为链表提供了头指针head和尾指针tail以及链表长度len。对于三个内置函数,分别表示:

  • dup函数用于复制链表节点所保存的value值。
  • free函数用于释放链表节点所保存的value值。
  • match函数用于对比链表节点所保存的value值与另一个输入值是否相等。

一个包含了三个listNode节点的链表list结构示意图如下:
在这里插入图片描述
————————————————————————————————————————
双端链表迭代器结构如下:

typedef struct listIter {
    listNode *next;
    int direction;
} listIter;

链表迭代时有从头到尾和从尾到头两种策略,通过direction属性表示,当其值为0时表示从头到尾,当其值为1时表示从尾到头。

主要函数

首先在adlist.h中为开发者提供了一系列的宏定义函数,以便结构体相关操作,源码如下:

/* Functions implemented as macros */
#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) // 获取当前节点的后一个节点
#define listNodeValue(n) ((n)->value) // 获取当前节点value值

#define listSetDupMethod(l,m) ((l)->dup = (m)) // 设定dup函数
#define listSetFreeMethod(l,m) ((l)->free = (m)) // 设定free函数
#define listSetMatchMethod(l,m) ((l)->match = (m)) // 设定match函数

#define listGetDupMethod(l) ((l)->dup) // 获取dup函数
#define listGetFree(l) ((l)->free) // 获取free函数
#define listGetMatchMethod(l) ((l)->match) //获取match函数

链表创建

源码实现非常简单,开辟空间,为各属性赋初值。

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

    if ((list = zmalloc(sizeof(*list))) == NULL)
        return NULL;
    list->head = list->tail = NULL;
    list->len = 0;
    list->dup = NULL;
    list->free = NULL;
    list->match = NULL;
    return list;
}

链表回收

对于链表的回收/删除,Redis提供了两个函数,分别是listEmpty和listRelease,前者释放链表内所有节点,但不回收list本身的空间,后者则全部释放

void listEmpty(list *list)
{
    unsigned long len;
    listNode *current, *next;

    current = list->head;
    len = list->len;
    // 通过计数器自减判0 依次释放节点空间
    while(len--) {
        next = current->next;
        // 如果有特别定义的链表释放函数 则先调用
        if (list->free) list->free(current->value);
        // 释放节点
        zfree(current);
        current = next;
    }
    list->head = list->tail = NULL;
    list->len = 0;
}

void listRelease(list *list)
{	
	// 重用listEmpty函数后释放链表本身
    listEmpty(list);
    zfree(list);
}

迭代器操作

迭代器相关实现简单,详见注释,相关函数源码如下:

// 获取迭代器
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;
}
// 迭代器释放
void listReleaseIterator(listIter *iter) {
    zfree(iter);
}

// 正向重置迭代器
void listRewind(list *list, listIter *li) {
    li->next = list->head;
    li->direction = AL_START_HEAD;
}
// 逆向重置迭代器
void listRewindTail(list *list, listIter *li) {
    li->next = list->tail;
    li->direction = AL_START_TAIL;
}
// 获取下一个节点
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;
}

添加操作

在Redis实际使用链表的过程中,开发人员一定会用到lpush和rpush指令,其功能分别由以下两个函数实现,由于是双端队列,可以很简单的获取头节点和尾节点,节点也可以访问其前后节点,因此lpush和rpush使用的同一套逻辑,相关函数源码如下:

// 新增一个节点最为链表的头节点
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 {
    	// 分别改变原头节点 新增节点 head指针的指向
        node->prev = NULL;
        node->next = list->head;
        list->head->prev = node;
        list->head = node;
    }
    // 计数器自增
    list->len++;
    return list;
}
// 新增一个节点最为链表的尾节点
// 逻辑与listAddNodeHead相同 只不过是操作tail指针和尾节点
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;
}

————————————————————————————————————————
除了push操作,还可以使用linsert插入操作新增节点,实际使用时语法为:
linsert key before|after value1 value2
语义为在链表中值为value1的节点之前|之后插入一个值为value2的节点,如果链表中不存在值为value1的节点,则不操作。下面展示的函数负责插入操作:

// 传入旧节点 将要插入的节点的值 以及插入位置
// 其中 after为0时表示向前插入 after为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;
}

删除操作

删除操作由listDelNode函数实现,通过传入目标节点而不是目标值实现。

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

查找操作

Redis内置了两种节点查找函数,分别是listSearchKey函数和listIndex函数,前者通过值查找,后者通过序号查找,即查找链表中的第某个节点。

// 通过值查找
listNode *listSearchKey(list *list, void *key)
{
	// 使用迭代器遍历 初始获取迭代器后默认正序迭代
    listIter iter;
    listNode *node;

    listRewind(list, &iter);
    while((node = listNext(&iter)) != NULL) {
    	// 如果有特殊定义的节点匹配函数 则调用
        if (list->match) {
            if (list->match(node->value, key)) {
                return node;
            }
        // 否则进行普通的value值比较
        } else {
            if (key == node->value) {
                return node;
            }
        }
    }
    // 找不到目标节点返回NULL
    return NULL;
}
// 通过序号查找
// 此处index有两种表示 当index值为正时表示正序从头到尾查找 反之倒序查找
listNode *listIndex(list *list, long index) {
    listNode *n;
	// 倒序查找
    if (index < 0) {
    	// index值不会为0 如果index是-1 则表示尾节点
        index = (-index)-1;
        n = list->tail;
        while(index-- && n) n = n->prev;
    // 正序查找
    } else {
        n = list->head;
        while(index-- && n) n = n->next;
    }
    return n;
}

链表复制

Redis提供了链表复制函数listDup,函数源码如下:

// 整个链表复制
list *listDup(list *orig)
{
    list *copy;
    listIter iter;
    listNode *node;

    if ((copy = listCreate()) == NULL)
        return NULL;
    // 相关操作函数复制
    copy->dup = orig->dup;
    copy->free = orig->free;
    copy->match = orig->match;
    listRewind(orig, &iter);
    while((node = listNext(&iter)) != NULL) {
        void *value;
		// 如果特别定义了dup复制函数 则执行
        if (copy->dup) {
            value = copy->dup(node->value);
            if (value == NULL) {
                listRelease(copy);
                return NULL;
            }
        // 否则直接值复制
        } else
            value = node->value;
        // 向尾部添加节点
        if (listAddNodeTail(copy, value) == NULL) {
            listRelease(copy);
            return NULL;
        }
    }
    return copy;
}

部分内容和图片参考/摘自:《Redis设计与实现》——黄健宏

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

7rulyL1ar

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值