字符串
C语言中的字符串标准形式是以NULL(十六进制的 0x\0) 作为结束符,如果要获取字符串的长度需要使用strlen标准库函数,该函数算法复杂度是O(n),需要对整个字符串进行遍历,Redis无法承受这么慢的速度,
因此Redis中的字符串在内存中是以字节数组的形式存在,叫做SDS (Simple Dynamic String),是一个带着长度信息的字节数组。
struct SDS<T> {
T capacity; // 数组容量
T len; // 数组长度
byte flags; // 特殊标志位
byte[] content; // 数组内容
}
SDS的结构很像Java中的ArrayList,content数组中存储着真实的内容,但是这个数组一般会比内容的实际长度要多一些冗余空间,
capacity表示数组的长度,len表示真实的字符串长度,因为字符串在redis中是可修改的,需要支持append操作,
如果数组没有冗余空间,append的时候就必须重新开辟一个新数组,然后将老数组中的内容复制过去,这是一个比较大的开销。
SDS中使用了泛型T,而不是直接使用int,因为当字符串比较短的时候,len和capacity可以通过byte和short表示,以此达到节省空间的作用,所以不同长度的字符串会用不同的结构体表示。
追加字符串的代码:
sds sdscatlen(sds s, const void *t, size_t len) {
// 原字符串长度
size_t curlen = sdslen(s);
// 按需调整空间,如果capacity不够容纳追加的内容,就会重新分配字节数组,并将原字符串中的内容复制到新数组中
s = sdsMakeRoomFor(s,len);
// 内存不足
if (s == NULL) return NULL;
// 追加目标字符串的内容到字节数组中
memcpy(s+curlen, t, len);
// 设置追加后的长度值
sdssetlen(s, curlen+len);
// 让字符串以\0结尾
s[curlen+len] = '\0';
return s;
}
Redis规定字符串长度最大不能超过512MB,创建字符串时 len 和 capacity 是一样长的,不会分配冗余空间,因为大多数情况下,不会用到字符串的append。
embstr 与 raw
Redis字符串有两种存储方式,长度很短时,使用 embstr 形式存储,字节长度超过44时,使用raw形式存储。
44个字符:
127.0.0.1:6379> set hello abcdefghijklmnopqrstuvwxyz012345678912345678
OK
127.0.0.1:6379> debug object hello
Value at:0xffff9f531e40 refcount:1 encoding:embstr serializedlength:45 lru:14436129 lru_seconds_idle:5
45个字符:
127.0.0.1:6379> set hello abcdefghijklmnopqrstuvwxyz0123456789123456789
OK
127.0.0.1:6379> debug object hello
Value at:0xffff9f18d2e0 refcount:1 encoding:raw serializedlength:46 lru:14436171 lru_seconds_idle:2
上面两端内容中,encoding表示存储形式,可以看到44个字节是embstr,45个就是raw了。
对象头
Redis每个对象都有一个头结构:
struct RedisObject {
int4 type ; // 4bits
int4 encoding ; // 4bits
int24 lru ; // 24bits
int32 refcount ; // 4bytes
void *ptr; // 8bytes
} robj ;
每个对象都有类型type、存储形式encoding,以及对象的lru信息,
还有refcount,代表对象的引用计数,为0时对象被销毁回收,ptr指针指向对象内容(body)的具体存储位置,
对象头将占用16字节的存储空间。
在字符串内容较小时,SDS结构体的内存占用空间情况如下:
struct SDS {
int8 capacity; //1byte
int8 len; //1byte
int8 flags; //1byte
byte[] content; //内容数组,长度为capacity
}
也就是说SDS对象头结构的16字节再加上SDS结构中的3个字节,意味着一个字符串至少占用19个字节,
embstr在内存中存储时,位置与对象头在一起,使用malloc方法一次性分配对象头+对象的空间,而raw不是,需要进行两次malloc分配,对象头和内容在内存上不是连续的。
因为内存分配器jemalloc、tcmalloc分配内存大小的单位都是2/4/8/16/32/64字节,为了容纳一个embstr的字符串,jemalloc一般会分配32个字节,如果字符串更长一些就是64个字节,
如果字符串总体超过64个字节的时候,Redis认为其不适合使用embstr形式存储,该使用raw了。
因为Redis对象头就占用了16个字节,再加上3个属性的总和3字节,还剩 64 - 19 = 45 个字节,字符串需要用NULL结尾,NULL又占了一个字节,所有最终只剩下44个字节用于存字符串的内容了,这就是上面看到的超过44个字节encoding就变成了raw的原因。
扩容策略
在字符串小于1MB前,拓容空间将直接翻倍,超过后,为了避免浪费,每次拓容仅追加1MB的冗余空间。