文章目录
简介
String 是最基本的 key-value 结构,key 是唯一标识,value 是具体的值,value其实不仅是字符串, 也可以是数字(整数或浮点数),value 最多可以容纳的数据长度是 512M。因为单个参数大于512M时redis会直接报错。
ok = string2ll(c->querybuf+pos+1,newline-(c->querybuf+pos+1),&ll);
if (!ok || ll < 0 || ll > 512*1024*1024) {
addReplyError(c,"Protocol error: invalid bulk length");
setProtocolError(c,pos);
return REDIS_ERR;
}
内部实现
int
String 类型的底层的数据结构实现主要是 int (实际上大小为long long) 和 SDS(简单动态字符串)。
字符串对象的内部编码(encoding)有 3 种 :int、raw和 embstr。
如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面(将void*转换成 long),并将字符串对象的编码设置为int。
在插入时会调用tryObjectEncoding尝试对val进行编码
// 对字符串进行检查
// 只对长度小于或等于 21 字节,并且可以被解释为整数的字符串进行编码
len = sdslen(s);
if (len <= 21 && string2l(s,len,&value)) {
/* This object is encodable as a long. Try to use a shared object.
* Note that we avoid using shared integers when maxmemory is used
* because every object needs to have a private LRU field for the LRU
* algorithm to work well. */
if (server.maxmemory == 0 &&
value >= 0 &&
value < REDIS_SHARED_INTEGERS)
{
decrRefCount(o);
incrRefCount(shared.integers[value]);
return shared.integers[value];
} else {
if (o->encoding == REDIS_ENCODING_RAW) sdsfree(o->ptr);
o->encoding = REDIS_ENCODING_INT;
o->ptr = (void*) value;
return o;
}
}
SDS
Redis没有适用传统农的C语言的字符串表示(以空字符结尾的字符数组),而是自己设计了一个名为简单动态字符串(SDS)的的抽象类型。
在Redis中,C字符串只会作为字符串字面量,用在一些无须对字符串值进行修改的地方,比如打印日志。
在Redis中,包含字符串的键值对在底层都是由SDS实现的。
SDS (Simple Dynamic Strings) 是redis 用于存储字符串的数据结构,有以下几个特性:
- 常数复杂度获取字符串的长度
- 缓冲区溢出问题
- 二进制安全
- 兼容C语言标准字符串处理函数
- 减少修改字符串时带来的内存重分配操作
定义
/*
* 保存字符串对象的结构
*/
struct sdshdr {
// buf 中已占用空间的长度
int len;
// buf 中剩余可用空间的长度
int free;
// 数据空间
char buf[];
};
SDS的优点
常数复杂度获取字符串的长度
如果使用C字符串要获取字符串长度,需要遍历整个字符串,时间复杂度O(n);但是使用SDS的话直接返回 len属性的值,时间复杂度O(1)
缓冲区溢出问题
使用C字符串如果小心,很容易造成缓冲区溢出,如:字符串拼接函数 strcat
char *strcat(char *dest, const char *src);
在执行这个函数的时候,需要保证dest已经分配了足够的空间,用来容纳src的字符串的所有内容,但是这个保证是由开发者来保证的,开发者一不小心就很容易造成缓冲区溢出,让新的字符串溢出到其他内存空空间,c语言是没有越界检测的,如果其他内存空间存的正是程序运行的其他值,就会出现大问题。
而SDS完全没有这个问题,当SDS需要进行修改时,会先检查SDS的空间是否满足修改所需的要求,如果不满足的话,会自动将空间扩容至修改后所需的大小,然后才执行修改操作。
二进制安全
C语言中用’\0’表示字符串结尾,如果字符串内容含有’\0’,就会提前截断(非二进制安全),redis 使用一个变量存放字符串长度,另一个字符柔性数组存放字符串实际内容,读取时根据len变量确定长度,而不是用’\0’表示结尾,不会出现截断。
兼容C语言标准字符串处理函数
SDS 对外暴露buf的指针,而不是结构体的起始地址,上层可以类似处理C语言字符串一样处理SDS字符串。
结构体使用了柔性数组,这种数组是可伸缩的,之所以使用柔性数组是因为数组地址和结构体地址是连续的,如图:
减少修改字符串时带来的内存重分配操作
因为C字符串不记录自身的长度,每次对增长或者缩短一个C字符串时,程序总要对这个保存C字符串的数组进行一次内存重分配操作
- 如果是增长字符串的操作,比如拼接操作,在执行这个操作之前,程序需要先通过内存重分配来扩展底层数组的的大小—如果忘了这一步就会产生缓冲区溢出;
- 如果是缩短字符串,比如截断操作,在执行这个操作之后,需要通过内存重分配来释放不再使用的那部分内存空间—如果忘了这一步就会产生内存泄漏。
因为内存分配涉及复杂的算法,并且可能需要执行系统调用,所以通常是一个耗时的操作:
- 在一般的程序中,如果修改字符串的情况不是很多,那么每次修改字符串都进行一次内存重分配是可以接受的;
- 但是Redis作为一个内存数据库,经常被用于速度要求严苛、数据被频繁修改的场景,如果每次修改都进行一次内存重分配,是会严重影响性能的。
SDS使用未使用空间来解决这个问题,SDS实现了空间预分配和惰性空间释放两种优化策略。
1) 空间预分配
空间预分配用于优化SDS的增长操作,当对一个SDS进行修改并且需要进行空间扩展时,程序不仅会为SDS分配修改所必须的空间,还会分配额外的未使用的空间。其额外未使用的空间数量由以下公式决定:
- 如果SDS进行修改后,len属性的指小于1MB,那么会分配和len属性同样大小的未使用空间。
- 如果SDS进行修改后,len属性的指大于等于1MB,那么将会分配1MB的未使用空间。
通过空间预分配策略,Redis可以减少连续字符串的增长操作所需的内存重分配次数。
sds sdsMakeRoomFor(sds s, size_t addlen) {
struct sdshdr *sh, *newsh;
// 获取 s 目前的空余空间长度
size_t free = sdsavail(s);
size_t len, newlen;
// s 目前的空余空间已经足够,无须再进行扩展,直接返回
if (free >= addlen) return s;
// 获取 s 目前已占用空间的长度
len = sdslen(s);
sh = (void*) (s-(sizeof(struct sdshdr)));
// s 最少需要的长度
newlen = (len+addlen);
// 根据新长度,为 s 分配新空间所需的大小
if (newlen < SDS_MAX_PREALLOC) //1024*1024
// 如果新长度小于 SDS_MAX_PREALLOC
// 那么为它分配两倍于所需长度的空间
newlen *= 2;
else
// 否则,分配长度为目前长度加上 SDS_MAX_PREALLOC
newlen += SDS_MAX_PREALLOC; //1024*1024
// T = O(N)
newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);
// 内存不足,分配失败,返回
if (newsh == NULL) return NULL;
// 更新 sds 的空余长度
newsh->free = newlen - len;
// 返回 sds
return newsh->buf;
}
2) 惰性空间释放
惰性空间释放由于优化SDS的缩短操作:当需要缩短SDS保存的字符串时,并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录下来,等待将来使用。
实现
embstr
如果字符串对象保存的是一个字符串,并且这个字符申的长度小于等于 39 字节(redis 3.0版本),那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串,并将对象的编码设置为embstr, embstr编码是专门用于保存短字符串的一种优化编码方式:
// 尝试将 RAW 编码的字符串编码为 EMBSTR 编码
if (len <= REDIS_ENCODING_EMBSTR_SIZE_LIMIT) { //将长度小于等于39字节的sds编码为embstr
robj *emb;
if (o->encoding == REDIS_ENCODING_EMBSTR) return o;
emb = createEmbeddedStringObject(s,sdslen(s));
decrRefCount(o);
return emb;
}
raw
如果字符串对象保存的是一个字符串,并且这个字符串的长度大于 39 字节(redis 3.0版本),那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串,并将对象的编码设置为raw:
注意,embstr 编码和 raw 编码的边界在 redis 不同版本中是不一样的:
- redis 2.+ 是 32 字节
- redis 3.0-4.0 是 39 字节
- redis 5.0 是 44 字节
为什么会不一样呢?
根本原因是为了配合cpu的缓存行为64字节,由于各个版本的sds实现有所差别:
在redis3.0中,39=64-0.5(type)-0.5(encoding)-3(lru)-refcount(4)-
*ptr(8)-SDS(len(4)+free(4)+buf中最后的’\0’(1))
可以看到embstr和raw编码都会使用SDS来保存值,但不同之处在于embstr会通过一次内存分配函数来分配一块连续的内存空间来保存redisObject和SDS,而raw编码会通过调用两次内存分配函数来分别分配两块空间来保存redisObject和SDS。
Redis这样做会有很多好处:
- embstr编码将创建字符串对象所需的内存分配次数从 raw 编码的两次降低为一次;
- 释放 embstr编码的字符串对象同样只需要调用一次内存释放函数;
- 因为embstr编码的字符串对象的所有数据都保存在一块连续的内存里面可以更好的利用 CPU 缓存提升性能。
但是 embstr 也有缺点的:
- 如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间,所以embstr编码的字符串对象实际上是只读的,redis没有为embstr编码的字符串对象编写任何相应的修改程序。当我们对embstr编码的字符串对象执行任何修改命令(例如append)时,程序会先将对象的编码从embstr转换成raw,然后再执行修改命令。