Redis:Sorted Set类型底层数据结构剖析

本文涉及的产品
云原生内存数据库 Tair,内存型 2GB
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Redis 版,经济版 1GB 1个月
简介: Redis:Sorted Set类型底层数据结构剖析

Redis:Sorted Set类型底层数据结构剖析


文章目录

Redis:Sorted Set

有序集合对象有2种编码方案,当同时满足以下条件时,集合对象采用ziplist编码,否则采用skiplist编码:

  • 有序集合保存的元素数量不超过128个
  • 有序集合保存的所有元素的成员长度都小于64字节

其中,ziplist编码的有序集合采用压缩列表作为底层实现,skiplist编码的有序集合采用zset结构作为底层实现

其中,zset是一个复合结构,它的内部采用字典和跳跃表来实现,其源码如下:

typedef struct zset { 
  dict *dict;   // 字典,保存了从成员到分值的映射关系; 
  zskiplist *zsl; // 跳跃表,按分值由小到大保存所有集合元素; 
} zset;

其中成员:

  • dict 是字典的实现,保存了从成员到分支的映射关系
  • zsl 是跳跃表的实现则按分值由小到大保存了所有的集合元素

这样,当按照成员来访问有序集合时可以直接从dict中取值,当按照分值的范围访问有序集合时可以直接从zsl中取值,采用了空间换时间的策略以提高访问效率。

综上,zset对象的底层数据结构包括:压缩列表、字典、跳跃表。

ziplist:压缩列表

压缩列表(ziplist):是Redis为了节约内存而设计的一种线性数据结构,它是由一系列具有特殊编码的连续内存块构成的。一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或一个整数值。

压缩列表的结构如下图所示:

该结构当中的字段含义如下表所示:

属性 类型 长度 说明
zlbytes uint32_t 4字节 压缩列表占用的内存字节数;
zltail uint32_t 4字节 压缩列表表尾节点距离列表起始地址的偏移量(单位字节);
zllen uint16_t 2字节 压缩列表包含的节点数量,等于UINT16_MAX时,需遍历列表计算真实数量;
entryX 列表节点 不周定 压缩列表包含的节点,节点的长度由节点所保存的内容决定;
zlend uint8_t 1字节 压缩列表的结尾标识,是一个固定值0xFF;

其中,压缩列表的节点(entryX)由以下字段构成:

previous_entry_length(pel)属性以字节为单位,记录当前节点的前一节点的长度,其自身占据1字节或5字节:

  1. 如果前一节点的长度小于254字节,则“pel”属性的长度为1字节(8bit,28=256位),前一节点的长度就保存在这一个字节内;
  2. 如果前一节点的长度达到254字节,则“pel”属性的长度为5字节,其中第一个字节被设置为0xFE,之后的四个字节用来保存前一节点的长度;

基于“pel”属性,程序便可以通过指针运算,根据当前节点的起始地址计算出前一节点的起始地址,从而实现从表尾向表头的遍历操作。

content属性负责保存节点的值(字节数组或整数),其类型和长度则由encoding属性决定,它们的关系如下(了解):

encoding 长度 content
00 xxxxxx 1字节 最大长度为26 -1的字节数组;
01 xxxxxx bbbbbbbb 2字节 最大长度为214-1的字节数组;
10 __ bbbbbbbb … … … 5字节 最大长度为232-1的字节数组;
11 000000 1字节 int16_t类型的整数;
11 010000 1字节 int32_t类型的整数;
11 100000 1字节 int64_t类型的整数;
11 110000 1字节 24位有符号整数;
11 111110 1字节 8位有符号整数;
11 11xxxx 1字节 没有content属性,xxxx直接存[0,12]范围的整数值;

hashtable:字典

字典(dict)又称为散列表,是一种用来存储键值对的数据结构。C语言没有内置这种数据结构,所以Redis构建了自己的字典实现。

Redis字典的实现主要涉及三个结构体:字典、哈希表、哈希表节点。其中,每个哈希表节点保存一个键值对,每个哈希表由多个哈希表节点构成,而字典则是对哈希表的进一步封装

这三个结构体的关系如下图所示:

其中,dict代表字典,dictht代表哈希表,dictEntry代表哈希表节点。可以看出,dictEntry是一个数组,这很好理解,因为一个哈希表里要包含多个哈希表节点。而dict里包含2个dictht,多出的哈希表用于REHASH。

REHASH

REHASH 流程

当哈希表保存的键值对数量过多或过少时,需要对哈希表的大小进行扩展或收缩操作,在Redis中,扩展和收缩哈希表是通过REHASH实现的,执行REHASH的大致步骤如下:

  1. 为字典的ht[1]哈希表分配内存空间

如果执行的是扩展操作,则ht[1]的大小为第1个大于等于ht[0].used*2=n的2n(比如说ht[0]是6,6*2=12,大于等于12的第一个2的n次方是16=24,所以ht[1]大小是16)。如果执行的是收缩操作,则ht[1]的大小为第1个大于等于ht[0].used=n的2n

  1. 将存储在ht[0]中的数据迁移到ht[1]上,重新计算键的哈希值和索引值,然后将键值对放置到ht[1]哈希表的指定位置上。
  2. 将字典的ht[1]哈希表晋升为默认哈希表,迁移完成后,清空ht[0],再交换ht[0]和ht[1]的值,为下一次REHASH做准备。

下面为一个rehash的实例 :

h[0]的大小为4,那么2 * 4 = 8 ( 第一个大于等于8的 2的n次方是2 ^ 3 ) ,所以 h[1]大小设置为8

重新计算索引,并复制, h [0] 所有的键值都迁移到 h [1]

     

完成 rehash 之后的字典

REHASH 触发条件

当满足以下任何一个条件时,程序会自动开始对哈希表执行扩展操作:

  1. 服务器目前没有执行bgsave或bgrewriteof命令,并且哈希表的负载因子大于等于1;
  2. 服务器目前正在执行bgsave或bgrewriteof命令,并且哈希表的负载因子大于等于5。

Redis有一个机制,可以自动的扫描AOF文件,并且把冗余的操作进行合并,该机制由bgrewriteof命令实现,该命令在执行后,会将Redis中的数据以命令的方式保存起来,并替换原有的文件。

渐进式REHASH

为了避免REHASH对服务器性能造成影响,REHASH操作不是一次性地完成的,而是分多次、渐进式地完成的。

渐进式REHASH的详细过程如下:

  1. 为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表;
  2. 在字典中的索引计数器rehashidx设置为0,表示REHASH操作正式开始;
  3. 在REHASH期间,每次对字典执行添加、删除、修改、查找操作时,程序除了执行指定的操作外,还会顺带将ht[0]中位于rehashidx上的所有键值对迁移到ht[1]中,再将rehashidx的值加1;
  4. 随着字典不断被访问,最终在某个时刻,ht[0]上的所有键值对都被迁移到ht[1]上,此时程序将rehashidx属性值设置为-1,标识REHASH操作完成。

REHSH期间键值对访问规则

REHSH期间,字典同时持有两个哈希表,此时的访问将按照如下原则处理:

  1. 新添加的键值对,一律被保存到ht[1]中;
  2. 删除、修改、查找等其他操作,会在两个哈希表上进行,即程序先尝试去ht[0]中访问要操作的数据,若不存在则到ht[1]中访问,再对访问到的数据做相应的处理

skiplist:跳跃表

跳跃表的查找复杂度为平均O(logN),最坏O(N),效率堪比红黑树,却远比红黑树实现简单。跳跃表是在链表的基础上,通过增加索引来提高查找效率的。

有序链表插入、删除的复杂度为O(1),而查找的复杂度为O(N)。例:若要查找值为60的元素,需要从第1个元素依次向后比较,共需比较6次才行,如下图:

跳跃表是从有序链表中选取部分节点,组成一个新链表,并以此作为原始链表的一级索引。再从一级索引中选取部分节点,组成一个新链表,并以此作为原始链表的二级索引。以此类推,可以有多级索引,如下图:

跳跃表在查找时,优先从高层开始查找,若next节点值大于目标值,或next指针指向NULL,则从当前节点下降一层继续向后查找,这样便可以提高查找的效率了。

跳跃表的实现主要涉及2个结构体:zskiplistzskiplistNode,它们的关系如下图所示:

其中,蓝色的表格代表zskiplist,红色的表格代表zskiplistNode:

  • zskiplist有指向头尾节点的指针,以及列表的长度,列表中最高的层级
  • zskiplistNode的头节点是空的,它不存储任何真实的数据,它拥有最高的层级,但这个层级不记录在zskiplist之内


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
3天前
|
消息中间件 存储 NoSQL
Redis数据结构—跳跃表 skiplist 实现源码分析
Redis 是一个内存中的数据结构服务器,使用跳跃表(skiplist)来实现有序集合。跳跃表是一种概率型数据结构,支持平均 O(logN) 查找复杂度,它通过多层链表加速查找,同时保持有序性。节点高度随机生成,最大为 32 层,以平衡查找速度和空间效率。跳跃表在 Redis 中用于插入、删除和按范围查询元素,其内部节点包含对象、分值、后退指针和多个前向指针。Redis 源码中的 `t_zset.c` 文件包含了跳跃表的具体实现细节。
|
4天前
|
存储 NoSQL Redis
Redis数据结构—跳跃表 skiplist
Redis数据结构—跳跃表 skiplist
|
10天前
|
存储 算法 C++
【C++高阶】探索STL的瑰宝 map与set:高效数据结构的奥秘与技巧
【C++高阶】探索STL的瑰宝 map与set:高效数据结构的奥秘与技巧
15 0
|
10天前
|
Java
Redis19----RedisTemplate操作Hash类型
Redis19----RedisTemplate操作Hash类型
|
10天前
|
NoSQL Java Redis
Redis16-RedisTemplate快速入门,max -idle,min-idle,max-wait,用set的方法,opsForValue().set的方法
Redis16-RedisTemplate快速入门,max -idle,min-idle,max-wait,用set的方法,opsForValue().set的方法
|
10天前
|
NoSQL Redis
Redis11-----Sortedset类型,SortedSet底层是由数据树实现的,SortedSet删除同学,获取Amy同学分数,获取Rose同学排名,查询80分以下的学生,给Amy同学加2分
Redis11-----Sortedset类型,SortedSet底层是由数据树实现的,SortedSet删除同学,获取Amy同学分数,获取Rose同学排名,查询80分以下的学生,给Amy同学加2分
|
10天前
|
存储 NoSQL Java
Redis10------Set类型,存在着无序的特征存储的顺序和插入的顺序是无关的,set集合的一大特点是不可重复,在redis中支持交集插集等特殊功能,好友列表,共同关注等等
Redis10------Set类型,存在着无序的特征存储的顺序和插入的顺序是无关的,set集合的一大特点是不可重复,在redis中支持交集插集等特殊功能,好友列表,共同关注等等
|
10天前
|
NoSQL Java Redis
Redis09-----List类型,有序,元素可以重复,插入和删除快,查询速度一般,一般保存一些有顺序的数据,如朋友圈点赞列表,评论列表等,LPUSH user 1 2 3可以一个一个推
Redis09-----List类型,有序,元素可以重复,插入和删除快,查询速度一般,一般保存一些有顺序的数据,如朋友圈点赞列表,评论列表等,LPUSH user 1 2 3可以一个一个推
|
20天前
|
算法 C语言
【数据结构与算法 经典例题】使用栈实现队列(图文详解)
【数据结构与算法 经典例题】使用栈实现队列(图文详解)
|
14天前
|
存储 缓存 算法
堆和栈的区别及应用场景
堆和栈的区别及应用场景