一.概述
在Redis中只将C字符串用于一些无需对字符串值进行修改的地方(常量),如日志打印,而其它需要修改(比如底层的键-值对存储)的地方都使用了自己构建的SDS(简单动态字符串)进行存储。
二.SDS结构
SDS是一种对C字符串的封装,其结构体如下:
struct sdshdr{
// 记录buf数组中已使用的字节数(不含末尾的空字节'\0')
int len;
// 记录buf中未使用字节的数量
int free;
// 字节数组,用于保存字符串
char buf[];
};
其中buf中存储的字符串采用C字符串的风格,以'\0'结尾,以便重用部分C字符串函数,比如:printf("%s", sds->buf),字符串比较函数等。
【注】:有些类似于TCP/IP中的mbuf.
三.SDS较C字符串的优点
1.获取字符串长度的时间复杂度
由于C字符串并不记录长度,因此需要进行遍历,直至遇到'\0',其时间复杂度为O(N),而SDS中由于记录了字符串长度,因此时间复杂度为O(1)。
2.杜绝缓冲区溢出
C字符串在拼接时并不负责检查和维护字符串的长度,这一切都依赖于程序用事先开辟足够大的缓冲区,否则将会造成溢出并可能覆盖掉后面的内存中的原先内容。同样在裁剪时,也依赖程序员重新调整大小。
SDS结构及其相应API会自动管理这些情况,并通过空间预分配(类似于vector的分配策略)和惰性空间释放两用策略优化效率
- 空间预分配:该分配策略用于优化SDS的字符串增长效率,当需要扩展空间时,SDS的API不仅分配所需大小的空间,还将预先分配一定的额外空间,以防止每次扩展时都要进行内存扩展。其预先分配内存的策略是:若SDS的长度(len)小于1M,则额外预分配空间大小与len值相同,若SDS的长度(len)大于1M,则分配1M的额外未使用空间。
预分配源码如下:
#typedef char *sds;
sds sdsMakeRoomFor(sds s, size_t addlen) {
struct sdshdr *sh, *newsh;
size_t free = sdsavail(s);
size_t len, newlen;
if (free >= addlen) return s;
len = sdslen(s);
sh = (void*) (s-(sizeof(struct sdshdr)));
newlen = (len+addlen);
// - 当字符串增长后的长度小于1M时,按2倍大小申请
// - 否则预扩充1M
if (newlen < SDS_MAX_PREALLOC) //#define SDS_MAX_PREALLOC (1024*1024)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);
if (newsh == NULL) return NULL;
newsh->free = newlen - len;
return newsh->buf;
}
- 惰性空间释放:该策略用于缩短SDS字符串的长度,当要对SDS中的字符串进行缩短操作时,并不立即回收空闲出来的字节,而是将其记录在free中,待需要时,通过相应的SDS的API进行空间释放。
3.二进制数据存储
C字符串字符需要符合ASCII编码,在字符串中只可在尾部出现空字符'\0',若在中间出现则无法读取到后面的字节,因此只能保存一些ASCII编码编码的文本数据,而对于图片,音频等数据便无法保存。
而SDS记录了字符串长度,且SDS的API都是二进制安全的(不会由于遇到某种特殊字符而无法处理,因为不以'\0'求长度),不会对其中数据做任何限制,过滤等,保证了数据写入与读取时的一致性。