本文大部分内容引自《Redis深度历险:核心原理和应用实践》,感谢作者!!!
Redis字符串结构
Redis中的字符串是可修改的字符串,在内存中是以字节数组形式存在的
SDS
Redis中的字符串叫做“SDS”,也就是Simple Dynamic String,它的结构是带长度信息的字节数组;字符串是可以修改的字符串,支持append操作
struct SDS<T> {
T capacity; //数组容量
T len; //数组长度
byte flags; //特殊标识位,不理睬它
byte[] content; //数组内容
}
1、content种存储了字符串的真正内容
2、capacity表示了所分配数组的长度
3、len表示字符串的实际长度
/**
* Append the specified binary-safe string pointed by 't' of 'len' bytes to the
* end of the specified sds string 's'.
*
* After the call, the passed sds string is no longer valid and all the
* references must be substituted with the new pointer returned by the call.
*/
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); //设置追加后的长度值
s[curlen+len] = '\0'; //让字符串以\0 结尾,便于调试打印,还可以直接使用 glibc 的字符串函数进行操作
return s;
}
4、扩容时如果capacity不够容纳追加的内容就会重新分配字节数组并复制原字符串的内容到新数组中;SDS结构使用了泛型T,因为当字符串较短时len和capacity可以使用byte和short表示;Redis中的字符串的长度不能超过512M,创建字符串时len和capacity一样大,不会多分配冗余空间,因为绝大多数场景下不会使用append操作来修改字符串
embstr vs raw
> set codehole abcdefghijklmnopqrstuvwxyz012345678912345678
OK
> debug object codehole
Value at:0x7fec2de00370 refcount:1 encoding:embstr serializedlength:45 lru:5958906 lru_seconds_idle:1
> set codehole abcdefghijklmnopqrstuvwxyz0123456789123456789
OK
> debug object codehole
Value at:0x7fec2dd0b750 refcount:1 encoding:raw serializedlength:46 lru:5958911 lru_seconds_idle:1
为什么两段字符串只有一个字符的差别存储形式就发生了变化?
先了解一下Redis对象头结构体,所有的Redis对象都有下面这个结构头
struct RedisObject {
int4 type; // 4bits
int4 encoding; // 4bits
int24 lru; // 24bits
int32 refcount; // 4bits
void *ptr; // 8bytes,64-bit system
} robj;
不同对象具有不同的类型type(4bit),同一个类型的type会有不同的存储形式encoding(4bit),使用24bit的长度来记录对象的LRU信息。每个对象都有引用计数,当引用计数为0时对象会被销毁,内存会被回收;ptr指针指向对象内容(body)的具体存储位置。一个RedisObject对象头需要占据16字节的存储空间
继续看SDS在字符串比较小的时候泛型T可以使用byte和short表示,SDS对象头的大小是capacity+3,至少是3字节,意味着分配一个字符串的最小空间占用为19字节;embstr存储形式是将对象头RedisObject和SDS对象连续存在一起(使用一次malloc),而raw存储需要两次malloc,两个对象头在内存地址上一般是不连续的;内存分配器jemalloc/tcmalloc分配内存大小的单位都是2、4、8、16、32、64等等,为了容纳一个完整的embstr对象最少会分配32字节的空间;如果字符串总长度超过了64字节,Redis认为它是一个大字符串,不再使用emdstr形式存储,而使用raw形式
struct SDS {
int8 capacity; //1byte,int8即为byte
int8 len; //1byte
byte flags; //1byte
byte[] content; //内联数组,长度为capacity
}
当内存分配了64字节的空间时,字符串最大长度是44的原因(一个字符占用1字节)
SDS结构中content中的字符串是以字节\0结尾的,\0便于glibc的字符串处理函数以及调试打印输出;留给content的长度最多只有45(64-19),content字符串以\0结尾,所以embstr最大能容纳的字符串长度是44
字符串扩容策略
字符串长度在小于1M之前扩容采用加倍策略,当长度超过1M之后,为避免家背后的冗余空间过大导致浪费,每次扩容只会多分配1M大小的冗余空间