目录
Redis中String类型的三种编码
String类型在底层对应的有三种编码
int: 当value为long类型的整数值且长度小于等于20字节时
redis启动时会先建立10000个redisObject,值为0 - 9999的值,将这10000个redisObject作为共享对象。所以如果我们set的值在0 - 10000之间,则指向共享对象,不需要创建新的redisObject。
在redis源码的object.c文件中有一个tryObjectEncoding()函数,该函数的作用是尝试对字符串对象进行编码以节省空间。
在这个函数中有这样一段代码:
raw:当value为大于44字节的字符串时
embstr:当value为小于44字节的字符串时
为什么embstr 形式,可以存储最大字符串长度是44字节?
首先我们需要看一下如果存储一个String类型的value,至少需要占用多少的内存空间。
在之前的一篇文章中,我们提到,redis中的value其实是以RedisObject的方式存储的。
再来看一下RedisObject的定义和RedisObject中每个属性占用的内存空间:
typedef struct redisObject {
unsigned type:4; // 占用4bits, 表示具体的数据类型
unsigned encoding:4; // 占用4bits, 表示具体的编码方式
unsigned lru:LRU_BITS; // 占用24bits, 最近一次被访问的时间
int refcount; // 占用4bytes, 对象引用计数
void *ptr; // 占用8bytes, 指向真正的数据地址的指针
} robj;
我们看到,一个RedisObject至少需要占用 4bits + 4bits + 24bits + 4bytes + 8bytes = 16byte
其中ptr是指向真正数据的内存地址的指针。如果我们存储的是字符串类型的数据, 这个指针就指向定义的SDS的地址。
我们再来看一下,sds的定义。
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; // 占用1byte, 表示当前字符数组长度
uint8_t alloc; // 占用1byte, 当前字符数组总共分配的内存大小
unsigned char flags; // 占用1byte, 表示当前字符数组的sdshdr类型。用来标识到底是sdshdr8 还是sdshdr16等
char buf[]; // 占用nbyte, 字符串真正的值,占用空间根据值的长度决定
};
可以看出,一个sds类型的数据,至少需要占用 1byte + 1byte + 1byte = 3 byte 的空间,这是在字符串值为空的情况下。
到这里我们可以分析出,redis要存储一个字符串类型的value,至少占用16bytes + 3bytes = 19bytes空间 (redisObject + sds)
而内存分配器分配内存大小的单位都是2的n次方,即 2/4/8/16/32/64/…,而redis定义了超过64字节的就属于大字符串了。所以在64byte以内的非long类型字符串都会以embstr类型来存储。而超过了64字节的话,会用raw的形式来存储。
所以 64 - 19 = 45字节。这里留给真正存储字符串值的空间就是45字节。因为字符串都是以\0结尾的,再去除一个字节,就剩44字节了。
所以,到这里得出,embstr 形式,可以存储最大字符串长度是44字节。而超过了这个长度,会使用raw来保存。
为什么要将字符串分为不同的方式来进行存储?
这里需要注意的是,embstr和raw类型的存储方式也有一些差异。
embstr形式存储,RedisObject和SDS内存中空间是连续的,而raw类型,则不连续。
所以,Redis对字符串以多种编码方式进行存储,可以节省内存空间,减少内存碎片。对于<= 44字节的字符串,使用embstr类型存储确实是更好的方式。
什么情况下embstr会转为raw?
-
当字符串长度超过44字节时
-
当对原字符串调用APPEND 命令追加时,这时,即使字符串长度不超过44,也会转为raw类型来进行存储。