Redis五大结构之字符串

209 阅读4分钟

SDS动态字符串

SDS简单动态字符串,是Redis的默认字符串,内部存储结构如下(buf不是指针,箭头仅代表buff数组存放的字符串):

比起C字符串,SDS具有的一下优点
1、常数复杂度获取字符串长度
2、杜绝缓冲区溢出
3、二进制安全
4、兼容部分C字符串函数
5、减少修改字符串时带来的内存重新分配次数
  1)空间预分配,当len < 1M,分配修改后总长度的二倍,若修过后>=1M,预留1M空间
  2)惰性空间释放,只是标记free,并不释放空间

字符串对象

对于Redis数据库保存的键值对来说,键总是一个字符串对象,而值则可以是字符串对象、列表对象、哈希对象、集合对象、有序集合对象的其中一种。字符串对象的编码可以是 int、raw、embstr,其中raw和embstr都是采用的SDS结构,区别在于embstr使用一块内存存储redisobject结构和SDS结构,而raw开辟两次内存,分别存储两个数据结构。

int编码

如果一个字符串对象保存的是整数值,并且这个整数值可以使用long类型存储,那么字符串对象会将value保存在redisObject结构的ptr属性里,并将字符串对象的编码设置为int。 举例说明,set num 123456,底层就是把ptr当成long类型存储字符串“123456”,截图如下:

int编码的字符串对象底层存储如下:

embstr编码

如果字符串对象保存的是一个字符串值,并且这个字符串值的长度小于等于44字节(redis_version:6.0.8),那么字符串对象使用embstr编码的方式来保存这个字符串值。embstr编码是专门用于保存短字符串的一种优化编码方式,通过调用一次内存分配函数来分配一块连续的空间,空间中依次包含redisObject和SDS两个结构,减少了分配释放内存次数,有效利用了cpu缓存。 举例说明:set msg 1.1,redis会把浮点数当成字符串存储,当运算时会强转类型,计算后,再次转成字符串存储,截图如下:

embstr编码的字符串对象底层存储如下:

raw编码

如果字符串对象保存的是一个字符串值,并且这个字符串大于44字节,那么字符串对象将使用SDS来保存这个字符串值,并将编码设置为raw。 举例说明 set msg 111111111111111111111111111111111111111111113,截图如下: raw编码的字符串对象底层存储如下: #### 编码自动转换

  1. 对应int编码的字符串对象,如果我们执行一些命令,使这个对象保存的不再是整数值,而是字符串值(浮点数也是字符串值),则自动转为raw
  2. 因为embstr编码是只读的,当我们对embstr编码的对象执行任何修改命令时,程序会将对象转为raw,然后再执行修改命令

Redis字符串常用命令

-------------------------------更新 2020.11.18----------------------------

Redis进一步对SDS优化

struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[]; //柔性数组,本身不占空间,它只是一个偏移量,必须在结构体的最后一个。sizeof(struct sdshdr5) = 1
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

packed 告知编译器不要优化,取消内存对齐的优化。当新建字符串对象时,根据字符串长度判断使用那种类型,主要为了进一步减少内存的使用,比如len、alloc在uint8_t占用两个字节,而在uint32_t需要占用8个字节,能表达的字符串长度越大,len和alloc占用的字节也越多,字符串对象作为使用频率最高的对象,这种优化也是很可观。判断创建字符串时选择哪种类型函数如下:

static inline char sdsReqType(size_t string_size) {
    if (string_size < 1<<5) // 2 的 5 次方等于32
        return SDS_TYPE_5;
    if (string_size < 1<<8) // 2 的 8 次方等于256,下面依次类推
        return SDS_TYPE_8;
    if (string_size < 1<<16)
        return SDS_TYPE_16;
#if (LONG_MAX == LLONG_MAX)
    if (string_size < 1ll<<32)
        return SDS_TYPE_32;
    return SDS_TYPE_64;
#else
    return SDS_TYPE_32;
#endif
} 

当新建字符串小于32字节,符合 struct sdshdr5时,flags 低三位保存SDS_TYPE_5类型,高五位记录initlen,比如保存“hello”,需要开辟sizeof(struct sdshdr5) + strlen("hello") + 1一共7个字节。