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;
}

image-20200903150454819

​ 如图所示, 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大小的冗余空间。

posted @ 2020-10-10 18:22  phper-liunian  阅读(140)  评论(0编辑  收藏  举报