SDS——Redis 源码剖析,java 工程师进阶书籍
if (sh == NULL) return NULL;
if (init==SDS_NOINIT)
init = NULL;
else if (!init)
memset(sh, 0, hdrlen+initlen+1);
/* 注意:返回的 s 并不是直接指向 sds 的指针,而是指向 sds 中字符串的指针,sds 的指针还需要
根据 s 和 hdrlen 计算出来 */
s = (char*)sh+hdrlen;
fp = ((unsigned char*)s)-1;
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] = '\0';
return s;
}
SDS 的使用
上面代码中我特意标注了一个注意,sdsnewlen()返回的 sds 指针并不是直接指向 sdshdr 的地址,而是直接指向了 sdshdr 中 buf 的地址。这样做有啥好处?好处就是这样可以兼容 c 原生字符串。buf 其实就是 C 原生字符串+部分空余空间,中间是特殊符号'\0'隔开,‘\0’有是标识 C 字符串末尾的符号,这样就实现了和 C 原生字符串的兼容,部分 C 字符串的 API 也就可以直接使用了。 当然这也有坏处,这样就没法直接拿到 len 和 alloc 的具体值了,但是也不是没有办法。
当我们拿到一个 sds,假设这个 sds 就叫s
吧,其实一开始我们对这个 sds 一无所知,连他是 sdshdr 几都不知道,这时候可以看下 s 的前面一个字节,我们已经知道 sdshdr 的数据结构了,前一个字节就是 flag,根据 flag 具体的值我们就可以推断出 s 具体是哪个 sdshdr,也可以推断出 sds 的真正地址,相应的就知道了它的 len 和 alloc,知道了这点,下面这些有点晦涩的代码就很好理解了。
oldtype = s[-1] & SDS_TYPE_MASK; // SDS_TYPE_MASK = 7 看下 s 前面一个字节(flag)推算出 sdshdr 的类型。
// 这个宏定义直接推算出 sdshdr 头部的内存地址
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
#define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS)
// 获取 sds 支持的长度
static inline size_t sdslen(const sds s) {
unsigned char flags = s[-1]; // -1 相当于获取到了 sdshdr 中的 flag 字段
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5:
return SDS_TYPE_5_LEN(flags);
case SDS_TYPE_8:
return SDS_HDR(8,s)->len; // 宏替换获取到 sdshdr 中的 len
...
// 省略 SDS_TYPE_16 SDS_TYPE_32 的代码……
case SDS_TYPE_64:
return SDS_HDR(64,s)->len;
}
return 0;
}
// 获取 sds 剩余可用空间大小
static inline size_t sdsavail(const sds s) {
unsigned char flags = s[-1];
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5: {
return 0;
}
case SDS_TYPE_8: {
SDS_HDR_VAR(8,s);
return sh->alloc - sh->len;
}
...
// 省略 SDS_TYPE_16 SDS_TYPE_32 的代码……
case SDS_TYPE_64: {
SDS_HDR_VAR(64,s);
return sh->alloc - sh->len;
}
}
return 0;
}
/* 返回 sds 实际的起始位置指针 */
void *sdsAllocPtr(sds s) {
return (void*) (s-sdsHdrSize(s[-1]));
}
SDS 的扩容
在做字符串拼接的时候,sds 可能剩余的可用空间不足,这个时候需要扩容,什么时候该扩容,又该怎么扩? 这是不得不考虑的问题。Java 中很多数据结构都有动态扩容的机制,比如和 sds 很类似的 StringBuffer,HashMap,他们都会在使用过程中动态判断是否空间充足,而且基本上都采用了先指数扩容,然后到一定大小限制后才开始线性扩容的方式,Redis 也不例外,Redis 在 1024_1024 以内都是 2 倍的方式扩容,只要不超出 1024_1024 都是先额外申请 200%的空间,但一旦总长度超过 1024_1024 字节,那每次最多只会扩容 1024_1024 字节。 Redis 中 sds 扩容的代码是在 sdsMakeRoomFor(),可以看到很多字符串变更的 API 开头都直接或者间接调用这个。 和 Java 中 StringBuffer 扩容不同的是,Redis 这里还需要考虑不同字符串长度时 sdshdr 类型的变化,具体代码如下:
// 扩大 sds 的实际可用空间,以便后续能拼接更多字符串。
// 注意:这里实际不会改变 sds 的长度,只是增加了更多可用的空间(buf)
sds sdsMakeRoomFor(sds s, size_t addlen) {
void *sh, *newsh;
size_t avail = sdsavail(s);
size_t len, newlen;
char type, oldtype = s[-1] & SDS_TYPE_MASK; // SDS_TYPE_MASK = 7
int hdrlen;
/* 如果有足够的剩余空间,直接返回 */
if (avail >= addlen) return s;
len = sdslen(s);
sh = (char*)s-sdsHdrSize(oldtype);
newlen = (len+addlen);
// 在未超出 SDS_MAX_PREALLOC 前,扩容都是按 2 倍的方式扩容,超出后只能递增
if (newlen < SDS_MAX_PREALLOC) // SDS_MAX_PREALLOC = 1024*1024
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
type = sdsReqType(newlen);
/* 在真正使用过程中不会用到 type5,如果遇到 type5 直接使用 type8*/
if (type == SDS_TYPE_5) type = SDS_TYPE_8;
hdrlen = sdsHdrSize(type);
if (oldtype==type) {
newsh = s_realloc(sh, hdrlen+newlen+1);
if (newsh == NULL) return NULL;
s = (char*)newsh+hdrlen;
} else {
// 扩容其实就是申请新的空间,然后把旧数据挪过
去
newsh = s_malloc(hdrlen+newlen+1);
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);
}
sdssetalloc(s, newlen);
return s;
}
评论