Redis--SDS字符串与集合的底层实现原理

简单动态字符串SDS

Redis的key和value,其基础数据类型都是字符串。例如:Hash型value的field和value、List、Set、ZSet的元素类型都是字符串。

Redis自定义了一种字符串,这种字符串本身的结构比较简单,但功能强大,称为简单动态字符串,Simple Dynamic String (SDS).

但不是所有的字符串都是SDS,也有C语言的字符串。比如命令执行成功返回的"ok"就是C语言的字符串。C语言的字符串只会出现在字符串"字面常量"中,并且该字符串不可能发生变更。

SDS结构

SDS是一个结构体,定义在Redis安装目录下的src/sds.h中:

例如执行 set country "china" 时,country 和"china" 都是SDS类型,只不过一个是SDS的变量,一个是SDS的字面常量。"china"在内存中的结构如下:

SDS的优势

防止"字符串长度获取"性能瓶颈

对于c字符串,要获取其长度,则必须遍历整个字符串才可以拿到。对于超长的字符串遍历,会称为性能瓶颈。

SDS结构体中直接存放了字符串的长度数据,不用遍历字符串旧可以拿到长度数据,所以不会成为Redis的性能瓶颈。

保障二进制安全

​ c字符串中只能包含某种编码格式的字符,例如:ASCII、UTF-8等,并且除了字符串末尾外,其他位置是不能包含空字符"\0"的,否则该字符串就会被程序误解为提前结束。而在图片、音频、视频、压缩文件、office文件等二进制数据中,以空字符"\0"作为分隔符的情况很常见,因此在c字符串中不能保存像图片、音频、视频、压缩文件等二进制数据。

​ 但SDS不是以空字符"\0"作为字符串结束标志的,是通过len属性来判断字符串是否结束。所以对于程序处理SDS中的字符串数据,只需读取即可,无需遍历等操作。数据写入的是什么,读到的就是什么。

减少内存再分配次数

​ SDS采用了空间预分配策略和惰性空间释放策略来减少内存再分配次数。

空间预分配策略是指,每次SDS进行空间扩展时,程序不但为其分配所需的空间,还会为其分配额外的未使用空间,以减少内存再分配的次数。而额外分配的空间大小取决于空间扩展后SDS的len属性值:

  • 如果len<1M ,那么分配的未使用空间free =len 属性值。

  • 如果len>=1M ,那么分配的未使用空间free =1M。

SDS对于空间释放采用的是惰性空间释放策略。该策略指,SDS字符串长度如果缩短,那么多出来的空间暂时不释放,而是增加到free,以使后期扩展SDS时减少内存再分配次数。

​ 如果要释放SDS的未使用空间,可通过sdsRemoveFreeSpace()函数来释放

兼容C函数

为了能够兼容C函数,SDS的底层数组buf【】中的字符串仍以空字符"\0"结尾。

比如要比较两个字符串,一个是SDS,一个是C字符串,此时可以通过C语言函数:strcmp(sds_str->buf,c_str)

集合(Hash和ZSet)的底层实现原理

对于Hash和ZSet集合,底层的实现有两种:压缩列表zipList(Redis7.0之前使用的是zipList,之后使用的是listPack)和跳跃列表skipList。

这两种实现对于用户来说是透明的,但用户写入不同的数据,系统会自动使用不同的实现。

使用压缩列表zipList的条件:同时满足配置文件redis.conf中相关集合元素数据阈值与元素大小阈值两个条件。

使用跳跃列表skipList的条件:只要上面有一个条件不满足,就使用跳跃列表skipList。

复制代码
例如,对于ZSet集合中这两个条件如下:
1.集合元素个数小于redis.conf中zset-max-ziplist-entries属性的值,其默认值为128
2.每个集合元素大小都小于redis.conf中zset-max-ziplist-value属性的值。默认值为64字节。

zipList(Hash和ZSet底层)

zipList 压缩列表,是一个经过特殊编码的用于存储字符串或整数的双向链表

底层结构由三部分构成:head、entries、end。这三部分在内存上是连续的。

head由三部分构成:

  • zlbytes:占4个字节,存放zipList列表整体数据结构所占的字节数,包括zlbytes本身的长度。

  • zltail:占4个字节,存放zipList中最后一个entry在整个数据结构中的偏移量(字节)。该数据的存在可以快速定位到列表的尾entry位置,方便操作。

  • zllen:占2个字节,存放列表包含entry的个数,由于其只有16位,所以zipList最多可以含有entry的个数为2²-1=65535(实际上可以更多!)。

entries

entries是真正的列表,由很多列元素entry构成。由于不同的元素类型、数值的不同,从而导致每个entry的长度不同。

​ 每个entry由三部分构成:

  • prevlength:记录上一个entry的长度,以实现逆序遍历。默认长度为1字节,只要上一个entry长度<254字节,prevlength 就占1个字节,否则会自动扩展为3个字节。

  • encoding:标志后面data的具体类型。如果data是整数型,encoding固定长度为1字节。

​ 如果data是字符串类型,则encoding长度可能是1字节、2字节或5字节。

​ data字符串不同的长度,对应不同的encoding长度。

  • data:真正存储的数据。数据类型只能是整数型或字符串类型。不同的数据占用的字节长度不同。

end

end只包含一部分,称为zlend。占1个字节,值固定为255,即二进制位全为1,表示一个zipList 列表的结束。

listPack

​ 由于zipList实现复杂,为了逆序遍历,每个entry中包含前一个entry的长度,这样导致在zipList中间修改或插入entry时需要进行级联更新。在高并发的写操作下会极度降低Redis性能。为了实现更紧凑、更快的解析,更简单的实现,重写了zipList,命名为listPack。

​ 从Redis7开始,listPack替换了zipList,但为了兼容性,在配置中也保留了zipList的相关属性。

​ listPack 也是一个经过特殊编码的用于存储字符串或整数的双向链表。其底层数据结构也由三部分构成:head、entries、end,且这三部分在内存上也是连续的。

​ listPack与zipList的区别在于head和每个entry的结构上,表示列表结束end与zipList相同,占一个字节,且8位全为1.

head有两部分构成:

  • totalBytes:占4字节,存放listPack列表整体数据结构所占的字节数,包括totalBytes本身的长度。

  • elemNum:占2字节,存放列表包含的entry的个数。其意义与zipList中的zllen相同。

与zipList中的head相比,没有了记录最后一个entry偏移量的zltail。

entries

​ entries也是listPack中真正的列表,由很多的列表元素entry构成。由于元素类型、数值不同,导致每个entry的长度不同。但与zipList的entry结构相比,listPack的entry结构发生了很大变化。

​ 最大的变化就是没有了记录前一个entry长度的prevlength,而增加了记录当前entry长度的element-total-len。这个改变也可以实现逆序遍历,却避免了由于在列表中间修改或插入entry时引发的级联更新问题。

​ 每个entry仍由三部分构成:

  • encoding:标志后面data的具体类型。

​ 如果data是整数型,encoding长度可能是1、2、3、4、5或9字节

​ 如果data是字符串类型,encoding长度可能是1、2或5字节

​ data字符串长度不同对应着不同的encoding长度。

  • data:真正存储的数据。只能是整数型或字符串类型。

  • element-total-len:记录当前entry的长度,用于实现逆序遍历。其本身占有的字节数据可能会是1、2、3、4、5字节。

skipList(ZSet底层)

skipList 跳跃列表,简称跳表,是一种随机化的数据结构,基于并联的链表,实现简单,查找效率高。

简单来说跳表也是链表的一种,只不过它在链表的基础上增加了跳跃功能。也正是这个跳跃功能,使得在查找元素时,能够提高效率。

skipList原理

普通的有序列表如下:

如果要插入一个元素,需要从头到尾一个一个对比,然后插入到指定位置。

增加偶数节点的层级,这样在插入元素时,先对比高层节点找到位置后再对比低层节点,然后插入,这样比上一种结构的对比次数少,效率高。

为了提高插入效率,可以再增加一级链表,这样比较次数就更少了。

这种分级链表是按照一定的规则设置的,但是如果我删掉某个节点,那么这个节点后面的所有节点的层级都要重新计算,这样会消耗系统资源。

那怎么解决呢?

使一个节点的层级由随机决定,这样删掉某个节点后,后面的节点保持不变即可。

quickList(List底层)

quickList 快速列表,quickList本身是一个双向无循环链表,它的每一个节点都是一个zipList。

从Redis3.2开始,对于List的底层实现 ,使用quickList代替了zipList和linkedList。

zipList和linkedList都有明显不足,而quickList对他们进行了改进,吸取了zipList和linkedList的优点,避开了他们的不足。

quickList本质上是zipList和linkedList的混合体 。其将linkedList按段切分,每一段使用zipList来紧凑存储若干真正的数据元素,多个zipList之间使用双向指针串接起来。

对于每个zipList中最多可存放多大容量的数据元素,在配置文件中通过list-max-ziplist-size属性来指定。

检索操作

对于List元素的检索,都是以其索引index为依据的。

quickList 由一个个zipList组成,每个zipList的zllen中记录当前zipList中包含entry的个数,即包含真正数据元素的个数。

根据要检索的元素的index,从quickList的头节点开始,逐个对zipList的zllen做sum求和,直到找到第一个求和后sum>index的zipList,那么要检索的元素就在这个zipList中。

插入操作

key与value中元素的数量

Redis中支持的key的数量、集合value中支持的元素数量非常庞大。

  • Redis最多可以处理232 个key(约42亿),并且经过测试,每个Redis实例至少可以处理2.5亿个key。

  • 每个Hash、LIst、Set、ZSet集合都可以包含232 个元素。

文章转载自: ++NE_STOP++

原文链接: https://www.cnblogs.com/alineverstop/p/19952178

体验地址: http://www.jnpfsoft.com/?from=430

相关推荐
oradh1 小时前
Oracle XTTS实现跨版本迁移和升级(Oracle 11g单库升级至19C RAC集群)
数据库·oracle·11g升级19c·xtts跨版本迁移和升级
z123456789862 小时前
2026最新两款AI编程工具深度对比实测
java·数据库·ai编程
程序猿DD2 小时前
一个 API Key,统一调用大模型、生图和联网搜索
数据库·网关
TlSfoward3 小时前
抓包代理链路下的 TLS 指纹变化分析 TLSFOWARD抓包工具
数据库·爬虫·网络协议·搜索引擎·php
数智化管理手记3 小时前
账龄分析手工统计易遗漏?自动账龄分析工具怎么搭建
大数据·网络·数据库·人工智能·数据挖掘
LabVIEW开发3 小时前
NI Package Manager安装LabVIEW时InternalServerError错误
网络·数据库·labview·labview知识·labview功能·labview程序
SelectDB4 小时前
PostgreSQL 实时同步到 Apache Doris:一站式 CDC 解决方案
数据库
Database_Cool_4 小时前
AI Agent 长期记忆怎么存?阿里云 PolarDB-X 对接 Mem0 记忆框架实践
数据库·阿里云
时空无限4 小时前
vllm 缓存对模型启动时间的影响
缓存·vllm
会编程的土豆4 小时前
数据库范式
数据库·oracle