O(1)的skiplist成员查找?

众所周知Redis中每种基本类型都有2种或以上的底层实现,一般谈到ZSET,我们会说它的实现是基于ziplist和skiplist的,这没有问题:

  • 当ZSET长度小于设定值(zset-max-ziplist-entries)或成员的长度小于设定值(zset-max-ziplist-value)时会使用ziplist的实现,否则使用skiplist实现

但是当ZSET在使用skiplist实现的时候,它对成员的查找也是O(1)复杂度。根据skiplist的结构,要查找某一个成员必须对各个SkiplistNode进行遍历,因此复杂度为O(n)。所以在ZSET-skiplist的实现中查找成员并不是根据skiplist进行的,而是使用字典(dict)。

先来看一下ZSET的结构源码,Redis5.0.5版本中数据结构的定义在redis/src/server.h中:

typedef struct zset {dict *dict;zskiplist *zsl;
} zset;

可以看到一个ZSET结构使用了一个dict和一个zskiplist(特殊版本的skiplist),具体代码在SkipList小节中再叙述。ZSET的结构可以由下图来标识:

通过这样的结构,当ZSET需要进行成员查询的时候,可以根据dict查询,时间复杂度为O(1);当ZSET需要进行范围查找的时候,根据skiplist结构可以实现平均O(logn)复杂度的查找。

这两种结构单独使用来实现ZSET是可行的,但是dict在范围型操作的时候需要对字典保存的所有元素进行排序因此需要至少O(nlogn)的时间复杂度和额外O(n)的空间复杂度;在单独使用skiplist根据成员查找分值的时候就由O(1)时间复杂度上升到了O(logn)复杂度。因此Redis中选择同时使用dict和skiplist来实现ZSET类型。

SkipList的实现

下面来具体聊一下SkipList数据结构。
在Redis源码中找到跳跃表的相关定义,就在zset的上面几行,补充一些注释:

/* ZSETs use a specialized version of Skiplists */
# 跳跃表节点(ZSET版)
typedef struct zskiplistNode {# 使用sds来存储成员名字sds ele;# 浮点型分数double score;# 每个zskiplist节点都带有向前的指针struct zskiplistNode *backward;# zskiplist分层,每层中包含指向其他zskiplist节点的指针struct zskiplistLevel {# zskiplist节点指针struct zskiplistNode *forward;# 本层指向的下个节点离本节点的跨度unsigned long span;} level[];
} zskiplistNode;# 跳跃表
typedef struct zskiplist {# 分别指向头尾的指针struct zskiplistNode *header, *tail;# 长度 即跳跃表中包含的节点数目(头节点不算)unsigned long length;# 层数 即跳跃表中各节点层数的最大值(头节点不算)int level;
} zskiplist;

skiplist的结构可以由下图来表示:

其中:

  • 头节点也是zskiplistNode因此也由对应的分数、向前指针、sds,只不过一般不使用,在图中没有表示出来。
  • skiplist结构中level为5,因为在第三个节点中层数为5。
  • skiplist结构中length为3,因为一共有头节点(不算在内),o1,o2,o3几个节点。

借助ZSET的各种API,来看一下skiplist在实际中是怎么使用的。
下面代码出现在redis/src/t_zset.c中,实现的是zset的插入成员操作:

# 输入一个zset的skiplist、新成员的得分和名字
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;unsigned int rank[ZSKIPLIST_MAXLEVEL];int i, level;serverAssert(!isnan(score));x = zsl->header;# 从头遍历跳跃表来查找当前元素应该插入在哪个节点之后for (i = zsl->level-1; i >= 0; i--) {/* store rank that is crossed to reach the insert position */rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];while (x->level[i].forward &&# 排名是由分数和sds名字共同决定的,同分数下按节点名排序(x->level[i].forward->score < score ||(x->level[i].forward->score == score &&sdscmp(x->level[i].forward->ele,ele) < 0))){# 注意这一句,说明span是用来便于计算节点排名的rank[i] += x->level[i].span;x = x->level[i].forward;}update[i] = x;}/* we assume the element is not already inside, since we allow duplicated* scores, reinserting the same element should never happen since the* caller of zslInsert() should test in the hash table if the element is* already inside or not. */level = zslRandomLevel();# 判断是否要重写头节点的level值if (level > zsl->level) {for (i = zsl->level; i < level; i++) {rank[i] = 0;update[i] = zsl->header;update[i]->level[i].span = zsl->length;}zsl->level = level;}x = zslCreateNode(level,score,ele);for (i = 0; i < level; i++) {x->level[i].forward = update[i]->level[i].forward;update[i]->level[i].forward = x;/* update span covered by update[i] as x is inserted here */x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);# span实际就是zset两个成员之间的rank差值update[i]->level[i].span = (rank[0] - rank[i]) + 1;}/* increment span for untouched levels */for (i = level; i < zsl->level; i++) {update[i]->level[i].span++;}x->backward = (update[0] == zsl->header) ? NULL : update[0];if (x->level[0].forward)x->level[0].forward->backward = x;elsezsl->tail = x;zsl->length++;return x;
}

可以观察到:

  • 跳跃表的不同节点之间由指针和跨度关联
  • 跨度值实际上为这两个节点之间的排名差距,如上图中o1与o3的排名差正是o1指向o3的第4层的跨度值2,也可以等于o1至o2的跨度值1加上o2至o3的跨度值1
  • 排名取决于分数,同分情况下取决于名字
  • 插入节点的时候判断层数是否大于头节点的层数值,是否需要更新
  • 节点的层数是zslRandomLevel()生成的,根据命名每个节点的层数应该是随机的
  • 注释中提到了在新增元素的时候要在哈希表中判断是否为重复,zset是不允许重复的成员出现的(但是可以有同分成员)

Redis的ZSET的实现及结合源码的跳跃表结构分析相关推荐

  1. Redis如何实现分布式锁延时队列以及限流应用丨Redis源码原理|跳表|B+树|分布式锁|中间件|主从同步|存储原理

    Redis如何实现分布式锁延时队列以及限流应用 视频讲解如下,点击观看: Redis如何实现分布式锁延时队列以及限流应用丨Redis源码原理|跳表|B+树|分布式锁|中间件|主从同步|存储原理|数据模 ...

  2. 修改gh-ost源码实现两表在线高速复制

    修改gh-ost源码实现两表在线高速复制 一.问题起源 笔者所在的公司的需要对核心业务表tb_doc 进行表分区,目前该表的记录数为190,522,155. 由于该表没有分区,新增分区需要创建影子表, ...

  3. Mybatis源码分析--关联表查询及延迟加载原理(二)

    在上一篇博客Mybatis源码分析--关联表查询及延迟加载(一)中我们简单介绍了Mybatis的延迟加载的编程,接下来我们通过分析源码来分析一下Mybatis延迟加载的实现原理. 其实简单来说Myba ...

  4. Redis(八):zset/zadd/zrange/zrembyscore 命令源码解析

    前面几篇文章,我们完全领略了redis的string,hash,list,set数据类型的实现方法,相信对redis已经不再神秘. 本篇我们将介绍redis的最后一种数据类型: zset 的相关实现. ...

  5. redis源码分析 ppt_【Redis】redis各类型数据结构和底层实现源码分析

    一.简介和应用 Redis是一个由ANSI C语言编写,性能优秀.支持网络.可持久化的K-K内存数据库,并提供多种语言的API.它常用的类型主要是 String.List.Hash.Set.ZSet ...

  6. 系统性详解Redis操作Hash类型数据(带源码分析及测试结果)

    1 缘起 系统讲解Redis的Hash类型CURD, 帮助学习者系统且准确学习Hash数据操作, 逐步养成测试的好习惯, 本文较长,Hash的操作比较多,请耐心看, 既可以集中时间看,亦可以碎片时间学 ...

  7. Spring mvc Data Redis—Pub/Sub(附Web项目源码)

    一.发布和订阅机制 当一个客户端通过 PUBLISH 命令向订阅者发送信息的时候,我们称这个客户端为发布者(publisher). 而当一个客户端使用 SUBSCRIBE 或者 PSUBSCRIBE ...

  8. Spring Data Redis—Pub/Sub(附Web项目源码)

    一.发布和订阅机制 当一个客户端通过 PUBLISH 命令向订阅者发送信息的时候,我们称这个客户端为发布者(publisher). 而当一个客户端使用 SUBSCRIBE 或者 PSUBSCRIBE ...

  9. 自研redis sdk支持自动dns切换(附源码)

    大家好,我是烤鸭: 标题起的有点大了,说是自研,其实就是个封装,不过倒是解决了dns切换的问题(虽然不太优雅). 背景 之前做活动的时候,用域名链接的redis,当时做了主备集群,在主集群宕机的时候, ...

最新文章

  1. 设为首页加入收藏代码
  2. 现代前端开发路线图:从零开始,一步步成为前端工程师
  3. python代码图片头像_Python帮你微信头像任意添加装饰别再@微信官方了
  4. NIOS II软核处理器
  5. vb光环褪去java、c/c++/c#成编程主流
  6. springboot超级详细的日志配置(基于logback)
  7. 面试题 01.03. URL化
  8. 百度贴吧高考作文强贴
  9. 基于JAVA+Servlet+JSP+MYSQL的超市管理系统
  10. signature=cb97f07fbd7b371e6311b0d8707b6398,vue 汉字转拼音(filter)
  11. 远程桌面命令是什么 如何使用命令连接远程桌面
  12. pyhon身份证验证
  13. VS Code 快速删除多行的部分内容
  14. 【obs】转载:OBS直播严重延迟和卡顿怎么办?
  15. windbg抓一个windows蓝屏分析
  16. esp8266WiFi模块通过MQTT连接华为云
  17. android与mysql连接不上去_安卓连接不上mysql怎么办
  18. 勒索病毒是什么?如何防勒索病毒
  19. 【Git】Git下载安装与使用(一)
  20. matlab绘四叶玫瑰线,玫瑰线 - calculus的日志 - 网易博客

热门文章

  1. ffmpeg probe一个文件的过程
  2. 微信的原创保护机制到底是如何实现的?
  3. OMF(Oracle Managed Files,Oracle管理的文件)介绍
  4. HUST1005 渊子赛马【枚举】
  5. BTC EmbeddedPlatform安装手记
  6. 安卓app开机自启动的几种方式
  7. cnn kaggle仙人掌_我如何开发可识别情绪并闯入Kaggle前10名的CNN
  8. java 自然周_java 使用Calendar类计算每月有多少自然周,并输出每周的开始和结束日期...
  9. vue+echarts实现多个仪表盘图表
  10. vue获取当前时间、时间戳方法