链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活地调整链表的长度。
Redis 的 List 对象的底层实现之一就是链表。C 语言本身没有链表这个数据结构,所以 Redis 自主设计了一个链表数据结构。
链表节点设计
「链表节点」
typedef struct listNode { |
多个 listNode 可以通过 prev 和 next 指针组成双端链表:
链表结构设计
虽然仅仅使用多个 listNode 结构就可以组成链表,但是 Redis 在 listNode 结构体基础上又封装了 list 这个数据结构,这样操作起来会更方便。
「链表结构」
typedef struct list { |
list 结构为链表提供了链表头指针 head、链表尾节点 tail、链表节点数量 len、以及可以自定义(实现多态链表)实现的 dup、free、match 函数。
链表的优势和缺陷
优势
- listNode 链表节点的结构里带有
prev
和next
指针,获取某个节点的前置节点或后置节点的时间复杂度为O(1),而且这两个指针都可以指向 NULL,所以链表是无环链表; - list 结构因为提供了表头指针
head
和表尾节点tail
,所以获取链表的表头节点和表尾节点的时间复杂度为O(1); - list 结构因为提供了链表节点数量
len
,所以获取链表中的节点数量的时间复杂度为O(1); - listNode 链表节使用
void*
指针保存节点值,并且可以通过 list 结构的dup
、free
、match
函数指针为节点设置该节点类型特定的函数,因此链表节点可以保存各种不同类型的值。
缺陷
- 链表的每个节点之间的内存都是不连续的,意味着无法很好利用 CPU 缓存。能够很好利用 CPU 缓存的数据结构就是数组,因为数组的内存空间是连续的,这样就可以充分利用 CPU 缓存来加速访问;
- 保存一个链表节点值都需要一个链表节点结构的分配,内存开销较大。
总结
Redis3.0 的 list 对象在数据量比较少的情况下,会采用「压缩列表」作为底层数据结构的实现,它的优势是节省内存空间,并且是内存紧凑型的数据结构。不过,压缩列表存在性能问题,所以 Redis 在 3.2 版本设计了新的数据结构 quicklist,并将 list 对象的底层数据结构改由 quicklist 实现。然后在 Redis 5.0 设计了新的数据结构 listpack,沿用了压缩列表紧凑型的内存布局,最终在最新的 Redis 版本,将 Hash 对象和 Zset 对象的底层数据结构实现之一的压缩列表,替换成由 listpack 实现。