Redis 使用了 C 语言编写,但因 C 语言是没有内置链表这种结构的,所以 Redis 使用了双向链表结构作为自己需要的实现。
众所周知,链表结构的好处在于不需要连续的的内存空间,以及在插入和删除的时间复杂度是 O(1) 级别的,效率较高,但比起数组它的缺点在于,查询效率上没有那么的高。
双向链表内容
Redis 中一个双向链表节点的样子:
具体代码
/* * 双向链表节点 */
typedef struct listNode {
// 前置节点的指针
struct listNode *prev;
// 后置节点的指针
struct listNode *next;
// 节点的值
void *value;
} listNode;
一个节点由头指针 prev ,尾指针 next ,以及节点的值 value (即图片中的 c)组成。
Redis 中一个具体的双向链表结构的样子:
具体代码
/* * 双向链表结构 */
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 函数用于复制双向链表节点所保存的值,free 函数用于释放双向链表节点所保存的值,match 函数用于对比双向链表节点所保存的值和另外一个的输入值是否相等。
双向链表使用优点
- Redis 的双向链表结构,通过前后指针,获取某个节点的前节点和后节点的时间复杂度都是 O(1)
- 通过结构中的 head 指针以及 tail 指针,程序获取双向链表的头节点以及尾节点的时间复杂度都是 O(1)
- 头节点的前置指针以及尾节点的后置指针都是指向 NULL ,对双向链表的访问以 NULL 结束
- 程序通过结构的 len 属性直接获取节点的数量,时间复杂度为 O(1) , 效率高
- 链表节点的值是使用 void* 指针来保存具体的节点值的,而且通过结构中的 dup、free、match 三个属性可以为节点值设置不同的类型,因为他们的入参都有 void* ,这样的设计使 双向链表节点基本能存储各种类型的数据
双向链表实现
创建链表
首先通过 zmolloc 为双向链表结构的 list 分配内存,假如分配内存失败,则返回 NULL ,创建成功之后,为 list 初始化,设置 head、tail 指针指向 NULL 值,以及 dup、free、match 属性设置为 NULL 值,len 属性设置为0 。初始化成功之后,返回 list 。
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;
}
插入节点
首先通过 zmolloc 函数为 node 新节点分配内存,如果分配失败,返回 NULL ,新节点创建成功之后,将 value 值保存到新节点 node 里面,然后根据 after 值,为0将新节点插入到老节点之前,为1则将新节点插入到老节点之后,根据老节点的位置,调整各自前后指针的位置,然后对结构的 len 值加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;
}
删除节点
首先判断需要被删除的节点的前置指针是否存在,存在的话,将该节点的上一个节点的后置指针指向该节点的下一个节点,不存在的话结构的 head 指针将指向该节点的下一个节点。
同理,接着判断被删除的节点的后置指针是否存在,存在的话,将该节点的下一个节点的前置指针指向该节点的上一个节点,不存在的话结构的 tail 指针将指向该节点的上一个节点。
假如 free 节点值释放函数存在,将释放该节点的值,最后通过 zfree 函数释放该节点且将链表的 len 长度减一。
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--;
}
释放链表
首先定义一个当前节点 current 并指向头指针 head ,以及获取一个原来 list 的节点数量 len ,通过 len 遍历每一个节点,使当前的节点 current 的下一个节点被指向定义好的新节点 next ,接着如果节点值释放函数存在,则调用该函数,释放当前节点保存的值,然后再释放该节点结构,此时 current 的节点值为空的了,然后再使 next 对应的节点改为对应 current ,开始新的一波操作,直至 len 属性值为0。最终调用 zfree 函数释放整个链表结构。
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);
}
通过源码上的部分阅读,了解了 Redis 双向链表结构的大概增删操作,具体的可阅读 Redis 源码中的 adlist.c 部分。
双向链表应用场景
Redis 中的发布与订阅、慢查询、监视器、数据结构 List 都用到了双向链表结构,还用链表来存储多个客户端的状态信息、以及用到了链表来构建客户端输出缓冲区。
参考:《 Redis设计与实现 》
更多Java后端开发相关技术,可以关注公众号「 红橙呀 」。