本系列所有文章基于《Redis设计与实现》学习而做的随笔,本文使用的redis源码为3.0版本,以后不再赘述
c语言的字符串
首先,我们都知道,Redis是用c语言实现的,而c语言,有个很大的弊端,那就是没有原生的字符串,字符串,是使用字符数组实现的。
基于这个原因,我们可以分析一下c字符串的缺点。
1. 字符串的拼接需要提前判断空间是否足够,否则可能造成缓冲区溢出。此外还必须进行malloc分配内存占用大量系统资源。
2. 字符串缩短时需要释放空间,否则可能造成内存泄漏。
那么,Redis的第一个要解决的就是实现一个高效的字符串
Redis的SDS
简单动态字符串(simple dynamic string,SDS)的定义如下:
sds.h
struct sdshdr{
int len; //记录buf中字符串的长度(strlen)
int free; //记录buf中未使用的空间大小;
char buf[]; //保存字符串的数组
}
举个例子:
buf 申请了10个sizeof(char)的空间,保存的内容是"Redis"
那么,len = 5;
free = 4;
*剩余的那个空间是用来存储'\0'的。
SDS的好处
- sds遵从了c字符串以’\0’结尾的惯例,好处是可以直接使用c字符串的一些库函数,如
printf("s%",s->buf);
- 计算sds长度无需再使用strlen(),strlen复杂度是O(n)的,而sds则比较容易,直接读取sds->len,时间复杂度为O(1)。
- 杜绝缓冲区溢出。sds操作时,会自动检测原始空间是否满足,如果不满足会重新分配内存。
- 减少修改字符串时重新分配内存的次数。
- 二进制安全
减少内存重分配次数
- 如果此次进行的操作会加长字符串,那么会判断空间是否足够,不足够才会重新分配。
- 如果此次操作会减短字符串,那么不会直接释放内存,而是改变free的大小。
具体实现
空间预分配
假如普通的c字符串,一般来说每次strcat()之前都要重新分配内存。
sds会先去检测是否足够,不足则重分,但这实际上的次数几乎达到n次。
redis使用以下策略大大降低了重分配的次数:
//分别表示free,len,buf
sds1={0,5,"Redis"}
sds2={0,5,"Hello"}
现在要将sds1连接到sds2之后。
那么首先,判断剩余空间是否足够,明显不足,分配内存到可以存下。
策略如下,如果分配内存后大小小于1M,则多分配和len大小相同同的空间。
sds2={0,10,"Hello"}
sds2={10,10,"Hello"} //最终的分配
如果大于1M,则多分配1M的free空间。
如此,最终的结果为
sds2={10,10,"HelloRedis"}
这种策略,将内存分配的次数从n次变成最多n次
惰性空间释放
sds={3,7,"AAbcbAA"}
sdstrim(sds,"Ac") //移除sds中所有的A和c字符
结果为:
sds={8,2,"bb"}
缩短sds,不直接释放空间,仅仅修改len和free,多余的free便于下次曾长sds时使用。
二进制安全
我们知道,c语言是以'/0'
来作为字符的结尾,那么如果有这样一个字符串"abc\0de\0"
,c只会识别到abc\0
。这使得存储二进制内容无法实现(二进制内容或许会存在很多'\0'
)。
sds是二进制安全的,原因在于其使用len成员来确定内容长度而非'\0'
。