1,sds定义
sds是simple dynamic string的缩写,意为简单动态字符串。
定义为:
struct sdshdr {
long len; //记录buf数组已使用的字节数量,即sds保存的字符串的长度,不含'\0'
long free; //记录buf数组中未使用字节的数量
char buf[0]; //字节数组,用于保存字符串
};
如图:
len:4,表示这个数组保存的字符串的长度是4。
free:4,表示这个数组的未使用空间是4。
buf:是一个字符数组,前4个字节保存的是字符'L'、‘u’、‘o’、‘L’,最后一个字节保存的是字符串结束标识‘\0’。
sds遵循C字符串以空字符结尾的惯例,保存空字符的1字节不计算在sds的len中,并且为空字符分配额外的1字节空间,以及添 加空字符到字符串末尾等操作,是由sds函数自动完成的。这样遵循空字符结尾这一惯例的好处是,sds可以直接重用一部分c字符串函数库里面的函数。
2,sds与c字符串的区别
既然sds中保存的也是字符串,为什么不直接使用字符串,而还要封装成一个结构体呢?
sds的三点优势:
- 常数复杂度获取字符串的长度,通过len属性获取长度,复杂度是O(1),字符串循环判断,是O(n)。
- 杜绝缓冲区溢出。
- 减少修改字符串时带来的内存分配次数。
3,杜绝缓冲区溢出
C字符串不记录自身长度带来的另一问题就是容易造成缓冲区溢出。而sds API需要对sds进行修改时,API会先检查sds的空间是否满足修改要求,如果不满足的话,API会自动将sds的空间扩展至需要的大小,然后执行下面的动作。
4,减少修改字符串时带来的内存分配次数
由于C字符串不记录长度,所以对于一个包含N个字符的C字符串来说,这个字符串的底层实现总是一个N+1个字符长的数组,所以每次增长或缩短一个C字符串,都要对这个数组进行一次内存重分配。
而sds中,buf数组的长度不一定是字符数量加一,数组里面可以包含未使用的字节,通过free 属性,实现了空间预分配和惰性空间释放两种优化策略。
1) 空间预分配。
空间预分配用于优化sds的字符串增长操作:当sds的API 对一个sds 进行修改,并且需要对sds进行空间扩展的时候,不仅会为sds 分配修改所必须的空间,还会为sds 分配额外的未使用空间。
其中,额外分配的未使用空间数量由以下公式决定:
A,如果对sds 进行修改后,sds的长度(len值)将小于1MB,那么分配和len 同样大小的未使用空间,这时sds 的len和free 的值相同。比如,进行修改后,sds 的len 将变成7字节,那么也会分配7字节的未使用空间,sds 的buf数组的实际长度将变成
7+ 7 + 1字节(额外的1字节保存空字符)。
B,如果对sds 进行修改后,sds的长度(len值)将大于等于1MB,那么分配1MB的未使用空间,即free的值是1MB。比如。如果修改后,sds 的len将变成30MB,那么会分配1MB 的未使用空间,sds 的buf数组的实际长度将变成30MB + 1MB + 1字节(额外的1字节保存空字符)。
2) 惰性空间释放
惰性空间释放用于优化sds 的字符串缩短操作:当sds的API 需要缩短sds 保存的字符串时,程序并不立即使用内存分配来回收缩短后多出来的字节,而是使用free 属性将这些字节数量记录起来,等待将来使用。
5,二进制安全
A,C字符串中的字符必须符合某种编码(一般是ASCII编码),并且除了字符串的末尾外,字符串里面不能有空字符。
B,sds 的API 都是二进制安全的,所有的sds API都会以处理二进制的方式来处理sds 存放在buf数组里的数据,不会对数据有限制。即redis 不是用这个数组来保存字符,而是用它来保存一系列二进制数据。因为sds 使用len属性的值来而不是空字符来判断字符串是否结束。
6,总结