Redis字符串存储实现原理
- Redis 中的字符串是可以修改的字符串,在内存中他是以字节数组的形式存在的。我们在入门语言C语言里面的字符串标准形式是以NULL(即0x\0)作为结束符,但是Redis里面,字符串表示方法不是这样,因为,要获取以null结尾的字符串需要遍历整个字符串,时间复杂度是O(n),对应单线程对外服务的Redis来说是无法承受的。
- Redis的字符串结构叫做SDS,Simple Dynamic String。他的结构是一个带上长度信息的字节数组,类似C语言中的结构体。
struct SDS<T>{T capacity; //数组容量T len; //已有数据长度byte flags; //特殊标志位byte[] content; //数组主体内容
}
- Redis中SDS存储结构的设计类似于ArrayList机构,因为Redis允许字符串的修改,因此初始申请可以有一部分的冗余空间。
- capacity 标识所有分配数组的长度,包括未存储数据的部分空间
- len标识字符串的实际长度
- 当冗余的空间不够时候,先扩容,在复制旧的内容,然后在添加新内容,如果字符串长度非常长,内存的分配和复制开销会特别大。
- 以上结构体中,使用的泛型T,其中Capacity和len的类型是T,因为Redis对存储的压缩优化
- 当存储字符串比较短的时候,了你和capacity可以使用byte和short来表示,
- Redis规定字符串长度不超过512M,创建字符串时候len和capacity一样长,不会多分配冗余空间,这是因为绝大多数场景下我们不会去修改字符串。
embstr OR raw
- Redis字符串有两种存储方式,在长度短的时候,使用embstr形式存储,长度超过44 字节时候,使用raw形式存储,如下实验:
新docker-redis:0>set codehole aaaaaaaabbbjjbjbdjjskjkjsdeuiopoiioioioioioi
"OK"
新docker-redis:0>debug object codehole
"Value at:0x7f0a3b3c22c0 refcount:1 encoding:embstr serializedlength:41 lru:12955894 lru_seconds_idle:8"
新docker-redis:0>set codehole aaaaaaaabbbjjbjbdjjskjkjsdeuiopoiioioioioioi1111
"OK"
新docker-redis:0>debug object codehole
"Value at:0x7f0a3d6d66a0 refcount:1 encoding:raw serializedlength:43 lru:12955928 lru_seconds_idle:2"
-
一下我们通过分析Redis字符串对象存储结构来说明两个问题
- 问题一:为什么是44个字节作为界限
- 问题二:embstr 和raw存储的区别
-
Redis对象存储都会有一个头部结构,如下形式
struct RedisObject{int4 type; //4bitint4 encoding; //4bitint24 lru; //24bit 3byteint32 refcount; //32bit 4bytevoid *ptr; //8byte, 64bit system
} robj;
-
不同的对象具有不同的type 类型(4bit)。
-
同一个类型的type也会有不同的存储方式encoding(4bit)。
-
为了记录对象的lru信息,使用了24bit来记录lru信息
-
每个对象都有一个引用计数,refcount,当他归零时候,对象不被任何地方使用,对象将被销毁,内存被回收
-
ptr指针结构将指向对象的具体存储位置(body)
-
以上所有的综合一起 4bit+ 4bit+ 24bit + 32 bit + 64bit = 128bit = 16byte,所有Redis对象的对象头结构都需要占据16字节存储空间。
-
接着我们在分享SDS结构体大小,在字符串比较小的时候,SDS对象头结构的大小如下:
struct SDS{int8 capacity; //1byteint8 len; //1byteint8 flags; //1bytebyte[] content; //存储数据的数组,长度capacity
}
-
如上结构中 capacity ,len, flags 三个都占用1byte的内存,其他的就是 capacity长度的数组,用来存储具体数据。也就是最少也要3 个字节的存储空间。加上上面的16byte,我们一个没有存储字符串的Redis字符串对象,已经有19 byte的空间被系统各种属性占用。
-
我们在内存分配的时候,使用jemalloc, tcmalloc等分配内存大小的单元都是2/4/4/8/16/32/64 byte,
-
为了容纳完整的embstr对象,jemalloc最少分配32byte空间,如果字符串稍微长点,那就是64byte,如果字符串超过64byte,Redis会认为是一个大字符串,不在适合emdstr存储的形式,而使用raw形式
-
我们用最大内存空间64 来计算最大字符串长度, 64 - 19 = 45 ,但是之前实验得到的是44
-
SDS结构中content中字符串是以null结尾,多出这个字节,便于直接使用glbc的字符串处理函数,以及便于字符串的调试打印输出。最终得出了44 的长度。如下图:
-
问题二中embstr存储形式与 raw的存储形式如下
- embst存储将RedisObject对象头结构和SDS对象连续存储在一起,使用malloc方法一次性分配内存
- raw存储形式不一样,他需要两次malloc方法,两个对象头在内存地址上不连续通过对象头中 ptr指针来寻址存储位置。
扩容策略
- 字符串的扩容两种方式:
- 字符串长度在1MB之前,扩容空间都是加倍扩容,也就是保留100%的冗余空间
- 字符串长度超过1MB后,避免加倍后冗余空间浪费过多,每次只多分配1MB大小的冗余空间。
上一篇:Redis服务信息–Info指令