redis数据类型-Strings

96 阅读7分钟

redis数据类型-Strings

参考链接

redis中的字符串使用的 简单动态字符串(simple dynamic string,SDS),是一种,动态,高效,存储安全的 结构。

存储结构

3.0.0

struct sdshdr {
    unsigned int len;
    unsigned int free;
    char buf[];
};
  • len

字符数组的元素的个数,不包括末尾的\0

  • free

表示sds未使用的空间

  • buf

存放内容的bug数组,最后一位为 '\0'

5.0.8

struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */  // 使用1字节
    unsigned char flags; /* 3 lsb of type, 5 unused bits */ // 使用1字节,低3位存储类型,  高5位保留使用
    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[];
};
/* ..... */
  • len

字符数组的元素的个数,不包括末尾的\0

  • alloc

字符数组分配空间的长度,不包括末尾的\0

  • flags

sds的数据类型标示,占用一个字节,仅用低3位标示sds的5数据类型

  • buf

存放内容的buf数组

扩容(空间预分配)

已字符串的追加为例,来分析 sds的扩容机制

// 将t中len长度字符数据 追加到 s 后面
sds sdscatlen(sds s, const void *t, size_t len) {
  	// 获取 添加模板字符串长度
    size_t curlen = sdslen(s);
		// 计算扩容
    s = sdsMakeRoomFor(s,len);
    if (s == NULL) return NULL;
  	// 数据的copy
    memcpy(s+curlen, t, len);
  	// 从新设置 长度为 s的长度+ copy内容t的len
    sdssetlen(s, curlen+len);
    // 设置 \0
    s[curlen+len] = '\0';
    return s;
}

// addlen 是要添加的数据内容长度
// #define SDS_MAX_PREALLOC (1024*1024)
sds sdsMakeRoomFor(sds s, size_t addlen) {
		/*......*/
    len = sdslen(s);
    newlen = (len+addlen);
  	// 如果新长度 < 1M则,2倍扩容,否则 直接扩充1M
    if (newlen < SDS_MAX_PREALLOC)
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC;
  
		/*......*/
	  sdssetalloc(s, newlen);
    return s;
}
  • 扩容策略:

如果sds的修改后长度 < 1M,

则 sds->alloc =(sdsLen+addLen) * 2, sds->buf = (sdsLen+addLen) * 2 + 1byte,

则未使用的空间和修改后的长度一致,

如果sds的修改后长度 >=1M,

则 sds->alloc = (sdsLen+addLen)+1M , sds->buf = (sdsLen+addLen)+1M + 1byte

则未使用的空间为1MB

未使用空间计算= sds->alloc - sds->len

释放(惰性空间释放)

从这个方法来看,sdstrim,是移除 s 左右两边cset所有出现的字符。

移除出现的字符后,并没有释放空间,

sds sdstrim(sds s, const char *cset) {
    char *start, *end, *sp, *ep;
    size_t len;

    sp = start = s;
    ep = end = s+sdslen(s)-1;
    while(sp <= end && strchr(cset, *sp)) sp++;
    while(ep > sp && strchr(cset, *ep)) ep--;
    len = (sp > ep) ? 0 : ((ep-sp)+1);
    if (s != sp) memmove(s, sp, len);
    s[len] = '\0';
    sdssetlen(s,len);
    return s;
}

// 惰性释放,只是将s[0]='\0'标记是最后的,
void hi_sdsclear(hisds s) {
    hi_sdssetlen(s, 0);
    s[0] = '\0';
}

sds和c字符串区别

  • 常数复杂度获取字符串长度O(1)

sds:sds内部维护了一个len属性

c字符串:需要循环遍历O(n)

  • 杜绝缓存区溢出

sds:每次在做修改的时候内部都会判断是否要自动扩容,

c字符串:c字符串不记录自身的长度,每次都需要自己分配好充足的空间,否则会吧其他数据覆盖掉,

  • 修改字符减少内存分配次数

1)、空间的预分配,

2)、惰性空间释放,

  • 二进制安全

sds:是判断len来决定是否读完的。

c字符串:是以 ‘\0’来表示该程序已到末尾,则可能会被程序误读,(比如我程序里面本来就有 \0,但是他并不表示则程序的结尾)

  • 兼容部分c字符串函数
C 字符串SDS
获取字符串长度的复杂度为 O(N) 。获取字符串长度的复杂度为 O(1) 。
API 是不安全的,可能会造成缓冲区溢出。API 是安全的,不会造成缓冲区溢出。
修改字符串长度 N 次必然需要执行 N 次内存重分配。修改字符串长度 N 次最多需要执行 N 次内存重分配。
只能保存文本数据。可以保存文本或者二进制数据。
可以使用所有 <string.h> 库中的函数。可以使用一部分 <string.h> 库中的函数。

Q&A

sds的新版本的数据结构做了那些优化?

// 3.0.0
struct sdshdr {
    unsigned int len;
    unsigned int free;
    char buf[];
};


// 5.08
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */  // 使用1字节
    unsigned char flags; /* 3 lsb of type, 5 unused bits */ // 使用1字节,低3位存储类型,  高5位保留使用
    char buf[]; 
};

不同长度的字符串他们占用的 头部是相同的,可能会造成不必要的空间的浪费。

  • 0x001、对结构头部存储字节的优化

v300,sdshdr的 len 和 free 固定长度 4字节,

v508,使用sdsdr8的 len 和 alloc 各占用1 个字节,可以根据 字符长度进行类型动态分配。

如何存储一个 say 字符则

image.png

  • 0x002、字段结构的内存对齐方式:

V300,使用默认的内存对齐方式;V508,则使用紧凑型内存对齐方式

比如:

Case1:

#include <stdio.h>
int main() { 
  
  struct s1 { 
    char a;  // 占用1 个byte
    int b; // 占用 4个byte
  } ts1; 
  
  // 默认情况下编译器会给 ts1 分配 8个字节,实际上只有5个,则浪费了3个,打印 8 
  printf("%lu\n", sizeof(ts1)); return 0; 
}

Case2:紧凑性布局

#include <stdio.h>
int main() { 
  
  struct __attribute__((packed)) s1 { 
    char a;  // 占用1 个byte
    int b; // 占用 4个byte
  } ts1; 
  
  // 则 编译器则会使用 紧凑性内存布局分配,打印 5
  printf("%lu\n", sizeof(ts1)); return 0; 
}

V508 的sds使用 __attrbute__((packed)) 方式来让结构字段布局更为紧凑。

sds是如何区分使用那种sds数据类型的?

unsigned char flags = s[-1];

flags && SDS_TYPE_MASK // 使用1字节,低3位存储类型, 高5位保留使用

#define SDS_TYPE_5  0
#define SDS_TYPE_8  1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4
#define SDS_TYPE_MASK 7

static inline size_t sdslen(const sds s) {
  
    unsigned char flags = s[-1];
  
  	switch(flags&SDS_TYPE_MASK) {
          	/* ... */
    }

    return 0;
}

这个又说明了,sds的指针以上来其实不是指向 len位置的,而是 执行buf位置的。

创建一个sds的数据结构,并且 指针位置指向 sds的 buf位置。

sds sdsnewlen(const void *init, size_t initlen) {
    void *sh;
    sds s;
    char type = sdsReqType(initlen);
    int hdrlen = sdsHdrSize(type);
    unsigned char *fp; /* flags pointer. */
		
  	// 头部的长度 + buf的初始化长度 + '\0' 以上来,指针指向该结构体初始地址
    sh = s_malloc(hdrlen+initlen+1);
  
    /* .... */
  

    s = (char*)sh+hdrlen;   	  // 此时sh的指针指向 buf
    fp = ((unsigned char*)s)-1; // 后退一步,则指向 flag为
  
    /* .... */
  
    s[initlen] = '\0';
    return s;
    
}    

不进行对齐填充,则只需要 后退一步,就可以获取 类型信息,若要对齐填充了,在不同系统中不知道要退多少步,才可以获得flags,

unit8_t是什么

可以理解为是类型的别名

#if (_MSC_VER < 1300)
   typedef signed char       int8_t;
   typedef signed short      int16_t;
   typedef signed int        int32_t;
   typedef unsigned char     uint8_t;
   typedef unsigned short    uint16_t;
   typedef unsigned int      uint32_t;
#else
   typedef signed __int8     int8_t;
   typedef signed __int16    int16_t;
   typedef signed __int32    int32_t;
   typedef unsigned __int8   uint8_t;
   typedef unsigned __int16  uint16_t;
   typedef unsigned __int32  uint32_t;
#endif
typedef signed __int64       int64_t;
typedef unsigned __int64     uint64_t;


redis的字符串的最大字符串容量是多大。?

Values can be strings (including binary data) of every kind, for instance you can store a jpeg image inside a value. A value can't be bigger than 512 MB.

sds如何释放无用空间

简单理解就是 将 无用的空间释放掉后,再将 len的长度 等于 alloc,