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

news/2025/2/28 16:06:29/

文章目录

  • 前言
  • 一、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/news/1575541.html

相关文章

【弹性计算】Guest OS

Guest OS 1.基础架构2.成本效率优化2.1 保障兼容性、简化生态环境&#xff0c;解决方案一键部署2.2 提升资源弹性和资源利用率2.2.1 系统资源的量化和监控2.2.2 资源隔离能力 3.安全性和稳定性增强3.1 安全性3.2 RAS3.2.1 可靠性&#xff08;Reliability&#xff09;3.2.2 可用…

jspssm542Springboot 医疗服务系统

&#x1f4d8; 博主小档案&#xff1a; 花花&#xff0c;一名来自世界500强的资深程序猿&#xff0c;毕业于国内知名985高校。 &#x1f527; 技术专长&#xff1a; 花花在深度学习任务中展现出卓越的能力&#xff0c;包括但不限于java、python等技术。近年来&#xff0c;花花更…

Linux | RHEL / CentOS 中 YUM history / downgrade 命令回滚操作

注&#xff1a;英文引文&#xff0c;机翻未校。 在 RHEL/CentOS 系统上使用 YUM history 命令回滚升级操作 作者&#xff1a; 2daygeek 译者&#xff1a; LCTT DarkSun 为服务器打补丁是 Linux 系统管理员的一项重要任务&#xff0c;为的是让系统更加稳定&#xff0c;性能更加…

Golang学习笔记_39——策略模式

Golang学习笔记_36——装饰器模式 Golang学习笔记_37——外观模式 Golang学习笔记_38——享元模式 文章目录 一、核心概念1. 定义2. 解决的问题3. 核心角色4. 类图 二、特点分析三、适用场景1. 支付系统2. 数据压缩3. 游戏AI4. 折扣计算 四、代码示例&#xff08;Go语言&#x…

国内短剧系统源码部署小程序体验测评讲解

在移动互联网飞速发展的今天&#xff0c;短剧作为一种新兴的娱乐形式&#xff0c;凭借其短小精悍、内容丰富的特点&#xff0c;迅速赢得了大量用户的青睐。作为一名软件测试人员&#xff0c;我有幸深入体验了一款功能全面、设计精良的短剧小程序。本文将从前端设计、后端功能、…

leetcode203-----移除链表元素

目录 一、题目简介 二、解题思路 三、代码 3.1 直接使用原来的链表来进行移除节点操作 1. 时间复杂度 删除头节点&#xff1a; 删除非头节点&#xff1a; 综合分析&#xff1a; 2. 空间复杂度 总结&#xff1a; 3.2 设置虚拟头节点 时间复杂度&#xff1a; 空间复…

常见锁类型介绍

下面结合代码详细介绍 Mutex、RW Lock、Futex、自旋锁、信号量、条件变量 和 synchronized&#xff0c;并分析它们的适用场景、特点以及为什么这些锁适用于特定场景。我们将从锁的实现机制和性能特点出发&#xff0c;解释其适用性。 1. Mutex&#xff08;互斥锁&#xff09; 代…

AcWing 蓝桥杯集训·每日一题2025

题目链接 : 5437. 拐杖糖盛宴 题意: 有m个不同的糖果和n个不同高度的奶龙, 奶龙可以根据自己的身高去吃糖果,糖果垂直于地面,对于一个糖果都需要让每个奶龙尝试能否吃到,如果吃到则减去相应吃到的长度, 奶龙长高吃掉糖果的长度即可,根据长度进行判断, 分类讨论。 解题思路 : …