1

Redis SDS简单动态字符串

 1 year ago
source link: https://www.leftpocket.cn/post/redis/redis_sds/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

原文地址:码农在新加坡的个人博客

SDS (Simple Dynamic String,简单动态字符串)是 Redis 底层所使用的字符串表示, 几乎所有的 Redis 模块中都用了 sds。

本章将对 SDS 的实现、性能和功能等方面进行介绍, 并说明 Redis 使用 SDS 而不是传统 C 字符串的原因。

简单动态字符串(SDS)

什么是SDS

举一个例子, 当我们set一个值时:

redis> SET msg "Redis"
OK

那么Redis将在数据库创建一个新的键值对,其中:

  • 键值对的键是一个字符串对象, 对象的底层实现是一个保存着字符串 “msg” 的 SDS 。
  • 键值对的值也是一个字符串对象, 对象的底层实现是一个保存着字符串 “hello world” 的 SDS 。

我们来查看一下SDS的数据结构: 每个 sds.h/sdshdr 结构表示一个 SDS 值:

struct sdshdr {

    // 记录 buf 数组中已使用字节的数量
    // 等于 SDS 所保存字符串的长度
    int len;

    // 记录 buf 数组中未使用字节的数量
    int free;

    // 字节数组,用于保存字符串
    char buf[];

};

SDS

上图展示了一个SDS示例:

  • free 属性的值为 0 , 表示这个 SDS 没有分配任何未使用空间。
  • len 属性的值为 5 , 表示这个 SDS 保存了一个五字节长的字符串。
  • buf 属性是一个 char 类型的数组, 数组的前五个字节分别保存了 ‘R’ 、 ‘e’ 、 ’d' 、 ‘i’ 、 ’s' 五个字符, 而最后一个字节则保存了空字符 ‘\0’ 。

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

这个时候我想大家应该对SDS有了一个大致的了解。

SDS相对于C字符串有什么好处呢?

1.获取字符串长度复杂度为O(1)

传统的C字符串并没有记录字符串长度,而想获取字符串长度时就需要遍历一遍字符串,复杂度为O(N),导致随着字符串长度变长,获取字符串长度的操作复杂度也会增加。 和C字符串不同,因为SDS在len属性中记录了SDS 身的长度,写代码时只要直接获取len属性值,就可以知道字符串长度,所以获取一个SDS长度的复杂度仅为 O(1) 。

2.防止缓冲区溢出(buffer overflow)

传统C字符串由于不记录自身长度,在对字符串进行操作的时候容易带来缓冲区溢出。 比如 strcpy()函数:strcpy()函数将源字符串复制到缓冲区。如果源字符串碰巧来自用户输入,且没有专门限制其大小,则有可能会造成缓冲区溢出。

再比如/strcat函数,将src字符串拼接到dest字符串后面。strcat假定用户在执行这个函数时, 已经为dest分配了足够多的内存, 可以容纳src字符串中的所有内容,而一旦这个假定不成立时,就会产生缓冲区溢出。

strcat

如果过程中,用户想对s1进行扩展

strcat(s1, "Cluster");

由于没有注意到s预分配的空间大小,执行以后会导致s1的内容缓冲到s2,以至于s2的内容被意外修改成Cluster,这种错误对数据库的使用过程致命的。

而SDS在sdscat时会预先检查空间是否足够,不够的话会通过sdsMakeRoomFor自动为要拼接的字符串扩展空间.

sds sdscatlen(sds s, const void *t, size_t len) {
    size_t curlen = sdslen(s);
    s = sdsMakeRoomFor(s,len);
    if (s == NULL) return NULL;
    memcpy(s+curlen, t, len);
    sdssetlen(s, curlen+len);
    s[curlen+len] = '\0';
    return s;
}

3.减少因修改字符串带来的内存重分配次数 因为 C 字符串的长度和底层数组的长度之间存在着这种关联性, 所以每次增长或者缩短一个 C 字符串, 程序都总要对保存这个 C 字符串的数组进行一次内存重分配操作

SDS对于内存操作实现了空间预分配和惰性空间释放两种优化策略。 1.空间预分配 我们上面提到过,在对字符串进行增长操作时,会使用一个sdsMakeRoomFor函数来扩展字符串。

2.惰性空间释放 惰性空间释放用于优化 SDS 的字符串缩短操作: 当 SDS 的 API 需要缩短 SDS 保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是把缩短后的长度记录在len属性中,剩余空间用于未来扩展字符串用。

4.可以存放二进制数据 C 字符串中的字符必须符合某种编码(比如 ASCII), 并且除了字符串的末尾之外, 字符串里面不能包含空字符, 否则最先被程序读入的空字符将被误认为是字符串结尾 —— 这些限制使得 C 字符串只能保存文本数据, 而不能保存像图片、音频、视频、压缩文件这样的二进制数据。 因此, 为了确保 Redis 可以适用于各种不同的使用场景, SDS 的 API 都是二进制安全的(binary-safe): 所有 SDS API 都会以处理二进制的方式来处理 SDS 存放在 buf 数组里的数据, 程序不会对其中的数据做任何限制、过滤、或者假设 —— 数据在写入时是什么样的, 它被读取时就是什么样。

这也是我们将 SDS 的 buf 属性称为字节数组的原因 —— Redis 不是用这个数组来保存字符, 而是用它来保存一系列二进制数据。

C字符串 SDS
获取字符串长度的复杂度为 O(N) 获取字符串长度的复杂度为 O(1)
操作字符串函数不安全,可能造成缓冲区溢出 安全的操作字符串API,避免缓冲区溢出
修改字符串长度 N 次必然需要执行 N 次内存重分配 修改字符串长度 N 次最多需要执行 N 次内存重分配
只能保存文本数据 可以保存文本以及图片、音频、视频、压缩文件这样的二进制数据。

<全文完>

欢迎关注我的微信公众号:码农在新加坡,有更多好的技术分享。

pic

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK