Redis中存字符串,是实际使用场景中最常用的方式,但是redis并没有直接使用C语言中传统的字符串表示,而是构建了一种名为简单动态字符串(SDS)的抽象类型,本文带你利用Java实现SDS基本结构。
1、SDS定义
我们首先看一下SDS的字符串的整体结构如下图:
所以,我们首先创建一个包含free、len和char类型数组buf,代码如下:
public class SDS {
private static char endChar= '\0';
//字符串长度
private int len;
//buf数组中未使用的字节数
private int free;
//字节数组
private char[] buf;
//构造函数
public SDS(String str){
this.len = str.length();
this.buf = Arrays.copyOf(str.toCharArray(),this.len+1);
this.buf[len] = endChar;
this.free = buf.length - len - 1;
}
//-----------省略set&get--------
}
类SDS自带有参构造函数,用于对sds赋值时进行初始化,为了保证sds的字符串符合传统C语言对字符串的存储,在buf数组后会添加 '\0’来代表字符串的结尾,用一下代码测试一下SDS的基本结构:
public class TestRedis {
public static void main(String[] args) {
SDS sds = new SDS("a a");
char[] a = sds.getBuf();
for(char aTemp:a){
System.out.println(aTemp);
}
}
}
输出结果如下:
从结果中我们发现,字符串中自带的空格和最后结尾增加的\0在控制台输出,都是空格,这是不是就代表如果字符串中存在空格,就会被误认为是字符串的结尾标志?基于此疑问,我们对代码进行了测试:
public class TestRedis {
private static char test = '\0';
public static void main(String[] args) {
SDS sds = new SDS("a a");
char[] a = sds.getBuf();
for(char aTemp:a){
System.out.println("--------");
System.out.println(aTemp == test);
}
}
}
测试结果如下:
我们发现,即使字符串中携带空格,也不会被当成字符串的结尾标志。
2、SDS和C字符串的区别
根据SDS的基本结构,我们可以对比一下SDS和C字符串的区别:
2.1 常量级获取字符串长度
SDS中存在私有属性len,用来表示buf中除去\0之外,字符串的长度,所以SDS获取字符串长度的时间复杂度是常数级O(1)的,而C语言中字符串是通过遍历字符串获取,其时间复杂度为O(N)
2.2 避免缓存区溢出
在C语言中,当对字符串进行修改时,如果没有分配足够多的内存,就会导致缓冲区溢出,但是SDS会先检查SDS的空间是否满足修改所需的要求,如果不满足,会自动对SDS进行扩容,然后在进行实际的修改操作;
2.3 内存重分配
在SDS中存在free空间,SDS通过free空间,实现了空间预分配和惰性释放两种优化策略,来实现内存重分配;
2.3.1 空间预分配
当对SDS进行修改时,会通过设置合理的free空间,来减少SDS内存分配的次数,从而增加效率,SDS对free空间分配应该符合以下条件:
1、如果修改后的SDS长度小于1MB(1 (2^20)B),那么free空间=len大小,buf的实际长度为2len+1;
2、如果修改后的SDS长度大于1MB(1 * (2^20)B),那么free空间=1MB,buf的实际长度为1MB+len+1;
我们通过实现字符串拼接功能sdscat(),来实现空间预分配策略。
//拼接字符串
public void sdscat(String str){
char[] strTemp = str.toCharArray();
int strTempLen = str.length();
int lastLen = this.len + strTempLen;
//先判断长度
if(lastLen < 1 * Math.pow(2,20)){
//小于1MB(2^20B),那么free空间=len大小,buf的实际长度为2*len+1
this.free = lastLen;
}else{
//大于1MB(2^20B),那么free空间=1MB,buf的实际长度为1MB+len+1
this.free = (int)Math.pow(2,20);
}
this.len = lastLen;
//拼接数组
char[] originChar = this.toString().toCharArray();
char[] result = Arrays.copyOf(originChar,lastLen);
System.arraycopy(strTemp,0,result,originChar.length,strTemp.length);
this.buf = Arrays.copyOf(result,lastLen+1);
this.buf[lastLen] = endChar;
}
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder("");
for(int i=0;i<this.buf.length;i++){
if(this.buf[i] != '\0'){
stringBuilder.append(this.buf[i]);
}
}
return stringBuilder.toString();
}
2.3.2 惰性空间释放
上一小节,空间预分配讲的的是free空间如何创建,而惰性空间释放讲的其实是free空间怎么用,既然我们创建了对于的free空间,那么如何使用呢?
在对SDS进行增加操作时,会优先使用free空间,如果free空间足够当前新增操作的字符串,则直接分配,不用执行内存的重新分配,如果free空间不足之处,则会按照2.3.1中提到的规则进行空间分配,根据此,我们对字符串拼接sdscat函数进行完善如下:
public void sdscat(String str){
char[] strTemp = str.toCharArray();
int strTempLen = str.length();
if(strTempLen>this.free){
int lastLen = this.len + strTempLen;
//先判断长度
if(lastLen < 1 * Math.pow(2,20)){
//小于1MB(2^20B),那么free空间=len大小,buf的实际长度为2*len+1
this.free = lastLen;
}else{
//大于1MB(2^20B),那么free空间=1MB,buf的实际长度为1MB+len+1
this.free = (int)Math.pow(2,20);
}
this.len = lastLen;
//拼接数组
char[] originChar = this.toString().toCharArray();
char[] result = Arrays.copyOf(originChar,lastLen);
System.arraycopy(strTemp,0,result,originChar.length,strTemp.length);
this.buf = Arrays.copyOf(result,lastLen+1);
this.buf[lastLen] = endChar;
}else{
//当free空间足够分配
char[] originChar = this.toString().toCharArray();
char[] result = Arrays.copyOf(originChar,this.len + this.free);
System.arraycopy(strTemp,0,result,originChar.length,strTemp.length);
this.buf[strTempLen+this.len] = endChar;
this.free = this.free - strTempLen;
this.len = this.len + strTempLen;
}
}
自此,SDS介绍完毕,如有问题,欢迎留言交流,谢谢。