redis 作为目前市面上应用最广泛的 key-value 非关系型数据库经常在项目中使用,它的高性能以及线程安全等优势可以在很多场景中大放异彩。从本篇开始,我将通过一个系列的博客系统的整理 redis 相关的知识。本篇先从它的基础类型开始,简单介绍下 redis 字符串类型原理
redis 有以下五种常用的数据类型:
redis 是使用C语言开发的,但是它的字符串没有使用C语言原生的字符数组,而是构建了一种名 简单动态字符串(simple dynamic String)的抽象类型,并且将 SDS 作为 redis 默认的字符串表示。
举个简单的例子,当我们在 redis 客户端执行以下命令:
redis> SET msg "hello world"
OK
那么就可以在 redis 数据库中保存一个键值对,其中:
这里的 “OK” 是 redis 服务端添加成功后的返回值,提示客户端添加成功。
需要注意的一点是,redis 中不是任何字符串都是使用 SDS 类型,对于一些不会改变的,如日志类型的数据,还是采用C语言原生字符数组实现,示例如下:
redisLog(REDIS_WARNING,"Redis is now ready to exit, bye bye...");
有了上面的介绍,下面我们来看看 SDS 的实现:
struct sdshdr {
// 记录buf数组中已使用字节的数量
// 等于SDS所保存字符串的长度
int len;
// 记录buf数组中未使用字节的数量
int free;
// 字符数组,用于保存字符串
char buf[];
};
每个 SDS 字符串都通过如上一个 结构体 保存,下面我们看一个具体的示例:
SDS 遵循C语言字符数组以空字符结束的惯例。其中为空字符串分配额外的空间,保存空字符串到数组末尾等操作都是 SDS 函数内部实现好的,整个过程对用户而言是无感知的。这样做的好处是:SDS 可以直接复用一部分C语言字符串函数。举个例子:
printf("%s", sds->buf);
当我们执行上述代码时,会打印 “Redis”,而不是 “Redis\0”。因为C语言原生字符串就是以空字符结尾的,SDS 无须在单独编写输出代码。
有了上面的介绍,我们再来看看 SDS 相比字符数组有哪些优势:
C语言字符数组本身是没有保存字符串长度的,当我们获取字符长度时,需要遍历整个数组,直到扫描到 ‘\0’ 位停止。也就是说,如果 redis 使用字符数组来保存数据,那么获取字符串长度的时间复杂度为 O(n),但如果使用 SDS 的话,因为结构体自身属性保含字符串长度,因此它的时间复杂度只有 O(1)。
其中需要说明的一点是:SDS 中 len 属性的更新都是在 SDS API 执行过程中自动实现的,无须用户手动修改该属性值。
SDS 通过这种空间换时间的设计模式,使字符串长度的获取不再成为性能瓶颈。
缓冲区溢出 也是由原生字符数组不保存长度所导致的。具体我们看示例:
如上图所示,存在字符串S1 和 S2,他们在物理地址上暂时是 连续的。如果此时我们调用如下方法:
strcat(s1, " Cluster");
在 s1 的字符串末尾连接 “Cluster” 字符串时,就可能导致以下结果:
字符串 s1 的数据溢出到了 s2 的内存地址,导致 s2 的数据被意外修改。
与C字符串不同,SDS 结构体则完全杜绝了缓冲区溢出的可能性:当我们调用函数修改 SDS 字符数组时,SDS API 首先会判断内存是否够用。如果不够用的话,SDS 首先会扩展字符串的长度,然后再进行相关操作。整个扩展过程对外也是透明的,用户无须手动操作。
也正是因为C语言字符数组本身不记录字符串长度,因此对于一个包含N个字符数组的C字符串来说,这个字符串的长度总是一个N+1字符长的数组。因为C字符数组和底层数组之间的关联性关系,每次我们扩容或者缩短时,程序都需要为这个字符数组执行一次内存重分配操作。
由于内存重分配涉及很复杂的算法,并且分配过程中可能需要执行 系统调用,这对于非常重视性能的 redis 数据库来说可能产生性能瓶颈。
为了避免由于频繁执行内存重分配所带来的性能问题,redis 自身做了如下优化:
空间预分配:空间预分配用于优化字符串增长操作,当 SDS 需要扩容时,程序不仅会为 SDS 分配修改必要的长度,还会为 SDS 分配额外的长度。具体的分配规则如下:
需要注意的一点是,这里的如果判断都是根据计算得来,此时还没有真正分配内存,真正分配内存都是在分配规则计算完毕后。SDS 通过这种预分配内存的方式,减少后续执行修改字符串长度时的内存重分配次数,以这种以空间换时间的方式提高效率。
惰性空间释放:惰性空间释放用于优化字符串减少操作,当 SDS 需要缩容时,程序不会立即执行内存重分配回收暂时没有使用的内存,而是通过 free 属性把它们先保存起来,以便后面再使用。
SDS 通过这种惰性空间释放的方式,减少了内存重分配的次数,总得来说还是借鉴了以空间换时间的思想。需要明确的一点是:SDS 提供了相应的 API 回收内存,当我们真正需要释放内存空间时,完全不用担心惰性空间释放会浪费内存资源。
由于C字符数组的种种限制,如必须符合ASCII规范、末尾必须以空字符串结尾,字符数组中不能包含空字符等,导致它只能保存文本数据,不能保存图片、音频、视频等二进制数据。
为了使 redis 满足各种业务场景,SDS 的 API 都是二进制安全的。也就是说 SDS 不会对字符串的格式有任何限制,数据在写入时是什么样子,在读取时就是什么样子。例如当我们使用 SDS 保存特殊字符串时,在读取过程中不会因为空字符串而停止,必须读取 len 长度的字符串才会结束。
总结以下,redis 不是用来保存字符的,而是用来保存二进制数据的。这也是有时候 redis 被称为 字节数组 的主要原因。
虽然 SDS 的 API 都是二进制安全的,但是它还是保留了部分C字符数组的规范:SDS 的 buf 字符数组仍然以空字符结束,系统会为它多分配一字节的空间,在字符串末尾保存空字符。
正是因为这些规范,redis 可以直接调用部分
下面我通过表格的形式列举出 redis 一些常用的 API 方法: