Redis阅读笔记-Redis字符串内部结构
Redis阅读笔记-Redis字符串内部结构
Redis中的字符串是可以修改的字符串,在内存中它是以字节数组的形式存在。我们知道C语言中的字符串标准形式是以NULL作为结束符, 但是在Redis里面字符串不是这么表示的。因为要获取NULL结尾的字符串的长度使用strlen标准库函数, 这个函数的算法复杂度是O(n),它需要对字节数组进行遍历扫描, 作为单线程的Redis表示承受不起。
Redis的字符串叫[SDS],也就是Simple Dynamic String(简单动态字符串)。它的结构是一个带长度的字节信息数组。
struct SDS<T> {
//数组容量
T capacity;
//数组长度
T len;
//特殊标志位
byte flags;
//数组内容
byte[] content;
}
如代码所示, content里存储了真正的字符串内容, 那么capacity和len表示什么呢?它有点类似Java语言的ArrayList结构, 需要比实际的内容常多多分配一些冗余空间。 capacity表示所分配数组的长度,len表示字符串的实际长度。 前面提到字符串是可以修改的字符串, 它要支持append操作。 如果数组没有冗余空间,那么追加操作必然涉及到分配新数组, 然后将旧内容复制过来, 在append新内容。 如果字符串的长度非常长, 这样的内存分配和复制开销就会非常大。
上面的SDS结构使用了范型T,为什么不直接用int呢, 这是因为当字符串比较短时, len和capacity可以使用byte和shot表示, Redis为了对内存做极致的优化, 不同长度的字符串使用不同的结构体表示。
Redis规定字符串的长度不得超过512M字节。创建字符串时len和capacity一样长, 不会多分配冗余空间,这是因为大多数场景下我们不会使用append操作来修改字符串。
embstr VS raw
Redis的字符串有两种存储方式, 在长度特别短时, 使用emb形式存储(embeded), 当长度超过44时, 使用raw形式存储。
这两种类型有什么区别呢?为什么分界线是44呢?
127.0.0.1:6379> set codehole abcdefghijklmnopqrstuvwxyz012345678912345678
OK
127.0.0.1:6379> debug object codehole
Value at:0x7fec2de00370 refcount:1 encoding:embstr serializedlength:45 lru:5958906 lru_seconds_idle:1
127.0.0.1:6379> set codehole abcdefghijklmnopqrstuvwxyz0123456789123456789
OK
127.0.0.1:6379> debug object codehole
Value at:0x7fec2dd0b750 refcount:1 encoding:raw serializedlength:46 lru:5958911 lru_seconds_idle:1
注意上面debug object输出中有个encoding字段,一个字符的差别, 存储形式就发生了变化。这是为什么呢?
为了解释这个现象, 我们首先来了解下Redis对象头结构体, 所有的Redis对象都有下面的结构头:
struct RedisObject {
//4bits
int4 type;
//4bits
int4 encoding;
//24bits
int24 lru;
//4bytes
int32 refcount;
//8bytes, 64-bit system
void *ptr;
}robj;
不同的对象具有不同的类型type(4bit), 同一类型的type会有不同的存储形象encoding(4bit),为了记录对象的LRU信息, 使用了24个bit来记录LRU信息。每隔对象都有个引用计数, 当引用计数为零时, 对象就会被销毁, 内存被回收。ptr指针将指向对象内容(body)的具体存储位置。这样一个RedisObject对象头需要占据16个字节的存储空间。
接着再看SDS结构体的大小, 在字符串比较小时, SDS对象头的大小是capacity+3,至少是3。意味着分配一个字符串的最小空间占用为19(16+3)个字节。
struct SDS {
//1byte
int8 capacity;
//1byte
int8 len;
//1byte
int8 flags;
//内联数组, 长度为capacity
byte[] content;
}
如图所示, embstr存储形式是这样的一个存储形式, 它将RedisObject对象头和SDS对象连续存在一起, 使用malloc方法一次分配。而raw存储形式不一样, 它需要两次malloc,两个对象头在存储地址上一般是不连续的。
而内存分配器jemalloc/tcmalloc等分配内存大小的单位都是2、4、8、16、32、64等, 为了能容纳一个完整的embstr对象, jemalloc最少分配32个字节的空间, 如果字符串在稍微长一点,那就是64字节的空间。如果总体超出了64字节, Redis认为它是大字符串,不在使用embstr形式存储, 而该用raw形式。
当内存分配器分配了64空间时, 那么这个字符串的最大长度可以是多少呢? 这个长度就是44。那为什么是44呢?
前面提到SDS结构体中的content中的字符串是以字节\0结尾的字符串,之所以多出这样一个字节, 就是为了便于直接使用glibc的字符串处理函数, 以及为了便于子剧场的调试打印输出。
看上面的这张图可以算出, 留给content的长度最多只有45(64-19)字节了。字符串又是以\0结尾, 所以embstr最大能容纳的字符串长度就是44。
扩容策略
字符串在长度小于1M之前,扩容空间采用加倍策略, 也就是保留100%的冗余空间。当长度超过1M后, 为了避免加倍后的冗余空间过大而导致浪费, 每次扩容只会多分1M大小的冗余空间。