目录
- 1.1 SDS(Simple Dynamic String)
- 1.1.1 概述
- 1.1.2 底层实现
- 1.1.3 源码实现
- 1.不使用结构体指针传递,而使用变长数组传递参数
- 2.底层数组扩容规则
- 1.1.4 使用SDS的好处
本系列所有的内容直接参考于redis3.0版本源码和《Redis设计与实现》圣经,请大家放心食用~
1.1 SDS(Simple Dynamic String)
1.1.1 概述
Redis的数据类型都是Key-Value键值对,Key永远都是String类型,而我们常说的Redis五大数据类型是指的Value的类型。Redis没用使用传统的C风格字符串作为String的实现,而是自定义了SDS用来作为redis的默认字符串表示。Redis的SDS除了用于String数据的存储之外,还用作缓冲区,如AOF的缓冲区,客户端状态的输入缓冲区等。
1.1.2 底层实现
Redis3.0源码中SDS的实现(sds.h)
/*
* 类型别名,用于指向 sdshdr 的 buf 属性
*/
typedef char *sds;
/*
* 保存字符串对象的结构
*/
struct sdshdr {
// buf 中已占用空间的长度
int len;
// buf 中剩余可用空间的长度
int free;
// 变长数组,存储数据空间
char buf[];
};
SDS结构体中的 len + free 的长度是整个SDS字符串的空间大小 - 1,因为在字符串末尾填充了’\0’,这个填充的作用是为了让redis的字符串能兼容部分C语言字符串的API,起到代码重用。
1.1.3 源码实现
Redis的SDS的实现思想其实很类似于Cpp中的vector或者Java中的ArrayList,相信大家一看到这个结构就明白大概是如何进行操作的了。这里就不详细介绍SDS中的所有API,只说一下关键的要点,有需要的朋友可以查看sds.h和sds.c源码文件。
1.不使用结构体指针传递,而使用变长数组传递参数
不过在查阅Redis源码中关于SDS结构体传递有一个注意点。就是所有SDS的传递都是通过直接传递SDS结构体中变长数组buf的地址来传递的(注意SDS结构体定义上方的typedef char *sds)。
那么只通过buf数据的地址如何得知整个结构体的数据呢?
C语言的变长数组的大小是不计入结构体的大小中的,因为数组名实际上不是指针,它就是个地址偏移。并且变长数组的地址是连续衔接在结构体的后方。那么我们使用数组的首地址减去结构体的大小,就得到了结构体的首地址,就可以对结构体数据进行操作了。
struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
2.底层数组扩容规则
当我们对SDS的字符串进行添加操作的时候,首先会判断当前剩余的长度是否足够,如果足够则不进行扩容,则进行扩容。(对应zmalloc.c文件中的zrealloc函数底层实际上使用realloc实现)
void *zrealloc(void *ptr, size_t size) {
#ifndef HAVE_MALLOC_SIZE
void *realptr;
#endif
size_t oldsize;
void *newptr;
if (ptr == NULL) return zmalloc(size);
#ifdef HAVE_MALLOC_SIZE
oldsize = zmalloc_size(ptr);
newptr = realloc(ptr,size);
if (!newptr) zmalloc_oom_handler(size);
update_zmalloc_stat_free(oldsize);
update_zmalloc_stat_alloc(zmalloc_size(newptr));
return newptr;
#else
realptr = (char*)ptr-PREFIX_SIZE;
oldsize = *((size_t*)realptr);
newptr = realloc(realptr,size+PREFIX_SIZE);
if (!newptr) zmalloc_oom_handler(size);
*((size_t*)newptr) = size;
update_zmalloc_stat_free(oldsize);
update_zmalloc_stat_alloc(size);
return (char*)newptr+PREFIX_SIZE;
#endif
}
扩容规则实际如下
- 当扩容之后的newlen小于1MB的时候,多分配和newlen大小相同的冗余空间,扩容为 2 * newLen + 1的大小,即SDS的结构体的成员 len == free ==newLen
- 当扩容之后的newLen大于1MB的时候,则多分配1MB的空间,即扩容为 newLen + 1MB + 1的大小。
1.1.4 使用SDS的好处
- 使用O(1)的时间获取字符串长度
因为SDS结构体中存储了字符串的长度,因此在获取字符串长度的时候无需调用strlen函数,直接就可以获取到。 - 防止缓冲区溢出
传统C语言的字符串拼接函数strcat(dest,source),需要我们程序员保证dest的空间足以容下拼接后的字符串长度,而SDS的free字段记录了当前SDS还有多少可用的空间。如果空间足够则直接拷贝内容,不足则先进行扩容,再执行操作。 - 减少字符串修改带来的内存分配次数
- 空间预先分配,SDS的空间总是预先分配足够大小的空间,防止String修改频繁申请和释放空间
- 惰性空间释放,当程序需要减少SDS的字符串长度的时候,redis并不会直接释放多余的空间,而是使用free字段进行记录,以便下次增加长度时候使用。当然不用担心这部分空间的冗余,如果有需要的话,redis底层会回收这段空间。
- SDS是二进制安全
传统C字符串以’\0’作为字符串的结束标志,但是二进制流等数据中可能就会包含’\0’等特殊字符,使用传统C的字符串会导致数据识别失败。而SDS采用len成员记录的数据的长度,因此可以正确保存图片等二进制数据。