Redis 系列 字符串

开启 redis 探索新篇章

Posted by lichao modified on December 24, 2019

Redis 简单动态字符串(SDS)是可以修改的字符串,在内存中它是以字节数组的形式存在的。

SDS

Redis 字符串叫「SDS」,也就是 Simple Dynamic String 。它的结构是一个带长度信息的字节数组。

1
2
3
4
5
6
struct SDS<T> {
    T capacity; // 数组容量
    T len; // 数组长度
    byte flags; // 特殊标识位,不理睬它
    byte[] content; // 数组内容
}

存储概览

如代码所示,content 里面存储了真正的字符串内容,capacity 表示所分配数组的长度,len 表示字符串的实际长度。SDS 字符串是可以修改的字符串,它要支持 append 操作。如果数组没有冗余空间,那么追加操作必然涉及到分配新数组,然后将旧内容复制过来,再 append 新内容。如果字符串的长度非常长,这样的内存分配和复制开销就会非常大。

上面的 SDS 结构使用了范型 T,为什么不直接用 int 呢,这是因为当字符串比较短时,len 和 capacity 可以使用 byte 和 short 来表示,Redis 为了对内存做极致的优化,不同长度的字符串使用不同的结构体来表示。

Redis 规定字符串的长度不得超过 512M 字节。创建字符串时 len 和 capacity 一样长,不会多分配冗余空间,这是因为绝大多数场景下不会使用 append 操作来修改字符串。

优点

  1. 常数复杂度获取字符串长度
  2. 杜绝缓冲区溢出
  3. 减少修改字符串时带来的内存重分配次数
    1. 空间预分配
    2. 懦性空间释放
  4. 二进制安全
  5. 兼容部分 C 字符串函数

存储方式

Redis 字符串有两种存储方式,在长度特别短时,使用 emb 形式存储 (embeded 嵌入的),当长度超过 44 时,使用 raw 形式存储。

存储概览

如图所示,embstr 存储形式 是这样一种 存储形式,它将 RedisObject 对象头和 SDS 对象连续存在一起,使用 malloc 方法一次分配。而 raw 存储形式不一样,它需要两次 malloc,两个对象头在内存地址上一般是不连续的。

> set codehole abcdefghijklmnopqrstuvwxyz012345678912345678
OK
> debug object codehole
Value at:0x7fec2de00370 refcount:1 encoding:embstr serializedlength:45 lru:5958906 lru_seconds_idle:1
> set codehole abcdefghijklmnopqrstuvwxyz0123456789123456789
OK
> debug object codehole
Value at:0x7fec2dd0b750 refcount:1 encoding:raw serializedlength:46 lru:5958911 lru_seconds_idle:1

embstr 与 raw

在字符串比较小时,SDS 对象头的大小是 capacity + 3,至少是 3。意味着分配一个字符串的最小空间占用为 19 字节 (16 + 3)。

struct SDS {
int8 capacity; // 1byte
int8 len; // 1byte
int8 flags; // 1byte
byte[] content; // 内联数组,长度为 capacity
}

内存分配器 jemalloc/tcmalloc 等分配内存大小的单位都是 2、4、8、16、32、64 等,为了能容纳一个完整的 embstr 对象,jemalloc 最少会分配 32 字节的空间,如果字符串再稍微长一点,那就是 64 字节的空间。如果总体超出了 64 字节,Redis 认为它是一个大字符串,不再使用 emdstr 形式存储,而该用 raw 形式。

当内存分配器分配了 64 空间时,那这个字符串的长度最大可以是多少呢?这个长度就是 44。那为什么是 44 呢?

前面我们提到 SDS 结构体中的 content 中的字符串是以字节\0结尾的字符串,之所以多出这样一个字节,是为了便于直接使用 glibc 的字符串处理函数,以及为了便于字符串的调试打印输出。

看上面这张图可以算出,留给 content 的长度最多只有 45(64-19) 字节了。字符串又是以\0结尾,所以 embstr 最大能容纳的字符串长度就是 44。

扩容策略

字符串在长度小于 1M 之前,扩容空间采用加倍策略,也就是保留 100% 的冗余空间。当长度超过 1M 之后,为了避免加倍后的冗余空间过大而导致浪费,每次扩容只会多分配 1M 大小的冗余空间。