有序集合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的节点:

  1. 从第三层开始查找,元素的值为8,因为9大于8并且8之后没有其他的节点所以接下来进入第二层
  2. 进入第二层,8的下一个节点为15,9小于15,所以进入第一层
  3. 进入第一层,获取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%。

跳跃表增加节点

  1. 因为跳跃表有多层,所以需要遍历每一层,寻找每层要插入的位置,update[i]就记录了每一层要插入的位置
  2. 随机生成跳跃表的层数,如果层数有变化,则需要调整跳跃表的层高
  3. 创建节点,并将节点插入到跳跃表中
  4. 设置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;
}

总结

  1. Sorted Set支持在添加元素的时候为元素添加一个分值,并根据分值排序,是一个有序的集合。
  2. Sorted Set在数据比较少的时候采用ziplist存储,超过阈值后使用哈希表和跳跃表来提高查找效率,其中哈希表用于单值查询,跳跃表用于范围查询。
  3. 跳跃表是一个多层的有序链表,它采用了空间换时间的方式将查找的时间复杂度降到了O(logN)。

参考

黄健宏《Redis设计与实现》

陈雷《Redis5设计与源码分析》

极客时间 - Redis源码剖析与实战(蒋德钧)

【unix21】redis源码分析--zslRandomLevel位运算解析

【有梦想的肥宅】Redis5设计与源码分析读后感(三)跳跃表

Redis版本:redis-6.2.5

【Redis】skiplist跳跃表的更多相关文章

  1. redis skiplist (跳跃表)

    redis skiplist (跳跃表) 概述 redis skiplist 是有序的, 按照分值大小排序 节点中存储多个指向其他节点的指针 结构 zskiplist 结构 // 跳跃表 typede ...

  2. 小白也能看懂的Redis教学基础篇——朋友面试被Skiplist跳跃表拦住了

    各位看官大大们,双节快乐 !!! 这是本系列博客的第二篇,主要讲的是Redis基础数据结构中ZSet(有序集合)底层实现之一的Skiplist跳跃表. 不知道那些是Redis基础数据结构的看官们,可以 ...

  3. Redis数据结构—跳跃表

    目录 Redis数据结构-跳跃表 跳跃表产生的背景 跳跃表的结构 利用跳跃表查询有序链表 Redis跳跃表图示 Redis跳跃表数据结构 小结 Redis数据结构-跳跃表 大家好,我是白泽,最近学校有 ...

  4. Redis(2)——跳跃表

    一.跳跃表简介 跳跃表(skiplist)是一种随机化的数据结构,由 William Pugh 在论文<Skip lists: a probabilistic alternative to ba ...

  5. 【Redis】跳跃表原理分析与基本代码实现(java)

    最近开始看Redis设计原理,碰到一个从未遇见的数据结构:跳跃表(skiplist).于是花时间学习了跳表的原理,并用java对其实现. 主要参考以下两本书: <Redis设计与实现>跳表 ...

  6. 浅析SkipList跳跃表原理及代码实现

    本文将总结一种数据结构:跳跃表.前半部分跳跃表性质和操作的介绍直接摘自<让算法的效率跳起来--浅谈“跳跃表”的相关操作及其应用>上海市华东师范大学第二附属中学 魏冉.之后将附上跳跃表的源代 ...

  7. 【转】浅析SkipList跳跃表原理及代码实现

    SkipList在Leveldb以及lucence中都广为使用,是比较高效的数据结构.由于它的代码以及原理实现的简单性,更为人们所接受.首先看看SkipList的定义,为什么叫跳跃表? "S ...

  8. redis的跳跃表

    跳跃表是一种插入.查询.删除的平均时间复杂度为O(nlogn)的数据结构,在最差情况下是O(n),当然这几乎很难出现. 和红黑树相比较 最差时间复杂度要差很多,红黑树是O(nlogn),而跳跃表是O( ...

  9. SkipList 跳跃表

    引子 考虑一个有序表:14->->34->->50->->66->72 从该有序表中搜索元素 < 23, 43, 59 > ,需要比较的次数分别为 ...

随机推荐

  1. Shiro之权限管理的概念

    文章目录 前言:什么是shiro 一.什么是权限管理? 举例 二.权限管理的具体分类 1.身份认证 2.授权 总结 前言:什么是shiro Apache Shiro 是一个开源安全框架,提供身份验证. ...

  2. 使用java生成备份sqlserver数据表的insert语句

    针对sqlserver数据表的备份工具很多,有时候条件限制需要我们自己生成insert语句,以便后期直接执行这些插入语句.下面提供了一个简单的思路,针对mysql或oracle有兴趣的以后可以试着修改 ...

  3. 鲜为人知帝国CMS内容页调用上一篇和下一篇的精华方法汇总

    <span style="float:left">上一篇:[!--info.pre--]</span><span style="float: ...

  4. String类 的基本用法

    1.String 对象的创建 String对象的创建有两种方式. 第1 种方式就是我们最常见的创建字符串的方式: String str1 = "Hello, 慕课网"; 第 2 种 ...

  5. Linux内核--链表结构(二)

    Linux内核链表定义了一系列用于链表遍历的宏,本章详细描述. 一.container_of和offsetof 首先介绍两个很好用的宏container_of和offsetof.offsetof宏用于 ...

  6. Java之万年历

    @(文章目录) 二.Java之万年历 2.1 要求 输入年份: 输入月份: 输出某年某月的日历. 2.2 思路 实现从控制台接收年和月,判断是否是闰年(判断是否是闰年:能被4整除但不能被100整除:或 ...

  7. uniapp-uni.setNavigationBarColor 动态修改顶部背景颜色

    uni.setNavigationBarColor({ frontColor: '#ffffff', backgroundColor: "#3583ff" })

  8. GraphScope v0.12.0 版本发布

    GraphScope 每月进行常规版本的迭代与发布,GraphScope v0.12.0 全新版本在四月如期而至.v0.12.0 为交互式图查询 GAIA 引入全新的 IR 层以及新增 Giraph ...

  9. 前后端分离后台管理系统 Gfast v3.0 全新发布

    GFast V3.0 平台简介 基于全新Go Frame 2.0+Vue3+Element Plus开发的全栈前后端分离的管理系统 前端采用vue-next-admin .Vue.Element UI ...

  10. JVM垃圾回收篇

    点赞再看,养成习惯,微信搜索「小大白日志」关注这个搬砖人. 文章不定期同步公众号,还有各种一线大厂面试原题.我的学习系列笔记. 基础概念 GC=jvm垃圾回收,垃圾回收机制是由垃圾回收器Garbage ...