文章摘要
GPT 4
此内容根据文章生成,并经过人工审核,仅用于文章内容的解释与总结

前提知识 🧀

我们先从百科上摘下 Redis 的解释:

Redis是一个使用 ANSI C 编写的开源、支持网络、基于内存、分布式、可选持久性的键值对存储数据库。
(不用过多在意 ANSI,它只是一个标准,你可以理解为早期民间版本很多,后来统一了标准,大学课程里包括现在在用的都是标准化后的 C 语言版本)

没错!Redis 的底层是由 C 语言 实现的!大学不管是什么专业应该都有这个课,但是不管大家还有没有它的记忆,都不影响我们接下来的学习哈哈哈~
发射爱心

redis 第一步,字符串是基础

回想当初学习 Java,第一个学习的数据类型应该是基本类型,但 redis 里最基础的结构其实是字符串,几乎哪里都有它。在 mysql 定义字段类型时,个别老哥会很中意 varchar 这个类型,因为万物皆可 varchar(不讨论性能的情况下),其实 redis 就有点这个意思,很多结构的基础都是字符串。

上面提到 Redis 的底层是由 C 语言实现的,那岂不是直接拿 C 语言的字符串(以空字符结尾的字符数组,以下简称 C 字符串)来用就行?

我先回答了:不行,因为 C 语言作为早期的编程语言,C 字符串是有些“不足”或者说是需要补充的,尤其是像 redis 这样对速度要求严苛,字符串可能会被频繁修改的服务,于是 redis 在 C 字符串的基础上,自己构造了名为 简单动态字符串(simple dynamic string,SDS)的抽象类型,用作 redis 自己的默认字符串。

(redis 也不是就不用 C 字符串了,只会运用在一些无需对字符串值做修改的地方,例如打印日志时)

C 字符串简单介绍,嗯,简单。

C字符串内存模型
首先上图就是一个值为”yikun”的 C 字符串。
不了解的小伙伴可能不知道为什么后面多了个’\0’,其实这个字符是空字符,也可以理解为 C 语言的“字符串终止符”。
长度为 N 的字符串,会用长度为 N+1 的字符数组来表示,最后多出来的 1 长度就是专门用来存储空符’\0’的。

然后没了。
C 语言的字符串就是这么简单。
就这?
不急,我们继续往后说~

那么 C 字符串有哪些“不足”呢?

俗话说得好:知己知彼,百战百胜。
我们得知道 C 语言的“不足”,才能知道 redis 为了弥补这种情况,在 SDS 中做了什么措施。

1.C 字符串并没有记录自身长度。 2.会根据空字符’\0’来判断字符串是否结束。 3.只能根据空字符’\0’来判断字符串是否结束。

2 和 3 好像差不多,但意思其实是有细微差别的。
(果然中华文化博大精深~)

C 字符串因为上面的“不足”会引起什么问题?

1.获取字符串长度复杂度高

因为 不足 1不足 3,所以 C 字符串只能通过遍历字符来获取长度。
Java的for循环举例
我们这里用 Java 的 for 循环(只是用 Java 举例,实际是 C 语言实现的)简单说明下 C 语言如何获取字符串长度。
可见字符串越长,这个操作就越耗时,复杂度为 O(N)。

2.缓冲区溢出 和 内存泄露

因为 不足 2,C 字符串可能会在修改字符串时出现问题,因为大家知道内存是连续的,假如我要在字符串后面拼接新的内容,我首先要通过 内存重分配 来扩展底层数组的空间大小。

内存重分配-1
比如我要在字符串’yikun’后拼接’lucky’,但现在后面紧挨存储的是’happy’字符串,所以我首先要扩展数组才能拼接’lucky’,不然就把’happy’给覆盖掉,造成缓存区溢出了。

那如果我要缩短’yikun’,改成’yi’,倒是不用扩展数组。
内存重分配-2
但我空的这部分又造成内存泄漏,所以还是需要进行 内存重分配
内存重分配 是个十分耗时的操作,如果上面的字符串修改频繁发生的话,对于性能的影响就会很大。

3.没办法存储二进制数据

因为 不足 3,假如我们的数据本身就包含空字符串’\0’,而代码逻辑是铁面无私的,会认为是字符串终止的意思;而像图片、音频、视频、压缩文件这样的二进制数据是会包含空字符串’\0’的,所以可想而知,数据会出现问题,可能只能识别到前面的部分数据,而丢失后面的内容。
把你丢掉!

所以针对上面的问题,redis 的 SDS 的结构是怎么设计的?

废话不多说,我们直接来看下 SDS 的结构:
SDS结构-1
直观来看,SDS 多维护了 free 属性和 len 属性。
len 属性用来记录 buf 数组中已使用字节的数量,同时也等于 SDS 所保存字符串的长度。
SDS结构-2
而属性 free 是用来记录 buf 数组中未使用字节的数量。

SDS 是如何依靠引入两个属性值 len、free 来解决具体问题呢?

1.获取字符串长度

现在可以直接访问 len 属性来获取字符串长度,就好比 Java 属性的 get 方法,复杂度由原来的 O(N)一下子降到了 O(1)。
而且这个属性是 SDS 在执行操作时自动完成的,我们无需进行任何维护。

2.空间预分配 和 惰性空间释放

上面我们说内存重分配操作耗时,所以在需要对 SDS 进行空间扩展的时候,会分配额外的未使用空间。

下面是额外分配空间数量的公式: 1.如果 SDS 修改之后,SDS 的长度(len 属性的值)小于 1MB,那么就会分配和 len 属性同样大的未使用空间,也就是 free 属性会和 len 属性相同,相当于将原数组长度 double。 2.如果 SDS 修改之后,SDS 的长度(len 属性的值)大于或等于 1MB,那么就会分配固定的 1MB 未使用空间。
字符串拼接操作

而 free 属性就决定了是不是需要进行额外的内存重分配操作,假如 free 为 7,而你需要拼接的字符串长度只有 5,那就不需要进行内存重分配操作,直接存储就可以。

free:太好了,我这空座就是给老弟你留的,快坐快坐~
我让你坐下

所以针对拼接操作来说,本来 N 次 一定需要 N 次的内存重分配操作,现在 最多只需要 N 次
(比如你第一次拼接后,free 就大于后续要拼接的字符串长度之和了,那其实就只有 1 次内存重分配操作,所以说最多 N 次)

缩短操作也是同理,删掉 N 个字符后,我 free 就增加 N,我先不做内存重分配操作,就先给你留着呗,万一你后面又做拼接是不是。
字符串缩短操作
所以总结:拼接时我们做 空间预分配,缩短时我们做 惰性空间释放,都是为了 减少内存重分配操作

(SDS 其实也提供了响应的方法,在有需要时,可以真正地释放 SDS 的未使用空间,所以不用担心未使用空间会造成内存浪费。)

3.存储二进制数据

C 字符串不能存储二进制数据的原因是只能根据’\0’来判断数据是否结束,不能保证其完整性,但因为 SDS 的 len属性,无论你数据里有多少’\0’都没关系,我是根据 len属性值来判断数据长度的,必定是完整的,所以 SDS 可以安全地存储二进制数据。

SDS 你都有 len 了,那为啥还要跟 C 字符串一样在字符串后面加’\0’?

这个很好回答,因为 SDS 保持和 C 字符串一样的特性,就不用专门编写多余的函数了,在一定的情况下可以直接用 C 语言的函数,避免了不必要的重写。

最后的总结

C 字符串和 SDS 之间的区别:

C 字符串 SDS
获取字符串长度复杂度为 O(N) 获取字符串长度复杂度为 O(1)
修改字符串时需要执行 N 次内存重分配 修改字符串时最多需要执行 N 次内存重分配
不能保存二进制数据 可以保存二进制数据
可以使用 C 字符串函数库的所有函数 可以使用 C 字符串函数库的一部分函数

写在最后的最后

我是苏易困,大家也可以叫我易困,一名 Java 开发界的小学生,文章可能不是很优质,但一定会很用心。

呜呜呜~ 真是边整理便有收获,自己学习的动力也增长了起来,还真是那句话:你自己明白,才能跟其他人说明白(也不知道是不是说明白了),而且写的时候也会考虑一些取舍,比如有的东西需不需要标注?有的地方是不是过于啰嗦了?是不是写的太入门了一点?可能这些知识在一些人看来都是很基础很入门甚至不用讲的东西。
但怎么说呢,我的初心其实也就是整理和梳理自己的知识,如果能帮到你,我会很开心;如果你觉得不够水平,我也会虚心接受;人嘛,总要有个学习的过程,我也会一直努力的~ 大家一起加油~

⭐️bulingbuling~