《Redis设计与实现》02——SDS 简单动态字符串

一、SDS 简单动态字符串介绍

简单动态字符串(simple dynamic string, SDS)
优点:

  • 常数级复杂度获取字符串长度
  • 杜绝缓冲区溢出
  • 减少修改字符串长度时所需的内存重分配次数
  • 二进制安全和保存二进制数据
  • 兼容部分C字符串函数



二、SDS 的定义

每个sds.h / sdshr 结构表示一个SDS值:

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

SDS示例1:
《Redis设计与实现》02——SDS 简单动态字符串_第1张图片

  • free 属性值为0,表示这个SDS没有分配任何未使用空间
  • len 属性值为5,表示这个SDS保存了一个5字节长的字符串
  • buf 属性 是一个char的字节数组,数组的前五个字节分别保存了’R’ ,‘e’ ,‘d’,‘i’, ‘s’, 最后一个字节则保存了空字符 ‘\0’
  • SDS遵循C字符串以空字符串结尾的惯例,保存空字符的1字节空间不计算在SDS的len属性里面,并且添加额外的1字节空间以及向字符串末尾添加空字符都是由SDS函数自动完成

SDS示例2——带有未使用空间:
《Redis设计与实现》02——SDS 简单动态字符串_第2张图片

  • free 属性值为5,表示这个SDS有分配有5个未使用空间
  • len 属性值为5,表示这个SDS保存了一个5字节长的字符串



三、SDS与C字符串的区别

3.1 常数复杂度获取字符串长度

因为C字符串 并不记录自身的长度信息,所有为了获取一个C字符串的长度,程序必须遍历整个字符串,这个操作的时间复杂度为O(N)

而SDS 在len属性中记录了SDS本身的长度,所以获取一个SDS长度的复杂度为O(1)




3.2 杜绝缓冲区溢出

因为C字符串不记录自身长度带来的另一个问题是容易造成缓冲区溢出(buffer overflow) 。

而SDS 的空间分配策略完全杜绝了发送缓冲区溢出的可能性:当SDS的API需要对SDS进行修改时,API 会检查SDS空间是否满足要求,如果不满足,则自动扩展空间到修改所需的大小,然后执行相关操作。即SDS可以 自动修改SDS空间大小,不存在缓冲区溢出问题。




3.3 减少修改字符串时带来的内存重分配次数

因为C字符串的底层实现总是一个n + 1 个字符串的数组(额外的一个字符空间用来保存空字符),内存重分配容易导致:缓冲区溢出和内存泄露问题

而SDS通过未使用空间解除了字符串长度和底层数组长度之间的关联,在SDS中,buf数组的长度不一定就是字符数量+1,数组里面可以包含未使用的字节,其中字节数量就是free属性。

通过未使用空间,SDS实现了空间预分配和惰性空间释放两种优化策略。




1. 空间预分配

用于优化SDS的字符串增长操作:当SDS的API对一个SDS操作时,并进行空间扩展,程序不仅会提供所需修改空间,还会为SDS分配额外的未使用空间。

分为两种情况讨论空间数量分配:

  • 对SDS进行修改之后,SDS的长度(len) 小于1 MB , 那么程序分配和len属性相同的未使用空间。

    例子:修改后,SDS的len变成13字节,那么程序也分配13字节给未使用空间,SDS的buf数组的实际长度为13+13+1 = 27字节(额外1字节保存空字符)

  • 对SDS进行修改之后,SDS的长度(len) 大于1 MB, 那么程序会分配1MB的未使用空间。

    例子:修改后,SDS的len变成30MB字节,那么程序分配1MB给未使用空间,SDS的buf数组的实际长度为30MB + 1MB + 1byte

因此:通过空间预分配策略,Redis可以减少连续执行字符串增长操作所需的内存重分配次数。




2. 惰性空间释放

用于优化SDS的字符串缩短操作:当SDS需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用 free 属性将这些字节的数量记录起来,并等待将来使用。

因此:通过惰性空间释放策略,SDS避免了缩短字符串时所需的内存重分配操作,并为将来可能有的增长操作提供了优化,避免了内存浪费。




3.4 二进制安全

因为 C字符串中必须符合某种编码(如ASCII) ,并且除了字符串的末尾之外,字符串里面不能包含字符,否则最先被程序读入的空字符会被误认为是字符串的结尾,因此C字符串只能保存文本数据。

而Redis为了可以适用于各种不同的使用场景,SDS的API都是二进制安全的,所有SDS 的API都会以处理二进制的方式来处理存放buf数组的数据。因此我们将SDS的buf属性也被称为字节数组的原因。

Redis不是用这个数组来保存字符,而是用它来保存一系列的二进制数据。




3.5 兼容部分C字符串函数

保存文本数据的SDS可以重用一部分库定义的函数




四、 总结

两者的区别

C字符串 SDS
获取字符串长度的复杂度为O(N) 获取字符串长度的复杂度为O(1)
API非安全的,可能造成缓冲区溢出 API是安全的,不会造成缓冲区溢出
修改字符串长度N次必然需要执行N次内存重分配 修改字符串长度N次最多需要执行N次内存重分配
只能保存文本数据 可以保存文本或者二进制数据
可以使用所有库中的函数 可以使用一部分库中的函数



五、SDS 常用API

函数 作用 时间复杂度
sdsnew 创建一个包含给定 C 字符串的 SDS 。 O(N) , N 为给定 C 字符串的长度。
sdsempty 创建一个不包含任何内容的空 SDS 。 O(1)
sdsfree 释放给定的 SDS 。 O(1)
sdslen 返回 SDS 的已使用空间字节数。 这个值可以通过读取 SDS 的 len 属性来直接获得, 复杂度为 O(1) 。
sdsavail 返回 SDS 的未使用空间字节数。 这个值可以通过读取 SDS 的 free 属性来直接获得, 复杂度为 O(1) 。
sdsdup 创建一个给定 SDS 的副本(copy)。 O(N) , N 为给定 SDS 的长度。
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 为被复制 C 字符串的长度。
sdsgrowzero 用空字符将 SDS 扩展至给定长度。 O(N) , N 为扩展新增的字节数。
sdsrange 保留 SDS 给定区间内的数据, 不在区间内的数据会被覆盖或清除。 O(N) , N 为被保留数据的字节数。
sdstrim 接受一个 SDS 和一个 C 字符串作为参数, 从 SDS 左右两端分别移除所有在 C 字符串中出现过的字符。 O(M*N) , M 为 SDS 的长度, N 为给定 C 字符串的长度。
sdscmp 对比两个 SDS 字符串是否相同。 O(N) , N 为两个 SDS 中较短的那个 SDS 的长度。

你可能感兴趣的:(Redis【设计与实现】,redis,数据库,缓存)