一.使用场景
redis没有直接使用C语言传统的字符串表示(以空字符结尾的字符数组,以下简称C字符串),而是构建了简单动态字符串(Simple dynamic String SDS)的抽象类型,并将SDS作为默认的字符串表示形式。
redis里面C字符串只用在字符串字面量(String literal) ,用在一些无需对字符串修改的地方。
比如打印日志redisLog(REDIS_WARING,"redis is now ready to exit, bye bye...")
1.redis需要的不仅仅是一个字符串字面量,而是一个可修改的字符串值时,redis就会使用SDS来表示字符串值,比如在redis数据库里面,包含字符串值的键值对在底层都是由SDS实现的。
2.SDS还被用作缓冲区:AOF模式中的缓冲区,以及客户端状态中的输入缓冲区,都是SDS实现的。
二.定义
1.每一个sds.h/sdshdr结构表示SDS的值
struct sdshdr {
//记录buf数组中已使用的字节数量
//等于SDS所保存的字符串长度
int len;
//记录buf数组中未使用的字节数量
int free;
//字节数组,用于保存字符串
char buf[];
}
free为0,表示SDS没有分配任何未使用空间
len =5,表示SDS保存了5字节长度的字符串
buf 是一个char类型的数组,数组的前五个字节分别保存了 'r','e','d','i','s' 五个字符,最后一个字节则表示了空字符' '
SDS遵循了C字符串以空字符结尾的惯例,保存空字符串的1个字节空间,不计算在SDS的属性里面,并且为控制符串分配额外的1字节空间,
以及添加空字符到字符串末尾等操作,都是有SDS自动完成的,所以这个空字符对于SDS使用者是完全透明的,
遵循空字符结尾这一惯例的好处是,SDS可以重用一部分C字符串函数库里的函数。
图2-2,展示了buf数组分配了5个未使用空间的字节,所以 free=5;
三.SDS和C字符串区别
C语言使用长度为N+1的字符来表示长度为N的字符串,最后一个元素是空字符 ' '
1.常数复杂度获取字符串长度
因为C字符串并不记录自身长度信息,所以获取C字符长度要从头到尾遍历,复杂度是O(N)。
和C字符串不同,SDS的len属性记录了自身长度,所以获取SDS长度的复杂度是O(1)。
2.杜绝缓冲区溢出
C字符串也是因为不记录自身长度带来的另一个问题就是易造成缓冲区溢出, C字符串不记录自身长度,所以在strcat的时候,如果没有足够多的内存容纳新的要拼接字符点长度,就会产生缓冲区溢出。
所以C字符串要在执行前就要分配足够的空间,如果忘了,可能产生缓冲区溢出或者在紧邻两个字符之s1 s2间,修改s1数据可能会溢出到s2的空间中,导致s2保存的内容被意外修改。
与C字符串不同,SDS的空间分配策略完全杜绝了发生缓冲区的可能,当SDS API需要对SDS进行修改,API会先检查SDS空间是否满足修改所需的要求,如果不满足的话,API会自动将SDS的空间扩展至执行修改所需要的大小,
所以SDS既不需要手动修改SDS的空间大小,也不会出现前面所说的缓冲区溢出。
3.减少修改字符串时带来的内存分配次数
因为C字符串不记录自身长度,所以对于一个包含N个字符的C字符串来说,这个C字符串的底层实现总是一个N+1个字符长的数组(额外一个字符空间用于保存空字符)。
因为C字符串的长度和底层数组的长度之间存在着这种关联性,所以每次增长或缩短一个C字符串,程序都总要对保存这个C字符串的数组进行一次内存重分配操作。
a.如果程序执行拼接操作,那么在执行这个操作之前,程序要通过内存重分配扩展底层数组的空间大小,如果忘记了这一步骤就会产生缓冲区溢出。
b.如果程序执行截断操作,那么在执行这个操作之后,程序需要通过内存重分配来释放字符串不在使用的那部分空间,如果忘了这一步骤就会产生内存泄漏。
为了避免C字符串的缺陷,SDS通过未使用空间解除了字符串长度和底层数组长度之间的关联,在SDS中,buf数组的长度不一定就是字符数量加1.数组里面可以包含未使用的字节,而这些字节的数量就由SDS的free属性记录。
3.1空间预分配
用于优化SDS的字符串增长操作,当SDS的API对SDS进行修改。并且需要对SDS空间进行扩展的时候,程序不仅会为SDS分配修改所必须要的空间,还会为SDS分配额外的未使用空间。
其中,额外分配的未使用空间数量由一下公式决定:
a.如果对SDS进行修改之后,SDS的长度(也即是len属性的值)将小于1MB,那么程序分配和len属性同样大小的未使用空间,这时SDS len属性的值将和free属性的值相同,
举个例子,如果进行修改之后,SDS len属性将变成13字节,那么程序也会分配13字节的未使用空间,SDS的buf数组的实际长度将变成13+13+1=27字节(额外一个空字节用于保存空字符)
b.如果对SDS进行修改之后,SDS的长度将大于1MB,那么程序会分配1MB的未使用空间。
举个例子,如果修改之后,SDS的len将变为30MB,那么程序会分配1MB的未使用,SDS的buf数组的实际长度为30MB+1MB+1byte
通过空间预分配策略,redis可以减少连续执行字符串增长操作所需的内存重分配次数。
3.2惰性空间释放
用于优化SDS字符串缩短的操作,当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用。
与此同时,SDS也提供了相应的api,让我们可以在有需要时,真正的释放未使用空间,所以不用担心惰性空间释放策略会造成内存浪费。
4.二进制安全
C字符串中的字符必须符合某种编码(比如ASCII),并且除字符串的末尾之外,字符串里面不能包括空字符,否则最先被程序读入的空字符被误认为字符串结尾,这些使得C字符串只能保存文本数据,不能保存图片、音频、视频、压缩文件这样的二进制数据。
SDS的API都是二进制安全的,因为SDS使用len属性的值而不是空字符来判断字符串是否结束。redis不仅可以保存文本,还可以保存任意格式的二进制数据。
5.兼容部分C字符串函数
SDS虽然是二进制安全的,但是遵循了C字符串以空字符结尾的惯例,这些API总会将SDS保存的数据结尾设置空字符,并且总会在为buf数组分配空间时多分配一个字节来容纳这个空字符, 这是为了让那些保存文本数据的SDS可以重用一部分<string.h>库定义的函数。
四.总结
五.SDS API
六.重点回顾
1.redis只会使用字符串作为字面量,在大多数情况下,redis使用SDS(Simple Dynamic String,简单动态字符串)作为字符串表示
2.比起C字符串,SDS具有以下优点
1).常数繁杂度获取字符串长度
2).杜绝缓冲区溢出
3).减少修改字符串长度时所需的内存重分配次数
4).二进制安全
5).兼容部分C字符串函数
本文摘自《redis设计与实现》 黄健宏 著