Redis加速器--SDS

392 阅读5分钟

简述

相信大家在面试的时候经常会被问到一个比较开放性的问题,redis为什么这么快?在系统学习redis之前,我在网上搜到此问题的答案往往是因为redis基于内存、redis的io多路复用模型,redis是单线程的减少了上下文切换等等。但是这些答案显得比较广泛,没法让面试者眼前一亮的感觉。所以在此给大家带来一个新的视角,从字符串对象(String)来看在数据结构层面提供给redis如此快的原因。

何为SDS

SDS -- Simple Dynamic String(简单动态字符串)在redis中的主要用途:

  • 字符串对象String的底层实现就是SDS
  • Redis中表示一个可被修改的字符串,就会使用SDS
  • AOF模块中的AOF缓冲区,以及客户端状态中输入缓冲区都是由SDS实现。

SDS定义

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

}

比如在添加一个字符串"Kobe"的SDS数据结构就会如下图所示

image.png

  • free属性值为0,表示这个SDS没有分配任何未使用空间,这个参数在之后要讲的「内存预分配」会大有用处
  • len的属性值为4,表明这个SDS保存了一个长度为4字节的字符串(不算最后空字符'\0')
  • buf属性就是存储真实数据字节。
  • 使用'\0'作为结束符,与C字符串表示方式一致

C字符串的较量

就如上面所述的结构,采用'\0'的方式表示一个字符串的结尾,好处在于可以直接重用一部分C语言的字符串函数库。 可需要详细介绍的是与C字符串不同的地方。

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

正如之前介绍字符串长度所介绍的len属性可以直接表示SDS的长度,比起C语言需要遍历字符串才能计算出字符串长度。SDS通过此种方式将获取字符串长度的时间复杂度变成了常数级别,也就是使用空间换时间的方式。

杜绝缓冲区溢出

打个比方说,我们要将之前的字符串'Kobe'改为'KobeBryant',调用stract(s1, 'Bryant')进行拼接,如果在拼接之前忘记为新的字符串分配足够的空间,就会造成缓冲区溢出的情况,也就可能会发生对其他数据的一个意外篡改。那么redis是怎么做的呢?

redis的拼接过程是这样的,调用stract(s1, 'Bryant')的时候会先检查给定的字符串空间是否足够(len+free的方式去判断,因为有记录长度所以减少了遍历获取的操作),如果发现空间不足够,就会扩展到足够的空间之后,在执行'Bryant'的操作,同时很重要的一个步骤就是还会为SDS分配10字节的未使用空间。操作如下图:

image.png

可以观察到不仅length数值变到了10,free数值也变成了10,至于为何要这样操作,请耐心往下看「空间预分配」

修改字符串时减少内存重分配

redis在实际业务使用中数据经常会频繁修改且对速度要求严苛,那么每次修改都需要执行一次内存重分配,很明显这样的情况是我们不愿意看到的。那么redis利用了free属性即未使用空间采取「内存预分配」、「惰性空间释放」的两种优化策略。

内存预分配

总结来说就是在增长操作中,使用空间换时间的方式的优化。redis在扩展的时候,不仅会为SDS分配修改所必须要的空间,还会为SDS分配额外的未使用空间。可以理解成为了未来可能的修改进行内存的预分配。执行的策略逻辑是这样的:

如果对SDS进行修改之后,SDS的长度(即len属性的值)小于1MB,那么程序分配和len属性同样大小的未使用空间,这时SDS的length属性值和free属性值相同

如果对SDS进行修改之后,SDS的长度(即len属性的值)大于等于1MB,那么程序分为未分配空间设置1MB的内存空间。

惰性空间释放

总体来说就是释放用于优化SDS字符串的缩短操作。即将'KobeBryant'缩短为'Kobe'的时候并不立即使用内存重分配回收缩短后多出来的字节空间,而是使用free属性(未使用空间)记录下来,以待后续使用。如下图所示

image.png

二进制安全

buf[]属性存储的是二进制数据,所以可以存储图片、视频、音频、压缩文件这样的二进制数据。

总结如下

  • Redis在大多数情况下将SDS作为字符串表示。
  • 兼容部分C字符串函数
  • 常数复杂度获取字符串长度
  • 杜绝缓冲区溢出
  • 修改字符串时减少内存重分配的优化策略: 「内存预分配」、「惰性空间释放」
  • 二进制安全

ps:今天是2021-8-24,以"Kobe"的名字为例作为缅怀科比的一种方式,希望「传奇永不朽」