数据结构实现
相信大家对 redis 的数据结构都比较熟悉:
- string:字符串(可以表示字符串、整数、位图)
- list:列表(可以表示线性表、栈、双端队列、阻塞队列)
- hash:哈希表
- set:集合
- zset:有序集合
为了将性能优化到极致,redis 作者为每种数据结构提供了不同的实现方式,以适应特定应用场景。
以最常用的 string 为例,其底层实现就可以分为 3 种:int, embstr, raw
127.0.0.1:6379> SET counter 1
OK
127.0.0.1:6379> OBJECT ENCODING counter
"int"
127.0.0.1:6379> SET name "Tom"
OK
127.0.0.1:6379> OBJECT ENCODING name
"embstr"
127.0.0.1:6379> SETBIT bits 1 1
(integer) 0
127.0.0.1:6379> OBJECT ENCODING bits
"raw"
这些特定的底层实现在 redis 中被称为 编码encoding,下面逐一介绍这些编码实现。
string
redis 中所有的 key 都是字符串,这些字符串是通过一个名为 简单动态字符串SDS的数据结构实现的。
typedef char *sds; // SDS 字符串指针,指向 sdshdr.buf
struct sdshdr? { // SDS header,[?] 可以为 8, 16, 32, 64
uint?_t len; // 已用空间,字符串的实际长度
uint?_t alloc; // 已分配空间,不包含'\0'
unsigned char flags; // 类型标记,指明了 len 与 alloc 的实际类型,可以通过 sds[-1] 获取
char buf[]; // 字符数组,保存以'\0'结尾的字符串,与传统 C 语言中的字符串的表达方式保持一致
};
内存布局如下:
+-------+---------+-----------+-------+
| len | alloc | flags | buf |
+-------+---------+-----------+-------+
^--sds[-1] ^--sds
相较于传统的 C 字符串,其优点如下:
- 高效:记录了已用空间,获取字符串长度的操作为O(1)
- 安全:记录了空闲空间,可以避免写缓冲区越界的问题
- 内存友好:通过记录了空间信息,可以预分配空间,实现惰性删除,减少内存分配的同时不会造成内存泄露
- 二进制安全:字符串内容可以为非 ASCII 编码,任意数据都能被编码为二进制字符串
- 兼容 C 字符串:可以复用部分 C 标准库代码,避免无用重复
list
redis 中 list 的底层实现之一是双向链表,该结构支持顺序访问,并提供了高效的元素增删功能。
typedef struct listNode {
struct listNode *prev; // 前置节点
struct listNode *next; // 后置节点
void *value; // 节点值
} listNode;
typedef struct list {
listNode *head; // 头节点
listNode *tail; // 尾节点
unsigned long len; // 列表长度
void *(*dup) (void *ptr); // 节点值复制函数
void (*free) (void *ptr); // 节点值释放函数
int (*match) (void *ptr); // 节点值比较函数
} list;
这里使用了函数指针来实现动态绑定,根据 value 类型,指定不同 dup, free, match 的函数,实现多态。
该数据结构有以下特征:
- 有长:获取列表长度的操作为O(1)
- 双端:可以同时支持正向和逆向遍历,获取前后位置的节点复杂度为O(1)
- 无环:没有设置哨兵节点,列表为空时,表头表尾均为 NULL
- 多态:通过函数指针实现多态,数据结构可以复用
dict
redis 中使用 dict 来保存键值对,其底层实现之一是哈希表。
typedef struct dictEntry {
void* key; // 键
union { // 值,可以为指针、有符号长整,无符号长整,双精度浮点
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
typedef struct dictht {
dictEntry **table; // 哈希表数组,数组中的每个元素是一个单向链表
unsigned long size; // 哈希表数组大小
unsigned long sizemask; // 哈希掩码,用于计算索引
unsigned long used; // 已有节点数量
} dictht;
typedef struct dictType {
unsigned int (*hashFunction) (const void *key); // 哈希函数,用于计算哈希值
int (*keyCompare)(void *privdata, const void *key1, const void *key2); // 键比较函数
void *(*keyDup)(void *privdata, const void *key); // 键复制函数
void *(*valDup)(void *privdata, const void *obj); // 值复制函数
void *(*keyDestructor)(void *privdata, const void *key); // 键销毁函数
void *(*valDestructor)(void *privdata, const void *obj); // 值销毁函数
} dictType;
typedef struct dict {
dictType *type; // 类型函数,用于实现多态
void *privdata; // 私有数据,用于实现多态
dictht ht[2]; // 哈希表,字典使用 ht[0] 作为哈希表,ht[1] 用于进行 rehash
int rehashidx; // rehash索引,当没有执行 rehash 时,其值为 -1
} dict;
该数据结构有以下特征:
- 哈希算法:使用 murmurhash2 作为哈希函数,时间复杂度为O(1)
- 冲突解决:使用链地址法解决冲突,新增元素会被放到表头,时间复杂度为O(1)
- 重新散列:每次 rehash 操作都会分成 3 步完成步骤1:为dict.ht[1]分配空间,其大小为 2 的 n 次方幂
步骤2:将dict.ht[0]中的所有键值对 rehash 到dict.ht[1]上
步骤3:释放dict.ht[0]的空间,用dict.ht[1]替换 dict.ht[0]
rehash 的一些细节
- 分摊开销为了减少停顿,步骤2 会分为多次渐进完成,将 rehash 键值对所需的计算工作,平均分摊到每个字典的增加、删除、查找、更新操作,期间会使用dict.rehashidx记录dict.ht[0]中已经完成 rehash 操作的dictht.table索引:每执行一次 rehash 操作,dict.rehashidx计数器会加1当 rehash 完成后,dict.rehashidx会被设置为 -1
- 触发条件
计算当前负载因子:loader_factor = ht[0].used / ht[0].size
收缩: 当 loader_factor < 0.1 时,执行 rehash 回收空闲空间
扩展:没有执行 BGSAVE 或 BGREWRITEAOF 命令,loader_factor >= 1 执行 rehash正在执行 BGSAVE 或 BGREWRITEAOF 命令