链表作为数组之外的一种常用序列抽象, 是大多数高级语言的基本数据类型;
但是C语言本身不提供链表类型, 大部分C程序都会自己去实现链表类型, Redis也不例外(实现了一个双端链表结构);
双端链表的应用
双端链表作为一种通用的数据结构, 在Redis内部使用得非常多: 既是Redis列表结构的底层实现之一, 同时为大量Redis 模块所用, 用于构建Redis的其他功能;
实现Redis的列表类型
双端链表还是 Redis 列表类型的底层实现之一, 当对列表类型的键进行操作 —— 比如执行 RPUSH 、 LPOP 或 LLEN 等命令时, 程序在底层操作的可能就是双端链表;
redis> RPUSH brands Apple Microsoft Google (integer) 3 redis> LPOP brands "Apple" redis> LLEN brands (integer) 2 redis> LRANGE brands 0 -1 1) "Microsoft" 2) "Google"
Redis 列表使用两种数据结构作为底层实现:1.双端链表;2.压缩列表;
因为双端链表占用的内存比压缩列表要多, 所以当创建新的列表键时, 列表会优先考虑使用压缩列表作为底层实现, 并且在有需要的时候, 才从压缩列表实现转换到双端链表实现;
Redis自身功能的构建
除了实现列表类型以外, 双端链表还被很多 Redis 内部模块所应用:
- 事务模块使用双端链表依序保存输入的命令;
- 服务器模块使用双端链表来保存多个客户端;
- 订阅/发送模块使用双端链表来保存订阅模式的多个客户端;
- 事件模块使用双端链表来保存时间事件(time event);
类似的应用还有很多, 在后续的章节中我们将看到, 双端链表在 Redis 中发挥着重要的作用。
双端链表的实现
双端链表的实现由 listNode
和 list
两个数据结构构成, 下图展示了由这两个结构组成的一个双端链表实例:
其中, listNode
是双端链表的节点:
typedef struct listNode {
// 前驱节点
struct listNode *prev;
// 后继节点
struct listNode *next;
// 值
void *value;
} listNode;
而 list
则是双端链表本身:
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;
注意, listNode
的 value
属性的类型是 void *
,说明这个双端链表对节点所保存的值的类型不做限制。
对于不同类型的值,有时候需要不同的函数来处理这些值,因此, list
类型保留了三个函数指针 —— dup
、 free
和 match
,分别用于处理值的复制、释放和对比匹配。在对节点的值进行处理时,如果有给定这些函数,就会调用这些函数。
举个例子:当删除一个 listNode
时,如果包含这个节点的 list
的 list->free
函数不为空,就会先调用删除函数 list->free(listNode->value)
来清空节点的值,再执行余下的删除操作(比如说,释放节点)。
另外,从这两个数据结构的定义上,也可以了解到一些行为和性能特征: