大家好,我是小趴菜,已经好久没更新文章,主要是这段时间忙着找工作,现在入职一家做跨境电商的公司担任后端研发工程师,趁着这段时间有时间来更新下文章
redis对于Java后端工程师来说是再熟悉不过了,我们不说它的使用方式了,来了解下它的底层实现
我们从一个面试题介入:为什么Redis的string数据结构不直接使用c++的,而是自己设计了一个SDS的数据结构
从redis的源码看到SDS的数据结构
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* 字符串现有长度 */
uint64_t alloc; /* 字符串数据已分配的空间 */
unsigned char flags; /* SDS类型 */
char buf[]; /* 字符数据 */
};
现在我们看下如果使用c++来存储的话会有什么缺点
1:能存储二进制数据
首先我们创建一个c++项目
当我们获取一个字符串的长度的时候,name的长度其实是7,但是打印出来却是5,结果是不对的,那是因为c++在判断一个字符串是否结束,是遍历到 \0 的时候,就认为这个字符串结束了
这时候如果我们的字符串内容本身就包含了 \0,那么就会造成数据的错误了,如果我们要存储图片二二进制数据,这时候二进制数据也是可能包含 \0的,那么这时候就拿不到完整的数据了
2:效率问题
举个例子,我们要获取这个字符串的长度,那么在c++中我们可以使用strlen函数来获取
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的三种编码格式

那么这三种编码格式有什么区别呢?
| 格式 | 区别 |
|---|---|
| int | 保存long型(长整型)的64位(8个字节)有符号整数,9223372036854775807,这是最大范围,只有整数会使用int,如果是浮点数,Redis内部其实先将浮点数转为字符串,然后仔保存 |
| embstr | 代表embstr格式的SDS(简单动态字符串), 保存长度小于44字节的字符串 |
| raw | 保存长度大于44字节的字符串 |
其实是为了节约内存考虑,int需要的存储成本最低,对于一般的整数类型直接使用int存储就行,可以节省内存空间,至于其它字符串可以使用最低成本来存储对应的内容了
3:内存的动态分配
在c++中,如果内容超出了范围,就需要重新定义长度了,不然就会有异常
但是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长度判断字符串的结束,二进制安全的问题就解决了 |
| 保存数据类型 | 只能保存文本数据类型 | 保存文本还有二进制数据 |