Redis【2】- SDS源码分析
1 简介&基础用法
Redis 中用得最多的就是字符串,在 C 语言中其实可以直接使用 char*
字符数组来实现字符串,也有很多可以直接使用得函数。但是 Redis 并没有使用 C 语言原生的字符串,而是自己实现了一个 SDS(简单动态字符串,Simple Dynamic String) 。
Redis 的 SDS 兼容了 C 语言的字符串类型的用法,
下面是 Redis 中 string 类型最常用的用法:
2 为什么 Redis 自实现字符串?
2.1 存储二进制的限制
C 语言的 char*
是以 \0
作为结束字符串的标识,如果需要存储的数据中本身就含有 \0
,那就没有办法正确表示,而像图像这种数据,一般存储下来都是二进制格式的,所以 Redis 不能直接使用 char*
。
下面是 C 语言的 \0
对字符串长度判断的影响:
输出结果则会不一样,\0
后面的数据会被截断:
在 SDS 结构中却能保证二进制安全,因为 SDS 保存了 len 属性,这就可以不适用 \0
这个标识来判断字符串是否结束。
2.2 操作效率问题
2.2.1 空间效率
2.2.1.1 预分配内存
原生的 C 语言字符串,在添加的时候,可能会因为可用空间不足,无法添加,而 Redis 追加字符串的时候,使用了预分配的策略,如果内存不够,先进行内存拓展,再追加,有效减少修改字符串带来的内存重新分配的次数。
类似于 Java 中的 ArrayList,采取预分配,内部真实的容量一般都是大于实际的字符串的长度的,当字符串的长度小于 1MB 的时候,如果内存不够,扩容都是加倍现在的空间;如果字符串的长度已经超过了 1MB,扩容的时候也只会多扩 1MB 的空间,但是最大的字符串的长度是 512MB。
2.2.1.2 惰性空间释放
惰性空间释放用于优化 SDS 的字符串缩短操作,当 SDS 的 API 需要缩短字符串保存的字符串的时候,程序并不会立即使用内存重新分配来回缩短多出来的字节,而是使用 free 属性将这些字节的数量记录下来,并等待将来使用。
当然 SDS 也提供了 SDS 显式调用,真正的释放未使用的空间。
2.2.2 操作效率
原生的 C 语言在获取字符的长度的时候,底层实际是遍历,时间复杂度是 O(n)
,String 作为 Redis 用得最多的数据类型,获取字符串的长度是比较频繁的操作,肯定不能这么干,那就用一个变量把 String 的长度存储起来,获取的时候时间复杂度是 O(1)
。
2.2.3 兼容性较好
Redis 虽然使用了 \0
来结尾,但是 sds 字符串的末端还是会遵循 c 语言的惯例,所以可以重用一部分<string. h> 的函数。比如对比的函数 strcasecmp
,可以用来对比 SDS 保存的字符串是否和另外一个字符串是否相同。
3 源码解读
3.1 简单指针介绍
数组的指针操作:
最终输出结果:
3.1.1 sdshdr 巧妙的结构设计
SDS 的相关代码主要在下面两个文件:
- sds. h:头文件
- sds. c:源文件
SDS 定义在 sds. h
中,为了兼容 C 风格的字符串,给 char 取了个别名叫 sds
:
《Redis 设计与实现》中,解释的是 Redis 3.0 的代码,提到 sds 的实现结构 sdshdr 是这样的:
但是实际上 7.0 版本已经是长这样:
-
- Sdshdr5 从未被使用,我们只是直接访问标志字节。
- 然而,这里文档标识 sdshdr5 的结构。
-
- 结构定义使用了__attribute__ ((packed))声明为非内存对齐, 紧凑排列形式(取消编译阶段的内存优化对齐功能)
-
- 如果定义了一个
sds *s
, 可以非常方便的使用s[-1]
获取到 flags 地址,避免了在上层调用各种类型判断。
- 如果定义了一个
上面定义了 4 种结构体,**Redis 根据不同的字符串的长度,来选择合适的结构体,每个结构体有对应数据部分和头部。
类型一共有这些:
用二进制表示,只需要 3 位即可,这也是为什么上面的结构体 sdshdr5
里面的 flags
字段注释里写的:前三位表示类型,后 5 位用于表示字符串长度。
而其他的 sds
结构体类型,因为长度太长了,存不下,所以后 5 位暂时没有作用,而是另外使用属性存储字符串的长度。
3.2 3.2 attribute 的作用是什么?
__attribute__ ((packed))
的作用就是告诉编译器取消结构在编译过程中的优化对齐, 按照实际占用字节数进行对齐,是 GCC 特有的语法。这个功能是跟操作系统没关系,跟编译器有关,gcc 编译器不是紧凑模式的。
attribute__关键字主要是用来在函数或数据声明中设置其属性。给函数赋给属性的主要目的在于让编译器进行优化。函数声明中的__attribute((noreturn)),就是告诉编译器这个函数不会返回给调用者,以便编译器在优化时去掉不必要的函数返回代码。
__attribute__书写特征是:__attribute__前后都有两个下划线,并且后面会紧跟一对括弧,括弧里面是相应的__attribute__参数,其语法格式为:
下面是实验的一些代码,实验环境为 Mac:
运行结果:
编译器压缩优化(内存不对齐)后,确实体积从 8 变成了 5,缩小了不少,别看这小小的变化,其实在巨大的数量面前,就是很大的空间优化。
3.3 宏操作
redis 基于前面 sds 设计,定义了一些十分巧妙的宏操作:
3.3.1 通过 sds 获取不同类型 sdshdr 变量
3.3.2 获取 sdshdr5 字符串类型的长度
3.3.3 通过 sds 获取 len 的值
3.4 创建新字符串
创建新的字符串一般是传递初始化的长度:
下面我们看具体的函数实现:
创建的返回的是指针,指向的是结构体中 buf 开始的位置,
3.5 获取可用空间
SDS 和我平常所用到的 C 语言的原生字符串有差别,因为从获取可用空间的计算方法来看,并未考虑到字符串需要以 \0
结尾,结构体本身带有长度的成员 len,不需要 \0
来做字符串结尾的判定,而且不使用 \0
作为结尾有很多好处, 分配的减去使用的即可。
3.6 设置 & 增加 sds 的长度
3.7 设置 & 获取已分配空间大小
3.8 扩大 sds 空间
/**
* 扩大sds字符串末尾的空闲空间,以便调用者确信在调用此函数后可以覆盖到字符串末尾 addlen字节,再加上null term的一个字节。
* 如果已经有足够的空闲空间,这个函数返回时不做任何操作,如果没有足够的空闲空间,它将分配缺失的部分,甚至更多:
* 当greedy为1时,放大比需要的更多,以避免将来在增量增长时需要重新分配。
* 当greedy为0时,将其放大到足够大以便为addlen腾出空间。
* 注意:这不会改变sdslen()返回的sds字符串的长度,而只会改变我们拥有的空闲缓冲区空间。
*/
// 扩大sds空间
sds _sdsMakeRoomFor(sds s, size_t addlen, int greedy) {
void *sh, *newsh;
// 获取剩余可用的空间
size_t avail = sdsavail(s);
size_t len, newlen, reqlen;
// 获取sds 具体数据类型
char type, oldtype = s[-1] & SDS_TYPE_MASK;
int hdrlen;
size_t usable;
/* Return ASAP if there is enough space left. */
// 可用空间足够直接返回
if (avail >= addlen) return s;
// 已用字符长度
len = sdslen(s);
// sh 回溯到sds起始位置
sh = (char*)s-sdsHdrSize(oldtype);
// newlen 为最小需要的长度
reqlen = newlen = (len+addlen);
assert(newlen > len); /* Catch size_t overflow */
// 在newlen小于SDS_MAX_PREALLOC(1M),对newlen进行翻倍,在newlen大于SDS_MAX_PREALLOC的情况下,让newlen加上SDS_MAX_PREALLOC。
if (greedy == 1) {
if (newlen < SDS_MAX_PREALLOC) // 小于1Kb 预分配2倍长度 = newlen + newlen
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC; // 多余1Mb 预分配 = newlen + 1Mb
}
// 获取新长度的类型
type = sdsReqType(newlen);
/* Don't use type 5: the user is appending to the string and type 5 is
* not able to remember empty space, so sdsMakeRoomFor() must be called
* at every appending operation. */
if (type == SDS_TYPE_5) type = SDS_TYPE_8;
// 新类型头部长度
hdrlen = sdsHdrSize(type);
// 校验是否溢出
assert(hdrlen + newlen + 1 > reqlen); /* Catch size_t overflow */
if (oldtype==type) {
/**
* 本质上是 使用 zrealloc_usable函数,指针ptr必须为指向堆内存空间的指针,即由malloc函数、calloc函数或realloc函数分配空间的指针。
* realloc函数将指针p指向的内存块的大小改变为n字节。
* 1.如果n小于或等于p之前指向的空间大小,那么。保持原有状态不变。
* 2.如果n大于原来p之前指向的空间大小,那么,系统将重新为p从堆上分配一块大小为n的内存空间,同时,将原来指向空间的内容依次复制到新的内存空间上,p之前指向的空间被释放。
* relloc函数分配的空间也是未初始化的。
*/
newsh = s_realloc_usable(sh, hdrlen+newlen+1, &usable);
// 申请空间失败
if (newsh == NULL) return NULL;
// s指向新sds结构的buf开始位置
s = (char*)newsh+hdrlen;
} else {
/* Since the header size changes, need to move the string forward,
* and can't use realloc */
// 数据结构发生变更,协议头部变更,需要从堆上重新申请数据空间
newsh = s_malloc_usable(hdrlen+newlen+1, &usable);
if (newsh == NULL) return NULL;
// 系统copy,越过头部结构长度,复制s的有效数据集合
memcpy((char*)newsh+hdrlen, s, len+1);
// 释放旧空间
s_free(sh);
// s执行新的空间,buf起始位置
s = (char*)newsh+hdrlen;
// flag 赋值 头部的第三个有效字段
s[-1] = type;
// 更新有效数据长度
sdssetlen(s, len);
}
// 实际可用数据空间
usable = usable-hdrlen-1;
if (usable > sdsTypeMaxSize(type))
usable = sdsTypeMaxSize(type);
// 更新分配的空间值
sdssetalloc(s, usable);
return s;
}
3.9 释放多余空间
3.10 拼接字符串
将一个字符串拼接到原 sds 后面:
3.11 拷贝
4 SDS 的优点
-
- 获取字符串的时间效率为
O (1)
- 获取字符串的时间效率为
-
- 杜绝缓冲区的溢出,复制或者追加字符串之前,会对空间进行检查与拓展,并且预分配一些容量,减少分片内存的次数。
-
- 可以存储二进制数据,含有
\0
则在读取时不会被截断。
- 可以存储二进制数据,含有
-
- 可以复用一部分 c 原生字符串的函数。
作者: 秦怀,个人站点 秦怀杂货店,纵使缓慢,驰而不息。
__EOF__
![](https://pic.cnblogs.com/face/1042250/20201017013944.png)
本文链接:https://www.cnblogs.com/Damaer/p/18592494.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 本地部署 DeepSeek:小白也能轻松搞定!
· 传国玉玺易主,ai.com竟然跳转到国产AI
· 自己如何在本地电脑从零搭建DeepSeek!手把手教学,快来看看! (建议收藏)
· 我们是如何解决abp身上的几个痛点
· 如何基于DeepSeek开展AI项目