数据结构实现
相信大家对 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)
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)
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
索引:dict.rehashidx dict.rehashidx
-
触发条件
计算当前负载因子:loader_factor = ht[0].used / ht[0].size
收缩:当 loader_factor < 0.1 时,执行 rehash 回收空闲空间
扩展:
- 没有执行 BGSAVE 或 BGREWRITEAOF 命令,loader_factor >= 1 执行 rehash
- 正在执行 BGSAVE 或 BGREWRITEAOF 命令,loader_factor >= 5 执行 rehash
大多操作系统都采用了 写时复制
copy-on-write
技术来优化子进程的效率:父子进程共享同一份数据,直到数据被修改时,才实际拷贝内存空间给子进程,保证数据隔离
在执行 BGSAVE 或 BGREWRITEAOF 命令时,redis 会创建子进程,此时服务器会通过增加 loader_factor 的阈值,避免在子进程存在期间执行不必要的内存写操作