Redis-简单动态字符串(SDS)

文章目录

    • 文章概要
    • 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 存储字符串的字节数组(这里称之为字节数据,而不是字符数组)

Redis-简单动态字符串(SDS)_第1张图片

SDS和C字符串的区别

常数时间复杂度获取字符串的长度
这一点是显而易见的,因为在结构体的内部维护了一个变量len来记录实际字符串的长度,而在C语言中, 我们不得不遍历字符串才能获取字符串的长度。

杜绝缓冲区溢出
首先在C语言中,我们如果想在一个字符串后面追加字符串时,如果预先知道该区域还有多少可用的空间,将很容易发生缓冲区溢出的错误。
举个例子:

#include 
#include 
#include 

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的未使用空间。

举个例子:

  1. 假设我们要存储一个字符串 “Redis”,此时将分配 11 Bytes = 5(len) + 5(free) + 1(null terminator)的空间
  2. 假设我们要存储的字符串为"…"(20MB),此时将分配 22 Bytes = 20(len) + 1(free) + 1(null terminator)

惰性空间释放策略,SDS避免了缩短字符串时所需的内存重分配操作,并为将来可能有的增长操作提供了优化。
具体来讲,当我们对现存的SDS字符串进行裁剪后,被裁剪的字符串所占用的空间不会被立即释放,当操作系统需要使用的时候才会释放。

二进制安全的
这一点比较容易理解,在C语言字符串是不允许有空字符的,否则程序将编译报错。所以C语言中的char性数组只能用来存储字符数据,无法存储图像、视频等二进制数据。
在Redis的SDS中,虽然也同样遵循在末尾使用’\0’来表示字符串结尾,但是在中间允许有空字符的存在,所以这也是为啥将SDS的buf称为字节数组。

总结

本篇文章分享了Redis的字符串编码结构SDS,了解了它的定义和内存分配策略。以及和C语言字符串的区别。希望你能从本篇文章中获取新的东西。

参考

《Redis设计与实现》

你可能感兴趣的:(Redis,redis,数据库)