Skip to content

Commit 8c48313

Browse files
committed
[docs update]完善 Redis常见面试题总结(上) - Redis 可以做消息队列么?
1 parent d06864f commit 8c48313

File tree

7 files changed

+250
-116
lines changed

7 files changed

+250
-116
lines changed

docs/database/mysql/mysql-index.md

+78-9
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,9 @@ MySQL 8.x 中实现的索引新特性:
123123

124124
![](https://oss.javaguide.cn/github/javaguide/open-source-project/cluster-index.png)
125125

126-
## 二级索引(辅助索引)
126+
## 二级索引
127127

128-
**二级索引又称为辅助索引,是因为二级索引的叶子节点存储的数据是主键。也就是说,通过二级索引,可以定位主键的位置。**
128+
**二级索引(Secondary Index)又称为辅助索引,是因为二级索引的叶子节点存储的数据是主键。也就是说,通过二级索引,可以定位主键的位置。**
129129

130130
唯一索引,普通索引,前缀索引等索引属于二级索引。
131131

@@ -147,7 +147,7 @@ PS: 不懂的同学可以暂存疑,慢慢往下看,后面会有答案的,
147147

148148
#### 聚簇索引介绍
149149

150-
**聚簇索引即索引结构和数据一起存放的索引,并不是一种单独的索引类型。InnoDB 中的主键索引就属于聚簇索引。**
150+
**聚簇索引(Clustered Index)即索引结构和数据一起存放的索引,并不是一种单独的索引类型。InnoDB 中的主键索引就属于聚簇索引。**
151151

152152
在 MySQL 中,InnoDB 引擎的表的 `.ibd`文件就包含了该表的索引和数据,对于 InnoDB 引擎表来说,该表的索引(B+树)的每个非叶子节点存储索引,叶子节点存储索引和索引对应的数据。
153153

@@ -167,7 +167,7 @@ PS: 不懂的同学可以暂存疑,慢慢往下看,后面会有答案的,
167167

168168
#### 非聚簇索引介绍
169169

170-
**非聚簇索引即索引结构和数据分开存放的索引,并不是一种单独的索引类型。二级索引(辅助索引)就属于非聚簇索引。MySQL 的 MyISAM 引擎,不管主键还是非主键,使用的都是非聚簇索引。**
170+
**非聚簇索引(Non-Clustered Index)即索引结构和数据分开存放的索引,并不是一种单独的索引类型。二级索引(辅助索引)就属于非聚簇索引。MySQL 的 MyISAM 引擎,不管主键还是非主键,使用的都是非聚簇索引。**
171171

172172
非聚簇索引的叶子节点并不一定存放数据的指针,因为二级索引的叶子节点就存放的是主键,根据主键再回表查数据。
173173

@@ -214,21 +214,90 @@ SELECT id FROM table WHERE id=1;
214214

215215
### 覆盖索引
216216

217-
如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为覆盖索引。我们知道在 InnoDB 存储引擎中,如果不是主键索引,叶子节点存储的是主键+列值。最终还是要“回表”,也就是要通过主键再查找一次。这样就会比较慢覆盖索引就是把要查询出的列和索引是对应的,不做回表操作!
217+
如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为 **覆盖索引(Covering Index)** 。我们知道在 InnoDB 存储引擎中,如果不是主键索引,叶子节点存储的是主键+列值。最终还是要“回表”,也就是要通过主键再查找一次。这样就会比较慢覆盖索引就是把要查询出的列和索引是对应的,不做回表操作!
218218

219219
**覆盖索引即需要查询的字段正好是索引的字段,那么直接根据该索引,就可以查到数据了,而无需回表查询。**
220220

221-
> 如主键索引,如果一条 SQL 需要查询主键,那么正好根据主键索引就可以查到主键。
222-
>
223-
> 再如普通索引,如果一条 SQL 需要查询 name,name 字段正好有索引,
221+
> 如主键索引,如果一条 SQL 需要查询主键,那么正好根据主键索引就可以查到主键。再如普通索引,如果一条 SQL 需要查询 name,name 字段正好有索引,
224222
> 那么直接根据这个索引就可以查到数据,也无需回表。
225223
226224
![覆盖索引](https://oss.javaguide.cn/github/javaguide/database/mysql20210420165341868.png)
227225

226+
我们这里简单演示一下覆盖索引的效果。
227+
228+
1、创建一个名为 `cus_order` 的表,来实际测试一下这种排序方式。为了测试方便, `cus_order` 这张表只有 `id``score``name`这 3 个字段。
229+
230+
```sql
231+
CREATE TABLE `cus_order` (
232+
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
233+
`score` int(11) NOT NULL,
234+
`name` varchar(11) NOT NULL DEFAULT '',
235+
PRIMARY KEY (`id`)
236+
) ENGINE=InnoDB AUTO_INCREMENT=100000 DEFAULT CHARSET=utf8mb4;
237+
```
238+
239+
2、定义一个简单的存储过程(PROCEDURE)来插入 100w 测试数据。
240+
241+
```sql
242+
DELIMITER ;;
243+
CREATE DEFINER=`root`@`%` PROCEDURE `BatchinsertDataToCusOder`(IN start_num INT,IN max_num INT)
244+
BEGIN
245+
DECLARE i INT default start_num;
246+
WHILE i < max_num DO
247+
insert into `cus_order`(`id`, `score`, `name`)
248+
values (i,RAND() * 1000000,CONCAT('user', i));
249+
SET i = i + 1;
250+
END WHILE;
251+
END;;
252+
DELIMITER ;
253+
```
254+
255+
存储过程定义完成之后,我们执行存储过程即可!
256+
257+
```sql
258+
CALL BatchinsertDataToCusOder(1, 1000000); # 插入100w+的随机数据
259+
```
260+
261+
等待一会,100w 的测试数据就插入完成了!
262+
263+
3、创建覆盖索引并使用 `EXPLAIN` 命令分析。
264+
265+
为了能够对这 100w 数据按照 `score` 进行排序,我们需要执行下面的 SQL 语句。
266+
267+
```sql
268+
SELECT `score`,`name` FROM `cus_order` ORDER BY `score` DESC;#降序排序
269+
```
270+
271+
使用 `EXPLAIN` 命令分析这条 SQL 语句,通过 `Extra` 这一列的 `Using filesort` ,我们发现是没有用到覆盖索引的。
272+
273+
![](https://oss.javaguide.cn/github/javaguide/mysql/not-using-covering-index-demo.png)
274+
275+
不过这也是理所应当,毕竟我们现在还没有创建索引呢!
276+
277+
我们这里以 `score``name` 两个字段建立联合索引:
278+
279+
```sql
280+
ALTER TABLE `cus_order` ADD INDEX id_score_name(score, name);
281+
```
282+
283+
创建完成之后,再用 `EXPLAIN` 命令分析再次分析这条 SQL 语句。
284+
285+
![](https://oss.javaguide.cn/github/javaguide/mysql/using-covering-index-demo.png)
286+
287+
通过 `Extra` 这一列的 `Using index` ,说明这条 SQL 语句成功使用了覆盖索引。
288+
289+
关于 `EXPLAIN` 命令的详细介绍请看:[MySQL 执行计划分析](https://javaguide.cn/database/mysql/mysql-query-execution-plan.html)这篇文章。
290+
228291
### 联合索引
229292

230293
使用表中的多个字段创建索引,就是 **联合索引**,也叫 **组合索引****复合索引**
231294

295+
`score``name` 两个字段建立联合索引:
296+
297+
```sql
298+
ALTER TABLE `cus_order` ADD INDEX id_score_name(score, name);
299+
```
300+
232301
### 最左前缀匹配原则
233302

234303
最左前缀匹配原则指的是,在使用联合索引时,**MySQL** 会根据联合索引中的字段顺序,从左到右依次到查询条件中去匹配,如果查询条件中存在与联合索引中最左侧字段相匹配的字段,则就会使用该字段过滤一批数据,直至联合索引中全部字段匹配完成,或者在执行过程中遇到范围查询(如 **`>`****`<`**)才会停止匹配。对于 **`>=`****`<=`****`BETWEEN`****`like`** 前缀匹配的范围查询,并不会停止匹配。所以,我们在使用联合索引时,可以将区分度高的字段放在最左边,这也可以过滤更多数据。
@@ -326,4 +395,4 @@ mysql> EXPLAIN SELECT `score`,`name` FROM `cus_order` ORDER BY `score` DESC;
326395
| filtered | 按表条件过滤后,留存的记录数的百分比 |
327396
| Extra | 附加信息 |
328397

329-
篇幅问题,我这里只是简单介绍了一下 MySQL 执行计划,详细介绍请看:[SQL 的执行计划](https://javaguide.cn/database/mysql/mysql-query-execution-plan.html)这篇文章。
398+
篇幅问题,我这里只是简单介绍了一下 MySQL 执行计划,详细介绍请看:[MySQL 执行计划分析](https://javaguide.cn/database/mysql/mysql-query-execution-plan.html)这篇文章。

docs/database/redis/redis-memory-fragmentation.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,4 @@ config set active-defrag-cycle-max 50
119119

120120
- Redis 官方文档:https://redis.io/topics/memory-optimization
121121
- Redis 核心技术与实战 - 极客时间 - 删除数据后,为什么内存占用率还是很高?:https://time.geekbang.org/column/article/289140
122-
- Redis 源码解析——内存分配:https://shinerio.cc/2020/05/17/redis/Redis%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90%E2%80%94%E2%80%94%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86/
122+
- Redis 源码解析——内存分配:<https://shinerio.cc/2020/05/17/redis/Redis源码解析——内存管理>

docs/database/redis/redis-questions-01.md

+71-14
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ Memcached 是分布式缓存最开始兴起的那会,比较常用的。后来
9393
9494
由此可见,直接操作缓存能够承受的数据库请求数量是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。进而,我们也就提高了系统整体的并发。
9595

96+
### 常见的缓存读写策略有哪些?
97+
98+
关于常见的缓存读写策略的详细介绍,可以看我写的这篇文章:[3 种常用的缓存读写策略详解](https://javaguide.cn/database/redis/3-commonly-used-cache-read-and-write-strategies.html)
99+
100+
## Redis 应用
101+
96102
### Redis 除了做缓存,还能做什么?
97103

98104
- **分布式锁** : 通过 Redis 来做分布式锁是一种比较常见的方式。通常情况下,我们都是基于 Redisson 来实现分布式锁。关于 Redis 实现分布式锁的详细介绍,可以看我写的这篇文章:[分布式锁详解](https://javaguide.cn/distributed-system/distributed-lock.html)
@@ -101,33 +107,81 @@ Memcached 是分布式缓存最开始兴起的那会,比较常用的。后来
101107
- **复杂业务场景** :通过 Redis 以及 Redis 扩展(比如 Redisson)提供的数据结构,我们可以很方便地完成很多复杂的业务场景比如通过 bitmap 统计活跃用户、通过 sorted set 维护排行榜。
102108
- ......
103109

110+
### 如何基于 Redis 实现分布式锁?
111+
112+
关于 Redis 实现分布式锁的详细介绍,可以看我写的这篇文章:[分布式锁详解](https://javaguide.cn/distributed-system/distributed-lock.html)
113+
104114
### Redis 可以做消息队列么?
105115

106-
Redis 5.0 新增加的一个数据结构 `Stream` 可以用来做消息队列,`Stream` 支持:
116+
> 实际项目中也没见谁使用 Redis 来做消息队列,对于这部分知识点大家了解就好了。
117+
118+
**Redis 2.0 之前,如果想要使用 Redis 来做消息队列的话,只能通过 List 来实现。**
119+
120+
通过 `RPUSH/LPOP` 或者 `LPUSH/RPOP`即可实现简易版消息队列 :
121+
122+
```bash
123+
# 生产者生产消息
124+
> RPUSH myList msg1 msg2
125+
(integer) 2
126+
> RPUSH myList msg3
127+
(integer) 3
128+
# 消费者消费消息
129+
> LPOP myList
130+
"msg1"
131+
```
132+
133+
不过,通过 `RPUSH/LPOP` 或者 `LPUSH/RPOP`这样的方式存在性能问题,我们需要不断轮询去调用 `RPOP``LPOP` 来消费消息。当 List 为空时,大部分的轮询的请求都是无效请求,这种方式大量浪费了系统资源。
134+
135+
因此,Redis 还提供了 `BLPOP``BRPOP` 这种阻塞式读取的命令(带 B-Bloking 的都是阻塞式),并且还支持一个超时参数。如果 List 为空,Redis 服务端不会立刻返回结果,它会等待 List 中有新数据后在返回或者是等待最多一个超时时间后返回空。如果将超时时间设置为 0 时,即可无限等待,直到弹出消息
136+
137+
```bash
138+
# 超时时间为 10s
139+
# 如果有数据立刻返回,否则最多等待10秒
140+
> BRPOP myList 10
141+
null
142+
```
143+
144+
**List 实现消息队列功能太简单,像消息确认机制等功能还需要我们自己实现,最要命的是没有广播机制,消息也只能被消费一次。**
145+
146+
**Redis 2.0 引入了 发布订阅 (pub/sub) 解决了 List 实现消息队列没有广播机制的问题。**
147+
148+
pub/sub 中引入了一个概念叫 channel(频道),发布订阅机制的实现就是基于这个 channel 来做的。
149+
150+
pub/sub 涉及发布者和订阅者(也叫消费者)两个角色:
151+
152+
- 发布者通过 `PUBLISH` 投递消息给指定 channel。
153+
- 订阅者通过`SUBSCRIBE`订阅它关心的 channel。并且,订阅者可以订阅一个或者多个 channel。
154+
155+
我们这里启动 3 个 Redis 客户端来简单演示一下:
156+
157+
![pub/sub 实现消息队列演示](https://oss.javaguide.cn/github/javaguide/database/redis/redis-pubsub-message-queue.png)
158+
159+
pub/sub 既能单播又能广播,还支持 channel 的简单正则匹配。不过,消息丢失(客户端断开连接或者 Redis 宕机都会导致消息丢失)、消息堆积(发布者发布消息的时候不会管消费者的具体消费能力如何)等问题依然没有一个比较好的解决办法。
160+
161+
为此,Redis 5.0 新增加的一个数据结构 `Stream` 来做消息队列。`Stream` 支持:
107162

108163
- 发布 / 订阅模式
109164
- 按照消费者组进行消费
110165
- 消息持久化( RDB 和 AOF)
111166

112-
不过,和专业的消息队列相比,还是有很多欠缺的地方比如消息丢失和堆积问题不好解决。因此,我们通常建议是不使用 Redis 来做消息队列的,你完全可以选择市面上比较成熟的一些消息队列比如 RocketMQ、Kafka。
113-
114-
相关文章推荐:[Redis 消息队列的三种方案(List、Streams、Pub/Sub)](https://javakeeper.starfish.ink/data-management/Redis/Redis-MQ.html)
167+
`Stream` 使用起来相对要麻烦一些,这里就不演示了。而且,`Stream` 在实际使用中依然会有一些小问题不太好解决比如在 Redis 发生故障恢复后不能保证消息至少被消费一次。
115168

116-
### 如何基于 Redis 实现分布式锁?
169+
综上,和专业的消息队列相比,使用 Redis 来实现消息队列还是有很多欠缺的地方比如消息丢失和堆积问题不好解决。因此,我们通常建议不要使用 Redis 来做消息队列,你完全可以选择市面上比较成熟的一些消息队列比如 RocketMQ、Kafka。
117170

118-
关于 Redis 实现分布式锁的详细介绍,可以看我写的这篇文章:[分布式锁详解](https://javaguide.cn/distributed-system/distributed-lock.html)
171+
相关阅读:[Redis 消息队列发展历程 - 阿里开发者 - 2022](https://mp.weixin.qq.com/s/gCUT5TcCQRAxYkTJfTRjJw)
119172

120173
## Redis 数据结构
121174

175+
> 关于 Redis 5 种基础数据结构和 3 种特殊数据结构的详细介绍请看下面这两篇文章:
176+
>
177+
> - [Redis 5 种基本数据结构详解](https://javaguide.cn/database/redis/redis-data-structures-01.html)
178+
> - [Redis 3 种特殊数据结构详解](https://javaguide.cn/database/redis/redis-data-structures-02.html)
179+
122180
### Redis 常用的数据结构有哪些?
123181

124182
- **5 种基础数据结构** :String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。
125183
- **3 种特殊数据结构** :HyperLogLogs(基数统计)、Bitmap (位存储)、Geospatial (地理位置)。
126184

127-
关于 5 种基础数据结构的详细介绍请看这篇文章:[Redis 5 种基本数据结构详解](https://javaguide.cn/database/redis/redis-data-structures-01.html)
128-
129-
关于 3 种特殊数据结构的详细介绍请看这篇文章:[Redis 3 种特殊数据结构详解](https://javaguide.cn/database/redis/redis-data-structures-02.html)
130-
131185
### String 的应用场景有哪些?
132186

133187
- 常规数据(比如 session、token、、序列化后的对象)的缓存;
@@ -364,20 +418,23 @@ Redis 通过 **IO 多路复用程序** 来监听来自客户端的大量连接
364418

365419
虽然,Redis6.0 引入了多线程,但是 Redis 的多线程只是在网络数据的读写这类耗时操作上使用了,执行命令仍然是单线程顺序执行。因此,你也不需要担心线程安全问题。
366420

367-
Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需要设置IO线程数 > 1,需要修改 redis 配置文件 `redis.conf`
421+
Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需要设置 IO 线程数 > 1,需要修改 redis 配置文件 `redis.conf`
368422

369423
```bash
370424
io-threads 4 #设置1的话只会开启主线程,官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程
371425
```
426+
372427
另外:
373-
- io-threads的个数一旦设置,不能通过config动态设置
374-
- 当设置ssl后,io-threads将不工作
375428

376-
开启多线程后,默认只会使用多线程进行IO写入writes,即发送数据给客户端,如果需要开启多线程IO读取reads,同样需要修改 redis 配置文件 `redis.conf` :
429+
- io-threads 的个数一旦设置,不能通过 config 动态设置。
430+
- 当设置 ssl 后,io-threads 将不工作。
431+
432+
开启多线程后,默认只会使用多线程进行 IO 写入 writes,即发送数据给客户端,如果需要开启多线程 IO 读取 reads,同样需要修改 redis 配置文件 `redis.conf` :
377433

378434
```bash
379435
io-threads-do-reads yes
380436
```
437+
381438
但是官网描述开启多线程读并不能有太大提升,因此一般情况下并不建议开启
382439

383440
相关阅读:

0 commit comments

Comments
 (0)