Redis设计与实现之双端链表

2023-12-15 06:31:15

目录

一、Redis为什么选择双端链表作为底层数据结构?

二、双端链表

1、双端链表的应用

实现Redis的列表类型

Note: Redis列表使用两种数据结构作为底层实现:

Redis自身功能的构建

2、双端链表的实现

?编辑3、迭代器

三、双端链表在Redis中的应用场景有哪些?

四、双端链表在Redis中如何实现数据的插入和删除操作?

五、Redis双端链表与传统的单向链表有什么区别?

六、Redis双端链表在高并发情况下可能会出现什么问题?如何解决这些问题?

七、Redis双端链表如何处理内存分配和回收的问题?

八、Redis是否支持链表的排序操作?如何实现链表的排序?

九、Redis双端链表如何处理数据的重复问题?

十、Redis双端链表在持久化时如何处理?

十一、小结


一、Redis为什么选择双端链表作为底层数据结构?

Redis选择双端链表作为底层数据结构有以下几个原因:

  1. 高效的插入和删除操作:双端链表可以在常数时间复杂度O(1)内完成节点的插入和删除操作。这对于Redis的一些操作,比如列表(List)的push和pop操作,非常高效。

  2. 集合类型的操作:双端链表可以支持集合操作,比如求交集、并集和差集等。这对于Redis的一些操作,比如集合(Set)的操作,非常有用。

  3. 排序功能:双端链表可以支持排序功能,这对于Redis的有序集合(Sorted Set)非常重要。双端链表可以通过额外的数据结构(比如跳跃表)实现高效的排序操作。

  4. 内存紧凑:双端链表在内存上的占用比较紧凑,因为它只需要存储指向前后节点的指针。这对于Redis的内存管理和存储效率非常重要。

综上所述,双端链表作为底层数据结构,能够满足Redis在插入、删除、集合操作和排序等方面的需求,并且具有较高的存储效率。

二、双端链表

链表作为数组之外的一种常用序列抽象,是大多数高级语言的基本数据类型,因为 C 语言本身不支持链表类型,大部分 C 程序都会自己实现一种链表类型,Redis 也不例外——它实现了一 个双端链表结构。

双端链表作为一种常见的数据结构,在大部分的数据结构或者算法书里都有讲解,因此,这一 章关注的是 Redis 双端链表的具体实现,以及该实现的 API ,而对于双端链表本身,以及双端 链表所对应的算法,则不做任何解释。

另外,一些书籍,比如 《算法: C语言实现》 和《数据结构与算法分析》 则提供了关于双端链表的更详细的信息。

1、双端链表的应用

双端链表作为一种通用的数据结构,在 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"

Note: Redis列表使用两种数据结构作为底层实现:

1.双端链表

2.压缩列表

因为双端链表占用的内存比压缩列表要多,所以当创建新的列表键时,列表会优先考虑使用压

缩列表作为底层实现,并且在有需要的时候,才从压缩列表实现转换到双端链表实现。

Redis自身功能的构建

除了实现列表类型以外,双端链表还被很多 Redis内部模块所应用:

?事务模块使用双端链表来按顺序保存输入的命令;

?订阅 /发送模块使用双端链表来保存订阅模式的多个客户端;

?事件模块使用双端链表来保存时间事件( time event ) ;

类似的应用还有很多,在后续的章节中我们将看到,双端链表在 Redis中发挥着重要的作用。

2、双端链表的实现

双端链表的实现由 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) 清空节点的值,再执行余下的删除 操作(比如说,释放节点)。

另外,从这两个数据结构的定义上,也可以它们的一些行为和性能特征:

  • listNode 带有 prev 和 next 两个指针,因此,对链表的遍历可以在两个方向上进行:从 表头到表尾,或者从表尾到表头。

  • list 保存了 head 和 tail 两个指针,因此,对链表的表头和表尾进行插入的复杂度都为 θ(1) ——这是高效实现 LPUSH 、RPOP 、RPOPLPUSH 等命令的关键。

  • list 带有保存节点数量的 len 属性,所以计算链表长度的复杂度仅为 θ(1) ,这也保证 了 LLEN 命令不会成为性能瓶颈。

    以下是用于操作双端链表的 API ,它们的作用以及算法复杂度:

3、迭代器

Redis 为双端链表实现了一个迭代器 ,这个迭代器可以从两个方向对双端链表进行迭代: ? 沿着节点的next指针前进,从表头向表尾迭代;
? 沿着节点的prev指针前进,从表尾向表头迭代;

以下是迭代器的数据结构定义:

typedef struct listIter { 
    // 下一节点
    listNode *next;
    // 迭代方向 
    int direction;
} listIter;

direction 记录迭代应该从那里开始:
? 如果值为adlist.h/AL_START_HEAD,那么迭代器执行从表头到表尾的迭代; ? 如果值为adlist.h/AL_START_TAIL,那么迭代器执行从表尾到表头的迭代;

以下是迭代器的操作 API ,它们的作用以及算法复杂度:

三、双端链表在Redis中的应用场景有哪些?

在Redis中,双端链表(Doubly Linked List)常用于以下场景:

  1. 列表结构:Redis提供了List数据类型,它实际上就是通过双端链表实现的。List可以在两端进行元素的添加和删除操作,可以用于实现队列(FIFO)或栈(LIFO)等数据结构。

  2. 发布与订阅:Redis的发布与订阅功能使用了双端链表来保存订阅者列表。当有新消息发布时,Redis会遍历链表,并将消息发送给每个订阅者。

  3. LRU缓存策略:Redis的LRU(Least Recently Used)缓存策略使用了双端链表来保存最近访问的键。当缓存空间不足时,Redis会删除链表尾部的键,以便为新的键腾出空间。

  4. 有序集合:Redis提供了有序集合(Sorted Set)数据类型,它是通过双端链表和散列表实现的。有序集合可以根据元素的分值进行排序,并支持按照分值范围获取元素。

总而言之,双端链表在Redis中的应用场景主要包括列表结构、发布与订阅、LRU缓存策略和有序集合。

四、双端链表在Redis中如何实现数据的插入和删除操作?

在Redis中,双端链表是通过使用list数据结构来实现的。Redis中的list数据结构使用双端链表来存储数据,并提供了一系列的命令来进行插入和删除操作。

下面是在Redis中实现双端链表数据插入和删除操作的一些常用命令:

  1. 插入操作:
  • LPUSH key value1 [value2 ...]:将一个或多个值插入到列表的头部。
  • RPUSH key value1 [value2 ...]:将一个或多个值插入到列表的尾部。
  • LINSERT key BEFORE|AFTER pivot value:在列表中指定元素的前面或后面插入一个新元素。
  1. 删除操作:
  • LPOP key:移除并返回列表头部的元素。
  • RPOP key:移除并返回列表尾部的元素。
  • LREM key count value:从列表中删除指定数量的元素。

需要注意的是,在双端链表中,插入和删除操作的时间复杂度都是O(1),即常数时间。

另外,Redis还提供了其他一些用于操作双端链表的命令,比如获取列表长度、获取指定范围内的元素等。

五、Redis双端链表与传统的单向链表有什么区别?

Redis双端链表和传统的单向链表有以下区别:

  1. 方向:传统的单向链表是单向的,每个节点只能指向下一个节点,而Redis的双端链表是双向的,每个节点既可以指向下一个节点,也可以指向前一个节点。

  2. 遍历:由于双端链表具有双向性,可以从头部或尾部开始遍历链表。传统的单向链表只能从头部开始遍历,如果需要从尾部开始遍历,则需要遍历整个链表到达尾部。

  3. 节点:双端链表的每个节点包含指向前一个节点和后一个节点的指针,而传统的单向链表的每个节点只包含指向下一个节点的指针。

  4. 操作:双端链表支持在链表的头部和尾部进行添加、删除节点的操作,并且这些操作的时间复杂度都是O(1)。传统的单向链表只能在头部进行添加和删除操作,如果需要在尾部进行添加和删除,则需要遍历整个链表到达尾部。

总的来说,Redis的双端链表在遍历和操作方面更加灵活和高效,特别适合用于实现队列和栈等数据结构。

六、Redis双端链表在高并发情况下可能会出现什么问题?如何解决这些问题?

在高并发情况下,Redis双端链表可能会出现以下问题:

  1. 竞争条件(Race Condition):多个线程同时修改链表结构,可能导致数据不一致或者链表的结构被破坏。

  2. 内存泄漏:在高并发情况下,如果有大量的插入和删除操作,可能会导致链表内存无法被及时释放,从而造成内存泄漏。

  3. 阻塞操作:当链表较长并且数据量较大时,插入和删除操作会变得较慢,从而阻塞其他并发操作。

为了解决这些问题,可以采取以下措施:

  1. 使用锁(Locking):通过在关键代码段使用适当的锁机制,可以保证每次只能有一个线程对链表进行修改,从而避免竞争条件。

  2. 使用乐观锁(Optimistic Locking):通过在链表节点中添加版本号(version)字段,并在修改节点时对版本号进行比较和更新,可以避免多个线程同时修改节点值和结构的问题。

  3. 分段锁(Segment Locking):将链表分成多个段,每个段都有一个锁。这样,在进行插入和删除操作时,只需要锁定对应的段,而不是整个链表,从而减少并发操作的阻塞。

  4. 使用更高效的数据结构:根据实际业务需求,考虑使用其他更高效的数据结构,如跳表(Skip List)或红黑树(Red-Black Tree),来替代双端链表。这些数据结构在并发情况下具有更好的性能和扩展性。

需要根据具体情况选择适当的解决方法,以保证在高并发情况下,Redis双端链表能够正常运行并保证数据一致性。

七、Redis双端链表如何处理内存分配和回收的问题?

Redis双端链表的内存分配和回收问题是通过Redis的内存分配器来处理的。Redis使用自己实现的内存分配器,默认是jemalloc,也可以配置为使用libc分配器或者其他内存分配器。

在双端链表的节点被创建时,Redis会使用内存分配器动态分配内存来存储节点的数据和指针等信息。当节点被删除或者不再使用时,Redis会通过内存分配器释放该节点所占用的内存。

Redis的内存分配器会维护一个内存池,用于存储分配出去的内存块。当需要分配内存时,内存分配器会从内存池中分配一块足够大小的内存,并将这块内存标记为已分配。当需要释放内存时,内存分配器会将已分配的内存块标记为可用,并将其返回给内存池,以供后续的内存分配。

Redis的内存分配器还会进行内存回收的优化,例如通过合并相邻的空闲内存块来减少内存碎片的产生。此外,Redis还会定期进行内存回收操作,将不再使用的内存块返回给操作系统,以便回收更多的内存空间。

总之,Redis双端链表的内存分配和回收问题主要是通过Redis的内存分配器来处理的,它负责分配和释放节点的内存,并进行内存回收的优化操作。

八、Redis是否支持链表的排序操作?如何实现链表的排序?

是的,Redis支持链表的排序操作。可以通过以下步骤来实现链表的排序:

  1. 使用RPUSH命令将元素依次插入到链表中。
  2. 使用LTRIM命令将链表裁剪为需要排序的部分。
  3. 使用SORT命令对链表进行排序。

下面是一个示例:

RPUSH mylist 5
RPUSH mylist 2
RPUSH mylist 10
RPUSH mylist 3
RPUSH mylist 8
LTRIM mylist 0 -1
SORT mylist ASC

执行完上述命令后,链表mylist中的元素将按升序排序。

九、Redis双端链表如何处理数据的重复问题?

Redis双端链表中并没有特别处理数据的重复问题。双端链表是一种有序的数据结构,可以存储重复的数据。如果需要处理数据的重复问题,可以在使用双端链表的时候,在外部进行处理,例如在插入前先检查数据是否已存在,或者在查询时排除重复的数据。

十、Redis双端链表在持久化时如何处理?

Redis中的双端链表在持久化时会被转化为一种特殊的序列化格式,然后存储在磁盘上。在加载数据时,Redis会重新构建双端链表。

具体来说,Redis会将双端链表中的每个节点都转化为一个字节序列。这个字节序列包括节点的值、前驱节点和后继节点的引用。这样,当需要恢复双端链表时,只需要按照序列化格式逐个节点进行解析,并重新构建双端链表的结构即可。

在持久化过程中,Redis会通过将链表的序列化格式写入到磁盘上的持久化文件中来保存链表的数据。而在加载数据时,Redis会从持久化文件中读取数据,并解析成链表的形式,以便于后续的操作。

需要注意的是,持久化过程中的链表数据可能会存在一些延迟,因为Redis使用的是异步的持久化机制。这意味着,在持久化数据时,可能会有一小段时间的数据丢失风险。但是大多数情况下,数据的丢失概率非常低。为了确保数据的可靠性,可以通过设置Redis的持久化策略,来控制持久化的频率和方式。

十一、小结

? Redis 实现了自己的双端链表结构。

? 双端链表主要有两个作用:

????????– 作为 Redis 列表类型的底层实现之一;

????????– 作为通用数据结构,被其他功能模块所使用;

? 双端链表及其节点的性能特性如下:

????????– ?节点带有前驱和后继指针,访问前驱节点和后继节点的复杂度为 O(1) ,并且对链表 的迭代可以在从表头到表尾和从表尾到表头两个方向进行;

????????– ?链表带有指向表头和表尾的指针,因此对表头和表尾进行处理的复杂度为 O(1) ;

????????– ?链表带有记录节点数量的属性,所以可以在 O(1) 复杂度内返回链表的节点数量(长

度);

文章来源:https://blog.csdn.net/LYX_WIN/article/details/134990008
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。