前言
SDS 在 Redis 中有着举足轻重的地位,Redis 没有使用 C 语言中提供的字符串,而是通过 SDS 存储字节序列。
它相比较与字符串有以下几个好处:
- SDS 使用二进制存储数据,不但可以存储字符串,还是可以存储图片、视频、其他二进制数据。
- SDS 的所有函数都是二进制安全的。
- SDS 可以自动扩容。
- SDS 可以直接获取字符串长度。
结构
首先我们看 SDS 在 redis 中是这么定义的:
typedef char *sds;
把 SDS 说的这么强大,居然只是一个 char 类型的指针!char 在 C 语言中占用 1 字节。那么说明通过 SDS 访问数据时,都是一个一个字节去访问的,这也说明为什么 SDS 存储的是字节序列。
Redis 为 SDS 定义了几种不同的结构体,用于表示不同大小的字符串:
它们的结构如下:
从图中可以看出,每一种结构可以记录字符串的长度都不相同:
类型 | 数据长度 |
---|---|
sdshdr5 | 2^5 – 1 |
sdshdr8 | 2^8 – 1 |
sdshdr16 | 2^16 – 1 |
sdshdr32 | 2^32 – 1 |
sdshdr64 | 2^64 – 1 |
Redis 这么设计的原因是为了节省内存,假设有 2000 万个字符串,如果都可以使用 sdshdr8 存储下,那么使用 sdshdr8 就可以比 sdshdr16 节省 38 M,比 sdshdr32 节省 76 M,比 sdshdr64 节省 152 M。随着存储的字符串越来越多,能够节省的内存就会越多。
创建 SDS
_sdsnewlen(const void *init, size_t initlen, int trymalloc
) 这是创建 SDS 的函数签名,它有三个参数:
- init:需要存储的字符串
- initlen:初始化的长度
- trymalloc:是否尝试分配内存。如果开启的话,内存分配失败,则只会返回 null,错误由调用方处理。如果不开启,则会直接将错误输出并中断进程。
创建 SDS 总共分为 3 个步骤:
- 根据 initlen 确定需要使用的类型
- 申请内存,内存大小 = 类型头部大小 + 数据大小 + 1
- 在申请的内存中写入数据
例子
假设现在要创建一个字符串 “abcdefghijkabcdefghijkabcdefghijk”,_sdsnewlen("abcdefghijk", 33)
;
执行第一步,确定需要使用的类型,这里长度为 33,使用类型为 sdshdr8。
执行第二步,申请内存空间。sdshdr8 头部的大小为 3byte,数据大小为 33 byte,额外大小 1byte,总共需要申请 37 byte。
第三步,写入数据。
第一个字节写入的是数据的长度 len,长度为 33。
第二个字节写入申请的内存空间大小,这里跟 len 相等。
第三个字节写入 flag,因为我们是使用 sdshdr8,类型为 1,所以低三位是 1,高 5 位未使用。
紧接着就是写入数据,直接将 init 中的数据拷贝过去即可。
最后会写入一个 \0,这就是为什么会多申请一个字节的原因。
整个 sds 的内存空间
扩容
扩容有两种模式,贪婪模式和非贪婪模式。贪婪模式下,会预先分配更多的内存,便于后续使用;非贪婪模式,只会分配刚好足够使用的内存。
_sdsMakeRoomFor(sds s, size_t addlen, int greedy)
,有三个参数:
- s,它指向原字符串
- addlen,需要增加的长度
- greedy,是否开启贪婪模式
执行步骤:
-
检查剩余可用空间是否大于 addlen,如果大于则直接返回,不需要进行扩容。
-
接着计算新的长度,计算公式:原字符串长度 + addlen = newlen
- 贪婪模式,会增大计算的 newlen。如果 newlen < 1M,则将 new * 2;如果 newlen >= 1M,则 newlen + 1M。
- 非贪婪模式,则直接使用上述计算的 newlen。
-
确定 newlen 需要使用的类型。每一种类型可以分配的字符串长度空间都是不一样的。
-
判断新类型与原类型是否相同
-
相同,则直接使用 realloc 重新分配内存空间。
-
不相同,则需要使用 malloc 申请新的空间。
- 拷贝原数据到新的内存空间,释放原内存空间
- 设置 flag 为新的类型
- 设置 len 字段
-
-
记录 alloc 字段。
realloc 与 malloc 的区别?
realloc 申请内存空间,会先查看后面是否有足够的内存空间可以使用,如果有则直接使用即可;如果没有,就会寻找到一块足够的内存空间,并将原地址的内存拷贝过来。
malloc 则是直接找到一块新的内存空间。
为什么需要判断新类型是否与原类型相同?因为如果类型相同,那么表示 len、alloc、flag 这三个属性占用的内存长度是相同,只需要修改内部的数据即可。如果类型不相同,那么这三个属性占用内存长度不同,如果直接在原内存空间中进行修改,会造成数据错乱。
例子
总结
- Redis 使用不同类型的结构去存储不同长度的字符串,目的是为了节省内存。
- Redis 会记录下存储字符串的长度,可以快速获取到字符串的长度,而不需要去遍历字符串。
- Redis 可以通过扩容,适配字符串长度的增加。