zskiplist(跳跃表)

原理

zskiplist是一个有序的集合,为了解决其他链表的插入、删除效率低、查询元素需要循环遍历等缺点,redis就采用了一个特殊的数据结构zskiplist(跳跃表)。

它在性能上跟红黑树差不多, 同时又比红黑树的实现简单。在插入、删除、查询等操作上的时间复杂度为o(logn)。

分析

  • 它通过在每个节点中维持多个指向其它节点的指针,从而达到快速访问节点的目的;
  • Redis 只在两个地方用到了跳跃表,一个是实现有序集合健,另一个是在集群节点中用做内部数据结构,除此之外,跳跃表在 Redis 里没有其它用途;
  • 一个常规的链表在做元素查找时,只能从头到尾的遍历来得到元素。时间复杂度为 O(n);
  • zskiplist的头结点不是一个有效的节点,它有ZSKIPLIST_MAXLEVEL层(32层),每层的forward指向该层跳跃表的第一个节点,若没有则为null;
  • redis限定了ZSKIPLIST_P抛硬币正面的概率为1/4

zskiplist 图解分析

Redis存储结构之zskiplist_redis

zskiplist如何查找元素分析

加入我们想查找 9 这个元素,步骤如下:

  1. 先遍历最高层第四层找到16;
  2. 9<16,找到第三层13;
  3. 9<13,找到二层8;
  4. 9>8,找到8的下一个节点9。

Redis存储结构之zskiplist_redis zskiplist_02

zskiplist插入元素分析

创建一个zskiplist的时间复杂度为O(1);

  1. 找到合适的位置插入
  2. 调用zslRandomLevel 得到一个插入节点的层数,有 1/4 概率加入上一层;
  3. 调用zslCreateNode创建一个新的节点;
  4. 修改插入位置的前后节点以及插入节点本身的​​backward​​​、​​forward​​​、​​span​​等属性

Redis存储结构之zskiplist_redis_03

zskiplist删除元素分析

释放一个节点的内存 时间复杂度O(1);

释放整个skiplist的内存 时间复杂度O(n);

从skiplist中删除并释放掉一个节点 时间复杂度O(logn),会涉及到3个步骤:

  • 根据​​ele​​​和​​score​​找到节点的位置(代码里变量x即为该节点,update记录每层x的上一个节点)
  • 调动​​zslDeleteNode​​把x节点从skiplist逻辑上删除
  • 释放x节点内存

根据范围删除节点 时间复杂度O(log(n)+m), m是范围内元素的个数;

zskiplist跟红黑树对比

优点

  • 比红黑树占用更多的内存,每个节点的大小取决于该节点的层数;
  • 空间局部性较差导致缓存命中率低,感觉上会比红黑树更慢;

优点:

  • 实现比红黑树简单;
  • 比红黑树更容易扩展,作者之后实现zrank指令时没怎么改动代码;
  • 红黑树插入删除时为了平衡高度需要旋转附近节点,高并发时需要锁。skiplist不需要考虑;
  • 一般用zset的操作都是执行zrange之类的操作,取出一片连续的节点。这些操作的缓存命中率不会比红黑树低。

源码

redis/src/server.h

#define ZSKIPLIST_MAXLEVEL 32/* Should be enough for 2^64 elements */
#define ZSKIPLIST_P 0.25/* Skiplist P = 1/4 */
typedef struct zskiplist {
struct zskiplistNode *header, *tail; // 跳跃表头尾节点
unsigned long length; //节点个数
int level; //除头结点外最大的层数
} zskiplist;
typedef struct zskiplistNode {
sds ele; //存储字符串类型数据 redis3.0版本中使用robj类型表示,但是在redis4.0.1中直接使用sds类型表示
double score; //存储排序的分值
struct zskiplistNode *backward; //指向上一个节点,用于zrevrange命令
struct zskiplistLevel {
struct zskiplistNode *forward; //指向下一个节点
unsigned long span; //到达后一个节点的跨度(两个相邻节点span为1)
} level[]; //该节点在各层的信息,柔性数组成员
} zskiplistNode;