Redis String 原理与源码分析
Redis String 是 Redis 最基础的数据类型之一,采用键值对的形式存储数据。String 的值可以是字符串、整数或浮点数,并且是二进制安全的。本文将分析 Redis 7.0.5 中 String 的实现原理,重点讨论三种编码方式、SDS 的必要性及其优点,以及 SDS 的实现原理,包括扩容机制和内存管理。
三种编码方式
Redis String 有三种内部编码方式:int、embstr 和 raw。编码方式的选择取决于 String 的长度和类型。
#define OBJ_ENCODING_RAW 0 /* Raw representation */
#define OBJ_ENCODING_INT 1 /* Encoded as integer */
#define OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */
-
int 编码: 当 String 的值为整数且长度小于等于 21 个字符时,使用 int 编码。此时,值直接存储在 RedisObject 的 ptr 指针中,无需额外的内存分配。
-
embstr 编码: 当 String 的值为字符串且长度小于等于 44 个字节时,使用 embstr 编码。embstr 编码将 SDS 和 RedisObject 连续分配在一块内存中,以减少内存分配和释放的开销。
-
raw 编码 当 String 的值为字符串且长度大于 44 个字节,或者值是整数但长度大于 21 个字符时,使用 raw 编码。此时,SDS 和 RedisObject 分别分配在不同的内存块中。
最大容量限制与扩容机制
Redis 中的字符串最大容量限制为 **512MB**。这个限制是为了在保证内存效率、性能和数据结构的情况下,提供高效的数据存储和访问。512MB 的限制源于 Redis 使用的 32 位整数可以表示的最大长度,同时也考虑到性能和内存管理的需求。
扩容机制
SDS 的扩容机制根据字符串的长度分为两种情况:
-
小于 1MB 的扩容: 当 SDS 的长度小于 1MB 时,扩容时会增加固定的 1MB 的空间。这意味着每次扩容都会预留 1MB 的额外空间,以减少频繁的内存分配操作。
-
大于 1MB 的扩容: 当 SDS 的长度大于 1MB 时,扩容时会根据当前字符串的长度进行动态扩展,通常是将当前长度的两倍作为新的分配空间。这种策略可以有效地减少内存重分配的次数,并提高性能。
这种扩容机制确保了在字符串增长时,SDS 可以灵活地调整内存大小,而不会造成频繁的内存分配和释放,从而提高了性能和内存利用率。
SDS 的必要性及优点
Redis 使用自定义的 SDS(Simple Dynamic String)来表示字符串,而不是使用 C 语言原生的 char *。SDS 的优点包括:
-
二进制安全: SDS 可以保存包含空字符 '\0' 的字符串,而 C 语言的 char * 会将 '\0' 视为字符串的结束。
-
获取字符串长度的复杂度为 O(1): SDS 维护了字符串的长度信息,因此可以在 O(1) 时间内获取长度,而 C 语言的 char * 需要遍历整个字符串,复杂度为 O(N)。
-
减少内存重分配次数: SDS 会根据字符串的长度动态分配足够的内存空间,字符串增长时会自动扩展内存,从而减少内存重分配的次数。
-
避免缓冲区溢出: SDS 的设计确保了不会发生缓冲区溢出,提供了安全的 API 接口,避免了 C 语言字符数组的安全隐患。
SDS 实现原理,结合源码 SDS 的定义如下(摘自 sds.h):
c
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 msb of string length */
char buf[];
};
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 msb of string length */
char buf[];
};
从源码可以看出,SDS 由两部分组成:头部和正文缓冲区。头部包含字符串的长度、分配的内存大小和类型标志位,正文缓冲区存储实际字符串内容。 SDS 根据字符串的长度选择不同的头部结构体,具体如下:
- 当字符串长度小于 32 字节时,使用 `sdshdr5`。
- 当字符串长度小于 256 字节时,使用 `sdshdr8`。
- 当字符串长度小于 65536 字节时,使用 `sdshdr16`。
- 当字符串长度小于 4GB 时,使用 `sdshdr32`。
- 当字符串长度小于 16EB 时,使用 `sdshdr64`。
这种设计可以有效减少内存的浪费,并且在修改字符串时只需修改头部的长度信息,而不需要修改正文缓冲区,从而提高了效率。
Redis 在 `sds.c` 文件中实现了一系列操作 SDS 的函数,如 `sdsnew`、`sdscatlen`、`sdsgrowzero` 等,用于创建、拼接和扩展 SDS。这些函数会根据需要自动扩展 SDS 的内存空间,并维护好长度信息。
总结
Redis 使用自定义的 SDS 来表示字符串,相比 C 语言原生的 `char *`,SDS 提供了二进制安全、获取长度 O(1) 复杂度、减少内存重分配次数等优点。SDS 的实现原理是使用头部存储元信息,根据字符串长度选择不同的头部结构体,提高了内存利用率和修改字符串的效率。此外,Redis 将字符串的最大容量限制为 512MB,并通过灵活的扩容机制确保在字符串增长时能够有效管理内存。