Redis学习笔记-简单动态字符串SDS

  1. 引言
    redis在我日常工作中使用的频率相当高,每个项目基本都会用到。在redis的使用过程中,字符串使用的频率也是相当的高,所以有必要学习redis的字符串的相关知识。文中涉及的代码为redis3.0版本

  2. 简单了解SDS(Simple Dynamic String 简单动态字符串)

    1. redis使用sds进行字符串的表示,没有使用C默认的char*类型,char*的功能单一,抽象层次低,不能高效的支持一些redis的常用操作,如追加和长度计算
    2. SDS数据结构
      struct sdshdr {
          
          // 记录buf已使用的字节数量
          // 即已保存的字符串的长度
          int len;
      
          // buf 中剩余可用的字节数量
          int free;
      
          // 字节数组,用于保存字符串
          char buf[];
      };
      
    3. SDS示例
      SDS示例
    	free:0,表示sds没有未使用空间
    	len:5,表示保存的字符串长度是5
    	buf:char数据,保存了R、e、d、i、s,buf其实保存了5个内容字符,还有一个\0结束字符,
    			但是\0不会被记录长度到len当中	
    

    4.结束符疑问
    为什么还要浪费1个字节空间去记录一个\0结束字符呢?
    因为C字符串是以空字符\0结尾的,sds遵循C 字符串以空字符结尾的惯例,并且不会把\0记录到len的长度当中,在分配空间时会额为分配1个字节去记录该字符,整个的操作都是sds的函数自动完成的,使用者无需关心。
    使用空字符\0结尾的好处是,可以使用C字符串函数库里面的函数

  3. SDS和C字符串
    3.1.C字符串
    C字符串“Redis”
    C字符串的结构如上所示,C字符串不能满足Redis对字符串的安全性、效率以及功能方面的需求,所以redis才会使用sds
    3.2. C字符串的问题

    1. 获取字符串长度,获取一个C字符串的长度,程序必须遍历整个字符串,直到遇到结尾的空字符\0为止,时间复杂度为O(n)
    2. 缓冲区溢出,如果存在两个字符串s1和s2,两个字符串是紧邻的如下图所示,其中s1内容为Redis,s2内容为MongoDB,内存结构图如下所示:
      s1和s2相邻的字符串
      此时对s1进行append操作,使用如下函数:
      char *strcat(char *dest, const char *src);
      执行:
      strcat(s1, " Cluster");
      

    但是append前并没有对s1分配足够的内存空间就会导致s1的数据溢出到s2,导致s2的内容被修改。如下图所示:
    s1内容溢出到s2

    1. 内存频繁分配和回收内存,当对字符串频繁的修改时,C字符串需要频繁的分配和回收内存。
    2. 二进制安全 ,C 字符串中的字符必须符合某种编码(比如 ASCII), 并且除了字符串的末尾之外, 字符串里面不能包含空字符, 否则最先被程序读入的空字符将被误认为是字符串结尾。举个栗子:
      Redis Cluster
      上图的字符串使用了空字符分隔,此时C字符串所用的函数只能识别其中的 “Reids”,会忽略后面的"Cluster"

    3.3 SDS如何解决这些问题

    1. 获取字符串的长度,在SDS的结构体当中使用了len属性记录字符串内容的长度,sds获取长度的代码如下:

      /*
       * 返回 sds 实际保存的字符串的长度
       *
       * T = O(1)
       */
      static inline size_t sdslen(const sds s) {
          struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
          return sh->len;
      }
      
    2. 杜绝内存溢出,当 sds api 需要对 sds 进行修改时, api会先检查 sds的空间是否满足修改所需的要求, 如果不满足的话, api会自动将 sds 的空间扩展至执行修改所需的大小, 然后才执行实际的修改操作。

    3. 减少修改字符串时带来的内存分配次数,C字符串修改字符串需要重新分配内存,否则会导致内存溢出,SDS采取空间预分配惰性空间释放方法从而较少内存分配次数
      空间预分配:

      1. sds api对sds修改时,如果此时需要对sds的空间进行扩展,sds不仅会分配当前操作所需要的空间,还会为sds分配额外的未使用空间,这样redis在后续执行字符串增长操作到时候可以减少内存重新分配的次数。
      2. 对sds修改后,如果len小于1M,则程序分配的和len相同的大小未使用空间,也就说此时len和free值是相等的。举个栗子:对sds的修改,sds的len变成了13字节,程序会分配13字节的未使用空间,则sds的buf数组的实际长度为13+13+1 = 27字节,即len+free+空字符=27字节
      3. 对sds修改后,如果len大于1MB,则程序会分配1MB的未使用空间,举个栗子:sds修改后为3MB,那么程序会分配1MB的未使用空间,则buf数组的实际长度为3MB+1MB+1byte
      4. 通过这种预分配策略, sds 将连续增长 N 次字符串所需的内存重分配次数必定 N 次降低为最多 N 次。(因为free的空间有可能不满足当前修改操作的所需空,所以是降低为最多N次)

      惰性空间释放:

      1. sds的api对sds的缩短操作时,程序不会立即回收内存,而是使用free属性将这些需要回收字节的数量记录起来,等待将来使用。字符串”XYXXYabcXYY”移除所有的“X”和“Y”,过程如下图所示。
        字符串”XYXXYabcXYY”
        字符串”XYXXYabcXYY”移除所有的“X”和“Y”
        如上图所示,sds并没有释放多出来的 8 字节空间, 而是将这 8 字节空间作为未使用空间保留在了 sds里面, 如果将来要对 sds进行增长操作的话, 这些未使用空间就可能会派上用场
    4. 二进制安全,sds的api都是二进制安全的,所有 SDS API 都会以处理二进制的方式来处理 SDS 存放在 buf 数组里的数据, 程序不会对其中的数据做任何限制、过滤、或者假设 —— 数据在写入时是什么样的, 它被读取时就是什么样。

  4. 总结
    sds比起c字符串有以下优点:

    1. 常数复杂度获取字符串长度
    2. 杜绝缓冲区溢出
    3. 减少修改字符串长度时所需的内存重分配次数
    4. 二进制安全
    5. 兼容部分 C 字符串函数
  5. 参考资料
    <<Redis设计与实现>>

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值