此文章已于 9:36:45 2020/10/15 发布到将军上座
2.1 数据结构
Redis 3.2之前的SDS
struct sds {
int len;// buf 中已占用字节数
int free;// buf 中剩余可用字节数
char buf[];// 数据空间
};
sdshdr5结构
在Redis5.0中,我们用如下结构来存储长度小于32的短字符串:
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 低3位存储类型, 高5位存储长度 */
char buf[]; /*柔性数组,存放实际内容*/
};
sdshdr5结构(图2-2)中,flags占1个字节,其低3位(bit)表示type,高5位(bit)表示长度,能表示的长度区间为0~31(2 5 -1),flags后面就是字符串的内容。
而长度大于31的字符串,1个字节依然存不下。我们按之前的思路,将len和free单独存放。sdshdr8、sdshdr16、sdshdr32和sdshdr64的结构相同,sdshdr16结构如图2-3所示。
其中"表头"共占用了S[2(len)+2(alloc)+1(flags)]个字节。flags的内容与sdshdr5类似,依然采用3位存储类型,但剩余5位不存储长度。
在Redis的源代码中,对类型的宏定义如下:
#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
在Redis 5.0中,sdshdr8、sdshdr16、sdshdr32和sdshdr64的数据结构如下:
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* 已使用长度,用1字节存储 */
uint8_t alloc; /*总长度,用1字节存储 */
unsigned char flags; /* 低3位存储类型, 高5位预留 */
char buf[]; /*柔性数组,存放实际内容*/
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* 已使用长度,用2字节存储 */
uint16_t alloc; /* 总长度,用2字节存储r */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* 已使用长度,用4字节存储 */
uint32_t alloc; /* 总长度,用4字节存储 */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /*已使用长度,用8字节存储 */
uint64_t alloc; /* 总长度,用8字节存储 */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
可以看到,这4种结构的成员变量类似,唯一的区别是len和alloc的类型不同。
结构体中4个字段的具体含义分别如下。
1)len :表示buf中已占用字节数。
2)alloc :表示buf中已分配字节数,不同于free,记录的是为buf分配的总长度。
3)flags :标识当前结构体的类型,低3位用作标识位,高5位预留。
4)buf :柔性数组,真正存储字符串的数据空间。
结构最后的buf依然是柔性数组,通过对数组指针作"减一"操作,能方便地定位到flags。
源码中的__attribute__((__packed__)) 需要重点关注。一般情况下,结构体会按其所有变量大小的最小公倍数做字节对齐,而用packed修饰后,结构体则变为按1字节对齐。
以sdshdr32为例,修饰前按4字节对齐大小为12(4×3)字节;修饰后按1字节对齐,注意buf是个char类型的柔性数组,地址连续,始终在flags之后。packed修饰前后示意如图2-4所示。
这样做有以下两个好处。
- 节省内存,例如sdshdr32可节省3个字节(12-9)。
2) SDS返回给上层的,不是结构体首地址,而是指向内容的buf指针。因为此时按1字节对齐,故SDS创建成功后,无论是sdshdr8、sdshdr16还是sdshdr32,都能通过(char*)sh+hdrlen得到buf指针地址(其中hdrlen是结构体长度,通过sizeof计算得到)。修饰后,无论是sdshdr8、sdshdr16还是sdshdr32,都能通过buf[-1]找到flags,因为此时按1字节对齐。若没有packed的修饰,还需要对不同结构进行处理,实现更复杂。
2.2 基本操作
本节着重介绍创建、释放、拼接字符串的相关API.
2.2.1 创建字符串
Redis通过sdsnewlen函数创建SDS。在函数中会根据字符串长度选择合适的类型,初始化完相应的统计值后,返回指向字符串内容的指针,根据字符串长度选择不同的类型:
sds sdsnewlen(const void *init, size_t initlen) { void *sh; sds s; char type = sdsReqType(initlen); // 根据字符串长度选择不同的类型 /* Empty strings are usually created in order to append. Use type 8 * since type 5 is not good at this. */ if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8; int hdrlen = sdsHdrSize(type); // 计算不同头部所需的长度 unsigned char *fp; /* flags pointer. 指向flags的指针 */ sh = s_malloc(hdrlen+initlen+1); //分配空间, "+1"是为了结束符' ' if (sh == NULL) return NULL; if (init==SDS_NOINIT) init = NULL; else if (!init) memset(sh, 0, hdrlen+initlen+1); s = (char*)sh+hdrlen; // s是指向buf的指针 fp = ((unsigned char*)s)-1; // s是柔性数组buf的指针,-1即指向flags switch(type) { case SDS_TYPE_5: { *fp = type | (initlen << SDS_TYPE_BITS); break; } case SDS_TYPE_8: { SDS_HDR_VAR(8,s); sh->len = initlen; sh->alloc = initlen; *fp = type; break; } case SDS_TYPE_16: { SDS_HDR_VAR(16,s); sh->len = initlen; sh->alloc = initlen; *fp = type; break; } case SDS_TYPE_32: { SDS_HDR_VAR(32,s); sh->len = initlen; sh->alloc = initlen; *fp = type; break; } case SDS_TYPE_64: { SDS_HDR_VAR(64,s); sh->len = initlen; sh->alloc = initlen; *fp = type; break; } } if (initlen && init) memcpy(s, init, initlen); s[initlen] = '