Redis 底层数据结构 —— SDS(简单动态字符串)

server/2025/2/28 6:21:50/

文章目录

  • 前言
  • 一、SDS是什么?
  • 二、为什么要采用SDS?
  • 三、SDS结构详解
    • 3.1 SDS 类型定义
    • 3.2 SDS结构组成
  • 四、SDS的预分配内存
  • 五、再谈为什么要采用SDS?
  • 六、总结

前言

我们都知道redis是用c语言实现的,但是c语言并没有字符串结构,而是通过字符数组来间接的表示字符串,如下形式:

char *name = "Jasmine"

而对于c语言来说,对应的存储结果,如下:
在这里插入图片描述

一、SDS是什么?

SDS(Simple Dynamic String) 是 Redis 自研的字符串类型,它是 Redis 为了解决 C 语言原生字符串(以 \0 结尾的字符数组)的一些局限性而设计的。SDS 是 动态字符串,可以高效地进行字符串操作并避免了 C 字符串的常见问题(如缓冲区溢出等问题)。

在 Redis 中,所有与字符串相关的数据(如键、值、集合成员等)都使用 SDS 作为底层存储结构。

举一个栗子,客户端set一个值时:

redis> SET msg "hello world"
OK

那么Redis将在数据库中创建了一个新的键值对,其中:

  • 键值对的键是一个字符串对象, 对象的底层实现是一个保存着字符串 “msg” 的 SDS 。
  • 键值对的值也是一个字符串对象, 对象的底层实现是一个保存着字符串 “hello world” 的 SDS 。

也就是key-value都是一个SDS结构

二、为什么要采用SDS?

C语言的字符数组存在的问题:

1.获取字符串长度需要通过运算,时间复杂度O(N)
2. 非二进制安全。某二进制字符串里面可能也存在’\0’,会导致读取提前结束。

什么是二进制安全?通俗讲,C语言中,用’\0’表示字符串的结束,如果字符串中本身就有’\0’字符,字符串就会被截断,即非二进制安全;若通过某种机制,保证读写字符串时不损害其内容,则为二进制安全。

3.不可修改。C语言存储的字符数组在内存中是保存在常量池中,如果要更改就需要重新开辟空间,而且这个过程是相当耗时的

三、SDS结构详解

Redis 根据字符串长度,使用不同的结构体优化内存(以 Redis 5.0 为例):

3.1 SDS 类型定义

// sdshdr5 结构(长度 ≤ 31)
struct __attribute__((packed)) sdshdr5 {unsigned char flags;  // 低3位存储类型,高5位存储长度char buf[];
};// sdshdr8 结构(长度 ≤ 255)
struct __attribute__((packed)) sdshdr8 {uint8_t len;        // 已使用长度uint8_t alloc;      // 总分配空间(不含头和空字符)unsigned char flags;// 低3位存储类型,高5位未使用char buf[];
};// 类似还有 sdshdr16、sdshdr32、sdshdr64,使用更大位宽的 len 和 alloc。

3.2 SDS结构组成

redis根据所存储的数据类型,将sds划分为五种,分别为sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64。虽然64位类型下最大字符串长度为2^63-1,但redis依然限制了字符串最大长度为512MB。在每一种sdshdr里,都包含lenallocflagsbuf,其中len + alloc + flags被称为header

  • lenbuf数组中已使用的字节数量,不包括结束符(字符串实际长度)。
  • alloc:分配的总空间(不含头和空字符),包括 '\0' 结束符。该值通常大于或等于 len,以避免频繁的内存分配和扩展。
  • flags:低3位标识 SDS 类型(如 SDS_TYPE_5、SDS_TYPE_8),高5位在 sdshdr5 中存储长度。
  • buf:柔性数组,存储实际数据,末尾保留 '\0'
    在这里插入图片描述

从上面插图可以看到sds由两部分构成,分别为sdshdralloced_buf,下面会分别说明

  • sdshdr:SDS 的头部结构,包含了关于字符串的元数据。这个结构存储了字符串的长度、已分配的空间以及与字符数组 buf 相关的信息。

为了尽可能的节省空间Redissdshdr的种类做了细分,根据字符串的长度不同,分配不同类型的sdshdr,不同sdshdr的大小也不一样

结构体类型适用长度范围len 和 alloc 位宽内存占用(头+元数据)
sdshdr50 ≤ len ≤ 31高5位存储长度1字节(仅 flags)
sdshdr832 ≤ len ≤ 2558位(1字节)3字节(flags+len+alloc)
sdshdr16256 ≤ len ≤ 6553516位(2字节)5字节
sdshdr3265536 ≤ len ≤ 2^32-132位(4字节)9字节
sdshdr64极大字符串(罕见)64位(8字节)17字节

SDS本质上就是一个char类型的指针,指向上图中sdshdralloced buf之间的位置,指针指向位置之后的内容和C字符串完全兼容,而SDS获取字符串长度,以及当前分配空间的大小是先通过解析SDS指向位置前一个字节的Flags字段内容,获取到当前sdshdr的类型,再通过不同sdshdr类型向前移动若干个字节获取LenAlloc字段内容实现的。
在这里插入图片描述
在这里插入图片描述
不管是什么类型的sdshdrflags字段只占用一个字节,flags字段的低三位用来存储该sdshdrtype,而高五位大多数场景下是无效的,除非当前是sdshdr5类型,就把当前字符串的长度存放到高五位当中(sdshd5类型的SDS,alloced buf的长度就是字符串的长度,没有空间预分配),从而节省了Len和Alloc字段占用的空间

四、SDS的预分配内存

在 Redis 中,SDS(Simple Dynamic String)采用了空间预分配和惰性空间释放的策略,以优化 C 字符串操作时频繁内存分配的问题。传统的 C 字符串,每次增加或缩短时,操作系统会进行内存重新分配,这会导致性能下降,尤其是在频繁进行字符串拼接或修改的情况下。为了解决这个问题,Redis 对存储字符串的缓冲区(Alloced buf)进行了优化,采取了“空间换时间”的策略,尽量避免内存重新分配的影响。

空间预分配

  • 若修改后长度 newlen < 1MB,则分配 newlen * 2
  • newlen ≥ 1MB,则额外分配 1MB

惰性空间释放:缩短字符串时不立即释放内存,保留空闲空间供后续操作复用。

五、再谈为什么要采用SDS?

采用SDS有以下几个优点
1.有单独的统计变量lenfree(称为头部)。可以很方便地得到字符串长度。
2.内容存放在柔性数组buf中,SDS对上层暴露的指针不是指向结构体SDS的指针,而是直接指向柔性数组buf的指针。上层可像读取C字符串一样读取SDS的内容,兼容C语言处理字符串的各种函数。
3.由于长度统计变量len的存在,读写字符串时不依赖'\0'终止符,保证了二进制安全。

buf[]是一个柔性数组。柔性数组成员,也叫伸缩性数组成员,只能被放在结构体的末尾,包含柔性数组成员的结构体,通过malloc函数为柔性数组动态分配内存。用柔性数组存放字符串,是因为柔性数组的地址和结构体是连续的,这样查找内存更快(因为不需要额外通过指针找到字符串的位置);可以很方便地通过柔性数组的首地址偏移得到结构体首地址,进而能很方便地获取其余变量。

六、总结

SDS是 Redis 为解决传统 C 字符串性能问题而设计的高效字符串数据结构。

  • 通过预分配内存、按倍数扩展、惰性空间释放等策略,SDS 避免了频繁的内存分配和缓冲区溢出的风险,从而提高了字符串操作的性能并减少了内存碎片。
  • SDS 保留了字符串末尾的 \0,保证与 C 字符串兼容,并通过封装常见操作提供了高效的字符串 API。
  • SDS 使用 len 来限制读取长度,而非依赖 \0,确保二进制安全。
  • 针对小字符串的存储,Redis 进一步优化了 sdshdr5 数据结构,通过将类型和长度信息合并,提高了内存使用效率
  • 在需要存储较大字符串时,sdshdr5 会被 sdshdr8 替代。

整体而言,SDS 通过高效的内存管理和灵活的字符串操作,使 Redis 在性能和内存利用上实现了优化。
在这里插入图片描述


http://www.ppmy.cn/server/171231.html

相关文章

使用MATLAB结合EasySpin进行ESR模拟的详细步骤及示例代码

以下是使用MATLAB结合EasySpin进行ESR模拟的详细步骤及示例代码&#xff0c;以实现对两个样品的单线态氧自由基&#xff08; 1 O 2 ^1O_2 1O2​&#xff09;和超氧自由基&#xff08; O 2 − O_2^- O2−​&#xff09;的模拟&#xff0c;并将模拟结果导出为Excel文件。 步骤概…

Is Noise Conditioning Necessary for Denoising Generative Models?论文阅读笔记

很吸引人的一个标题&#xff0c;很吸引人的一个作者&#xff0c;来读一读明神的新作&#xff0c;讲的是怎么把去噪领域的一些有意思的思想&#xff0c;特别是blind denoising和noise-level estimation的思想&#xff0c;应用到denoising diffusion模型中&#xff0c;从而去掉de…

【蓝桥杯嵌入式】各模块学习总结

系列文章目录 留空 文章目录 系列文章目录前言一、LED模块1.1 赛题要求1.2 模块原理图1.3 编写代码1.4 赛题实战 二、LCD模块2.1 赛题要求2.2 模块原理图2.3 编写代码2.4 赛题实战 三、按键模块3.1 赛题要求3.2 模块原理图3.3 编写代码3.4 赛题实战 四、串口模块4.1 赛题要求4…

Xcode如何高效的一键重命名某个关键字

1.选中某个需要修改的关键字&#xff1b; 2.右击&#xff0c;选择Refactor->Rename… 然后就会出现如下界面&#xff1a; 此时就可以一键重命名了。 还可以设置快捷键。 1.打开Settings 2.找到Key Bindings 3.搜索rename 4.出现三个&#xff0c;点击一个地方设置后其…

派可数据BI接入DeepSeek,开启智能数据分析新纪元

派可数据BI产品完成接入DeepSeek&#xff0c;此次接入标志着派可数据BI在智能数据分析领域迈出了重要一步&#xff0c;将为用户带来更智能、更高效、更便捷的数据分析体验。 派可数据BI作为国内领先的商业智能解决方案提供商&#xff0c;一直致力于为用户提供高效、稳定易扩展…

Springboot + Ollama + IDEA + DeepSeek 搭建本地deepseek简单调用示例

1. 版本说明 springboot 版本 3.3.8 Java 版本 17 spring-ai 版本 1.0.0-M5 deepseek 模型 deepseek-r1:7b 需要注意一下Ollama的使用版本&#xff1a; 2. springboot项目搭建 可以集成在自己的项目里&#xff0c;也可以到 spring.io 生成一个项目 生成的话&#xff0c;如下…

【Android】ViewPager的使用

AndroidManifest.xml <?xml version"1.0" encoding"utf-8"?> <manifest xmlns:android"http://schemas.android.com/apk/res/android"xmlns:tools"http://schemas.android.com/tools"><applicationandroid:allowBac…

【Go】十七、grpc 服务的具体功能编写

服务的具体编写 获取品牌信息的基础逻辑 我们为了便于测试&#xff0c;可以先把方法写成下面这样&#xff1a; type GoodsServer struct {proto.UnimplementedGoodsServer }之后再 test/brands.go 中进行编写测试代码&#xff1a; // 创建客户端 var brandClient proto.Goo…