一、什么是简单动态字符串(SDS)
struct sdshdr{
//记录buf组数已经使用的字节的数量
//等于SDS所保存字符串的长度
int len;
//记录buf数组未使用字节的数量
int free;
//字节数组,用来存放字符串
char buf[];
}
redis没有使用c中的传统string,而是自己创建了一个SDS,并且将他作为redis的默认字符串。
单看还是看不懂,我们来举例子,这里我直接拿redis设计与实现这本书的画的图进行讲述
顾名思义,我的buf是存在字符串的,为什么是末尾是‘\0'是因为,他也需要标记结束字符,遇到'\0'就默认字符串结束,这里的好处是SDS可以复用c字符串中函数中的一些函数。这里如果不懂,可以去看看c的string类型。
这里我们举个例子我们可以复用printf()函数
printf("%s", s->buf);
len:指的是buf里面目前存放着5字节长的字符串
free:我剩下还有多少字节可以进行分配空间
然后再贴一张还有剩下空间的SDS图
想必你也了解他们代表的意思的吧,那恭喜你redis设计与实现开始入门了
接下来我们就要进阶了解了哦,准备好了没有!
二、SDS与c字符串string的区别
说了SDS的结构,我们上文也提及到了redis选择自己构建sds作为默认字符串,让我们为什么要吃力不讨的构建新的类型呢?
1.优点常量级遍历
c语言的string遍历
c的字符串遍历为O(n)
而SDS遍历只需要访问len的长度
即O(1)常量级
2.杜绝缓冲区溢出
c字符串在拼接的
我使用
strcat(s1, " Cluster");
则你会发现我s1的内容溢出到s2的位置了,则就出现了缓冲区的溢出
而SDS就杜绝的缓冲区溢出的原因,原因是什么呢?
当SDSApi,需要对SDS进行修改,他会坚持SDS的空间是否满足修改条件,如果不满足的画,就api会自动将sds的空间扩展至修改所需的大小,然后才会实际的修改操作。
例
执行上图一样的连接及操作后
他们是这么分配多的空间呢?别急,接下来就马上讲
3.减少修改字符串时带来的内存重分配次数
因为 C 字符串并不记录自身的长度, 所以对于一个包含了
N
个字符的 C 字符串来说, 这个 C 字符串的底层实现总是一个N+1
个字符长的数组(额外的一个字符空间用于保存空字符)。
因为 C 字符串的长度和底层数组的长度之间存在着这种关联性, 所以每次增长或者缩短一个 C 字符串, 程序都总要对保存这个 C 字符串的数组进行一次内存重分配操作:
- 如果程序执行的是增长字符串的操作, 比如拼接操作(append), 那么在执行这个操作之前, 程序需要先通过内存重分配来扩展底层数组的空间大小 —— 如果忘了这一步就会产生缓冲区溢出。
- 如果程序执行的是缩短字符串的操作, 比如截断操作(trim), 那么在执行这个操作之后, 程序需要通过内存重分配来释放字符串不再使用的那部分空间 —— 如果忘了这一步就会产生内存泄漏。
但是由于redis一般都被要求低延迟,高读写,高必发,这种是效率很低的所以才有sds的空间预分配和惰性空间释放两种优化策略。
空间预分配:
空间预分配用于优化 SDS 的字符串增长操作: 当 SDS 的 API 对一个 SDS 进行修改, 并且需要对 SDS 进行空间扩展的时候, 程序不仅会为 SDS 分配修改所必须要的空间, 还会为 SDS 分配额外的未使用空间。
其中, 额外分配的未使用空间数量由以下公式决定:
- 如果对 SDS 进行修改之后, SDS 的长度(也即是
len
属性的值)将小于1 MB
, 那么程序分配和len
属性同样大小的未使用空间, 这时 SDSlen
属性的值将和free
属性的值相同。 举个例子, 如果进行修改之后, SDS 的len
将变成13
字节, 那么程序也会分配13
字节的未使用空间, SDS 的buf
数组的实际长度将变成13 + 13 + 1 = 27
字节(额外的一字节用于保存空字符)。- 如果对 SDS 进行修改之后, SDS 的长度将大于等于
1 MB
, 那么程序会分配1 MB
的未使用空间。 举个例子, 如果进行修改之后, SDS 的len
将变成30 MB
, 那么程序会分配1 MB
的未使用空间, SDS 的buf
数组的实际长度将为30 MB + 1 MB + 1 byte
。通过空间预分配策略, Redis 可以减少连续执行字符串增长操作所需的内存重分配次数。
我们继续拿上面那个图为例子哦
首先我没有超过1m,那么就再需要的基础上再多开一一次,我有13个字节的字符串,然后我free也会多开13个字节的空间,留着空闲
懒惰空间释放
: 当 SDS 的 API 需要缩短 SDS 保存的字符串时, 程序并不立即使用内存重分配来回收缩短后多出来的字节, 而是使用
free
属性将这些字节的数量记录起来, 并等待将来使用。举个例子,
sdstrim
函数接受一个 SDS 和一个 C 字符串作为参数, 从 SDS 左右两端分别移除所有在 C 字符串中出现过的字符
sdstrim(s, "XY"); // 移除 SDS 字符串中的所有 'X' 和 'Y'
注意执行 sdstrim
之后的 SDS 并没有释放多出来的 8
字节空间, 而是将这 8
字节空间作为未使用空间保留在了 SDS 里面, 如果将来要对 SDS 进行增长操作的话, 这些未使用空间就可能会派上用场。
举个例子, 如果现在对 s
执行:
sdscat(s, " Redis");
二进制安全
兼容部分c字符串函数
上文有提及到SDS也有'\0\m,则他也可以复用c字符串的部分函数
总结、
c字符串 | SDS |
获取字符串长度的时间复杂度为O(n),一个一个的遍历 | 为常量级O(1),访问len即可 |
api,不安全,会造成缓冲区溢出 | 安全,会自动分配空间 |
修改字符串长度 N 次必然需要执行 N 次内存重分配 | 修改字符串长度 N 次最多需要执行 n次内存重分配 |
只可以存储文本数据 | 可以保存文本和二进制 |
使用所有string库的函数 | 使用string部分函数 |
这个是重点,牢固记
SDS的api
如果看的满意,请你动动你的手指点点赞,评论,你的关注是我莫大的支持,我也会继续加油的!