一.SDS定义

- free 属性值为0,表示这个SDS没有分配任何未使用空间。
- len 属性值为5,表示这个SDS保存了一个5字节长的字符串。
- buf 属性是一个char类型数组,数组的前5个字节保存了,'R' 'e' 'd' 'i' 's' 五个字符,最后一个保存空字符串 '\0'。SDS为了遵循C字符串规则,则结尾是以一个字节空字符串 '\0'结尾。这样SDS可以使用C的一些函数,例如 <stdio.h>/printf 函数。
一下是含有 "未使用空间" 字符串展示:
未使用在设计中有特殊的作用,下面会做介绍。

二.SDS与C字符串的区别
| 'R' | 'e' | 'd' | 'i' | 's' | '\0' |
C语言使用长度为N+1 的字符出数组来表示长度为N的字符串,并且最后一个元素总是空字符串 '\0',C使用这种简单的字符串表示方式,并不能满足Redis 在数据结构方面安全性,效率性以及功能性方面的要求。下面将详细比较SDS与C的区别
2.1 获取字符串长度的复杂度
C字符串:C不记录自身的长度信息,所以为了获取一个C字符串的长度,程序必须遍历整个字符串,对于遇到的每个字符进行计数,直到遇到空字符串为止。这个操作的复杂度是 O(N)
SDS字符串:因为本身记录了 len 属性,所以读取长度直接使用API就可以获取到长度数据,不会对系统性能造成任何影响,把C字符串 O(N)的复杂度降低到 O(1)的复杂度。
2.2 杜绝缓冲区溢出
C字符串:因为不记录自身长度,极容易造成缓冲区溢出。
例如我们在使用C的函数 char * strcat(dest,src) 进行字符串拼接末尾的时候,假设我们拼接S1:Redis S2:MongoDB 两个字符串,已经分配了足够的内存空间,如下:

现在A程序员通过执行 strcat(S1,' Cluster') 把S1修改成'Redis Cluster' 结果你会发现 'Cluster' 溢出到S2的空间里面了

SDS字符串:SDS与C字符串不同,SDS使用空间分配策略杜绝了发生缓冲区溢出的可能。当SDS API 需要对SDS修改时,API会先检查SDS的空间是否满足修改所需的要求,如果不满足,API会自动扩展空间至所需大小,然后才执行实际的修改操作。所以使用SDS既不需要手动修改空间大小,也不会出现溢出的情况
2.3 减少修改字符串带来的内存分配次数
C字符串:因为C字符串不记录自身的长度,所以对于一个包含了N个字符串的C字符串来说,这个C字符串的底层实现总是N+1的字符长的数组。因为C字符出的长度和底层的数组的长度之间存在着这种关联,所以在增长或者缩短一个C字符串,程序都要对保存这个C字符串的数组进行一次内存重分配操作。
SDS字符串:频繁的内存重分配,对性能会造成影响,为了避免这种缺陷,SDS通过未使用空间解除字符串长度和底层数组长度之间的关系。通过未使用空间策略,SDS实现了空间预分配和惰性空间释放两种优化策略。
2.3.1 空间预分配(优化字符串增长):
-
如果扩展后的字符串长度小于 1MB:
new_alloc = alloc * 2- 其中,
alloc是当前已分配的空间大小(即当前字符串长度加上空闲空间),new_alloc是新的分配空间大小。 - 举例来说,如果当前字符串的总长度(包括空闲空间)为 50 字节,并且需要追加 10 字节,使得总长度变为 60 字节,那么 Redis 会将新分配的空间大小扩展为 100 字节。
-
如果扩展后的字符串长度大于等于 1MB:
new_alloc = alloc + n + 1MB- 其中,
alloc是当前已分配的空间大小,n是所需的附加空间大小(即追加的字符串长度),new_alloc是新的分配空间大小。 - 举例来说,如果当前字符串的总长度(包括空闲空间)为 1MB,并且需要追加 512KB,使得总长度变为 1.5MB,那么 Redis 会将新分配的空间大小扩展为 2.5MB(即 1MB 当前空间 + 512KB 追加空间 + 1MB 预分配空间)。
2.3.2 惰性空间释放(优化字符串缩短操作):
惰性空间释放的基本原理是:当 SDS 字符串的长度减少时,仅仅修改长度字段,而不立即缩减内存分配。这种策略有助于减少内存分配和释放的频率,从而提高性能。
在 SDS 的实现中,惰性空间释放主要涉及以下两个字段:
len:当前字符串的长度。free:当前字符串剩余的可用空间。
当一个 SDS 字符串缩短时,新的长度会更新到 len 字段,而原来多出来的空间则更新到 free 字段。这些空间不会立即被释放,而是保留在 SDS 结构中,以备将来追加字符串时使用。
2.4 二进制安全
在 Redis 中,SDS(Simple Dynamic String)字符串的一个重要特性是二进制安全。二进制安全意味着 SDS 可以存储任意类型的二进制数据,包括那些包含空字符('\0')的字节序列。这种能力使 SDS 不仅可以用来存储文本数据,还可以用来存储图像、音频、视频等任意二进制数据。
2.4.1 进制安全的原理
C 语言中的字符串以空字符('\0')作为结束标志,因此不能包含 '\0' 字符。如果一个字符串包含 '\0',C 语言函数如 strlen 将错误地认为字符串在 '\0' 处结束,这会导致截断和数据丢失。
SDS 通过记录字符串长度而不是依赖于 '\0' 结束符来实现二进制安全。SDS 结构中的 len 字段直接存储字符串的长度,因此 SDS 可以包含任意字符,包括 '\0'。
2.4.2 进制安全的实现
SDS 的二进制安全通过以下几个方面实现:
-
长度字段:SDS 使用
len字段存储字符串长度,而不是依赖于'\0'结束符。因此,SDS 可以处理包含'\0'字符的二进制数据。 -
内存操作:SDS 的各种操作函数(如创建、追加、复制等)使用
memcpy等函数进行内存操作,而不是使用strcpy等处理 C 字符串的函数。
2.4.3 进制安全的优点
-
多用途:由于 SDS 可以处理任意二进制数据,因此不仅可以存储文本数据,还可以存储图像、音频、视频等其他类型的二进制数据。
-
高效性:SDS 在进行字符串操作时使用
len字段而不是'\0'结束符,提高了操作的效率。 -
安全性:避免了由于
'\0'字符导致的截断问题,确保了数据的完整性。
2.4.4 使用场景
二进制安全的 SDS 广泛应用于需要存储和处理非文本数据的场景,包括但不限于:
- 缓存系统:缓存图片、音频、视频等二进制内容。
- 消息队列:传输包含二进制数据的消息。
- 数据存储:存储序列化的对象、压缩数据等。
| 序号 | C字符串 | SDS |
| 1 | 获取字符串长度的复杂度为 O(N) | 获取字符串长度的复杂度为 O(1) |
| 2 | API是不安全的,可能会造成缓冲区溢出 | API是安全的,不会造成缓冲区溢出 |
| 3 | 修改字符串长度N次必然需要执行N次的内存重分配 |
修改字符串长度N次最多需要执行N次内存重分配 1.空间预分配 2.惰性空间释放 |
| 4 | 只能保存文本数据 | 可以保存文本或者二进制数据(二进制安全) |
| 5 | 可以使用所有<string.h> 库中的函数 | 可以使用一部分<string.h>库中的函数 |
本文深入解析Redis中使用的简单动态字符串(SDS)的原理与特点,包括其如何优化字符串操作的效率与安全性,以及实现二进制安全的方法。
2319

被折叠的 条评论
为什么被折叠?



