有序集合对象是有序的。与列表使用索引下标作为排序依据不同,有序集合为每个元素设置一个分数(score)作为排序依据。

①、编码

有序集合的编码可以是 ziplist 或者 skiplist。

ziplist编码

  • 有序集合保存的元素数量小于128个
  • 有序集合保存的所有元素的长度小于64字节

 当ziplist作为zset的底层存储结构时候,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个元素保存元素的分值。

ziplist 编码的有序集合对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个节点保存元素的分值。并且压缩列表内的集合元素按分值从小到大的顺序进行排列,小的放置在靠近表头的位置,大的放置在靠近表尾的位置。

//操作
ZADD price 8.5 apple 5.0 banana 6.0 cherry//存储顺序

存储顺序.png

skiplist 编码

的有序集合对象使用 zet 结构作为底层实现,一个 zset 结构同时包含一个字典和一个跳跃表:

typedef struct zset{//跳跃表zskiplist *zsl;//字典dict *dice;
} zset;

字典的键保存元素的值,字典的值则保存元素的分值;跳跃表节点的 object 属性保存元素的成员,跳跃表节点的 score 属性保存元素的分值。

这两种数据结构会通过指针来共享相同元素的成员和分值,所以不会产生重复成员和分值,造成内存的浪费。

说明:其实有序集合单独使用字典或跳跃表其中一种数据结构都可以实现,但是这里使用两种数据结构组合起来,原因是假如我们单独使用 字典,虽然能以 O(1) 的时间复杂度查找成员的分值,但是因为字典是以无序的方式来保存集合元素,所以每次进行范围操作的时候都要进行排序;假如我们单独使用跳跃表来实现,虽然能执行范围操作,但是查找操作有 O(1)的复杂度变为了O(logN)。因此Redis使用了两种数据结构来共同实现有序集合。

②、编码转换

当有序集合对象同时满足以下两个条件时,对象使用 ziplist 编码:

1、保存的元素数量小于128;

2、保存的所有元素长度都小于64字节。

不能满足上面两个条件的使用 skiplist 编码。以上两个条件也可以通过Redis配置文件zset-max-ziplist-entries 选项和 zset-max-ziplist-value 进行修改

redis有序集合zset的底层实现——跳跃表skiplist

redis作为一种内存KV数据库,提供了string, hash, list, set, zset等多种数据结构。其中有序集合zset在增删改查的性质上类似于C++ stl的map和Java的TreeMap,提供了一组“键-值”对,并且“键”按照“值”的顺序排序。但是与C++ stl或Java的红黑树实现不同的是,redis中有序集合的实现采用了另一种数据结构——跳跃表。跳跃表是有序单链表的一种改进,其查询、插入、删除也是O(logN)的时间复杂度

跳跃表的思想来自于一篇论文:Skip Lists: A Probabilistic Alternative to Balanced Trees. 如果想要深入了解跳跃表,可以阅读论文原文。这里引用论文中的一幅图对跳跃表的原理作一个简单的说明。

上图用a,b,c,d,e五种有序链表及其变式(变式的名字是我随便起的)说明了跳跃表的motivation.

[a]单链表:查询时间复杂度O(n)
[b]level-2单链表:每隔一个节点为一个level-2节点,每个level-2节点有2个后继指针,分别指向单链表中的下一个节点和下一个level-2节点。查询时间复杂度为O(n/2)
[c]level-3单链表:每隔一个节点为一个level-2节点,每隔4个节点为一个level-3节点,查询时间复杂度O(n/4)
[d]指数式单链表:每2^i个节点的level为i+1,查询时间复杂度为O(log2N)
[e]跳跃表:各个level的节点个数同指数式单链表,但出现的位置随机,查询复杂度仍然是O(log2N)吗
之所以这里关心查询复杂度,因为有序链表的插入和删除复杂度等于查询复杂度。

作为一种概率性算法,文章证明了跳跃表查询复杂度的期望是O(logN).

redis有序集合采用跳跃表的原因

为什么redis的有序集合采用跳跃表而不是红黑树呢?对于这个问题,可以在https://news.ycombinator.com/item?id=1171423找到作者本人的一个回答

They are not very memory intensive. It’s up to you basically. Changing parameters about the probability of a node to have a given number of levels will make then less memory intensive than btrees.
A sorted set is often target of many ZRANGE or ZREVRANGE operations, that is, traversing the skip list as a linked list. With this operation the cache locality of skip lists is at least as good as with other kind of balanced trees.
They are simpler to implement, debug, and so forth. For instance thanks to the skip list simplicity I received a patch (already in Redis master) with augmented skip lists implementing ZRANK in O(log(N)). It required little changes to the code.
About the Append Only durability & speed, I don’t think it is a good idea to optimize Redis at cost of more code and more complexity for a use case that IMHO should be rare for the Redis target (fsync() at every command). Almost no one is using this feature even with ACID SQL databases, as the performance hint is big anyway.
About threads: our experience shows that Redis is mostly I/O bound. I’m using threads to serve things from Virtual Memory. The long term solution to exploit all the cores, assuming your link is so fast that you can saturate a single core, is running multiple instances of Redis (no locks, almost fully scalable linearly with number of cores), and using the “Redis Cluster” solution that I plan to develop in the future.

可以看到redis选择跳跃表而非红黑树作为有序集合实现方式的原因并非是基于并发上的考虑,因为redis是单线程的,选用跳跃表的原因仅仅是因为跳跃表的实现相较于红黑树更加简洁。

redis跳跃表的源码

跳跃表节点,跳跃表和zset结构体的定义在server.h

/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {sds ele;double score;struct zskiplistNode *backward;struct zskiplistLevel {struct zskiplistNode *forward;unsigned long span;} level[];
} zskiplistNode;typedef struct zskiplist {struct zskiplistNode *header, *tail;unsigned long length;int level;
} zskiplist;typedef struct zset {dict *dict;zskiplist *zsl;
} zset;

skiplist相关函数定义在t_zset.c中。跳跃表中,一个节点的level符合一定的概率,决定一个新增节点的level的函数是zslRandomLevel

/* Returns a random level for the new skiplist node we are going to create.* The return value of this function is between 1 and ZSKIPLIST_MAXLEVEL* (both inclusive), with a powerlaw-alike distribution where higher* levels are less likely to be returned. */
int zslRandomLevel(void) {int level = 1;while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))level += 1;return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

该函数被用于skiplist的节点插入函数zslInsert.

跳跃表的原理

zskiplist跳表结构保存跳跃表信息,表头、表尾、长度、最大层数
header:表头节点
tail:表尾节点
level:最大层数
length:跳表节点数量

zskiplistNode跳表节点每个节点层高1~32随机数

ele:sds字符串对象,保存节点的member成员,唯一的。
score:double类型的分数,从小到大排序。score相同,按照ele的字典顺序排序。
backward:后退指针,节点的prev节点,用于表尾向表头遍历。
level数组:每个元素都包含一个forward前进指针和span跨度。
forward:前进指针,每一层都有指向表尾方向的前进指针,用于实现多层链表。
span:跨度,记录两个节点之间的距离,用于计算rank排名。

跳跃表的思想来自于一篇论文:Skip Lists: A Probabilistic Alternative to Balanced Trees. 如果想要深入了解跳跃表,可以阅读论文原文。这里引用论文中的一幅图对跳跃表的原理作一个简单的说明。

上图用a,b,c,d,e五种有序链表及其变式(变式的名字是我随便起的)说明了跳跃表的motivation.

[a] 单链表:查询时间复杂度O(n)
[b] level-2单链表:每隔一个节点为一个level-2节点,每个level-2节点有2个后继指针,分别指向单链表中的下一个节点和下一个level-2节点。查询时间复杂度为O(n/2)
[c] level-3单链表:每隔一个节点为一个level-2节点,每隔4个节点为一个level-3节点,查询时间复杂度O(n/4)
[d] 指数式单链表:每2^i个节点的level为i+1,查询时间复杂度为O(log2N)
[e] 跳跃表:各个level的节点个数同指数式单链表,但出现的位置随机,查询复杂度仍然是O(log2N)吗
之所以这里关心查询复杂度,因为有序链表的插入和删除复杂度等于查询复杂度。

作为一种概率性算法,文章证明了跳跃表查询复杂度的期望是O(logN).

redis有序集合采用跳跃表的原因

为什么redis的有序集合采用跳跃表而不是红黑树呢?对于这个问题,可以在 https://news.ycombinator.com/item?id=1171423 找到作者本人的一个回答:

They are not very memory intensive. It’s up to you basically. Changing parameters about the probability of a node to have a given number of levels will make then less memory intensive than btrees.

A sorted set is often target of many ZRANGE or ZREVRANGE operations, that is, traversing the skip list as a linked list. With this operation the cache locality of skip lists is at least as good as with other kind of balanced trees.

They are simpler to implement, debug, and so forth. For instance thanks to the skip list simplicity I received a patch (already in Redis master) with augmented skip lists implementing ZRANK in O(log(N)). It required little changes to the code.

About the Append Only durability & speed, I don’t think it is a good idea to optimize Redis at cost of more code and more complexity for a use case that IMHO should be rare for the Redis target (fsync() at every command). Almost no one is using this feature even with ACID SQL databases, as the performance hint is big anyway.

About threads: our experience shows that Redis is mostly I/O bound. I’m using threads to serve things from Virtual Memory. The long term solution to exploit all the cores, assuming your link is so fast that you can saturate a single core, is running multiple instances of Redis (no locks, almost fully scalable linearly with number of cores), and using the “Redis Cluster” solution that I plan to develop in the future.

可以看到redis选择跳跃表而非红黑树作为有序集合实现方式的原因并非是基于并发上的考虑,因为redis是单线程的,选用跳跃表的原因仅仅是因为跳跃表的实现相较于红黑树更加简洁。

链接:https://blog.csdn.net/da_kao_la/article/details/94744886

redis里面是用skiplist是为了实现zset这种对外的数据结构。zset提供的操作非常丰富,可以满足许多业务场景,同时也意味着zset相对来说实现比较复杂。

skiplist数据结构简介

如图,跳表的底层是一个顺序链表,每隔一个节点有一个上层的指针指向下下一个节点,并层层向上递归。这样设计成类似树形的结构,可以使得对于链表的查找可以到达二分查找的时间复杂度。

按照上面的生成跳表的方式上面每一层节点的个数是下层节点个数的一半,这种方式在插入数据的时候有很大的问题。就是插入一个新节点会打乱上下相邻两层链表节点个数严格的2:1的对应关系。如果要维持这种严格对应关系,就必须重新调整整个跳表,这会让插入/删除的时间复杂度重新退化为O(n)。

为了解决这一问题,skiplist他不要求上下两层链表之间个数的严格对应关系,他为每个节点随机出一个层数。比如,一个节点的随机出的层数是3,那么就把它插入到三层的空间上,如下图。

那么,这就产生了一个问题,每次插入节点时随机出一个层数,真的能保证跳表良好的性能能么,

首先跳表随机数的产生,不是一次执行就产生的,他有自己严格的计算过程,

1首先每个节点都有最下层(第1层)指针

2如果一个节点有第i层指针,那么他有第i层指针的概率为p。

3节点的最大层数不超过MaxLevel

我们注意到,每个节点的插入过程都是随机的,他不依赖于其他节点的情况,即skiplist形成的结构和节点插入的顺序无关。

这样形成的skiplist查找的时间复杂度约为O(log n)。

redis中的skiplist

当数据较少的时候,zset是由一个ziplist来实现的
当数据较多的时候,zset是一个由dict 和一个 skiplist来实现的,dict用来查询数据到分数的对应关系,而skiplist用来根据分数查询数据。
为了支持排名rank查询,redis中对skiplist做了扩展,使得根据排名能够快速查到数据,或者根据分数查到数据之后容易获得排名,二者都是O(log n)。

typedef struct zset{//跳跃表zskiplist *zsl;//字典dict *dice;
} zset;

  dict的key保存元素的值,字典的value保存元素的score,跳表节点的robj保存元素的成员,节点的score保存对应score。并且会通过指针来共享元素相同的robj和score。

skiplist的数据结构定义

//server.h<br>#define ZSKIPLIST_MAXLEVEL 32
#define ZSKIPLIST_P 0.25
typedef struct zskiplistNode {robj *obj;double score;struct zskiplistNode *backward;struct zskiplistLevel {struct zskiplistNode *forward;unsigned int span;} level[];
} zskiplistNode;typedef struct zskiplist {struct zskiplistNode *header, *tail;unsigned long length;int level;
} zskiplist;

开头定义了两个常量 ZSKIPLIST_MAXLEVEL和ZSKIPLIST_P,即上文所提到的p和maxlevel。

zskiplistNode表示skiplist的节点结构

obj字段存放节点数据,存放string robj。
score字段对应的是节点的分数。
backward字段是指向前一个节点的指针,节点只有一个向前指针,最底层是一个双向链表。
level[]存放各层链表的向后指针结构,包含一个forward ,指向对应层后一个节点;span字段指的是这层的指针跨越了多少个节点值,用于计算排名。(level是一个柔性数组,因此他占用的内存不在zskiplistNode里,也需要单独为其分配内存。)
zskiplist 定义了skiplist的外观,包含

header和tail指针
链表长度 length
level表示 跳表的最大层数

上图就是redis中一个skiplist可能的结构,括号中的数字代表 level数组中span的值,即跨越了多少个节点。

假设我们在这个skiplist中查找score=89的元素,在查找路径上,我们只需要吧所有的level指针对应的span值求和,就可以得到对应的排名;相反,如果查找排名的时候,只需要不断累加span保证他不超过指定的值就可以求得对应的节点元素。

三、REDIS_ENCODING_INTSET

redis中使用intset实现数量较少数字的set。
set-max-intset-entries 512
 实际上 intset是一个由整数组成的有序集合,为了快速查找元素,数组是有序的,用二分查找判断一个元素是否在这个结合上。在内存分配上与ziplist类似,用一块连续的内存保存数组元素,并且对于大整数和小证书 采用了不同的编码。

结构如下

//intset.h
typedef struct intset {uint32_t encoding;uint32_t length;int8_t contents[];
} intset;#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))

encoding 数据编码 表示intset中的每个元素用几个字节存储。(INTSET_ENC_INT16 用两个字节存储,即两个contents数组位置 INTSET_ENC_INT32表示4个字节 INTSET_ENC_INT64表示8个字节)

length 表示inset中元素的个数

contents 柔性数组,表示存储的实际数据,数组长度 = encoding * length。

另外,intset可能会随着数据的添加改编他的编码,最开始创建的inset使用 INTSET_ENC_INT16编码。

如上图 intset采用小端存储。

关于插入逻辑。

intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {uint8_t valenc = _intsetValueEncoding(value);uint32_t pos;if (success) *success = 1;/* Upgrade encoding if necessary. If we need to upgrade, we know that* this value should be either appended (if > 0) or prepended (if < 0),* because it lies outside the range of existing values. */if (valenc > intrev32ifbe(is->encoding)) {/* This always succeeds, so we don't need to curry *success. */return intsetUpgradeAndAdd(is,value);} else {/* Abort if the value is already present in the set.* This call will populate "pos" with the right position to insert* the value when it cannot be found. */if (intsetSearch(is,value,&pos)) {if (success) *success = 0;return is;}is = intsetResize(is,intrev32ifbe(is->length)+1);if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);}_intsetSet(is,pos,value);is->length = intrev32ifbe(intrev32ifbe(is->length)+1);return is;
}

intsetadd在intset中添加新元素value。如果value在添加前已经存在,则不会重复添加,这个时候success设置值为0

如果要添加的元素编码比当前intset的编码大。调用intsetUpgradeAndAdd将intset的编码进行增长,然后插入。

调用intsetSearch 如果能查找到,不会重复添加。没查到调用intsetResize对其进行扩容(realloc),同时intsetMoveTail将带插入位置后面的元素统一向后移动一个位置。返回值是一个新的intset指针,替换原来的intset指针,总的时间复杂度为O(n)。

redis缓存数据库中zset数据结构底层算法实现原理:ziplist 和 skiplist相关推荐

  1. 05-如何全部清除redis缓存数据库中的缓存数据

    在redis缓存数据库的使用过程中,有时会遇到因为连接不同的数据库导致redis缓存数据库中缓存了多个数据库的信息,产生脏数据进而影响程序的正常运行 如何一次性清除所有的缓存数据让redis重新缓存? ...

  2. redis缓存数据库中的bitmap到底是个什么鬼?

    bitmap位图 语法 SETBIT key offset value GETBIT key offset 优点 计算效率极高: 及其节省空间(二进制),几亿人的状态也就几十兆空间: 案例 比如查询用 ...

  3. iOS标准库中常用数据结构和算法之内存池

    上一篇:iOS标准库中常用数据结构和算法之位串 ⛲️内存池 内存池提供了内存的复用和持久的存储功能.设想一个场景,当你分配了一块大内存并且填写了内容,但是你又不是经常去访问这块内存.这样的内存利用率将 ...

  4. Redis缓存数据库服务器

    Redis缓存数据库服务器 Redis是一个开源的科技与内存也可持久化的日志型.Key-Value数据库 Redis的存储分为内存存储.磁盘存储和Log文件三部分,配置文件中有三个参数对其进行配置. ...

  5. Redis 缓存数据库

    Redis 缓存数据库 第1章 Redis简介: redis是使用C语言编写的开源的,支持网络,基于内存,可持久性的键值对存储数据库,2013年5月之前,Redis是最流行的键值对存储数据库 Redi ...

  6. JavaScript中的数据结构和算法

    JavaScript不仅是一门用于网页交互的脚本语言,还可以用于编写高效的数据结构和算法.在本文中,我们将介绍JavaScript中可用的数据结构和常见的算法,并说明它们在实际应用中的用途和性能. 数 ...

  7. 【免费使用】【redis】【数据库】快速使用redislabs免费套餐 注册和配置redis 缓存 数据库 nosql

    [免费]快速使用redislabs免费套餐 注册和配置redis 缓存数据库 简介 视频教程 操作步骤 参考资料 结束の语 今日问答 YOUTUBE频道 欢迎订阅我的bilibili频道 [免费]快速 ...

  8. redis 清空db下_Redis常用命令集,清空redis缓存数据库

    清空数据库: flushdb   // 清除当前数据库的所有keys flushall    // 清除所有数据库的所有keys Redis常用命令集,清空redis缓存数据库 1)连接操作命令qui ...

  9. 在Object-C中学习数据结构与算法之排序算法

    笔者在学习数据结构与算法时,尝试着将排序算法以动画的形式呈现出来更加方便理解记忆,本文配合Demo 在Object-C中学习数据结构与算法之排序算法阅读更佳. 目录 选择排序 冒泡排序 插入排序 快速 ...

最新文章

  1. SAP WMSD集成之Copy WM Quantity – Copy WM qty as delivery qty into delivery
  2. wxpython的sizer_wxPython BoxSizer布局
  3. 1736. 替换隐藏数字得到的最晚时间
  4. 从零开始学android开发-布局中 layout_gravity、gravity、orientation、layout_weight
  5. [转]spring入门(六)【springMVC中各数据源配置】
  6. java enum 泛型,Java Enum作为Enum中的泛型类型
  7. 为何new出的对象数组必须要用delete[]删除,而普通数组delete和delete[]都一样-------_CrtMemBlockHeader
  8. mac 没有java_maven在Mac OS X上没有使用Java
  9. Linux下如何解压.zip和.rar文件
  10. 调通sina33下的AP6212A0版本的BT(V1.0)
  11. iptables、firewalld和ufw区别linux
  12. TCL电子软件开发生活记录(更新中)
  13. Altium Designer快捷键布线无法实现网络线自动编号
  14. 远程桌面访问计算机的步骤,如何开启远程桌面连接功能
  15. 无人驾驶汽车系统入门(二十三)——迁移学习和端到端无人驾驶
  16. 病毒分析报告-熊猫烧香
  17. node.js中公培训笔记大全(讲的一般,小白基础入门)
  18. tomorrow - 明天
  19. 从mybatis拦截器维度处理读写分离的多数据源问题
  20. 图像处理——饱和度与色调

热门文章

  1. 正高职称计算机考试题,职称计算机考试试题高分技巧
  2. 1376:信使(msner)(dijkstar)
  3. Java学习-14 CSS与CSS3美化页面及网页布局
  4. DGIOT实战教程——甲烷传感器接入
  5. 带你走进虚拟化世界之kvm(转载)
  6. 合理的薪酬机制是网格营销的关键
  7. 手机端调起支付宝支付
  8. 言必信,行必果,硁硁然小人哉!
  9. Linux系列命令 —— chmod,chown 权限命令详解
  10. 给麦田PT做的新主题:Flower