本文中展示的源码都是基于Redis7.0。
SDS定义
Redis 中的字符串类型使用的是 sds,也就是 “simple dynamic string”。sds 是 Redis 自己实现的一种字符串类型,它比 C 语言中的原始字符串类型 char[] 更为高效、安全,并提供了一些额外的功能。
sds 实际上是通过一个固定结构的 sdshdr 来实现的,其中 sdshdr 是 sds 的实际存储数据结构,包含字符串长度、可用空间、引用计数等信息。
相比较于 C 语言中的原始字符串类型,sds 在以下几个方面有很大的优势:
-
获取字符串长度的时间复杂度为 O(1),而原始字符串类型需要遍历整个字符串才能得到长度,时间复杂度为 O(N)。
-
避免了缓冲区溢出和内存泄漏等常见问题。
-
在内存不足时,采用类似于 C++ 中的 vector 的方式进行内存分配,避免了频繁的内存分配和释放操作。
-
提供了字符串的动态修改功能。
sds和sdshdr定义
src/sds.h
typedef char *sds;
/* Note: sdshdr5 is never used, we just access the flags byte directly.
* However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
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[];
};
通过源代码我们可以看到,sdshdr结构体定义了若干属性,具体属性说明如下:
len
:保存已使用的空间长度,即字符串的实际长度。在创建sds
时,len
被初始化为 0,然后在字符串追加、修改等操作中会动态增加和减少。alloc
:保存总分配的空间长度,包括实际保存字符串内容的空间和额外的空间,额外空间主要用于避免频繁调用realloc()
函数,提升字符串操作的效率。在创建sds
时,alloc
会根据实际需要的空间长度动态增加,一般情况下会比字符串实际长度大一些。flags
:标志位,用于储存类型信息和其它特殊信息,如是否可用realloc()
等,在不同的实现中,flags
可能保存的信息不同。buf
:实际保存字符串内容的字符数组,类型为char*
,长度未指定。
编码方式
sds字符串有两种编码方式,分别是embstr和raw。
内存图示
embstr编码的字符串"Python"如下图所示,可以看到redisObject
和sds
在内存中是连续的,这样是为了字符串较短的时候,采用embstr编码就可以只分配一次内存。
raw编码的字符串如下图所示,可以看到redisObject
和sds
在内存中是不连续的。
决定条件
当字符串大小大于44字节会使用raw编码,否则使用embstr编码。那么为什么是44呢?
redisObject的定义
src/server.h
#define LRU_BITS 24
struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */
int refcount;
void *ptr;
};
这是redisObject
结构体的定义,每个属性占用空间大小:
- type为4bits
- encoding为4bits
- lru为24bits
- refcount为4bytes
- ptr为8bytes(64位系统)
做一个加法
0.5
+
0.5
+
3
+
4
+
8
=
16
0.5+0.5+3+4+8=16
0.5+0.5+3+4+8=16,也就是redisObject
会占用16个字节,因为内存分配器 (如jemalloc、tcmalloc等) 使用的内存块大小通常是2的幂,也就是说分配给这个redis对象的字节数可能是32、64等。再算算sdshdr至少占用的空间,也就是sdshdr8
,可以很直观看到是3个字节,所以当使用embstr编码的时候,总共至少占用19个字节,如果分配空间的时候分配32,那么实际留给字符串的空间就只有12个字节(结尾\0占一个字节),这样可能太少了。所以作者就按照64字节分配,所以得到
64
−
19
−
1
=
44
64-19-1=44
64−19−1=44这个最终结果。
总结:embstr编码相比raw编码,少进行一次内存分配的操作,提升了效率。决定使用哪种编码的因素是字符串大小,这个阈值为44字节,这个值是根据redisObject
和sdshdr
占用空间算出来的。
空间分配
我们就通过源码调试来看下SDS到底是如何分配空间的(可以在这里配置调试源码Redis-调试源码)。
我们启动源码调试之前,先在这里打个断点,看下sz_index2size_tab
的值,方便后续理解如何分配空间给reids字符串的。
redis/deps/jemalloc/include/jemalloc/internal/sz.h
文件中sz_index2size_lookup
set字符串
1.打断点
createStringObject源码如下:
/* Create a string object with EMBSTR encoding if it is smaller than
* OBJ_ENCODING_EMBSTR_SIZE_LIMIT, otherwise the RAW encoding is
* used.
*
* The current limit of 44 is chosen so that the biggest string object
* we allocate as EMBSTR will still fit into the 64 byte arena of jemalloc. */
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
robj *createStringObject(const char *ptr, size_t len) {
if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)
return createEmbeddedStringObject(ptr,len);
else
return createRawStringObject(ptr,len);
}
2.输入命令
输入命令set name python
,在WATCH中输入c->argv[0]->ptr
、c->argv[1]->ptr
、c->argv[2]->ptr
这三个值,当代码卡在断点之后,我们Continue
三次之后,可以看到结果:
没错就是我们输入的命令,实际上到了这里,redis服务端已经将我们输入的命令解析为了三个sds字符串,接下来我们卡看字符串"python"的具体属性。
3.查看len和alloc
直接输入sdslen(c->argv[2]->ptr)
和sdsalloc(c->argv[2]->ptr)
可以看看到sds的字符串"python"的len
和alloc
都为6
4.查看编码
此时编码格式是embstr
,与我们料想的一致。
encoding为8就是对应源码中的OBJ_ENCODING_EMBSTR
src/object.c
/* Objects encoding. Some kind of objects like Strings and Hashes can be
* internally represented in multiple ways. The 'encoding' field of the object
* is set to one of this fields for this object. */
#define OBJ_ENCODING_RAW 0 /* Raw representation */
#define OBJ_ENCODING_INT 1 /* Encoded as integer */
#define OBJ_ENCODING_HT 2 /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3 /* No longer used: old hash encoding. */
#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
#define OBJ_ENCODING_ZIPLIST 5 /* No longer used: old list/hash/zset encoding. */
#define OBJ_ENCODING_INTSET 6 /* Encoded as intset */
#define OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist */
#define OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of listpacks */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */
#define OBJ_ENCODING_LISTPACK 11 /* Encoded as a listpack */
char *strEncoding(int encoding) {
switch(encoding) {
case OBJ_ENCODING_RAW: return "raw";
case OBJ_ENCODING_INT: return "int";
case OBJ_ENCODING_HT: return "hashtable";
case OBJ_ENCODING_QUICKLIST: return "quicklist";
case OBJ_ENCODING_LISTPACK: return "listpack";
case OBJ_ENCODING_INTSET: return "intset";
case OBJ_ENCODING_SKIPLIST: return "skiplist";
case OBJ_ENCODING_EMBSTR: return "embstr";
case OBJ_ENCODING_STREAM: return "stream";
default: return "unknown";
}
}
5.设置raw字符串
如果设置编码类型为raw的SDS字符串呢?
再来一次上述步骤,不同的是这次我们设置一个长度超过44的字符串set longstr a_value_gt_44_a_value_gt_44_a_value_gt_44_a_value_gt_44
,同样查看属性:
- len: 55
- alloc: 60
通过分析源码_sdsnewlen
,如上图所示,这个sds字符产会占用hdrlen+initlen+1
长度也就是
3
+
55
+
1
=
59
3+55+1=59
3+55+1=59字节的空间,此时jemalloc会分配出64bytes的空间。其中再减去sdshdr和’\0’的4bytes,也就是
64
−
4
=
60
64-4=60
64−4=60得到alloc为60。
append字符串
1.打断点
我们找到appendCommand
方法,在里面打个断点。
然后分别执行命令:
127.0.0.1:6379> set a python
OK
127.0.0.1:6379> append a " is the best language"
可以看到终端命令卡住,应该是代码执行到了断点处,接下来就单步调试看一下具体情况。
2.单步执行
可以根据下图,最终执行到_sdsMakeRoomFor
方法,接下来我们来看看这个方法。
3._sdsMakeRoomFor
源码:
sds _sdsMakeRoomFor(sds s, size_t addlen, int greedy) {
void *sh, *newsh;
size_t avail = sdsavail(s);
size_t len, newlen, reqlen;
char type, oldtype = s[-1] & SDS_TYPE_MASK;
int hdrlen;
size_t usable;
/* Return ASAP if there is enough space left. */
if (avail >= addlen) return s;
len = sdslen(s);
sh = (char*)s-sdsHdrSize(oldtype);
reqlen = newlen = (len+addlen);
assert(newlen > len); /* Catch size_t overflow */
if (greedy == 1) {
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
}
type = sdsReqType(newlen);
/* Don't use type 5: the user is appending to the string and type 5 is
* not able to remember empty space, so sdsMakeRoomFor() must be called
* at every appending operation. */
if (type == SDS_TYPE_5) type = SDS_TYPE_8;
hdrlen = sdsHdrSize(type);
assert(hdrlen + newlen + 1 > reqlen); /* Catch size_t overflow */
if (oldtype==type) {
newsh = s_realloc_usable(sh, hdrlen+newlen+1, &usable);
if (newsh == NULL) return NULL;
s = (char*)newsh+hdrlen;
} else {
/* Since the header size changes, need to move the string forward,
* and can't use realloc */
newsh = s_malloc_usable(hdrlen+newlen+1, &usable);
if (newsh == NULL) return NULL;
memcpy((char*)newsh+hdrlen, s, len+1);
s_free(sh);
s = (char*)newsh+hdrlen;
s[-1] = type;
sdssetlen(s, len);
}
usable = usable-hdrlen-1;
if (usable > sdsTypeMaxSize(type))
usable = sdsTypeMaxSize(type);
sdssetalloc(s, usable);
return s;
}
代码就不详细说明了,只说下关键点:
-
如果原SDS剩余空间足够,则直接返回,不需要重新申请空间。
代码:if (avail >= addlen) return s;
-
如果新字符串长度大小于1M,则新长度再乘2,否则新长度在原基础上加1M。
代码:if (greedy == 1) { if (newlen < SDS_MAX_PREALLOC) newlen *= 2; else newlen += SDS_MAX_PREALLOC; }
原SDS字符串“python”长度为6,增加的“ is the best language”长度为21,则
newlen
为27,27小于1M,所以newlen
为54(本例中greedy恒为1)。 -
申请空间
代码:newsh = s_malloc_usable(hdrlen+newlen+1, &usable);
hdrlen为3,newlen为54,所以会申请 54 + 3 + 1 = 58 54+3+1=58 54+3+1=58字节的空间,根据
sz_index2size_tab
实际会得到64字节的空间,接着usable = usable-hdrlen-1;
得到usable
为60,这里的usable
其实就是新字符串的alloc
属性。
4.属性查看
可以看到,与我们料想的一致,len为27,alloc为60。