• 深入了解Redis(1)字符串底层实现


    一.简单动态字符串(SDS)

      Redis中字符串实现有两种方式,C语言传统字符串(以空字符结尾的字符数组)和简单动态字符串(SDS),并将SDS作为默认字符串表示.

      C字符串只会作为字符串字面量,用在一些无需对字符串值进行修改的地方,比如打印日志:

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

    二.SDS的实现

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

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

    图 2-1 展示了一个 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与C字符串的区别

      c字符串是由长度为n+1的字符串数组实现的,并且数组的最后一个值总是为空字符'\0'.

    比如说, 图 2-3 就展示了一个值为 "Redis" 的 C 字符串:

    1.SDS可以更快的获取字符串长度

      因为c字符串的数据结构为数组,所以也继承了数组的基本特性,比如获取该字符串的长度,需要去遍历整个数组,该操作的复杂度为O(N).

      而SDS本身就维护了len属性记录了字符串的长度,所以获取SDS字符串长度的操作复杂度为O(1).

    2.杜绝缓冲区溢出

      c字符串容易造成缓冲区溢出,因为c字符串本身不记录自身长度,比如<string.h>/strcat 函数拼接字符串时,可能导致内存空间不足.

      SDS的空间分配策略完全杜绝了发生缓冲区溢出的可能性,当 SDS API 需要对 SDS 进行修改时,API 会先检查 SDS 的空间是否满足修改所需的要求,如果不满足的话,API 会自动将 SDS 的空间扩展至执行修改所需的大, 然后才执行实际的修改操作,所以使用 SDS 既不需要手动修改 SDS 的空间大小,也不会出现前面所说的缓冲区溢出问题.

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

      因为 C 字符串并不记录自身的长, 所以对于一个包含了 N 个字符的 C 字符串来说,这个 C 字符串的底层实现总是一个 N+1 个字符长的数组.在对字符串的增长或者缩短操作中,很容易造出内存溢出和内存泄漏.

      为了避免 C 字符串的这种缺陷,SDS 通过未使用空间解除了字符串长度和底层数组长度之间的关联:在 SDS 中,buf 数组的长度不一定就是字符数量加一,数组里面可以包含未使用的字节,而这些字节的数量就由 SDS 的 free 属性记录.通过未使用空间,SDS 实现了空间预分配和惰性空间释放两种优化策.

    4.空间预分配 

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

    5.惰性空间释放

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

    6.二进制安全

      C 字符串中的字符必须符合某种编码(比如 ASCII),并且除了字符串的末尾之外,字符串里面不能包含空字符,否则最先被程序读入的空字符将被误认为是字符串结尾 —— 这些限制使得 C 字符串只能保存文本数据,而不能保存像图片、音频、视频、压缩文件这样的二进制数据.

      为了确保 Redis 可以适用于各种不同的使用场景,SDS 的 API 都是二进制安全的(binary-safe): 所有 SDS API 都会以处理二进制的方式来处理 SDS 存放在 buf 数组里的数据,程序不会对其中的数据做任何限制、过滤、或者假设 —— 数据在写入时是什么样的,它被读取时就是什么.这也是我们将 SDS 的 buf 属性称为字节数组的原因 —— Redis 不是用这个数组来保存字符, 而是用它来保存一系列二进制数据.比如, 使用 SDS 来保存之前提到的特殊数据格式就没有任何问题,因为 SDS 使用 len 属性的值而不是空字符来判断字符串是否结束.

    7.兼容部分c字符串函数

      虽然 SDS 的 API 都是二进制安全的,但它们一样遵循 C 字符串以空字符结尾的惯例:这些 API 总会将 SDS 保存的数据的末尾设置为空字符,并且总会在为 buf 数组分配空间时多分配一个字节来容纳这个空字符,这是为了让那些保存文本数据的 SDS 可以重用一部分 <string.h>库定义的函数.

    总结区别:

    C 字符串SDS
    获取字符串长度的复杂度为 O(N) 。 获取字符串长度的复杂度为 O(1) 。
    API 是不安全的,可能会造成缓冲区溢出。 API 是安全的,不会造成缓冲区溢出。
    修改字符串长度 N 次必然需要执行 N 次内存重分配。 修改字符串长度 N 次最多需要执行 N 次内存重分配。
    只能保存文本数据。 可以保存文本或者二进制数据。
    可以使用所有 <string.h> 库中的函数。 可以使用一部分 <string.h> 库中的函数。
  • 相关阅读:
    springboot解析带控制字符\"的json字符串
    SpringBoot使用JSR356 Websocket使用configurator = SpringConfigurator.class自动注入Spring IOC的bean报错: Failed to find the root WebApplicationContext. Was ContextLoaderListener not used?
    REST API 调用新方法
    mac终端提示zsh: operation not permitted:怎么办?
    (6.1)分栏布局
    (6.2)弹性布局
    比较器 Comparison 与 IComparer
    requireJS 的回顾
    分享刚出炉的基于Blazor技术的Web应用开发框架
    架构必备技能第一谈
  • 原文地址:https://www.cnblogs.com/iceggboom/p/13424953.html
Copyright © 2020-2023  润新知