概述
链表可以提供高效节点重排和顺序性节点访问的方式,并且可以通过增删节点灵活的调整链表长度。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设计与实现》——黄健宏