Skip to content

Commit 7922954

Browse files
committed
深究Redis的底层结构
1 parent 2cfe314 commit 7922954

2 files changed

Lines changed: 187 additions & 0 deletions

File tree

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
# 深究Redis的底层结构
2+
3+
## 简单动态字符串(SDS)
4+
> Redis没有直接使用C语言传统的字符串(以空字符结尾的字符数组,以下简称C字符串),而是自己构建了一种名为简单动态字符串(Simple dynamic string,SDS)的抽象类型,并将SDS用作Redis的默认字符串表示。
5+
6+
### 看一下SDS的定义
7+
```c
8+
struct sdshdr {
9+
// 记录buf数组中已使用字节的数量
10+
// 等于sds所保存字符串的长度
11+
int len;
12+
13+
// 记录buf数组中未使用字节的数量
14+
int free;
15+
16+
// 字节数组,用于保存字符串
17+
char buf[];
18+
}
19+
```
20+
21+
### 有何优点
22+
1. 获取字符串长度的复杂度为O(1)。
23+
2. 杜绝缓冲区溢出。
24+
3. 减少修改字符串长度时所需要的内存重分配次数。
25+
4. 二进制安全。
26+
5. 兼容部分C字符串函数。
27+
28+
## 链表
29+
> 毫无疑问,Redis也不会不适用链表的,毕竟链表这么好的结构,用起来很香的。当有一个列表键包含了数量比较多的元素,又或者列表中包含的元素都是比较长的额字符串时,Redis就会使用链表作为列表建的底层实现。
30+
31+
### 节点底层结构
32+
```c
33+
typedef struct listNode {
34+
// 前置节点
35+
struct listNode *prev;
36+
// 后置节点
37+
struct listNode *next;
38+
// 节点的值
39+
void *value;
40+
} listNode;
41+
```
42+
和其他的节点结构没啥区别...
43+
44+
### list底层结构
45+
```c
46+
typedef struct list {
47+
// 表头节点
48+
listNode *head;
49+
// 表尾节点
50+
listNode *tail;
51+
// 链表所包含的节点数量
52+
unsigned long len;
53+
// 节点值复制函数
54+
void *(*dup)(void *ptr);
55+
// 节点值是放过函数
56+
void (*free)(void *ptr);
57+
// 节点值对比函数
58+
int(*match)(void *ptr, void *key);
59+
} list;
60+
```
61+
62+
### 特性
63+
1. 链表被广泛用于实现Redis的各种功能,比如列表建、发布与订阅、慢查询、监视器等。
64+
2. 每个链表节点由一个listNode结构来表示,每个节点都有一个指向前置节点和后置节点的指针,所以Redis的链表实现是双端链表。
65+
3. 每个链表使用一个list结构表示,这个结构带有表头节点指针、表尾节点指针,以及链表长度等信息。
66+
4. 因为链表表头的前置节点和表尾节点的后置节点都指向NULL,所以Redis的链表实现是无环链表。
67+
5. 通过为链表设置不同的类型特定函数,Redis的链表可以用于保存各种不同类型的值。
68+
69+
## 字典
70+
> 说白了, 字典就是键值对。Redis的字典的底层就是哈希表而已。
71+
72+
### 哈希表
73+
```c
74+
typedef struct dictht {
75+
// 哈希表数组
76+
dictEntry **table;
77+
// 哈希表大小
78+
unsigned long size;
79+
// 哈希表大小掩码,用于计算索引值
80+
// 总是等于size-1
81+
unsigned long sizemark;
82+
// 该哈希表已有节点的数量
83+
unsigned long used;
84+
} dichht;
85+
```
86+
87+
那么,哈希表数组的结构table呢?
88+
89+
### dictEntry的结构
90+
```c
91+
typedef struct dictEntry {
92+
// 键
93+
void *key;
94+
// 值
95+
union {
96+
void *val;
97+
uint64_t u64;
98+
int64_t s64;
99+
} v;
100+
// 指向下个哈希表节点,形成链表
101+
struct dictEntry *nect;
102+
} dictEntry;
103+
```
104+
dict的底层结构省略。
105+
106+
### 哈希算法
107+
当字典被用作数据库的底层实现,或者哈希键的底层实现时,Redis使用MurmurHash算法。这种算法的优点在于即使输入的键是规律的,算法仍能给出一个个很好的随机分布性,并且算法的计算速度非常快。
108+
109+
### 哈希冲突
110+
Redis的哈希表使用链地址法来解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用这个单向链表连接起来,这就解决了键冲突的问题。
111+
112+
### 特性
113+
1. 字典被广泛用于实现Redis的各种功能,其中包括数据库和哈希键。
114+
2. Redis中的字典使用哈希表作为底层结构实现,每个字典带有两个哈希表,一个平时使用,另一个仅在进行rehash时使用。
115+
3. Redis使用MurmurHash2算法来计算键的哈希值。
116+
4. 哈希表使用链地址法来解决键冲突。
117+
118+
## 跳跃表
119+
120+
先看这样一张图:
121+
![](https://user-gold-cdn.xitu.io/2019/4/18/16a30ca4a17c6280?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)
122+
123+
如上图,我们要查找一个元素,就需要从头节点开始遍历,直到找到对应的节点或者是第一个大于要查找的元素的节点(没找到)。时间复杂度为O(N)。
124+
125+
这个查找效率是比较低的,但如果我们把列表的某些节点拔高一层,如下图,例如把每两个节点中有一个节点变成两层。那么第二层的节点只有第一层的一半,查找效率也就会提高。
126+
127+
![](https://user-gold-cdn.xitu.io/2019/4/18/16a30ca4aa58e4bb?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)
128+
129+
查找的步骤是从头节点的顶层开始,查到第一个大于指定元素的节点时,退回上一节点,在下一层继续查找。
130+
131+
比如我们要查找16:
132+
1. 从头节点的最顶层开始,先到节点7。
133+
2. 7的下一个节点是39,大于16,因此我们退回到7
134+
3. 从7开始,在下一层继续查找,就可以找到16。
135+
136+
这个例子中遍历的节点不比一维列表少,但是当节点更多,查找的数字更大时,这种做法的优势就体现出来了。还是上面的例子,如果我们要**查找的是39**,那么只需要访问两个节点(7、39)就可以找到了。这比一维列表要减少一半的数量。
137+
138+
为了避免插入操作的时间复杂度是O(N),skiplist每层的数量不会严格按照2:1的比例,而是对每个要插入的元素随机一个层数。
139+
140+
随机层数的计算过程如下:
141+
1. 每个节点都有第一层
142+
2. 那么它有第二层的概率是p,有第三层的概率是p*p
143+
3. 不能超过最大层数
144+
145+
### zskiplistNode
146+
```c
147+
typedef struct zskiplistNode {
148+
// 后退指针
149+
struct zskiplistNode *backward;
150+
// 分值 权重
151+
double score;
152+
// 成员对象
153+
robj *obj;
154+
// 层
155+
struct zskiplistLevel {
156+
// 前进指针
157+
struct zskiplistNode *forward;
158+
// 跨度
159+
unsigned int span;
160+
} leval[];
161+
} zskiplistNode;
162+
```
163+
一般来说,层的数量越多,访问其他节点的速度越快。
164+
165+
### zskipList
166+
```c
167+
typedef struct zskiplist {
168+
// 表头节点和表尾节点
169+
struct zskiplistNode *header, *tail;
170+
// 表中节点的数量
171+
unsigned long length;
172+
// 表中层数最大的节点的层数
173+
int leval;
174+
} zskiplist;
175+
```
176+
177+
### 特性
178+
1. 跳跃表是有序集合的底层实现之一
179+
2. Redis的跳跃表实现由zskiplist和zskiplistNode两个结构组成,其中zskiplist用于保存跳跃表信息(比如表头节点、表尾节点、长度),而zskiplistNode则用于表示跳跃表节点
180+
3. 每个跳跃表节点的层高都是1至32之间的随机数
181+
4. 在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是唯一的。
182+
5. 跳跃表中的节点按照分值大小进行排序,当分值相同时,节点按照成员对象的大小进行排序。
183+
184+
## 压缩列表
185+
> 一看名字,就是为了节省内存造的列表结构。压缩列表(ziplist)是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
- [Redis常见问题](https://www.processon.com/view/link/5ea2da5907912948b0d89a0a) **常见的Redis面试的问题...**
4343
- [计算机网络常见问题](https://www.processon.com/view/link/5eb8c93be401fd16f42b5f77) **常见的计算机网络面试的问题...**
4444
- [Dubbo常见问题](https://www.processon.com/view/link/5eb8c9715653bb6f2aff7c11) **常见的Dubbo的问题...**
45+
- [RocketMQ常见问题](https://www.processon.com/view/link/5ecf208f7d9c08156c6c37e3) **常见的RocketMQ的问题...**
4546

4647
## 微服务班车在线预约系统
4748
- [微服务班车在线预约系统](https://github.com/DreamCats/SchoolBus) 个人撸的项目是基于微服务架构的班车预约系统,采用**springboot+mybatis+dubbo+rocketmq+mysql+redis等**。当然,该项目也是前后端分离,前端采用比较流行的vue框架。
@@ -270,6 +271,7 @@
270271
- [MVCC的缺点](/Interview/codes/mysql/MVCC的缺点.md)
271272
- [MySQL读写分离主从复制原理](/Interview/codes/mysql/MySQL读写分离主从复制原理.md)
272273
- [查看Redis吐血系列](/Interview/crazy/个人吐血系列-总结Redis.md)
274+
- [深究Redis的底层结构](/Interview/codes/redis/深究Redis的底层结构.md)
273275
- [查看计算机网络吐血系列](/Interview/crazy/个人吐血系列-总结计算机网络.md)
274276

275277

0 commit comments

Comments
 (0)