字符串是 Redis 最基础的数据结构, Redis 本身主要是一个 K-V 数据库, Redis 的所有 Key 都是字符串类型, 本文结合 Redis 3.0 的源码, 讨论 Redis 字符串类型的数据结构

26.1 Redis 字符串的结构

Redis String 类型在底层对应的是一个结构体, 称为 Simple Dynamic String, 简写为 sds, 它的路径在 src/sds.h 中, 结构如下所示:

struct sdshdr {
    unsigned int len;
    unsigned int free;
    char buf[];
};

其中 len 是字符串的长度, 因此 Redis 计算字符串长度 (STRLEN KEY 命令) 的时间复杂度为 O(1), buf[] 是一个字节数组, 它是存放字符串内容的物理结构, free 字段指示了当前还有多少可用空间, 从底层来看, Redis 的所有字符串都是 C 风格的字符串 (即 null termianted C string), len 的值与 strlen() 函数的返回值相同, 不包括最后的 '\0'

redis 在创建字符串时会自动在字符串末尾添加 '\0', 这使得其可以复用 C 风格字符串的 API, Redis 在实现时避免了某些不安全的 C API, 比如 strcat() 函数, 该函数在拼接字符串时可能存在缓冲区溢出问题, Redis 在 src/sds.c 实现的 sdscat() 会在进行操作之前首先调用 sdsMakeRoomFor() 函数检查空间是否足够, 如空间不足则首先进行 sds 的扩容再进行字符串拼接操作

Redis 的字符串是二进制安全 (binary-safe) 的, 对于 C 风格字符串来说, '\0' 被释放字符串的结尾, 因此相关的 API 在遇到 '\0' 后会认为这已经是字符串的末尾, 比如使用 strlen() 计算字符串长度, 若字符串内含有 '\0' 字符, 则 strlen() 返回的值是第一个 '\0' 之前的长度, 这使得 C 风格字符串只能存储文本数据, Redis 的字符串是二进制安全的 这句话的含义是 Redis 的字符串可以存放任意的二进制数据, 如图片 / 视频等, 对于 Redis 来说, 计算字符串长度是通过读取 len 属性来获取的

26.2 Redis 字符串的扩容与回收

内存空间的申请和释放都需要通过系统调用来实现, 频繁的申请和回收将影响程序的性能, 尤其是像内存 K-V 数据库这样对性能非常敏感的场景, 因此 Redis 对字符串空间的申请和释放做了额外的优化, 当需要对字符串进行修改时, 如果修改后的长度没有达到 buf[] 的容量限制则不需要进行扩容, 直接修改 buf[] 的值并更新 len 和 free 即可, 如若修改后的字符串长度超过 buf[] 的容量, 则需要进行扩容, 为了避免频繁扩容对性能的影响, Redis 3.0 进行扩容时并不仅仅申请此次需要的内存空间, 还会申请额外的空间, 我们将字符串修改之后的长度 (即 len 属性的值) 记为 L, 在 Redis 3.0 中, 若 L < SDS_MAX_PREALLOC, 则扩容之后的空间大小为 2L + 1, 其中 SDS_MAX_PREALLOC 是 redis 对字符串的最大预分配长度, 其值为 1 MB, 其中加 1 用于存放结尾的 '\0', 否则扩容之后的空间大小为 L + SDS_MAX_PREALLOC + 1, 换句话说 Redis 在进行扩容时最大会预留 1 MB + 1 Byte 的空余空间

当对字符串修改使得字符串的长度变短之后, Redis 不会立即缩小 buf[] 占用的空间, 而是通过更新 free 属性的值将空间标记为空白, 使得其可以在之后再次需要时复用已获得的内存空间, src/sds.c 中也设置了 sdsfree() 函数用于在需要时释放 sds 申请的内存