文章目录
目的
- 编写本文章的目的是为了理解Redis底层实现的重要数据结构:简单动态字符串,并实际动手通过java代码实现简单动态字符串的空间预分配机制,让我们更加生动地理解底层技术。
一、简单动态字符串(SDS)
1.1 定义
- 简单动态字符串是Redis底层结构中非常关键的一类数据结构,定义如下:
struct sdshdr {
// 字符串数组
char[] buff;
// buff字符串数组中已用的长度
int len;
// buff字符串数组中未用的长度
int free;
}
- 举个例子: buff数组中存放了“redis”5个字符,那buff字符串数组中已用的长度len即为5,但buff同时还预留了另外5个字节空间,未存放任何字符,那预留的这5个字节空间用free表示。
1.1 优点
- 相比普通的字符串,使获取字符串长度时间复杂度降为O(1),即直接取len值就获取到了字符串的长度。
- 相比普通的字符串,这种预留未分配空间的机制可以减少修改字符串时带来的内存重分配次数,加快了程序执行的速度。
二、空间预分配
2.1 原则
- 空间预分配用于优化SDS的字符串增长操作:当SDS的API对一个SDS进行修改,并且需要对SDS进行空间扩展的时候,程序不仅会为SDS分配修改所必须要的空间,还会为SDS分配额外的未使用空间,分配原则如下:
- 如果对SDS进行修改之后,SDS的长度(也即是len属性的值)将小于1MB,那么程序分配和len属性同样大小的未使用空间,这时SDS len属性的值将和free属性的值相同。
-
如果对SDS进行修改之后,SDS的长度(也即是len属性的值)将大于等于1MB,那么程序会分配1MB的未使用空间。
2.2 java代码实现
- 创建SDS类
- 编定api :新建,扩展空间,追加字符串等
- 测试api,检查空间预分配机制
/**
* Redis 简单动态字符串空间预分配的实现
*
* @author zhuhuix
* @date 2021-01-12
*/
public class SDS {
// 已使用的长度
private int len;
// 未使用的长度
private int free;
// 字符数组
private char[] buff;
private SDS set(SDS sds) {
this.buff = sds.buff;
this.len = sds.len;
this.free = sds.free;
return this;
}
// 创建一个给定字符数组的SDS
public SDS sdsNew(char[] s) {
this.len = s.length;
this.free = 0;
this.buff = new char[this.len];
this.buff = Arrays.copyOf(s, this.len);
return this;
}
// 按新长度给SDS重新分配空间
public SDS sdsNewLen(int newLen) {
if (this.len < newLen) {
this.free = newLen - this.len;
// 缓存原有SDS字符串
char[] buffCopy = Arrays.copyOf(this.buff, this.len);
// 新空间分配
this.buff = new char[newLen];
// 将缓存的字符串复制到SDS中
this.buff = Arrays.copyOf(buffCopy, newLen);
}
return this;
}
// 将给定字符数组拼接到SDS结尾处
public SDS sdsCat(char[] s) {
int lenCat = s.length;
int lenNew = lenCat + this.len;
// 如果SDS中的空闲不够新的字符数组存储则需要扩充
if (this.free < lenCat) {
// 如果需要扩充的长度小于1M,
// 按空间预分配的原则,程序分配和len属性同样大小的未使用空间,
// 这时SDS len属性的值将和free属性的值相同
if (lenNew < 1024) {
set(sdsNewLen(lenNew * 2));
}
// 如果需要扩充的长度大于等于1M,
// 按空间预分配的原则,程序分配1M的未使用空间
else {
set(sdsNewLen(lenNew + 1024));
}
}
for (int i = 0; i < lenCat; i++) {
this.buff[this.len + i] = s[i];
}
this.len = lenNew;
this.free = this.buff.length -lenNew;
return this;
}
@Override
public String toString() {
return "SDS{" +
"len=" + len +
", free=" + free +
", buff=" + Arrays.toString(buff) +
'}';
}
public static void main(String[] args) {
SDS sds = new SDS();
// 新建
sds.sdsNew("redis".toCharArray());
System.out.println("new: "+sds.toString());
// 扩充
sds.sdsNewLen(10);
System.out.println("newLen to 10: "+sds.toString());
// 追加
String s1 = " welcome";
sds.sdsCat(s1.toCharArray());
System.out.println("append string: "+sds.toString());
// 再次追加
String s2 = "!!!";
sds.sdsCat(s2.toCharArray());
System.out.println("append string again: "+sds.toString());
}
}
- 在以上测试代码中我们通过自行编写的api创建了一个SDS–“redis”, 并将此SDS扩展到10位长度,即已用长度5位,未用长度5位,在此基础上进行拼接字符串,观察预分配机制是否实现,最后直接通过已预留的空间再次拼接字符串,以下就是这个过程的输出结果:
三 、小结
- 在一般程序中,如果修改字符串长度的情况不太常出现,那么每次修改都执行一次内存重分配是可以接受的。
- Redis作为数据库,经常被用于速度要求严苛、数据被频繁修改的场合,如果每次修改字符串的长度都需要执行一次内存重分配的话,那么光是执行内存重分配的时间就会占去修改字符串所用时间的一大部分,如果这种修改频繁地发生的话,可能还会对性能造成影响。
- 简单动态字符串(SDS)的空间预分配机制正是Redis性能保证的一种有效手段。