共计 3334 个字符,预计需要花费 9 分钟才能阅读完成。
当初在高铁上, 赶着春节回家过年, 无座站票, 电脑只能放行李架上, 面对着行李架撸键盘 – 看过 <Redis 的设计与实现 > 这本书, 忽然想起, 便整顿下 SDS 的内容, 绝对前面的章节, 算是比较简单的~
大多数状况下, Redis 应用 SDS(Simple Dynamic String, 简略动静字符串)作为字符串示意, 比起 C 字符串, SDS 具备以下长处:
- 常数复杂度获取字符串长度;
- 杜绝缓冲区溢出;
- 缩小批改字符串时带来的内存重调配次数;
- 二进制平安;
- 兼容局部 C 字符串函数.
以上列举的长处, 也算是这篇文章的次要内容. 先从定义说起吧:
1. SDS 的定义
SDS 构造定义如下(sds.h/sdshdr
):
struct sdshdr {
// 记录 buf 数组中已应用字节的数量, 等于 SDS 所保留字符串的长度
int len;
// 记录 buf 数组中未应用字节的数量
int free;
// 字节数组, 用于保留字符串
char buf[];}
- len 属性记录了已应用的字节数量(字符串长度);
- free 属性的值为 0, 示意这个 SDS 没有未应用的空间;
- free 属性的值为 5, 示意这个 SDS 保留了一个 5 字节长的字符串;
- buf 属性是一个 char 类型的数组, 数组的最初一个字节保留了空字符 \0.
SDS 遵循 C 字符串以空字符串结尾的常规, 空字符 不计入 SDS 的 len 属性 , 即 额定为空字符调配了 1 字节的空间 , 并且 增加空字符到字符串开端均由 SDS 函数主动实现, 对使用者齐全通明. 该个性带来的益处是, SDS 能够间接复用 C 字符串函数库的局部函数.
2. SDS 与 C 字符串的区别
2.1 常数复杂度获取字符串长度
- 因为 C 字符串不记录本身长度, 所以获取长度时须要遍历整个字符串, 直到遇到空字符 \0 为止, 该操作的复杂度为 O(N);
- 因为 SDS 在 len 属性中记录了 SDS 自身的长度, 所以获取一个 SDS 的长度的复杂度为 O(1).
Redis 应用 SDS, 将获取字符串长度所需的复杂度从 O(N)升高到 O(1), 确保获取字符串长度的工作不会成为 Redis 的性能瓶颈.
2.2 杜绝缓冲区溢出
因为 C 字符串不记录本身长度, 以函数 strcat
来说, 当执行该函数时, 都是认为曾经为 dest
调配了足够的内存包容 src
字符串, 但如果该假如不成立, 就会产生缓冲区溢出.
然而, SDS 的字符串空间调配策略, 从根本上杜绝了缓冲区溢出的可能性: 当 SDS-API 须要对 SDS 进行批改时, API 会先查看 SDS 的空间是否满足批改所需的需要, 如果不满足的话, API 会主动扩容至所需大小, 再执行批改操作. 所以, SDS 无需手工保护 SDS 的空间大小, 也不会产生缓冲区溢出的问题.
2.3 缩小批改字符串时带来的内存重调配次数
因为 C 字符串不记录本身长度, 所以每次增长或缩减字符串, 须要对保留这个 C 字符串的数组进行一次内存重调配操作:
1. 如果程序执行的是 增长 字符串的操作, 比方拼接操作 append
, 须要进行内存重调配操作, 扩大底层数组至适合大小, 否则将会产生 缓冲区溢出 ;
2. 如果程序执行的是 缩短 字符串的操作, 比方截断操作 trim
, 须要进行内存开释操作, 否则将会产生 内存泄露.
Redis 作为数据库, 常常用于速度要求严苛, 数据被频繁批改的场合, 缩小内存的重调配次数能进步性能. SDS 通过未应用空间解除了 字符串长度 和底层数组 长度之间的关联: 在 SDS 中, buf 数组的长度不肯定就是字符数加一, 数组外面能够蕴含未应用的字节, 而这些字节的数量就由 SDS 的 free 属性记录.
通过未应用空间, SDS 实现了空间预调配和惰性空间开释两种优化策略.
1. 空间预调配
当 SDS-API 对 SDS 进行批改, 并且须要对 SDS 进行空间扩大的时候, 程序不仅会为 SDS 调配批改所需的空间, 还会为 SDS 调配额定的未应用空间.
额定调配的未应用空间数量, 由以下公式决定:
- 如果对 SDS 进行批改之后, SDS 的长度 (即 len 属性) 将小于 1MB, 那么程序调配和 len 属性同样大小的未应用空间, 这时 SDS 的 len 属性的值将和 free 属性的值雷同: 比方批改之后, SDS 的 len 将变成 13 字节, 那么程序也会调配 13 字节的未应用空间, buf 长度将变成 13Byte + 13Byte + 1Byte = 27Byte;
- 如果对 SDS 进行批改之后, SDS 的长度将大于等于 1MB, 那么程序会调配 1MB 的未应用空间: 比方批改之后, SDS 的 len 将变成 30MB, 那么程序会调配 1MB 的未应用空间, buf 长度将变成 30MB + 1MB + 1Byte.
通过空间预调配策略, Redis 能够缩小间断执行字符串增长操作所需的内存重调配次数.
2. 惰性空间开释
惰性空间开释用于优化 SDS 的字符串缩短操作: 当 SDS-API 须要缩短 SDS 字符串时, 程序并不会立刻回收内存, 而是应用 free 属性将这些字节的数量记录起来, 并期待未来应用.
当然, SDS 也提供了相应的 API 真正地开释 SDS 的未应用空间, 无需放心该策略带来的内存节约问题.
2.4 二进制平安
C 字符串除了开端, 不能蕴含空字符, 否则最先被程序读入的空字符将被误认为是字符串结尾, 所以 C 字符串只能保留文本数据, 而不能保留像图片, 音频, 视频, 压缩文件这样的二进制数据.
Redis 为了能够实用于各种不同的场景, SDS-API 都是二进制平安的(binary-safe), 所有 SDS-API 都会以二进制的形式解决 buf 数组外面的数据, 不限度, 不过滤, 不假如, 写入什么就读到什么.
2.5 兼容局部 C 字符串函数
尽管 SDS-API 是二进制平安的, 但它们一样遵循 C 字符串以空字符结尾的常规: 这些 API 总是将 SDS 保留的数据开端数组为空字符, 并且总会在为 buf 调配空间时多调配一个字节来包容这个空字符, 以复用 <string.h> 库定义的函数.
3. SDS 的 API
函数 | 作用 | 复杂度 |
---|---|---|
sdsnew | 创立一个蕴含给定 C 字符串的 SDS | O(N), N 为给定 C 字符串的长度 |
sdsempty | 创立一个不蕴含任何内容的空 SDS | O(1) |
sdsfree | 开释给定的 SDS | O(N), N 为被开释 SDS 的长度 |
sdslen | 返回 SDS 的已应用空间字节数 | 这个值能够通过读取 SDS 的 len 属性来间接取得, 复杂度为 O(1) |
sdsavail | 返回 SDS 的未应用空间字节数 | 这个值能够通过读取 SDS 的 free 属性来间接取得, 复杂度为 O(1) |
sdsdup | 创立一个给定 SDS 的正本(copy) | O(N), N 为给定 C 字符串的长度 |
sdsclear | 清空 SDS 保留的字符串内容 | 因为惰性空间开释策略, 复杂度为 O(1) |
sdscat | 将给定 C 字符串拼接到另一个 SDS 字符串的开端 | O(N), N 为被拼接 C 字符串的长度 |
sdscatsds | 将给定 SDS 字符串拼接到另一个 SDS 字符串的开端 | O(N), N 为被拼接 SDS 字符串的长度 |
sdscpy | 将给定的 C 字符串复制到 SDS 外面, 笼罩 SDS 原有的字符串 | O(N), N 为被复制 SDS 字符串的长度 |
sdsgrowzero | 用空字符将 SDS 扩大至给定长度 | O(N), N 为扩大新增的字节数 |
sdsrange | 保留 SDS 给定区间内的数据, 不在区间内的数据会被笼罩或革除 | O(N), N 为被保留数据的字节数 |
sdstrim | 承受一个 SDS 和一个 C 字符串作为参数, 从 SDS 中移除所有在 C 字符串中呈现过的字符 | O(N^2), N 为给定 C 字符串的长度 |
sdscmp | 比照两个 SDS 字符串是否雷同 | O(N), N 为两个 SDS 钟较短的那个 SDS 的长度 |
4. 总结
- Redis 在大多数状况下应用 SDS 作为字符串的示意;
- 相比 C 字符串, SDS 具备以下长处:
- 常熟复杂度获取字符串长度;
- 杜绝缓冲区溢出;
- 缩小批改字符串长度时所需的内存重调配次数;
- 二进制平安;
- 兼容局部 C 字符串函数.
- C 字符串和 SDS 之间的区别:
C 字符串 | SDS |
---|---|
获取字符串长度的复杂度为 O(N) | 获取字符串长度的复杂度为 O(1) |
API 是不平安的,可能造成缓冲区溢出 | API 是平安的,不会造成缓冲区溢出 |
批改字符串长度 N 次必然须要执行 N 次内存重调配 | 批改字符串长度 N 次最多需啊哟执行 N 次内存重调配 |
能够应用所有 <string.h> 库中的函数 | 能够应用一部分 <string.h> 库中的函数 |
以上笔记都是整顿自 <Redis 的设计与实现 >, 真的很感激作者, 也心愿本人能保持写下去 ^_^.
文章来源于自己博客,公布于 2018-02-15,原文链接:https://imlht.com/archives/106/