redis的字符串底层结构
struct sdshdr {
// 记录 buf 数组中已使用字节的数量
// 等于 SDS 所保存字符串的长度
int len;
// 记录 buf 数组中未使用字节的数量
int free;
// 字节数组,用于保存字符串
char buf[];
};
遵循空字符结尾这一惯例的好处是, SDS 可以直接重用一部分 C 字符串函数库里面的函数。C保存的原因我认为是判断字符串长度的一个临界值
那么它与c字符串的区别?
-
读取字符串效率
c字符串通过一个个字节的长度读取,时间是O(n),而SDS则直接获取len属性就可以了==,时间是O(1);比如说, 因为字符串键在底层使用 SDS 来实现, 所以即使我们对一个非常长的字符串键反复执行 STRLEN 命令, 也不会对系统性能造成任何影响, 因为 STRLEN 命令的复杂度仅为 O(1) 。
-
杜绝缓冲区溢出
C 字符串不记录自身长度带来的另一个问题是容易造成缓冲区溢出,比如一个字符串Redis\0hello,在C字符串Redis后加入个hell,他会因为直接覆盖后面的数据,就变成Redishello\0,在执行拼接操作之前检查
s
的长度是否足够, 在发现s
目前的空间不足以拼接,Redis中就是自动将 SDS 的空间扩展至执行修改所需的大小,再修改,先检查给定 SDS 的空间是否足够 -
减少修改字符串时带来的内存重分配次数
因为 C 字符串的长度和底层数组的长度之间存在着这种关联性, 所以每次增长或者缩短一个 C 字符串, 程序都总要对保存这个 C 字符串的数组进行一次内存重分配操作
比如增加字符,不重排内存分配来扩展数组大小的话,就会上面说的缓冲区溢出
比如减少字符,不重排内存分配来释放空间,就容易内存泄漏
如果修改字符串长度的情况不太常出现, 那么每次修改都执行一次内存重分配是可以接受的。
如果每次修改字符串的长度都需要执行一次内存重分配的话, 那么光是执行内存重分配的时间就会占去修改字符串所用时间的一大部分, 如果这种修改频繁地发生的话, 可能还会对性能造成影响。
如何避免空间增长呢?
空间预分配: 在存的数据小于1M时 存3字节的字符串会多分配3字节+1字节存空字符
超过1M,比如存30M就多分配1M+1字节
通过空间预分配策略, Redis 可以减少连续执行字符串增长操作所需的内存重分配次数。
在扩展 SDS 空间之前, SDS API 会先检查未使用空间是否足够, 如果足够的话, API 就会直接使用未使用空间, 而无须执行内存重分配。
如何避免空间浪费呢?
惰性空间释放: 当 SDS 的 API 需要缩短 SDS 保存的字符串时, 程序并不立即使用内存重分配来回收缩短后多出来的字节, 而是使用 free
属性将这些字节的数量记录起来, 并等待将来使用。
-
二进制安全
C字符串无法存放带有空字符的数据,不然误以为是结尾,到时候判断长度会错,而redis使用len字段获取长度就避免了这个问题,而且redis存放的是字节,而不是字符,所以本身就是二进制安全的
C 字符串 | SDS |
---|---|
获取字符串长度的复杂度为 O(N) 。 | 获取字符串长度的复杂度为 O(1) 。 |
API 是不安全的,可能会造成缓冲区溢出。 | API 是安全的,不会造成缓冲区溢出。 |
修改字符串长度 N 次必然需要执行 N 次内存重分配。 | 修改字符串长度 N 次最多需要执行 N 次内存重分配。 |
只能保存文本数据。 | 可以保存文本或者二进制数据。 |
可以使用所有 <string.h> 库中的函数。 | 可以使用一部分 <string.h> 库中的函数。 |