文章目录
- 文章概要
- SDS数据结构定义
- SDS和C字符串的区别
- 总结
- 参考
文章概要
本篇文章,我们来学习Redis字符串的编码格式SDS编码,文章将将从以下几个方面介绍SDS:
- SDS的底层数据结构定义
- Redis是C写的,那SDS和C中的字符串的区别是什么
SDS数据结构定义
//redis/deps/sds.h
/* Note: sdshdr5 is never used, we just access the flags byte directly.
* However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) hisdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) hisdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) hisdshdr16 {
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) hisdshdr32 {
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) hisdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
以上代码来自Redis的源码,其中包含了redis关于sds字符串编码的定义。从源码中可以看出,redis定义了5种sds的编码,但是hisdshdr5
被声明是没有被使用的,它的定义个后面的四种也略显区别。根据注释的描述,该结构只是为了展示 5 型sds的布局结构,flags的低3位表示类型,高5位表示字符串的长度。
重点看后面的几种定义。
各个字段的含义:
- len 表示实际字符串的长度,比如我们存入一个"HELLO",len = 5
- alloc 表示实际分配给buf的内存空间大小,但是不包括头和空结束符
- flags 只使用低三位作为有效位,表示sds类型(因为有四种,所以要三位,0不算),高5位未使用
- buf 存储字符串的字节数组(这里称之为字节数据,而不是字符数组)
SDS和C字符串的区别
常数时间复杂度获取字符串的长度
这一点是显而易见的,因为在结构体的内部维护了一个变量len来记录实际字符串的长度,而在C语言中, 我们不得不遍历字符串才能获取字符串的长度。
杜绝缓冲区溢出
首先在C语言中,我们如果想在一个字符串后面追加字符串时,如果预先知道该区域还有多少可用的空间,将很容易发生缓冲区溢出的错误。
举个例子:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char **argv)
{
int size = sizeof(char) * 7;
char *dest = (char*)malloc(size);
memset(dest, 0, size);
const char *src = "HelloWorld";
// 此时将发生缓冲区溢出,因为追加的字符串超出了dest所申请的内存
char *result = strcat(dest, src);
free(dest);
return 0;
}
还有一种情况,假设一个字符串s1和另一个字符串s2相邻存储的,当我们向s1后面追加字符串时,将意外地修改s2的值。
对于SDS存储的字符串,当需要修改值时,相关的API会先检查该SDS所分配的空间是否满足此次修改,如果不满足,将自动执行扩容操作。所以对于SDS存储的字符串,杜绝了缓冲区溢出的可能性。
减少字符串修改时内存重分配的次数
在C语言中,字符串底层实际上是一个字符数组,数组的长度为字符串的长度+1,当对该字符串进行追加和缩减的时候,程序都要对其进行重新分配内存。内存分配涉及系统调用,会发生用户态和内核态的切换,如果大量的这种操作将严重影响程序的性能。
在Redis中,为了减少内存分配的次数,采用了两种内存空间分配的优化策略:
- 空间预分配
- 空间惰性删除
空间预分配技术用于优化字符串的增长操作,当程序为字符串分配空间时,不仅会为它分配字符串大小的空间,而且会分配一些未使用的空间,策略如下:
- 如果修改后字符串的大小小于1MB,则此时未使用的空间和len的大小相等。也就是多分配一倍。
- 和上面相对应的,如果修改后字符串的大小大于1MB,此时程序会为该SDS分配1MB的未使用空间。
举个例子:
- 假设我们要存储一个字符串 “Redis”,此时将分配 11 Bytes = 5(len) + 5(free) + 1(null terminator)的空间
- 假设我们要存储的字符串为"…"(20MB),此时将分配 22 Bytes = 20(len) + 1(free) + 1(null terminator)
惰性空间释放策略,SDS避免了缩短字符串时所需的内存重分配操作,并为将来可能有的增长操作提供了优化。
具体来讲,当我们对现存的SDS字符串进行裁剪后,被裁剪的字符串所占用的空间不会被立即释放,当操作系统需要使用的时候才会释放。
二进制安全的
这一点比较容易理解,在C语言字符串是不允许有空字符的,否则程序将编译报错。所以C语言中的char性数组只能用来存储字符数据,无法存储图像、视频等二进制数据。
在Redis的SDS中,虽然也同样遵循在末尾使用’\0’来表示字符串结尾,但是在中间允许有空字符的存在,所以这也是为啥将SDS的buf称为字节数组。
总结
本篇文章分享了Redis的字符串编码结构SDS,了解了它的定义和内存分配策略。以及和C语言字符串的区别。希望你能从本篇文章中获取新的东西。
参考
《Redis设计与实现》