C语言存储字符串的问题
C语言中没有类似Java中的String高级对象
在Java中 String
用的不亦乐乎,其自带的 equals
、split
、charAt
等方法真的很方便,但是C是没有这种高级对象的。
如果在C语言中使用字符串有两种方式:
- 使用字符串指针
const char *str = "hello";
- 使用数组
const char str[] = "hello";
方法一最大的缺陷在于字符串定义后即不能修改,所以更多情况下还是使用数组。
二进制安全
C语言中表示字符串结尾的符号是 \0
,如果字符串本身就具有 \0
字符,就会被截断,即非二进制安全。
计算字符串的长度性能低
C语言中有一个计算字符串长度的函数 strlen
,但这个函数与Java的不一样,需要遍历整个字符串来计算长度,时间复杂度是O(n),如果需要在循环中计算,性能将十分低下。
字符串拼接性能低
因为C语言字符串不记录长度,对于一个长度n的字符串来说,底层是n+1的字符数组(n是我们要存的字符,1是最后的结束符 \0
)。
底层虽然使用了n+1的字符数组,但是在使用
strlen
方法的时候不会计算\0
Simple Dynamic String 简单的动态字符串
在 redis3.2
之前,sds只有一个类型,定义如下:
1 | struct sdshdr { |
在后续版本中改为了
1 | struct __attribute__ ((__packed__)) sdshdr5 { |
sdshdr5
、sdshdr8
、sdshdr16
、sdshdr32
、sdshdr64
一共五个方法,其中官方注解有些到 sdshdr5
还没有被使用过。
类型机制
上面介绍了一共有五个 sdshdrX
方法,看着都差不多只是 len
和 alloc
类型不同,为什么要这么做呢?
其实也很简单就是省内存,而且真的是把能省就省发挥到了机制,在做业务开发的时候很少会为了这点内存再去做一套特殊的处理…
节省内存
我们已知的是 len
和 alloc
服务于字符串的长度,分别记录字符串长度和
uint8
: 字符串的长度最大(2^8)-1 = 255uint16
: 字符串的长度最大(2^16)-1 = 35535uint32
: 字符串的长度最大(2^32)-1 = 4294967295uint64
: 字符串的长度最大(2^64)-1 = 1.8446744e+19
绝大多数情况下字符串的长度都不会超过35536,如果使用 uint64
存储几十个几百就太浪费了。
最大长度问题
虽然类型限制了最大长度为(2^64)-1 ,但是真正限制的并不是它。
getBitOffsetFromArgument
是一个计算偏移量防止为负或者溢出的方法。在其方法内有这么一个判断:
1 | /* Limit offset to server.proto_max_bulk_len (512MB in bytes by default) */ |
根据源码注释可以看到,他是设置了512MB的一个长度限制,超出会返回异常。
在 config.c
文件中会对配置对象统一的定义。
1 | createLongLongConfig("proto-max-bulk-len", NULL, MODIFIABLE_CONFIG, 1024*1024, LONG_MAX, server.proto_max_bulk_len, 512ll*1024*1024, MEMORY_CONFIG, NULL, NULL), /* Bulk request max size */ |
字符串的追加与扩容
1 | SET msg "hello world" |
如上执行了一个简单的字符串追加工作。接下来以伪代码的形式介绍整个过程经历了什么。
SET 命令,创建一个SDS对象
1 | struct sdshdr { |
执行 APPEND 命令
此时相应的 sdshdr
被更新,字符串 " again!"
会被追加到原来的 "hello world"
之后:
1 | struct sdshdr { |
不难发现,free
从0->18,len
从11->18,这次扩容整体从11->22(18-11+18)。
具体代码
1 | sds sdscatlen(sds s, const void *t, size_t len) { |
1 | sds sdsMakeRoomFor(sds s, size_t addlen) { |
编码格式
在Redis中为了进一步的节省内存还为SDS设置了三种编码格式:
OBJ_ENCODING_EMBSTR
:长度小于44字节的字符串,该编码中RedisObject
、SDS
放置在连续的内存中。OBJ_ENCODING_RAW
:长度大于44字节,除此之外与OBJ_ENCODING_EMBSTR
区别仅在于是否为连续内存OBJ_ENCODING_INT
: 有时候我们会在String寸INT类型的自然数,例如一个手机号如果为字符串则需要11字节,如果使用long long 只需要8字节。
总结
- Redis 的字符串表示为
sds
,而不是 C 字符串(以\0
结尾的char*
),相比较有如下特性- 在计算字符长度时更高效,直接读取
len
熟悉为O(1)复杂度,传统C语言需要逐个遍历为O(n) - 提供更高效的扩容机制,牺牲一部分内存的情况下换取更快的追加速度
- 二进制安全,不强制通过
\0
作为结尾。也就是说在SDS中可以存储任何值
- 在计算字符长度时更高效,直接读取
致谢
- 本文标题:Redis数据结构源码解析(一) SDS
- 本文作者:SunRan
- 创建时间:2022-01-18 16:25:33
- 本文链接:https://lksun.cn/2022/01/18/Redis数据结构源码解析-一-SDS/
- 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!