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 字符则
- 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,