带你深入了解Redis字符串数据结构

    

1 前言

  Redis数据库里面的每个键值对都是对象组成的,其中:
  • 数据库键总是一个字符串对象(string object);
  • 而数据库键的值则可以是字符串对象(string)、列表对象(list)、哈希对象(hash)、集合对象(set)、有序集合对象这五种对象中的一种。
  字符串对象作为Redis五种数据结构中最常用的一种,我们有必要了解该对象的底层数据结构,底层数据结构又是如何深刻地影响对象的功能和性能的,以便我们更好地实现使用它或者出现错误的时候更好地排查。

2 正文

  Redis没有直接使用C语言传统的字符串表示,而是自己构建了一种名为简单动态字符串(simple dynamic string, SDS)的抽象类型,并将SDS用作Redis的默认字符串表示。

 2.1 SDS的底层数据结构

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

 2.2 SDS对象与C语言字符串对象的异同点:

   相同点:

     1、以空字符 '\0' 结尾,并为空字符分配额外的空间(不算在字符长度内)
 

   不同点:

     1、C语言字符串对象底层的数据结构是字符数组,SDS对象底层数据结构是字节数组
     2、SDS设置len保存字符串长度,C语言字符串通过遍历数组获得字符串长度
     3、SDS设置free记录buf数组未使用字节的长度
 

 2.3 SDS对象的优点

  1、常数复杂度获取字符串长度

  C字符串并不记录自身的长度信息,获取一个C字符串的长度,程序必须遍历整个字符串,对遇到的每个字符进行技术,直到遇到代表字符串结尾的空字符为止,这个操作的复杂度为O(N)。
SDS在len属性中记录了SDS本身的长度,获取一个SDS长度的复杂度仅为O(1)。

  2、杜绝缓冲区溢出

  C字符串不记录自身的长度带来的另一个问题是容易造成缓冲区溢出。
 
  而当SDS API需要对SDS进行修改时,API会先检查SDS空间是否满足修改所需的要求,如果不满足的话,API会自动将空间扩展至执行所需的大小,然后才执行实际的修改操作。

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

  C字符串每次增长或者缩短一个C字符串时,程序都要对保存这个C字符串的数组进行一次内存重分配操作。
  SDS实现了空间预分配和惰性空间释放两种内存分配策略,避免每次修改字符串都执行一次内存重分配。
  • 空间预分配:空间预分配用于优化SDS的字符串增长操作。当SDS需要空间扩展时,程序不仅会为SDS分配修改所必须要的空间,还会为SDS分配额外的未使用空间。未使用空间数量由如下公示决定:
    • 如果对SDS进行修改之后,SDS的长度(len的值)小于1mb,那么就预分配和原数组长度大小的未使用空间(即free = len)。
    • 如果对SDS进行修改之后,SDS的长度(len的值)大于1mb,那么就预分配1mb的未使用空间。举个例子,如果进行修改之后,SDS的len将变成30mb,那么程序分配1mb的未使用空间,buf数组的实际长度将为30mb + 1mb + 1byte。
    • 通过空间预分配策略,Redis可以减少连续执行字符串增长操作所需的内存分配次数。
  • 惰性空间释放:当执行SDS字符数组的缩减时,程序并不立即回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用。通过惰性空间释放策略,SDS避免了缩短字符串所需的内存重分配操作,并为将来可能有的增长操作提供了优化。与此同时,SDS也提供了相应的API,在有需要时真正去释放SDS的未使用空间,所以不用担心该策略造成内存浪费。
 

  4、二进制安全

  不同于C字符串的只能保存符合某种编码(比如ASCII)字符的字符数组,Redis所以SDS API会以二进制的方式来处理SDS存放在buf数组里的数据,程序不会对数组做任何的限制、过滤,数组写入时是什么样的,它被读取是就是什么样,这也是为什么将SDS的buf属性称为字节数组的原因。Redis不仅可以保存文本数据,还可以保存任意格式的二进制数据。
 

  5、兼容部分C字符串函数

  SDS遵循C字符串以空字符结尾的惯例,是为了让那些保存文本数据的SDS可以重用一部分<string.h>库定义的函数。
  举个例子,如果我们有一个保存“Redis”文本数据的SDS值sds,那么我们就可以重用<string.h>/strcasecmp函数,使用它来对比另一个C字符串:
    strcasecmp(sds->buf, "hello world");
   这样Redis就不用自己专门去写一个函数来对比SDS值和C字符串值了。
 

3 总结

  • Redis只会使用C字符串作为字面量,在大多数情况下,Redis使用SDS作为字符串表示。
  • 比起C字符串,SDS具有以下优点:
    • 常数复杂度获取字符串长度
    • 杜绝缓冲区溢出
    • 减少修改字符串时带来的内存重分配次数
    • 二进制安全
    • 兼容部分C字符串函数
 
 

posted on 2020-10-18 17:43  pufeng  阅读(326)  评论(0编辑  收藏  举报

导航