11- Redis 中的 SDS 数据结构

news2025/1/22 12:11:44

字符串在 Redis 中是很常用的,键值对中的键是字符串类型,值有时也是字符串类型。

Redis 是用 C 语言实现的,但是它没有直接使用 C 语言的 char* 字符数组来实现字符串,而是自己封装了一个名为简单动态字符串(simple dynamic string,SDS)的数据结构来表示字符串,也就是 Redis 的 String 数据类型的底层数据结构是 SDS。

既然 Redis 设计了 SDS 结构来表示字符串,肯定是 C 语言的 char* 字符数组存在一些缺陷。

要了解这一点,得先来看看 char* 字符数组的结构。

1. C 语言字符串的缺陷

C 语言的字符串其实就是一个字符数组,即数组中每个元素是字符串中的一个字符。

比如:

char* name = "abc"

字符数组的结构对应为:

a,b,c,\0

在 C 语言里,对字符串操作时,char * 指针只是指向字符数组的起始位置,而字符数组的结尾位置就用“\0”表示,意思是指字符串的结束

因此,C 语言标准库中的字符串操作函数就通过判断字符是不是“\0”来决定要不要停止操作,如果当前字符不是“\0”,说明字符串还没有结束,可以继续操作,如果当前字符是“\0”则说明字符串结束了,就要停止操作。

举个例子,C 语言获取字符串长度的函数 strlen,就是通过字符数组中的每一个字符,并进行计数,等遇到字符为"\0"后,就会停止遍历,然后返回已统计到的字符个数,即为字符串长度。

很明显,C 语言获取字符串长度的时间复杂度是 O(N) (这是一个可以改进的地方)

C 语言字符串用“\0”字符作为结尾标记有个缺陷。假设有个字符串中有个“\0”字符,这时在操作这个字符串时就会提早结束,比如“ab\0c“字符串,计算字符串长度的时候则会是 4。

因此,除了字符串的末尾之外,字符串里面不能还有“\0”字符,否则最先被程序读入的“\0”字符将被误认为是字符串结尾,这个限制使得 C 语言的字符串只能保存文本数据,不能保存像图片、音频、视频这样的二进制数据 (这也是一个可以改进的地方)

另外,C 语言标准库中字符串的操作函数是很不安全的,对程序员很不友好,稍微一不注意,就会导致缓冲区溢出。

举个例子,strcat 函数是可以将两个字符串拼接在一起。

// 将 src 字符串拼接到 dest 字符串后面
char *strcat(char *dest, const char* src);

C 语言的字符串是不会记录自身缓冲区大小的,所以 strcat 函数假定程序员在执行这个函数时,已经为 dest 分配了足够多的内存,可以容纳 src 字符串中的所有内容,而一旦这个假定不成立,就会发生缓冲区溢出可能会造成程序运行终止 (这也是一个可以改进的地方)

好了,通过以上的分析,我们可以得知 C 语言的字符串不足之处以及可以改进的地方:

  • 获取字符串长度的时间复杂度为 O(N);

  • 字符串的结尾是以“\0”字符表示,字符串中不能包含有“\0”字符,因此不能保存二进制数据;

  • 字符串操作函数不高效且不安全,比如有缓冲区溢出的风险,有可能会造成程序运行终止;

Redis 实现的 SDS 的结构就把上面的这些问题解决了,我们看看 Redis 是如何解决的。

2. SDS 结构设计

下面是 Redis 5.0 的 SDS 数据结构:

结构中的每个成员变量分别是:

  • len,记录了字符串长度。这样获取字符串长度的时候,只需要返回这个成员变量值就行,时间复杂度只需要 O(1)。

  • alloc,分配给字符数组的空间长度。这样在修改字符串的时候,可以通过 alloc - len 计算出剩余的空间大小,可以用来判断空间是否满足修改需求,如果不满足的话,就会自动将 SDS 的空间扩展至执行修改所需要的大小,然后才执行实际的修改操作,所以使用 SDS 既不需要手动修改 SDS 的空间大小,也不会出现前面所说的缓冲区溢出的问题。

  • flags,用来表示不同类型的 SDS。一共设计了 5 种类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64,后面再说明区别之处。

  • buf[],字符数组,用来保存实际数据。不仅可以保存字符串,也可以保存二进制数据。

总的来说,Redis 的 SDS 结构在原本字符数组之上,增加了三个元数据:len、alloc、flags,用来解决 C 语言字符串的缺陷。

2.1 O(1)复杂度获取字符串长度

C 语言的字符串长度获取 strlen 函数,需要通过遍历的方式来统计字符串长度,时间复杂度是 O(N)。

而 Redis 的 SDS 结构因为加入了 len 成员变量,那么获取字符串长度的时候,直接返回这个成员变量的值就行,所以复杂度只有 O(1)

2.2 二进制安全

因为 SDS 不需要用 “\0”字符来表示字符串结尾了,而是有个专门的 len 成员变量来记录长度,所以可存储包含 “\0”的数据。但是 SDS 为了兼容部分 C 语言标准库的函数,SDS 字符串结尾还是会加上 “\0” 字符。

因此,SDS 的 API 都是以处理二进制的方式来处理 SDS 存放在 buf[] 里的数据,程序不会对其中的数据做任何限制,数据写入的时候是什么样的,它被读取时就是什么样的。

通过使用二进制安全的 SDS,而不是 C 字符串,使得 Redis 不仅可以保存文本数据,也可以保存任意格式的二进制数据。

2.3 不会发生缓冲区溢出

C 语言的字符串标准库提供的字符串操作函数,大多数(比如 strcat 追加字符串函数)都是不安全的,因为这些函数把缓冲区大小是否满足操作需求的工作交由开发者来保证,程序内部并不会判断缓冲区大小是否足够用,当发生了缓冲区溢出就有可能造成程序异常结束。

所以,Redis 的 SDS 结构里引入了 alloc 和 len 成员变量,这样 SDS API 通过 alloc - len计算,可以算出剩余可用的空间大小,这样在对字符串做修改操作的时候,就可以由程序内部判断缓冲区大小是否足够用。

而且,当判断出缓冲区大小不够用时,Redis 会自动扩大 SDS 的空间大小,以满足修改所需的大小。

SDS 扩容的规则代码如下:

hisds hi_sdsMakeRoomFor(hisds s, size_t addlen)
{
    ... ...
    // s 目前的剩余空间已足够,无需扩展,直接返回
    if (avail >= addlen)
        return s;
    // 获取目前 s 的长度
    len = hi_sdslen(s);
    sh = (char *)s - hi_sdsHdrSize(oldtype);
    // 扩展之后,s 至少需要的长度
    newlen = (len + addlen);
    // 根据新长度,为 s 分配新空间所需的大小
    if (newlen < HI_SDS_MAX_PREALLOC)
        // 新长度 < HI_SDS_MAX_PREALLOC 则分配所需空间 *2 的空间
        newlen *= 2;
    else
        // 否则,分配长度为目前长度 +1 MB
        newlen += HI_SDS_MAX_PREALLOC;
    ...
}
  • 如果所需的 sds 长度小于 1 MB,那么最后的扩容是按照翻倍扩容来执行的,即 2 倍 的 newlen

  • 如果所需的 sds 长度超过 1 MB,那么最后的扩容长度应该是 newlen + 1MB

在扩容 SDS 空间之前,SDS API 会优先检查未使用空间是否足够,如果不够的话,API 不仅会为 SDS 分配修改所必须要的空间,还会给 SDS 分配额外的【未使用空间】。

这样的好处是,下次再操作 SDS 时,如果 SDS 空间够的话,API 就会直接使用【未使用空间】,而无须执行内存分配,有效的减少内存分配次数

所以,使用 SDS 既不需要手动修改 SDS 的空间大小,也不会出现缓冲区溢出的问题。

2.4 节省内存空间

SDS 结构中有个 flags 成员变量,表示的是 SDS 类型。

Redis 一共设计了 5 种类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64。

这 5 中类型的主要区别就在于,它们数据结构中的 len 和 alloc 成员变量的数据类型不同

比如 sdshdr16 和 sdshdr32 这两个类型,它们的定义分别如下:

struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len;
    uint16_t alloc; 
    unsigned char flags; 
    char buf[];
};
​
​
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len;
    uint32_t alloc; 
    unsigned char flags;
    char buf[];
};

可以看到:

  • sdshdr16 类型的 len 和 alloc 的数据类型都是 uint16_t,表示字符数组长度和分配空间大小不能超过 2 的 16 次方。

  • sdshdr32 则都是 uint32_t,表示字符数组长度和分配空间大小不能超过 2 的 32 次方。

之所以 SDS 设计不同类型的结构体,是为了能灵活保存不同大小的字符串,从而有效节省内存空间。比如,在保存小字符串时,结构头占用空间也比较小。

除了设计不同类型的结构体,Redis 在编程上还使用了专门的编译优化来节省内存空间,即在 struct 声明了 __attribute__ ((packed)) ,它的作用是:告诉编译器取消结构体在编译过程中的优化对齐,按照实际占用字节数进行对齐。

比如,sdshdr16 类型的 SDS,默认情况下,编译器会按照 2 字节对齐的方式给变量分配内存,这意味着,即使一个变量的大小不到 2 个字节,编译器也会给它分配 2 个字节。

举个例子,假设下面这个结构体,它有两个成员变量,类型分别是 char 和 int,如下所示:

#include <stdio.h>
​
struct test1 {
    char a;
    int b;
 } test1;
 
int main() {
     printf("%lu\n", sizeof(test1));
     return 0;
}

这个结构体大小计算出来是 8:

这是因为默认情况下,编译器是使用【字节对齐】的方式分配内存,虽然 char 类型只占一个字节,但是由于成员变量有 int 类型,它占用 4 个字节,所以在成员变量为 char 类型分配内存时,会分配 4 个字节,其中这多余的 3 个字节是为了字节对齐而分配的,相当于有 3 个字节被浪费掉了。

如果不想编译器使用字节对齐的方式进行分配内存,可以采用 __attribute__((packed)) 属性定义结构体,这样一来,结构体实际占用多少内存空间,编译器就分配多少空间。

比如,我用 __attribute__ ((packed)) 属性定义下面的结构体 ,同样包含 char 和 int 两个类型的成员变量,代码如下所示:

#include <stdio.h>
​
struct __attribute__((packed)) test2  {
    char a;
    int b;
 } test2;
 
int main() {
     printf("%lu\n", sizeof(test2));
     return 0;
}

这是打印的结果是 5 (1 个字节 char + 4 字节 int)。

可以看得出,这是按照实际占用字节数进行分配内存的,这样可以节省内存空间。

内存对齐:

是一种典型的“空间换时间”,通过牺牲一部分内存空间(通过填充来保持数据对齐),可以显著提高 CPU 访问内存的效率。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1719179.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于强化学习的控制率参数自主寻优

1.介绍 针对控制建模与设计场景中控制参数难以确定的普遍问题&#xff0c;提出了一种基于强化学习的控制律参数自主优化解决方案。该方案以客户设计的控制律模型为基础&#xff0c;根据自定义的控制性能指标&#xff0c;自主搜索并确定最优的、可状态依赖的控制参数组合。 可…

ToDesk提示会话数通道限制 - 解决方案及兑惠码分享

如果您最近在体验ToDesk这款远程操控工具时&#xff0c;遇到了提示信息告知“高速通道服务已到期”或“会话数受限”&#xff0c;这表明您本月享受的免费额度——即120小时的使用时间和最多300次的连接机会——已经耗尽。为了解锁无限制的使用时长与连接次数&#xff0c;建议您…

自动驾驶中的长尾问题

自动驾驶中的长尾问题 定义 长尾问题&#xff08;Long-Tail Problem&#xff09;是指在数据分布中&#xff0c;大部分的数据集中在少数类别上&#xff0c;而剩下的大多数类别却只有少量的数据。这种数据分布不平衡的现象在许多实际应用中广泛存在&#xff0c;特别是在自动驾驶…

20240531在飞凌的OK3588-C开发板上跑原厂的Buildroot测试USB摄像头

20240531在飞凌的OK3588-C开发板上跑原厂的Buildroot测试USB摄像头 2024/5/31 20:04 USB摄像头分辨率&#xff1a;1080p&#xff08;1920x1080&#xff09; 默认编译Buildroot的SDK即可点亮USB摄像头。v4l2-ctl --list-devices v4l2-ctl --list-formats-ext -d /dev/video74 …

双指针法 ( 快乐数 )

「快乐数」 定义为&#xff1a; 对于一个正整数&#xff0c;每一次将该数替换为它每个位置上的数字的平方和。然后重复这个过程直到这个数变为 1&#xff0c;也可能是 无限循环 但始终变不到 1。如果这个过程 结果为 1&#xff0c;那么这个数就是快乐数 编写一个算法来判断一个…

linux /www/server/cron内log文件占用空间过大,/www/server/cron是什么内容,/www/server/cron是否可以删除

linux服务器长期使用宝塔自带计划任务&#xff0c;计划任务执行记录占用服务器空间过大&#xff0c;导致服务器根目录爆满&#xff0c;需要长期排查并删除 /www/server/cron 占用空间过大问题处理 /www/server/cron是什么内容&#xff1f;/www/server/cron是否可以删除&#xf…

基于VGG16使用图像特征进行迁移学习的时装推荐系统

前言 系列专栏:【深度学习&#xff1a;算法项目实战】✨︎ 涉及医疗健康、财经金融、商业零售、食品饮料、运动健身、交通运输、环境科学、社交媒体以及文本和图像处理等诸多领域&#xff0c;讨论了各种复杂的深度神经网络思想&#xff0c;如卷积神经网络、循环神经网络、生成对…

AutoMQ 自动化持续测试平台技术内幕

01 背景 AutoMQ[1] 作为一款流系统&#xff0c;被广泛应用在客户的核心链路中&#xff0c;对可靠性的要求非常的高。所以我们需要一套模拟真实生产场景、长期运行的测试环境&#xff0c;在注入各种故障场景的前提下验证 SLA 的可行性&#xff0c;为新版本的发布和客户的使用提…

c# - 运算符 << 不能应用于 long 和 long 类型的操作数

Compiler Error CS0019 c# - 运算符 << 不能应用于 long 和 long 类型的操作数 处理方法 特此记录 anlog 2024年5月30日

【论文精读】SAM

摘要 本文提出Segment Anything&#xff08;SA&#xff09;&#xff0c;一个可prompt的视觉分割模型&#xff0c;通过一个 能实现视觉特征强大泛化的任务在包含大量图像的数据集上对模型进行预 训练&#xff0c;旨在通过使用prompt工程解决新数据 分布上的一系列下游分割问题。…

深入了解diffusion model

diffusion model是如何运作的 会输入当时noise的严重程度&#xff0c;根据我们的输入来确定在第几个step&#xff0c;并做出不同的回应。 Denoise模组内部实际做的事情 产生一张图片和产生noise难度是不一样的&#xff0c;若denoise 模块产生一只带噪声的猫说明这个模块已经会…

【Python】 Python中的递增和递减操作符:简单易懂的指南

基本原理 在Python中&#xff0c;递增&#xff08;increment&#xff09;和递减&#xff08;decrement&#xff09;操作符是用于快速增加或减少变量值的快捷方式。这些操作符在很多编程语言中都有出现&#xff0c;它们提供了一种方便的方式来对变量进行自增&#xff08;&#…

【JavaEE进阶】——带你详细了解Spring日志以及配置日志

目录 &#x1f6a9;Spring日志的认识 &#x1f6a9;Spring日志的作用 &#x1f6a9;观察日志 &#x1f6a9;使用日志 &#x1f388;在程序中得到日志对象 &#x1f388;使⽤⽇志对象输出要打印的内容 &#x1f6a9;日志框架的介绍 &#x1f388;门面模式(外观模式&…

鸿蒙ArkTS声明式开发:跨平台支持列表【透明度设置】 通用属性

透明度设置 设置组件的透明度。 说明&#xff1a; 开发前请熟悉鸿蒙开发指导文档&#xff1a; gitee.com/li-shizhen-skin/harmony-os/blob/master/README.md点击或者复制转到。 从API Version 7开始支持。后续版本如有新增内容&#xff0c;则采用上角标单独标记该内容的起始版…

GIS Java 生成四至图

目录 前言 操作步骤&#xff1a; 1&#xff0c;求出多边形的四至点 2&#xff0c;下载地图 3&#xff0c;绘制多边形 前言 对于地图上的一个多边形地块&#xff0c;其四至图就是能够覆盖这个多边形的最小矩形&#xff0c;也就是求出这个多边形的最东点&#xff0c;最西点&…

如何从浅入深理解transformer?

前言 在人工智能的浩瀚海洋中&#xff0c;大模型目前无疑是其中一颗璀璨的明星。从简单的图像识别到复杂的自然语言处理&#xff0c;大模型在各个领域都取得了令人瞩目的成就。而在这其中&#xff0c;Transformer模型更是成为大模型技术的核心。 一、大模型的行业发展现状如…

AI实时免费在线图片工具3:人物换脸、图像编辑

1、FaceAdapter 人物换脸 https://huggingface.co/spaces/FaceAdapter/FaceAdapter 2、InstaDrag https://github.com/magic-research/InstaDrag

M-G364PD惯性测量单元:相机及微小层面的革命性应用

在现代科技飞速发展的今天&#xff0c;精准控制和精确测量是众多高端设备实现卓越性能的关键。爱普生推出的M-G364PD惯性测量单元&#xff08;IMU&#xff09;&#xff0c;因其卓越的性能和微小尺寸&#xff0c;成为相机以及其他微小层面应用的理想选择&#xff0c;为科技创新提…

实现计算器的基本操作:加减乘除与百分数

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、引言 二、加减乘除的实现 1. 操作数与操作符 2. 逻辑处理 3. 示例代码 三、求百分数…

vue3 setup 使用 beforeRouteEnter 组件内路由守卫

vue3 setup 使用 beforeRouteEnter 组件内路由守卫 setup 中只有onBeforeRouteLeave、onBeforeRouteUpdate两个钩子函数&#xff0c; 没有beforeRouteEnter对应的钩子函数&#xff0c;所以无法在setup中直接使用 <script setup> onBeforeRouteLeave((to, from) > {// …