Redis是C语言开发的,但是Redis字符串没有直接使用C语言的字符串,学过c语言的朋友应该都知道,C语言字符串是以"\0"结尾的,那为什么Redis没有直接使用C语言的字符串,而是使用简单动态字符串SDS(Simple Danamic String)?
1、SDS(Simple Danamic String)数据结构
struct sdshdr{
//记录buf数组中已使用字节的数量
//等于 SDS 保存字符串的长度
int len;
//记录 buf 数组中未使用字节的数量
int free;
//字节数组,用于保存字符串
char buf[];
}
SDS保存字符串 “Redis”具体图示如下:
2、为什么这样设计?
解释:
① 降低时间复杂度
len 为字符串的实际长度,在Redis中获取字符串的key长度的时间复杂度为O(1) 通过 strlen key 命令可以获取 key 的字符串长度;而如果使用C语言的结构需要遍历整个字符串,时间复杂度为O(N),因为Redis是内存级别的,不应该反复遍历获取字符串长度。
② 杜绝缓冲区溢出
free 为buf数组中剩余的空间大小,使用free杜绝了缓冲区溢出,会根据free和len空间的长度是否满足,如果不满足,会进行相应的空间扩展,然后在进行修改操作,所以不会出现缓冲区溢出
③ 减少修改字符串的内存重新分配次数
C语言由于不记录字符串的长度,所以如果要修改字符串,必须要重新分配内存,因为如果没有重新分配。而对于SDS,由于len属性和free属性的存在,对于修改字符串SDS实现了空间预分配和惰性空间释放两种策略:
1、空间预分配:对字符串进行空间扩展的时候,扩展的内存比实际需要的多,这样可以减少连续执行字符串增长操作所需的内存重分配次数。
2、惰性空间释放:对字符串进行缩短操作时,程序不立即使用内存重新分配来回收缩短后多余的字节,而是使用 free 属性将这些字节的数量记录下来,等待后续使用。(当然SDS也提供了相应的API,当我们有需要时,也可以手动释放这些未使用的空间。)
④ 二进制安全
二进制安全(Redis不是采用c语言字符串的以\0来判断字符串结束 由于这种限制,使得C字符串只能保存文本数据,像音视频、图片等二进制格式的数据是无法存储的, 而sds通过判断len的长度来判断字符串的长度
3、扩容规则
如果新字符串长度比SDS_MAX_PREALLOC小,则将其长度double,如果大于SDS_MAX_PREALLOC则再给SDS_MAX_PREALLOC空间。这个值redis定义的是1024*1024,即1MB。
这样一来,扩容一次多给一倍的存储空间,这样可以减少分配内存的次数,当然稍微有点浪费,代价是浪费一些内存,并且不会主动释放。
4 、String的编码格式
新版本数据结构
struct sdsshr<T>{
T len; //buf已使用长度 1byte
T alloc;//alloc分配长度 1byte
unsigned flags;//sdshdr类型 1byte
char buf[];//数组内容
}
- int编码:8个字节的长整形,当数字长度小于20同时能够被强制转换成long, long类型使用int编码,长度大于20,或者超出long类型范围的时候会转换使用embstr
- embstr编码:小于44字节的字符串(redis 3.2版本及以上)
- raw编码:大于44个字节的字符串
对于embstr
和raw
这两种encoding
类型,其存储方式还不太一样。对于embstr
类型,它将RedisObject
对象头和SDS
对象在内存中地址是连在一起的,但对于raw
类型,二者在内存地址不是连续的。
对比两者内存布局可以发现:
64 - 16(redisObject)- 1(len)- 1 (alloc) - flags = 45位;
45-1(\0)= 44位。
旧版本:
struct SDS {
unsigned int capacity; // 4byte
unsigned int len; // 4byte
byte[] content; // 内联数组,长度为 capacity
}
这里的unsigned int 一个4字节,加起来是8字节.
内存分配器jemalloc分配的内存如果超出了64个字节就认为是一个大字符串,就会用到raw编码。
前面提到 SDS 结构体中的 content 的字符串是以字节\0结尾的字符串,之所以多出这样一个字节,是为了便于直接使用 glibc 的字符串处理函数,以及为了便于字符串的调试打印输出。所以我们还要减去1字节
64byte - 16byte - 8byte - 1byte = 39byte