动态字符串SDS 是 Redis 众多数据结构中最简单的一个,但其实也并不那么简单…
Redis 使用了自主定义的 SDS数据结构重新实现 字符串 相关操作,这一看似多此一举的操作,实际上设计及其精妙…
与 普通 C 字符串相比,其突出特点如下:
- 获取字符串长度复杂度 O(1)
- 拒绝缓冲区溢出
- 优化动态字符串的内存分配
- 二进制安全
- 兼容 C 字符串部分函数
突出特点
|
|
获取字符串长度复杂度 O(1)
众所周知 C 字符串 获取长度 的原理是 遍历字符串,对遇到的每个字符进行计数,直到遇到 结束空字符 \0,复杂度为 O(n)
而 SDS 则不同,通过上面的 SDS 结构体可以看出,如果需要获取 SDS 的长度,则直接 读取 len 属性即可,复杂度就是 O(1),这是一个 典型的 空间换取时间
的案例。而且即使修改 SDS 字符串内容,那么也会有对应的 API 去更新 len 属性
拒绝缓冲区溢出
C 字符串由于没有记录 len,还会出现一个隐患就是 缓冲区溢出;比如现在有两个字符串 str1 和 str2 是连续的,分别存储 “hello” 和 “nihao”,突然需要对 str1 里追加 “ world”,那么 如果没有进行容量判断,则有可能会出现 str1 的内容溢出到 str2 空间,造成数据污染
反观 SDS 则是由 API 先判断 str1 剩余空间是否足够,如果不够,则会自动升级存储空间,然后再进行数据修改
优化动态字符串的内存分配
针对 C 字符串进行 增加、缩减 操作的时候,实际都会 重新分配内存变量;比如 对字符串 追加字符内容 做拼接操作,则会扩展 变量内存空间大小,如果忘了,则会 缓冲区溢出;那么如果对字符串 删减字符内容 做剪切操作,则会在操作之后对 多余的变量内存空间进行释放,否则就会 内存泄露
为了防止 内存分配操作成为 Redis 的瓶颈,所以 SDS 在这方面也做了优化,主要是 空间预分配、惰性空间释放 等方式,也是 极其典型的 空间换取时间
的案例,可见这一理念在软件设计方面的应用之广泛…
二进制安全
C 字符串内容 必须是 类似 ASCII 一样的编码,并且除了字符串结尾 不能有 空字符,否则会被提前当做 结尾,这样的话就没法 存储 图片、音频、视频、压缩文件等 二进制数据
而SDS 就不同了,依赖自带 的 len 属性,不会按照 字符串内容里的 空字符串 判断结尾。所以这一特性,使得 Redis 不仅支持 文本存储,还支持 多样性的二进制数据存储
兼容 C 字符串部分函数
SDS 虽然有很多优秀的特性,但是根据上面提到的 typedef char *sds;
,本质上还是兼容 char 的,完全可以使用 大部分 char 操作函数,SDS 还会往字符串结尾添加 空字符(虽然不依赖)…
源码解析
由于有些方法里的代码量比较大,我们这里按照 典型的代码片段进行解析,同志们可以根据文章提示的代码位置 和 代码里面的关键词 在源码中搜素,可能数据结构一些元素 看不太懂什么意思,没关系,先混个脸熟,后面看完回头再看过来就明白了
结构体
接上面 【突出特点】部分 的 SDS 结构体代码,结构字段主要有如下内容
SDS 一般是采用了一段连续的内存空间来存储动态字符串 header (len / alloc / flags) + buf
而且为了防止 编译器的 C 内存对其原则 优化,所以 结构体前添加了 __attribute__ ((__packed__))
,否则经过优化就不能保证 header 和 buf数据部分 是紧紧相邻 的内存空间了。所以现在 获取 flag 的值可以直接往前便宜1个字节即可:unsigned char flags = s[-1]
另外SDS 提供了一些 宏,方便进行操作
比如有个 sdshdr8 类型的 SDS,那么 获取 长度就可以使用 SDS_HDR(8,s)->len
,宏的调用过程如下,由于 内存空间是连续的,所以 SDS 指针地址 减去 sdshd8 的长度,即往前偏移 header 长度,就获取到了 header 的指针地址
C 宏定义中『##连接符』详细,可以参照资料 http://www.web-lovers.com/c-joiner.html
SDS API
sdsnewlen()
为 SDS 创建函数
sdsMakeRoomFor()
容量扩展函数
sdscatlen()
字符串连接函数
除却上面比较典型的 函数,实际还有一些 其他常用的 SDS操作函数:
本文作者: wettper
本文链接: http://www.web-lovers.com/redis-source-sds.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!