redis系列 一、字符串原理

概述

redis 作为目前市面上应用最广泛的 key-value 非关系型数据库经常在项目中使用,它的高性能以及线程安全等优势可以在很多场景中大放异彩。从本篇开始,我将通过一个系列的博客系统的整理 redis 相关的知识。本篇先从它的基础类型开始,简单介绍下 redis 字符串类型原理

redis 数据类型

redis 有以下五种常用的数据类型:

String:字符串类型
Hash:哈希类型
list:链表类型
set:集合类型
zSet:有序集合类型

1、String 字符串类型

redis 是使用C语言开发的,但是它的字符串没有使用C语言原生的字符数组,而是构建了一种名 简单动态字符串(simple dynamic String)的抽象类型,并且将 SDS 作为 redis 默认的字符串表示。

举个简单的例子,当我们在 redis 客户端执行以下命令:

redis> SET msg "hello world"
OK

那么就可以在 redis 数据库中保存一个键值对,其中:

  • 键值对的键是一个字符串对象,对应底层保存一个 SDS 类型的 “msg”
  • 键值对的值也是一个字符串对象,对应底层保存一个 SDS 类型的 “hello world”
  • 这里的 “OK” 是 redis 服务端添加成功后的返回值,提示客户端添加成功。

需要注意的一点是,redis 中不是任何字符串都是使用 SDS 类型,对于一些不会改变的,如日志类型的数据,还是采用C语言原生字符数组实现,示例如下:

redisLog(REDIS_WARNING,"Redis is now ready to exit, bye bye...");


1-1、SDS 原理

有了上面的介绍,下面我们来看看 SDS 的实现:

struct sdshdr {
    // 记录buf数组中已使用字节的数量
    // 等于SDS所保存字符串的长度
    int len;
    // 记录buf数组中未使用字节的数量
    int free;
    // 字符数组,用于保存字符串
    char buf[];
};


每个 SDS 字符串都通过如上的结构体 保存,下面我们看一个具体的示例:

 

  • free 属性的值等于0,表示数组中所有字节都被占用
  • len 属性的值等于5,表示保存的字符串长度等于5
  • buf 属性是一个 char 类型的数组,数组的前 len 位保存了具体的字符串属性值,最后一位保存 ‘\0’ 空字符

SDS 遵循C语言字符数组以空字符结束的惯例。其中为空字符串分配额外的空间,保存空字符串到数组末尾等操作都是 SDS 函数内部实现好的,整个过程对用户而言是无感知的。这样做的好处是:SDS 可以直接复用一部分C语言字符串函数。举个例子:

printf("%s", sds->buf);

当我们执行上述代码时,会打印 “Redis”,而不是 “Redis\0”。因为C语言原生字符串就是以空字符结尾的,SDS 无须在单独编写输出代码。

1-2、SDS 相比字符数组的优势

有了上面的介绍,我们再来看看 SDS 相比字符数组有哪些优势:

  • 常数复杂度获取字符串长度
  • 杜绝缓冲区溢出
  • 减少修改字符串时所带来的内存重分配次数
  • 二进制安全

1-2-1、常数复杂度获取字符串长度

C语言字符数组本身是没有保存字符串长度的,当我们获取字符长度时,需要遍历整个数组,直到扫描到 ‘\0’ 位停止。也就是说,如果 redis 使用字符数组来保存数据,那么获取字符串长度的时间复杂度为 O(n),但如果使用 SDS 的话,因为结构体自身属性保含字符串长度,因此它的时间复杂度只有 O(1)。

其中需要说明的一点是:SDS 中 len 属性的更新都是在 SDS API 执行过程中自动实现的,无须用户手动修改该属性值。

SDS 通过这种空间换时间的设计模式,使字符串长度的获取不再成为性能瓶颈。

1-2-2、杜绝缓冲区溢出

缓冲区溢出 也是由原生字符数组不保存长度所导致的。具体我们看示例:

如上图所示,存在字符串S1 和 S2,他们在物理地址上暂时是 连续的。如果此时我们调用如下方法:

strcat(s1, " Cluster"); 

在 s1 的字符串末尾连接 “Cluster” 字符串时,就可能导致以下结果:

字符串 s1 的数据溢出到了 s2 的内存地址,导致 s2 的数据被意外修改。

与C字符串不同,SDS 结构体则完全杜绝了缓冲区溢出的可能性:

当我们调用函数修改 SDS 字符数组时,SDS API 首先会判断内存是否够用。如果不够用的话,SDS 首先会扩展字符串的长度,然后再进行相关操作。整个扩展过程对外也是透明的,用户无须手动操作。

1-2-3、减少修改字符串时所带来的内存重分配次数

也正是因为C语言字符数组本身不记录字符串长度,因此对于一个包含N个字符数组的C字符串来说,这个字符串的长度总是一个N+1字符长的数组。因为C字符数组和底层数组之间的关联性关系,每次我们扩容或者缩短时,程序都需要为这个字符数组执行一次内存重分配操作。

  • 如果程序执行增长字符串的操作,如连接字符串,那么在执行函数前需要为字符数组分配更大的内存数,否则就会产生缓冲区溢出
  • 如果程序执行减少字符串的长度,如截断字符串,那么在函数执行后需要为字符数组重新分配来释放不被使用的内存,如果忘记这一步就会产生内存泄露
  • 由于内存重分配涉及很复杂的算法,并且分配过程中可能需要执行 系统调用,这对于非常重视性能的 redis 数据库来说可能产生性能瓶颈。

为了避免由于频繁执行内存重分配所带来的性能问题,redis 自身做了如下优化:

空间预分配:空间预分配用于优化字符串增长操作,当 SDS 需要扩容时,程序不仅会为 SDS 分配修改必要的长度,还会为 SDS 分配额外的长度。具体的分配规则如下:

  • 修改之后,如果 SDS 的长度小于 1MB,那么在原来的基础上预分配和修改后长度相同的内存空间。假如修改之后,SDS的长度变为13,此时需要为 SDS 额外再分配13字节的长度,也就是 free 属性和 len 属性都等于13
  • 修改之后,如果 SDS 的长度大于 1MB,那么在原来的基础上直接预分配 1MB空间。假如修改之后,SDS 的长度为10MB,此时需要为 SDS 额外再预分配1MB的长度。

需要注意的一点是,这里的如果判断都是根据计算得来,此时还没有真正分配内存,真正分配内存都是在分配规则计算完毕后。SDS 通过这种预分配内存的方式,减少后续执行修改字符串长度时的内存重分配次数,以这种以空间换时间的方式提高效率。

惰性空间释放:惰性空间释放用于优化字符串减少操作,当 SDS 需要缩容时,程序不会立即执行内存重分配回收暂时没有使用的内存,而是通过 free 属性把它们先保存起来,以便后面再使用。

SDS 通过这种惰性空间释放的方式,减少了内存重分配的次数,总得来说还是借鉴了以空间换时间的思想。需要明确的一点是:SDS 提供了相应的 API 回收内存,当我们真正需要释放内存空间时,完全不用担心惰性空间释放会浪费内存资源。

1-2-4、二进制安全

由于C字符数组的种种限制,如必须符合ASCII规范、末尾必须以空字符串结尾,字符数组中不能包含空字符等,导致它只能保存文本数据,不能保存图片、音频、视频等二进制数据。

为了使 redis 满足各种业务场景,SDS 的 API 都是二进制安全的。也就是说 SDS 不会对字符串的格式有任何限制,数据在写入时是什么样子,在读取时就是什么样子。例如当我们使用 SDS 保存特殊字符串时,在读取过程中不会因为空字符串而停止,必须读取 len 长度的字符串才会结束。

总结以下,redis 不是用来保存字符的,而是用来保存二进制数据的。这也是有时候 redis 被称为 字节数组 的主要原因。

1-2-5、兼容部分C字符串函数
虽然 SDS 的 API 都是二进制安全的,但是它还是保留了部分C字符数组的规范:SDS 的 buf 字符数组仍然以空字符结束,系统会为它多分配一字节的空间,在字符串末尾保存空字符。

正是因为这些规范,redis 可以直接调用部分 <String.h> 库函数。这样 SDS 就不需要重新实现部分已存在的函数。


转载自:https://blog.csdn.net/meiyongdesan/article/details/107100320

参考:《redis设计与实现》黄健宏著

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值