Redis的字符串采用的是自定义的struct,名字叫做简单动态字符串(simple dynamic string,SDS)。
结构如下:
struct sdshdr{
int len;
int free;
char buf[];
};
采用如此结构的好处是:
【1】获取length的时候复杂度为O(1),不需要O(n);
【2】动态分配空间,避免缓冲区溢出,避免每次修改或者append都重新分配;
【3】二进制安全;
关于第一点显而易见,第二点,为了减少修改字符串带来的内存重分配次数,redis采用了2个措施:
1)空间预分配;假设如下sds(状态【0】),执行命令【sdscat(s,”redis”)】往其中append一个字符串“redis”之后将变为状态【1】,发生了什么呢?当sds发现当前free长度无法分配新添加的字符串时,将发生一次空间分配,如果修改之后sds长度小于1MB,则会分配总长度为【修改后长度】*2+1,即为(5+5)*2+1,1为结尾符号空间。如果修改后sds总长度大于等于1MB,则会分配【修改后总长度】+1MB的长度。假设再次执行命令【sdscat(s,”redis”)】,由于当前状态【1】free=10,有足够空间分配新加入的字符串,则不会发生空间分配操作,变为状态【2】;
状态【0】
struct sdshdr{
int len=5;
int free=4;
char buf[]={‘h‘,‘e‘,‘l‘,‘l‘,‘o‘,‘\0‘,‘‘,‘‘,‘‘,‘‘};
};
状态【1】:
struct sdshdr{
int len=10;
int free=10;
char buf[]={‘h‘,‘e‘,‘l‘,‘l‘,‘o‘,‘r‘,‘e‘,‘d‘,‘i‘,‘s‘,‘\0‘,...};
};
状态【2】:
struct sdshdr{
int len=15;
int free=5;
char buf[]={‘h‘,‘e‘,‘l‘,‘l‘,‘o‘,‘r‘,‘e‘,‘d‘,‘i‘,‘s‘,‘r‘,‘e‘,‘d‘,‘i‘,‘s‘,‘\0‘,...};
};
2)惰性空间释放;现在假设从状态【2】执行命令【sdstrim(s,”re”)】,移除sds中所有的’r’,’e’,将转换为状态【3】; 此时并没有释放空间,len+free依然等于20;这样做的目的是避免了缩短字符串时的内存重分配操作,并且未将来的字符串扩充预留了空间 ; 当然也可以使用【sdsfree】真正释放sds;
状态【3】:
struct sdshdr{
int len=11;
int free=9;
char buf[]={‘h‘,‘e‘,‘l‘,‘l‘,‘o‘,‘d‘,‘i‘,‘s‘,‘d‘,‘i‘,‘s‘,‘\0‘,...};
};
关于第三点,SDS的buf属性称为字节数组,保存的是二进制数据;同时由于使用len字段来判断字符串是否结束,所以是安全的,不会出现c字符串中以空格作为结尾判断,导致字符串被截断的问题。