【Redis】深入理解 Redis 常用数据类型源码及底层实现(6.详解Set和ZSet数据结构)

恭谢啦~这是深入理解 Redis 常用数据类型源码及底层实现系列的最后一篇~由于前面的铺垫,对于大多数知识不会重复赘述,感兴趣的朋友可以移步到如下文章( ̄∇ ̄)🎉~

【Redis】深入理解 Redis 常用数据类型源码及底层实现(1.结构与源码概述)

【Redis】深入理解 Redis 常用数据类型源码及底层实现(2.版本区别+dictEntry & redisObject详解)

【Redis】深入理解 Redis 常用数据类型源码及底层实现(3.详解String数据结构)

【Redis】深入理解 Redis 常用数据类型源码及底层实现(4.详解Hash数据结构)

【Redis】深入理解 Redis 常用数据类型源码及底层实现(5.详解List数据结构)

好啦~安利结束~正餐开始!

Set数据结构

Set数据结构相对其他的数据结构比较简单,因为从Redis 6到Redis 7都没变~底层都是哈希表+整数数组,即使用intset和hashtable存储set的,如果元素都是整数类型就使用intset,如果不是则使用hashtable(数组+链表,key是元素值,value是null)

我们看下当执行命令sadd时,Redis底层到底做了些什么

在方法setTypeCreate()中,我们可以看到当集合元素都是LongLong类型并且集合元素个数<=server.set_max_intset_entries时,就采用intset的编码方式,不能同时满足上面两个条件的话则判断集合元素是否<=server.set_max_listpack_entrie,如果满足则采用listpack的编码方式(Redis 7),不满足则调用方法createSetObject()

方法createSetObject()中则采用hashtable的编码方式

上面源码中的参数server.set_max_intset_entries和server.set_max_listpack_entrie声明于文件server.h中

我们按照注释到redis.conf中就可以看到默认配置啦

我们总结下,对于Set数据结构,Redis 6 和Redis 7的底层都是intset和hashtable,当添加的数据是LongLong类型并且集合元素数<=512(默认配置,一般不作修改)时,就会使用intset的编码方式,反之则使用hashtable的编码方式。

ZSet数据结构

ZSet有两种编码格式,并且跟之前介绍的Hash数据结构类似,Redis 6是ziplist和skiptable而Redis 7是listpack和skiptable,我们先来看下不同版本基本配置信息的区别:

我们分别介绍下这些配置参数的含义:

  • zset-max-ziplist-entries:使用压缩列表保存数据时,zset有序集合中最大的元素个数(默认128)
  • zset-max-ziplist-value:使用压缩列表保存数据时,zset有序集合中单个元素的最大长度(默认64)
    • 单位byte:一个英文字母一个byte
  • zset-max-listpack-entries:使用紧凑列表保存数据时,zset有序集合中最大的元素个数(同样默认128)
  • zset-max-listpack-value:使用紧凑列表保存数据时,zset有序集合中单个元素的最大长度(默认64)

Redis 7兼容ziplist,当我们修改zset-max-listpack-entries和zset-max-listpack-value时,对应的zset-max-ziplist-entries和zset-max-ziplist-value也会被修改:

反之,当修改zset-max-ziplist-entries和zset-max-ziplist-value时,对应的zset-max-listpack-entries和zset-max-listpack-value也会被修改:

而且,当集合中的数据元素超过zset-max-ziplist-entries/zset-max-listpack-entries(不包括等于),或者元素长度大于zset-max-ziplist-value/zset-max-listpack-value时(不包括等于),底层会使用skiptable进行数据的存储,我们可以做下测试:

t_zset.c

接下来我们从源码层面看下,当执行刚才测试的添加元素的操作时,Redis底层执行的逻辑是什么。

打开t_zset.c文件(以Redis 7为示例),找到方法zsetAdd()

这里可以看到ZSet的两种编码方式OBJ_ENCODING_LISTPACK和OBJ_ENCODING_SKIPLIST

skiplist

为什么会出现skiplist跳表?

一个新的东西产生一般都是由问题导致的,原有的部分产生了问题,或者说痛点,就像原有的链表和数组,他们就各有优缺点(数组插入删除慢,链表遍历慢),于是在寻求解决方法的时候,skiplist跳表就应运而生。

其实skiplist是通过升维对链表进行优化的,就是我们非常熟悉的空间换时间(还记不记得ziplist是时间换空间?),既然链表遍历太慢,我们就给它加个(或者多个)索引,即"索引升级",我们来看张图大家应该就明白了( ̄∇ ̄)/

总的来说,skiplist跳表是可以实现二分查找的有序链表(跳表 = 链表 + 多级索引),是一种以空间换取时间的结构。由于链表无法进行二分查找,提取出链表中关键节点作为索引(借鉴数据库索引的思想),先在关键节点上查找,再进入下层链表查找,提取多层关键节点,就形成了跳跃表,但是索引也要占据一定空间的(索引添加的越多,空间占用的越多,空间换时间)

跳表的时间复杂度

我们举个🌰来分析下跳表的时间复杂度,有一个有N个结点的链表,两两取首,每两个节点抽出一个作为上一级索引的节点,于是:

第一级索引的结点个数大约就是 N/2(2=2^1),

第二级索引的结点个数大约就是 N/4(4=2^2),

第三级索引的结点个数大约就是 N/8(8=2^3),

......(依此类推

即第k级的节点个数是第k-1级的1/2

假设索引有h级,最高级的索引有2个结点。通过上面的公式

我们可以得到N/(2^h)=2,如果包含原始链表这一层,整个跳表的高度就是logN,因此其时间复杂度是O(logN),顺便提下,hash的间复杂度是O(1),整数数组、压缩链表、双向链表的时间复杂度都是间复杂度是O(N)。

相关推荐
Deepincode5 小时前
Redis源码探究系列—跳表(skiplist)源码实现详解
redis
古城小栈6 小时前
从 cargo-whero 库中,找到提升 rust 的契机
开发语言·后端·rust
keep one's resolveY6 小时前
SpringBoot实现重试机制的四种方案
java·spring boot·后端
阿丰资源8 小时前
基于Spring Boot的电影城管理系统(直接运行)
java·spring boot·后端
IT_陈寒8 小时前
SpringBoot自动配置的坑差点让我加班到天亮
前端·人工智能·后端
虹科网络安全9 小时前
艾体宝产品|深度解读 Redis 8.4 新增功能:原子化 Slot 迁移(下)
数据库·redis·bootstrap
消失的旧时光-19439 小时前
Spring Boot 工程化进阶:统一返回 + 全局异常 + AOP 通用工具包
java·spring boot·后端·aop·自定义注解
追风筝的人er10 小时前
SpringBoot+Vue3 企业考勤如何处理法定假期?节假日方案、调休补班与工作日判断链路拆解
前端·vue.js·后端
金銀銅鐵11 小时前
[git] 如何丢弃对一个文件的改动?
git·后端
橘子海全栈攻城狮11 小时前
【最新源码】养老院系统管理A013
java·spring boot·后端·web安全·微信小程序