回顾
在 Redis 基础 我们介绍了 Redis 数据类型很多,有 String(Redis String 数据结构底层 SDS)、Hash(Redis Hash 数据结构底层)、List(Redis List 数据结构底层)、Set、Sorted set(Redis Sorted set 数据结构底层跳表)、Bitmaps 等多种数据类型,也介绍了 Redis 快的原因是和其底层数据结构有关。
这篇文章中我们就来介绍下 Redis 的数据结构 SDS( Simple Dynamic String,简单动态字符串)。
基础
一般咱们说的 Redis 的数据类型指的是 key-value 中的 value 的数据类型。至于 Redis 存的时候,它也是类似一个 hashmap(只不过没红黑树),有数据、有也链表。大概样子就是这样子的:
version:3.0
结构
以版本为 3.0 的 Redis 源码进行分析。
说到结构,还是直接看源码吧,地址:3.0/src/sds.h:
struct sdshdr {
unsigned int len;
unsigned int free;
char buf[];
};
解释下吧:
- len:buf 数组中已经使用的字节的数量
- free:buf 数组中未使用的字节的数量
- buf[]:字节数组,用于保存字符串
一个简单的示例:
特性
获取长度的时间复杂度为 O(1)
为什么呢?想下 SDS 的结构,里面存储了 len
字段,这个就是当前数组内字符串的长度,不像咱们印象中的字符串——计算长度得进行一次遍历。
不会造成缓冲区溢出
在 c 的字符串中,没记录可用的长度,如果现在要进行拼接的操作,如果没计算能不能存下,是不是就溢出了。而 SDS 不会,它使用了 free
字段记录了还可用的长度,拼接前先算下,如果能存下就直接拼,如果不能拼,再扩容就完事了。
减少修改字符串时带来的内存重分配次数
空间预分配用于优化 SDS 的字符串增长操作: 当 SDS 的 API 对一个 SDS 进行修改, 并且需要对 SDS 进行空间扩展的时候, 程序不仅会为 SDS 分配修改所必须要的空间, 还会为 SDS 分配额外的未使用空间。
有了「额外的未使用空间」是不是就意味着减少了扩容(内存重分配)的次数。
惰性空间释放用于优化 SDS 的字符串缩短操作: 当 SDS 的 API 需要缩短 SDS 保存的字符串时, 程序并不立即使用内存重分配来回收缩短后多出来的字节, 而是使用 free 属性将这些字节的数量记录起来, 并等待将来使用。
要是咱们删除了每个元素,SDS 立马就刚才删除元素所在空间回收,如果要再插入一个呢,是不是就增加了重分配的次数。
二进制安全
重点是「SDS 使用 len
属性的值而不是空字符(\0
)来判断字符串是否结束。
这里自己底子弱,就不介绍了,贴下官方的话吧:C 字符串中的字符必须符合某种编码(比如 ASCII), 并且除了字符串的末尾之外, 字符串里面不能包含空字符, 否则最先被程序读入的空字符将被误认为是字符串结尾 —— 这些限制使得 C 字符串只能保存文本数据, 而不能保存像图片、音频、视频、压缩文件这样的二进制数据。
version:3.2+
以版本为 3.2 的 Redis 源码进行分析。
结构
源码地址:3.2/src/sds.h
typedef char *sds;
/* Note: sdshdr5 is never used, we just access the flags byte directly.
* However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 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 unused bits */
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 unused bits */
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 unused bits */
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 unused bits */
char buf[];
};
解释下其中的字段名字吧:
- len:表示 buf 的已用长度
- alloc:表示 buf 的实际分配长度,一般大于 len
- flags:字符串的类型标记,具体的类型有5种
SDS_TYPE_5
、SDS_TYPE_8
、SDS_TYPE_16
、SDS_TYPE_32
、SDS_TYPE_64
- buf[]:数组,可以理解为字符串内容
特性
大体上和 Redis 3.0 中的 SDS 特性一致,但是在内存的空间利用上,更为极致。
在 Redis 3.2 及以上版本中,将之前 sdshdr
设计成了 sdshdr5
、 sdshdr8
、 sdshdr16
、 sdshdr32
、 sdshdr64
五种类型。
如 3.2+ 中代码所示,不同的 sdshdr
有不同的 len
和 alloc
:
字符串类型 | 占用内存空间 | 可以表示的字符数组最大长度 |
sdshdr8 | 1 | 2^8 字节 |
sdshdr16 | 2 | 2^16 字节 |
sdshdr32 | 4 | 2^32 字节 |
sdshdr64 | 8 | 2^64 字节 |
有了上述设计,可以灵活保存不同大小的字符串,从而有效节省内存空间。