双向链表为redis列表类型的实现方法之一,列表类型实现除了用到双向链表,还有压缩列表。因为双向链表占用内存较多,所以redis优先采用压缩列表来实现自己的列表类型。压缩列表后续分析,先看看双向链表的代码。
双向链表作为一个基本的数据结构,在很多书上都会有描述了,下面就一个个函数看看redis的双向链表的实现方法。文件参见src/adlist.c和src/adlist.h。关于这个命名是双向链表还是双端链表在huangz1990博客上还有争议,哈哈,我就先不管这些概念上的问题了。很多内容参考:http://www.redisbook.com/en/latest/internal-datastruct/adlist.html
先看下adlist.h中双向链表的节点定义:
/* * 链表节点 */ typedef struct listNode { // 前驱节点 struct listNode *prev; // 后继节点 struct listNode *next; // 值 void *value; } listNode;
节点listNode定义很常规,一个前驱节点指针,一个后继节点指针,一个值的指针,值的类型为void *,可以存储各种类型。
双向链表本身的定义如下:
/* * 链表 */ typedef struct list { // 头指针 listNode *head; // 尾指针 listNode *tail; // 节点数量 unsigned long len; // 复制函数 void *(*dup)(void *ptr); // 释放函数 void (*free)(void *ptr); // 匹配函数 int (*match)(void *ptr, void *key); } list;
即定义了链表头指针,尾指针,长度以及复制,释放和匹配的函数指针。
实际的链表结构图如下:
图1 redis双向链表结构示意图(取自huangz1990博客)
正是因为有表头、表尾指针以及链表长度记录,所以redis在表头和表尾插入数据都非常方便,时间复杂度为O(1),这也是lpush,lpop,llen等命令效率较高的原因。
/* * 创建一个新链表 * * 创建成功时返回链表,创建失败返回 NULL * */ 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; }
创建链表即是分配struct list的大小的内存,并初始化链表节点数目为0、链表头尾指针和基本函数指针为NULL。
/* * 释放整个链表(以及列表包含的节点) * * T = O(N),N 为链表的长度 */ void listRelease(list *list) { unsigned long len; listNode *current, *next; current = list->head; len = list->len; while(len--) { next = current->next; // 如果链表有自带的 free 方法,那么先对节点值调用它 if (list->free) list->free(current->value); // 之后再释放节点 zfree(current); current = next; } zfree(list); //释放链表本身的管理结构的内存 }
释放列表即从表头根据链表节点数目遍历,并依次释放各个节点。如果链表定义了free方法,则先释放节点值占用的内存。最后,释放链表本身的管理结构的内存。
/* 新建一个包含给定 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 == 0) { // 第一个节点 list->head = list->tail = node; node->prev = node->next = NULL; } else { // 不是第一个节点 node->prev = NULL; node->next = list->head; list->head->prev = node; list->head = node; } list->len++; return list; }
添加节点的代码很简洁。首先分配内存空间,然后设置节点值为指定的值。然后建立链接关系:如果是第一个节点,直接设置表头和表尾指针为新加入节点;否则,设置新加入节点与原来的头节点的链接关系,并设定头节点为新加入的节点。最后更新链表节点数目,并返回传入的链表。
/* * 新建一个包含给定 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 == 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; }
与表头添加节点类似,只是添加在了链表尾部而已。
/* * 创建一个包含值 value 的节点 * 并根据 after 参数的指示,将新节点插入到 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) { // 插入到 old_node 之后 node->prev = old_node; node->next = old_node->next; // 处理表尾节点 if (list->tail == old_node) { list->tail = node; } } else { // 插入到 old_node 之前 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; }
这里需要注意的是处理表头和表尾节点,以及后面更新前置节点和后继节点指针部分。
/* * 删除链表中指定的节点 * 清除节点私有值(private value)的工作由调用者完成 * * 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--; }
Redis为双向链表还实现了一个迭代器,可以从两个方向迭代链表。迭代器定义如下:
/* * 链表迭代器 */ typedef struct listIter { // 下一节点 listNode *next; // 迭代方向 int direction; } listIter;
direction如果设定为AL_START_HEAD(0),则从表头到表尾迭代。
direction如果设定为AL_START_TAIL(1),则从表尾向表头迭代。
/* * 这个函数不处理失败情形 * * 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; }
/* * 释放迭代器 iter * * T = O(1) */ void listReleaseIterator(listIter *iter) { zfree(iter); } /* * 将迭代器 iter 的迭代指针倒回 list 的表头 * * T = O(1) */ void listRewind(list *list, listIter *li) { li->next = list->head; li->direction = AL_START_HEAD; } /* * 将迭代器 iter 的迭代指针倒回 list 的表尾 * * T = O(1) */ void listRewindTail(list *list, listIter *li) { li->next = list->tail; li->direction = AL_START_TAIL; }
/* * 返回迭代器的当前节点 * * 可以使用 listDelNode() 删除当前节点,但是不可以删除其他节点。 * * 函数要么返回当前节点,要么返回 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; }
迭代器在迭代过程中,根据迭代方向,每次更新next指针即可。