丝分缕析——探索“字符串”内部
Redis中的字符串是可以修改的字符串,在内存中是以字节数组的形式存在的。C语言里面的字符春标准形式以NULL作为结束符,但在Redis里面字符串不是这么表示的。因为获取NULL结尾的字符串要strlen函数,复杂度为O(n)。
Redis的字符串叫“SDS”,也即是Simple Dynamic String。它的结构是一个带长度信息的字节数组。
struct SDS<T>{
T capacity; // 数组容量
T len; // 数组长度
byte flags; // 特殊标志位,不用理睬它
byte[] conten; // 数组内容
}
capacity 表示分配数组的长度,len表示字符串的实际长度。字符串是可以修改的,要支持append操作,如果数组没有冗余空间,追加操作必然涉及分配新数组,然后将旧内容复制过来,在append新内容。
SDS结构使用泛型T,而不直接使用int,因为当字符串比较短时,len和capacity可以使用byte和short来表示,Redis为了对内存做极致的优化,不同长度的字符串使用不同的结构体来表示。
Redis规定字符串的长度不得超过512MB,创建字符串时len和capacity一样长,不会多分配冗余空间,因为大多数场景下,不会使用append操作来修改字符串。
embstr VS raw
Redis 的字符串又两种存储方式,在长度特别短时,使用embstr 形式存储,而当长度超过44字节时,使用raw形式存储。
set codehole ndosdoand
debug object codehole
encoding字段,有存储形式
Redis对象头结构
struct RedisObject{
int4 type;
int4 encoding;
int24 lru;
int32 refcount;
void *ptr;
}robj;
不同的对象具有不同的类型type,同一个类型的type会有不同的存储形式encoding。为了记录对象的LRU信息,使用24个bit来记录LRU信息。每个对象都是引用计数,当引用计数为零时,对象就会被销毁,内存被回收。ptr指针将指向对象内容的具体存储位置。这样一个RedisObject对象头结构需要占据16字节的存储空间。
再看SDS结构体的大小,在字符串比较小时,SDS对象头结构的大小是capacity+3。
struct SDS {
int8 capacity; // 1byte
int8 len; // 1byte
int8 flags; // 1byte
byte[] conten; // 数组内容
}
embstr存储形式是这样一种存储形式,它将RedisObject对象头结构和SDS对象连续存在一起,使用malloc方法一次分配,而raw存储形式不一样,需要使用两次malloc方法,两个对象头在内存地址上一般是不连续的。
而内存分配器jemalloc、tcmalloc等分配内存大小的单位都是2/4/8/16/32/64字节等,为了容纳一个完整的embstr对象,jemalloc最少会分配32字节的空间,如果字符串再稍微长一点,那就是64字节的空间。如果字符串总体超出64字节,Redis认为是一个大字符串,不再适合使用emdstr形式存储,而使用raw形式
当内存分配器分配64字节空间时,这个字符串长度最大可以是 44 字节
SDS结构体中的content中的字符串是以NULL结尾的字符串,之所以多出这样一个字节,是为了便于直接使用glibc的字符串处理函数,以及便于字符串的调试打印输出。
16 字节(RedisObject对象头)+ 3字节(SSD对象)+1字节(NULL结尾)= 20字节
扩容策略
在字符串长度小于1MB之前,扩容空间采用加倍策略,也就是保留100%冗余空间。当字符串长度超过1MB之后,为了避免加倍后的冗余空间过大而导致浪费,每次扩容只会多分配1MB大小的冗余空间