Redis String 类型的底层实现是什么?(SDS)

Sherwin.Wei Lv7

Redis String 类型的底层实现是什么?(SDS)

回答重点

Redis 中的 String 类型底层实现主要基于 SDS(Simple Dynamic String 简单动态字符串)结构,并结合 int、embstr、raw 等不同的编码方式进行优化存储。

扩展知识

C 语言字符串的缺陷

Redis 为什么没有使用 C 标准库提供的字符串,而是实现了一种动态字符串?因为 C 语言的字符串本质上就是 char* 的字符数组,存在一定缺陷:

  • C 语言字符数组的结尾位置就用“\0”表示,意思是指字符串的结束
  • C 语言字符数组获取长度只能通过遍历获得,时间复杂度是 O(n)
  • 字符串操作函数不高效且不安全,比如缓冲区溢出,其可能导致程序异常终止

对比 C 语言字符串

特性 SDS(Redis 动态字符串) C语言原生字符串
结构 自定义结构,包含长度、分配空间等元信息 \0 结尾的字符数组
长度记录 独立记录字符串长度,获取长度为 O(1) 通过遍历计算字符串长度,获取长度为 O(n)
二进制安全 支持任意二进制数据,包括 \0 不支持,遇到 \0 时认为字符串结束
动态扩展 自动扩展或缩减内存,减少内存分配次数 无动态扩展能力,需手动管理内存
内存分配策略 使用 预分配策略,预留额外空间以减少扩展时的内存重新分配次数 每次重新分配内存需要拷贝数据
内存碎片 减少碎片,扩展时预留空间,缩减时可以主动释放多余内存 手动分配内存,易造成碎片
API安全性 提供丰富且安全的操作接口(避免越界、溢出) 使用不当易越界或内存溢出
多种数据存储 支持存储普通字符串、二进制数据或其他自定义结构 仅支持以 \0 结尾的字符序列
内存浪费 可能浪费少量预分配的内存 没有预分配,可能节省内存

sds 进一步解析

SDS 底层结构是 sdshdr,在 redis 4.x 及以上版本引入了 sdshdr 变种,如 sdshdr16、sdshdr64 等,根据字符串长度动态选择不同的实现,进一步优化内存使用。

例如以下的 sdshdr64

1
2
3
4
5
6
struct __attribute__ ((__packed__)) sdshdr64 {
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[];
};

1)len (长度):记录了 SDS 字符串数组的长度,当需要获取字符串长度的时候,只需要返回这个成员变量的值就可以了,时间复杂度是 O(1)。

2)alloc(分配空间长度):这个字段的主要作用是指分配给字符数组的存储的空间大小,当需要计算剩余空间大小的时候,只需要 alloc - len 就可以直接进行计算,然后判断空间大小是否符合修改需求,如果不满足需求的话,就执行相应的修改操作,这样的话就可以很好地解决我们上面所说的缓冲区溢出问题。

3)flags(表示 SDS 的类型):一共设计了五种类型的 SDS,分别是 sdshdr 5、sdshdr 8、sdshdr 16、sdshdr 32、sdshdr 64(这个的记忆也很简单,就是 32 开始,128,即 2 的多少次方去记忆就可以了),通过使用不同存储类型的结构题,灵活保存不同大小的字符串,从而节省内存空间。

4)buf(存储数据的字符数组):主要起到保存数据的作用,如字符串、二进制数据(二进制安全就是一个重要原因)等。

不同编码的选择

1)int 编码:如果一个字符串可以被解析为整数,并且整数值比较小,Redis 会直接使用整数编码。

1
2
3
4
5
struct redisObject {
unsigned type:4; // 数据类型(字符串、哈希等)
unsigned encoding:4; // 编码类型(int、embstr、raw等)
int64_t ptr; // 实际的数据指针,这里直接存储整数值
};

比如直接存储 123,则:

image.png

2)embstr 编码:当字符串长度比较短(小于等于 44 字节),Redis 会使用 embstr 编码,这种编码将所有的字符串相关结构体和字符数据存放在连续的内存块中,分配内存的时候,只需要分配一次,减少内存分配和管理的开销。

1
2
3
4
5
6
7
8
9
10
11
12
struct redisObject {
unsigned type:4; // 数据类型
unsigned encoding:4; // 编码类型,这里是 embstr
void *ptr; // 指向 sdshdr 结构
};

struct sdshdr {
uint32_t len; // 当前字符串长度
uint32_t alloc; // 已分配的内存大小
unsigned char flags; // 编码类型
char buf[]; // 实际字符串数据
};
image.png

3)raw 编码:当字符串长度超过 44 字节时,Redis 会使用 raw 编码,这种编码方式将结构体和实际字符串数据分开存储,以便处理更长的数据。

image.png
1
2
3
4
5
6
7
8
9
10
11
12
struct redisObject {
unsigned type:4; // 数据类型
unsigned encoding:4; // 编码类型,这里是 raw
void *ptr; // 指向 sdshdr 结构
};

struct sdshdr {
uint32_t len; // 当前字符串长度
uint32_t alloc; // 已分配的内存大小
unsigned char flags; // 编码类型
char buf[]; // 实际字符串数据
};

redis 4.0 版本及之后的版本,这个界限是 44,前面版本是 39,详情见:827. Redis 中 EMBSTR 对象的阈值设置为何为 44?其调整历史是什么?

小结

  • int 编码:用于存储可以解析为整数的字符串,内存消耗最小,适合数字值。
  • embstr 编码:用于存储较短的字符串,将元数据和内容存储在同一块内存中,适合读多写少的场景。
  • raw 编码:用于存储较长的字符串,元数据和内容分开存储,适合需要频繁操作的大字符串。
Comments