Redis源码分析 - adlist

ADLIST 应用

链表在 Redis 里面的应用面很广,除了我们常用的 LPUSHRPUSHRPOPLPUSH 等更多的命令,Redis 很多底层功能都用了链表,发布与订阅就是其中一员。所以我们今天来看看 Redis 的链表是如何实现的,尽管链表是一种很基础的数据结构。


ADLIST 相关设计

链表是由一个或者多个节点串联起来的数据结构,所以需要定义一个节点的结构体,对应 Redis 中的 struct listNode 。 成员包括了指向上一个节点的指针 listNode *prev 、指向下一个节点的指针 listNode *next 和保留节点保留的值的指针 void *value

不难发现 struct listNode 是一个双链表结构,通过单个节点我们不仅能够访问到下一个节点,还能访问到上一个节点。使用双链表而非单链表的目的是想增加结构的灵活性,因为只有经常对数据进行增加或者删除才会用到链表这种数据结构。无论是增加节点抑或删除节点,双链表都要比单链表更加简单高效。但提高灵活性的代价是需要更多的空间,但这个代价小到可以被我们忽略。

typedef struct listNode {
    struct listNode *prev;
    struct listNode *next;
    void *value;
} listNode;

有了节点结构,接下来就可以组装一个链表结构 struct list 。当我第一看到这个结构体的时候,我觉得太有意思。因为我是一个 C++ 程序员,我对 C 语言的认识并没有我对 C++ 这门语言一样的深入。当我看到 Redis 把函数指针放在结构体里面的时候,我第一个想法就是,这不就是变相地把 C 语言当作面向对象编程语言使用嘛!!!

成员 *head*tail 是头尾节点指针。 unsigned long len 记录了链表的总长度,但我们需要知道链表长度的时候,我们可以通过调用函数 listLength(l) 马上得到结果,而不需要把链表遍历一次。void *(*dup)(void *ptr) 用来拷贝链表,void (*free)(void *ptr) 用于释放链表资源,int (*match)(void *ptr, void *key) 用作判断某个节点是否匹配。

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;

再加上一堆宏定义函数,简直太妙了,这样一来 struct list 仿佛有了成员函数。

/* Functions implemented as macros */
#define listLength(l) ((l)->len)
#define listFirst(l) ((l)->head)
#define listLast(l) ((l)->tail)
#define listPrevNode(n) ((n)->prev)
#define listNextNode(n) ((n)->next)
#define listNodeValue(n) ((n)->value)

#define listSetDupMethod(l,m) ((l)->dup = (m))
#define listSetFreeMethod(l,m) ((l)->free = (m))
#define listSetMatchMethod(l,m) ((l)->match = (m))

#define listGetDupMethod(l) ((l)->dup)
#define listGetFreeMethod(l) ((l)->free)
#define listGetMatchMethod(l) ((l)->match)

不仅如此,Redis 还为 adlist 设计了简单的迭代器 。成员 listNode *next 虽然名叫 next ,但是它可以代表下一个节点,或者上一个节点,这要取决于另外一个成员 int direction ,它代表着迭代器的遍历方向。

typedef struct listIter {
    listNode *next;
    int direction;
} listIter;

ADLIST 操作

除了上面提及的 “成员函数” (比喻),还有众多 struct list 操作函数。这里我不打算对所有函数进行分析,只挑选出一部分比较重要的函数。

/* Prototypes */
list *listCreate(void);
void listRelease(list *list);
void listEmpty(list *list);
list *listAddNodeHead(list *list, void *value);
list *listAddNodeTail(list *list, void *value);
list *listInsertNode(list *list, listNode *old_node, void *value, int after);
void listDelNode(list *list, listNode *node);
listIter *listGetIterator(list *list, int direction);
listNode *listNext(listIter *iter);
void listReleaseIterator(listIter *iter);
list *listDup(list *orig);
listNode *listSearchKey(list *list, void *key);
listNode *listIndex(list *list, long index);
void listRewind(list *list, listIter *li);
void listRewindTail(list *list, listIter *li);
void listRotateTailToHead(list *list);
void listRotateHeadToTail(list *list);
void listJoin(list *l, list *o);

listCreate

第一要介绍的函数是 list *listCreate(void) ,因为一切都始于这个函数。Redis 的 struct list 的初始化实现跟我平常的双链表的做法不太一样。我不会把 *head*tail 用于存放真实数据,我一般都会把它们当作哨兵节点去使用。使用哨兵节点的好处就是,不用检验头尾节点的合法性,因为它们永远都不为 NULL ,并且在最开始的时候,首尾相连,头尾指针互相指向对方。

而 Redis 的做法是,在链表创建之初,*head*tail 都为 NULL,因为像我之前所说 Redis 没有哨兵节点这一玩法。

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;
}

listInsertNode

我们看一下 Redis 是如何插入一个链表节点。参数 after 用于判断新的节点是插在 *old_node 之前还是之后。参数 void *value 是新的节点所需保存的值,所以并不需要自己分配内存去创建一个新的节点再传入函数 listInsertNode

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;
}

Redis 的 LINSERT 命令大概就是调用了 llistInsertNode

LINSERT fruits BEFORE "orange" "apple"

listSearchKey

链表有它自身的优势,同时它也有不足之处。想要在链表中寻找某个特定的节点,我们需要把链表从头到尾遍历一次直到命中目标节点为止。如果某个搜索节点不存在于链表之中,这是一件很浪费时间的事情。为了遍历链表,Redis 设计的迭代器派上用场。函数 void listRewind(list *list, listIter *li) 用来创建一个新的迭代器,并且指向链表得 *head 节点。然后通过调用 listNode *listNext(listIter *iter) 拿到下一个节点的指针。如果最终没有找到目标节点,那么返回 NULL

listNode *listSearchKey(list *list, void *key)
{
    listIter iter;
    listNode *node;

    listRewind(list, &iter);
    while((node = listNext(&iter)) != NULL) {
        if (list->match) {
            if (list->match(node->value, key)) {
                return node;
            }
        } else {
            if (key == node->value) {
                return node;
            }
        }
    }
    return NULL;
}

结语

链表虽然是一种很普通常见的数据结构,但链表的操作可以其实是可以变得十分复杂。不过总体上链表的设计不会有太多变化,Redis 的设计实在值得让我学习。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值