Redis系列-深入理解数据结构之String

194 阅读5分钟

大家好,我是小趴菜,已经好久没更新文章,主要是这段时间忙着找工作,现在入职一家做跨境电商的公司担任后端研发工程师,趁着这段时间有时间来更新下文章

redis对于Java后端工程师来说是再熟悉不过了,我们不说它的使用方式了,来了解下它的底层实现

我们从一个面试题介入:为什么Redis的string数据结构不直接使用c++的,而是自己设计了一个SDS的数据结构

Dingtalk_20241016161040.jpg

从redis的源码看到SDS的数据结构

struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* 字符串现有长度 */
    uint64_t alloc; /* 字符串数据已分配的空间 */
    unsigned char flags; /* SDS类型 */
    char buf[];  /* 字符数据 */
};

现在我们看下如果使用c++来存储的话会有什么缺点

1:能存储二进制数据

首先我们创建一个c++项目

screenshot-20241016-203200.png

当我们获取一个字符串的长度的时候,name的长度其实是7,但是打印出来却是5,结果是不对的,那是因为c++在判断一个字符串是否结束,是遍历到 \0 的时候,就认为这个字符串结束了

这时候如果我们的字符串内容本身就包含了 \0,那么就会造成数据的错误了,如果我们要存储图片二二进制数据,这时候二进制数据也是可能包含 \0的,那么这时候就拿不到完整的数据了

2:效率问题

举个例子,我们要获取这个字符串的长度,那么在c++中我们可以使用strlen函数来获取

12.png

strlen函数底层的实现就是遍历整个字符串,直到遍历到 \0结束,这时候返回字符长度,那么这个时间复杂度就是 O(n)

但是Redis的SDS数据结构中,是有一个值记录了这个字符串长度的,所以要获取字符串长度的时间复杂度就是O(1)

struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* 字符串现有长度 */
    uint64_t alloc; /* 字符串数据已分配的空间 */
    unsigned char flags; /* SDS类型 */
    char buf[];  /* 字符数据 */
};

除此之外。这里还有一个flags的类型参数,Redis存储的时候并不是所有的数据都当成一种类型去存储的,而是分了三种类型的

仔细看下面三幅图,都是设置一个key-value,但是发现三个值的编码都是不一样的,有int,embstr,raw,这也是redis中String的三种编码格式

截屏2021-12-25 下午1.44.00.png 截屏2021-12-25 下午1.45.34.png

截屏2021-12-25 下午1.46.19.png 那么这三种编码格式有什么区别呢?

格式区别
int保存long型(长整型)的64位(8个字节)有符号整数,9223372036854775807,这是最大范围,只有整数会使用int,如果是浮点数,Redis内部其实先将浮点数转为字符串,然后仔保存
embstr代表embstr格式的SDS(简单动态字符串), 保存长度小于44字节的字符串
raw保存长度大于44字节的字符串

其实是为了节约内存考虑,int需要的存储成本最低,对于一般的整数类型直接使用int存储就行,可以节省内存空间,至于其它字符串可以使用最低成本来存储对应的内容了

3:内存的动态分配

在c++中,如果内容超出了范围,就需要重新定义长度了,不然就会有异常

33.png

但是Redis的SDS之所以被称为动态字符串,主要有以下两方面

空间预分配:SDS修改后。len长度小于1M,那么将会额外分配与len相同长度的未使用空间,如果修改后长度大于1M,那么将再分配1M的使用空间。。。。。。。。。。。。。。。。。。。。。。

惰性空间释放:有空间分配对应的就会有空间释放,SDS缩短时候并不会回收多余的内存空间,而是使用free字段记录下来,如果后续有变更操作,直接使用free记录的空间,减少内存分配,因为回收分配内存空间都是有消耗的

总结一下

c语言SDS
字符串长度处理需要从头开始遍历,时间复杂度为O(n)记录了当前字符串的长度,时间复杂度为O(1)
内存重新分配分配内存空间超过后,会导致数组下标越界或者内存分配溢出,也就是不会动态的去分配内存空间预分配:SDS修改后。len长度小于1M,那么将会额外分配与len相同长度的未使用空间,如果修改后长度大于1M,那么将再分配1M的使用空间。。。。。。。。。。。。。。。。。。。。。。惰性空间释放:有空间分配对应的就会有空间释放,SDS缩短时候并不会回收多余的内存空间,而是使用free字段记录下来,如果后续有变更操作,直接使用free记录的空间,减少内存分配,因为回收分配内存空间都是有消耗的
二进制安全二进制数据并不是规则的字符串格式,可能包含一些特殊的字符,比如'\O'等 ,c字符串中遇到\O就代表这个字符串结束,那\O之后的数据就读不到了根据len长度判断字符串的结束,二进制安全的问题就解决了
保存数据类型只能保存文本数据类型保存文本还有二进制数据