Redis有string、hash、list、set、zset几种数据结构,这些数据结构仅仅是面向使用者的上层结构,其底层的具体实现有多种。string的内部编码有三种:raw,embstr,int,其如图所示:
我们可以通过type
命令查看数据结构类型,object encoding
命令查看内部编码:
127.0.0.1:6379> type hello
string
127.0.0.1:6379> object encoding hello
"raw"
这里我们结合redis源码,来看一下这几种底层的数据结构是如何实现的。
redisObject
Redis的设计颇有“万物皆对象”的意思,无论是string,还是hash等其他的数据类型,皆为redisObject
,其结构体源码如下:
typedef struct redisObject {
// 类型
unsigned type:4;
// 编码
unsigned encoding:4;
// 对象最后一次被访问的时间
unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
// 引用计数
int refcount;
// 指向实际值的指针
void *ptr;
} robj;
当我们使用object encoding
命令时,访问的就是该结构体内的encoding
字段;ptr
字段就是指向实际数据的指针。
sds(Simple Dynamic String)
sds
对象是字符串的底层实现,raw
和embstr
类型均基于此实现:
struct sdshdr {
// buf 中已占用空间的长度
int len;
// buf 中剩余可用空间的长度,初始化为0
int free;
// 数据空间
char buf[];
};
len
字段用于保存字符串长度,所以用strlen
求字符串长度时,直接读取此值,时间复杂度为 O(1);free
字段指剩余可用的空间,新建字符串时该值为0;buf
则为储存字符串的数组。
raw与embstr
raw
是string的默认编码类型,embstr
与其类似,创建两者的函数如下:
// 创建一个 REDIS_ENCODING_RAW 编码的字符对象
robj *createRawStringObject(char *ptr, size_t len) {
return createObject(REDIS_STRING,sdsnewlen(ptr,len));
}
我们可以看到,创建raw 类型字符串时,首先创建 sds 对象(sdsnewlen
函数),然后将新建的redisObject 中的 ptr 字段指向该 sds 对象,共进行了两次内存分配,如图所示:
// 创建一个 REDIS_ENCODING_EMBSTR 编码的字符对象
robj *createEmbeddedStringObject(char *ptr, size_t len) {
robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr)+len+1);
struct sdshdr *sh = (void*)(o+1);
o->type = REDIS_STRING;
o->encoding = REDIS_ENCODING_EMBSTR;
o->ptr = sh+1;
o->refcount = 1;
o->lru = LRU_CLOCK();
sh->len = len;
if (ptr) {
memcpy(sh->buf,ptr,len);
sh->buf[len] = '\0';
} else {
memset(sh->buf,0,len+1);
}
return o;}
与 raw 类型类似,embstr 编码类型同样由 sds 对象和 redisObject 实现;但与 raw 类型不用的是,sds 和 redisObject 是同时分配的,在内存中是连续的,如图所示:
同时,embstr 是只读的。所谓只读是指,如果embstr 对象有修改,则会转变成 raw 类型。
那么什么时候选择 raw, 什么时候是 emstr 呢?
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 39
robj *createStringObject(const char *ptr, size_t len) {
if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)
return createEmbeddedStringObject(ptr,len);
else
return createRawStringObject(ptr,len);
}
OBJ_ENCODING_EMBSTR_SIZE_LIMIT
这个宏定义了限制长度为39(新版本为44),即长度小于39字节的字符串采用 embstr 编码,否则为 raw。我们可以把 embstr 看作为短字符串的优化。
int
Redis中整数有共享对象的概念,也就是驻留,通过REDIS_SHARED_INTEGERS
定义,值为10000:
#define REDIS_SHARED_INTEGERS 10000
当欲创建的整数值在0到10000之间时,为节约内存,并不会创建新的对象,而是增加一个引用,返回共享对象:
robj *createStringObjectFromLongLong(long long value) {
robj *o;
// 如果值符合范围,则不创建新对象,仅仅返回一个共享对象
if (value >= 0 && value < REDIS_SHARED _INTEGERS) {
incrRefCount(shared.integers[value]);
o = shared.integers[value];
}
这里我们在 redis 客户端可以用object refcount
命令查看一个对象的引用数:
127.0.0.1:6379> set number1 100
OK
127.0.0.1:6379> object refcount number1
(integer) 2
127.0.0.1:6379> set number2 100
OK
127.0.0.1:6379> object refcount number2
(integer) 3
如果整数值属于 long 类型范围,则创建一个 REDIS_ENCODING_INT 编码的字符串对象,否则用 REDIS_ENCODING_RAW 类型的字符串保存。
//值可以用 long 类型保存
if (value >= LONG_MIN && value <= LONG_MAX) {
o = createObject(REDIS_STRING, NULL);
o->encoding = REDIS_ENCODING_INT;
o->ptr = (void*)((long)value);
} else {
o = createObject(REDIS_STRING,sdsfromlonglong(value));
}
字符串的扩容
当对字符串进行修改时,如append
操作,可能会触发字符串的扩容机制。该机制的具体实现如下:
sds sdsMakeRoomFor(sds s, size_t addlen) {
struct sdshdr *sh, *newsh;
size_t free = sdsavail(s);// 获取 s 目前的空余空间长度,时间复杂度 O(1)
size_t len, newlen;
if (free >= addlen) return s; //剩余空间足够,无需扩容
len = sdslen(s);
sh = (void*) (s-(sizeof(struct sdshdr)));
newlen = (len+addlen); //新字符串的长度
if (newlen < SDS_MAX_PREALLOC) // SDS_MAX_PREALLOC的值为1024*1024,即 1Mb
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
// 分配内存,这里多出的 1 字节是为 "\0"准备的
newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);
// ...
}
可以看到,字符串的扩容的过程:
- 初次创建字符串时,剩余空间( free 字段)为0;
- 进行扩容时,如果新字符串小于1MB,则预分配两倍的容量;例如原有len=50,新增addlen=100,则新长度 newlen=300,最终使用情况为used(50+100)+free(150);
- 若新字符串大于等于1MB,则预分配1MB空间;如原有len=10MB,新增addlen=50,新长度newlen=10MB+50+1MB,最终使用情况为used(10MB+50)+free(1MB)。
字符串修改后,因预分配机制的存在,可能会有空余的空间未使用,这是为了多次修改时,减少内存的分配次数以及拷贝操作次数,但这同时也增加了内存消耗。我们在使用字符串时需要清楚这一点。