结构体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| typedef char *sds;
struct __attribute__ ((__packed__)) sdshdr5 { unsigned char flags; char buf[]; }; struct __attribute__ ((__packed__)) sdshdr8 { uint8_t len; uint8_t alloc; unsigned char flags; char buf[]; }; struct __attribute__ ((__packed__)) sdshdr16 { uint16_t len; uint16_t alloc; unsigned char flags; char buf[]; }; struct __attribute__ ((__packed__)) sdshdr32 { uint32_t len; uint32_t alloc; unsigned char flags; char buf[]; }; struct __attribute__ ((__packed__)) sdshdr64 { uint64_t len; uint64_t alloc; unsigned char flags; char buf[]; };
#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
|
示意图:
image20250218122020595.png
alloc - len 就等于还有多少空闲空间,可用来存储数据。
由于已经记录字符串的长度,可以通过 O(1)
时间复杂度获取字符串长度。
__attribute__ ((__packed__))
避免结构体内存对齐,Redis
选择牺牲一部分性能,节省内存。
image20250218122800584.png
Redis
的字符串经过这样设计,不仅可以存储字符串型的数据,还可以存储二进制数据。原生字符串是通过末尾的\0
判断字符串的结束,但是这样就让二进制数据无法存储,因为你不确定字符串哪里是完整的部分。但现在我们通过
len 成员就可以避免这个问题,实现二进制数据存储。
创建字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
|
sds _sdsnewlen(const void *init, size_t initlen, int trymalloc) { void *sh; sds s; char type = sdsReqType(initlen); if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8; int hdrlen = sdsHdrSize(type); unsigned char *fp; size_t usable;
sh = trymalloc ? s_trymalloc_usable(hdrlen + initlen + 1, &usable) : s_malloc_usable(hdrlen + initlen + 1, &usable); if (sh == NULL) return NULL;
if (init == SDS_NOINIT) init = NULL; else if (!init) memset(sh, 0, hdrlen + initlen + 1);
s = (char*)sh + hdrlen; fp = ((unsigned char*)s) - 1; usable = usable - hdrlen - 1;
if (usable > sdsTypeMaxSize(type)) usable = sdsTypeMaxSize(type);
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 = usable; *fp = type; break; } case SDS_TYPE_16: { SDS_HDR_VAR(16, s); sh->len = initlen; sh->alloc = usable; *fp = type; break; } case SDS_TYPE_32: { SDS_HDR_VAR(32, s); sh->len = initlen; sh->alloc = usable; *fp = type; break; } case SDS_TYPE_64: { SDS_HDR_VAR(64, s); sh->len = initlen; sh->alloc = usable; *fp = type; break; } }
if (initlen && init) memcpy(s, init, initlen); s[initlen] = '\0';
return s; }
|
核心步骤:
- 确定 SDS 类型
- 分配内存,内存大小为
头部长度(不同SDS类型对应的结构体大小)+申请长度+1(填充空字符)
- 根据 SDS_NOINIT 规则决定是否清空已分配内存
- 初始化 SDS 结构体
- 如果提供了初始化数据,则拷贝到 sds 数据区域
- 最后,返回字符串的起始地址(不是SDS 结构体的起始地址哦)
释放字符串
1 2 3 4
| void sdsfree(sds s) { if (s == NULL) return; s_free((char*)s-sdsHdrSize(s[-1])); }
|
sdsfree 会确确实实调用 free 回收内存,但 Redis 还提供 sdsclear
操作来通过重新设置 len 来假装释放掉字符串对应的内存空间。
1 2 3 4
| void sdsclear(sds s) { sdssetlen(s, 0); s[0] = '\0'; }
|
拼接字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| sds sdscatsds(sds s, const sds t) { return sdscatlen(s, t, sdslen(t)); }
sds sdscatlen(sds s, const void *t, size_t len) { size_t curlen = sdslen(s);
s = sdsMakeRoomFor(s, len); if (s == NULL) return NULL; memcpy(s + curlen, t, len); sdssetlen(s, curlen + len);
s[curlen + len] = '\0'; return s; }
|
如果扩容,申请足够的内存之后,就会把原数据拷贝到新空间,并且把要拼接的数据也拼接到后面,返回给用户新指针。
因此,重点看看 sdsMakeRoomFor 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
|
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;
if (avail >= addlen) return s;
len = sdslen(s); sh = (char*)s - sdsHdrSize(oldtype); reqlen = newlen = (len + addlen); assert(newlen > len);
if (greedy == 1) { if (newlen < SDS_MAX_PREALLOC) newlen *= 2; else newlen += SDS_MAX_PREALLOC; }
type = sdsReqType(newlen);
if (type == SDS_TYPE_5) type = SDS_TYPE_8;
hdrlen = sdsHdrSize(type); assert(hdrlen + newlen + 1 > reqlen);
if (oldtype == type) { newsh = s_realloc_usable(sh, hdrlen + newlen + 1, &usable); if (newsh == NULL) return NULL; s = (char*)newsh + hdrlen; } else { 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; }
|
如果剩余空间能够容纳追加的字符串,就不会额外申请空间。否则,就额外申请空间。
如果指定 greedy 为 0,就申请追加字符串长度大小空间;如果指定 greedy
为
1,就申请追加两倍字符串长度大小空间,避免后续再次分配,减少性能损耗。