Redis 中最常用的可能就是字符串了吧,我们通过 set 命令可以存储一个键值到缓存中,key 是一个字符串,value 也是一个字符串,可是你知道 Redis 中的这个字符串的底层结构吗?
探秘SDS
大家的可能都知道 Redis 是基于 C 语言实现的,但是其实 Redis 并没有直接使用 C 语言里面的字符串,即传统的以空字符(\0)结尾的字符数组,而是字节构建了一种名为 SDS (Simple Dynamic String)的简单动态字符串。
Redis 中几乎所有用到字符串的地方都是使用的 SDS 这种结构,而像 C 语言中的字符串只会用在不会对字符串进行修改的地方,比如打印日志。
下面我们来看一下 SDS 的结构
struct sdshdr{
int len;
int free;
char buf[];
}
buf即字节数组,用于保存我们的字符串,跟 C 语言中的字符串一样,末尾以\0结尾,它这么做的目的是为了可以重用一部分 C 语言的函数,当然\0的管理不需要我们操心,Redis 提供的API已经为我们做了这个事。
其中len记录了buf数组中已经使用的字节数量,也就是字符串的长度(这个长度不包括末尾的\0),free记录了buf数组中没有使用的字节数量。
下面我们来看看 Redis 为什么要这么设计。
len的设计让我们可以以常数的复杂度来获取字符串的长度,当我们想要获取字符串长度时,只需要访问一下这个len属性即可,而不必遍历字符数组。
free的设计可以减少修改字符串时重新分配内存的次数,比如 Redis 中支持 append 操作。如果数组没有冗余空间,那么追加操作必然涉及到分配新数组,然后将旧内容复制过来,再 append 新内容。如果字符串的长度非常长,这样的内存分配和复制开销就会非常大。
扩容
SDS在长度小于 1M 之前,扩容时采用的加倍策略,也就是保留 100% 的冗余空间,这句话的意思就是SDS在进行空间扩展的时候,不但会为SDS分配修改后所需要的空间还会为它分配额外的未使用空间,即当修改后的len如果小于1M,那么此时free的值和len相同,这个也就是100% 的冗余空间的意思。
当修改后 SDS len 超过 1M 之后,为了冗余空间过大而导致浪费,扩容后只会给 free 分配 1M 大小的冗余空间。
这里需要注意的是 Redis 中默认字符串最大长度为512M。
缩容
对于缩容 SDS 采用的是惰性空间释放的策略,即 SDS 缩短后不会立即重新分配内存来回收多余的字节,而是通过 free 将这些字节数量记录起来,等待将来的使用,不过也可能这些空间永远用不上了 ,造成了内存泄漏,不用担心,SDS 也提供了相应的API可以让我们手动释放未使用空间。
Redis 对象头
Redis 中所有的对象都有一个结构头:
struct RedisObject {
int4 type;
int4 encoding;
int24 lru;
int32 refcount;
void *ptr;
} robj;
不同的对象的类型 type(4bit)是不同的,即使是同一个类型的 type 可能也会有不同的存储形式 encoding(4bit)
lru 使用了 24 个 bit 来记录 LRU 信息。
refcount 即每个对象的引用计数,当引用计数为零时,对象就会被销毁,内存被回收。
ptr 指针将指向对象内容 (body) 的具体存储位置。
这样一个 RedisObject 对象头需要占据 16 字节的存储空间。
SDS 两种存储方式
SDS 有两种存储方式,在长度较短时使用 emb 形式存储 (embeded),当长度超过 44 时,使用 raw 形式存储。
embstr 将 RedisObject 对象头和 SDS 对象连续存在一起,使用 malloc 方法一次分配。
而 raw 存储形式不一样,它需要两次 malloc,两个对象头在内存地址上一般是不连续的。