【Redis 数据结构】为什么 Redis 要用 SDS?

Redis 没有直接使用 C 语言传统的字符串表示,而是自己创建了一种新的数据结构叫简单动态字符串(Simple Dynamic String)SDS,并且将它作为默认字符串的表示。

SDS数据结构

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

SDS 遵循 C 字符串以空字符串结尾的惯例,保存空字符串的 1 字节空间不计算在 SDS 的 len 属性里面,并且为空字符串分配额外的 1 字节空间,以及添加空字符到字符串末尾等操作,都是由 SDS 函数自动完成的。遵循 C 字符串以空字符串结尾的好处是,SDS 可以直接重用一部分 C 字符串函数库里面的函数。

SDS 和 C 字符串的区别

获取字符串长度的时间复杂度

C 字符串本身是不记录自身的长度信息的,当我们需要获取 C 字符串的长度时就需要遍历整个字符串计数,直到遇到空字符为止,这个操作的时间复杂度是 O(N)。

SDS 和 C 字符串并不同,当你需要获取长度信息的时候直接访问 SDS 的 len 属性即可,这个操作的时间复杂度是 O(1)。设置和更新 len 属性都由 SDS 的 API 在执行时自动完成。

缓冲区溢出

C 字符串不记录自身的长度信息的特性会带来一个问题,那就是缓冲区溢出。比如说我们使用 C 语言中的 strcat 函数,这个函数是将一个字符串拼接到另一个字符串末尾,假设现在我们要把 "Cluster" 拼接到 "Redis" 后,如果我们在执行函数的时候,已经为 "Redis" 分配了一定的内存,但是我们拼接 "Cluster" 的时候,分配的内存不足以存储 "Cluster",那么就会产生缓冲区溢出。

SDS 的空间分配策略就完全杜绝了发生缓冲区溢出的可能性,当需要修改 SDS 时,SDS 的 API 会检查空间是否满足所修要的修改,如果不满足,会扩充存储空间的大小,然后才执行修改的操作。

内存分配次数

C 字符串不记录自身的长度信息的特性会带来一个问题,C 字符串需要多记录一个 '\0' 来记录结束的位置,以便于计算长度的时候使用,C 的底层维护了 N+1 字符长的这样一个数组,那么如果有修改,我们的程序总是需要去对这个数组进行一次内存重分配的操作,内存重分配的涉及复杂的算法并且可能执行系统调用,通常会是一个比较耗时的操作,当 Redis 作为一个数据库时,速度的要求会比较严苛,作为一个数据库,肯定会频繁的改动,如果每次改动都进行一次内存重分配,会对性能造成影响。

SDS 使用空间预分配和惰性空间释放这两种优化策略优化了 C 字符串的这种缺陷。

空间预分配

  • 如果对 SDS 修改之后, len 属性小于 1MB,那么程序会分配和 len 属性同样大小的未使用空间。比如说 SDS 进行修改了之后,len 属性变成 13 字节,那么程序也会分配 13 字节的未使用空间,也就是 free 属性为 13 字节,SDS 的 buff 数组实际的长度就变为 13 + 13 + 1 = 27。
  • 如果对 SDS 修改之后,SDS 的长度大于等于 1MB,那么程序会分配 1MB 的未使用空间。比如说 SDS 进行修改之后,SDS 的 len 属性变成 10MB,那么程序会分配 1MB的未使用空间,SDS 的 buff 数组实际的长度就变为 10MB + 1MB +1Byte。

通过空间预分配策略,Redis 就可以减少字符串增长操作所需的内存重分配次数。

我们来分析一下上面的过程,刚开始有这么一个值 str"Redis",如图1-1所示,当我们进行"is"拼接的时候, SDS 会进行一次内存重分配,将 str 的长度修改为 8 字节,同时分配 8 字节的未使用空间,如图 1-2所示,当我们再拼接"fast"时,这次 SDS 不会进行内存重分配,因为我们还有 8 字节的未使用空间,这足以保存 4 字节的"fast",如图 1-3 所示。通过这种策略,将连续增长 N 次字符串所需内存重分配次数从必定 N 次降低为最多 N 次。

惰性空间释放

当我们截断字符串或者缩短字符串长度的时候,SDS 并不会内存重分配来回收那些不使用的内存,而是将它记录在 free 属性里以便后续使用。

还是用刚才的例子,我们得到最后的值 str "Redis is fast",如图2-1所示,现在我们需要截断字符串,去掉 "is fast",去掉以后,SDS 并不会内存重分配,而是将去掉的 8 字节保留了下来,并加入到了 free 属性里,当我们再拼接字符串 "is" 的时候,SDS 就不需要内存重分配扩展内存空间,SDS 剩余 11 字节的空间足以拼接 2 字节的 "is",通过这种惰性空间释放策略,避免了 SDS 在缩短字符串时需要内存重分配的操作,也为后续需要拼接增长的操作提供了优化。

二进制安全

因为 C 字符串是以空字符串来判断结尾的,那么存取的 C 字符串里面不能包含空字符,那么程序会误判断提前结束,这使得 C 字符串只能保存文本数据,不能保存图片、音频、视频和压缩文件这样的二进制数据。虽然数据库一般都保存文本数据,但是为了让数据库满足各种场景,SDS 的 API 是二进制安全的,数据在写入时是怎么样的,读取的时候就是怎么样的,并不会对数据进行处理,SDS 不会就出现像 C 字符串一样误判,是因为 SDS 是依靠 len 属性的值判断字符串是否结束而不是空字符。

总结

C 字符串 SDS
时间复杂度 需要遍历整个字符串统计长度,O(N) 只需要访问 len 属性即可得到长度,O(1)
缓冲区溢出 API不安全,存在缓冲区溢出 API安全,不存在缓冲区溢出
内存分配次数 字符串修改 N 次就需要 N 次内存重分配 字符串修改 N 次最多需要 N 次内存重分配
二进制安全 只保存文本,不安全 可以保存文本和二进制数据,安全

参考

《Redis设计与实现》黄建宏

相关推荐
_oP_i1 小时前
Pinpoint 是一个开源的分布式追踪系统
java·分布式·开源
mmsx1 小时前
android sqlite 数据库简单封装示例(java)
android·java·数据库
武子康1 小时前
大数据-258 离线数仓 - Griffin架构 配置安装 Livy 架构设计 解压配置 Hadoop Hive
java·大数据·数据仓库·hive·hadoop·架构
豪宇刘2 小时前
MyBatis的面试题以及详细解答二
java·servlet·tomcat
秋恬意2 小时前
Mybatis能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别
java·数据库·mybatis
Ren_xixi3 小时前
redis和mysql的区别
数据库·redis·mysql
FF在路上3 小时前
Knife4j调试实体类传参扁平化模式修改:default-flat-param-object: true
java·开发语言
真的很上进3 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
众拾达人4 小时前
Android自动化测试实战 Java篇 主流工具 框架 脚本
android·java·开发语言
皓木.4 小时前
Mybatis-Plus
java·开发语言