理解动态字符串SDS

参考1
参考2

什么是动态字符串(SDS)

动态字符串(Simple Dynamic String)是Redis五大数据结构之一。
SDS在Redis中有以下作用

  1. 作为Redis中的字符串对象
  2. 替代C语言中的char*类型

为什么需要SDS替代char*

SDS相比于char*有以下优势

  1. 支持O(1)的查询字符串长度strlen(s)操作
  2. 修改字符串时对于内存的重分配较少
  3. 二进制安全
  4. 杜绝缓冲区溢出

O(1)的strlen()

SDS中是通过一个len字段存储字符串长度的。
它不以'\0'决定字符串结尾
不需要遍历字符串来得到字符串长度

typedef char* sds

struct sdshdr{
	unsigned int len;
	unsigned int free;
	char* buf;
}

内存重分配更少

从上面的结构中可以发现,SDS还有个free字段。
这个字段是用于内存预分配和惰性释放的,用于存储预分配内存后字符串的额外长度。

C语言中n个字符的字符串势必要n+1个连续空间存储,每次修改都需要对这个字符数组重新分配内存。

redis为了优化内存重分配次数,采取了这种策略。

修改字符串时,会对字符串空间进行预分配

  • 如果修改后的字符串长度大于原来分配的字符串长度,那么会基于修改后字符串长度,给SDS分配额外内存(SDS_MAX_PREALLOC在redis中为1MB)
    • after_modified.len < SDS_MAX_PREALLOC, 额外分配after_modified.len(free中存储额外长度)
    • after_modified.len >= SDS_MAX_PREALLOC,额外分配SDS_MAX_PREALLOC
  • 如果修改后的字符串长度小于于原来分配的字符串长度,直接用预分配好的内存
sds sdsMakeRoomFor(sds s, const char *t, size_t len) 
{ ... 
if (newlen < SDS_MAX_PREALLOC) 
	newlen *= 2; 
else newlen += SDS_MAX_PREALLOC; 

newsh = realloc(sh, sizeof(struct sdshdr) + newlen + 1); 

if (newsh == NULL) 
	return  NULL; 

newsh->free = newlen - len; return newsh->buf; 
}

这种策略在当字符串长度不断缩减时,可能会导致一个问题: 字符串占用了过多不必要的内存
但Redis也为其设计了相应策略

惰性空间释放
上面暗示了,redis不会在字符串长度减少时对多余空间立即释放,那么什么时候会释放不必要的多余空间呢?

当SDS的字符串占用率低于%25时
即free>3*len时,
SDS会释放掉原来size的(即free+len)的%50的大小,即缩一半

杜绝缓冲区溢出

SDS 的空间分配策略完全杜绝了发生缓冲区溢出的可能性,具体的实现在 sds.c 中。通过阅读源码,我们可以明白之所以 SDS 能杜绝缓冲区溢出是因为再调用 sdsMakeRoomFor 时,会检查 SDS 的空间是否满足修改所需的要求(即 free >= addlen 条件),如果满足 Redis 将会将 SDS 的空间扩展至执行所需的大小,在执行实际的 concat 操作,这样就避免了溢出发生

二进制安全

C 字符串必须符合某种编码,并且除了字符串的末尾之外,字符串不能包含空字符(\0),否则会被误认为字符串的末尾。这些限制导致不能保存图片、音频等这种二进制数据。

但是 Redis 就可以存储二进制数据,原因是因为 SDS 是使用 len 属性值而不是空字符来判断字符串是否结束的。

另外虽然SDS不以空字符(\0)决定字符串结尾,但它在字符串结尾还是会加上空字符,因此他**兼容C语言的字符串操作函数*

posted @ 2020-12-13 10:01  踱浪漫步  阅读(326)  评论(0编辑  收藏  举报