Redis使用C语言编写的,但是并没有直接使用字符数组来表示字符串,而是自己抽象了一种出了名为 SDS(简单动态字符串)的类型,实际上,对应C语言的一个结构体:
struct sdshdr {
int len;
int free;
char buf[];
}
buf[]:这是一个字节数组,用于真实数据的存放。
len:表示数据内容所占的字节数(free值为0时,代表内容长度)
free:表示字节数组的空闲字节数
为什么叫做字节数组呢?
数据以二进制的形式在硬件上流通,当然这个二进制是一个抽象模型,物理层面怎么表现的我们不需要关心。也就是说,所谓的字符串,也是一堆二进制。
像char、int、long这些关键字,很多同学在学习的时候,只是去关注了字面的限制。比如说,声明一个char类型的变量,那本质上就是在内存中去开辟一字节的存储空间(ASCII码表),至于你存什么东西,这个无所谓的,反正最后放入内存中的,是一堆二进制。
所以,SDS 除了能够存储字符串外,还可以存储二进制数据。
扩展
SDS 并没有丢弃C语言中字符串默认补'\0'的习惯,这样做并不会有二进制安全的问题,因为len属性记录着内容的字节长度,取值的时候是根据len属性的值去取的。
(只有在存储字符串的时候会补'\0')
这样做还有一个好处就是可以复用C语言提供的一些库函数,不用编写重复代码。
为什么要引入这样一个结构体呢?
这可以从优化了什么以及解决了什么问题来回答。
优化了什么?
len属性记录了内容的字节长度,即字符串的长度。所以,查看字符串长度命令的时间复杂度是O(1)。
解决了什么问题?
C语言中,对于字符串增长和缩减的操作,往往需要进行内存重分配,很有可能操作的大部分时间都耗在内存的分配上,所以,使用一些适当的策略来减少内存重分配的次数,可以提高Redis整体的性能。
还有就是,C语言没有自动化的垃圾回收机制,所以缓冲区溢出和内存泄露问题,也需要注意避免。
空间预分配
该策略主要针对于字符串的增长操作,buf[]的长度总是大于增长后的字符串长度,公式如下:
- 如果增长后的字符串小于1024字节(1MB),那么buf[]的长度为,2 * 新字符串的长度 + 1。
- 如果增长后的字符串长度大于等于1024字节(1MB),那么buf[]的长度为,新字符串的长度 + 1024 + 1。
(1字节的内容是'\0')
举例:
- 原字符串内容字节数:5 + 1,增长后:10 + 1,buf[]的长度为:2 * 10 + 1 = 21
- 原字符串内容字节数:1024 + 1,增长后:2048 + 1,buf[]的长度为:2048 + 1024 + 1 = ?
通过空间预分配策略,将原本需要进行N次的空间分配操作,降至最多分配N次,即 <= N。
惰性空间释放
该策略主要针对字符串的缩减操作,buf[]的长度保持不变。比如说字符串从“Hello”变成了“H”,显然,空闲空间增大了,但是我们并不会进行内存重分配,释放掉多余的空间。这也减少了空间分配的操作次数。
(一般键都会有过期时间的,不必担心空间浪费的问题,如果有大量的字符串增长操作,可能就需要修改一下Redis的配置去定期释放一些空间)
2024.10.21
writeBy kaiven