Redis底层数据结构之链表
一、Redis中链表的实现
我们都知道在列表的插入与删除的操作,如果数组的中间插入一个元素,那么这个元素后的所有元素的内存地址都要往后移动。删除的话同理,只有对数据的最后一个元素进行插入删除操作时,才比较快。链表并不需要更改节点的内存地址,链表的优势在于增和删,查找时间复杂度为O(n),链表的扩展性比数组好。链表作为一种重要的数据结构广泛用于实现redis的各种功能,由于在数据结构/算法中很多时候都学过链表,这里就不啰嗦了,直接上代码;
Redis中对链表数据结构的定义在Adlist.h文件中listNode结构体,具体的结果如下:
/* Node, List, and Iterator are the only data structures used currently. */
/* listNode结点 */
typedef struct listNode {
//结点的前一结点
struct listNode *prev;
//结点的下一结点
struct listNode *next;
//节点的值
void *value;
} listNode;
这是链表节点的基本定义,但是为了实现链表的各项操作,方便用户调用,Redis又做了一层封装,使用list来持有链表,具体的结构定义如下:
/* listNode 列表 */
typedef struct list {
//链表头结点
listNode *head;
//链表尾结点
listNode *tail;
/* 下面3个指针函数为所有结点公用的方法,相当于我们在面向对象编程中类的方法*/
// 复制链表节点所保存的值
void *(*dup)(void *ptr);
// 释放链表节点所保存的值
void (*free)(void *ptr);
// 匹配两个节点的值是否相等
int (*match)(void *ptr, void *key);
// 链表长度
unsigned long len;
} list;
还有一个数据结构是迭代器,熟悉C++ STL或者Java集合的,应该再熟悉不过了,迭代器就要是封装集合(这里是链表)的遍历规则,让用户不必拘泥于遍历细节,下面是Redis中链表的结构定义
/* list迭代器,只能为单向 */
typedef struct listIter {
//当前迭代位置的下一结点
listNode *next;
//迭代器的方向
int direction;
} listIter;
列举完链表的数据结构完之后,下面我们重点挑几个函数配合代码讲解链表的具体实现过程,首先看查找函数listSearchKey,
/* Search the list for a node matching a given key.
* The match is performed using the 'match' method
* set with listSetMatchMethod(). If no 'match' method
* is set, the 'value' pointer of every node is directly
* compared with the 'key' pointer.
*
* On success the first matching node pointer is returned
* (search starts from head). If no matching node exists
* NULL is returned. */
/* 查找链表中是否存在值为key的节点,存在则返回改节点,否则返回NULL */
listNode *listSearchKey(list *list, void *key)
{
// 这里用到了迭代器,接下来我们可以看到时如何迭代遍历的
listIter *iter;
listNode *node;
//获取迭代器,遍历方向是从head往后
iter = listGetIterator(list, AL_START_HEAD);
//遍历循环
while((node = listNext(iter)) != NULL) {
//如果list定义了match方法,则调用match方法比较节点的值
if (list->match) {
if (list->match(node->value, key)) {
//如果函数返回true,则代表找到结点,释放迭代器空间,返回找到的节点
listReleaseIterator(iter);
return node;
}
} else {
//如果没有定义list 的match方法,则直接比较函数指针
if (key == node->value) {
//如果相等,则代表找到结点,释放迭代器
listReleaseIterator(iter);
return node;
}
}
}
listReleaseIterator(iter);
return NULL;
}
接下我们再看一个函数的实现,头插法加入节点listAddNodeHead
/* Add a new node to the list, to head, contaning the specified 'value'
* pointer as value.
*
* On error, NULL is returned and no operation is performed (i.e. the
* list remains unaltered).
* On success the 'list' pointer you pass to the function is returned. */
/* 头插法加入节点 */
list *listAddNodeHead(list *list, void *value)
{
listNode *node;
//定义新的listNode,并赋值函数指针
if ((node = zmalloc(sizeof(*node))) == NULL)
return NULL;
node->value = value;
if (list->len == 0) {
//当此时没有任何结点时,头尾结点是同一个结点,前后指针为NULL
list->head = list->tail = node;
node->prev = node->next = NULL;
} else {
//设置此结点next与前头结点的位置关系
node->prev = NULL;
node->next = list->head;
list->head->prev = node;
list->head = node;
}
//结点计数递增并返回
list->len++;
return list;
}
链表其他的函数,这里就不一一分析了,有兴趣的朋友,把Redis源码,下载下来,一看便知。
下面来总结下Redis采用双端链表实现的好处有哪些:
- 双端链表可实现在O(1)的实践复杂度访问前后节点
- 无环,表头的前驱节点和表尾的后继节点指向NULL
- 带表头和表尾节点,程序获取表头和表尾的时间复杂度为O(1)
- 使用链表长度计数器,可在O(1)的时间复杂度内获取链表长度
- 链表节点使用void*指针来保存节点值,并且可以通过list结构的dup、free、match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。