文章目录
第一部分:内部数据结构
简单动态字符串(simple dynamic string)
在 Redis 中, 一个字符串对象除了可以保存字符串值之外, 还可以保存 long 类型的值
Sds 在 Redis 中的主要作用有以下两个:
- 实现字符串对象(StringObject);
- 在 Redis 程序内部用作 char* 类型的替代品;
在 C 语言中,字符串可以用一个 \0 结尾的 char 数组来表示。它并不能高效地支持长度计算和追加(append)这两种操作:
- 每次计算字符串长度(strlen(s))的复杂度为 θ(N) 。
- 对字符串进行 N 次追加,必定需要对字符串进行 N 次内存重分配(realloc)。
考虑到这两个原因, Redis 使用 sds 类型替换了 C 语言的默认字符串表示
在前面的内容中, 我们一直将 sds 作为一种抽象数据结构来说明, 实际上, 它的实现由以下两部分组成:
其中,类型 sds 是 char * 的别名(alias),而结构 sdshdr 则保存了 len 、 free 和 buf 三个属性。
typedef char *sds;
struct sdshdr {
// buf 已占用长度
int len;
// buf 剩余可用长度
int free;
// 实际保存字符串数据的地方
char buf[];
};
sds.c/sdsMakeRoomFor 函数描述了 sdshdr 的这种内存预分配优化策略, 以下是这个函数的伪代码版本:
def sdsMakeRoomFor(sdshdr, required_len):
# 预分配空间足够,无须再进行空间分配
if (sdshdr.free >= required_len):
return sdshdr
# 计算新字符串的总长度
newlen = sdshdr.len + required_len
# 如果新字符串的总长度小于 SDS_MAX_PREALLOC
# 那么为字符串分配 2 倍于所需长度的空间
# 否则就分配所需长度加上 SDS_MAX_PREALLOC 数量的空间
if newlen < SDS_MAX_PREALLOC:
newlen *= 2
else:
newlen += SDS_MAX_PREALLOC
# 分配内存
newsh = zrelloc(sdshdr, sizeof(struct sdshdr)+newlen+1)
# 更新 free 属性
newsh.free = newlen - sdshdr.len
# 返回
return newsh
伪代码解释:如果剩余空间足够,则不进行内存重新分配。如果不够则会计算当前字符串的长度=原字符串长度+新增的字符串长度。
如果计算的当前字符串的长度没有超过一个固定值SDS_MAX_PREALLOC,就分配给他两倍自身长度的空间,如果超过了就分配一个固定值长度的空间。
这个固定值SDS_MAX_PREALLOC大小为1MB,是能够分配空间的最大值。
应用举例:
redis> SET msg "hello world"
OK
redis> APPEND msg " again"
(integer) 17
redis> GET msg
"hello world again"
当SET这个msg时,会保存到一个sdshdr中,记录长度、剩余空间、数据,值得注意的是,SET命令不会分配剩余空间,只有对字符串进行追加才时才会分配剩余空间。
struct sdshdr {
len = 11;
free = 0;
buf = "hello world\0";
}
当执行 APPEND 命令时,相应的 sdshdr 被更新,字符串 " again" 会被追加到原来的 “hello world” 之后:它将原本长度为11的字符串加上更新的字符串长度作为新字符串的长度,并且赋给此长度的剩余空间。如果再次执行APPEND,添加的字符串长度小于剩余空间时,不会再进行内存的分配。
struct sdshdr {
len = 17;
free = 17;
buf = "hello world again\0";// 空白的地方为预分配空间,共 17 + 17 + 1 个字节
}
这种分配策略会浪费内存吗?
执行过 APPEND 命令的字符串会带有额外的预分配空间, 这些预分配空间不会被释放, 除非该字符串所对应的键被删除, 或者等到关闭 Redis 之后, 再次启动时重新载入的字符串对象将不会有预分配空间。
因为执行 APPEND 命令的字符串键数量通常并不多, 占用内存的体积通常也不大, 所以这一般并不算什么问题。
另一方面, 如果执行 APPEND 操作的键很多, 而字符串的体积又很大的话, 那可能就需要修改 Redis 服务器, 让它定时释放一些字符串键的预分配空间, 从而更有效地使用内存。
双端链表
Redis 列表使用两种数据结构作为底层实现:
- 双端链表
- 压缩列表
因为双端链表占用的内存比压缩列表要多, 所以当创建新的列表键时, 列表会优先考虑使用压缩列表作为底层实现, 并且在有需要的时候, 才从压缩列表实现转换到双端链表实现。
双端链表的实现由 listNode 和 list 两个数据结构构成.
typedef struct listNode {
// 前驱节点
struct listNode *prev;
// 后继节点
struct listNode *next;
// 值
void *value;
} listNode;
typedef struct list {
// 表头指针
listNode *head;
// 表尾指针
listNode *tail;
// 节点数量
unsigned long len;