1 redis存储结构
1.1 键值对
redis都是以键值对存储的。
-
键:字符串对象
-
值:可以是字符串,也可以是集合类型
redis用hash表保存所有键值对,大概是这么个结构
- key和value就是指向键值对象
一个dictEntry:
typedef struct dictEntry {
//键值对中的键
void *key;
//键值对中的值
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
//指向下一个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
-
这里使用union是因为当值就是uint64_t, int64_t, double时直接放到entry中,不需要用一个指针指向
-
next指向hash冲突时相同hash值的节点
1.2 对象
redis中的对象:
-
type:数据类型,比如string,set等
-
encoding:数据结构
-
ptr:指向底层数据结构的指针
2 SDS
redis自己实现了简单动态字符串
2.1 c字符串的问题
-
获取字符串长度时间复杂度高
-
以'\ 0'作为结束标识,不能存放二进制数据
-
字符串操作(相加等)不安全不高效,可能造成内存溢出
2.2 sds结构
-
len:字符数组长度
-
alloc:分配的长度,相当于容量
-
flags:SDS类型
-
buf:底层数据
2.2.1 扩容
-
所需空间小于1M:分配所需空间的2倍
-
大于1M:分配所需空间 + 1M
2.2.2 SDS类型
有5种SDS类型,每种类型的len和alloc的类型不同,这样就可以根据不同的情况使用不同的类型从而节省内存
还用了__attribute__ ((packed))来取消编译对齐
3 链表
节点:
typedef struct listNode {
struct listNode *prev;
struct listNode *next;
void *value;
} listNode;
list在节点上进行封装
typedef struct list {
listNode *head;
listNode *tail;
void *(*dup)(void *ptr);
void (*free)(void *ptr);
int (*match)(void *ptr, void *key);
unsigned long len;
} list;
- 添加了复制、释放、和比对函数还有长度
4 压缩列表
4.1 结构
-
zlbytes:整个压缩列表长度
-
zltail:尾部距离头部的偏移量
-
zllen:节点数
-
zlend:压缩列表结束标识,是一个固定值0xff
4.2 entry
-
prelen:前一个节点的长度,用于从后往前遍历
-
如果前一个节点 < 254字节,那么prelen是1字节
-
= 254字节,prelen是5字节
-
encoding:记录类型和长度,对于不同类型的数据和不同长度的数据,用不同长度的encoding字段表示
-
data:实际数据
4.3 连锁更新
由于prelen表示前一个节点和不同长度节点用不同长度prelen表示导致:
-
假设一开始有10个253字节的节点
-
然后插入一个256字节的节点到压缩列表头,这时候第一个253字节的节点会使用5字节的prelen,导致第一个253字节节点自身变成258字节,那么第二个253字节的节点也需要用258字节来表示之前的第一个节点,如此就引发了连锁更新
所以压缩列表不宜太长
5 hash表
对于hash冲突使用链式hash
5.1 rehash
相同hash的节点太多,影响查询效率,对hash大小进行扩展。
hash表:
struct dict {
dictType *type;
dictEntry **ht_table[2];
unsigned long ht_used[2];
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
/* Keep small vars at end for optimal (minimal) struct padding */
unsigned pauserehash : 15; /* If >0 rehashing is paused */
unsigned useStoredKeyApi : 1; /* See comment of storedHashFunction above */
signed char ht_size_exp[2]; /* exponent of size. (size = 1<<exp) */
int16_t pauseAutoResize; /* If >0 automatic resizing is disallowed (<0 indicates coding error) */
void *metadata[];
};
这里的dictEntry ** ht_table[2];表示有两个hash表,一般有第一个,在rehash的时候用第二个,流程:
-
给第二个hash表分配两倍于hash表1的内存
-
将表1的数据迁移到表2
-
用表2替代表1,删除原表1,创建空的新表2下次用
数据量很大的时候,要迁移很多数据
5.2 渐进式hash
主要就是将迁移工作分配到多次:
-
redis每次操作时,除了完成命令的操作,还会将涉及的key迁移到表2,直到全部迁移完
-
新增只会新增到表2
5.3 触发rehash
负载因子 = hash表的节点个数 / hash表大小,以下条件触发rehash:
-
负载因子 > =1:如果没在RDB快照或者aof重写,就触发
-
负载因子 > =5:强制rehash
6 整数集合
typedef struct intset {
uint32_t encoding;
uint32_t length;
int8_t contents[];
} intset;
- 实际类型由encoding决定,而不是都是int8_t
6.1 整数升级
如果一开始插入的数据都是int16,那整数集合的类型就是int16,然后来了个int32的数据,就会将整数集合的类型升级成int32。
升级的过程是在原数组的位置上增加容量,并保持数据间的顺序不变
可以节省内存。
不支持降级。
7 跳表
将节点分层,然后以二分法查找节点
7.1 结构
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
- level:最大层级
7.2 节点
typedef struct zskiplistNode {
sds ele;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned long span;
} level[];
} zskiplistNode;
实现跳表的关键在level数组,这是一个zskiplistLevel类型的数组,level数组表示一个节点在某一个层的角色,这个角色的意思是这个节点在这一层的前一个节点是谁,和前一个节点之间的跨度是多少
7.3 查找节点a流程
从最顶层开始遍历:
-
如果当前节点的score < a的score,遍历当前层的下一个
-
如果当前节点的score = a的score,但是当前节点的ele < a的ele,那么也会遍历当前层的下一个
如果以上两个条件不满足,就遍历到当前节点level数组的下一层
7.4 层数设置
对于新创建的节点,随机生成一个数,当这个数 < 0.25,就将新节点的level + 1,直到随机数 > 0.25,返回level、
这样层数越高概率越低
int zslRandomLevel(void) {
static const int threshold = ZSKIPLIST_P*RAND_MAX;
int level = 1;
while (random() < threshold)
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
7.5 用跳表的原因
- 跳表在内存占用和查找效率方面和btree差不多,但是跳表实现简单很多
8 quicklist
链表加压缩列表,链表的节点是压缩列表
9 listpack
用于替换压缩列表,取出了prelen,测地避免了连锁更新