Redis常用数据结构之字符串

345 阅读6分钟

前言

你要是问我,在程序世界中我们程序员打交道最多的一种数据类型是什么?

我肯定会回答你是 字符串

那么在Redis中是如何实现的这种字符串数据结构呢?在Java中我们有 java.lang.String类,在C语言中通过char[]字符数组实现的字符串,众所周知Redis是由C语言实现的,那么Redis中的字符串就是通过C语言的char[]字符数组实现的吗?

不是的,Redis是通过一个名叫SDS(Simple Dynamic String)简单动态字符串实现的

接下来,我们就来研究研究,为什么不直接使用C字符串,而是要在实现一套Redis自己的字符串数据结构。请系好安全带,马上发车!

什么是SDS

代码定义

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

上面这个名为sdshdr的结构体就是Redis中SDS的定义,很简单的对吧

下面使用两幅插图,形象的展示一下SDS中存放数据的时候是个什么样子的

在这里插入图片描述

​ 图1

在图1中我们可以看到,当存放一个叫做‘Redis’这样的一个字符串的时候,在SDS中的一个内存分布的情况

  • free属性的值为0,表示这个SDS没有分配任何未被使用的空间
  • len属性的值为5,表示这个SDS的长度为5个字节
  • buf属性是一个char字符数组,数组的前5个字节分别保存了Redis字符串的5个字符,最后一个字节保存了空字符‘\0’

SDS遵守C字符串的规定,在字符串的末尾保存一个字节的‘\0’表示字符串结束,遵守这规定的好处是,SDS可以直接使用C字符串函数库中的一部分函数

在这里插入图片描述

图2

在图2中我肯可以看到和图1的最大的区别就是多出了3个分配未被使用的字节空间,这种空间预分配的策略正是SDS和C字符串实现上的最大的不同之处。

SDS与C字符串的区别

传统的C字符串使用长度N+1的字符数组表示一个长度为N的字符串,并且字符串以'\0'结尾。

C语言使用这种简单的字符串表示方式,并不能满足Redis在对字符串的安全性,效率以及功能方面的要求,所以SDS需要去解决C字符串不能满足的这些问题。

获取字符串长度时效率不同

在SDS中因为存储了len表示字符串长度的属性,所以可以以O(1)的时间复杂度获取字符串的长度;在C字符串中,需要遍历char[]字符数组才能获取到字符串的长度,时间复杂度为O(N);

避免缓冲区溢出

在C字符串中,如果用户在执行strcat函数的时候,会存在内存覆盖的问题。比如有两个字符串,它们在内存中刚好是连续分布存在的。如下图所示:

在这里插入图片描述

​ 图3

有S1 ‘Bob’,S2 ‘Dave’,当执行 strcat(s1,"niu")的时候,那么就会出现如下情况

在这里插入图片描述

​ 图4

字符串S2原本的值被覆盖了,这种现象又被称为缓冲区溢出。SDS为了避免这种情况,在修改的时候,会先检查SDS的空间是否满足修改的所需要求,如果不满足的话,会自动将SDS的空间扩展至执行修改所需的大小,然后才执行实际的修改操作。这样就避免了缓冲区溢出的问题。

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

由于C字符串中不会保存自身的长度,所以它的长度总是和被使用的字节数相等,就会导致了每次修改字符串的时候,都会重新分配一块连续的内存空间,然后在修改。内存分配是一件耗时的操作,如果一个字符串被修改的频率不高,那么使用这种方式也是Ok的,但是Redis作为一款数据库它是需要解决这种对数据频繁修改的问题的。

在SDS中通过实现了空间预分配惰性空间释放这两种优化策略

空间与分配

空间预分配用于优化SDS增长的操作:当对一个SDS进行修改的时候,在需要对SDS进行空间扩展的时候,不仅为SDS分配修改所必要的空间,还会对SDS分配未使用的空间。

其中未被使用的空间分配数量算法如下:

  • 如果对SDS进行修改之后,SDS的长度小于1MB,那么程序分配和len属性同样大小未使用空间,这时SDS的len属性的值将和free属性的值相同。比如说在在修改之后SDS的len变为了10个字节,还会分配10个字节作为未使用空间。那么在buf数组的实际长度成为了10+10+1=21
  • 如果对SDS进行修改之后,SDS的长度大于1MB,那么会分配1MB的未使用空间。比如说当对一个SDS修改之后,len变为了30M,那么还会再分配1MB的字节空间作为未被使用的空间。在bug数组中的实际长度变为了30MB+1MB+1字节

惰性空间释放

惰性空间释放用于优化SDS的字符串缩短的操作。当一个SDS需要缩短的时候,并不会立即使用内存重分配来回收缩短后的字节,而是使用free属性将这些字节数量记录下来,并等待将来使用

二进制安全

在C字符串中,因要求在字符串中除了在末尾,其它位置上不能包含空字符,否则就会被误识别为字符串结尾。这个限制了C字符串只能保存文本数据,不能存储二进制的数据,例如视频、音频等。

SDS不会有任何限制,当数据被存放进去的时候是什么样子,在取出来的时候就是什么样子的。

总结

总结一下C字符串和SDS字符串的主要区别

C字符串SDS字符串
获取字符串长度时间复杂度O(N)获取字符串长度时间复杂度O(1)
API安全,会造成缓冲区溢出API安全,会造成缓冲区溢出
修改字符串长度N次,必然进行N次内存重新分配修改字符串长度N次,最多进行N次内存重新分配
只能保存文本数据能保存文本、视频等二进制数据
能使用<string.h>所有的函数能使用<string.h>部分的函数