【Redis】skiplist跳跃表
有序集合Sorted Set
zadd
zadd用于向集合中添加元素并且可以设置分值,比如添加三门编程语言,分值分别为1、2、3:
127.0.0.1:6379> zadd language 1 java
(integer) 1
127.0.0.1:6379> zadd language 2 c++
(integer) 1
127.0.0.1:6379> zadd language 3 python
(integer) 1
zrange
zrange根据分值区间返回符合条件的数据:
127.0.0.1:6379> zrange language 1 3 withscores
1) "c++"
2) "2"
3) "python"
4) "3"
zscore
zscore根据key和元素值返回元素的分值
127.0.0.1:6379> zscore language python
"3"
Sorted Set是Redis中的一种数据结构,它可以用来存储带有分值的元素,并且根据分值进行排序,是一个有序的集合。
Sorted Set的结构定义如下,它包含了一个哈希表dict和一个跳跃表zskiplist,其中哈希表可以在O(1)的时间复杂度内进行元素查找,而跳跃表可以支持高效的范围查询:
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
跳跃表
如果一个有序集合中包含的元素数量比较多或者有序集合中的元素是比较长的字符串时,Redis就会使用跳跃表作为有序集合的底层实现。
跳跃表是一种多层的有序链表,在一个普通的有序链表中如果想要查找某个元素,必须遍历链表,时间复杂度为O(n),那么如何提高查找效率呢,可以使用跳跃表,从列表中抽出一些元素进行分层,比如每隔一个节点就抽出一层:

此时如果需要查找元素为9的节点:
- 从第三层开始查找,元素的值为8,因为9大于8并且8之后没有其他的节点所以接下来进入第二层
- 进入第二层,8的下一个节点为15,9小于15,所以进入第一层
- 进入第一层,获取8的下一个节点,等于要查找的值9,查找结束
结构定义
跳跃表的结构定义
- header:指向跳跃表中节点的头指针,跳跃表中的节点定义为zskiplistNode,跳跃表实际上也是一个链表,所以会有一个头结点
- tail:指向跳跃表中节点的尾指针
- length:跳跃表中节点的数量
- level:跳跃表的层级
// 跳跃表
typedef struct zskiplist {
// 指向跳跃表的头尾指针
struct zskiplistNode *header, *tail;
// 长度
unsigned long length;
// 层级
int level;
} zskiplist;
节点的结构定义
- ele:一个sds类型的变量,存储实际的数据
- score:存储数据的分值,跳跃表就是按照这个分值进行排序的
- backward:一个指向前一个节点的指针,为了便于从后往前查找
- zskiplistLevel:一个层级数组,因为跳跃表可以有多层,每一层中都有一个指向当前层级中的下一个节点的指针forward和span跨度,跨度代表了当前层级里面,当前节点与下一个节点直接跨越了几个节点
//跳跃表中的节点结构定义
typedef struct zskiplistNode {
// 存储的元素
sds ele;
// 分值
double score;
// 后向指针,指向当前节点的前一个节点
struct zskiplistNode *backward;
// 层级数组
struct zskiplistLevel {
// 指向当前层级中的下一个节点
struct zskiplistNode *forward;
// 跨度
unsigned long span;
} level[];
} zskiplistNode;

跳跃表的创建
/* 创建跳跃表节点*/
zskiplistNode *zslCreateNode(int level, double score, sds ele) {
// 分配内存
zskiplistNode *zn =
zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));
zn->score = score;
zn->ele = ele;
return zn;
}
/* 创建跳跃表 */
zskiplist *zslCreate(void) {
int j;
zskiplist *zsl;
// 跳跃表分配内存
zsl = zmalloc(sizeof(*zsl));
// 层级初始化为1
zsl->level = 1;
// 长度为0
zsl->length = 0;
// 创建头结点
zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);
// 初始化每一层的头结点
for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {
zsl->header->level[j].forward = NULL;
zsl->header->level[j].span = 0;
}
// 头结点的下一个节点指向NULL
zsl->header->backward = NULL;
// 尾节点
zsl->tail = NULL;
return zsl;
}
跳跃表层数的设置
跳跃表根据什么规则来进行层数划分呢,有以下几种方案:
方案一
每隔一个节点,就取出一个节点作为新的一层的节点,这样每一层上节点的数量大约是下一层节点数的一半,此时类似于二分查找,查找复杂度为O(logn)
优点:查找的时间复杂度降低了
缺点:由于需要维护每个层级的节点数,在节点进行插入或者删除的时候,要调整层级节点,带来额外的开销
方案二
新增加节点的时候,调用随机生成层数方法,随机生成一个当前跳跃表所需要的层数,如果生成的层数等于当前层数,新节点只需要加入跳跃表中即可,不需要额外的维护每一个层级的节点数,Redis中就是使用的随机生成层数的方式维护跳跃表的层级。
随机生成层数方法:
#define ZSKIPLIST_MAXLEVEL 32 // 最大层级不超过32
#define ZSKIPLIST_P 0.25
// 随机生成层数
int zslRandomLevel(void) {
int level = 1;
// 如果生成的随机数的值小于ZSKIPLIST_P,层数就+1
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
// 是否超过了最大层数,超过就使用最大层数
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
0xFFFF = 65535
random()&0xFFFF运算之后会生成一个0和65535之间的数,ZSKIPLIST_P * 0xFFFF = 0.25 * 65535,所以random()&0xFFFF 小于 0.25 * 65535的概率为25%,也就是层数会增加1的概率不超过25%。
跳跃表增加节点
- 因为跳跃表有多层,所以需要遍历每一层,寻找每层要插入的位置,update[i]就记录了每一层要插入的位置
- 随机生成跳跃表的层数,如果层数有变化,则需要调整跳跃表的层高
- 创建节点,并将节点插入到跳跃表中
- 设置backward,新插入节点的前一个节点是update[0],如果update[0]为头结点,当前节点的前一个节点设为null,否则backward设置为update[0]
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--) {
// rank[i]记录了当前层从header节点到update[i]节点所经历的步长
rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
// 如果当前层级下一个节点不为空 并且 下一个节点的score小于要插入节点的分值 或者 下一个节点的score等于要插入节点的score并且对比两个节点存储的元素值之后小于0(字符串比较)
while (x->level[i].forward &&
(x->level[i].forward->score < score ||
(x->level[i].forward->score == score &&
sdscmp(x->level[i].forward->ele,ele) < 0)))
{
// 更新rank[i]的值
rank[i] += x->level[i].span;
// 获取下一个节点
x = x->level[i].forward;
}
// 记录每层需要插入的位置
update[i] = x;
}
// 随机生成跳跃表的层数
level = zslRandomLevel();
// 如果大于当前的层数
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;
x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
update[i]->level[i].span = (rank[0] - rank[i]) + 1;
}
/* 更新跨度 */
for (i = level; i < zsl->level; i++) {
update[i]->level[i].span++;
}
// 设置当前节点的前一个节点,如果update[0]为头结点,当前节点的前一个节点设为null,否则backward设置为update[0]
x->backward = (update[0] == zsl->header) ? NULL : update[0];
if (x->level[0].forward)
x->level[0].forward->backward = x;
else
zsl->tail = x;
// 增加长度
zsl->length++;
return x;
}
总结
- Sorted Set支持在添加元素的时候为元素添加一个分值,并根据分值排序,是一个有序的集合。
- Sorted Set在数据比较少的时候采用ziplist存储,超过阈值后使用哈希表和跳跃表来提高查找效率,其中哈希表用于单值查询,跳跃表用于范围查询。
- 跳跃表是一个多层的有序链表,它采用了空间换时间的方式将查找的时间复杂度降到了O(logN)。
参考
黄健宏《Redis设计与实现》
陈雷《Redis5设计与源码分析》
【unix21】redis源码分析--zslRandomLevel位运算解析
【有梦想的肥宅】Redis5设计与源码分析读后感(三)跳跃表
Redis版本:redis-6.2.5
【Redis】skiplist跳跃表的更多相关文章
- redis skiplist (跳跃表)
redis skiplist (跳跃表) 概述 redis skiplist 是有序的, 按照分值大小排序 节点中存储多个指向其他节点的指针 结构 zskiplist 结构 // 跳跃表 typede ...
- 小白也能看懂的Redis教学基础篇——朋友面试被Skiplist跳跃表拦住了
各位看官大大们,双节快乐 !!! 这是本系列博客的第二篇,主要讲的是Redis基础数据结构中ZSet(有序集合)底层实现之一的Skiplist跳跃表. 不知道那些是Redis基础数据结构的看官们,可以 ...
- Redis数据结构—跳跃表
目录 Redis数据结构-跳跃表 跳跃表产生的背景 跳跃表的结构 利用跳跃表查询有序链表 Redis跳跃表图示 Redis跳跃表数据结构 小结 Redis数据结构-跳跃表 大家好,我是白泽,最近学校有 ...
- Redis(2)——跳跃表
一.跳跃表简介 跳跃表(skiplist)是一种随机化的数据结构,由 William Pugh 在论文<Skip lists: a probabilistic alternative to ba ...
- 【Redis】跳跃表原理分析与基本代码实现(java)
最近开始看Redis设计原理,碰到一个从未遇见的数据结构:跳跃表(skiplist).于是花时间学习了跳表的原理,并用java对其实现. 主要参考以下两本书: <Redis设计与实现>跳表 ...
- 浅析SkipList跳跃表原理及代码实现
本文将总结一种数据结构:跳跃表.前半部分跳跃表性质和操作的介绍直接摘自<让算法的效率跳起来--浅谈“跳跃表”的相关操作及其应用>上海市华东师范大学第二附属中学 魏冉.之后将附上跳跃表的源代 ...
- 【转】浅析SkipList跳跃表原理及代码实现
SkipList在Leveldb以及lucence中都广为使用,是比较高效的数据结构.由于它的代码以及原理实现的简单性,更为人们所接受.首先看看SkipList的定义,为什么叫跳跃表? "S ...
- redis的跳跃表
跳跃表是一种插入.查询.删除的平均时间复杂度为O(nlogn)的数据结构,在最差情况下是O(n),当然这几乎很难出现. 和红黑树相比较 最差时间复杂度要差很多,红黑树是O(nlogn),而跳跃表是O( ...
- SkipList 跳跃表
引子 考虑一个有序表:14->->34->->50->->66->72 从该有序表中搜索元素 < 23, 43, 59 > ,需要比较的次数分别为 ...
随机推荐
- Struts2封装获取表单数据方式
一.属性封装 1.创建User实体类` package cn.entity; public class User { private String username; private String p ...
- 微信小程序,制作属于自己的Icon图标
前言 最近在接手一个微信小程序,发现里面的图标都是使用的image组件,看起来非常别扭,加载也不太顺畅. 就想着看看微信有没有类似自带的图标库可以使用. 有是有,就是太少了,翻来翻去好像也就 8 种, ...
- 讲解CPU之NUMA硬件体系以及机制(lscpu查看相关信息)
先看看从系统层面反映出来的numa cpu信息.采样机器为实体机.80核.128内存. [root@ht2 src]# lscpu Architecture: x86_64 #x86架构下的64位 C ...
- 编译实战 | 手摸手教你在Windows环境下运行Redis6.x
原创:微信公众号 码农参上,欢迎分享,转载请保留出处. 哈喽大家好啊,我是没事就愿意瞎捣鼓的Hydra. 不知道有没有小伙伴像我一样,平常开发中用的是windows操作系统,有时候想装点什么软件,一看 ...
- Metalama简介1. 不止是一个.NET跨平台的编译时AOP框架
Metalama是一个基于微软编译器Roslyn的元编程的库,可以解决我在开发中遇到的重复代码的问题.但是其实Metalama不止可以提供编译时的代码转换,更可以提供自定义代码分析.与IDE结合的自定 ...
- 【直播回顾】OpenHarmony知识赋能第四期直播——标准系统HDF开发
3月10日晚上19点,OpenHarmony开发者成长计划社群内,我们举办了知识赋能第四期直播课<OpenHarmony标准系统HDF框架介绍>,吸引了数千名开发者线上观看学习,并 ...
- 「BUAA OO Unit 2 HW8」第二单元总结
「BUAA OO Unit 2 HW8」第二单元总结 目录 「BUAA OO Unit 2 HW8」第二单元总结 Part 0 前言 Part 1 第五次作业 1.1 作业要求 1.2 架构设计 1. ...
- ZooKeeper 基本原理你懂了么?
点击上方"开源Linux",选择"设为星标" 回复"学习"获取独家整理的学习资料! 作者:阿凡卢来源:cnblogs.com/luxiaox ...
- python之函数的进阶
1.名称空间: 定义:用来存放名字的(变量,函数名,类名,引入的模块名) 分类: 内置名称空间:python解释器提供好的一些内置内容 全局名称空间:py文件中自己写的变量 局部名称空间:执行函数时会 ...
- 万字长文详解HBase读写性能优化
一.HBase 读优化 1. HBase客户端优化 和大多数系统一样,客户端作为业务读写的入口,姿势使用不正确通常会导致本业务读延迟较高实际上存在一些使用姿势的推荐用法,这里一般需要关注四个问题: 1 ...