双端链表在Redis中的地位:它作为一种通用数据结构,在Redis的内部使用非常多。是Redis列表结构的底层实现之一,也被大量Redis模块使用,用于构建其他功能。
1、双端链表的定义
Redis双端列表的定义可以参看 adlist.h 和 adlist.c 两个文件。
与双链表定义一致,引入了链表节点,并在此基础上增加头尾节点构建双端链表。
链表节点如下定义:
/* Node, List, and Iterator are the only data structures used currently. */
/*
* 链表节点
*/
typedef struct listNode {
// 前驱节点
struct listNode *prev;
// 后继节点
struct listNode *next;
// 值
void *value;
} listNode;
/*
* 链表
*/
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;
双端链表的API如下所示:
函数 | 作用 | 算法复杂度 |
---|---|---|
listCreate | 创建新链表 | O(1) |
listRelease | 释放链表,以及该链表所包含的节点 | O(N) |
listDup | 创建给定链表的副本 | O(N) |
listRotate | 取出链表的表尾节点,并插入到表头 | O(1) |
listAddNodeHead | 将包含给定值的节点添加到链表的表头 | O(1) |
listAddNodeTail | 将包含给定值的节点添加到链表的表尾 | O(1) |
listInsertNode | 将包含给定值的节点添加到某个节点的之前或之后 | O(1) |
listDelNode | 删除给定节点 | O(1) |
listSearchKey | 在链表中查找和给定 key 匹配的节点 | O(N) |
listIndex | 给据给定索引,返回列表中相应的节点 | O(N) |
listLength | 返回给定链表的节点数量 | O(1) |
listFirst | 返回链表的表头节点 | O(1) |
listLast | 返回链表的表尾节点 | O(1) |
listPrevNode | 返回给定节点的前一个节点 | O(1) |
listNextNode | 返回给定节点的后一个节点 | O(1) |
listNodeValue | 返回给定节点的值 | O(1) |
双端列表的结构对于增加、删除 两种常用操作,复杂度降到了O(1)
这对于实现一些内部命令如LPUSH、RPOP 等平凡命令,降低了时间开销。
2、迭代器
Redis实现了一个双端链表的迭代器,方便从两个方向对双端链表进行迭代。
- 沿着节点 next 指针,从表头向表尾迭代
- 沿着节点 prev 指针,从表尾向表头迭代
迭代器定义如下:
/*
* 链表迭代器
*/
typedef struct listIter {
// 下一节点
listNode *next;
// 迭代方向
int direction;
} listIter;
获取迭代器实现如下:
/*
* 创建列表 list 的一个迭代器,迭代方向由参数 direction 决定
*
* 每次对迭代器调用 listNext() ,迭代器就返回列表的下一个节点
*
* 这个函数不处理失败情形
*
* 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;
}
迭代器每次根据迭代方向,返回下一个节点。
迭代器API如下所示:
函数 | 作用 | 算法复杂度 |
---|---|---|
listGetIterator | 创建一个列表迭代器 | O(1) |
listReleaseIterator | 释放迭代器 | O(1) |
listRewind | 将迭代器的指针指向表头 | O(1) |
listRewindTail | 将迭代器的指针指向表尾 | O(1) |
listNext | 取出迭代器当前指向的节点 | O(1) |
3、双端链表用途
3.1实现Redis列表类型
对列表类型的键进行操作时,程序底层操作可能就是用的双端链表。比如执行 RPUSH、LPOP、LLEN 等命令。
命令手册。
RPUSH 的使用如下,其他命令可以查看
RPUSH key value [value ...]
将一个或多个值 value 插入到列表 key 的表尾(最右边)。
如果有多个 value 值,那么各个 value 值按从左到右的顺序依次插入到表尾:比如对一个空列表 mylist 执行 RPUSH mylist a b c ,得出的结果列表为 a b c ,等同于执行命令 RPUSH mylist a 、 RPUSH mylist b 、 RPUSH mylist c 。
如果 key 不存在,一个空列表会被创建并执行 RPUSH 操作。
当 key 存在但不是列表类型时,返回一个错误。
如果 key 不存在,一个空列表会被创建并执行 RPUSH 操作。
当 key 存在但不是列表类型时,返回一个错误。
3.2Redis自身功能的构建
除了实现列表类型以外, 双端链表还被很多 Redis 内部模块所应用:
- 事务模块使用双端链表依序保存输入的命令
- 服务器模块使用双端链表来保存多个客户端
- 订阅/发送模块使用双端链表来保存订阅模式的多个客户端
- 事件模块使用双端链表来保存时间事件(time event)