init: 初始化提交--源码学习笔记

This commit is contained in:
2025-07-21 01:06:10 +08:00
commit 018831037a
924 changed files with 164246 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -0,0 +1,731 @@
# 精尽 Netty 面试题「最新更新时间2024-02」
以下面试题,基于网络整理,和自己编辑。具体参考的文章,会在文末给出所有的链接。
如果胖友有自己的疑问,欢迎在星球提问,我们一起整理吊吊的 Netty 面试题的大保健。
而题目的难度,艿艿尽量按照从容易到困难的顺序,逐步下去。
## BIO 是什么?
🦅 **概念**
- BIO ,全称 Block-IO ,是一种**阻塞** + **同步**的通信模式。
- 是一个比较传统的通信方式,模式简单,使用方便。但并发处理能力低,通信耗时,依赖网速。
🦅 **原理**
- 服务器通过一个 Acceptor 线程,负责监听客户端请求和为每个客户端创建一个新的线程进行链路处理。典型的**一请求一应答模式**。
- 若客户端数量增多,频繁地创建和销毁线程会给服务器打开很大的压力。后改良为用线程池的方式代替新增线程,被称为伪异步 IO 。
🦅 **示例**
- 代码参见 [bio](https://github.com/ITDragonBlog/daydayup/tree/master/Netty/socket-io/src/com/itdragon/bio) 。
🦅 **小结**
BIO 模型中,通过 Socket 和 ServerSocket 实现套接字通道的通信。阻塞,同步,建立连接耗时。
## NIO 是什么?
🦅 **概念**
- NIO ,全称 New IO ,也叫 Non-Block IO ,是一种**非阻塞** + 同步的通信模式。
- [《精尽 Netty 源码分析 —— NIO 基础(一)之简介》](http://svip.iocoder.cn/Netty/nio-1-intro/)
🦅 **原理**
- NIO 相对于 BIO 来说一大进步。客户端和服务器之间通过 Channel 通信。NIO 可以在 Channel 进行读写操作。这些 Channel 都会被注册在 Selector 多路复用器上。Selector 通过一个线程不停的轮询这些 Channel 。找出已经准备就绪的 Channel 执行 IO 操作。
- NIO 通过一个线程轮询,实现千万个客户端的请求,这就是非阻塞 NIO 的特点。
- 缓冲区 Buffer :它是 NIO 与 BIO 的一个重要区别。
- BIO 是将数据直接写入或读取到流 Stream 对象中。
- NIO 的数据操作都是在 Buffer 中进行的。Buffer 实际上是一个数组。Buffer 最常见的类型是ByteBuffer另外还有 CharBufferShortBufferIntBufferLongBufferFloatBufferDoubleBuffer。
- [《精尽 Netty 源码分析 —— NIO 基础(三)之 Buffer》](http://svip.iocoder.cn/Netty/nio-3-buffer/)
- 通道 Channel :和流 Stream 不同通道是双向的。NIO可以通过 Channel 进行数据的读、写和同时读写操作。
- 通道分为两大类一类是网络读写SelectableChannel一类是用于文件操作FileChannel。我们使用的是前者 SocketChannel 和 ServerSocketChannel 都是SelectableChannel 的子类。
- [《精尽 Netty 源码分析 —— NIO 基础(二)之 Channel》](http://svip.iocoder.cn/Netty/nio-2-channel/)
- 多路复用器 Selector NIO 编程的基础。多路复用器提供选择已经就绪的任务的能力:就是 Selector 会不断地轮询注册在其上的通道Channel如果某个通道处于就绪状态会被 Selector 轮询出来,然后通过 SelectionKey 可以取得就绪的Channel集合从而进行后续的 IO 操作。
- 服务器端只要提供一个线程负责 Selector 的轮询,就可以接入成千上万个客户端,这就是 JDK NIO 库的巨大进步。
- [《精尽 Netty 源码分析 —— NIO 基础(四)之 Selector》](http://svip.iocoder.cn/Netty/nio-4-selector/)
🦅 **示例**
- 代码参见 [nio](https://github.com/ITDragonBlog/daydayup/tree/master/Netty/socket-io/src/com/itdragon/nio)
- [《精尽 Netty 源码分析 —— NIO 基础(五)之示例》](http://svip.iocoder.cn/Netty/nio-5-demo/)
🦅 **小结**
NIO 模型中通过 SocketChannel 和 ServerSocketChannel 实现套接字通道的通信。非阻塞,同步,避免为每个 TCP 连接创建一个线程。
🦅 **继续挖掘**
可能有胖友对非阻塞和阻塞,同步和异步的定义有点懵逼,我们再来看下 [《精尽 Netty 源码分析 —— NIO 基础(一)之简介》](http://svip.iocoder.cn/Netty/nio-1-intro/) 提到的一段话:
> 老艿艿:在一些文章中,会将 Java NIO 描述成**异步** IO ,实际是不太正确的: Java NIO 是**同步** IO Java AIO ( 也称为 NIO 2 )是**异步** IO。具体原因推荐阅读文章
>
> - [《异步和非阻塞一样吗? (内容涉及 BIO, NIO, AIO, Netty)》](https://blog.csdn.net/matthew_zhang/article/details/71328697) 。
> - [《BIO与NIO、AIO的区别(这个容易理解)》](https://blog.csdn.net/skiof007/article/details/52873421)
>
> 总结来说,在 **Unix IO 模型**的语境下:
>
> - 同步和异步的区别:数据拷贝阶段是否需要完全由操作系统处理。
> - 阻塞和非阻塞操作:是针对发起 IO 请求操作后,是否有立刻返回一个标志信息而不让请求线程等待。
>
> 因此Java NIO 是**同步**且非阻塞的 IO 。
- 另外,胖友在瞅瞅下面这个图来理解下:[![Unix 的 5 种 IO 模型](http://static.iocoder.cn/images/Netty/2017_10_24/01.png)](http://static.iocoder.cn/images/Netty/2017_10_24/01.png)Unix 的 5 种 IO 模型
## AIO 是什么?
> 艿艿:这个面试题,重点在于陈述我们对 BIO、NIO 的理解,对于 AIO 来说,基本理解即可。
🦅 **概念**
- AIO ,全称 Asynchronous IO ,也叫 NIO**2** ,是一种**非阻塞** + **异步**的通信模式。在 NIO 的基础上,引入了新的异步通道的概念,并提供了异步文件通道和异步套接字通道的实现。
- 原理:
- AIO 并没有采用 NIO 的多路复用器,而是使用异步通道的概念。其 readwrite 方法的返回类型,都是 Future 对象。而 Future 模型是异步的,其核心思想是:**去主函数等待时间**。
🦅 **示例**
- 代码参见 [aio](https://github.com/ITDragonBlog/daydayup/tree/master/Netty/socket-io/src/com/itdragon/aio)
🦅 **小结**
AIO 模型中通过 AsynchronousSocketChannel 和 AsynchronousServerSocketChannel 实现套接字通道的通信。非阻塞,异步。
## BIO、NIO 有什么区别?
- 线程模型不同
- BIO一个连接一个线程客户端有连接请求时服务器端就需要启动一个线程进行处理。所以线程开销大。可改良为用线程池的方式代替新创建线程被称为伪异步 IO 。
- NIO一个请求一个线程但客户端发送的连接请求都会注册到多路复用器上多路复用器轮询到连接有新的 I/O 请求时,才启动一个线程进行处理。可改良为一个线程处理多个请求,基于 [多 Reactor 模型](http://svip.iocoder.cn/Netty/EventLoop-1-Reactor-Model/)。
- BIO 是面向流( Stream )的,而 NIO 是面向缓冲区( Buffer )的。
- BIO 的各种操作是阻塞的,而 NIO 的各种操作是非阻塞的。
- BIO 的 Socket 是单向的,而 NIO 的 Channel 是双向的。
可能文字比较难记,整理出来就是下图:[![BIO 对比 NIO 对比 AIO](01-Netty 面试题.assets/02.png)](http://static.iocoder.cn/images/Netty/2017_10_24/02.png)BIO 对比 NIO 对比 AIO
- 有一点要注意,虽然图中说 NIO 的性能一般但是在绝大多数我们日常业务场景NIO 和 AIO 的性能差距实际没这么大。在 Netty5 中,基于 AIO 改造和支持,最后发现,性能并没有想象中这么强悍,所以 Netty5 被废弃,而是继续保持 Netty4 为主版本,使用 NIO 为主。
为了胖友能更好的记住和理解 BIO、NIO、AIO 的流程,胖友可以在理解下图:[![BIO、NIO、AIO 的流程图](http://static.iocoder.cn/images/Netty/2017_10_24/03.png)](http://static.iocoder.cn/images/Netty/2017_10_24/03.png)BIO、NIO、AIO 的流程图
## 什么是 Netty
> Netty 是一款提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。
>
> 也就是说Netty 是一个基于 NIO 的客户、服务器端编程框架。使用 Netty 可以确保你快速和简单地开发出一个网络应用例如实现了某种协议的客户服务端应用。Netty 相当简化和流线化了网络应用的编程开发过程例如TCP 和 UDP 的 socket 服务开发。
>
> (以上摘自百度百科)。
Netty 具有如下特性( 摘自《Netty in Action》 )
| 分类 | Netty的特性 |
| :------- | :----------------------------------------------------------- |
| 设计 | 1. 统一的 API ,支持多种传输类型( 阻塞和非阻塞的 ) 2. 简单而强大的线程模型 3. 真正的无连接数据报套接字( UDP )支持 4. 连接逻辑组件( ChannelHander 中顺序处理消息 )以及组件复用( 一个 ChannelHandel 可以被多个ChannelPipeLine 复用 ) |
| 易于使用 | 1. 详实的 Javadoc 和大量的示例集 2. 不需要超过 JDK 1.6+ 的依赖 |
| 性能 | 拥有比 Java 的核心 API 更高的吞吐量以及更低的延迟( 得益于池化和复用 ),更低的资源消耗以及最少的内存复制 |
| 健壮性 | 1. 不会因为慢速、快速或者超载的连接而导致 OutOfMemoryError 2. 消除在高速网络中 NIO 应用程序常见的不公平读 / 写比率 |
| 安全性 | 完整的 SSL/TLS 以及 StartTLs 支持,可用于受限环境下,如 Applet 和 OSGI |
| 社区驱动 | 发布快速而且频繁 |
## 为什么选择 Netty
- **使用简单**API 使用简单,开发门槛低。
- **功能强大**:预置了多种编解码功能,支持多种主流协议。
- **定制能力强**:可以通过 ChannelHandler 对通信框架进行灵活的扩展。
- **性能高**:通过与其它业界主流的 NIO 框架对比Netty 的综合性能最优。
- **成熟稳定**Netty 修复了已经发现的所有 JDK NIO BUG业务开发人员不需要再为 NIO 的 BUG 而烦恼。
- **社区活跃**版本迭代周期短发现的BUG可以被及时修复同时更多的新功能会被加入。
- **案例丰富**:经历了大规模的商业应用考验,质量已经得到验证。在互联网、大数据、网络游戏、企业应用、电信软件等众多行业得到成功商用,证明了它可以完全满足不同行业的商业应用。
实际上,这个也是我们做技术选型的一些参考点,不仅仅适用于 Netty ,也同样适用于其他技术栈。当然,😈 面试都可以酱紫回答,显得很高端。
## 为什么说 Netty 使用简单?
🦅 我们假设要搭建一个 Server 服务器,使用 **Java NIO 的步骤**如下:
1. 创建 ServerSocketChannel 。
- 绑定监听端口,并配置为非阻塞模式。
2. 创建 Selector将之前创建的 ServerSocketChannel 注册到 Selector 上,监听
```
SelectionKey.OP_ACCEPT
```
- 循环执行 `Selector#select()` 方法,轮询就绪的 Channel。
3. 轮询就绪的 Channel 时,如果是处于
```
OP_ACCEPT
```
状态,说明是新的客户端接入,调用
```
ServerSocketChannel#accept()
```
方法,接收新的客户端。
- 设置新接入的 SocketChannel 为非阻塞模式,并注册到 Selector 上,监听 `OP_READ` 。
4. 如果轮询的 Channel 状态是
```
OP_READ
```
,说明有新的就绪数据包需要读取,则构造 ByteBuffer 对象,读取数据。
- 这里,解码数据包的过程,需要我们自己编写。
> 艿艿:注意噢,上述步骤还是最简的 Java NIO 启动步骤,不包括**多 Reactor 多线程模型**噢!可能有胖友不知道什么是 Reactor 模型,在 [「什么是 Reactor 模型?」](https://svip.iocoder.cn/Netty/Interview/#) 问题中,我们会详细解释。
🦅 使用 **Netty 的步骤**如下:
1. 创建 NIO 线程组 EventLoopGroup 和 ServerBootstrap。
- 设置 ServerBootstrap 的属性线程组、SO_BACKLOG 选项,设置 NioServerSocketChannel 为 Channel
- 设置业务处理 Handler 和 编解码器 Codec 。
- 绑定端口,启动服务器程序。
2. 在业务处理 Handler 中,处理客户端发送的数据,并给出响应。
🦅 那么相比 Java NIO使用 Netty 开发程序,都**简化了哪些步骤**呢?
1. 无需关心 `OP_ACCEPT`、`OP_READ`、`OP_WRITE` 等等 **IO 操作**Netty 已经封装,对我们在使用是透明无感的。
2. 使用 boss 和 worker EventLoopGroup Netty 直接提供**多 Reactor 多线程模型**。
3. 在 Netty 中,我们看到有使用一个解码器 FixedLengthFrameDecoder可以用于处理定长消息的问题能够解决 **TCP 粘包拆包**问题,十分方便。如果使用 Java NIO ,需要我们自行实现解码器。
------
😈 如果胖友不知道如何使用 Java NIO 编写一个 Server ,建议自己去实现以下。
😈 如果胖友没有使用过 Netty 编写一个 Server ,建议去入门下。
## 说说业务中 Netty 的使用场景?
- 构建高性能、低时延的各种 Java 中间件Netty 主要作为基础通信框架提供高性能、低时延的通信服务。例如:
- RocketMQ ,分布式消息队列。
- Dubbo ,服务调用框架。
- Spring WebFlux ,基于响应式的 Web 框架。
- HDFS ,分布式文件系统。
- 公有或者私有协议栈的基础通信框架,例如可以基于 Netty 构建异步、高性能的 WebSocket、Protobuf 等协议的支持。
- 各领域应用例如大数据、游戏等Netty 作为高性能的通信框架用于内部各模块的数据分发、传输和汇总等,实现模块之间高性能通信。
## 说说 Netty 如何实现高性能?
1. **线程模型** :更加优雅的 Reactor 模式实现、灵活的线程模型、利用 EventLoop 等创新性的机制,可以非常高效地管理成百上千的 Channel 。
2. **内存池设计** :使用池化的 Direct Buffer 等技术,在提高 IO 性能的同时,减少了对象的创建和销毁。并且,内吃吃的内部实现是用一颗二叉查找树,更好的管理内存分配情况。
3. **内存零拷贝** :使用 Direct Buffer ,可以使用 Zero-Copy 机制。
> Zero-Copy ,在操作数据时,不需要将数据 Buffer 从一个内存区域拷贝到另一个内存区域。因为少了一次内存的拷贝,因此 CPU 的效率就得到的提升。
4. **协议支持** :提供对 Protobuf 等高性能序列化协议支持。
5. 使用更多本地代码
。例如:
- 直接利用 JNI 调用 Open SSL 等方式,获得比 Java 内建 SSL 引擎更好的性能。
- 利用 JNI 提供了 Native Socket Transport ,在使用 Epoll edge-triggered 的情况下,可以有一定的性能提升。
6. 其它:
- 利用反射等技术直接操纵 SelectionKey ,使用数组而不是 Java 容器等。
- 实现 [FastThreadLocal](https://segmentfault.com/a/1190000012926809) 类,当请求频繁时,带来更好的性能。
- ….
另外,推荐阅读白衣大大的两篇文章:
1. [《Netty高性能编程备忘录(上)》](http://calvin1978.blogcn.com/articles/netty-performance.html)
2. [《Netty高性能编程备忘录》](http://calvin1978.blogcn.com/articles/netty-performance2.html)
> 下面三连问!
>
> Netty 是一个高性能的、高可靠的、可扩展的异步通信框架,那么高性能、高可靠、可扩展设计体现在哪里呢?
## Netty 的高性能如何体现?
> 这个问题,和 [「说说 Netty 如何实现高性能?」](https://svip.iocoder.cn/Netty/Interview/#) 问题,会有点重叠。没事,反正理解就好,也背不下来。哈哈哈哈。
性能是设计出来的而不是测试出来的。那么Netty 的架构设计是如何实现高性能的呢?
1. **线程模型** :采用异步非阻塞的 I/O 类库,基于 Reactor 模式实现,解决了传统同步阻塞 I/O 模式下服务端无法平滑处理客户端线性增长的问题。
2. **堆外内存** TCP 接收和发送缓冲区采用直接内存代替堆内存,避免了内存复制,提升了 I/O 读取和写入性能。
3. **内存池设计** :支持通过内存池的方式循环利用 ByteBuf避免了频繁创建和销毁 ByteBuf 带来的性能消耗。
4. **参数配置** :可配置的 I/O 线程数目和 TCP 参数等,为不同用户提供定制化的调优参数,满足不同的性能场景。
5. **队列优化** :采用环形数组缓冲区,实现无锁化并发编程,代替传统的线程安全容器或锁。
6. **并发能力** :合理使用线程安全容器、原子类等,提升系统的并发能力。
7. **降低锁竞争** :关键资源的使用采用单线程串行化的方式,避免多线程并发访问带来的锁竞争和额外的 CPU 资源消耗问题。
8. 内存泄露检测
:通过引用计数器及时地释放不再被引用的对象,细粒度的内存管理降低了 GC 的频率,减少频繁 GC 带来的时延增大和 CPU 损耗。
- [《精尽 Netty 源码解析 —— Buffer 之 ByteBuf内存泄露检测》](http://svip.iocoder.cn/Netty/ByteBuf-1-3-ByteBuf-resource-leak-detector/)
## Netty 的高可靠如何体现?
1. 链路有效性检测
:由于长连接不需要每次发送消息都创建链路,也不需要在消息完成交互时关闭链路,因此相对于短连接性能更高。为了保证长连接的链路有效性,往往需要通过心跳机制周期性地进行链路检测。使用心跳机制的原因是,避免在系统空闲时因网络闪断而断开连接,之后又遇到海量业务冲击导致消息积压无法处理。为了解决这个问题,需要周期性地对链路进行有效性检测,一旦发现问题,可以及时关闭链路,重建 TCP 连接。为了支持心跳Netty 提供了两种链路空闲检测机制:
- 读空闲超时机制:连续 T 周期没有消息可读时,发送心跳消息,进行链路检测。如果连续 N 个周期没有读取到心跳消息,可以主动关闭链路,重建连接。
- 写空闲超时机制:连续 T 周期没有消息需要发送时,发送心跳消息,进行链路检测。如果连续 N 个周期没有读取对方发回的心跳消息,可以主动关闭链路,重建连接。
- [《精尽 Netty 源码解析 —— ChannelHandler之 IdleStateHandler》](http://svip.iocoder.cn/Netty/ChannelHandler-5-idle/)
2. 内存保护机制
Netty 提供多种机制对内存进行保护,包括以下几个方面:
- 通过对象引用计数器对 ByteBuf 进行细粒度的内存申请和释放,对非法的对象引用进行检测和保护。
- 可设置的内存容量上限,包括 ByteBuf、线程池线程数等避免异常请求耗光内存。
3. 优雅停机
优雅停机功能指的是当系统推出时JVM 通过注册的 Shutdown Hook 拦截到退出信号量,然后执行推出操作,释放相关模块的资源占用,将缓冲区的消息处理完成或清空,将待刷新的数据持久化到磁盘和数据库中,等到资源回收和缓冲区消息处理完成之后,再退出。
- [《精尽 Netty 源码解析 —— EventLoop之 EventLoop 优雅关闭》](http://svip.iocoder.cn/Netty/EventLoop-8-EventLoop-shutdown/)
## Netty 的可扩展如何体现?
可定制、易扩展。
- **责任链模式** ChannelPipeline 基于责任链模式开发,便于业务逻辑的拦截、定制和扩展。
- **基于接口的开发** :关键的类库都提供了接口或抽象类,便于用户自定义实现。
- **提供大量的工厂类** :通过重载这些工厂类,可以按需创建出用户需要的对象。
- **提供大量系统参数** :供用户按需设置,增强系统的场景定制性。
------
> 艿艿:说个题外话。
>
> 实际上,任何的技术的研究,我们都可以去思考,它的高性能是怎么体现的,它的可靠性是怎么体现的,它的可拓展是怎么体现的。
>
> 当然,因为很多时候有近义词,所以:
>
> - 高性能 => 高并发
> - 可靠性 => 高可用
> - 可拓展 => 高拓展
>
> 例如说MySQL 如何实现高性能MySQL 如何搭建高可用,😈 MySQL 如何做拓展貌似暂时没,哈哈哈哈。
## 简单介绍 Netty 的核心组件?
Netty 有如下六个核心组件:
- Bootstrap & ServerBootstrap
- Channel
- ChannelFuture
- EventLoop & EventLoopGroup
- ChannelHandler
- ChannelPipeline
详细的,请直接阅读 [《精尽 Netty 源码分析 —— Netty 简介(二)之核心组件》](http://svip.iocoder.cn/Netty/intro-2/) 一文。
## 说说 Netty 的逻辑架构?
Netty 采用了典型的**三层网络架构**进行设计和开发,其逻辑架构如下图所示:
[![Netty 逻辑架构图](http://static.iocoder.cn/85d98e3cb0e6e39f80d02234e039a4dd)](http://static.iocoder.cn/85d98e3cb0e6e39f80d02234e039a4dd)Netty 逻辑架构图
> 艿艿:注意,这个图是自下向上看。哈哈哈~
1. **Reactor 通信调度层**:由一系列辅助类组成,包括 Reactor 线程 NioEventLoop 及其父类NioSocketChannel 和 NioServerSocketChannel 等等。该层的职责就是监听网络的读写和连接操作,负责将网络层的数据读到内存缓冲区,然后触发各自网络事件,例如连接创建、连接激活、读事件、写事件等。将这些事件触发到 pipeline 中,由 pipeline 管理的职责链来进行后续的处理。
2. **职责链 ChannelPipeline**:负责事件在职责链中的有序传播,以及负责动态地编排职责链。职责链可以选择监听和处理自己关心的事件,拦截处理和向后传播事件。
3. **业务逻辑编排层**:业务逻辑编排层通常有两类,一类是纯粹的业务逻辑编排,一类是应用层协议插件,用于特定协议相关的会话和链路管理。由于应用层协议栈往往是开发一次到处运行,并且变动较小,故而将应用协议到 POJO 的转变和上层业务放到不同的 ChannelHandler 中,就可以实现协议层和业务逻辑层的隔离,实现架构层面的分层隔离。
## 什么是 Reactor 模型?
直接阅读 [《精尽 Netty 源码解析 —— EventLoop之 Reactor 模型》](http://svip.iocoder.cn/Netty/EventLoop-1-Reactor-Model/) 一文。
认真仔细读,这是一个高频面试题。
## 请介绍 Netty 的线程模型?
还是阅读 [《精尽 Netty 源码解析 —— EventLoop之 Reactor 模型》](http://svip.iocoder.cn/Netty/EventLoop-1-Reactor-Model/) 一文。
认真仔细读,这真的真的真的是一个高频面试题。
## 什么是业务线程池?
🦅 **问题**
在 [「什么是 Reactor 模型?」](https://svip.iocoder.cn/Netty/Interview/#) 问题中,无论是那种类型的 Reactor 模型,都需要在 Reactor 所在的线程中,进行读写操作。那么此时就会有一个问题,如果我们读取到数据,需要进行业务逻辑处理,并且这个业务逻辑需要对数据库、缓存等等进行操作,会有什么问题呢?假设这个数据库操作需要 5 ms ,那就意味着这个 Reactor 线程在这 5 ms 无法进行注册在这个 Reactor 的 Channel 进行读写操作。也就是说,多个 Channel 的所有读写操作都变成了串行。势必,这样的效率会非常非常非常的低。
🦅 **解决**
那么怎么解决呢创建业务线程池将读取到的数据提交到业务线程池中进行处理。这样Reactor 的 Channel 就不会被阻塞,而 Channel 的所有读写操作都变成了并行了。
🦅 **案例**
如果胖友熟悉 Dubbo 框架,就会发现 [《Dubbo 用户指南 —— 线程模型》](http://dubbo.apache.org/zh-cn/docs/user/demos/thread-model.html) 。😈 认真读下,可以跟面试官吹一吹啦。
## TCP 粘包 / 拆包的原因?应该这么解决?
🦅 **概念**
TCP 是以流的方式来处理数据,所以会导致粘包 / 拆包。
- 拆包:一个完整的包可能会被 TCP 拆分成多个包进行发送。
- 粘包:也可能把小的封装成一个大的数据包发送。
🦅 **原因**
- 应用程序写入的字节大小大于套接字发送缓冲区的大小,会发生**拆包**现象。而应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生**粘包**现象。
- 待发送数据大于 MSS最大报文长度TCP 在传输前将进行**拆包**。
- 以太网帧的 payload净荷大于 MTU默认为 1500 字节)进行 IP 分片**拆包**。
- 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生**粘包**。
🦅 **解决**
在 Netty 中,提供了多个 Decoder 解析类,如下:
- ① FixedLengthFrameDecoder ,基于**固定长度**消息进行粘包拆包处理的。
- ② LengthFieldBasedFrameDecoder ,基于**消息头指定消息长度**进行粘包拆包处理的。
- ③ LineBasedFrameDecoder ,基于**换行**来进行消息粘包拆包处理的。
- ④ DelimiterBasedFrameDecoder ,基于**指定消息边界方式**进行粘包拆包处理的。
实际上,上述四个 FrameDecoder 实现可以进行规整:
- ① 是 ② 的特例,**固定长度**是**消息头指定消息长度**的一种形式。
- ③ 是 ④ 的特例,**换行**是于**指定消息边界方式**的一种形式。
感兴趣的胖友,可以看看如下两篇文章:
- [《精尽 Netty 源码解析 —— Codec 之 ByteToMessageDecoderCumulator》](http://svip.iocoder.cn/Netty/Codec-1-1-ByteToMessageDecoder-core-impl/)
- [《精尽 Netty 源码解析 —— Codec 之 ByteToMessageDecoderFrameDecoder》](http://svip.iocoder.cn/Netty/Codec-1-2-ByteToMessageDecoder-FrameDecoder/)
## 了解哪几种序列化协议?
🦅 **概念**
- 序列化(编码),是将对象序列化为二进制形式(字节数组),主要用于网络传输、数据持久化等。
- 反序列化(解码),则是将从网络、磁盘等读取的字节数组还原成原始对象,主要用于网络传输对象的解码,以便完成远程调用。
🦅 **选型**
在选择序列化协议的选择,主要考虑以下三个因素:
- 序列化后的**字节大小**。更少的字节数,可以减少网络带宽、磁盘的占用。
- 序列化的**性能**。对 CPU、内存资源占用情况。
- 是否支持**跨语言**。例如,异构系统的对接和开发语言切换。
🦅 **方案**
> 如果对序列化工具了解不多的胖友,可能一看有这么多优缺点会比较懵逼,可以先记得有哪些序列化工具,然后在慢慢熟悉它们的优缺点。
>
> 重点,还是知道【**选型**】的考虑点。
1. 【重点】Java 默认提供的序列化
- 无法跨语言;序列化后的字节大小太大;序列化的性能差。
2. 【重点】XML 。
- 优点:人机可读性好,可指定元素或特性的名称。
- 缺点:序列化数据只包含数据本身以及类的结构,不包括类型标识和程序集信息;只能序列化公共属性和字段;不能序列化方法;文件庞大,文件格式复杂,传输占带宽。
- 适用场景:当做配置文件存储数据,实时数据转换。
3. 【重点】JSON ,是一种轻量级的数据交换格式。
- 优点:兼容性高、数据格式比较简单,易于读写、序列化后数据较小,可扩展性好,兼容性好。与 XML 相比,其协议比较简单,解析速度比较快。
- 缺点:数据的描述性比 XML 差、不适合性能要求为 ms 级别的情况、额外空间开销比较大。
- 适用场景(可替代 XML 跨防火墙访问、可调式性要求高、基于Restful API 请求、传输数据量相对小,实时性要求相对低(例如秒级别)的服务。
4. 【了解】Thrift ,不仅是序列化协议,还是一个 RPC 框架。
- 优点:序列化后的体积小, 速度快、支持多种语言和丰富的数据类型、对于数据字段的增删具有较强的兼容性、支持二进制压缩编码。
- 缺点:使用者较少、跨防火墙访问时,不安全、不具有可读性,调试代码时相对困难、不能与其他传输层协议共同使用(例如 HTTP、无法支持向持久层直接读写数据即不适合做数据持久化序列化协议。
- 适用场景:分布式系统的 RPC 解决方案。
5. 【了解】Avro Hadoop 的一个子项目解决了JSON的冗长和没有IDL的问题。
- 优点:支持丰富的数据类型、简单的动态语言结合功能、具有自我描述属性、提高了数据解析速度、快速可压缩的二进制数据形式、可以实现远程过程调用 RPC、支持跨编程语言实现。
- 缺点:对于习惯于静态类型语言的用户不直观。
- 适用场景:在 Hadoop 中做 Hive、Pig 和 MapReduce 的持久化数据格式。
6. 【重点】Protobuf ,将数据结构以
```
.proto
```
文件进行描述,通过代码生成工具可以生成对应数据结构的 POJO 对象和 Protobuf 相关的方法和属性。
- 优点序列化后码流小性能高、结构化数据存储格式XML JSON等、通过标识字段的顺序可以实现协议的前向兼容、结构化的文档更容易管理和维护。
- 缺点需要依赖于工具生成代码、支持的语言相对较少官方只支持Java 、C++、python。
- 适用场景:对性能要求高的 RPC 调用、具有良好的跨防火墙的访问属性、适合应用层对象的持久化。
7. 其它
- 【重点】Protostuff ,基于 Protobuf 协议但不需要配置proto 文件,直接导包即可。
- 目前,微博 RPC 框架 Motan 在使用它。
- 【了解】Jboss Marshaling ,可以直接序列化 Java 类, 无须实 `java.io.Serializable` 接口。
- 【了解】Message Pack ,一个高效的二进制序列化格式。
- 【重点】
Hessian
,采用二进制协议的轻量级 remoting on http 服务。
- 目前,阿里 RPC 框架 Dubbo 的**默认**序列化协议。
- 【重要】kryo 是一个快速高效的Java对象图形序列化框架主要特点是性能、高效和易用。该项目用来序列化对象到文件、数据库或者网络。
- 目前,阿里 RPC 框架 Dubbo 的可选序列化协议。
- 【重要】FST fast-serialization 是重新实现的 Java 快速对象序列化的开发包。序列化速度更快2-10倍、体积更小而且兼容 JDK 原生的序列化。要求 JDK 1.7 支持。
- 目前,阿里 RPC 框架 Dubbo 的可选序列化协议。
## Netty 的零拷贝实现?
Netty 的零拷贝实现,是体现在多方面的,主要如下:
1. 【重点】Netty 的接收和发送 ByteBuffer 采用堆外直接内存
Direct Buffer
- 使用堆外直接内存进行 Socket 读写不需要进行字节缓冲区的二次拷贝使用堆内内存会多了一次内存拷贝JVM 会将堆内存 Buffer 拷贝一份到直接内存中,然后才写入 Socket 中。
- Netty 创建的 ByteBuffer 类型,由 ChannelConfig 配置。而 ChannelConfig 配置的 ByteBufAllocator 默认创建 Direct Buffer 类型。
2. CompositeByteBuf
类,可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf ,避免了传统通过内存拷贝的方式将几个小 Buffer 合并成一个大的 Buffer 。
- `#addComponents(...)` 方法,可将 header 与 body 合并为一个逻辑上的 ByteBuf 。这两个 ByteBuf 在CompositeByteBuf 内部都是单独存在的,即 CompositeByteBuf 只是逻辑上是一个整体。
3. 通过
FileRegion
包装的 FileChannel 。
- `#tranferTo(...)` 方法,实现文件传输, 可以直接将文件缓冲区的数据发送到目标 Channel ,避免了传统通过循环 write 方式,导致的内存拷贝问题。
4. 通过 **wrap** 方法, 我们可以将 `byte[]` 数组、ByteBuf、ByteBuffer 等包装成一个 Netty ByteBuf 对象, 进而避免了拷贝操作。
## 原生的 NIO 存在 Epoll Bug 是什么Netty 是怎么解决的?
🦅 **Java NIO Epoll BUG**
Java NIO Epoll 会导致 Selector 空轮询,最终导致 CPU 100% 。
官方声称在 JDK 1.6 版本的 update18 修复了该问题,但是直到 JDK 1.7 版本该问题仍旧存在,只不过该 BUG 发生概率降低了一些而已,它并没有得到根本性解决。
🦅 **Netty 解决方案**
对 Selector 的 select 操作周期进行**统计**,每完成一次**空**的 select 操作进行一次计数,若在某个周期内连续发生 N 次空轮询,则判断触发了 Epoll 死循环 Bug 。
> 艿艿:此处**空**的 select 操作的定义是select 操作执行了 0 毫秒。
此时Netty **重建** Selector 来解决。判断是否是其他线程发起的重建请求,若不是则将原 SocketChannel 从旧的 Selector 上取消注册,然后重新注册到新的 Selector 上,最后将原来的 Selector 关闭。
## 什么是 Netty 空闲检测?
在 Netty 中,提供了 IdleStateHandler 类,正如其名,空闲状态处理器,用于检测连接的读写是否处于空闲状态。如果是,则会触发 IdleStateEvent 。
IdleStateHandler 目前提供三种类型的心跳检测,通过构造方法来设置。代码如下:
```
// IdleStateHandler.java
public IdleStateHandler(
int readerIdleTimeSeconds,
int writerIdleTimeSeconds,
int allIdleTimeSeconds) {
this(readerIdleTimeSeconds, writerIdleTimeSeconds, allIdleTimeSeconds,
TimeUnit.SECONDS);
}
```
- `readerIdleTimeSeconds` 参数:为读超时时间,即测试端一定时间内未接受到被测试端消息。
- `writerIdleTimeSeconds` 参数:为写超时时间,即测试端一定时间内向被测试端发送消息。
- `allIdleTimeSeconds` 参数:为读或写超时时间。
------
另外我们会在网络上看到类似《IdleStateHandler 心跳机制》这样标题的文章,实际上空闲检测和心跳机制是**两件事**。
- 只是说,因为我们使用 IdleStateHandler 的目的,就是检测到连接处于空闲,通过心跳判断其是否还是**有效的连接**。
- 虽然说TCP 协议层提供了 Keeplive 机制,但是该机制默认的心跳时间是 2 小时,依赖操作系统实现不够灵活。因而,我们才在应用层上,自己实现心跳机制。
具体的,我们来看看下面的问题 [「Netty 如何实现重连?」](https://svip.iocoder.cn/Netty/Interview/#) 。
## Netty 如何实现重连?
- 客户端,通过 IdleStateHandler 实现定时检测是否空闲,例如说 15 秒。
- 如果空闲,则向服务端发起心跳。
- 如果多次心跳失败,则关闭和服务端的连接,然后重新发起连接。
- 服务端,通过 IdleStateHandler 实现定时检测客户端是否空闲,例如说 90 秒。
- 如果检测到空闲,则关闭客户端。
- 注意,如果接收到客户端的心跳请求,要反馈一个心跳响应给客户端。通过这样的方式,使客户端知道自己心跳成功。
如下艿艿在自己的 [TaroRPC](https://github.com/YunaiV/TaroRPC) 中提供的一个示例:
- [NettyClient.java](https://github.com/YunaiV/TaroRPC/blob/master/transport/transport-netty4/src/main/java/cn/iocoder/taro/transport/netty4/NettyClient.java) 中,设置 IdleStateHandler 和 ClientHeartbeatHandler。核心代码如下
```
// NettyHandler.java
.addLast("idleState", new IdleStateHandler(TaroConstants.TRANSPORT_CLIENT_IDLE, TaroConstants.TRANSPORT_CLIENT_IDLE, 0, TimeUnit.MILLISECONDS))
.addLast("heartbeat", new ClientHeartbeatHandler())
```
- [NettyServer.java](https://github.com/YunaiV/TaroRPC/blob/master/transport/transport-netty4/src/main/java/cn/iocoder/taro/transport/netty4/NettyServer.java) 中,设置 IdleStateHandler 和 ServerHeartbeatHandler。核心代码如下
```
// NettyServer.java
.addLast("idleState", new IdleStateHandler(0, 0, TaroConstants.TRANSPORT_SERVER_IDLE, TimeUnit.MILLISECONDS))
.addLast("heartbeat", new ServerHeartbeatHandler())
```
- [ClientHeartbeatHandler.java](https://github.com/YunaiV/TaroRPC/blob/master/transport/transport-netty4/src/main/java/cn/iocoder/taro/transport/netty4/heartbeat/ClientHeartbeatHandler.java) 中,碰到空闲,则发起心跳。不过,如何重连,暂时没有实现。需要考虑,重新发起连接可能会失败的情况。具体的,可以看看 [《一起学Netty十四之 Netty生产级的心跳和重连机制》](https://blog.csdn.net/linuu/article/details/51509847) 文章中的ConnectionWatchdog 的代码。
- [ServerHeartbeatHandler.java](https://github.com/YunaiV/TaroRPC/blob/6ce2af911ccec9ed5dc75c7f3ebda9c758272f3b/transport/transport-netty4/src/main/java/cn/iocoder/taro/transport/netty4/heartbeat/ServerHeartbeatHandler.java) 中,检测到客户端空闲,则直接关闭连接。
## Netty 自己实现的 ByteBuf 有什么优点?
如下是 [《Netty 实战》](http://svip.iocoder.cn/Netty/ByteBuf-1-1-ByteBuf-intro/#) 对它的**优点总**结:
> - A01. 它可以被用户自定义的**缓冲区类型**扩展
> - A02. 通过内置的符合缓冲区类型实现了透明的**零拷贝**
> - A03. 容量可以**按需增长**
> - A04. 在读和写这两种模式之间切换不需要调用 `#flip()` 方法
> - A05. 读和写使用了**不同的索引**
> - A06. 支持方法的**链式**调用
> - A07. 支持引用计数
> - A08. 支持**池化**
- 特别是第 A04 这点,相信很多胖友都被 NIO ByteBuffer 反人类的读模式和写模式给坑哭了。在 [《精尽 Netty 源码分析 —— NIO 基础(三)之 Buffer》](http://svip.iocoder.cn/Netty/nio-3-buffer/) 中,我们也吐槽过了。😈
想要进一步深入的,可以看看 [《精尽 Netty 源码解析 —— Buffer 之 ByteBuf简介》](http://svip.iocoder.cn/Netty/ByteBuf-1-1-ByteBuf-intro/) 。
## Netty 为什么要实现内存管理?
🦅 **老艿艿的理解**
在 Netty 中IO 读写必定是非常频繁的操作而考虑到更高效的网络传输性能Direct ByteBuffer 必然是最合适的选择。但是 Direct ByteBuffer 的申请和释放是高成本的操作,那么进行**池化**管理,多次重用是比较有效的方式。但是,不同于一般于我们常见的对象池、连接池等**池化**的案例ByteBuffer 是有**大小**一说。又但是,申请多大的 Direct ByteBuffer 进行池化又会是一个大问题,太大会浪费内存,太小又会出现频繁的扩容和内存复制!!!所以呢,就需要有一个合适的内存管理算法,解决**高效分配内存**的同时又解决**内存碎片化**的问题。
🦅 **官方的说法**
> FROM [《Netty 学习笔记 —— Pooled buffer》](https://skyao.gitbooks.io/learning-netty/content/buffer/pooled_buffer.html)
>
> Netty 4.x 增加了 Pooled Buffer实现了高性能的 buffer 池,分配策略则是结合了 buddy allocation 和 slab allocation 的 **jemalloc** 变种,代码在`io.netty.buffer.PoolArena` 中。
>
> 官方说提供了以下优势:
>
> - 频繁分配、释放 buffer 时减少了 GC 压力。
> - 在初始化新 buffer 时减少内存带宽消耗( 初始化时不可避免的要给buffer数组赋初始值 )。
> - 及时的释放 direct buffer 。
🦅 **hushi55 大佬的理解**
> > C/C++ 和 java 中有个围城,城里的想出来,城外的想进去!**
>
> 这个围城就是自动内存管理!
>
> **Netty 4 buffer 介绍**
>
> Netty4 带来一个与众不同的特点是其 ByteBuf 的实现,相比之下,通过维护两个独立的读写指针, 要比 `io.netty.buffer.ByteBuf` 简单不少也会更高效一些。不过Netty 的 ByteBuf 带给我们的最大不同,就是他不再基于传统 JVM 的 GC 模式,相反,它采用了类似于 C++ 中的 malloc/free 的机制需要开发人员来手动管理回收与释放。从手动内存管理上升到GC是一个历史的巨大进步 不过在20年后居然有曲线的回归到了手动内存管理模式正印证了马克思哲学观 **社会总是在螺旋式前进的,没有永远的最好。**
>
> **① GC 内存管理分析**
>
> 的确就内存管理而言GC带给我们的价值是不言而喻的不仅大大的降低了程序员的心智包袱 而且,也极大的减少了内存管理带来的 Crash 困扰,为函数式编程(大量的临时对象)、脚本语言编程带来了春天。 并且高效的GC算法也让大部分情况下程序可以有更高的执行效率。 不过,也有很多的情况,可能是手工内存管理更为合适的。譬如:
>
> - 对于类似于业务逻辑相对简单譬如网络路由转发型应用很多erlang应用其实是这种类型 但是 QPS 非常高比如1M级在这种情况下在每次处理中即便产生1K的垃圾都会导致频繁的GC产生。 在这种模式下erlang 的按进程回收模式,或者是 C/C++ 的手工回收机制,效率更高。
> - Cache 型应用由于对象的存在周期太长GC 基本上就变得没有价值。
>
> 所以理论上尴尬的GC实际上比较适合于处理介于这 2 者之间的情况: 对象分配的频繁程度相比数据处理的时间要少得多的,但又是相对短暂的, 典型的对于OLTP型的服务处理能力在 1K QPS 量级,每个请求的对象分配在 10K-50K 量级, 能够在 5-10s 的时间内进行一 次younger GC 每次GC的时间可以控制在 10ms 水平上, 这类的应用,实在是太适合 GC 行的模式了,而且结合 Java 高效的分代 GC ,简直就是一个理想搭配。
>
> **② 影响**
>
> Netty 4 引入了手工内存的模式,我觉得这是一大创新,这种模式甚至于会延展, 应用到 Cache 应用中。实际上,结合 JVM 的诸多优秀特性,如果用 Java 来实现一个 Redis 型 Cache、 或者 In-memory SQL Engine或者是一个 Mongo DB我觉得相比 C/C++ 而言,都要更简单很多。 实际上JVM 也已经提供了打通这种技术的机制,就是 Direct Memory 和 Unsafe 对象。 基于这个基础,我们可以像 C 语言一样直接操作内存。实际上Netty4 的 ByteBuf 也是基于这个基础的。
更多详细的内容,胖友可以看看 [《精尽 Netty 源码解析 —— Buffer 之 Jemalloc简介》](http://svip.iocoder.cn/Netty/ByteBuf-3-1-Jemalloc-intro/) 。
## Netty 如何实心内存管理?
> 这个题目,简单了解即可,如果深入,就要去看 [《精尽 Netty 源码解析 —— Buffer》](http://svip.iocoder.cn/categories/Netty/) 相关的源码。而且,看完就忘记,比较难和复杂。
>
> 当然,看懂那一刻,乐趣无穷,哈哈哈哈。
Netty 内存管理机制,基于 [Jemalloc](http://www.cnhalo.net/2016/06/13/memory-optimize/) 算法。
- 首先会预申请一大块内存 **Arena** Arena 由许多 Chunk 组成,而每个 Chunk 默认由2048个page组成。
- **Chunk** 通过 **AVL** 树的形式组织 **Page** ,每个叶子节点表示一个 Page ,而中间节点表示内存区域,节点自己记录它在整个 Arena 中的偏移地址。当区域被分配出去后,中间节点上的标记位会被标记,这样就表示这个中间节点以下的所有节点都已被分配了。大于 8k 的内存分配在 **PoolChunkList** 中,而 **PoolSubpage** 用于分配小于 8k 的内存,它会把一个 page 分割成多段,进行内存分配。
## 什么是 Netty 的内存泄露检测?是如何进行实现的?
> 艿艿:这是一道比较复杂的面试题,可以挑战一下。
推荐阅读如下两篇文章:
- [《Netty 之有效规避内存泄漏》](http://calvin1978.blogcn.com/articles/netty-leak.html) 从原理层面解释。
- [《精尽 Netty 源码解析 —— Buffer 之 ByteBuf内存泄露检测》](http://svip.iocoder.cn/Netty/ByteBuf-1-3-ByteBuf-resource-leak-detector/) 从源码层面解读。
另外Netty 的内存泄露检测的实现,是对 WeakReference 和 ReferenceQueue 很好的学习。之前很多胖友在看了 [《Java 中的四种引用类型》](http://www.iocoder.cn/Fight/Four-reference-types-in-Java) 之后,是不太理解 Java 的四种引用的具体使用场景,这不就来了么。
# 666. 彩蛋
😈 撸完 Netty 源码之后,一段时间没去用,东西都忘了蛮多。不过因为读过源码,在看这些面试题,会发现轻松太多了。有一种,“老朋友”的感觉。妥妥的。
参考与推荐如下文章:
- itdragon [《Netty 序章之 BIO NIO AIO 演变》](https://segmentfault.com/a/1190000012976683)
- 白夜行515 [《【面试题】Netty相关》](https://blog.csdn.net/baiye_xing/article/details/76735113)
- albon [《Netty 权威指南笔记Java NIO 和 Netty 对比》](https://www.jianshu.com/p/93876f1bf6d1)
- albon [《Netty 权威指南笔记(四):架构剖析》](https://www.jianshu.com/p/ececec069cc1)
- 狗窝 [《面试题之 Netty》](https://nizouba.com/articles/2018/11/04/1541322296629.html) 题目很不错,就是图片都挂了。

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>http://svip.iocoder.cn</title>
<link href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-M/bootstrap/4.6.1/css/bootstrap.min.css"
type="text/css" rel="stylesheet" />
<link href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css" rel="stylesheet" crossorigin="anonymous"/>
</head>
<body>
<div class="container">
<form action="/user/auth" class="form-signin" method="post"><input type="hidden" name="_csrf" value="d9fb8669-e03f-4554-8b00-5629cb129814"/>
<h2 class="form-signin-heading">登录</h2>
<p>
<label for="username" class="sr-only">用户名</label>
<input type="text" id="username" name="username" class="form-control" placeholder="用户名" required autofocus>
</p>
<p>
<label for="password" class="sr-only">密码</label>
<input type="password" id="password" name="password" class="form-control" placeholder="密码" required>
</p>
<button class="btn btn-lg btn-primary btn-block" type="submit">登录</button>
<p>如果账号密码不正确,访问 <a href="https://t.zsxq.com/11JUHkGgl">https://t.zsxq.com/11JUHkGgl</a> 或 <a href="https://t.zsxq.com/yBUj2NN">https://t.zsxq.com/yBUj2NN</a> 解决</p>
</form>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 KiB

View File

@@ -0,0 +1,486 @@
# 精尽 Spring MVC 面试题「最新更新时间2024-02」
以下面试题,基于网络整理,和自己编辑。具体参考的文章,会在文末给出所有的链接。
如果胖友有自己的疑问,欢迎在星球提问,我们一起整理吊吊的 Spring MVC 面试题的大保健。
而题目的难度,艿艿尽量按照从容易到困难的顺序,逐步下去。
当然,艿艿还是非常推荐胖友去撸一撸 Spring MVC 的源码,特别是如下两篇:
- [《精尽 Spring MVC 源码分析 —— 组件一览》](http://svip.iocoder.cn/Spring-MVC/Components-intro/)
- [《精尽 Spring MVC 源码分析 —— 请求处理一览》](http://svip.iocoder.cn/Spring-MVC/DispatcherServlet-process-request-intro/)
考虑到 Spring MVC 和 Rest 关系比较大,所以本文一共分成两大块:
- Spring MVC
- REST
# Spring MVC
## Spring MVC 框架有什么用?
Spring Web MVC 框架提供”模型-视图-控制器”( Model-View-Controller )架构和随时可用的组件,用于开发灵活且松散耦合的 Web 应用程序。
MVC 模式有助于分离应用程序的不同方面,如输入逻辑,业务逻辑和 UI 逻辑,同时在所有这些元素之间提供松散耦合。
## 介绍下 Spring MVC 的核心组件?
Spring MVC 一共有九大核心组件,分别是:
- MultipartResolver
- LocaleResolver
- ThemeResolver
- HandlerMapping
- HandlerAdapter
- HandlerExceptionResolver
- RequestToViewNameTranslator
- ViewResolver
- FlashMapManager
虽然很多,但是在前后端分离的架构中,在 [「描述一下 DispatcherServlet 的工作流程?」](https://svip.iocoder.cn/Spring-MVC/Interview/#) 问题中,我们会明白,最关键的只有 HandlerMapping + HandlerAdapter + HandlerExceptionResolver 。
关于每个组件的说明,直接看 [《精尽 Spring MVC 源码分析 —— 组件一览》](http://svip.iocoder.cn/Spring-MVC/Components-intro/) 。
## 描述一下 DispatcherServlet 的工作流程?
DispatcherServlet 的工作流程可以用一幅图来说明:
[![DispatcherServlet 的工作流程](04-Spring MVC 面试题.assets/15300766829012.jpg)](https://blog-pictures.oss-cn-shanghai.aliyuncs.com/15300766829012.jpg)DispatcherServlet 的工作流程
**发送请求**
用户向服务器发送 HTTP 请求,请求被 Spring MVC 的调度控制器 DispatcherServlet 捕获。
**映射处理器**
DispatcherServlet 根据请求 URL ,调用 HandlerMapping 获得该 Handler 配置的所有相关的对象(包括 **Handler** 对象以及 Handler 对象对应的**拦截器**),最后以 HandlerExecutionChain 对象的形式返回。
- 即 HandlerExecutionChain 中,包含对应的 **Handler** 对象和**拦截器**们。
> 此处,对应的方法如下:
>
> ```
> > // HandlerMapping.java
> >
> > @Nullable
> > HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
> >
> ```
**处理器适配**
DispatcherServlet 根据获得的 Handler选择一个合适的HandlerAdapter 。(附注:如果成功获得 HandlerAdapter 后,此时将开始执行拦截器的 `#preHandler(...)` 方法)。
提取请求 Request 中的模型数据,填充 Handler 入参开始执行HandlerController)。 在填充Handler的入参过程中根据你的配置Spring 将帮你做一些额外的工作:
- HttpMessageConverter :会将请求消息(如 JSON、XML 等数据)转换成一个对象。
- 数据转换:对请求消息进行数据转换。如 String 转换成 Integer、Double 等。
- 数据格式化:对请求消息进行数据格式化。如将字符串转换成格式化数字或格式化日期等。
- 数据验证: 验证数据的有效性(长度、格式等),验证结果存储到 BindingResult 或 Error 中。
Handler(Controller) 执行完成后,向 DispatcherServlet 返回一个 ModelAndView 对象。
> 此处,对应的方法如下:
>
> ```
> > // HandlerAdapter.java
> >
> > @Nullable
> > ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
> >
> ```
.
> 图中没有 ④ 。
**解析视图**
根据返回的 ModelAndView ,选择一个适合的 ViewResolver必须是已经注册到 Spring 容器中的 ViewResolver),解析出 View 对象,然后返回给 DispatcherServlet。
> 此处,对应的方法如下:
>
> ```
> > // ViewResolver.java
> >
> > @Nullable
> > View resolveViewName(String viewName, Locale locale) throws Exception;
> >
> ```
⑥ ⑦ **渲染视图** + **响应请求**
ViewResolver 结合 Model 和 View来渲染视图并写回给用户( 浏览器 )。
> 此处,对应的方法如下:
>
> ```
> > // View.java
> >
> > void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception;
> >
> ```
------
这样一看,胖友可能有点懵逼,所以还是推荐看看:
- [《精尽 Spring MVC 源码分析 —— 组件一览》](http://svip.iocoder.cn/Spring-MVC/Components-intro/)
- [《精尽 Spring MVC 源码分析 —— 请求处理一览》](http://svip.iocoder.cn/Spring-MVC/DispatcherServlet-process-request-intro/)
**但是 Spring MVC 的流程真的一定是酱紫么**
既然这么问,答案当然不是。对于目前主流的架构,前后端已经进行分离了,所以 Spring MVC 只负责 **M**odel 和 **C**ontroller 两块,而将 **V**iew 移交给了前端。所以,在上图中的步骤 ⑤ 和 ⑥ 两步,已经不在需要。
那么变成什么样了呢?在步骤 ③ 中,如果 Handler(Controller) 执行完后,如果判断方法有 `@ResponseBody` 注解,则直接将结果写回给用户( 浏览器 )。
但是 HTTP 是不支持返回 Java POJO 对象的,所以需要将结果使用 [HttpMessageConverter](http://svip.iocoder.cn/Spring-MVC/HandlerAdapter-5-HttpMessageConverter/) 进行转换后,才能返回。例如说,大家所熟悉的 [FastJsonHttpMessageConverter](https://github.com/alibaba/fastjson/wiki/在-Spring-中集成-Fastjson) ,将 POJO 转换成 JSON 字符串返回。
😈 是不是略微有点复杂,还是那句话,撸下源码,捅破这个窗口。当然,如果胖友精力有限,只要看整体流程的几篇即可。
------
嘻嘻,再来补充两个图,这真的是 Spring MVC 非常关键的问题,所以要用心理解。
> FROM [《SpringMVC - 运行流程图及原理分析》](https://blog.csdn.net/J080624/article/details/77990164)
>
> **流程示意图**
>
> [![流程示意图](04-Spring MVC 面试题.assets/01.png)](http://static.iocoder.cn/images/Spring/2022-02-21/01.png)流程示意图
>
> **代码序列图**
>
> [![代码序列图](04-Spring MVC 面试题.assets/02.png)](http://static.iocoder.cn/images/Spring/2022-02-21/02.png)代码序列图
>
> ------
>
> FROM [《看透 Spring MVC源代码分析与实践》](https://item.jd.com/11807414.html) P123
>
> **流程示意图**
>
> [![《流程示意图》](04-Spring MVC 面试题.assets/03.png)](http://static.iocoder.cn/images/Spring/2022-02-21/03.png)《流程示意图》
## @Controller 注解有什么用?
`@Controller` 注解,它将一个类标记为 Spring Web MVC **控制器** Controller 。
## @RestController 和 @Controller 有什么区别?
`@RestController` 注解,在 `@Controller` 基础上,增加了 `@ResponseBody` 注解,更加适合目前前后端分离的架构下,提供 Restful API ,返回例如 JSON 数据格式。当然,返回什么样的数据格式,根据客户端的 `"ACCEPT"` 请求头来决定。
## @RequestMapping 注解有什么用?
`@RequestMapping` 注解,用于将特定 HTTP 请求方法映射到将处理相应请求的控制器中的特定类/方法。此注释可应用于两个级别:
- 类级别:映射请求的 URL。
- 方法级别:映射 URL 以及 HTTP 请求方法。
## @RequestMapping 和 @GetMapping 注解的不同之处在哪里?
- `@RequestMapping` 可注解在类和方法上;`@GetMapping` 仅可注册在方法上。
- `@RequestMapping` 可进行 GET、POST、PUT、DELETE 等请求方法;`@GetMapping``@RequestMapping` 的 GET 请求方法的特例,目的是为了提高清晰度。
## 返回 JSON 格式使用什么注解?
可以使用 **`@ResponseBody`** 注解,或者使用包含 `@ResponseBody` 注解的 **`@RestController`** 注解。
当然,还是需要配合相应的支持 JSON 格式化的 HttpMessageConverter 实现类。例如Spring MVC 默认使用 MappingJackson2HttpMessageConverter 。
## 介绍一下 WebApplicationContext
WebApplicationContext 是实现ApplicationContext接口的子类专门为 WEB 应用准备的。
- 它允许从相对于 Web 根目录的路径中**加载配置文件****完成初始化 Spring MVC 组件的工作**。
- 从 WebApplicationContext 中,可以获取 ServletContext 引用,整个 Web 应用上下文对象将作为属性放置在 ServletContext 中,以便 Web 应用环境可以访问 Spring 上下文。
关于这一块,如果想要详细了解,可以看看如下两篇文章:
- [《精尽 Spring MVC 源码分析 —— 容器的初始化(一)之 Root WebApplicationContext 容器》](http://svip.iocoder.cn/Spring-MVC/context-init-Root-WebApplicationContext/)
- [《精尽 Spring MVC 源码分析 —— 容器的初始化(二)之 Servlet WebApplicationContext 容器》](http://svip.iocoder.cn/Spring-MVC/context-init-Servlet-WebApplicationContext/)
## Spring MVC 的异常处理?
Spring MVC 提供了异常解析器 HandlerExceptionResolver 接口,将处理器( `handler` )执行时发生的异常,解析( 转换 )成对应的 ModelAndView 结果。代码如下:
```
// HandlerExceptionResolver.java
public interface HandlerExceptionResolver {
/**
* 解析异常,转换成对应的 ModelAndView 结果
*/
@Nullable
ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
}
```
- 也就是说,如果异常被解析成功,则会返回 ModelAndView 对象。
- 详细的源码解析,见 [《精尽 Spring MVC 源码解析 —— HandlerExceptionResolver 组件》](http://svip.iocoder.cn/Spring-MVC/HandlerExceptionResolver/) 。
一般情况下,我们使用 `@ExceptionHandler` 注解来实现过异常的处理,可以先看看 [《Spring 异常处理 ExceptionHandler 的使用》](https://www.jianshu.com/p/12e1a752974d) 。
- 一般情况下,艿艿喜欢使用**第三种**。
## Spring MVC 有什么优点?
1. 使用真的真的真的非常**方便**,无论是添加 HTTP 请求方法映射的方法,还是不同数据格式的响应。
2. 提供**拦截器机制**,可以方便的对请求进行拦截处理。
3. 提供**异常机制**,可以方便的对异常做统一处理。
4. 可以任意使用各种**视图**技术,而不仅仅局限于 JSP ,例如 Freemarker、Thymeleaf 等等。
5. 不依赖于 Servlet API (目标虽是如此,但是在实现的时候确实是依赖于 Servlet 的,当然仅仅依赖 Servlet ,而不依赖 Filter、Listener )。
## Spring MVC 怎样设定重定向和转发
- 结果转发:在返回值的前面加 `"forward:/"`
- 重定向:在返回值的前面加上 `"redirect:/"`
当然,目前前后端分离之后,我们作为后端开发,已经很少有机会用上这个功能了。
## Spring MVC 的 Controller 是不是单例?
绝绝绝大多数情况下Controller 是**单例**。
那么Controller 里一般不建议存在**共享的变量**。实际场景下,艿艿也没碰到需要使用共享变量的情况。
## Spring MVC 和 Struts2 的异同?
1. 入口
不同
- Spring MVC 的入门是一个 Servlet **控制器**
- Struts2 入门是一个 Filter **过滤器**
2. 配置映射
不同,
- Spring MVC 是基于**方法**开发,传递参数是通过**方法形参**,一般设置为**单例**。
- Struts2 是基于**类**开发,传递参数是通过**类的属性**,只能设计为**多例**。
- 视图
不同
- Spring MVC 通过参数解析器是将 Request 对象内容进行解析成方法形参,将响应数据和页面封装成 **ModelAndView** 对象,最后又将模型数据通过 **Request** 对象传输到页面。其中,如果视图使用 JSP 时,默认使用 **JSTL**
- Struts2 采用**值栈**存储请求和响应的数据,通过 **OGNL** 存取数据。
当然,更详细的也可以看看 [《面试题Spring MVC 和 Struts2 的区别》](http://www.voidcn.com/article/p-ylualwcj-c.html) 一文。
## 详细介绍下 Spring MVC 拦截器?
`org.springframework.web.servlet.HandlerInterceptor` ,拦截器接口。代码如下:
```
// HandlerInterceptor.java
/**
* 拦截处理器,在 {@link HandlerAdapter#handle(HttpServletRequest, HttpServletResponse, Object)} 执行之前
*/
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return true;
}
/**
* 拦截处理器,在 {@link HandlerAdapter#handle(HttpServletRequest, HttpServletResponse, Object)} 执行成功之后
*/
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
}
/**
* 拦截处理器,在 {@link HandlerAdapter#handle(HttpServletRequest, HttpServletResponse, Object)} 执行完之后,无论成功还是失败
*
* 并且,只有该处理器 {@link #preHandle(HttpServletRequest, HttpServletResponse, Object)} 执行成功之后,才会被执行
*/
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) throws Exception {
}
```
- 一共有三个方法,分别为:
- `#preHandle(...)` 方法,调用 Controller 方法之**前**执行。
- `#postHandle(...)` 方法,调用 Controller 方法之**后**执行。
- ```
#afterCompletion(...)
```
方法,处理完 Controller 方法返回结果之
执行。
- 例如,页面渲染后。
- **当然,要注意,无论调用 Controller 方法是否成功,都会执行**。
- 举个例子:
- 当俩个拦截器都实现放行操作时,执行顺序为 `preHandle[1] => preHandle[2] => postHandle[2] => postHandle[1] => afterCompletion[2] => afterCompletion[1]` 。
- 当第一个拦截器 `#preHandle(...)` 方法返回 `false` ,也就是对其进行拦截时,第二个拦截器是完全不执行的,第一个拦截器只执行 `#preHandle(...)` 部分。
- 当第一个拦截器 `#preHandle(...)` 方法返回 `true` ,第二个拦截器 `#preHandle(...)` 返回 `false` ,执行顺序为 `preHandle[1] => preHandle[2] => afterCompletion[1]` 。
- 总结来说:
- `#preHandle(...)` 方法,按拦截器定义**顺序**调用。若任一拦截器返回 `false` ,则 Controller 方法不再调用。
- `#postHandle(...)` 和 `#afterCompletion(...)` 方法,按拦截器定义**逆序**调用。
- `#postHandler(...)` 方法,在调用 Controller 方法之**后**执行。
- `#afterCompletion(...)` 方法,只有该拦截器在 `#preHandle(...)` 方法返回 `true` 时,才能够被调用,且一定会被调用。为什么“且一定会被调用”呢?即使 `#afterCompletion(...)` 方法,按拦截器定义**逆序**调用时,前面的拦截器发生异常,后面的拦截器还能够调用,**即无视异常**。
------
关于这块,可以看看如下两篇文章:
- [《Spring MVC 多个拦截器执行顺序及拦截器使用方法》](https://blog.csdn.net/amaxiaochen/article/details/77210880) 文章,通过**实践**更加理解。
- [《精尽 Spring MVC 源码分析 —— HandlerMapping 组件(二)之 HandlerInterceptor》](http://svip.iocoder.cn/Spring-MVC/HandlerMapping-2-HandlerInterceptor/) 文章,通过**源码**更加理解。
## Spring MVC 的拦截器可以做哪些事情?
拦截器能做的事情非常非常非常多,例如:
- 记录访问日志。
- 记录异常日志。
- 需要登陆的请求操作,拦截未登陆的用户。
- …
## Spring MVC 的拦截器和 Filter 过滤器有什么差别?
看了文章 [《过滤器( Filter )和拦截器( Interceptor )的区别》](https://blog.csdn.net/xiaodanjava/article/details/32125687) ,感觉对比的怪怪的。艿艿觉得主要几个点吧:
- **功能相同**:拦截器和 Filter都能实现相应的功能谁也不比谁强。
- **容器不同**:拦截器构建在 Spring MVC 体系中Filter 构建在 Servlet 容器之上。
- **使用便利性不同**:拦截器提供了三个方法,分别在不同的时机执行;过滤器仅提供一个方法,当然也能实现拦截器的执行时机的效果,就是麻烦一些。
另外,😈 再补充一点小知识。我们会发现,拓展性好的框架,都会提供相应的拦截器或过滤器机制,方便的我们做一些拓展。例如:
- Dubbo 的 Filter 机制。
- Spring Cloud Gateway 的 Filter 机制。
- Struts2 的拦截器机制。
# REST
本小节的内容,基本是基于 [《排名前 20 的 REST 和 Spring MVC 面试题》](http://www.spring4all.com/article/1445) 之上,做增补。
## REST 代表着什么?
REST 代表着抽象状态转移,它是根据 HTTP 协议从客户端发送数据到服务端,例如:服务端的一本书可以以 XML 或 JSON 格式传递到客户端。
然而假如你不熟悉REST我建议你先看看 [REST API design and development](http://bit.ly/2zIGzWK) 这篇文章来更好的了解它。不过对于大多数胖友的英语,可能不太好,所以也可以阅读知乎上的 [《怎样用通俗的语言解释 REST以及 RESTful》](https://www.zhihu.com/question/28557115) 讨论。
## 资源是什么?
资源是指数据在 REST 架构中如何显示的。将实体作为资源公开 ,它允许客户端通过 HTTP 方法如:[GET](http://javarevisited.blogspot.sg/2012/03/get-post-method-in-http-and-https.html), [POST](http://www.java67.com/2014/08/difference-between-post-and-get-request.html),[PUT](http://www.java67.com/2016/09/when-to-use-put-or-post-in-restful-web-services.html), DELETE 等读,写,修改和创建资源。
## 什么是安全的 REST 操作?
REST 接口是通过 HTTP 方法完成操作。
- 一些HTTP操作是安全的如 GET 和 HEAD ,它不能在服务端修改资源
- 换句话说PUT,POST 和 DELETE 是不安全的,因为他们能修改服务端的资源。
所以,是否安全的界限,在于**是否修改**服务端的资源。
## 什么是幂等操作? 为什么幂等操作如此重要?
有一些HTTP方法GET不管你使用多少次它都能产生相同的结果在没有任何一边影响的情况下发送多个 GET 请求到相同的[URI](http://www.java67.com/2013/01/difference-between-url-uri-and-urn.html) 将会产生相同的响应结果。因此,这就是所谓**幂等**操作。
换句话说,[POST方法不是幂等操作](http://javarevisited.blogspot.sg/2016/05/what-are-idempotent-and-safe-methods-of-HTTP-and-REST.html) ,因为如果发送多个 POST 请求它将在服务端创建不同的资源。但是假如你用PUT更新资源它将是幂等操作。
甚至多个 PUT 请求被用来更新服务端资源,将得到相同的结果。你可以通过 Pluralsight 学习 [HTTP Fundamentals](http://pluralsight.pxf.io/c/1193463/424552/7490?u=https%3A%2F%2Fwww.pluralsight.com%2Fcourses%2Fxhttp-fund) 课程来了解 HTTP 协议和一般的 HTTP 的更多幂等操作。
## REST 是可扩展的或说是协同的吗?
是的,[REST](http://javarevisited.blogspot.sg/2015/08/difference-between-soap-and-restfull-webservice-java.html) 是可扩展的和可协作的。它既不托管一种特定的技术选择,也不定在客户端或者服务端。你可以用 [Java](http://javarevisited.blogspot.sg/2017/11/top-5-free-java-courses-for-beginners.html), [C++](http://www.java67.com/2018/02/5-free-cpp-courses-to-learn-programming.html), [Python](http://www.java67.com/2018/02/5-free-python-online-courses-for-beginners.html), 或 [JavaScript](http://www.java67.com/2018/04/top-5-free-javascript-courses-to-learn.html) 来创建 RESTful Web 服务,也可以在客户端使用它们。
我建议你读一本关于REST接口的书来了解更多[RESTful Web Services](http://javarevisited.blogspot.sg/2017/02/top-5-books-to-learn-rest-and-restful-web-services-in-java.html) 。
> 艿艿:所以这里的“可拓展”、“协同”对应到我们平时常说的,“跨语言”、“语言无关”。
## REST 用哪种 HTTP 方法呢?
REST 能用任何的 HTTP 方法,但是,最受欢迎的是:
- 用 GET 来检索服务端资源
- 用 POST 来创建服务端资源
- [用 PUT 来更新服务端资源](http://javarevisited.blogspot.sg/2016/04/what-is-purpose-of-http-request-types-in-RESTful-web-service.html#axzz56WGunSwy)
- 用 DELETE 来删除服务端资源。
恰好,这四个操作,对上我们日常逻辑的 CRUD 操作。
> 艿艿:经常能听到胖友抱怨自己做的都是 CRUD 的功能。看了这个面试题,有没觉得原来 CRUD 也能玩的稍微高级一点?!
>
> 实际上,每个 CRUD 也是可以通过不断的打磨,玩的很高级。例如说 DDD 领域驱动,完整的单元测试,可扩展的设计。
## 删除的 HTTP 状态返回码是什么 ?
在删除成功之后,您的 REST API 应该返回什么状态代码,并没有严格的规则。它可以返回 200 或 204 没有内容。
- 一般来说,如果删除操作成功,响应主体为空,返回 [204](http://www.netingcn.com/http-status-204.html) 。
- 如果删除请求成功且响应体不是空的,则返回 200 。
## REST API 是无状态的吗?
**是的**REST API 应该是无状态的,因为它是基于 HTTP 的,它也是无状态的。
REST API 中的请求应该包含处理它所需的所有细节。它**不应该**依赖于以前或下一个请求或服务器端维护的一些数据,例如会话。
**REST 规范为使其无状态设置了一个约束,在设计 REST API 时,您应该记住这一点**。
## REST安全吗? 你能做什么来保护它?
安全是一个宽泛的术语。它可能意味着消息的安全性,这是通过认证和授权提供的加密或访问限制提供的。
REST 通常不是安全的,但是您可以通过使用 Spring Security 来保护它。
- 至少,你可以通过在 Spring Security 配置文件中使用 HTTP 来启用 HTTP Basic Auth 基本认证。
- 类似地,如果底层服务器支持 HTTPS ,你可以使用 HTTPS 公开 REST API 。
## RestTemplate 的优势是什么?
在 Spring Framework 中RestTemplate 类是 [模板方法模式](http://www.java67.com/2012/09/top-10-java-design-pattern-interview-question-answer.html) 的实现。跟其他主流的模板类相似,如 JdbcTemplate 或 JmsTempalte ,它将在客户端简化跟 RESTful Web 服务的集成。正如在 RestTemplate 例子中显示的一样,你能非常容易地用它来调用 RESTful Web 服务。
> 艿艿:当然,实际场景我还是更喜欢使用 [OkHttp](http://square.github.io/okhttp/) 作为 HTTP 库,因为更好的性能,使用也便捷,并且无需依赖 Spring 库。
## HttpMessageConverter 在 Spring REST 中代表什么?
HttpMessageConverter 是一种[策略接口](http://www.java67.com/2014/12/strategy-pattern-in-java-with-example.html) ,它指定了一个转换器,它可以转换 HTTP 请求和响应。Spring REST 用这个接口转换 HTTP 响应到多种格式例如JSON 或 XML 。
每个 HttpMessageConverter 实现都有一种或几种相关联的MIME协议。Spring 使用 `"Accept"` 的标头来确定客户端所期待的内容类型。
然后,它将尝试找到一个注册的 HTTPMessageConverter ,它能够处理特定的内容类型,并使用它将响应转换成这种格式,然后再将其发送给客户端。
如果胖友对 HttpMessageConverter 不了解,可以看看 [《Spring 中 HttpMessageConverter 详解》](https://leokongwq.github.io/2017/06/14/spring-MessageConverter.html) 。
## 如何创建 HttpMessageConverter 的自定义实现来支持一种新的请求/响应?
我们仅需要创建自定义的 AbstractHttpMessageConverter 的实现,并使用 WebMvcConfigurerAdapter 的 `#extendMessageConverters(List<HttpMessageConverter<?>> converters)` 方法注中册它,该方法可以生成一种新的请求 / 响应类型。
具体的示例,可以学习 [《在 Spring 中集成 Fastjson》](https://github.com/alibaba/fastjson/wiki/在-Spring-中集成-Fastjson) 文章。
## @PathVariable 注解,在 Spring MVC 做了什么? 为什么 REST 在 Spring 中如此有用?
`@PathVariable` 注解,是 Spring MVC 中有用的注解之一,它允许您从 URI 读取值,比如查询参数。它在使用 Spring 创建 RESTful Web 服务时特别有用,因为在 REST 中,资源标识符是 URI 的一部分。
具体的使用示例,胖友如果不熟悉,可以看看 [《Spring MVC 的 @RequestParam 注解和 @PathVariable 注解的区别》](https://blog.csdn.net/cx361006796/article/details/52829759) 。
# 666. 彩蛋
文末的文末,艿艿还是那句话!!!!还是非常推荐胖友去撸一撸 Spring MVC 的源码,特别是如下两篇:
- [《精尽 Spring MVC 源码分析 —— 组件一览》](http://svip.iocoder.cn/Spring-MVC/Components-intro/)
- [《精尽 Spring MVC 源码分析 —— 请求处理一览》](http://svip.iocoder.cn/Spring-MVC/DispatcherServlet-process-request-intro/)
参考和推荐如下文章:
- [《排名前 20 的 REST 和 Spring MVC 面试题》](http://www.spring4all.com/article/1445)
- [《跟着 Github 学习 Restful HTTP API 的优雅设计》](http://www.iocoder.cn/Fight/Learn-Restful-HTTP-API-design-from-Github/)

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -0,0 +1,802 @@
# 精尽 Spring Boot 面试题「最新更新时间2024-02」
以下面试题,基于网络整理,和自己编辑。具体参考的文章,会在文末给出所有的链接。
如果胖友有自己的疑问,欢迎在星球提问,我们一起整理吊吊的 Spring Boot 面试题的大保健。
而题目的难度,艿艿尽量按照从容易到困难的顺序,逐步下去。
在内容上,我们会分成两大块:
- 核心技术篇,分享 Spring Boot 的核心技术相关的内容。
- 整合篇,分享 Spring Boot 整合一些框架的面试题,例如 JPA 如何集成到 Spring Boot 中。
# 核心技术篇
## Spring Boot 是什么?
[Spring Boot](https://github.com/spring-projects/spring-boot) 是 Spring 的**子项目**,正如其名字,提供 Spring 的引导( **Boot** )的功能。
通过 Spring Boot ,我们开发者可以快速配置 Spring 项目,引入各种 Spring MVC、Spring Transaction、Spring AOP、MyBatis 等等框架,而无需不断重复编写繁重的 Spring 配置,降低了 Spring 的使用成本。
> 艿艿犹记当年Spring XML 为主的时代,大晚上各种搜索 Spring 的配置,苦不堪言。现在有了 Spring Boot 之后,生活真美好。
Spring Boot 提供了各种 Starter 启动器,提供标准化的默认配置。例如:
- [`spring-boot-starter-web`](https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web/2.1.1.RELEASE) 启动器,可以快速配置 Spring MVC 。
- [`mybatis-spring-boot-starter`](https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter/1.3.2) 启动器,可以快速配置 MyBatis 。
并且Spring Boot 基本已经一统 Java 项目的开发,大量的开源项目都实现了其的 Starter 启动器。例如:
- [`incubator-dubbo-spring-boot-project`](https://github.com/apache/incubator-dubbo-spring-boot-project) 启动器,可以快速配置 Dubbo 。
- [`rocketmq-spring-boot-starter`](https://github.com/maihaoche/rocketmq-spring-boot-starter) 启动器,可以快速配置 RocketMQ 。
## Spring Boot 提供了哪些核心功能?
- 1、独立运行 Spring 项目
Spring Boot 可以以 jar 包形式独立运行,运行一个 Spring Boot 项目只需要通过 `java -jar xx.jar` 来运行。
- 2、内嵌 Servlet 容器
Spring Boot 可以选择内嵌 Tomcat、Jetty 或者 Undertow这样我们无须以 war 包形式部署项目。
> 第 2 点是对第 1 点的补充,在 Spring Boot 未出来的时候,大多数 Web 项目,是打包成 war 包,部署到 Tomcat、Jetty 等容器。
- 3、提供 Starter 简化 Maven 配置
Spring 提供了一系列的 starter pom 来简化 Maven 的依赖加载。例如,当你使用了 `spring-boot-starter-web` ,会自动加入如下依赖:[![`spring-boot-starter-web` 的 pom 文件](05-Spring Boot 面试题.assets/01.png)](http://static.iocoder.cn/images/Spring/2018-12-26/01.png)`spring-boot-starter-web` 的 pom 文件
- 4、[自动配置 Spring Bean](https://www.jianshu.com/p/ddb6e32e3faf)
Spring Boot 检测到特定类的存在,就会针对这个应用做一定的配置,进行自动配置 Bean ,这样会极大地减少我们要使用的配置。
当然Spring Boot 只考虑大多数的开发场景并不是所有的场景若在实际开发中我们需要配置Bean ,而 Spring Boot 没有提供支持,则可以自定义自动配置进行解决。
- 5、[准生产的应用监控](https://blog.csdn.net/wangshuang1631/article/details/72810412)
Spring Boot 提供基于 HTTP、JMX、SSH 对运行时的项目进行监控。
- 6、无代码生成和 XML 配置
Spring Boot 没有引入任何形式的代码生成,它是使用的 Spring 4.0 的条件 `@Condition` 注解以实现根据条件进行配置。同时使用了 Maven /Gradle 的**依赖传递解析机制**来实现 Spring 应用里面的自动配置。
> 第 6 点是第 3 点的补充。
## Spring Boot 有什么优缺点?
> 艿艿:任何技术栈,有优点必有缺点,没有银弹。
>
> 另外,这个问题的回答,我们是基于 [《Spring Boot浅谈(是什么/能干什么/优点和不足)》](https://blog.csdn.net/fly_zhyu/article/details/76407830) 整理,所以胖友主要看下这篇文章。
**Spring Boot 的优点**
> 艿艿:优点和 [「Spring Boot 提供了哪些核心功能?」](https://svip.iocoder.cn/Spring-Boot/Interview/#) 问题的答案,是比较重叠的。
- 1、使【编码】变简单。
- 2、使【配置】变简单。
- 3、使【部署】变简单。
- 4、使【监控】变简单。
**Spring Boot 的缺点**
> 艿艿:如下的缺点,基于 [《Spring Boot浅谈(是什么/能干什么/优点和不足)》](https://blog.csdn.net/fly_zhyu/article/details/76407830),考虑的出发点是把 Spring Boot 作为微服务的框架的选型的角度进行考虑。
- 1、没有提供相应的【服务发现和注册】的配套功能。
> 艿艿:当然,实际上 Spring Boot 本身是不需要提供这样的功能。服务发现和注册的功能,是在 Spring Cloud 中进行提供。
- 2、自身的 acturator 所提供的【监控功能】,也需要与现有的监控对接。
- 3、没有配套的【安全管控】方案。
> 艿艿关于这一点艿艿也有点迷糊Spring Security 是可以比较方便的集成到 Spring Boot 中,所以不晓得这里的【安全管控】的定义是什么。所以这一点,面试的时候回答,可以暂时先省略。
- 4、对于 REST 的落地,还需要自行结合实际进行 URI 的规范化工作
> 艿艿:这个严格来说,不算缺点。本身,是规范的范畴。
所以上面的缺点严格来说可能不太适合在面试中回答。艿艿认为Spring Boot 的缺点主要是,因为自动配置 Spring Bean 的功能,我们可能无法知道,哪些 Bean 被进行创建了。这个时候,如果我们想要自定义一些 Bean ,可能存在冲突,或者不知道实际注入的情况。
## Spring Boot、Spring MVC 和 Spring 有什么区别?
Spring 的完整名字,应该是 Spring Framework 。它提供了多个模块Spring IoC、Spring AOP、Spring MVC 等等。所以Spring MVC 是 Spring Framework 众多模块中的一个。
而 Spring Boot 是构造在 Spring Framework 之上的 Boot 启动器,旨在更容易的配置一个 Spring 项目。
总结说来,如下图所示:[![Spring Boot 对比 Spring MVC 对比 Spring ](05-Spring Boot 面试题.assets/02.png)](http://static.iocoder.cn/images/Spring/2018-12-26/02.png)Spring Boot 对比 Spring MVC 对比 Spring
## Spring Boot 中的 Starter 是什么?
比较**通俗**的说法:
> FROM [《Spring Boot 中 Starter 是什么》](https://www.cnblogs.com/EasonJim/p/7615801.html)
>
> 比如我们要在 Spring Boot 中引入 Web MVC 的支持时,我们通常会引入这个模块 `spring-boot-starter-web` ,而这个模块如果解压包出来会发现里面什么都没有,只定义了一些 **POM** 依赖。如下图所示:[![`spring-boot-starter-web`](05-Spring Boot 面试题.assets/03.png)](http://static.iocoder.cn/images/Spring/2018-12-26/03.png)`spring-boot-starter-web`
>
> 经过研究Starter 主要用来简化依赖用的。比如我们之前做MVC时要引入日志组件那么需要去找到log4j的版本然后引入现在有了Starter之后直接用这个之后log4j就自动引入了也不用关心版本这些问题。
比较**书名**的说法:
> FROM [《Spring Boot Starter 介绍》](http://www.importnew.com/27101.html)
>
> 依赖管理是任何复杂项目的关键部分。以手动的方式来实现依赖管理不太现实,你得花更多时间,同时你在项目的其他重要方面能付出的时间就会变得越少。
>
> Spring Boot Starter 就是为了解决这个问题而诞生的。Starter **POM** 是一组方便的依赖描述符,您可以将其包含在应用程序中。您可以获得所需的所有 Spring 和相关技术的一站式服务,无需通过示例代码搜索和复制粘贴依赖。
## Spring Boot 常用的 Starter 有哪些?
- `spring-boot-starter-web` :提供 Spring MVC + 内嵌的 Tomcat 。
- `spring-boot-starter-data-jpa` :提供 Spring JPA + Hibernate 。
- `spring-boot-starter-data-redis` :提供 Redis 。
- `mybatis-spring-boot-starter` :提供 MyBatis 。
## 创建一个 Spring Boot Project 的最简单的方法是什么?
Spring Initializr 是创建 Spring Boot Projects 的一个很好的工具。打开 `"https://start.spring.io/"` 网站,我们可以看到 Spring Initializr 工具,如下图所示:
[![Spring Initializr](05-Spring Boot 面试题.assets/04.png)](http://static.iocoder.cn/images/Spring/2018-12-26/04.png)Spring Initializr
- 图中的每一个**红线**,都可以填写相应的配置。相信胖友都很熟悉,就不哔哔了。
- 点击生 GenerateProject ,生成 Spring Boot Project 。
- 将项目导入 IDEA ,记得选择现有的 Maven 项目。
------
当然,我们以前使用 IDEA 创建 Spring 项目的方式,也一样能创建 Spring Boot Project 。Spring Initializr 更多的是,提供一个便捷的工具。
## 如何统一引入 Spring Boot 版本?
**目前有两种方式**
① 方式一:继承 `spring-boot-starter-parent` 项目。配置代码如下:
```
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.1.RELEASE</version>
</parent>
```
② 方式二:导入 spring-boot-dependencies 项目依赖。配置代码如下:
```
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>1.5.1.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
```
**如何选择?**
因为一般我们的项目中,都有项目自己的 Maven parent 项目,所以【方式一】显然会存在冲突。所以实际场景下,推荐使用【方式二】。
详细的,推荐阅读 [《Spring Boot 不使用默认的 parent改用自己的项目的 parent》](https://blog.csdn.net/rainbow702/article/details/55046298) 文章。
另外,在使用 Spring Cloud 的时候,也可以使用这样的方式。
## 运行 Spring Boot 有哪几种方式?
- 1、打包成 Fat Jar ,直接使用 `java -jar` 运行。目前主流的做法,推荐。
- 2、在 IDEA 或 Eclipse 中,直接运行应用的 Spring Boot 启动类的 `#main(String[] args)` 启动。适用于开发调试场景。
- 3、如果是 Web 项目,可以打包成 War 包,使用外部 Tomcat 或 Jetty 等容器。
## 如何打包 Spring Boot 项目?
通过引入 `spring-boot-maven-plugin` 插件,执行 `mvn clean package` 命令,将 Spring Boot 项目打成一个 Fat Jar 。后续,我们就可以直接使用 `java -jar` 运行。
关于 `spring-boot-maven-plugin` 插件,更多详细的可以看看 [《创建可执行 jar》](https://qbgbook.gitbooks.io/spring-boot-reference-guide-zh/II. Getting started/11.5. Creating an executable jar.html) 。
## 如果更改内嵌 Tomcat 的端口?
- 方式一,修改 `application.properties` 配置文件的 `server.port` 属性。
```
server.port=9090
```
- 方式二,通过启动命令增加 `server.port` 参数进行修改。
```
java -jar xxx.jar --server.port=9090
```
当然,以上的方式,不仅仅适用于 Tomcat ,也适用于 Jetty、Undertow 等服务器。
## 如何重新加载 Spring Boot 上的更改,而无需重新启动服务器?
一共有三种方式,可以实现效果:
- 【推荐】`spring-boot-devtools` 插件。注意,这个工具需要配置 IDEA 的自动编译。
- Spring Loaded 插件。
> Spring Boot 2.X 后,官方宣布不再支持 Spring Loaded 插件 的更新,所以基本可以无视它了。
- [JRebel](https://www.jianshu.com/p/bab43eaa4e14) 插件,需要付费。
关于如何使用 `spring-boot-devtools` 和 Spring Loaded 插件,胖友可以看看 [《Spring Boot 学习笔记Spring Boot Developer Tools 与热部署》](https://segmentfault.com/a/1190000014488100) 。
## Spring Boot 的配置文件有哪几种格式?
Spring Boot 目前支持两种格式的配置文件:
- `.properties` 格式。示例如下:
```
server.port = 9090
```
- `.yaml` 格式。示例如下:
```
server:
port: 9090
```
------
可能有胖友不了解 **YAML 格式**
YAML 是一种人类可读的数据序列化语言,它通常用于配置文件。
- 与 Properties 文件相比,如果我们想要在配置文件中添加复杂的属性 YAML 文件就更加**结构化**。从上面的示例,我们可以看出 YAML 具有**分层**配置数据。
- 当然 YAML 在 Spring 会存在一个缺陷,
`@PropertySource`
注解不支持读取 YAML 配置文件,仅支持 Properties 配置文件。
- 不过这个问题也不大,可以麻烦一点使用 [`@Value`](https://blog.csdn.net/lafengwnagzi/article/details/74178374) 注解,来读取 YAML 配置项。
实际场景下,艿艿相对比较喜欢使用 Properties 配置文件。个人喜欢~当然YAML 已经越来越流行了。
## Spring Boot 默认配置文件是什么?
对于 Spring Boot 应用,默认的配置文件根目录下的 **application** 配置文件,当然可以是 Properties 格式,也可以是 YAML 格式。
可能有胖友说,我在网上看到面试题中,说还有一个根目录下的 **bootstrap** 配置文件。这个是 Spring Cloud 新增的启动配置文件,[需要引入 `spring-cloud-context` 依赖后,才会进行加载](https://my.oschina.net/freeskyjs/blog/1843048)。它的特点和用途主要是:
> 参考 [《Spring Cloud 中配置文件名 bootstrap.yml 和 application.yml 区别》](https://my.oschina.net/neverforget/blog/1525947) 文章。
- 【特点】因为 bootstrap 由父 ApplicationContext 加载,比 application 优先加载。
- 【特点】因为 bootstrap 优先于 application 加载,所以不会被它覆盖。
- 【用途】使用配置中心 Spring Cloud Config 时,需要在 bootstrap 中配置配置中心的地址,从而实现父 ApplicationContext 加载时,从配置中心拉取相应的配置到应用中。
另外,[《Appendix A. Common application properties》](https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html) 中,有 application 配置文件的通用属性列表。
## Spring Boot 如何定义多套不同环境配置?
可以参考 [《Spring Boot 教程 - Spring Boot Profiles 实现多环境下配置切换》](https://blog.csdn.net/top_code/article/details/78570047) 一文。
但是,需要考虑一个问题,生产环境的配置文件的安全性,显然我们不能且不应该把生产的配置放到项目的 Git 仓库中进行管理。那么应该怎么办呢?
- 方案一,生产环境的配置文件放在生产环境的服务器中,以 `java -jar myproject.jar --spring.config.location=/xxx/yyy/application-prod.properties` 命令,设置 参数 `spring.config.location` 指向配置文件。
- 方案二,使用 Jenkins 在执行打包,配置上 Maven Profile 功能,使用服务器上的配置文件。😈 整体来说,和【方案一】的差异是,将配置文件打包进了 Jar 包中。
- 方案三,使用配置中心。
## Spring Boot 配置加载顺序?
在 Spring Boot 中,除了我们常用的 application 配置文件之外,还有:
- 系统环境变量
- 命令行参数
- 等等…
参考 [《Externalized Configuration》](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html) 文档,我们整理顺序如下:
1. ```
spring-boot-devtools
```
依赖的
```
spring-boot-devtools.properties
```
配置文件。
> 这个灰常小众,具体说明可以看看 [《Spring Boot参考文档12开发者工具》](https://blog.csdn.net/u011499747/article/details/71746325) ,建议无视。
2. 单元测试上的
```
@TestPropertySource
```
```
@SpringBootTest
```
注解指定的参数。
> 前者的优先级高于后者。可以看看 [《Spring、Spring Boot 和TestNG 测试指南 - @TestPropertySource》](https://segmentfault.com/a/1190000010854607) 一文。
3. 命令行指定的参数。例如 `java -jar springboot.jar --server.port=9090` 。
4. 命令行中的 `spring.application.json` 指定参数。例如 `java -Dspring.application.json='{"name":"Java"}' -jar springboot.jar` 。
5. ServletConfig 初始化参数。
6. ServletContext 初始化参数。
7. JNDI 参数。例如 `java:comp/env` 。
8. Java 系统变量,即 `System#getProperties()` 方法对应的。
9. 操作系统环境变量。
10. RandomValuePropertySource 配置的 `random.*` 属性对应的值。
11. Jar **外部**的带指定 profile 的 application 配置文件。例如 `application-{profile}.yaml` 。
12. Jar **内部**的带指定 profile 的 application 配置文件。例如 `application-{profile}.yaml` 。
13. Jar **外部** application 配置文件。例如 `application.yaml` 。
14. Jar **内部** application 配置文件。例如 `application.yaml` 。
15. 在自定义的 `@Configuration` 类中定于的 `@PropertySource` 。
16. 启动的 main 方法中,定义的默认配置。即通过 `SpringApplication#setDefaultProperties(Map<String, Object> defaultProperties)` 方法进行设置。
嘿嘿,是不是很多很长,不用真的去记住。
- 一般来说,面试官不会因为这个题目回答的不好,对你扣分。
- 实际使用时,做下测试即可。
- 每一种配置方式的详细说明,可以看看 [《Spring Boot 参考指南(外部化配置)》](https://segmentfault.com/a/1190000015069140) 。
## Spring Boot 有哪些配置方式?
和 Spring 一样,一共提供了三种方式。
- 1、XML 配置文件。
Bean 所需的依赖项和服务在 XML 格式的配置文件中指定。这些配置文件通常包含许多 bean 定义和特定于应用程序的配置选项。它们通常以 bean 标签开头。例如:
```
<bean id="studentBean" class="org.edureka.firstSpring.StudentBean">
<property name="name" value="Edureka"></property>
</bean>
```
- 2、注解配置。
您可以通过在相关的类,方法或字段声明上使用注解,将 Bean 配置为组件类本身,而不是使用 XML 来描述 Bean 装配。默认情况下Spring 容器中未打开注解装配。因此,您需要在使用它之前在 Spring 配置文件中启用它。例如:
```
<beans>
<context:annotation-config/>
<!-- bean definitions go here -->
</beans>
```
- 3、Java Config 配置。
Spring 的 Java 配置是通过使用 @Bean 和 @Configuration 来实现。
- `@Bean` 注解扮演与 `<bean />` 元素相同的角色。
- `@Configuration` 类允许通过简单地调用同一个类中的其他 `@Bean` 方法来定义 Bean 间依赖关系。
- 例如:
```
@Configuration
public class StudentConfig {
@Bean
public StudentBean myStudent() {
return new StudentBean();
}
}
```
- 是不是很熟悉 😈
目前主要使用 **Java Config** 配置为主。当然,三种配置方式是可以混合使用的。例如说:
- Dubbo 服务的配置,艿艿喜欢使用 XML 。
- Spring MVC 请求的配置,艿艿喜欢使用 `@RequestMapping` 注解。
- Spring MVC 拦截器的配置,艿艿喜欢 Java Config 配置。
------
另外,现在已经是 Spring Boot 的天下,所以更加是 **Java Config** 配置为主。
## Spring Boot 的核心注解是哪个?
```
package cn.iocoder.skywalking.web01;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Web01Application {
public static void main(String[] args) {
SpringApplication.run(Web01Application.class, args);
}
}
```
- `@SpringBootApplication` 注解,就是 Spring Boot 的核心注解。
`org.springframework.boot.autoconfigure.@SpringBootApplication` 注解的代码如下:
```
// SpringBootApplication.java
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
@AliasFor(
annotation = EnableAutoConfiguration.class
)
Class<?>[] exclude() default {};
@AliasFor(
annotation = EnableAutoConfiguration.class
)
String[] excludeName() default {};
@AliasFor(
annotation = ComponentScan.class,
attribute = "basePackages"
)
String[] scanBasePackages() default {};
@AliasFor(
annotation = ComponentScan.class,
attribute = "basePackageClasses"
)
Class<?>[] scanBasePackageClasses() default {};
}
```
- 它组合了 3 个注解,详细说明,胖友看看 [《Spring Boot 系列:@SpringBootApplication 注解》](https://blog.csdn.net/claram/article/details/75125749) 。
- `@Configuration` 注解,指定类是 **Bean 定义**的配置类。
> `@Configuration` 注解,来自 `spring-context` 项目,用于 Java Config ,不是 Spring Boot 新带来的。
- `#ComponentScan` 注解,扫描指定包下的 Bean 们。
> `@ComponentScan` 注解,来自 `spring-context` 项目,用于 Java Config ,不是 Spring Boot 新带来的。
- `@EnableAutoConfiguration` 注解,打开自动配置的功能。如果我们想要关闭某个类的自动配置,可以设置注解的 `exclude` 或 `excludeName` 属性。
> `@EnableAutoConfiguration` 注解,来自 `spring-boot-autoconfigure` 项目,**它才是 Spring Boot 新带来的**。
## 什么是 Spring Boot 自动配置?
在 [「Spring Boot 的核心注解是哪个?」](https://svip.iocoder.cn/Spring-Boot/Interview/#) 中,我们已经看到,使用 `@@EnableAutoConfiguration` 注解,打开 Spring Boot 自动配置的功能。具体如何实现的,可以看看如下两篇文章:
- [《@EnableAutoConfiguration 注解的工作原理》](https://www.jianshu.com/p/464d04c36fb1) 。
- [《一个面试题引起的 Spring Boot 启动解析》](https://juejin.im/post/5b679fbc5188251aad213110)
- 建议,能一边调试,一边看这篇文章。调试很简单,任一搭建一个 Spring Boot 项目即可。
如下是一个比较简单的总结:
1. Spring Boot 在启动时扫描项目所依赖的 jar 包,寻找包含`spring.factories` 文件的 jar 包。
2. 根据 `spring.factories` 配置加载 AutoConfigure 类。
3. 根据 [`@Conditional` 等条件注解](https://svip.iocoder.cn/Spring-Boot/Interview/Spring Boot 条件注解) 的条件,进行自动配置并将 Bean 注入 Spring IoC 中。
## Spring Boot 有哪几种读取配置的方式?
Spring Boot 目前支持 **2** 种读取配置:
1. `@Value` 注解,读取配置到属性。最最最常用。
> 另外,支持和 `@PropertySource` 注解一起使用,指定使用的配置文件。
2. `@ConfigurationProperties` 注解,读取配置到类上。
> 另外,支持和 `@PropertySource` 注解一起使用,指定使用的配置文件。
详细的使用方式,可以参考 [《Spring Boot 读取配置的几种方式》](https://aoyouzi.iteye.com/blog/2422837) 。
## 使用 Spring Boot 后,项目结构是怎么样的呢?
我们先来说说项目的分层。一般来说,主流的有两种方式:
- 方式一,`controller`、`service`、`dao` 三个包,每个包下面添加相应的 XXXController、YYYService、ZZZDAO 。
- 方式二,按照业务模块分包,每个包里面放 Controller、Service、DAO 类。例如,业务模块分成 `user`、`order`、`item` 等等包,在 `user` 包里放 UserController、UserService、UserDAO 类。
那么,使用 Spring Boot 的项目怎么分层呢?艿艿自己的想法
- 现在项目都会进行服务化分拆,每个项目不会特别复杂,所以建议使用【方式一】。
- 以前的项目,大多是单体的项目,动则项目几万到几十万的代码,当时多采用【方式二】。
下面是一个简单的 Spring Boot 项目的 Demo ,如下所示:[![Spring Boot 项目的 Demo](05-Spring Boot 面试题.assets/05.png)](http://static.iocoder.cn/images/Spring/2018-12-26/05.png)Spring Boot 项目的 Demo
## 如何在 Spring Boot 启动的时候运行一些特殊的代码?
如果需要在 SpringApplication 启动后执行一些特殊的代码,你可以实现 ApplicationRunner 或 CommandLineRunner 接口,这两个接口工作方式相同,都只提供单一的 run 方法,该方法仅在 `SpringApplication#run(...)` 方法**完成之前调用**。
一般情况下,我们不太会使用该功能。如果真需要,胖友可以详细看看 [《使用 ApplicationRunner 或 CommandLineRunner 》](https://qbgbook.gitbooks.io/spring-boot-reference-guide-zh/IV. Spring Boot features/23.8 Using the ApplicationRunner or CommandLineRunner.html) 。
## Spring Boot 2.X 有什么新特性?
1. 起步 JDK 8 和支持 JDK 9
2. 第三方库的升级
3. Reactive Spring
4. HTTP/2 支持
5. 配置属性的绑定
6. Gradle 插件
7. Actuator 改进
8. 数据支持的改进
9. Web 的改进
10. 支持 Quartz 自动配置
11. 测试的改进
12. 其它…
详细的说明,可以看看 [《Spring Boot 2.0系列文章(二)Spring Boot 2.0 新特性详解》](http://www.54tianzhisheng.cn/2018/03/06/SpringBoot2-new-features) 。
# 整合篇
## 如何将内嵌服务器换成 Jetty
默认情况下,`spring-boot-starter-web` 模块使用 Tomcat 作为内嵌的服务器。所以需要去除对 `spring-boot-starter-tomcat` 模块的引用,添加 `spring-boot-starter-jetty` 模块的引用。代码如下:
```
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion> <!-- 去除 Tomcat -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency> <!-- 引入 Jetty -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
```
## Spring Boot 中的监视器 Actuator 是什么?
`spring-boot-actuator` 提供 Spring Boot 的监视器功能,可帮助我们访问生产环境中正在运行的应用程序的**当前状态**。
- 关于 Spring Boot Actuator 的教程,可以看看 [《Spring Boot Actuator 使用》](https://www.jianshu.com/p/af9738634a21) 。
- 上述教程是基于 Spring Boot 1.X 的版本,如果胖友使用 Spring Boot 2.X 的版本,你将会发现 `/beans` 等 Endpoint 是不存在的,参考 [《Spring boot 2 - Actuator endpoint, where is /beans endpoint》](https://stackoverflow.com/questions/49174700/spring-boot-2-actuator-endpoint-where-is-beans-endpoint) 问题来解决。
**安全性**
Spring Boot 2.X 默认情况下,`spring-boot-actuator` 产生的 Endpoint 是没有安全保护的,但是 Actuator 可能暴露敏感信息。
所以一般的做法是,引入 `spring-boot-start-security` 依赖,使用 Spring Security 对它们进行安全保护。
## 如何集成 Spring Boot 和 Spring MVC
1. 引入 `spring-boot-starter-web` 的依赖。
2. 实现 WebMvcConfigurer 接口,可添加自定义的 Spring MVC 配置。
> 因为 Spring Boot 2 基于 JDK 8 的版本,而 JDK 8 提供 `default` 方法,所以 Spring Boot 2 废弃了 WebMvcConfigurerAdapter 适配类,直接使用 WebMvcConfigurer 即可。
```
// WebMvcConfigurer.java
public interface WebMvcConfigurer {
/** 配置路径匹配器 **/
default void configurePathMatch(PathMatchConfigurer configurer) {}
/** 配置内容裁决的一些选项 **/
default void configureContentNegotiation(ContentNegotiationConfigurer configurer) { }
/** 异步相关的配置 **/
default void configureAsyncSupport(AsyncSupportConfigurer configurer) { }
default void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { }
default void addFormatters(FormatterRegistry registry) {
}
/** 添加拦截器 **/
default void addInterceptors(InterceptorRegistry registry) { }
/** 静态资源处理 **/
default void addResourceHandlers(ResourceHandlerRegistry registry) { }
/** 解决跨域问题 **/
default void addCorsMappings(CorsRegistry registry) { }
default void addViewControllers(ViewControllerRegistry registry) { }
/** 配置视图解析器 **/
default void configureViewResolvers(ViewResolverRegistry registry) { }
/** 添加参数解析器 **/
default void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
}
/** 添加返回值处理器 **/
default void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) { }
/** 这里配置视图解析器 **/
default void configureMessageConverters(List<HttpMessageConverter<?>> converters) { }
/** 配置消息转换器 **/
default void extendMessageConverters(List<HttpMessageConverter<?>> converters) { }
/** 配置异常处理器 **/
default void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) { }
default void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) { }
@Nullable
default Validator getValidator() { return null; }
@Nullable
default MessageCodesResolver getMessageCodesResolver() { return null; }
}
```
------
在使用 Spring MVC 时,我们一般会做如下几件事情:
1. 实现自己项目需要的拦截器,并在 WebMvcConfigurer 实现类中配置。可参见 [MVCConfiguration](https://github.com/YunaiV/oceans/blob/2a2d3746905f1349e260e88049e7e28346c7648f/bff/webapp-bff/src/main/java/cn/iocoder/oceans/webapp/bff/config/MVCConfiguration.java) 类。
2. 配置 `@ControllerAdvice` + `@ExceptionHandler` 注解,实现全局异常处理。可参见 [GlobalExceptionHandler](https://github.com/YunaiV/oceans/blob/2a2d3746905f1349e260e88049e7e28346c7648f/bff/webapp-bff/src/main/java/cn/iocoder/oceans/webapp/bff/config/GlobalExceptionHandler.java) 类。
3. 配置 `@ControllerAdvice` ,实现 ResponseBodyAdvice 接口,实现全局统一返回。可参见 [GlobalResponseBodyAdvice](https://github.com/YunaiV/oceans/blob/2a2d3746905f1349e260e88049e7e28346c7648f/bff/webapp-bff/src/main/java/cn/iocoder/oceans/webapp/bff/config/GlobalResponseBodyAdvice.java) 。
当然有一点需要注意WebMvcConfigurer、ResponseBodyAdvice、`@ControllerAdvice`、`@ExceptionHandler` 接口,都是 Spring MVC 框架自身已经有的东西。
- `spring-boot-starter-web` 的依赖,帮我们解决的是 Spring MVC 的依赖以及相关的 Tomcat 等组件。
## 如何集成 Spring Boot 和 Spring Security
目前比较主流的安全框架有两个:
1. Spring Security
2. Apache Shiro
对于任何项目来说,安全认证总是少不了,同样适用于使用 Spring Boot 的项目。相对来说Spring Security 现在会比 Apache Shiro 更流行。
Spring Boot 和 Spring Security 的配置方式比较简单:
1. 引入 `spring-boot-starter-security` 的依赖。
2. 继承 WebSecurityConfigurerAdapter ,添加**自定义**的安全配置。
当然,每个项目的安全配置是不同的,需要胖友自己选择。更多详细的使用,建议认真阅读如下文章:
- [《Spring Boot中 使用 Spring Security 进行安全控制》](http://blog.didispace.com/springbootsecurity/) ,快速上手。
- [《Spring Security 实现原理与源码解析系统 —— 精品合集》](http://www.iocoder.cn/Spring-Security/good-collection/) ,深入源码。
另外,安全是一个很大的话题,感兴趣的胖友,可以看看 [《Spring Boot 十种安全措施》](https://www.jdon.com/49653) 一文。
## 如何集成 Spring Boot 和 Spring Security OAuth2
参见 [《Spring Security OAuth2 入门》](http://www.iocoder.cn/Spring-Security/OAuth2-learning/) 文章,内容有点多。
## 如何集成 Spring Boot 和 JPA
1. 引入 `spring-boot-starter-data-jpa` 的依赖。
2. 在 application 配置文件中,加入 JPA 相关的少量配置。当然,数据库的配置也要添加进去。
3. 具体编码。
详细的使用,胖友可以参考:
- [《一起来学 SpringBoot 2.x | 第六篇:整合 Spring Data JPA》](http://www.iocoder.cn/Spring-Boot/battcn/v2-orm-jpa/)
有两点需要注意:
- Spring Boot 2 默认使用的数据库连接池是 [HikariCP](https://github.com/brettwooldridge/HikariCP) ,目前最好的性能的数据库连接池的实现。
- `spring-boot-starter-data-jpa` 的依赖,使用的默认 JPA 实现是 Hibernate 5.X 。
## 如何集成 Spring Boot 和 MyBatis
1. 引入 `mybatis-spring-boot-starter` 的依赖。
2. 在 application 配置文件中,加入 MyBatis 相关的少量配置。当然,数据库的配置也要添加进去。
3. 具体编码。
详细的使用,胖友可以参考:
- [《一起来学 SpringBoot 2.x | 第七篇:整合 Mybatis》](http://www.iocoder.cn/Spring-Boot/battcn/v2-orm-mybatis/)
## 如何集成 Spring Boot 和 RabbitMQ
1. 引入 `spring-boot-starter-amqp` 的依赖
2. 在 application 配置文件中,加入 RabbitMQ 相关的少量配置。
3. 具体编码。
详细的使用,胖友可以参考:
- [《一起来学 SpringBoot 2.x | 第十二篇:初探 RabbitMQ 消息队列》](http://www.iocoder.cn/Spring-Boot/battcn/v2-queue-rabbitmq/)
- [《一起来学 SpringBoot 2.x | 第十三篇RabbitMQ 延迟队列》](http://www.iocoder.cn/Spring-Boot/battcn/v2-queue-rabbitmq-delay/)
## 如何集成 Spring Boot 和 Kafka
1. 引入 `spring-kafka` 的依赖。
2. 在 application 配置文件中,加入 Kafka 相关的少量配置。
3. 具体编码。
详细的使用,胖友可以参考:
- [《Spring Boot系列文章SpringBoot Kafka 整合使用》](http://www.54tianzhisheng.cn/2018/01/05/SpringBoot-Kafka/)
## 如何集成 Spring Boot 和 RocketMQ
1. 引入 `rocketmq-spring-boot` 的依赖。
2. 在 application 配置文件中,加入 RocketMQ 相关的少量配置。
3. 具体编码。
详细的使用,胖友可以参考:
- [《我用这种方法在 Spring 中实现消息的发送和消费》](http://www.iocoder.cn/RocketMQ/start/spring-boot-example)
## Spring Boot 支持哪些日志框架?
Spring Boot 支持的日志框架有:
- Logback
- Log4j2
- Log4j
- Java Util Logging
默认使用的是 Logback 日志框架,也是目前较为推荐的,具体配置,可以参见 [《一起来学 SpringBoot 2.x | 第三篇SpringBoot 日志配置》](http://www.iocoder.cn/Spring-Boot/battcn/v2-config-logs/) 。
因为 Log4j2 的性能更加优秀,也有人在生产上使用,可以参考 [《Spring Boot Log4j2 日志性能之巅》](https://www.jianshu.com/p/f18a9cff351d) 配置。
# 666. 彩蛋
😈 看完之后,复习复习 Spring Boot 美滋滋。有一种奇怪的感觉,把面试题写成了 Spring 的学习指南。
当然,如果胖友有新的面试题,欢迎在星球一起探讨补充。
参考和推荐如下文章:
- 我有面试宝典 [《[经验分享\] Spring Boot面试题总结》](http://www.wityx.com/post/242_1_1.html)
- Java 知音 [《Spring Boot 面试题精华》](https://cloud.tencent.com/developer/article/1348086)
- 祖大帅 [《一个面试题引起的 Spring Boot 启动解析》](https://juejin.im/post/5b679fbc5188251aad213110)
- 大胡子叔叔_ [《Spring Boot + Spring Cloud 相关面试题》](https://blog.csdn.net/panhaigang123/article/details/79587612)
- 墨斗鱼博客 [《20 道 Spring Boot 面试题》](https://www.mudouyu.com/article/26)
- 夕阳雨晴 [《Spring Boot Starter 的面试题》](https://blog.csdn.net/sun1021873926/article/details/78176354)

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>http://svip.iocoder.cn</title>
<link href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-M/bootstrap/4.6.1/css/bootstrap.min.css"
type="text/css" rel="stylesheet" />
<link href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css" rel="stylesheet" crossorigin="anonymous"/>
</head>
<body>
<div class="container">
<form action="/user/auth" class="form-signin" method="post"><input type="hidden" name="_csrf" value="ef402abe-a008-4752-9666-9dab4eb1363c"/>
<h2 class="form-signin-heading">登录</h2>
<p>
<label for="username" class="sr-only">用户名</label>
<input type="text" id="username" name="username" class="form-control" placeholder="用户名" required autofocus>
</p>
<p>
<label for="password" class="sr-only">密码</label>
<input type="password" id="password" name="password" class="form-control" placeholder="密码" required>
</p>
<button class="btn btn-lg btn-primary btn-block" type="submit">登录</button>
<p>如果账号密码不正确,访问 <a href="https://t.zsxq.com/11JUHkGgl">https://t.zsxq.com/11JUHkGgl</a> 或 <a href="https://t.zsxq.com/yBUj2NN">https://t.zsxq.com/yBUj2NN</a> 解决</p>
</form>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,517 @@
# 精尽 Spring Cloud 面试题「最新更新时间2024-02」
以下面试题,基于网络整理,和自己编辑。具体参考的文章,会在文末给出所有的链接。
如果胖友有自己的疑问,欢迎在星球提问,我们一起整理吊吊的 Spring Cloud 面试题的大保健。
而题目的难度,艿艿尽量按照从容易到困难的顺序,逐步下去。
> 为什么标题会带有 Beta 呢?
>
> 因为艿艿自己生产并未使用 Spring Cloud ,而是使用 Dubbo 。所以,在实战经验方面,会相对匮乏。所以,这篇面试题,献丑了~
另外,本文不会详细解答每个问题,而是提供很多问题回答的文章。等后续艿艿详细的研究了 Spring Cloud Alibaba 后,补充好这篇文章~
# 什么是 Spring Cloud
Spring Cloud 是构建在 Spring Boot 基础之上,用于快速构建分布式系统的通用模式的工具集。或者说,换成大家更为熟知的,用于构建微服务的技术栈。
## Spring Cloud 核心功能是什么?
毫无疑问Spring Cloud 可以说是目前微服务架构的最好的选择涵盖了基本我们需要的所有组件所以也被称为全家桶。Spring Cloud 主要提供了如下核心的功能:
- Distributed/versioned configuration 分布式/版本化的配置管理
- Service registration and discovery 服务注册与服务发现
- Routing 路由
- Service-to-service calls 端到端的调用
- Load balancing 负载均衡
- Circuit Breakers 断路器
- Global locks 全局锁
- Leadership election and cluster state 选举与集群状态管理
- Distributed messaging 分布式消息
## Spring Cloud 有哪些组件?
Spring Cloud的 组件相当繁杂,拥有诸多子项目。如下脑图所示:
[![Spring Cloud的 组件](06-Spring Cloud 面试题.assets/4935fcc0a209fd1d4b70cade94986f59.jpeg)](http://static.iocoder.cn/4935fcc0a209fd1d4b70cade94986f59)Spring Cloud的 组件
我们最为熟知的,可能就是 [Spring Cloud Netflix](https://github.com/spring-cloud/spring-cloud-netflix) ,它是 Netflix 公司基于它们自己的 Eureka、Hystrix、Zuul、Ribbon 等组件,构建的一个 Spring Cloud 实现技术栈。
当然,可能关心 Spring Cloud 体系的胖友,已经知道 [Spring Cloud Netflix](https://github.com/spring-cloud/spring-cloud-netflix) 要进入维护模式,可能会略感担心。实际上,目前已经开始有新的基于 Spring Cloud 实现,可以作为新的选择。如下表格:
> [《Spring Cloud Netflix 项目进入维护模式》](https://blog.csdn.net/alex_xfboy/article/details/85258425) ,感兴趣的胖友,可以看看新闻。
| | Netflix | 阿里 | 其它 |
| :------- | :------ | :---------- | :----------------------------------------------------------- |
| 注册中心 | Eureka | Nacos | Zookeeper、Consul、Etcd |
| 熔断器 | Hystrix | Sentinel | Resilience4j |
| 网关 | Zuul1 | 暂无 | Spring Cloud Gateway |
| 负载均衡 | Ribbon | Dubbo(未来) | [`spring-cloud-loadbalancer`](https://github.com/spring-cloud/spring-cloud-commons/tree/master/spring-cloud-loadbalancer) |
其它组件,例如配置中心、链路追踪、服务引用等等,都有相应其它的实现。妥妥的~
## Spring Cloud 和 Spring Boot 的区别和关系?
1. Spring Boot 专注于快速方便的开发单个个体微服务。
2. Spring Cloud 是关注全局的微服务协调整理治理框架以及一整套的落地解决方案,它将 Spring Boot 开发的一个个单体微服务整合并管理起来,为各个微服务之间提供:配置管理,服务发现,断路器,路由,微代理,事件总线等的集成服务。
3. Spring Boot 可以离开 Spring Cloud 独立使用,但是 Spring Cloud 离不开 Spring Boot ,属于依赖的关系。
总结:
- Spring Boot ,专注于快速,方便的开发单个微服务个体。
- Spring Cloud ,关注全局的服务治理框架。
## Spring Cloud 和 Dubbo 的区别?
参见 [《精尽 Dubbo 面试题》](http://svip.iocoder.cn/Dubbo/Interview) 文章的 [「Spring Cloud 与 Dubbo 怎么选择?」](https://svip.iocoder.cn/Spring-Cloud/Interview/#) 问题的解答。
# 什么是微服务?
直接看 [什么是微服务?](http://www.iocoder.cn/Geek/Learn-micro-services-from-zero/What-is-a-micro-service/) 文章。
## 微服务的优缺点分别是什么?
**1优点**
- 每一个服务足够内聚,代码容易理解
- 开发效率提高,一个服务只做一件事
- 微服务能够被小团队单独开发
- 微服务是松耦合的,是有功能意义的服务
- 可以用不同的语言开发,面向接口编程
- 易于与第三方集成
- 微服务只是业务逻辑的代码,不会和 HTML、CSS 或者其他界面组合
- 开发中,两种开发模式
- 前后端分离
- 全栈工程师
- 可以灵活搭配,连接公共库/连接独立库
**2缺点**
- 分布式系统的负责性
- 多服务运维难度,随着服务的增加,运维的压力也在增大
- 系统部署依赖
- 服务间通信成本
- 数据一致性
- 系统集成测试
- 性能监控
# 注册中心
在 Spring Cloud 中,能够使用的注册中心,还是比较多的,如下:
- [`spring-cloud-netflix-eureka-server`](https://github.com/spring-cloud/spring-cloud-netflix/tree/master/spring-cloud-netflix-eureka-server) 和 [`spring-cloud-netflix-eureka-client`](https://github.com/spring-cloud/spring-cloud-netflix/tree/master/spring-cloud-netflix-eureka-server) ,基于 Eureka 实现。
- [`spring-cloud-alibaba-nacos-discovery`](https://github.com/spring-cloud-incubator/spring-cloud-alibaba/tree/master/spring-cloud-alibaba-nacos-discovery) ,基于 Nacos 实现。
- [`spring-cloud-zookeeper-discovery`](https://github.com/spring-cloud/spring-cloud-zookeeper/tree/master/spring-cloud-zookeeper-discovery) ,基于 Zookeeper 实现。
- … 等等
以上的实现,都是基于 [`spring-cloud-commons`](https://github.com/spring-cloud/spring-cloud-commons) 的 [`discovery`](https://github.com/spring-cloud/spring-cloud-commons/blob/master/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/) 的 [DiscoveryClient](https://github.com/spring-cloud/spring-cloud-commons/blob/master/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/DiscoveryClient.java) 接口,实现统一的客户端的注册发现。
## 为什么要使用服务发现?
简单来说,通过注册中心,调用方(Consumer)获得服务方(Provider)的地址,从而能够调用。
当然,实际情况下,会分成两种注册中心的发现模式:
1. 客户端发现模式
2. 服务端发现模式
在 Spring Cloud 中,我们使用前者,即客户端发现模式。
详细的内容,可以看看 [《为什么要使用服务发现》](https://blog.csdn.net/u013035373/article/details/79414529) 。
## Eureka
[![Eureka 集群](06-Spring Cloud 面试题.assets/25e9704082444add1192bc69c87198e9.jpeg)](http://static.iocoder.cn/25e9704082444add1192bc69c87198e9)Eureka 集群
- 作用:实现服务治理(服务注册与发现)
- 简介Spring Cloud Eureka是Spring Cloud Netflix项目下的服务治理模块。
- 由两个组件组成Eureka 服务端和 Eureka 客户端。
- Eureka 服务端,用作服务注册中心,支持集群部署。
- Eureka 客户端,是一个 Java 客户端,用来处理服务注册与发现。
在应用启动时Eureka 客户端向服务端注册自己的服务信息,同时将服务端的服务信息缓存到本地。客户端会和服务端周期性的进行心跳交互,以更新服务租约和服务信息。
Eureka 原理,整体如下图:[![Eureka 原理](06-Spring Cloud 面试题.assets/80c74f1d7cb9fc2a416e7b61a055d778.jpeg)](http://static.iocoder.cn/80c74f1d7cb9fc2a416e7b61a055d778)Eureka 原理
关于 Eureka 的源码解析,可以看看艿艿写的 [《Eureka 源码解析系列》](http://www.iocoder.cn/categories/Eureka/) 。
### Eureka 如何实现集群?
[《配置 Eureka Server 集群》](https://www.jianshu.com/p/5d5b2cf7d476)
此处,也很容易引申出一个问题,为什么 Eureka 被设计成 AP 的系统,答案可以看看 [《为什么不应该使用 ZooKeeper 做服务发现》](http://dockone.io/article/78) 。
### 聊聊 Eureka 缓存机制?
艿艿画了下 Eureka 的缓存机制,如下图所示:
[![Eureka 缓存机制](06-Spring Cloud 面试题.assets/01.png)](http://svip.iocoder.cn/images/SpringCloud/2018_10_28/01.png)Eureka 缓存机制
> 原图可见地址https://www.processon.com/view/link/5f49e2055653bb0c71de11e4
>
> 建议胖友可以手绘下,便于面试和面试官你侬我侬~
推荐额外阅读下如下三篇文章:
- [《详解 Eureka 缓存机制》](https://www.infoq.cn/article/y_1BCrbLONU61s1gbGsU)
- [《Eureka 的多级缓存机制》](https://blog.csdn.net/qq_38545713/article/details/105535950)
- [《Eureka 缓存细节以及生产环境的最佳配置》](http://bhsc881114.github.io/2018/04/01/eureka缓存细节以及生产环境的最佳配置/)
### 什么是 Eureka 自我保护机制?
[《[Spring Cloud\] Eureka 的自我保护模式及相关问题》](https://blog.csdn.net/t894690230/article/details/78207495)
# 负载均衡
在 Spring Cloud 中,能够使用的负载均衡,如下:
- [`spring-cloud-netflix-ribbon`](https://github.com/spring-cloud/spring-cloud-netflix/tree/master/spring-cloud-netflix-ribbon) ,基于 Ribbon 实现。
- [`spring-cloud-loadbalancer`](https://github.com/spring-cloud/spring-cloud-commons/tree/master/spring-cloud-loadbalancer) ,提供简单的负载均衡功能。
以上的实现,都是基于 [`spring-cloud-commons`](https://github.com/spring-cloud/spring-cloud-commons) 的 [`loadbalancer`](https://github.com/spring-cloud/spring-cloud-commons/tree/master/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer) 的 [ServiceInstanceChooser](https://github.com/spring-cloud/spring-cloud-commons/blob/ecabe2bb8d9cb14aa6edcff41fdb79dc304ed004/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/ServiceInstanceChooser.java) 接口,实现统一的服务的选择。并且,负载均衡组件在选择需要调用的服务之后,还提供调用该服务的功能,具体方法见 [LoadBalancerClient](https://github.com/spring-cloud/spring-cloud-commons/blob/master/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerClient.java) 接口的 `#execute(...)` 方法。
## 为什么要负载均衡?
简单来说,随着业务的发展,单台服务无法支撑访问的需要,于是搭建多个服务形成集群。那么随之要解决的是,每次请求,调用哪个服务,也就是需要进行负载均衡。
目前负载均衡有两种模式:
1. 客户端模式
2. 服务端模式
在 Spring Cloud 中,我们使用前者,即客户端模式。
详细的内容,可以看看 [《客户端负载均衡与服务端负载均衡》](https://blog.csdn.net/u014401141/article/details/78676296) 。
🦅 **负载平衡的意义什么?**
在计算中,负载平衡可以改善跨计算机,计算机集群,网络链接,中央处理单元或磁盘驱动器等多种计算资源的工作负载分布。负载平衡旨在优化资源使用,最大化吞吐量,最小化响应时间并避免任何单一资源的过载。使用多个组件进行负载平衡而不是单个组件可能会通过冗余来提高可靠性和可用性。负载平衡通常涉及专用软件或硬件,例如多层交换机或域名系统服务器进程。
## Ribbon
[![Ribbon](06-Spring Cloud 面试题.assets/950702ef9d35f23b5081c341c1de329a.jpeg)](http://static.iocoder.cn/950702ef9d35f23b5081c341c1de329a)Ribbon
- 作用:主要提供客户侧的软件负载均衡算法。
- 简介Spring Cloud Ribbon 是一个基于 HTTP 和 TCP 的客户端负载均衡工具,它基于 Netflix Ribbon 实现。通过 Spring Cloud 的封装,可以让我们轻松地将面向服务的 REST 模版请求自动转换成客户端负载均衡的服务调用。
- 注意看上图,关键点就是将外界的 rest 调用,根据负载均衡策略转换为微服务调用。
Ribbon 原理,整体如下图:[![Ribbon 原理](06-Spring Cloud 面试题.assets/36465fd7d91b3a4aeb3b28c3777649e6.jpeg)](http://static.iocoder.cn/36465fd7d91b3a4aeb3b28c3777649e6)Ribbon 原理
关于 Ribbon 的源码解析,可以看看艿艿整理的 [《Ribbon 源码解析系列》](http://www.iocoder.cn/Ribbon/good-collection/?vip) 。
### Ribbon 有哪些负载均衡算法?
[《Ribbon 负载均衡策略配置》](https://blog.csdn.net/rickiyeat/article/details/64918756)
其中,默认的负载均衡算法是 Round Robin 算法,顺序向下轮询。
### 聊聊 Ribbon 缓存机制?
还是 [《Eureka 缓存细节以及生产环境的最佳配置》](http://bhsc881114.github.io/2018/04/01/eureka缓存细节以及生产环境的最佳配置/) 这篇文章Ribbon 的缓存,可能也坑道蛮多人了。
### 聊聊 Ribbon 重试机制?
[《Spring Cloud Ribbon 重试机制》](https://www.jianshu.com/p/cdb6fedcab70)
除了重试次数,还有请求的超时可以配置。
### Ribbon 是怎么和 Eureka 整合的?
对着我们看到那张 Ribbon 原理图:
[![Ribbon 原理](06-Spring Cloud 面试题.assets/36465fd7d91b3a4aeb3b28c3777649e6.jpeg)](http://static.iocoder.cn/36465fd7d91b3a4aeb3b28c3777649e6)Ribbon 原理
- 首先Ribbon 会从 Eureka Client 里获取到对应的服务列表。
- 然后Ribbon 使用负载均衡算法获得使用的服务。
- 最后Ribbon 调用对应的服务。
另外,此处的 Eureka 仅仅是作为注册中心的举例,也是可以配合其它的注册中心使用,例如 Zookeeper 。可参考 [《以 Zookeeper 为注册中心搭建 Spring Cloud 环境》](https://www.jianshu.com/p/775c363d0fda) 文章。
# 声明式调用
在 Spring Cloud 中,目前使用的声明式调用组件,只有:
- `spring-cloud-openfeign` ,基于 Feign 实现。
如果熟悉 Dubbo 胖友的会知道Dubbo 的 Service API 接口,也是一种声明式调用的提现。
> 艿艿注意噢Feign 并非 Netflix 团队开发的组件。所有基于 Netflix 组件都在 `spring-cloud-netflix` 项目下噢。
## Feign
[Feign](https://github.com/OpenFeign/feign) 是受到 Retrofit、JAXRS-2.0 和 WebSocket 启发的 Java 客户端联编程序。Feign 的主要目标是将Java Http 客户端变得简单。
具体 Feign 如何使用,可以看看 [《对于 Spring Cloud Feign 入门示例的一点思考》](https://blog.csdn.net/u013815546/article/details/76637541) 。
有一点要注意Feign 并非一定要在 Spring Cloud 下使用,单独使用也是没问题的。
### Feign 实现原理?
**Feign的一个关键机制就是使用了动态代理**。咱们一起来看看下面的图,结合图来分析:
- 首先,如果你对某个接口定义了 `@FeignClient` 注解Feign 就会针对这个接口创建一个动态代理。
- 接着你要是调用那个接口,本质就是会调用 Feign 创建的动态代理,这是核心中的核心。
- Feig n的动态代理会根据你在接口上的 `@RequestMapping` 等注解,来动态构造出你要请求的服务的地址。
- 最后针对这个地址,发起请求、解析响应。
[![Feign 原理](06-Spring Cloud 面试题.assets/6650aa32de0def76db0e4c5228619aef.jpeg)](http://static.iocoder.cn/6650aa32de0def76db0e4c5228619aef)Feign 原理
### Feign 和 Ribbon 的区别?
Ribbon 和 Feign 都是使用于调用用其余服务的,不过方式不同。
- 启动类用的注解不同。
- Ribbon 使用的是 `@RibbonClient`
- Feign 使用的是 `@EnableFeignClients`
- 服务的指定位置不同。
- Ribbon 是在 `@RibbonClient` 注解上设置。
- Feign 则是在定义声明方法的接口中用 `@FeignClient` 注解上设置。
- 调使用方式不同。
- Ribbon 需要自己构建 Http 请求,模拟 Http 请求而后用 RestTemplate 发送给其余服务,步骤相当繁琐。
- Feign 采使用接口的方式,将需要调使用的其余服务的方法定义成声明方法就可,不需要自己构建 Http 请求。不过要注意的是声明方法的注解、方法签名要和提供服务的方法完全一致。
### Feign 是怎么和 Ribbon、Eureka 整合的?
结合 [「 Ribbon 是怎么和 Eureka 整合的?」](https://svip.iocoder.cn/Spring-Cloud/Interview/#) 问题,并结合如下图:
[![Feign + Ribbon + Eureka](06-Spring Cloud 面试题.assets/252461fbb6d64d3dbc1914b7eadbfb86.jpeg)](http://static.iocoder.cn/252461fbb6d64d3dbc1914b7eadbfb86)Feign + Ribbon + Eureka
- 首先,用户调用 Feign 创建的动态代理。
- 然后Feign 调用 Ribbon 发起调用流程。
- 首先Ribbon 会从 Eureka Client 里获取到对应的服务列表。
- 然后Ribbon 使用负载均衡算法获得使用的服务。
- ~~最后Ribbon 调用对应的服务。~~最后Ribbon 调用 Feign ,而 Feign 调用 HTTP 库最终调用使用的服务。
> 这可能是比较绕的,艿艿自己也困惑了一下,后来去请教了下 didi 。因为 Feign 和 Ribbon 都存在使用 HTTP 库调用指定的服务,那么两者在集成之后,必然是只能保留一个。比较正常的理解,也是保留 Feign 的调用,而 Ribbon 更纯粹的只负责负载均衡的功能。
想要完全理解,建议胖友直接看如下两个类:
- [LoadBalancerFeignClient](https://github.com/spring-cloud/spring-cloud-openfeign/blob/master/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/ribbon/LoadBalancerFeignClient.java) Spring Cloud 实现 Feign Client 接口的二次封装,实现对 Ribbon 的调用。
- [FeignLoadBalancer](https://github.com/spring-cloud/spring-cloud-openfeign/blob/master/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/ribbon/FeignLoadBalancer.java) Ribbon 的集成。
> 集成的是 AbstractLoadBalancerAwareClient 抽象类,它会自动注入项目中所使用的负载均衡组件。
- LoadBalancerFeignClient =》调用=》 FeignLoadBalancer 。
### 聊聊 Feign 重试机制?
可以看看 [《Spring Cloud 各组件重试总结》](http://www.itmuch.com/spring-cloud-sum/spring-cloud-retry/) 文章。因为 Ribbon 和 Feign 都有重试机制,在整合 Ribbon 的情况下,不使用 Feign 重试,而是使用 Ribbon 的重试。
# 服务保障
在 Spring Cloud 中,能够使用的服务保证,如下:
- [`spring-cloud-netflix-hystrix`](https://github.com/spring-cloud/spring-cloud-netflix/tree/master/spring-cloud-netflix-hystrix) ,基于 Hystrix 实现。
- Resilience4j
- [`spring-cloud-alibaba-sentinel`](https://github.com/spring-cloud-incubator/spring-cloud-alibaba/tree/master/spring-cloud-alibaba-sentinel) ,基于 Sentinel 实现。
## 为什么要使用服务保障?
在微服务架构中我们将业务拆分成一个个的服务服务与服务之间可以相互调用RPC。为了保证其高可用单个服务又必须集群部署。由于网络原因或者自身的原因服务并不能保证服务的 100% 可用,如果单个服务出现问题,调用这个服务就会出现网络延迟,此时若有大量的网络涌入,会形成任务累积,导致服务瘫痪,甚至导致服务“雪崩”。为了解决这个问题,就出现断路器模型。
详细的内容,可以看看 [《为什么要使用断路器 Hystrix](https://www.cnblogs.com/xyhero/p/53852cf0245c229fe3e22756a220508b.html) 。
## Hystrix
[![Hystrix](06-Spring Cloud 面试题.assets/7832f7526998500b2253f5bc0683e930.jpeg)](http://static.iocoder.cn/7832f7526998500b2253f5bc0683e930)Hystrix
- 作用:断路器,保护系统,控制故障范围。
- 简介Hystrix 是一个延迟和容错库,旨在隔离远程系统,服务和第三方库的访问点,当出现故障是不可避免的故障时,停止级联故障并在复杂的分布式系统中实现弹性。
Hystrix 原理,整体如下图:[![Hystrix 原理](06-Spring Cloud 面试题.assets/8848af2a2e093d0421d1c7113dedefc1.jpeg)](http://static.iocoder.cn/8848af2a2e093d0421d1c7113dedefc1)Hystrix 原理
关于 Hystrix 的源码解析,可以看看艿艿写的 [《Hystrix 源码解析系列》](http://www.iocoder.cn/categories/Hystrix/) 。
### Hystrix 隔离策略?
Hystrix 有两种隔离策略:
- 线程池隔离
- 信号量隔离
实际场景下,使用线程池隔离居多,因为支持超时功能。
详细的,可以看看 [《Hystrix 的资源隔离策略》](https://blog.csdn.net/liuchuanhong1/article/details/73718794) 文章。
### 聊聊 Hystrix 缓存机制?
Hystrix 提供缓存功能,作用是:
- 减少重复的请求数。
- 在同一个用户请求的上下文中,相同依赖服务的返回数据始终保持一致。
详细的,可以看看 [《Hystrix 缓存功能的使用》](https://blog.csdn.net/zhuchuangang/article/details/74566185) 文章。
### 什么是 Hystrix 断路器?
Hystrix 断路器通过 HystrixCircuitBreaker 实现。
HystrixCircuitBreaker 有三种状态
- `CLOSED` :关闭
- `OPEN` :打开
- `HALF_OPEN` :半开
其中,断路器处于 `OPEN` 状态时,链路处于**非健康**状态,命令执行时,直接调用**回退**逻辑,跳过**正常**逻辑。
HystrixCircuitBreaker 状态变迁如下图
[![HystrixCircuitBreaker 状态](06-Spring Cloud 面试题.assets/01-17182493081051.png)](http://static.iocoder.cn/images/Hystrix/2018_11_08/01.png)HystrixCircuitBreaker 状态
- 红线
:初始时,断路器处于
```
CLOSED
```
状态,链路处于
健康
状态。当满足如下条件,断路器从
```
CLOSED
```
变成
```
OPEN
```
状态:
- **周期**( 可配,`HystrixCommandProperties.default_metricsRollingStatisticalWindow = 10000 ms` )内,总请求数超过一定**量**( 可配,`HystrixCommandProperties.circuitBreakerRequestVolumeThreshold = 20` ) 。
- **错误**请求占总请求数超过一定**比例**( 可配,`HystrixCommandProperties.circuitBreakerErrorThresholdPercentage = 50%` ) 。
- **绿线** :断路器处于 `OPEN` 状态,命令执行时,若当前时间超过断路器**开启**时间一定时间( `HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds = 5000 ms` ),断路器变成 `HALF_OPEN` 状态,**尝试**调用**正常**逻辑,根据执行是否成功,**打开或关闭**熔断器【**蓝线**】。
### 什么是 Hystrix 服务降级?
在 Hystrix 断路器熔断时,可以调用一个降级方法,返回相应的结果。当然,降级方法需要配置和编码,如果胖友不需要,也可以不写,也就是不会有服务降级的功能。
具体的使用方式,可以看看 [《通过 Hystrix 理解熔断和降级》](https://blog.csdn.net/jiaobuchong/article/details/78232920) 。
# 网关服务
在 Spring Cloud 中,能够使用的网关服务,主要是两个,如下:
- [`spring-cloud-netflix-zuul`](https://github.com/spring-cloud/spring-cloud-netflix/tree/e460b94b90ea93658a9bedb1af2509ea71cacae4/spring-cloud-netflix-zuul) ,基于 Zuul1 实现。
> Netflix 最新开源的网关服务是 Zuul2 ,基于响应式的网关服务。
- [`spring-cloud-gateway`](https://github.com/spring-cloud/spring-cloud-gateway) ,基于 Spring Webflux 实现。
> 艿艿:比较大的可能性,是未来 Spring Cloud 网关的主流选择。考虑到目前资料的情况,建议使用 Zuul1 可能是更稳妥的选择,因为 Zuul1 已经能满足绝大数性能要求,实在不行也可以集群。
## 为什么要网关服务?
使用网关服务,我们实现统一的功能:
- 动态路由
- 灰度发布
- 健康检查
- 限流
- 熔断
- 认证: 如数支持 HMAC, JWT, Basic, OAuth 2.0 等常用协议
- 鉴权: 权限控制IP 黑白名单,同样是 OpenResty 的特性
- 可用性
- 高性能
详细的,可以看看 [《为什么微服务需要 API 网关?》](http://dockone.io/article/2033) 。
## Zuul
[![Zuul](06-Spring Cloud 面试题.assets/c1a8294fcaf03a88818e4194bb348f0b.jpeg)](http://static.iocoder.cn/c1a8294fcaf03a88818e4194bb348f0b)Zuul
- 作用API 网关,路由,负载均衡等多种作用。
- 简介:类似 Nginx ,反向代理的功能,不过 Netflix 自己增加了一些配合其他组件的特性。
- 在微服务架构中,后端服务往往不直接开放给调用端,而是通过一个 API网关根据请求的 url 路由到相应的服务。当添加API网关后在第三方调用端和服务提供方之间就创建了一面墙这面墙直接与调用方通信进行权限控制后将请求均衡分发给后台服务端。
Zuul 原理,整体如下图:[![Zuul 原理](06-Spring Cloud 面试题.assets/20944a735d250d7d3338fe9deea179f8.jpeg)](http://static.iocoder.cn/20944a735d250d7d3338fe9deea179f8)Zuul 原理
关于 Zuul 的源码解析,可以看看艿艿整理的 [《Zuul 源码解析系列》](http://www.iocoder.cn/Zuul/good-collection/) 。
## Spring Cloud Gateway
关于 Spring Cloud Gateway 的源码解析,可以看看艿艿写的 [《Spring Cloud Gateway 源码解析系列》](http://www.iocoder.cn/categories/Spring-Cloud-Gateway/) 。
# 配置中心
在 Spring Cloud 中,能够使用的配置中心,如下:
- [`spring-cloud-config`](https://github.com/spring-cloud/spring-cloud-config) ,基于 Git、SVN 作为存储。
- [`spring-cloud-alibaba-nacos-config`](https://github.com/spring-cloud-incubator/spring-cloud-alibaba/tree/master/spring-cloud-alibaba-nacos-config) ,基于 Nacos 实现。
- [Apollo](https://github.com/ctripcorp/apollo) ,携程开源的配置中心。
> 艿艿:目前 Spring Cloud 最成熟的配置中心的选择。
## Spring Cloud Config
[![Spring Cloud Config](06-Spring Cloud 面试题.assets/d4610e439ae20ceb2d24020e9ff25c3a.jpeg)](http://static.iocoder.cn/d4610e439ae20ceb2d24020e9ff25c3a)Spring Cloud Config
- 作用:配置管理
- 简介Spring Cloud Config 提供服务器端和客户端。服务器存储后端的默认实现使用 Git ,因此它轻松支持标签版本的配置环境,以及可以访问用于管理内容的各种工具。
- 这个还是静态的,得配合 Spring Cloud Bus 实现动态的配置更新。
虽然 Spring Cloud Config 官方并未推出管理平台,我们可以考虑看看 [《为 Spring Cloud Config 插上管理的翅膀》](http://www.iocoder.cn/Spring-Cloud-Config/didi/spring-cloud-config-admin-1-0-0-release/) 。
## Apollo
关于 Apollo 的源码解析,可以看看艿艿写的 [《Spring Cloud Apollo 源码解析系列》](http://www.iocoder.cn/categories/Apollo/) 。
# 链路追踪
在 Spring Cloud 中,能够使用的链路追踪,主要是两个,如下:
- [`skywalking`](https://github.com/apache/incubator-skywalking) ,已经进入 Apache ,不仅仅能够透明的监控链路,还可以监控 JVM 等等。
- [`spring-cloud-sleuth`](https://github.com/spring-cloud/spring-cloud-sleuth) ,基于 Zipkin 实现。
## SkyWalking
关于 SkyWalking 的源码解析,可以看看艿艿写的 [《SkyWalking 源码解析系列》](http://www.iocoder.cn/categories/SkyWalking/) 。
## Spring Cloud Sleuth
Spring Cloud Sleuth 原理,整体如下图:[![Spring Cloud Sleuth 原理](06-Spring Cloud 面试题.assets/43f306a2aa7bc27015d3baa7832d13b6.jpeg)](http://static.iocoder.cn/43f306a2aa7bc27015d3baa7832d13b6)Spring Cloud Sleuth 原理
# TODO 消息队列
# 彩蛋
第一个版本的 Spring Cloud 面试题,主要把整个文章的大体结构定了,后续在慢慢完善补充~
如下是 Eureka + Ribbon + Feign + Hystrix + Zuul 整合后的图:[![Eureka + Ribbon + Feign + Hystrix + Zuul](06-Spring Cloud 面试题.assets/64e9a7827c76d38f899160da6f736ea2.jpeg)](http://static.iocoder.cn/64e9a7827c76d38f899160da6f736ea2)Eureka + Ribbon + Feign + Hystrix + Zuul
参考与推荐如下文章:
- [《微服务框架之 Spring Cloud 面试题汇总》](http://www.3da4.com/thread-4948-1-1.html)
- [《Spring Boot 和 Spring Cloud 面试题》](https://yiweifen.com/v-1-339414.html)
- [《Spring Cloud 简介与 5 大常用组件》](https://www.toutiao.com/a6642286207961137671/)
- [《面试必问的 Spring Cloud 实现原理图》](https://www.youdingyi.com/thread-2500-1-1.html)
- [《拜托!面试请不要再问我 Spring Cloud 底层原理》](https://juejin.im/post/5be13b83f265da6116393fc7)

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -0,0 +1,655 @@
# 精尽 MyBatis 面试题「最新更新时间2024-02」
以下面试题,基于网络整理,和自己编辑。具体参考的文章,会在文末给出所有的链接。
如果胖友有自己的疑问,欢迎在星球提问,我们一起整理吊吊的 MyBatis 面试题的大保健。
而题目的难度,艿艿尽量按照从容易到困难的顺序,逐步下去。
## MyBatis 编程步骤
1. 创建 SqlSessionFactory 对象。
2. 通过 SqlSessionFactory 获取 SqlSession 对象。
3. 通过 SqlSession 获得 Mapper 代理对象。
4. 通过 Mapper 代理对象,执行数据库操作。
5. 执行成功,则使用 SqlSession 提交事务。
6. 执行失败,则使用 SqlSession 回滚事务。
7. 最终,关闭会话。
## `#{}` 和 `${}` 的区别是什么?
`${}` 是 Properties 文件中的变量占位符,它可以用于 XML 标签属性值和 SQL 内部,属于**字符串替换**。例如将 `${driver}` 会被静态替换为 `com.mysql.jdbc.Driver`
```
<dataSource type="UNPOOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
</dataSource>
```
`${}` 也可以对传递进来的参数**原样拼接**在 SQL 中。代码如下:
```
<select id="getSubject3" parameterType="Integer" resultType="Subject">
SELECT * FROM subject
WHERE id = ${id}
</select>
```
- 实际场景下,不推荐这么做。因为,可能有 SQL 注入的风险。
------
`#{}` 是 SQL 的参数占位符Mybatis 会将 SQL 中的 `#{}` 替换为 `?` 号,在 SQL 执行前会使用 PreparedStatement 的参数设置方法,按序给 SQL 的 `?` 号占位符设置参数值,比如 `ps.setInt(0, parameterValue)` 。 所以,`#{}` 是**预编译处理**,可以有效防止 SQL 注入,提高系统安全性。
------
另外,`#{}``${}` 的取值方式非常方便。例如:`#{item.name}` 的取值方式,为使用反射从参数对象中,获取 `item` 对象的 `name` 属性值,相当于 `param.getItem().getName()`
## 当实体类中的属性名和表中的字段名不一样 ,怎么办?
第一种, 通过在查询的 SQL 语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。代码如下:
```
<select id="selectOrder" parameterType="Integer" resultType="Order">
SELECT order_id AS id, order_no AS orderno, order_price AS price
FROM orders
WHERE order_id = #{id}
</select>
```
- 这里,艿艿还有几点建议:
- 1、数据库的关键字统一使用大写例如`SELECT``AS``FROM``WHERE`
- 2、每 5 个查询字段换一行,保持整齐。
- 3、`,` 的后面,和 `=` 的前后,需要有空格,更加清晰。
- 4、`SELECT``FROM``WHERE` 等,单独一行,高端大气。
------
第二种,是第一种的特殊情况。大多数场景下,数据库字段名和实体类中的属性名差,主要是前者为**下划线风格**,后者为**驼峰风格**。在这种情况下,可以直接配置如下,实现自动的下划线转驼峰的功能。
```
<setting name="logImpl" value="LOG4J"/>
<setting name="mapUnderscoreToCamelCase" value="true" />
</settings>
```
😈 也就说,约定大于配置。非常推荐!
------
第三种,通过 `<resultMap>` 来映射字段名和实体类属性名的一一对应的关系。代码如下:
```
<resultMap type="me.gacl.domain.Order" id=”OrderResultMap”>
<!- 用 id 属性来映射主键字段 ->
<id property="id" column="order_id">
<!- 用 result 属性来映射非主键字段property 为实体类属性名column 为数据表中的属性 ->
<result property="orderNo" column ="order_no" />
<result property="price" column="order_price" />
</resultMap>
<select id="getOrder" parameterType="Integer" resultMap="OrderResultMap">
SELECT *
FROM orders
WHERE order_id = #{id}
</select>
```
- 此处 `SELECT *` 仅仅作为示例只用,实际场景下,千万千万千万不要这么干。用多少字段,查询多少字段。
- 相比第一种,第三种的**重用性**会一些。
## XML 映射文件中,除了常见的 select | insert | update | delete标 签之外,还有哪些标签?
如下部分,可见 [《MyBatis 文档 —— Mapper XML 文件》](http://www.mybatis.org/mybatis-3/zh/sqlmap-xml.html)
- ```
<cache />
```
标签,给定命名空间的缓存配置。
- `<cache-ref />` 标签,其他命名空间缓存配置的引用。
- `<resultMap />` 标签,是最复杂也是最强大的元素,用来描述如何从数据库结果集中来加载对象。
- ~~`` 标签,已废弃!老式风格的参数映射。内联参数是首选,这个元素可能在将来被移除,这里不会记录。~~
- ```
<sql />
```
标签,可被其他语句引用的可重用语句块。
- `<include />` 标签,引用 `<sql />` 标签的语句。
- `<selectKey />` 标签,不支持自增的主键生成策略标签。
如下部分,可见 [《MyBatis 文档 —— 动态 SQL》](http://www.mybatis.org/mybatis-3/zh/dynamic-sql.html)
- `<if />`
- `<choose />`、`<when />`、`<otherwise />`
- `<trim />`、`<where />`、`<set />`
- `<foreach />`
- `<bind />`
## Mybatis 动态 SQL 是做什么的?都有哪些动态 SQL ?能简述一下动态 SQL 的执行原理吗?
- Mybatis 动态 SQL ,可以让我们在 XML 映射文件内,以 XML 标签的形式编写动态 SQL ,完成逻辑判断和动态拼接 SQL 的功能。
- Mybatis 提供了 9 种动态 SQL 标签:`<if />`、`<choose />`、`<when />`、`<otherwise />`、`<trim />`、`<where />`、`<set />`、`<foreach />`、`<bind />` 。
- 其执行原理为,使用 **OGNL** 的表达式,从 SQL 参数对象中计算表达式的值,根据表达式的值动态拼接 SQL ,以此来完成动态 SQL 的功能。
如上的内容,更加详细的话,请看 [《MyBatis 文档 —— 动态 SQL》](http://www.mybatis.org/mybatis-3/zh/dynamic-sql.html) 文档。
## 最佳实践中,通常一个 XML 映射文件,都会写一个 Mapper 接口与之对应。请问,这个 Mapper 接口的工作原理是什么Mapper 接口里的方法,参数不同时,方法能重载吗?
Mapper 接口,对应的关系如下:
- 接口的全限名,就是映射文件中的 `"namespace"` 的值。
- 接口的方法名,就是映射文件中 MappedStatement 的 `"id"` 值。
- 接口方法内的参数,就是传递给 SQL 的参数。
Mapper 接口是没有实现类的,当调用接口方法时,接口全限名 + 方法名拼接字符串作为 key 值,可唯一定位一个对应的 MappedStatement 。举例:`com.mybatis3.mappers.StudentDao.findStudentById` ,可以唯一找到 `"namespace"` 为 `com.mybatis3.mappers.StudentDao` 下面 `"id"` 为 `findStudentById` 的 MappedStatement 。
总结来说,在 Mybatis 中,每一个 `<select />`、`<insert />`、`<update />`、`<delete />` 标签,都会被解析为一个 MappedStatement 对象。
另外Mapper 接口的实现类,通过 MyBatis 使用 **JDK Proxy** 自动生成其代理对象 Proxy ,而代理对象 Proxy 会拦截接口方法,从而“调用”对应的 MappedStatement 方法,最终执行 SQL ,返回执行结果。整体流程如下图:[![流程](07-MyBatis 面试题.assets/02.png)](http://static.iocoder.cn/images/MyBatis/2020_03_15/02.png)流程
- 其中SqlSession 在调用 Executor 之前,会获得对应的 MappedStatement 方法。例如:`DefaultSqlSession#select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler)` 方法,代码如下:
```
// DefaultSqlSession.java
@Override
public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
try {
// 获得 MappedStatement 对象
MappedStatement ms = configuration.getMappedStatement(statement);
// 执行查询
executor.query(ms, wrapCollection(parameter), rowBounds, handler);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
```
- 完整的流程,胖友可以慢慢撸下 MyBatis 的源码。
------
Mapper 接口里的方法,是不能重载的,因为是**全限名 + 方法名**的保存和寻找策略。😈 所以有时,想个 Mapper 接口里的方法名,还是蛮闹心的,嘿嘿。
## Mapper 接口绑定有几种实现方式,分别是怎么实现的?
接口绑定有三种实现方式:
第一种,通过 **XML Mapper** 里面写 SQL 来绑定。在这种情况下,要指定 XML 映射文件里面的 `"namespace"` 必须为接口的全路径名。
第二种,通过**注解**绑定,就是在接口的方法上面加上 `@Select`、`@Update`、`@Insert`、`@Delete` 注解,里面包含 SQL 语句来绑定。
第三种,是第二种的特例,也是通过**注解**绑定,在接口的方法上面加上 `@SelectProvider`、`@UpdateProvider`、`@InsertProvider`、`@DeleteProvider` 注解,通过 Java 代码,生成对应的动态 SQL 。
------
实际场景下,最最最推荐的是**第一种**方式。因为SQL 通过注解写在 Java 代码中,会非常杂乱。而写在 XML 中,更加有整体性,并且可以更加方便的使用 OGNL 表达式。
## Mybatis 的 XML Mapper文件中不同的 XML 映射文件id 是否可以重复?
不同的 XML Mapper 文件,如果配置了 `"namespace"` ,那么 id 可以重复;如果没有配置 `"namespace"` ,那么 id 不能重复。毕竟`"namespace"` 不是必须的,只是最佳实践而已。
原因就是,`namespace + id` 是作为 `Map<String, MappedStatement>` 的 key 使用的。如果没有 `"namespace"`,就剩下 id ,那么 id 重复会导致数据互相覆盖。如果有了 `"namespace"`,自然 id 就可以重复,`"namespace"`不同,`namespace + id` 自然也就不同。
## 如何获取自动生成的(主)键值?
不同的数据库,获取自动生成的(主)键值的方式是不同的。
MySQL 有两种方式,但是**自增主键**,代码如下:
```
// 方式一,使用 useGeneratedKeys + keyProperty 属性
<insert id="insert" parameterType="Person" useGeneratedKeys="true" keyProperty="id">
INSERT INTO person(name, pswd)
VALUE (#{name}, #{pswd})
</insert>
// 方式二,使用 `<selectKey />` 标签
<insert id="insert" parameterType="Person">
<selectKey keyProperty="id" resultType="long" order="AFTER">
SELECT LAST_INSERT_ID()
</selectKey>
INSERT INTO person(name, pswd)
VALUE (#{name}, #{pswd})
</insert>
```
- 其中,**方式一**较为常用。
------
Oracle 有两种方式,**序列**和**触发器**。因为艿艿自己不了解 Oracle ,所以问了银行的朋友,他们是使用**序列**。而基于**序列**,根据 `<selectKey />` 执行的时机,也有两种方式,代码如下:
```
// 这个是创建表的自增序列
CREATE SEQUENCE student_sequence
INCREMENT BY 1
NOMAXVALUE
NOCYCLE
CACHE 10;
// 方式一,使用 `<selectKey />` 标签 + BEFORE
<insert id="add" parameterType="Student">
  <selectKey keyProperty="student_id" resultType="int" order="BEFORE">
select student_sequence.nextval FROM dual
</selectKey>
INSERT INTO student(student_id, student_name, student_age)
VALUES (#{student_id},#{student_name},#{student_age})
</insert>
// 方式二,使用 `<selectKey />` 标签 + AFTER
<insert id="save" parameterType="com.threeti.to.ZoneTO" >
<selectKey resultType="java.lang.Long" keyProperty="id" order="AFTER" >
SELECT SEQ_ZONE.CURRVAL AS id FROM dual
</selectKey>
INSERT INTO TBL_ZONE (ID, NAME )
VALUES (SEQ_ZONE.NEXTVAL, #{name,jdbcType=VARCHAR})
</insert>
```
- 他们使用第一种方式,没有具体原因,可能就没什么讲究吧。嘿嘿。
至于为什么不用**触发器**呢?朋友描述如下:
> 朋友:触发器不行啊,我们这边原来也有触发器,一有数据更改就会有问题了呀
> 艿艿:数据更改指的是?
> 朋友:就改线上某几条数据
> 艿艿:噢噢。手动改是吧?
> 朋友:不行~
------
当然,数据库还有 SQLServer、PostgreSQL、DB2、H2 等等,具体的方式,胖友自己 Google 下噢。
关于如何获取自动生成的(主)键值的**原理**,可以看看 [《精尽 MyBatis 源码分析 —— SQL 执行(三)之 KeyGenerator》](http://svip.iocoder.cn/MyBatis/executor-3/) 。
## Mybatis 执行批量插入,能返回数据库主键列表吗?
JDBC 都能做Mybatis 当然也能做。
## 在 Mapper 中如何传递多个参数?
第一种,使用 Map 集合,装载多个参数进行传递。代码如下:
```
// 调用方法
Map<String, Object> map = new HashMap();
map.put("start", start);
map.put("end", end);
return studentMapper.selectStudents(map);
// Mapper 接口
List<Student> selectStudents(Map<String, Object> map);
// Mapper XML 代码
<select id="selectStudents" parameterType="Map" resultType="Student">
SELECT *
FROM students
LIMIT #{start}, #{end}
</select>
```
- 显然,这不是一种优雅的方式。
------
第二种,保持传递多个参数,使用 `@Param` 注解。代码如下:
```
// 调用方法
return studentMapper.selectStudents(0, 10);
// Mapper 接口
List<Student> selectStudents(@Param("start") Integer start, @Param("end") Integer end);
// Mapper XML 代码
<select id="selectStudents" resultType="Student">
SELECT *
FROM students
LIMIT #{start}, #{end}
</select>
```
- 推荐使用这种方式。
------
第三种,保持传递多个参数,不使用 `@Param` 注解。代码如下:
```
// 调用方法
return studentMapper.selectStudents(0, 10);
// Mapper 接口
List<Student> selectStudents(Integer start, Integer end);
// Mapper XML 代码
<select id="selectStudents" resultType="Student">
SELECT *
FROM students
LIMIT #{param1}, #{param2}
</select>
```
- 其中,按照参数在方法方法中的位置,从 1 开始,逐个为 `#{param1}`、`#{param2}`、`#{param3}` 不断向下。
## Mybatis 是否可以映射 Enum 枚举类?
Mybatis 可以映射枚举类,对应的实现类为 EnumTypeHandler 或 EnumOrdinalTypeHandler 。
- EnumTypeHandler ,基于 `Enum.name` 属性( String )。**默认**。
- EnumOrdinalTypeHandler ,基于 `Enum.ordinal` 属性( `int` )。可通过 `<setting name="defaultEnumTypeHandler" value="EnumOrdinalTypeHandler" />` 来设置。
😈 当然,实际开发场景,我们很少使用 Enum 类型,更加的方式是,代码如下:
```
public class Dog {
public static final int STATUS_GOOD = 1;
public static final int STATUS_BETTER = 2;
public static final int STATUS_BEST = 3
private int status;
}
```
------
并且不单可以映射枚举类Mybatis 可以映射任何对象到表的一列上。映射方式为自定义一个 TypeHandler 类,实现 TypeHandler 的`#setParameter(...)` 和 `#getResult(...)` 接口方法。
TypeHandler 有两个作用:
- 一是,完成从 javaType 至 jdbcType 的转换。
- 二是,完成 jdbcType 至 javaType 的转换。
具体体现为 `#setParameter(...)` 和 `#getResult(..)` 两个方法,分别代表设置 SQL 问号占位符参数和获取列查询结果。
关于 TypeHandler 的**原理**,可以看看 [《精尽 MyBatis 源码分析 —— 类型模块》](http://svip.iocoder.cn/MyBatis/type-package/) 。
## Mybatis 都有哪些 Executor 执行器?它们之间的区别是什么?
Mybatis 有四种 Executor 执行器,分别是 SimpleExecutor、ReuseExecutor、BatchExecutor、CachingExecutor 。
- SimpleExecutor :每执行一次 update 或 select 操作,就创建一个 Statement 对象,用完立刻关闭 Statement 对象。
- ReuseExecutor :执行 update 或 select 操作,以 SQL 作为key 查找**缓存**的 Statement 对象,存在就使用,不存在就创建;用完后,不关闭 Statement 对象,而是放置于缓存 `Map<String, Statement>` 内,供下一次使用。简言之,就是重复使用 Statement 对象。
- BatchExecutor :执行 update 操作(没有 select 操作,因为 JDBC 批处理不支持 select 操作),将所有 SQL 都添加到批处理中(通过 addBatch 方法),等待统一执行(使用 executeBatch 方法)。它缓存了多个 Statement 对象,每个 Statement 对象都是调用 addBatch 方法完毕后,等待一次执行 executeBatch 批处理。**实际上,整个过程与 JDBC 批处理是相同**。
- CachingExecutor :在上述的三个执行器之上,增加**二级缓存**的功能。
------
通过设置 `<setting name="defaultExecutorType" value="">` 的 `"value"` 属性,可传入 SIMPLE、REUSE、BATCH 三个值,分别使用 SimpleExecutor、ReuseExecutor、BatchExecutor 执行器。
通过设置 `<setting name="cacheEnabled" value=""` 的 `"value"` 属性为 `true` 时,创建 CachingExecutor 执行器。
------
这块的源码解析,可见 [《精尽 MyBatis 源码分析 —— SQL 执行(一)之 Executor》](http://svip.iocoder.cn/MyBatis/executor-1) 。
## MyBatis 如何执行批量插入?
首先,在 Mapper XML 编写一个简单的 Insert 语句。代码如下:
```
<insert id="insertUser" parameterType="String">
INSERT INTO users(name)
VALUES (#{value})
</insert>
```
然后,然后在对应的 Mapper 接口中,编写映射的方法。代码如下:
```
public interface UserMapper {
void insertUser(@Param("name") String name);
}
```
最后,调用该 Mapper 接口方法。代码如下:
```
private static SqlSessionFactory sqlSessionFactory;
@Test
public void testBatch() {
// 创建要插入的用户的名字的数组
List<String> names = new ArrayList<>();
names.add("占小狼");
names.add("朱小厮");
names.add("徐妈");
names.add("飞哥");
// 获得执行器类型为 Batch 的 SqlSession 对象,并且 autoCommit = false ,禁止事务自动提交
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH, false)) {
// 获得 Mapper 对象
UserMapper mapper = session.getMapper(UserMapper.class);
// 循环插入
for (String name : names) {
mapper.insertUser(name);
}
// 提交批量操作
session.commit();
}
}
```
代码比较简单,胖友仔细看看。当然,还有另一种方式,代码如下:
```
INSERT INTO [表名]([列名],[列名])
VALUES
([列值],[列值])),
([列值],[列值])),
([列值],[列值]));
```
- 对于这种方式,需要保证单条 SQL 不超过语句的最大限制 `max_allowed_packet` 大小,默认为 1 M 。
这两种方式的性能对比,可以看看 [《[实验\]mybatis批量插入方式的比较》](https://www.jianshu.com/p/cce617be9f9e) 。
## 介绍 MyBatis 的一级缓存和二级缓存的概念和实现原理?
内容有些长,直接参见 [《聊聊 MyBatis 缓存机制》](https://tech.meituan.com/mybatis_cache.html) 一文。
------
这块的源码解析,可见 [《精尽 MyBatis 源码分析 —— 缓存模块》](http://svip.iocoder.cn/MyBatis/cache-package) 。
## Mybatis 是否支持延迟加载?如果支持,它的实现原理是什么?
Mybatis 仅支持 association 关联对象和 collection 关联集合对象的延迟加载。其中association 指的就是**一对一**collection 指的就是**一对多查询**。
在 Mybatis 配置文件中,可以配置 `<setting name="lazyLoadingEnabled" value="true" />` 来启用延迟加载的功能。默认情况下,延迟加载的功能是**关闭**的。
------
它的原理是,使用 CGLIB 或 Javassist( 默认 ) 创建目标对象的代理对象。当调用代理对象的延迟加载属性的 getting 方法时,进入拦截器方法。比如调用 `a.getB().getName()` 方法,进入拦截器的 `invoke(...)` 方法,发现 `a.getB()` 需要延迟加载时,那么就会单独发送事先保存好的查询关联 B 对象的 SQL ,把 B 查询上来,然后调用`a.setB(b)` 方法,于是 `a` 对象 `b` 属性就有值了,接着完成`a.getB().getName()` 方法的调用。这就是延迟加载的基本原理。
当然了,不光是 Mybatis几乎所有的包括 Hibernate 在内,支持延迟加载的原理都是一样的。
------
这块的源码解析,可见 [《 精尽 MyBatis 源码分析 —— SQL 执行(五)之延迟加载》](http://svip.iocoder.cn/MyBatis/executor-5) 文章。
## Mybatis 能否执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别。
> 艿艿:这道题有点难度。理解倒是好理解,主要那块源码的实现,艿艿看的有点懵逼。大体的意思是懂的,但是一些细节没扣完。
Mybatis 不仅可以执行一对一、一对多的关联查询,还可以执行多对一,多对多的关联查询。
> 艿艿:不过貌似,我自己实际开发中,还是比较喜欢自己去查询和拼接映射的数据。😈
- 多对一查询,其实就是一对一查询,只需要把 `selectOne(...)` 修改为 `selectList(...)` 即可。案例可见 [《MyBatis多对一表关系详解》](https://blog.csdn.net/xzm_rainbow/article/details/15336959) 。
- 多对多查询,其实就是一对多查询,只需要把 `#selectOne(...)` 修改为 `selectList(...)` 即可。案例可见 [《【MyBatis学习10】高级映射之多对多查询》](https://blog.csdn.net/eson_15/article/details/51655188) 。
------
关联对象查询,有两种实现方式:
> 艿艿:所有的技术方案,即会有好处,又会有坏处。很难出现,一个完美的银弹方案。
- 一种是单独发送一个 SQL 去查询关联对象,赋给主对象,然后返回主对象。好处是多条 SQL 分开,相对简单,坏处是发起的 SQL 可能会比较多。
- 另一种是使用嵌套查询,嵌套查询的含义为使用 `join` 查询,一部分列是 A 对象的属性值,另外一部分列是关联对象 B 的属性值。好处是只发一个 SQL 查询,就可以把主对象和其关联对象查出来,坏处是 SQL 可能比较复杂。
那么问题来了,`join` 查询出来 100 条记录,如何确定主对象是 5 个,而不是 100 个呢?其去重复的原理是 `<resultMap>` 标签内的`<id>` 子标签,指定了唯一确定一条记录的 `id` 列。Mybatis 会根据`<id>` 列值来完成 100 条记录的去重复功能,`<id>` 可以有多个,代表了联合主键的语意。
同样主对象的关联对象,也是根据这个原理去重复的。尽管一般情况下,只有主对象会有重复记录,关联对象一般不会重复。例如:下面 `join` 查询出来6条记录一、二列是 Teacher 对象列,第三列为 Student 对象列。Mybatis 去重复处理后,结果为 1 个老师和 6 个学生,而不是 6 个老师和 6 个学生。
| t_id | t_name | s_id |
| :--- | :------ | :--- |
| 1 | teacher | 38 |
| 1 | teacher | 39 |
| 1 | teacher | 40 |
| 1 | teacher | 41 |
| 1 | teacher | 42 |
| 1 | teacher | 43 |
## 简述 Mybatis 的插件运行原理?以及如何编写一个插件?
Mybatis 仅可以编写针对 ParameterHandler、ResultSetHandler、StatementHandler、Executor 这 4 种接口的插件。
Mybatis 使用 JDK 的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这 4 种接口对象的方法时,就会进入拦截方法,具体就是 InvocationHandler 的 `#invoke(...)`方法。当然,只会拦截那些你指定需要拦截的方法。
------
编写一个 MyBatis 插件的步骤如下:
1. 首先,实现 Mybatis 的 Interceptor 接口,并实现 `#intercept(...)` 方法。
2. 然后,在给插件编写注解,指定要拦截哪一个接口的哪些方法即可
3. 最后,在配置文件中配置你编写的插件。
具体的,可以参考 [《MyBatis 官方文档 —— 插件》](http://www.mybatis.org/mybatis-3/zh/configuration.html#plugins) 。
------
插件的详细解析,可以看看 [《精尽 MyBatis 源码分析 —— 插件体系(一)之原理》](http://svip.iocoder.cn/MyBatis/plugin-1) 。
## Mybatis 是如何进行分页的?分页插件的原理是什么?
Mybatis 使用 RowBounds 对象进行分页,它是针对 ResultSet 结果集执行的**内存分页**,而非**数据库分页**。
所以,实际场景下,不适合直接使用 MyBatis 原有的 RowBounds 对象进行分页。而是使用如下两种方案:
- 在 SQL 内直接书写带有数据库分页的参数来完成数据库分页功能
- 也可以使用分页插件来完成数据库分页。
这两者都是基于数据库分页,差别在于前者是工程师**手动**编写分页条件,后者是插件**自动**添加分页条件。
------
分页插件的基本原理是使用 Mybatis 提供的插件接口,实现自定义分页插件。在插件的拦截方法内,拦截待执行的 SQL ,然后重写 SQL 根据dialect 方言,添加对应的物理分页语句和物理分页参数。
举例:`SELECT * FROM student` ,拦截 SQL 后重写为:`select * FROM student LIMI 010` 。
目前市面上目前使用比较广泛的 MyBatis 分页插件有:
- [Mybatis-PageHelper](https://github.com/pagehelper/Mybatis-PageHelper)
- [MyBatis-Plus](https://github.com/baomidou/mybatis-plus)
从现在看来,[MyBatis-Plus](https://github.com/baomidou/mybatis-plus) 逐步使用的更加广泛。
关于 MyBatis 分页插件的原理深入,可以看看 [《精尽 MyBatis 源码分析 —— 插件体系(二)之 PageHelper》](http://svip.iocoder.cn/MyBatis/plugin-2) 。
## MyBatis 与 Hibernate 有哪些不同?
Mybatis 和 Hibernate 不同,它**不完全是**一个 ORM 框架因为MyBatis 需要程序员自己编写 SQL 语句。不过 MyBatis 可以通过 XML 或注解方式灵活配置要运行的 SQL 语句,并将 Java 对象和 SQL 语句映射生成最终执行的 SQL ,最后将 SQL 执行的结果再映射生成 Java 对象。
Mybatis 学习门槛低,简单易学,程序员直接编写原生态 SQL ,可严格控制 SQL 执行性能,灵活度高。但是灵活的前提是 MyBatis 无法做到数据库无关性,如果需要实现支持多种数据库的软件则需要自定义多套 SQL 映射文件,工作量大。
Hibernate 对象/关系映射能力强,数据库无关性好。如果用 Hibernate 开发可以节省很多代码,提高效率。但是 Hibernate 的缺点是学习门槛高,要精通门槛更高,而且怎么设计 O/R 映射,在性能和对象模型之间如何权衡,以及怎样用好 Hibernate 需要具有很强的经验和能力才行。
总之,按照用户的需求在有限的资源环境下只要能做出维护性、扩展性良好的软件架构都是好架构,所以框架只有适合才是最好。简单总结如下:
- Hibernate 属于全自动 ORM 映射工具,使用 Hibernate 查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取。
- Mybatis 属于半自动 ORM 映射工具,在查询关联对象或关联集合对象时,需要手动编写 SQL 来完成。
另外,在 [《浅析 Mybatis 与 Hibernate 的区别与用途》](https://www.jianshu.com/p/96171e647885) 文章,也是写的非常不错的。
当然实际上MyBatis 也可以搭配自动生成代码的工具,提升开发效率,还可以使用 [MyBatis-Plus](http://mp.baomidou.com/) 框架,已经内置常用的 SQL 操作,也是非常不错的。
## JDBC 编程有哪些不足之处MyBatis是如何解决这些问题的
问题一SQL 语句写在代码中造成代码不易维护,且代码会比较混乱。
解决方式:将 SQL 语句配置在 Mapper XML 文件中,与 Java 代码分离。
------
问题二:根据参数不同,拼接不同的 SQL 语句非常麻烦。例如 SQL 语句的 WHERE 条件不一定,可能多也可能少,占位符需要和参数一一对应。
解决方式MyBatis 提供 `<where />`、`<if />` 等等动态语句所需要的标签,并支持 OGNL 表达式,简化了动态 SQL 拼接的代码,提升了开发效率。
------
问题三对结果集解析麻烦SQL 变化可能导致解析代码变化,且解析前需要遍历。
解决方式Mybatis 自动将 SQL 执行结果映射成 Java 对象。
------
问题四,数据库链接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库链接池可解决此问题。
解决方式:在 `mybatis-config.xml` 中,配置数据链接池,使用连接池管理数据库链接。
😈 当然,即使不使用 MyBatis ,也可以使用数据库连接池。
另外MyBatis 默认提供了数据库连接池的实现,只是说,因为其它开源的数据库连接池性能更好,所以一般很少使用 MyBatis 自带的连接池实现。
## Mybatis 比 IBatis 比较大的几个改进是什么?
> 这是一个选择性了解的问题,因为可能现在很多面试官,都没用过 IBatis 框架。
1. 有接口绑定,包括注解绑定 SQL 和 XML 绑定 SQL 。
2. 动态 SQL 由原来的节点配置变成 OGNL 表达式。
3. 在一对一或一对多的时候,引进了 `association` ,在一对多的时候,引入了 `collection`节点,不过都是在 `<resultMap />` 里面配置。
## Mybatis 映射文件中,如果 A 标签通过 include 引用了B标签的内容请问B 标签能否定义在 A 标签的后面还是说必须定义在A标签的前面
> 老艿艿:这道题目,已经和源码实现,有点关系了。
虽然 Mybatis 解析 XML 映射文件是**按照顺序**解析的。但是,被引用的 B 标签依然可以定义在任何地方Mybatis 都可以正确识别。**也就是说,无需按照顺序,进行定义**。
原理是Mybatis 解析 A 标签,发现 A 标签引用了 B 标签,但是 B 标签尚未解析到尚不存在此时Mybatis 会将 A 标签标记为**未解析状态**。然后,继续解析余下的标签,包含 B 标签待所有标签解析完毕Mybatis 会重新解析那些被标记为未解析的标签此时再解析A标签时B 标签已经存在A 标签也就可以正常解析完成了。
可能有一些绕,胖友可以看看 [《精尽 MyBatis 源码解析 —— MyBatis 初始化(一)之加载 mybatis-config》](http://svip.iocoder.cn/MyBatis/builder-package-1) 。
此处我们在引申一个问题Spring IOC 中,存在互相依赖的 Bean 对象,该如何解决呢?答案见 [《【死磕 Spring】—— IoC 之加载 Bean创建 Bean之循环依赖处理》](http://svip.iocoder.cn/Spring/IoC-get-Bean-createBean-5/) 。
## 简述 Mybatis 的 XML 映射文件和 Mybatis 内部数据结构之间的映射关系?
> 老艿艿:这道题目,已经和源码实现,有点关系了。
Mybatis 将所有 XML 配置信息都封装到 All-In-One 重量级对象Configuration内部。
在 XML Mapper 文件中:
- `<parameterMap>` 标签,会被解析为 ParameterMap 对象,其每个子元素会被解析为 ParameterMapping 对象。
- `<resultMap>` 标签,会被解析为 ResultMap 对象,其每个子元素会被解析为 ResultMapping 对象。
- 每一个 `<select>`、`<insert>`、`<update>`、`<delete>` 标签,均会被解析为一个 MappedStatement 对象,标签内的 SQL 会被解析为一个 BoundSql 对象。
## 666. 彩蛋
参考与推荐如下文章:
- 祖大俊 [《Mybatis3.4.x技术内幕二十三Mybatis面试问题集锦大结局](https://my.oschina.net/zudajun/blog/747682)
- Java3y [《Mybatis 常见面试题》](https://segmentfault.com/a/1190000013678579)
- Homiss [《MyBatis 面试题》](https://github.com/Homiss/Java-interview-questions/blob/master/框架/MyBatis面试题.md)

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -0,0 +1,484 @@
# 精尽【消息队列 】面试题「最新更新时间2024-02」
以下面试题,基于网络整理,和自己编辑。具体参考的文章,会在文末给出所有的链接。
如果胖友有自己的疑问,欢迎在星球提问,我们一起整理吊吊的【消息队列】面试题的大保健。
而题目的难度,艿艿尽量按照从容易到困难的顺序,逐步下去。
另外,本文只分享通用的【消息队列】的面试题,关于 RocketMQ、Kafka、RabbitMQ 会单独分享。
## 什么是消息队列?
消息队列,是分布式系统中重要的组件。
- 主要解决应用耦合,异步消息,流量削锋等问题。
- 可实现高性能,高可用,可伸缩和最终一致性架构,是大型分布式系统不可缺少的中间件。
目前主流的消息队列有
- Kafka
- RabbitMQ
- RocketMQ ,老版本是 MetaQ 。
- ActiveMQ ,目前用的人越来越少了。
另外,消息队列容易和 Java 中的本地 MessageQueue 搞混,所以消息队列更多被称为消息中间件、分布式消息队列等等。
## 消息队列由哪些角色组成?
如下图所示:
[![MQ 角色](08-精尽【消息队列 】面试题.assets/01.png)](http://static.iocoder.cn/images/MQ/2019_11_12/01.png)MQ 角色
- 生产者Producer负责产生消息。
- 消费者Consumer负责消费消息
- 消息代理Message Broker负责存储消息和转发消息两件事情。其中转发消息分为推送和拉取两种方式。
- 拉取Pull是指 Consumer 主动从 Message Broker 获取消息
- 推送Push是指 Message Broker 主动将 Consumer 感兴趣的消息推送给 Consumer 。
## 消息队列有哪些使用场景?
一般来说,有四大类使用场景:
- 应用解耦
- 异步处理
- 流量削峰
- 消息通讯
- 日志处理
**其中,应用解耦、异步处理是比较核心的**
> 艿艿:这个问题,也适合回答《为什么使用消息队列?》,当然需要扩充下,下面我们来看看。
## 为什么使用消息队列进行应用解耦?
传统模式下,如下图所示:[![传统模式](08-精尽【消息队列 】面试题.assets/03.png)](http://static.iocoder.cn/images/MQ/2019_11_12/03.png)传统模式
- 缺点比较明显,系统间耦合性太强。系统 A 在代码中直接调用系统 B 和系统 C 的代码,如果将来 D 系统接入,系统 A 还需要修改代码,过于麻烦!并且,万一系统 A、B、C 万一还改接口,还要持续跟进。
引入消息队列后,如下图所示:[![新模式](08-精尽【消息队列 】面试题.assets/04.png)](http://static.iocoder.cn/images/MQ/2019_11_12/04.png)新模式
- 将消息写入消息队列,需要消息的系统自己从消息队列中订阅,从而系统 A 不需要做任何修改。
所以,有了消息队列之后,从主动调用的方式,变成了消息的订阅发布( 或者说,事件的发布和监听 ),从而解耦。
举个实际场景的例子,用户支付订单完成后,系统需要给用户发红包、增加积分等等行为,就可以通过这样的方式进行解耦。
## 为什么使用消息队列进行异步处理?
> 艿艿:这个应该对于大多数开发者,这是最最最核心的用途了!!!
传统模式下,如下图所示:[![传统模式](08-精尽【消息队列 】面试题.assets/05.png)](http://static.iocoder.cn/images/MQ/2019_11_12/05.png)传统模式
- A 系统需要
串行
逐个
同步
调用系统 B、C、D 。这其中会有很多问题:
- 如果每个系统调用执行是 200ms ,那么这个逻辑就要执行 600ms ,非常慢。
- 如果任一一个系统调用异常报错,那么整个逻辑就报错了。
- 如果任一一个系统调用超时,那么整个逻辑就超时了。
-
引入消息队列后,如下图所示:[![新模式](08-精尽【消息队列 】面试题.assets/06.png)](http://static.iocoder.cn/images/MQ/2019_11_12/06.png)新模式
- 通过发送 3 条 MQ 消息,通过 Consumer 消费,从而
异步
并行
调用系统 B、C、D 。
- 因为发送 MQ 消息是比较快的,假设每个操作 2 ms ,那么这个逻辑只要执行 6 ms ,非常快。
- 当然,有胖友会有,可能发送 MQ 消息会失败。当然这个是会存在的此时可以异步重试。当然可能异步重试的过程中JVM 进程挂了,此时又需要其他的机制来保证。不过,相比**串行**逐个**同步**调用系统 B、C、D 来说,出错的几率会低很多很多。
另外,使用消息队列进行异步处理,会有一个前提,返回的结果不依赖于处理的结果。
## 为什么使用消息队列进行流量消峰?
传统模式下,如下图所示:[![传统模式](08-精尽【消息队列 】面试题.assets/07.png)](http://static.iocoder.cn/images/MQ/2019_11_12/07.png)传统模式
- 对于大多数系统,一定会有访问量的波峰和波谷。比较明显的,就是我们经常使用的美团外卖,又或者被人诟病的小米秒杀。
- 如果在并发量大的时间,所有的请求直接打到数据库,造成数据库直接挂掉。
引入消息队列后,如下图所示:[![新模式](08-精尽【消息队列 】面试题.assets/08.png)](http://static.iocoder.cn/images/MQ/2019_11_12/08.png)新模式
- 通过将请求先转发到消息队列中。然后,系统 A 慢慢的按照数据库能处理的并发量,从消息队列中逐步拉取消息进行消费。在**生产中,这个短暂的高峰期积压是允许的**,😈 相比把数据库打挂来说。
- 当然,可能有胖友说,访问量这么大,不会把消息队列给打挂么?相比来说,消息队列的性能会比数据库性能更好,并且,横向的扩展能力更强。
## 为什么使用消息队列进行消息通信?
消息通讯是指,消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯。比如实现:
- IM 聊天。
- 点对点消息队列。可能大家会比较懵逼,有基于消息队列的 RPC 框架实现,例如 [rabbitmq-jsonrpc](https://github.com/rabbitmq/rabbitmq-jsonrpc) ,虽然现在用的人比较少。
- 面向物联网的 MQTT 。阿里在开源的 RocketMQ 基础上,增加了 MQTT 协议的支持,可见 [消息队列 for IoT](https://cn.aliyun.com/product/ons) 。
- ….
## 如何使用消息队列进行日志处理?
日志处理,是指将消息队列用在日志处理中,比如 Kafka 的应用,解决大量**日志传输**的问题。
[![日志传输](08-精尽【消息队列 】面试题.assets/09.png)](http://static.iocoder.cn/images/MQ/2019_11_12/09.png)日志传输
- 日志采集客户端,负责日志数据采集,定时批量写入 Kafka 队列。
- Kafka 消息队列,负责日志数据的接收,存储和转发。
- 日志处理应用:订阅并消费 Kafka 队列中的日志数据。
大家最熟悉的就是 [ELK + Kafka 日志方案](http://www.demodashi.com/demo/10181.html),如下:
> 详细的,胖友可以点击链接,查看文章。
- Kafka :接收用户日志的消息队列。
- Logstash :对接 Kafka 写入的日志,做日志解析,统一成 JSON 输出给 Elasticsearch 中。
- Elasticsearch :实时日志分析服务的核心技术,一个 schemaless ,实时的数据存储服务,通过 index 组织数据,兼具强大的搜索和统计功能。
- Kibana :基于 Elasticsearch 的数据可视化组件,超强的数据可视化能力是众多公司选择 ELK stack 的重要原因。
## 消息队列有什么优缺点?
任何中间件的引入,带来优点的时候,也同时会带来缺点。
优点,在上述的 [「消息队列有哪些使用场景?」](https://svip.iocoder.cn/MQ/Interview/#) 问题中,我们已经看到了。
缺点,主要是如下三点:
- 系统可用性降低。
系统引入的外部依赖越多,越容易挂掉。本来你就是 A 系统调用 BCD 三个系统的接口就好了,本来 ABCD 四个系统好好的,没啥问题,你偏加个 MQ 进来,万一 MQ 挂了咋整MQ 一挂,整套系统崩溃的,你不就完了?**所以,消息队列一定要做好高可用**。
- 系统复杂度提高。
主要需要多考虑1消息怎么不重复消息。2消息怎么保证不丢失。3需要消息顺序的业务场景怎么处理。
- 一致性问题。
A 系统处理完了直接返回成功了,人都以为你这个请求就成功了。但是问题是,要是 B、C。D 三个系统那里B、D 两个系统写库成功了,结果 C 系统写库失败了,咋整?你这数据就不一致了。
当然,这不仅仅是 MQ 的问题,引入 RPC 之后本身就存在这样的问题。**如果我们在使用 MQ 时,一定要达到数据的最终一致性**。即C 系统最终执行完成。
## 消息队列有几种消费语义?
一共有 3 种,分别如下:
1. 消息至多被消费一次At most once消息可能会丢失但绝不重传。
2. 消息至少被消费一次At least once消息可以重传但绝不丢失。
3. 消息仅被消费一次Exactly once每一条消息只被传递一次。
为了支持上面 3 种消费语义,可以分 3 个阶段考虑消息队列系统中Producer、Message Broker、Consumer 需要满足的条件。
> 下面的内容,可能比较绕,胖友耐心理解。
🦅 **1. 消息至多被消费一次**
该语义是最容易满足的,特点是整个消息队列吞吐量大,实现简单。适合能容忍丢消息,~~消息重复消费的任务~~(和厮大沟通了下,这句话应该是错的,所以去掉)。
> 和晓峰又讨论了下,“~~消息重复消费的任务~~”的意思是,因为不会重复投递,所以间接解决了消息重复消费的问题。
- Producer 发送消息到 Message Broker 阶段
- Producer 发消息给Message Broker 时,不要求 Message Broker 对接收到的消息响应确认Producer 也不用关心 Message Broker 是否收到消息了。
- Message Broker 存储/转发阶段
- 对 Message Broker 的存储不要求持久性。
- 转发消息时,也不用关心 Consumer 是否真的收到了。
- Consumer 消费阶段
- Consumer 从 Message Broker 中获取到消息后,可以从 Message Broker 删除消息。
- 或 Message Broker 在消息被 Consumer 拿去消费时删除消息,不用关心 Consumer 最后对消息的消费情况如何。
🦅 **2. 消息至少被消费一次**
适合不能容忍丢消息,允许重复消费的任务。
- Producer 发送消息到 Message Broker 阶段
- Producer 发消息给 Message Broker Message Broker 必须响应对消息的确认。
- Message Broker 存储/转发阶段
- Message Broker 必须提供持久性保障。
- 转发消息时Message Broker 需要 Consumer 通知删除消息,才能将消息删除。
- Consumer消费阶段
- Consumer 从 Message Broker 中获取到消息必须在消费完成后Message Broker上的消息才能被删除。
🦅 **3. 消息仅被消费一次**
适合对消息消费情况要求非常高的任务,实现较为复杂。
在这里需要考虑一个问题,就是这里的“仅被消费一次”指的是如下哪种场景:
- Message Broker 上存储的消息被 Consumer 仅消费一次。
- Producer 上产生的消息被 Consumer 仅消费一次。
① Message Broker 上存储的消息被 Consumer 仅消费一次
- Producer 发送消息到 Message Broker 阶段
- Producer 发消息给 Message Broker 时,不要求 Message Broker 对接收到的消息响应确认Producer 也不用关心Message Broker 是否收到消息了。
- Message Broker 存储/转发阶段
- Message Broker 必须提供持久性保障
- 并且,每条消息在其消费队列里有唯一标识(这个唯一标识可以由 Producer 产生,也可以由 Message Broker 产生)。
- Consumer 消费阶段
- Consumer 从 Message Broker中获取到消息后需要记录下消费的消息标识以便在后续消费中防止对某个消息重复消费比如 Consumer 获取到消息,消费完后,还没来得及从 Message Broker 删除消息,就挂了,这样 Message Broker 如果把消息重新加入待消费队列的话,那么这条消息就会被重复消费了)。
② Producer 上产生的消息被 Consumer 仅消费一次
- Producer 发送消息到 Message Broker 阶段
- Producer 发消息给 Message Broker 时Message Broker 必须响应对消息的确认,并且 Producer 负责为该消息产生唯一标识,以防止 Consumer 重复消费(因为 Producer 发消息给Message Broker 后,由于网络问题没收到 Message Broker 的响应,可能会重发消息给到 Message Broker )。
- Message Broker 存储/转发阶段
- Message Broker 必须提供持久性保障
- 并且每条消息在其消费队列里有唯一标识这个唯一标识需要由Producer产生
- Consumer 消费阶段
- 和【① Message Broker 上存储的消息被 Consumer 仅消费一次】相同。
------
虽然 3 种方式看起来比较复杂,但是我们会发现,是层层递进,越来越可靠。
实际生产场景下,我们是倾向第 3 种的 ② 的情况,每条消息从 Producer 保证被送达,并且被 Consumer 仅消费一次。当然,重心还是如何保证 **Consumer 仅消费一次**,虽然说,消息产生的唯一标志可以在框架层级去做排重,但是最稳妥的,还是业务层也保证消费的幂等性。
## 消息队列有几种投递方式?分别有什么优缺点
在 [「消息队列由哪些角色组成?」](https://svip.iocoder.cn/MQ/Interview/#) 中,我们已经提到消息队列有 **push 推送**和 **pull 拉取**两种投递方式。
一种模型的某些场景下的优点,在另一些场景就可能是缺点。无论是 push 还是 pull ,都存在各种的利弊。
- push
- 优点,就是及时性。
- 缺点就是受限于消费者的消费能力可能造成消息的堆积Broker 会不断给消费者发送不能处理的消息。
- pull
- 优点,就是主动权掌握在消费方,可以根据自己的消息速度进行消息拉取。
- 缺点,就是消费方不知道什么时候可以获取的最新的消息,会有消息延迟和忙等。
目前的消息队列,基于 push + pull 模式结合的方式Broker 仅仅告诉 Consumer 有新的消息,具体的消息拉取,还是 Consumer 自己主动拉取。
> 艿艿:其实这个问题,会告诉我们两个道理。
>
> 1. 一个功能的实现,有多种实现方式,有优点就有缺点。并且,一个实现的缺点,恰好是另外一个实现的优点。
> 2. 一个功能的实现,可能是多种实现方式的结合,取一个平衡点,不那么优,也不那么缺。😈 再说一句题外话,是和否之间,还有灰色地方。
## 如何保证消费者的消费消息的幂等性?
🦅 **分析原因**
在 [「消息队列有几种消费语义?」](https://svip.iocoder.cn/MQ/Interview/#) 中,我们已经看了三种消费语义。如果要达到消费者的消费消息的幂等性,就需要**消息仅被消费一次**,且**每条消息从 Producer 保证被送达,并且被 Consumer 仅消费一次**。
那么,我们就基于这个场景,来思考下,为什么会出现消息重复的问题?
- 对于 Producer 来说
- 可能因为网络问题Producer 重试多次发送消息,实际第一次就发送成功,那么就会产生多条相同的消息。
- ….
- 对于 Consumer 来说
- 可能因为 Broker 的消息进度丢失,导致消息重复投递给 Consumer 。
- Consumer 消费成功,但是因为 JVM 异常崩溃,导致消息的消费进度未及时同步给 Consumer 。
> 对于大多数消息队列,考虑到性能,消费进度是异步定时同步给 Broker 。
-
🦅 **如何解决**
所以,上述的种种情况,都可能导致消费者会获取到重复的消息,那么我们的思考就无法是解决不发送、投递重复的消息,而是消费者在消费时,如何保证幂等性。
消费者实现幂等性,有两种方式:
1. 框架层统一封装。
2. 业务层自己实现。
**框架层统一封装**
首先,需要有一个消息排重的唯一标识,该编号只能由 Producer 生成,例如说使用 uuid、或者其它唯一编号的算法 。
然后,就需要有一个排重的存储器,例如说:
- 使用关系数据库,增加一个排重表,使用消息编号作为唯一主键。
- 使用 KV 数据库KEY 存储消息编号VALUE 任一。*此处,暂时不考虑 KV 数据库持久化的问题*
那么,我们要什么时候插入这条排重记录呢?
- 在消息消费执行业务逻辑**之前**,插入这条排重记录。但是,此时会有可能 JVM 异常崩溃。那么 JVM 重启后,这条消息就无法被消费了。因为,已经存在这条排重记录。
- 在消息消费执行业务逻辑
之后
,插入这条排重记录。
- 如果业务逻辑执行失败,显然,我们不能插入这条排重记录,因为我们后续要消费重试。
- 如果业务逻辑执行成功,此时,我们可以插入这条排重记录。但是,万一插入这条排重记录失败呢?**那么,需要让插入记录和业务逻辑在同一个事务当中,此时,我们只能使用数据库**。
😈 感觉好复杂,嘿嘿。
**业务层自己实现**
方式很多,这个和 HTTP 请求实现幂等是一样的逻辑:
- 先查询数据库,判断数据是否已经被更新过。如果是,则直接返回消费完成,否则执行消费。
- 更新数据库时,带上数据的状态。如果更新失败,则直接返回消费完成,否则执行消费。
-
如果胖友的系统的并发量非常大,可以使用 Zookeeper 或者 Redis 实现分布式锁,避免并发带来的问题。当然,引入一个组件,也会带来另外的复杂性:
1. 系统的并发能力下降。
2. Zookeeper 和 Redis 在获取分布式锁时,发现它们已经挂掉,此时到底要不要继续执行下去呢?嘿嘿。
**选择**
正常情况下,出现重复消息的概率其实很小,如果由框架层统一封装来实现的话,肯定会对消息系统的吞吐量和高可用有影响,所以最好还是由业务层自己实现处理消息重复的问题。
当然,这两种方式不是冲突的。可以提供不同类型的消息,根据配置,使用哪种方式。例如说:
- 默认情况下,开启【框架层统一封装】的功能。
- 可以通过配置,关闭【框架层统一封装】的功能。
当然,如果可能的话,尽可能业务层自己实现。/(ㄒoㄒ)/~~但是,实际上,很多时候,开发者不太会注意,哈哈哈哈。
## 如何保证生产者的发送消息的可靠性?
不同的消息队列,其架构不同,所以实现发送消息的可靠性的方案不同。所以参见如下文章:
- RocketMQ [《精尽 RocketMQ 面试题》](http://svip.iocoder.cn/RocketMQ/Interview/) 的 [「RocketMQ 是否会弄丢数据?」](https://svip.iocoder.cn/MQ/Interview/#) 的面试题。
- RabbitMQ [《精尽 RabbitMQ 面试题》](http://svip.iocoder.cn/RabbitMQ/Interview/) 的 [「RabbitMQ 是否会弄丢数据?」](https://svip.iocoder.cn/MQ/Interview/#) 的面试题。
- Kafka [《精尽 Kafka 面试题》](http://svip.iocoder.cn/Kafka/Interview/) 的 [「Kafka 是否会弄丢数据?」](https://svip.iocoder.cn/MQ/Interview/#) 的面试题。
## 如何保证消息的顺序性?
不同的消息队列,其架构不同,所以实现消息的顺序性的方案不同。所以参见如下文章:
- RocketMQ [《精尽 RocketMQ 面试题》](http://svip.iocoder.cn/RocketMQ/Interview/) 的 [「什么是顺序消息?如何实现?」](https://svip.iocoder.cn/MQ/Interview/#) 的面试题。
- RabbitMQ [《精尽 RabbitMQ 面试题》](http://svip.iocoder.cn/RabbitMQ/Interview/) 的 [「RabbitMQ 如何保证消息的顺序性?」](https://svip.iocoder.cn/MQ/Interview/#) 面试题。
- Kafka [《精尽 Kafka 面试题》](http://svip.iocoder.cn/Kafka/Interview/) 的 [「Kafka 如何保证消息的顺序性?」](https://svip.iocoder.cn/MQ/Interview/#) 的面试题。
## 如何解决消息积压的问题?
TODO
## 如何解决消息过期的问题?
TODO
## 消息队列如何实现高可用?
不同的消息队列,其架构不同,所以实现高可用的方案不同。所以参见如下文章:
- RocketMQ [《精尽 RocketMQ 面试题》](http://svip.iocoder.cn/RocketMQ/Interview/) 的 [「如何实现 RocketMQ 高可用?」](https://svip.iocoder.cn/MQ/Interview/#) 的面试题。
- RabbitMQ [《精尽 RabbitMQ 面试题》](http://svip.iocoder.cn/RabbitMQ/Interview/) 的 [「RabbitMQ 如何实现高可用?」](https://svip.iocoder.cn/MQ/Interview/#) 的面试题。
- Kafka [《精尽 Kafka 面试题》](http://svip.iocoder.cn/Kafka/Interview/) 的 [「Kafka 如何实现高可用?」](https://svip.iocoder.cn/MQ/Interview/#) 的面试题。
## Kafka、ActiveMQ、RabbitMQ、RocketMQ 有什么优缺点?
这四者,对比如下表格如下:
| 特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka |
| :----------------------- | :------------------------------------ | :------------------------------------------------- | :----------------------------------------------------------- | :----------------------------------------------------------- |
| 单机吞吐量 | 万级,比 RocketMQ、Kafka 低一个数量级 | 同 ActiveMQ | 10 万级,支撑高吞吐 | 10 万级,高吞吐,一般配合大数据类的系统来进行实时数据计算、日志采集等场景 |
| topic 数量对吞吐量的影响 | | | topic 可以达到几百/几千的级别,吞吐量会有较小幅度的下降,这是 RocketMQ 的一大优势,在同等机器下,可以支撑大量的 topic | topic 从几十到几百个时候吞吐量会大幅度下降在同等机器下Kafka 尽量保证 topic 数量不要过多,如果要支撑大规模的 topic需要增加更多的机器资源 |
| 时效性 | ms 级 | 微秒级,这是 RabbitMQ 的一大特点,延迟最低 | ms 级 | 延迟在 ms 级以内 |
| 可用性 | 高,基于主从架构实现高可用 | 同 ActiveMQ | 非常高,分布式架构 | 非常高,分布式,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 |
| 消息可靠性 | 有较低的概率丢失数据 | | 经过参数优化配置,可以做到 0 丢失 | 同 RocketMQ |
| 功能支持 | MQ 领域的功能极其完备 | 基于 erlang 开发,并发能力很强,性能极好,延时很低 | MQ 功能较为完善,还是分布式的,扩展性好 | 功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用 |
🦅 **ActiveMQ**
一般的业务系统要引入 MQ最早大家都用 ActiveMQ ,但是现在确实大家用的不多了( 特别是互联网公司 ),没经过大规模吞吐量场景的验证( **性能较差** ),社区也不是很活跃( 主要精力在研发 [ActiveMQ Apollo](https://activemq.apache.org/apollo/) ),所以大家还是算了,我个人不推荐用这个了。
🦅 **RabbitMQ**
后来大家开始用 RabbitMQ但是确实 Erlang 语言阻止了大量的 Java 工程师去深入研究和掌控它,对公司而言,几乎处于不可控的状态,但是确实人家是开源的,比较稳定的支持,社区活跃度也高。另外,因为 Spring Cloud 在消息队列的支持上,对 RabbitMQ 是比较不错的,所以在选型上又更加被推崇。
🦅 **RocketMQ**
不过现在确实越来越多的公司,会去用 RocketMQ确实很不错阿里出品。~~但社区可能有突然黄掉的风险,对自己公司技术实力有绝对自信的,推荐用 RocketMQ否则回去老老实实用 RabbitMQ 吧,人家有活跃的开源社区,绝对不会黄。~~ 目前已经加入 Apache ,所以社区层面有相应的保证,并且是使用 Java 语言进行实现,对于 Java 工程师更容易去深入研究和掌控它。目前,也是比较推荐去选择的。并且,如果使用阿里云,可以直接使用其云服务。
当然,现在比较被社区诟病的是,官方暂未提供比较好的中文文档,国内外也缺乏比较好的 RocketMQ 书籍,所以是比较大的痛点。
🦅 **总结**
- 所以**中小型公司**,技术实力较为一般,技术挑战不是特别高,用 RabbitMQ 是不错的选择
- 大型公司
,基础架构研发实力较强,用 RocketMQ 是很好的选择。
- 当然,中小型公司使用 RocketMQ 也是没什么问题的选择,特别是以 Java 为主语言的公司。
- 如果是
大数据领域
的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。
- 另外,目前国内也是有非常多的公司,将 Kafka 应用在业务系统中,例如唯品会、陆金所、美团等等。
目前,艿艿的团队使用 RocketMQ 作为消息队列,因为有 RocketMQ 5 年左右使用经验,并且目前线上环境是使用阿里云,适合我们团队。
🦅 **补充**
推荐阅读如下几篇文章:
- [《Kafka、RabbitMQ、RocketMQ等消息中间件的对比》](https://blog.csdn.net/belvine/article/details/80842240)
- [《Kafka、RabbitMQ、RocketMQ消息中间件的对比 —— 消息发送性能》](http://jm.taobao.org/2016/04/01/kafka-vs-rabbitmq-vs-rocketmq-message-send-performance/)
- [《RocketMQ与Kafka对比18项差异](https://www.cnblogs.com/BYRans/p/6100653.html)
当然,很多测评放在现在已经不适用了,特别是 Kafka ,大量评测是基于 0.X 版本,而 Kafka 目前已经演进到 2.X 版本,已经不可同日而语了。
🔥 **使用示例**
- [《芋道 Spring Boot 消息队列 RocketMQ 入门》](http://www.iocoder.cn/Spring-Boot/RocketMQ/?vip) 对应 [lab-31](https://github.com/YunaiV/SpringBoot-Labs/tree/master/lab-31) 。
- [《芋道 Spring Boot 消息队列 Kafka 入门》](http://www.iocoder.cn/Spring-Boot/Kafka/?vip) 对应 [lab-03-kafka](https://github.com/YunaiV/SpringBoot-Labs/tree/master/lab-03-kafka)
- [《芋道 Spring Boot 消息队列 RabbitMQ 入门》](http://www.iocoder.cn/Spring-Boot/RabbitMQ/?vip) 对应 [lab-04-rabbitmq](https://github.com/YunaiV/SpringBoot-Labs/tree/master/lab-04-rabbitmq)
- [《芋道 Spring Boot 消息队列 ActiveMQ 入门》](http://www.iocoder.cn/Spring-Boot/ActiveMQ/?vip) 对应 [lab-32](https://github.com/YunaiV/SpringBoot-Labs/tree/master/lab-32) 。
## 消息队列的一般存储方式有哪些?
当前业界几款主流的MQ消息队列采用的存储方式主要有以下三种方式。
🦅 **1. 分布式KV存储**
这类 MQ 一般会采用诸如 LevelDB 、RocksDB 和 Redis 来作为消息持久化的方式。由于分布式缓存的读写能力要优于 DB ,所以在对消息的读写能力要求都不是比较高的情况下,采用这种方式倒也不失为一种可以替代的设计方案。
消息存储于分布式 KV 需要解决的问题在于如何保证 MQ 整体的可靠性。
🦅 **2. 文件系统**
目前业界较为常用的几款产品RocketMQ / Kafka / RabbitMQ均采用的是消息刷盘至所部署虚拟机/物理机的文件系统来做持久化(刷盘一般可以分为异步刷盘和同步刷盘两种模式)。
> 刷盘指的是存储到硬盘。
消息刷盘为消息存储提供了一种高效率、高可靠性和高性能的数据持久化方式。除非部署 MQ 机器本身或是本地磁盘挂了,否则一般是不会出现无法持久化的故障问题。
🦅 **3. 关系型数据库 DB**
Apache下开源的另外一款MQ—ActiveMQ默认采用的KahaDB做消息存储可选用 JDBC 的方式来做消息持久化,通过简单的 XML 配置信息即可实现JDBC消息存储。
由于,普通关系型数据库(如 MySQL )在单表数据量达到千万级别的情况下,其 IO 读写性能往往会出现瓶颈。因此,如果要选型或者自研一款性能强劲、吞吐量大、消息堆积能力突出的 MQ 消息队列,那么并不推荐采用关系型数据库作为消息持久化的方案。在可靠性方面,该种方案非常依赖 DB ,如果一旦 DB 出现故障,则 MQ 的消息就无法落盘存储会导致线上故障。
🦅 **小结**
因此,综合上所述从存储效率来说,**文件系统 > 分布式 KV 存储 > 关系型数据库 DB** ,直接操作文件系统肯定是最快和最高效的,而关系型数据库 TPS 一般相比于分布式 KV 系统会更低一些(简略地说,关系型数据库本身也是一个需要读写文件 Server ,这时 MQ 作为 Client与其建立连接并发送待持久化的消息数据同时又需要依赖 DB 的事务等这一系列操作都比较消耗性能所以如果追求高效的IO读写那么选择操作文件系统会更加合适一些。但是如果从易于实现和快速集成来看**文件系统 > 分布式 KV 存储 > 关系型数据库 DB**,但是性能会下降很多。
另外,从消息中间件的本身定义来考虑,应该尽量减少对于外部第三方中间件的依赖。一般来说依赖的外部系统越多,也会使得本身的设计越复杂,所以个人的理解是采用**文件系统**作为消息存储的方式,更贴近消息中间件本身的定义。
## 如何自己设计消息队列?
TODO
## 666. 彩蛋
写的头疼,嘻嘻。继续加油~~
参考与推荐如下文章:
- 小火箭 [《关于消息队列的思考》](http://yangxikun.com/2017/03/22/message-queue.html)
- zhangxd [《JAVA 高级面试题 1》](http://zhangxianda.com/2017/06/22/JAVA高级面试题/)
- wayen [《面试:分布式之消息队列要点复习》](https://segmentfault.com/a/1190000015301449)
- 步积 [《消息队列技术介绍》](https://www.jianshu.com/p/689ce4205021) 。如果胖友对 MQ 没有系统了解过,可以认真仔细看看。
- 送人玫瑰手留余香 [《面试阿里后的总结》](http://www.voidcn.com/article/p-dzmqlwhn-boa.html)
- yanglbme [《为什么使用消息队列消息队列有什么优点和缺点Kafka、ActiveMQ、RabbitMQ、RocketMQ 都有什么优点和缺点?》](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/why-mq.md)
- 癫狂侠 [《消息中间件—RocketMQ消息存储](https://www.jianshu.com/p/b73fdd893f98)
- hacpai [《【面试宝典】消息队列如何保证幂等性?》](https://hacpai.com/article/1542160729029)
- yanglbme [《如何保证消息不被重复消费?(如何保证消息消费时的幂等性)》](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/how-to-ensure-that-messages-are-not-repeatedly-consumed.md)

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

View File

@@ -0,0 +1,531 @@
# 精尽 RocketMQ 面试题「最新更新时间2024-02」
以下面试题,基于网络整理,和自己编辑。具体参考的文章,会在文末给出所有的链接。
如果胖友有自己的疑问,欢迎在星球提问,我们一起整理吊吊的 RocketMQ 的大保健。
而题目的难度,艿艿尽量按照从容易到困难的顺序,逐步下去。
> 友情提示:在开始阅读之前,胖友至少对 [《RocketMQ —— 角色与术语详解》](http://jaskey.github.io/blog/2016/12/15/rocketmq-concept/) 有简单的了解。
另外,这个面试题是建立在胖友看过 [《精尽【消息队列 】面试题》](http://svip.iocoder.cn/MQ/Interview) 。
## RocketMQ 是什么?
RocketMQ 是阿里巴巴在 2012 年开源的分布式消息中间件,目前已经捐赠给 Apache 软件基金会,并于 2017 年 9 月 25 日成为 Apache 的顶级项目。作为经历过多次阿里巴巴双十一这种“超级工程”的洗礼并有稳定出色表现的国产中间件,以其高性能、低延时和高可靠等特性近年来已经也被越来越多的国内企业使用。
------
如下是 RocketMQ 产生的原因:
> 淘宝内部的交易系统使用了淘宝自主研发的 Notify 消息中间件,使用 MySQL 作为消息存储媒介可完全水平扩容为了进一步降低成本我们认为存储部分可以进一步优化2011 年初Linkin开源了 Kafka 这个优秀的消息中间件,淘宝中间件团队在对 Kafka 做过充分 Review 之后, Kafka 无限消息堆积,高效的持久化速度吸引了我们,但是同时发现这个消息系统主要定位于日志传输,对于使用在淘宝交易、订单、充值等场景下还有诸多特性不满足,为此我们重新用 Java 语言编写了 RocketMQ 定位于非日志的可靠消息传输日志场景也OK目前 RocketMQ 在阿里集团被广泛应用在订单,交易,充值,流计算,消息推送,日志流式处理, binglog 分发等场景。
## RocketMQ 由哪些角色组成?
如下图所示:[![RocketMQ 角色](09-RocketMQ 面试题.assets/01.png)](http://static.iocoder.cn/images/RocketMQ/2019_11_12/01.png)RocketMQ 角色
- 生产者Producer负责产生消息生产者向消息服务器发送由业务应用程序系统生成的消息。
- 消费者Consumer负责消费消息消费者从消息服务器拉取信息并将其输入用户应用程序。
- 消息服务器Broker是消息存储中心主要作用是接收来自 Producer 的消息并存储, Consumer 从这里取得消息。
- 名称服务器NameServer用来保存 Broker 相关 Topic 等元信息并给 Producer ,提供 Consumer 查找 Broker 信息。
## 请描述下 RocketMQ 的整体流程?
[![整体流程](09-RocketMQ 面试题.assets/02.png)](http://static.iocoder.cn/images/RocketMQ/2019_11_12/02.png)整体流程
- 1、启动 **Namesrv**Namesrv起 来后监听端口,等待 Broker、Producer、Consumer 连上来,相当于一个路由控制中心。
- 2、**Broker** 启动,跟所有的 Namesrv 保持长连接,定时发送心跳包。
> 心跳包中,包含当前 Broker 信息(IP+端口等)以及存储所有 Topic 信息。
> 注册成功后Namesrv 集群中就有 Topic 跟 Broker 的映射关系。
- 3、收发消息前先创建 Topic 。创建 Topic 时,需要指定该 Topic 要存储在 哪些 Broker上。也可以在发送消息时自动创建Topic。
- 4、**Producer** 发送消息。
> 启动时,先跟 Namesrv 集群中的其中一台建立长连接并从Namesrv 中获取当前发送的 Topic 存在哪些 Broker 上,然后跟对应的 Broker 建立长连接,直接向 Broker 发消息。
- 5、**Consumer** 消费消息。
> Consumer 跟 Producer 类似。跟其中一台 Namesrv 建立长连接,获取当前订阅 Topic 存在哪些 Broker 上,然后直接跟 Broker 建立连接通道,开始消费消息。
------
> 艿艿:下面,我们先逐步对 RocketMQ 每个角色进行介绍。
>
> 对不了解 RocketMQ 的胖友来说,可能概念会有点多。淡定~
## 请说说你对 Namesrv 的了解?
- 1、 Namesrv 用于存储 Topic、Broker 关系信息,功能简单,稳定性高。
- 多个 Namesrv 之间相互没有通信,单台 Namesrv 宕机不影响其它 Namesrv 与集群。
> 多个 Namesrv 之间的信息共享,**通过 Broker 主动向多个 Namesrv 都发起心跳**。正如上文所说Broker 需要跟所有 Namesrv 连接。
- 即使整个 Namesrv 集群宕机,已经正常工作的 Producer、Consumer、Broker 仍然能正常工作,但新起的 Producer、Consumer、Broker 就无法工作。
> 这点和 Dubbo 有些不同,不会缓存 Topic 等元信息到本地文件。
- 2、 Namesrv 压力不会太大,平时主要开销是在维持心跳和提供 Topic-Broker 的关系数据。但有一点需要注意Broker 向 Namesr 发心跳时,会带上当前自己所负责的所有 Topic 信息,如果 Topic 个数太多(万级别),会导致一次心跳中,就 Topic 的数据就几十 M网络情况差的话网络传输失败心跳失败导致 Namesrv 误认为 Broker 心跳失败。
> 当然,一般公司,很难达到过万级的 Topic ,因为一方面体量达不到,另一方面 RocketMQ 提供了 Tag 属性。
>
> 另外,内网环境网络相对是比较稳定的,传输几十 M 问题不大。同时如果真的要优化Broker 可以把心跳包做压缩,再发送给 Namesrv 。不过,这样也会带来 CPU 的占用率的提升。
## 如何配置 Namesrv 地址到生产者和消费者?
将 Namesrv 地址列表提供给客户端( 生产者和消费者 ),有四种方法:
- 编程方式,就像 `producer.setNamesrvAddr("ip:port")`
- Java 启动参数设置,使用 `rocketmq.namesrv.addr`
- 环境变量,使用 `NAMESRV_ADDR`
- HTTP 端点,例如说:`http://namesrv.rocketmq.xxx.com` 地址,通过 DNS 解析获得 Namesrv 真正的地址。
## 请说说你对 Broker 的了解?
- 1、 **高并发读写服务**。Broker的高并发读写主要是依靠以下两点
- 消息顺序写,所有 Topic 数据同时只会写一个文件一个文件满1G ,再写新文件,真正的顺序写盘,使得发消息 TPS 大幅提高。
- 消息随机读RocketMQ 尽可能让读命中系统 Pagecache ,因为操作系统访问 Pagecache 时,即使只访问 1K 的消息,系统也会提前预读出更多的数据,在下次读时就可能命中 Pagecache ,减少 IO 操作。
- 2、 **负载均衡与动态伸缩**
- 负载均衡Broker 上存 Topic 信息Topic 由多个队列组成,队列会平均分散在多个 Broker 上,而 Producer 的发送机制保证消息尽量平均分布到所有队列中,最终效果就是所有消息都平均落在每个 Broker 上。
- 动态伸缩能力非顺序消息Broker 的伸缩性体现在两个维度Topic、Broker。
- Topic 维度:假如一个 Topic 的消息量特别大,但集群水位压力还是很低,就可以扩大该 Topic 的队列数, Topic 的队列数跟发送、消费速度成正比。
> Topic 的队列数一旦扩大,就无法很方便的缩小。因为,生产者和消费者都是基于相同的队列数来处理。
> 如果真的想要缩小,只能新建一个 Topic ,然后使用它。
> 不过Topic 的队列数,也不存在什么影响的,淡定。
- Broker 维度:如果集群水位很高了,需要扩容,直接加机器部署 Broker 就可以。Broker 启动后向 Namesrv 注册Producer、Consumer 通过 Namesrv 发现新Broker立即跟该 Broker 直连,收发消息。
> 新增的 Broker 想要下线,想要下线也比较麻烦,暂时没特别好的方案。大体的前提是,消费者消费完该 Broker 的消息,生产者不往这个 Broker 发送消息。
- 3、 **高可用 & 高可靠**
- 高可用:集群部署时一般都为主备,备机实时从主机同步消息,如果其中一个主机宕机,备机提供消费服务,但不提供写服务。
- 高可靠:所有发往 Broker 的消息,有同步刷盘和异步刷盘机制。
- 同步刷盘时,消息写入物理文件才会返回成功。
- 异步刷盘时只有机器宕机才会产生消息丢失Broker 挂掉可能会发生,但是机器宕机崩溃是很少发生的,除非突然断电。
> 如果 Broker 挂掉,未同步到硬盘的消息,还在 Pagecache 中呆着。
- 4、 **Broker 与 Namesrv 的心跳机制**
- 单个 Broker 跟所有 Namesrv 保持心跳请求心跳间隔为30秒心跳请求中包括当前 Broker 所有的 Topic 信息。
- Namesrv 会反查 Broker 的心跳信息,如果某个 Broker 在 2 分钟之内都没有心跳,则认为该 Broker 下线,调整 Topic 跟 Broker 的对应关系。但此时 Namesrv 不会主动通知Producer、Consumer 有 Broker 宕机。也就说,只能等 Producer、Consumer 下次定时拉取 Topic 信息的时候,才会发现有 Broker 宕机。
从上面的描述中,我们也已经发现 Broker 是 RocketMQ 中最最最复杂的角色,主要包括如下五个模块:
[Broker 组件图](https://ws1.sinaimg.cn/large/006tKfTcgy1fo4vbpoxtej30r30dsdgs.jpg)
- 远程处理模块:是 Broker 的入口,处理来自客户的请求。
- Client Manager :管理客户端(生产者/消费者),并维护消费者的主题订阅。
- Store Service :提供简单的 API 来存储或查询物理磁盘中的消息。
- HA 服务:提供主节点和从节点之间的数据同步功能。
- 索引服务:通过指定键为消息建立索引,并提供快速的消息查询。
## Broker 如何实现消息的存储?
关于 Broker 如何实现消息的存储,这是一个很大的话题,所以艿艿建议直接看如下的资料,保持耐心。
- 《读懂这篇文章,你的阿里技术面就可以过关了 | Apache RocketMQ》
的如下部分:
- [「三、RocketMQ的存储模型」](https://svip.iocoder.cn/RocketMQ/Interview/#)
- 《RocketMQ 原理简介》
的如下部分:
- [「6.3 数据存储结构」](https://svip.iocoder.cn/RocketMQ/Interview/#)
- [「6.4 存储目录结构」](https://svip.iocoder.cn/RocketMQ/Interview/#)
- [「7.1 单机支持 1 万以上持久化队列」](https://svip.iocoder.cn/RocketMQ/Interview/#)
- [「7.2 刷盘策略」](https://svip.iocoder.cn/RocketMQ/Interview/#)
- 癫狂侠
- [《消息中间件 —— RocketMQ消息存储](https://www.jianshu.com/p/b73fdd893f98)
- [《消息中间件 —— RocketMQ消息存储](https://www.jianshu.com/p/6d0c118c17de)
## 请说说你对 Producer 的了解?
- 1、**获得 Topic-Broker 的映射关系**。
- Producer 启动时,也需要指定 Namesrv 的地址,从 Namesrv 集群中选一台建立长连接。如果该 Namesrv 宕机,会自动连其他 Namesrv ,直到有可用的 Namesrv 为止。
- 生产者每 30 秒从 Namesrv 获取 Topic 跟 Broker 的映射关系,更新到本地内存中。然后再跟 Topic 涉及的所有 Broker 建立长连接,每隔 30 秒发一次心跳。
- 在 Broker 端也会每 10 秒扫描一次当前注册的 Producer ,如果发现某个 Producer 超过 2 分钟都没有发心跳,则断开连接。
- 2、**生产者端的负载均衡**。
- 生产者发送时会自动轮询当前所有可发送的broker一条消息发送成功下次换另外一个broker发送以达到消息平均落到所有的broker上。
> 这里需要注意一点:假如某个 Broker 宕机,意味生产者最长需要 30 秒才能感知到。在这期间会向宕机的 Broker 发送消息。当一条消息发送到某个 Broker 失败后,会自动再重发 2 次,假如还是发送失败,则抛出发送失败异常。
>
> 客户端里会自动轮询另外一个 Broker 重新发送,这个对于用户是透明的。
## Producer 发送消息有几种方式?
Producer 发送消息,有三种方式:
1. 同步方式
2. 异步方式
3. Oneway 方式
其中,方式 1 和 2 比较常见,具体使用哪一种方式需要根据业务情况来判断。而方式 3 ,适合大数据场景,允许有一定消息丢失的场景。
具体的代码实现,可以看看 [《芋道 Spring Boot 消息队列 RocketMQ 入门》](http://www.iocoder.cn/Spring-Boot/RocketMQ/?vip)的[「3. 快速入门」](https://svip.iocoder.cn/RocketMQ/Interview/#) 小节
## 请说说你对 Consumer 的了解?
- 1、
获得 Topic-Broker 的映射关系
- Consumer 启动时需要指定 Namesrv 地址,与其中一个 Namesrv 建立长连接。消费者每隔 30 秒从 Namesrv 获取所有Topic 的最新队列情况,这意味着某个 Broker 如果宕机,客户端最多要 30 秒才能感知。连接建立后,从 Namesrv 中获取当前消费 Topic 所涉及的 Broker直连 Broker 。
- Consumer 跟 Broker 是长连接,会每隔 30 秒发心跳信息到Broker 。Broker 端每 10 秒检查一次当前存活的 Consumer ,若发现某个 Consumer 2 分钟内没有心跳,就断开与该 Consumer 的连接,并且向该消费组的其他实例发送通知,触发该消费者集群的负载均衡。
- 2、**消费者端的负载均衡**。根据消费者的消费模式不同,负载均衡方式也不同。
> 消费者有两种消费模式:集群消费和广播消费。
>
> - 集群消费:一个 Topic 可以由同一个消费这分组( Consumer Group )下所有消费者分担消费。
> 具体例子:假如 TopicA 有 6 个队列,某个消费者分组起了 2 个消费者实例,那么每个消费者负责消费 3 个队列。如果再增加一个消费者分组相同消费者实例,即当前共有 3 个消费者同时消费 6 个队列,那每个消费者负责 2 个队列的消费。
- 广播消费:每个消费者消费 Topic 下的所有队列。
## 消费者消费模式有几种?
消费者消费模式有两种:集群消费和广播消费。
🦅 **1. 集群消费**
消费者的一种消费模式。一个 Consumer Group 中的各个 Consumer 实例分摊去消费消息,即一条消息只会投递到一个 Consumer Group 下面的一个实例。
- 实际上,每个 Consumer 是平均分摊 Message Queue 的去做拉取消费。例如某个 Topic 有 3 个队列,其中一个 Consumer Group 有 3 个实例(可能是 3 个进程,或者 3 台机器),那么每个实例只消费其中的 1 个队列。
- 而由 Producer 发送消息的时候是轮询所有的队列,所以消息会平均散落在不同的队列上,可以认为队列上的消息是平均的。那么实例也就平均地消费消息了。
- 这种模式下,消费进度的存储会持久化到 Broker 。
- 当新建一个 Consumer Group 时,默认情况下,该分组的消费者会从 min offset 开始重新消费消息。
具体的代码实现,可以看看 [《芋道 Spring Boot 消息队列 RocketMQ 入门》](http://www.iocoder.cn/Spring-Boot/RocketMQ/?vip)的[「3. 快速入门」](https://svip.iocoder.cn/RocketMQ/Interview/#) 小节,在[「3.10 简单测试」](https://svip.iocoder.cn/RocketMQ/Interview/#)中有集群消费的示例。
🦅 **2. 广播消费**
消费者的一种消费模式。消息将对一 个Consumer Group 下的各个 Consumer 实例都投递一遍。即即使这些 Consumer 属于同一个Consumer Group ,消息也会被 Consumer Group 中的每个 Consumer 都消费一次。
- 实际上,是一个消费组下的每个消费者实例都获取到了 Topic 下面的每个 Message Queue 去拉取消费。所以消息会投递到每个消费者实例。
- 这种模式下,消费进度会存储持久化到实例本地。
具体的代码实现,可以看看 [《芋道 Spring Boot 消息队列 RocketMQ 入门》](http://www.iocoder.cn/Spring-Boot/RocketMQ/?vip)的[「7. 广播消费」](https://svip.iocoder.cn/RocketMQ/Interview/#) 小节。
## 消费者获取消息有几种模式?
消费者获取消息有两种模式:推送模式和拉取模式。
🦅 **1. PushConsumer**
推送模式(虽然 RocketMQ 使用的是长轮询)的消费者。消息的能及时被消费。使用非常简单,内部已处理如线程池消费、流控、负载均衡、异常处理等等的各种场景。
- 长轮询,就是我们在 [《 精尽【消息队列 】面试题》](http://svip.iocoder.cn/MQ/Interview) 提到的push + pull 模式结合的方式。
🦅 **2. PullConsumer**
拉取模式的消费者。应用主动控制拉取的时机,怎么拉取,怎么消费等。主动权更高。但要自己处理各种场景。
------
决绝绝大多数场景下,我们只会使用 PushConsumer 推送模式。😈 至少艿艿目前,暂时还没用过 PullConsumer 。
## 如何对消息进行重放?
消费位点就是一个数字,把 Consumer Offset 改一下,就可以达到重放的目的了。
## 什么是顺序消息?如何实现?
消费消息的顺序要同发送消息的顺序一致。由于 Consumer 消费消息的时候是针对 Message Queue 顺序拉取并开始消费,且一条 Message Queue 只会给一个消费者(集群模式下),所以能够保证同一个消费者实例对于 Queue 上消息的消费是顺序地开始消费(不一定顺序消费完成,因为消费可能并行)。
- Consumer :在 RocketMQ 中,顺序消费主要指的是都是 Queue 级别的局部顺序。这一类消息为满足顺序性,必须 Producer 单线程顺序发送,且发送到同一个队列,这样 Consumer 就可以按照 Producer 发送的顺序去消费消息。
- Producer :生产者发送的时候可以用 MessageQueueSelector 为某一批消息通常是有相同的唯一标示id选择同一个 Queue 则这一批消息的消费将是顺序消息并由同一个consumer完成消息。或者 Message Queue 的数量只有 1 ,但这样消费的实例只能有一个,多出来的实例都会空跑。
**当然上面的文字比较绕总的来说RocketMQ 提供了两种顺序级别**
- 普通顺序消息 Producer 将相关联的消息发送到相同的消息队列。
- 严格顺序消息 在【普通顺序消息】的基础上Consumer 严格顺序消费。
> 也就说顺序消息包括两块Producer 的顺序发送,和 Consumer 的顺序消费。
🦅 **1. 普通顺序消息**
顺序消息的一种正常情况下可以保证完全的顺序消息但是一旦发生异常Broker 宕机或重启,由于队列总数发生发化,消费者会触发负载均衡,而默认地负载均衡算法采取哈希取模平均,这样负载均衡分配到定位的队列会发化,使得队列可能分配到别的实例上,则会短暂地出现消息顺序不一致。
如果业务能容忍在集群异常情况(如某个 Broker 宕机或者重启)下,消息短暂的乱序,使用普通顺序方式比较合适。
🦅 **2. 严格顺序消息**
顺序消息的一种,无论正常异常情况都能保证顺序,但是牺牲了分布式 Failover 特性,即 Broker 集群中只要有一台机器不可用,则整个集群都不可用,服务可用性大大降低。
如果服务器部署为同步双写模式,此缺陷可通过备机自动切换为主避免,不过仍然会存在几分钟的服务不可用。(依赖同步双写,主备自动切换,自动切换功能目前并未实现)
🦅 **小结**
目前已知的应用只有数据库 binlog 同步强依赖严格顺序消息,其他应用绝大部分都可以容忍短暂乱序,推荐使用普通的顺序消息。
🦅 **实现原理**
顺序消息的实现,相对比较复杂,想要深入理解的胖友,可以看看 [《RocketMQ 源码分析 —— Message 顺序发送与消费》](http://www.iocoder.cn/RocketMQ/message-send-and-consume-orderly/) 。
具体的代码实现,可以看看 [《芋道 Spring Boot 消息队列 RocketMQ 入门》](http://www.iocoder.cn/Spring-Boot/RocketMQ/?vip)的[「8. 顺序消息」](https://svip.iocoder.cn/RocketMQ/Interview/#) 小节。
## 顺序消息扩容的过程中,如何在不停写的情况下保证消息顺序?
1. 成倍扩容,实现扩容前后,同样的 keyhash 到原队列,或者 hash 到新扩容的队列。
2. 扩容前,记录旧队列中的最大位点。
3. 对于每个 Consumer Group ,保证旧队列中的数据消费完,再消费新队列,也即:先对新队列进行禁读即可。
## 什么是定时消息?如何实现?
定时消息,是指消息发到 Broker 后,不能立刻被 Consumer 消费,要到特定的时间点或者等待特定的时间后才能被消费。
目前,开源版本的 RocketMQ 只支持固定延迟级别的延迟消息,不支持任一时刻的延迟消息。如下表格:
| 延迟级别 | 时间 |
| :------- | :--- |
| 1 | 1s |
| 2 | 5s |
| 3 | 10s |
| 4 | 30s |
| 5 | 1m |
| 6 | 2m |
| 7 | 3m |
| 8 | 4m |
| 9 | 5m |
| 10 | 6m |
| 11 | 7m |
| 12 | 8m |
| 13 | 9m |
| 14 | 10m |
| 15 | 20m |
| 16 | 30m |
| 17 | 1h |
| 18 | 2h |
- 可通过配置文件,自定义每个延迟级别对应的延迟时间。当然,这是全局的。
- 如果胖友想要实现任一时刻的延迟消息,比较简单的方式是插入延迟消息到数据库中,然后通过定时任务轮询,到达指定时间,发送到 RocketMQ 中。
🦅 **实现原理**
- 1、 定时消息发送到 Broker 后,会被存储 Topic 为 `SCHEDULE_TOPIC_XXXX` 中,并且所在 Queue 编号为延迟级别 - 1 。
> 需要 -1 的原因是,延迟级别是从 1 开始的。如果延迟级别为 0 ,意味着无需延迟。
- 2、Broker 针对每个 `SCHEDULE_TOPIC_XXXX` 的队列,都创建一个定时任务,**顺序**扫描到达时间的延迟消息,重新存储到延迟消息**原始**的 Topic 的**原始** Queue 中,这样它就可以被 Consumer 消费到。此处会有两个问题:
- 为什么是“**顺序**扫描到达时间的延迟消息”?因为先进 `SCHEDULE_TOPIC_XXXX` 的延迟消息,在其所在的队列,意味着先到达延迟时间。
- 会不会存在重复扫描的情况?每个 `SCHEDULE_TOPIC_XXXX` 的扫描进度,会每 10s 存储到 `config/delayOffset.json` 文件中,所以正常情况下,不会存在重复扫描。如果异常关闭,则可能导致重复扫描。
详细的,胖友可以看看 [《RocketMQ 源码分析 —— 定时消息与消息重试》](http://www.iocoder.cn/RocketMQ/message-schedule-and-retry/) 。
具体的代码实现,可以看看 [《芋道 Spring Boot 消息队列 RocketMQ 入门》](http://www.iocoder.cn/Spring-Boot/RocketMQ/?vip)的[「5. 定时消息」](https://svip.iocoder.cn/RocketMQ/Interview/#) 小节。
## 什么是消息重试?如何实现?
消息重试Consumer 消费消息失败后,要提供一种重试机制,令消息再消费一次。
- Consumer 会将消费失败的消息发回 Broker进入延迟消息队列。即消费失败的消息不会立即消费。
- 也就是说,消息重试是构建在定时消息之上的功能。
🦅 **消息重试的主要流程**
1. Consumer 消费失败,将消息发送回 Broker 。
2. Broker 收到重试消息之后置换 Topic ,存储消息。
3. Consumer 会拉取该 Topic 对应的 retryTopic 的消息。
4. Consumer 拉取到 retryTopic 消息之后,置换到原始的 Topic ,把消息交给 Listener 消费。
这里,可能有几个点,胖友会比较懵逼,艿艿简单解释下:
1. Consumer 消息失败后,会将消息的 Topic 修改为 `%RETRY%` + Topic 进行,添加 `"RETRY_TOPIC"` 属性为原始 Topic ,然后再返回给 Broker 中。
2. Broker 收到重试消息之后,会有两次修改消息的 Topic 。
- 首先,会将消息的 Topic 修改为 `%RETRY%` + ConsumerGroup ,因为这个消息是当前消费这分组消费失败,只能被这个消费组所重新消费。😈 注意噢,消费者会默认订阅 Topic 为 `%RETRY%` + ConsumerGroup 的消息。
- 然后,会将消息的 Topic 修改为 `SCHEDULE_TOPIC_XXXX` ,添加 `"REAL_TOPIC"` 属性为 `%RETRY%` + ConsumerGroup ,因为重试消息需要延迟消费。
3. Consumer 会拉取该 Topic 对应的 retryTopic 的消息,此处的 retryTopic 为 `%RETRY%` + ConsumerGroup 。
4. Consumer 拉取到 retryTopic 消息之后,置换到原始的 Topic ,因为有消息的 `"RETRY_TOPIC"` 属性是原始 Topic ,然后把消息交给 Listener 消费。
😈 有一丢丢复杂,胖友可以在思考思考~详细的,胖友可以看看 [《RocketMQ 源码分析 —— 定时消息与消息重试》](http://www.iocoder.cn/RocketMQ/message-schedule-and-retry/) 。
具体的代码实现,可以看看 [《芋道 Spring Boot 消息队列 RocketMQ 入门》](http://www.iocoder.cn/Spring-Boot/RocketMQ/?vip)的[「6. 消费重试」](https://svip.iocoder.cn/RocketMQ/Interview/#) 小节。
## 多次消费失败后,怎么办?
默认情况下,当一条消息被消费失败 16 次后,会被存储到 Topic 为 `"%DLQ%"` + ConsumerGroup 到死信队列。
为什么 Topic 是 `"%DLQ%"` + ConsumerGroup 呢?因为,是这个 ConsumerGroup 对消息的消费失败,所以 Topic 里要以 ConsumerGroup 为维度。
后续,我们可以通过订阅 `"%DLQ%"` + ConsumerGroup ,做相应的告警。
## 什么是事务消息?如何实现?
关于事务消息的概念和原理,胖友可以看看官方对这块的解答,即 [《RocketMQ 4.3 正式发布,支持分布式事务》](https://www.infoq.cn/article/2018%2F08%2Frocketmq-4.3-release) 的 [「四 事务消息」](https://svip.iocoder.cn/RocketMQ/Interview/#) 小节。
艿艿 16 年的时候,基于 RocketMQ 早期的版本,写了 [《RocketMQ 源码分析 —— 事务消息》](http://www.iocoder.cn/RocketMQ/message-transaction/) 文章,虽然 RocketMQ 版本不太一样,但是大体的思路是差不多的,可以帮助胖友更容易的读懂事务消息相关的源码。
- 简单看了下最新版本的 RocketMQ 的事务代码,新增了
```
RMQ_SYS_TRANS_HALF_TOPIC
```
```
RMQ_SYS_TRANS_OP_HALF_TOPIC
```
两个队列。
- Producer 发送 PREPARED Message 到 Broker 后,先存储到 `RMQ_SYS_TRANS_HALF_TOPIC` 队列中。
- Producer 提交或回滚 PREPARED Message 时,会添加一条消息到 `RMQ_SYS_TRANS_OP_HALF_TOPIC` 队列中,标记这个消息已经处理。
- Producer 提交 PREPARED Message 时,会将当前消息存储到原 Topic 的队列中,从而该消息能够被 Consumer 拉取消费。
具体的代码实现,可以看看 [《芋道 Spring Boot 消息队列 RocketMQ 入门》](http://www.iocoder.cn/Spring-Boot/RocketMQ/?vip)的[「9. 事务消息」](https://svip.iocoder.cn/RocketMQ/Interview/#) 小节。
## 如何实现 RocketMQ 高可用?
在 [「RocketMQ 由哪些角色组成?」](https://svip.iocoder.cn/RocketMQ/Interview/#) 中,我们看到 RocketMQ 有四个角色,需要考虑每个角色的高可用。
[![RocketMQ 集群](09-RocketMQ 面试题.assets/02.png)](http://static.iocoder.cn/images/RocketMQ/2019_11_12/02.png)RocketMQ 集群
🦅 **1. Producer**
- 1、Producer 自身在应用中,所以无需考虑高可用。
- 2、Producer 配置多个 Namesrv 列表,从而保证 Producer 和 Namesrv 的连接高可用。并且,会从 Namesrv 定时拉取最新的 Topic 信息。
- 3、Producer 会和所有 Broker 直连,在发送消息时,会选择一个 Broker 进行发送。如果发送失败,则会使用另外一个 Broker 。
- 4、Producer 会定时向 Broker 心跳,证明其存活。而 Broker 会定时检测,判断是否有 Producer 异常下线。
🦅 **2. Consumer**
- 1、Consumer 需要部署多个节点,以保证 Consumer 自身的高可用。当相同消费者分组中有新的 Consumer 上线,或者老的 Consumer 下线,会重新分配 Topic 的 Queue 到目前消费分组的 Consumer 们。
- 2、Consumer 配置多个 Namesrv 列表,从而保证 Consumer 和 Namesrv 的连接高可用。并且,会从 Consumer 定时拉取最新的 Topic 信息。
- 3、Consumer 会和所有 Broker 直连,消费相应分配到的 Queue 的消息。如果消费失败,则会发回消息到 Broker 中。
- 4、Consumer 会定时向 Broker 心跳,证明其存活。而 Broker 会定时检测,判断是否有 Consumer 异常下线。
🦅 **3. Namesrv**
- 1、Namesrv 需要部署多个节点,以保证 Namesrv 的高可用。
- 2、Namesrv 本身是无状态,不产生数据的存储,是通过 Broker 心跳将 Topic 信息同步到 Namesrv 中。
- 3、多个 Namesrv 之间不会有数据的同步,是通过 Broker 向多个 Namesrv 多写。
🦅 **4. Broker**
- 1、多个 Broker 可以形成一个 Broker 分组。每个 Broker 分组存在一个 Master 和多个 Slave 节点。
- Master 节点可提供读和写功能。Slave 节点,可提供读功能。
- Master 节点会不断发送新的 CommitLog 给 Slave节点。Slave 节点不断上报本地的 CommitLog 已经同步到的位置给 Master 节点。
- Slave 节点会从 Master 节点拉取消费进度、Topic 配置等等。
- 2、多个 Broker 分组,形成 Broker 集群。
- Broker 集群和集群之间,不存在通信与数据同步。
- 3、Broker 可以配置同步刷盘或异步刷盘,根据消息的持久化的可靠性来配置。
🦅 **总结**
目前官方提供三套配置:
- **2m-2s-async**
| brokerClusterName | brokerName | brokerRole | brokerId |
| :---------------- | :--------- | :----------- | :------- |
| DefaultCluster | broker-a | ASYNC_MASTER | 0 |
| DefaultCluster | broker-a | SLAVE | 1 |
| DefaultCluster | broker-b | ASYNC_MASTER | 0 |
| DefaultCluster | broker-b | SLAVE | 1 |
- **2m-2s-sync**
| brokerClusterName | brokerName | brokerRole | brokerId |
| :---------------- | :--------- | :---------- | :------- |
| DefaultCluster | broker-a | SYNC_MASTER | 0 |
| DefaultCluster | broker-a | SLAVE | 1 |
| DefaultCluster | broker-b | SYNC_MASTER | 0 |
| DefaultCluster | broker-b | SLAVE | 1 |
- **2m-noslave**
| brokerClusterName | brokerName | brokerRole | brokerId |
| :---------------- | :--------- | :----------- | :------- |
| DefaultCluster | broker-a | ASYNC_MASTER | 0 |
| DefaultCluster | broker-b | ASYNC_MASTER | 0 |
相关的源码解析,胖友可以看看 [《RocketMQ 源码分析 —— 高可用》](http://www.iocoder.cn/RocketMQ/high-availability/) 。
## RocketMQ 是否会弄丢数据?
> 艿艿注意RocketMQ 是否会丢数据,主要取决于我们如何使用。这点,非常重要噢。
🦅 **消费端弄丢了数据?**
对于消费端,如果我们在使用 Push 模式的情况下,只有我们消费返回成功,才会异步定期更新消费进度到 Broker 上。
如果消费端异常崩溃,可能导致消费进度未更新到 Broker 上,那么无非是 Consumer 可能重复拉取到已经消费过的消息。关于这个,就需要消费端做好消费的幂等性。
🦅 **Broker 弄丢了数据?**
在上面的问题中,我们已经看到了 Broker 提供了两个特性:
- 刷盘方式:同步刷盘、异步刷盘。
- 复制方式:同步复制、异步复制。
如果要保证 Broker 数据最大化的不丢,需要在搭建 Broker 集群时,设置为同步刷盘、同步复制。当然,带来了可靠性,也会一定程度降低性能。
如果想要在可靠性和性能之间做一个平衡,可以选择同步复制,加主从 Broker 都是和异步刷盘。因为,刷盘比较消耗性能。
🦅 **生产者会不会弄丢数据?**
Producer 可以设置三次发送消息重试。
## 如何保证消费者的消费消息的幂等性?
在 [《精尽【消息队列 】面试题》](http://svip.iocoder.cn/MQ/Interview) 中,已经解析过该问题。当然,我们有几点要补充下:
- Producer 在发送消息时,默认会生成消息编号( `msgId` ),可见 `org.apache.rocketmq.common.message.MessageClientExt` 类。
- Broker 在存储消息时,会生成结合 offset 的消息编号( `offsetMsgId` ) 。
- Consumer 在消费消息失败后,将该消息发回 Broker 后,会产生新的 `offsetMsgId` 编号,但是 `msgId` 不变。
## 重点补充说明
RocketMQ 涉及的内容很多,能够问的问题也特别多,但是我们不能仅仅为了面试,应该是为了对 RocketMQ 了解更多,使用的更优雅。所以,强烈胖友认真撸下如下三个 PDF
- [《RocketMQ 用户指南》](http://gd-rus-public.cn-hangzhou.oss-pub.aliyun-inc.com/attachment/201604/08/20160408164726/RocketMQ_userguide.pdf) 基于 RocketMQ 3 的版本。
- [《RocketMQ 原理简介》](http://alibaba.github.io/RocketMQ-docs/document/design/RocketMQ_design.pdf) 基于 RocketMQ 3 的版本。
- [《RocketMQ 最佳实践》](http://gd-rus-public.cn-hangzhou.oss-pub.aliyun-inc.com/attachment/201604/08/20160408164929/RocketMQ_experience.pdf) 基于 RocketMQ 3 的版本。
## 666. 彩蛋
RocketMQ 能够问的东西,真的挺多的,中间也和一些朋友探讨过。如果胖友有什么想要问的,可以在星球给艿艿留言。
参考与推荐如下文章:
- javahongxi [《RocketMQ 架构模块解析》](https://blog.csdn.net/javahongxi/article/details/72956608)
- 薛定谔的风口猪 [《RocketMQ —— 角色与术语详解》](http://jaskey.github.io/blog/2016/12/15/rocketmq-concept/)
- 阿里中间件小哥 [《一文讲透 Apache RocketMQ 技术精华》](http://www.10tiao.com/html/683/201811/2650718577/1.html)

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -0,0 +1,549 @@
# 精尽 RabbitMQ 面试题「最新更新时间2024-02」
以下面试题,基于网络整理,和自己编辑。具体参考的文章,会在文末给出所有的链接。
如果胖友有自己的疑问,欢迎在星球提问,我们一起整理吊吊的 RabbitMQ 面试题的大保健。
而题目的难度,艿艿尽量按照从容易到困难的顺序,逐步下去。
> 艿艿:我们生产主要使用的是 RocketMQ ,所以对 RabbitMQ 灰常不熟悉(😈 看了《RabbitMQ 实战指南》入门级的萌新)。所以本文以整理为主。如果胖友在实际面试场景下,碰到 RabbitMQ 的一些问题,可以返回到星球,艿艿可以去请教厮哒哒,然后来装比。
## RabbitMQ 是什么?
RabbitMQ 是一个由 Erlang 语言开发的 AMQP 的开源实现。
> AMQP Advanced Message Queue高级消息队列协议。它是应用层协议的一个开放标准为面向消息的中间件设计基于此协议的客户端与消息中间件可传递消息并不受产品、开发语言等条件的限制。
RabbitMQ 最初起源于金融系统,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。具体特点包括:
- 1、可靠性Reliability
> RabbitMQ 使用一些机制来保证可靠性,如持久化、传输确认、发布确认。
- 2、灵活的路由Flexible Routing
> 在消息进入队列之前,通过 Exchange 来路由消息的。对于典型的路由功能RabbitMQ 已经提供了一些内置的 Exchange 来实现。针对更复杂的路由功能,可以将多个 Exchange 绑定在一起,也通过插件机制实现自己的 Exchange 。
- 3、消息集群Clustering
> 多个 RabbitMQ 服务器可以组成一个集群,形成一个逻辑 Broker 。
- 4、高可用Highly Available Queues
> 队列可以在集群中的机器上进行镜像,使得在部分节点出问题的情况下队列仍然可用。
- 5、多种协议Multi-protocol
> RabbitMQ 支持多种消息队列协议,比如 STOMP、MQTT 等等。
- 6、多语言客户端Many Clients
> RabbitMQ 几乎支持所有常用语言,比如 Java、.NET、Ruby 等等。
- 7、管理界面Management UI
> RabbitMQ 提供了一个易用的用户界面,使得用户可以监控和管理消息 Broker 的许多方面。
- 8、跟踪机制Tracing
> 如果消息异常RabbitMQ 提供了消息跟踪机制,使用者可以找出发生了什么。
- 9、插件机制Plugin System
> RabbitMQ 提供了许多插件,来从多方面进行扩展,也可以编写自己的插件。
更详细的,推荐阅读 [《消息队列之 RabbitMQ》](http://www.iocoder.cn/RabbitMQ/yuliu/doc/?vip) 。它提供了:
- 1、RabbitMQ 的介绍
- 2、RabbitMQ 的概念
- 3、RabbitMQ 的 Server 安装
- 4、RabbitMQ 的 Java Client 使用示例
- 5、RabbitMQ 的集群
## RabbitMQ 中的 Broker 是指什么Cluster 又是指什么?
- Broker ,是指一个或多个 erlang node 的逻辑分组,且 node 上运行着 RabbitMQ 应用程序。
- Cluster ,是在 Broker 的基础之上,增加了 node 之间共享元数据的约束。
🦅 **vhost 是什么?起什么作用?**
vhost 可以理解为虚拟 Broker ,即 mini-RabbitMQ server 。其内部均含有独立的 queue、exchange 和 binding 等,但最最重要的是,其拥有独立的权限系统,可以做到 vhost 范围的用户控制。当然,从 RabbitMQ 的全局角度vhost 可以作为不同权限隔离的手段(一个典型的例子就是不同的应用可以跑在不同的 vhost 中)。
> 这个,和 Tomcat、Nginx、Apache 的 vhost 是一样的概念。
## 什么是元数据?元数据分为哪些类型?包括哪些内容?
在非 Cluster 模式下,元数据主要分为:
- Queue 元数据queue 名字和属性等)
- Exchange 元数据exchange 名字、类型和属性等)
- Binding 元数据(存放路由关系的查找表)
- Vhost 元数据vhost 范围内针对前三者的名字空间约束和安全属性设置)。
🦅 **与 Cluster 相关的元数据有哪些?元数据是如何保存的?元数据在 Cluster 中是如何分布的?**
在 Cluster 模式下,还包括 Cluster 中 node 位置信息和 node 关系信息。
元数据根据 erlang node 的类型确定是仅保存于 RAM 中,还是同时保存在 RAM 和 disk 上。元数据在 Cluster 中是全 node 分布的。
下图所示为 queue 的元数据在单 node 和 cluster 两种模式下的分布图:
[![分布图](10-RabbitMQ 面试题.assets/c7566feb7a61cb647296a7224ba7f39d.png)](http://static.iocoder.cn/c7566feb7a61cb647296a7224ba7f39d)分布图
🦅 **RAM node 和 Disk node 的区别?**
- RAM node 仅将 fabric即 queue、exchange 和 binding等 RabbitMQ基础构件相关元数据保存到内存中但 Disk node 会在内存和磁盘中均进行存储。
- RAM node 上唯一会存储到磁盘上的元数据是 Cluster 中使用的 Disk node 的地址。并且要求在 RabbitMQ Cluster 中至少存在一个 Disk node 。
## RabbitMQ 概念里的 channel、exchange 和 queue 是什么?
- queue 具有自己的 erlang 进程;
- exchange 内部实现为保存 binding 关系的查找表;
- channel 是实际进行路由工作的实体,即负责按照 routing_key 将 message 投递给 queue 。
由 AMQP 协议描述可知channel 是真实 TCP 连接之上的虚拟连接,所有 AMQP 命令都是通过 channel 发送的,且每一个 channel 有唯一的 ID 。
- 一个 channel 只能被单独一个操作系统线程使用,故投递到特定 channel 上的 message 是有顺序的。但一个操作系统线程上允许使用多个 channel 。
- channel 号为 0 的 channel 用于处理所有对于当前 connection 全局有效的帧,而 1-65535 号 channel 用于处理和特定 channel 相关的帧。
- AMQP 协议给出的 channel 复用模型如下:
![channel 复用模型](10-RabbitMQ 面试题.assets/0cb69bbcda6043bc0c9d7733de251d76.png)
channel 复用模型
- 其中每一个 channel 运行在一个独立的线程上,多线程共享同一个 socket 。
🦅 **消息基于什么传输?**
由于 TCP 连接的创建和销毁开销较大且并发数受系统资源限制会造成性能瓶颈。RabbitMQ 使用信道的方式来传输数据。信道是建立在真实的 TCP 连接内的虚拟连接,且每条 TCP连接上的信道数量没有限制。
🦅 **RabbitMQ 上的一个 queue 中存放的 message 是否有数量限制?**
可以认为是无限制,因为限制取决于机器的内存,但是消息过多会导致处理效率的下降。
> 下面的几个问题Cluster 相关。
🦅 **在单 node 系统和多 node 构成的 cluster 系统中声明 queue、exchange ,以及进行 binding 会有什么不同?**
- 当你在单 node 上声明 queue 时,只要该 node 上相关元数据进行了变更,你就会得到 `Queue.Declare-ok` 回应;而在 cluster 上声明 queue ,则要求 cluster 上的全部 node 都要进行元数据成功更新,才会得到 `Queue.Declare-ok` 回应。
- 另外,若 node 类型为 RAM node 则变更的数据仅保存在内存中,若类型为 Disk node 则还要变更保存在磁盘上的数据。
🦅 **客户端连接到 Cluster 中的任意 node 上是否都能正常工作?**
是的。客户端感觉不到有何不同。
🦅 **若 Cluster 中拥有某个 queue 的 owner node 失效了,且该 queue 被声明具有 durable 属性,是否能够成功从其他 node 上重新声明该 queue **
- 不能,在这种情况下,将得到 404 NOT_FOUND 错误。只能等 queue 所属的 node 恢复后才能使用该 queue 。
- 但若该 queue 本身不具有 durable 属性,则可在其他 node 上重新声明。
🦅 **Cluster 中 node 的失效会对 consumer 产生什么影响?若是在 cluster 中创建了 mirrored queue ,这时 node 失效会对 consumer 产生什么影响?**
- 若是 consumer 所连接的那个 node 失效(无论该 node 是否为 consumer 所订阅 queue 的 owner node则 consumer 会在发现 TCP 连接断开时,按标准行为执行重连逻辑,并根据 “Assume Nothing” 原则重建相应的 fabric 即可。
- 若是失效的 node 为 consumer 订阅 queue 的 owner node则 consumer 只能通过 Consumer Cancellation Notification 机制来检测与该 queue 订阅关系的终止,否则会出现傻等却没有任何消息来到的问题。
🦅 **Consumer Cancellation Notification 机制用于什么场景?**
用于保证当镜像 queue 中 master 挂掉时,连接到 slave 上的 consumer 可以收到自身 consume 被取消的通知,进而可以重新执行 consume 动作从新选出的 master 出获得消息。
若不采用该机制,连接到 slave 上的 consumer 将不会感知 master 挂掉这个事情,导致后续无法再收到新 master 广播出来的 message 。
另外,因为在镜像 queue 模式下,存在将 message 进行 requeue 的可能,所以实现 consumer 的逻辑时需要能够正确处理出现重复 message 的情况。
🦅 **能够在地理上分开的不同数据中心使用 RabbitMQ cluster 么?**
不能。
- 第一,你无法控制所创建的 queue 实际分布在 cluster 里的哪个 node 上(一般使用 HAProxy + cluster 模型时都是这样),这可能会导致各种跨地域访问时的常见问题。
- 第二Erlang 的 OTP 通信框架对延迟的容忍度有限,这可能会触发各种超时,导致业务疲于处理。
- 第三,在广域网上的连接失效问题将导致经典的“脑裂”问题,而 RabbitMQ 目前无法处理。(该问题主要是说 Mnesia
## 如何确保消息正确地发送至 RabbitMQ
RabbitMQ 使用**发送方确认模式**,确保消息正确地发送到 RabbitMQ。
- 发送方确认模式:将信道设置成 confirm 模式(发送方确认模式),则所有在信道上发布的消息都会被指派一个唯一的 ID 。一旦消息被投递到目的队列后或者消息被写入磁盘后可持久化的消息信道会发送一个确认给生产者包含消息唯一ID。如果 RabbitMQ 发生内部错误从而导致消息丢失,会发送一条 nacknot acknowledged未确认消息。
- 发送方确认模式是异步的,生产者应用程序在等待确认的同时,可以继续发送消息。当确认消息到达生产者应用程序,生产者应用程序的回调方法就会被触发来处理确认消息。
具体的代码实现,可以看看 [《芋道 Spring Boot 消息队列 RabbitMQ 入门》](http://www.iocoder.cn/Spring-Boot/RabbitMQ/?vip)的[「14. 生产者的发送确认」](https://svip.iocoder.cn/RabbitMQ/Interview/#) 小节
🦅 **向不存在的 exchange 发 publish 消息会发生什么?向不存在的 queue 执行 consume 动作会发生什么?**
都会收到 `Channel.Close` 信令告之不存在(内含原因 404 NOT_FOUND
🦅 **什么情况下会出现 blackholed 问题?**
> blackholed ,对应中文为“黑洞”。
blackholed 问题是指,向 exchange 投递了 message ,而由于各种原因导致该 message 丢失,但发送者却不知道。可导致 blackholed 的情况:
- 1、向未绑定 queue 的 exchange 发送 message 。
- 2、exchange 以 binding_key key_A 绑定了 queue queue_A但向该 exchange 发送 message 使用的 routing_key 却是 key_B 。
🦅 **如何防止出现 blackholed 问题?**
没有特别好的办法,只能在具体实践中通过各种方式保证相关 fabric 的存在。另外,如果在执行 `Basic.Publish` 时设置 `mandatory=true` ,则在遇到可能出现 blackholed 情况时,服务器会通过返回 `Basic.Return` 告之当前 message 无法被正确投递(内含原因 312 NO_ROUTE
具体的代码实现,可以看看 [《芋道 Spring Boot 消息队列 RabbitMQ 入门》](http://www.iocoder.cn/Spring-Boot/RabbitMQ/?vip)的[「14.3 ReturnCallback」](https://svip.iocoder.cn/RabbitMQ/Interview/#) 小节
🦅 **routing_key 和 binding_key 的最大长度是多少?**
255 字节。
🦅 **消息怎么路由?**
从概念上来说,消息路由必须有三部分:交换器、路由、绑定。
- 生产者把消息发布到**交换器**上;
- **绑定**决定了消息如何从路由器路由到特定的队列;
> 如果一个路由绑定了两个队列,那么发送给该路由时,这两个队列都会增加一条消息。
- 消息最终到达**队列**,并被消费者接收。
详细来说,就是:
- 消息发布到交换器时消息将拥有一个路由键routing key在消息创建时设定。
- 通过队列路由键,可以把队列绑定到交换器上。
- 消息到达交换器后RabbitMQ 会将消息的路由键与队列的路由键进行匹配(针对不同的交换器有不同的路由规则)。如果能够匹配到队列,则消息会投递到相应队列中;如果不能匹配到任何队列,消息将进入 “黑洞”。
常用的交换器主要分为一下三种:
- direct如果路由键完全匹配消息就被投递到相应的队列。
- fanout如果交换器收到消息将会广播到所有绑定的队列上。
- topic可以使来自不同源头的消息能够到达同一个队列。 使用 topic 交换器时,可以使用通配符,比如:`“*”` 匹配特定位置的任意文本, `“.”` 把路由键分为了几部分,`“#”` 匹配所有规则等。特别注意:发往 topic 交换器的消息不能随意的设置选择键routing_key必须是由 `"."` 隔开的一系列的标识符组成。
具体的代码实现,可以看看 [《芋道 Spring Boot 消息队列 RabbitMQ 入门》](http://www.iocoder.cn/Spring-Boot/RabbitMQ/?vip)的[「3. 快速入门」](https://svip.iocoder.cn/RabbitMQ/Interview/#) 小节
## 如何确保消息接收方消费了消息?
RabbitMQ 使用**接收方消息确认机制**,确保消息接收方消费了消息。
- 接收方消息确认机制消费者接收每一条消息后都必须进行确认消息接收和消息确认是两个不同操作。只有消费者确认了消息RabbitMQ 才能安全地把消息从队列中删除。
这里并没有用到超时机制RabbitMQ 仅通过 Consumer 的连接中断来确认是否需要重新发送消息。也就是说只要连接不中断RabbitMQ 给了 Consumer 足够长的时间来处理消息。
下面罗列几种特殊情况:
- 如果消费者接收到消息在确认之前断开了连接或取消订阅RabbitMQ 会认为消息没有被分发,然后重新分发给下一个订阅的消费者。(可能存在消息重复消费的隐患,需要根据 bizId 去重)
- 如果消费者接收到消息却没有确认消息,连接也未断开,则 RabbitMQ 认为该消费者繁忙,将不会给该消费者分发更多的消息。
具体的代码实现,可以看看 [《芋道 Spring Boot 消息队列 RabbitMQ 入门》](http://www.iocoder.cn/Spring-Boot/RabbitMQ/?vip)的[「13. 消费者的消息确认」](https://svip.iocoder.cn/RabbitMQ/Interview/#) 小节。
🦅 **如何避免消息重复投递或重复消费?**
在消息生产时MQ 内部针对每条生产者发送的消息生成一个 inner-msg-id ,作为去重和幂等的依据(消息投递失败并重传),避免重复的消息进入队列
在消息消费时,要求消息体中必须要有一个 bizId对于同一业务全局唯一如支付 ID、订单 ID、帖子 ID 等)作为去重和幂等的依据,避免同一条消息被重复消费。
🦅 **消息如何分发?**
若该队列至少有一个消费者订阅消息将以循环round-robin的方式发送给消费者。每条消息只会分发给一个订阅的消费者前提是消费者能够正常处理消息并进行确认
🦅 **RabbitMQ 有几种消费模式?**
RabbitMQ 有 pull 和 push 两种消费模式。具体的使用,可参见 [《RabbitMQ 之 Consumer 消费模式Push & Pull](https://blog.csdn.net/u013256816/article/details/62890189) 文章。
## 为什么不应该对所有的 message 都使用持久化机制?
首先,必然导致性能的下降,因为写磁盘比写 RAM 慢的多message 的吞吐量可能有 10 倍的差距。
其次message 的持久化机制用在 RabbitMQ 的内置 Cluster 方案时会出现“坑爹”问题。矛盾点在于:
- 若 message 设置了 persistent 属性,但 queue 未设置 durable 属性,那么当该 queue 的 owner node 出现异常后,在未重建该 queue 前,发往该 queue 的 message 将被 blackholed 。
- 若 message 设置了 persistent 属性,同时 queue 也设置了 durable 属性,那么当 queue 的 owner node 异常且无法重启的情况下,则该 queue 无法在其他 node 上重建,只能等待其 owner node 重启后,才能恢复该 queue 的使用,而在这段时间内发送给该 queue 的 message 将被 blackholed 。
所以,是否要对 message 进行持久化,需要综合考虑性能需要,以及可能遇到的问题。若想达到 100,000 条/秒以上的消息吞吐量(单 RabbitMQ 服务器),则要么使用其他的方式来确保 message 的可靠 delivery ,要么使用非常快速的存储系统以支持全持久化(例如使用 SSD。另外一种处理原则是仅对关键消息作持久化处理根据业务重要程度且应该保证关键消息的量不会导致性能瓶颈。
🦅 **RabbitMQ 允许发送的 message 最大可达多大?**
根据 AMQP 协议规定,消息体的大小由 64-bit 的值来指定,所以你就可以知道到底能发多大的数据了。
🦅 **为什么说保证 message 被可靠持久化的条件是 queue 和 exchange 具有 durable 属性,同时 message 具有 persistent 属性才行?**
binding 关系可以表示为 exchangebindingqueue 。从文档中我们知道,若要求投递的 message 能够不丢失,要求 message 本身设置 persistent 属性,同时要求 exchange 和 queue 都设置 durable 属性。
- 其实这问题可以这么想,若 exchange 或 queue 未设置 durable 属性,则在其 crash 之后就会无法恢复,那么即使 message 设置了 persistent 属性,仍然存在 message 虽然能恢复但却无处容身的问题。
- 同理,若 message 本身未设置 persistent 属性,则 message 的持久化更无从谈起。
🦅 **如何确保消息不丢失?**
消息持久化的前提是:将交换器/队列的 durable 属性设置为 true ,表示交换器/队列是持久交换器/队列,在服务器崩溃或重启之后不需要重新创建交换器/队列(交换器/队列会自动创建)。
如果消息想要从 RabbitMQ 崩溃中恢复,那么消息必须:
- 在消息发布前,通过把它的 “投递模式” 选项设置为2持久来把消息标记成持久化
- 将消息发送到持久交换器
- 消息到达持久队列
RabbitMQ 确保持久性消息能从服务器重启中恢复的方式是,将它们写入磁盘上的一个持久化日志文件。
- 当发布一条持久性消息到持久交换器上时RabbitMQ 会在消息提交到日志文件后才发送响应(如果消息路由到了非持久队列,它会自动从持久化日志中移除)。
- 一旦消费者从持久队列中消费了一条持久化消息RabbitMQ 会在持久化日志中把这条消息标记为等待垃圾收集。如果持久化消息在被消费之前 RabbitMQ 重启,那么 RabbitMQ 会自动重建交换器和队列(以及绑定),并重播持久化日志文件中的消息到合适的队列或者交换器上。
## 什么是死信队列?
DLXDead-Letter-Exchange。利用 DLX 当消息在一个队列中变成死信dead message之后它能被重新 publish 到另一个 Exchange ,这个 Exchange 就是DLX。消息变成死信一向有一下几种情况
- 消息被拒绝basic.reject / basic.nack并且 `requeue=false`
- 消息 TTL 过期参考RabbitMQ之TTLTime-To-Live 过期时间))。
- 队列达到最大长度。
详细的,可以看看 [《RabbitMQ 之死信队列》](http://www.iocoder.cn/RabbitMQ/dead-letter-queue/?vip) 文章。
🦅 **“dead letter”queue 的用途?**
当消息被 RabbitMQ server 投递到 consumer 后,但 consumer 却通过 `Basic.Reject` 进行了拒绝时(同时设置 `requeue=false`),那么该消息会被放入 “dead letter” queue 中。该 queue 可用于排查 message 被 reject 或 undeliver 的原因。
🦅 **`Basic.Reject` 的用法是什么?**
该信令可用于 consumer 对收到的 message 进行 reject 。
- 若在该信令中设置 `requeue=true` ,则当 RabbitMQ server 收到该拒绝信令后,会将该 message 重新发送到下一个处于 consume 状态的 consumer 处(理论上仍可能将该消息发送给当前 consumer
- 若设置 `requeue=false` ,则 RabbitMQ server 在收到拒绝信令后,将直接将该 message 从 queue 中移除。
另外一种移除 queue 中 message 的小技巧是consumer 回复 `Basic.Ack` 但不对获取到的 message 做任何处理。而 `Basic.Nack`是对 Basic.Reject 的扩展,以支持一次拒绝多条 message 的能力。
## RabbitMQ 中的 cluster、mirrored queue以及 warrens 机制分别用于解决什么问题?
🦅 **1cluster**
- cluster 是为了解决当 cluster 中的任意 node 失效后producer 和 consumer 均可以通过其他 node 继续工作,即提高了可用性;另外可以通过增加 node 数量增加 cluster 的消息吞吐量的目的。
- cluster 本身不负责 message 的可靠性问题(该问题由 producer 通过各种机制自行解决cluster 无法解决跨数据中心的问题(即脑裂问题)。
- 另外在cluster 前使用 HAProxy 可以解决 node 的选择问题,即业务无需知道 cluster 中多个 node 的 ip 地址。可以利用 HAProxy 进行失效 node 的探测,可以作负载均衡。下图为 HAProxy + cluster 的模型:[HAProxy + cluster 的模型](https://img-blog.csdnimg.cn/20181219191433895)
🦅 **2Mirrored queue**
- Mirrored queue 是为了解决使用 cluster 时所创建的 queue 的完整信息仅存在于单一 node 上的问题,从另一个角度增加可用性。
- 若想正确使用该功能需要保证1consumer 需要支持 Consumer Cancellation Notification 机制2consumer 必须能够正确处理重复 message 。
🦅 **3Warrens**
Warrens 是为了解决 cluster 中 message 可能被 blackholed 的问题,即不能接受 producer 不停 republish message 但 RabbitMQ server 无回应的情况。
Warrens 有两种构成方式:
- 一种模型,是两台独立的 RabbitMQ server + HAProxy ,其中两个 server 的状态分别为 active 和 hot-standby 。该模型的特点为:两台 server 之间无任何数据共享和协议交互,两台 server 可以基于不同的 RabbitMQ 版本。如下图所示:[模型一](https://img-blog.csdnimg.cn/20181219191433914)
- 另一种模型,为两台共享存储的 RabbitMQ server + keepalived ,其中两个 server 的状态分别为 active 和 cold-standby 。该模型的特点为:两台 server 基于共享存储可以做到完全恢复,要求必须基于完全相同的 RabbitMQ 版本。如下图所示:[模型二](https://img-blog.csdnimg.cn/20181219191433931)
Warrens 模型存在的问题:
- 对于第一种模型,虽然理论上讲不会丢失消息,但若在该模型上使用持久化机制,就会出现这样一种情况,即若作为 active 的 server 异常后,持久化在该 server 上的消息将暂时无法被 consume ,因为此时该 queue 将无法在作为 hot-standby 的 server 上被重建,所以,只能等到异常的 active server 恢复后,才能从其上的 queue 中获取相应的 message 进行处理。而对于业务来说需要具有a.感知 AMQP 连接断开后重建各种 fabric 的能力b.感知 active server 恢复的能力c.切换回 active server 的时机控制,以及切回后,针对 message 先后顺序产生的变化进行处理的能力。
- 对于第二种模型,因为是基于共享存储的模式,所以导致 active server 异常的条件,可能同样会导致 cold-standby server 异常;另外,在该模型下,要求 active 和 cold-standby 的 server 必须具有相同的 node 名和 UID ,否则将产生访问权限问题;最后,由于该模型是冷备方案,故无法保证 cold-standby server 能在你要求的时限内成功启动。
## RabbitMQ 如何实现高可用?
> 这个问题,和 [「RabbitMQ 中的 cluster、mirrored queue以及 warrens 机制分别用于解决什么问题?」](https://svip.iocoder.cn/RabbitMQ/Interview/#) 会比较类似。
RabbitMQ 的高可用,是基于**主从**做高可用性的。它有三种模式:
- 单机模式
- 普通集群模式
- 镜像集群模式
🦅 **1单机模式**
单机模式,就是启动单个 RabbitMQ 节点,一般用于本地开发或者测试环境。实际生产环境下,基本不会使用。
🦅 **普通集群模式(无高可用性)**
> 这种方式,就是上面问题的 **cluster** 。
普通集群模式,意思就是在多台机器上启动多个 RabbitMQ 实例,每个机器启动一个。
- 你**创建的 queue只会放在一个 RabbitMQ 实例上**,但是每个实例都同步 queue 的元数据(元数据可以认为是 queue 的一些配置信息,通过元数据,可以找到 queue 所在实例)。
- 你消费的时候,实际上如果连接到了另外一个实例,那么那个实例会从 queue 所在实例上拉取数据过来。
[![架构图](10-RabbitMQ 面试题.assets/75d36ed17c91932e28b5eeba681ad8ec.png)](http://static.iocoder.cn/75d36ed17c91932e28b5eeba681ad8ec)架构图
这种方式确实很麻烦,也不怎么好,**没做到所谓的分布式**,就是个普通集群。因为这导致你要么消费者每次随机连接一个实例然后拉取数据,要么固定连接那个 queue 所在实例消费数据,前者有**数据拉取的开销**,后者导致**单实例性能瓶颈**。
而且如果那个放 queue 的实例宕机了,会导致接下来其他实例就无法从那个实例拉取,如果你**开启了消息持久化**,让 RabbitMQ 落地存储消息的话,**消息不一定会丢**,得等这个实例恢复了,然后才可以继续从这个 queue 拉取数据。
所以这个事儿就比较尴尬了,这就**没有什么所谓的高可用性****这方案主要是提高吞吐量的**,就是说让集群中多个节点来服务某个 queue 的读写操作。
🦅 **镜像集群模式(高可用性)**
> 艿艿:请教了下胖友,他们采用这种方式。
>
> 这种方式,就是上面问题的 **Mirrored queue** 。
这种模式,才是所谓的 RabbitMQ 的高可用模式。跟普通集群模式不一样的是,在镜像集群模式下,你创建的 queue无论元数据还是 queue 里的消息都会**存在于多个实例上**,就是说,每个 RabbitMQ 节点都有这个 queue 的一个**完整镜像**,包含 queue 的全部数据的意思。然后每次你写消息到 queue 的时候,都会自动把**消息同步**到多个实例的 queue 上。
[![架构图](10-RabbitMQ 面试题.assets/20a6c4d82a08becf7e8d913662b00357.png)](http://static.iocoder.cn/20a6c4d82a08becf7e8d913662b00357)架构图
那么**如何开启这个镜像集群模式**呢其实很简单RabbitMQ 有很好的管理控制台,就是在后台新增一个策略,这个策略是**镜像集群模式的策略**,指定的时候是可以要求数据同步到所有节点的,也可以要求同步到指定数量的节点,再次创建 queue 的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。
这样的话,好处在于,你任何一个机器宕机了,没事儿,其它机器(节点)还包含了这个 queue 的完整数据,别的 consumer 都可以到其它节点上去消费数据。坏处在于,第一,这个性能开销也太大了吧,消息需要同步到所有机器上,导致网络带宽压力和消耗很重!第二,这么玩儿,不是分布式的,就**没有扩展性可言**了,如果某个 queue 负载很重,你加机器,新增的机器也包含了这个 queue 的所有数据,并**没有办法线性扩展**你的 queue。你想如果这个 queue 的数据量很大,大到这个机器上的容量无法容纳了,此时该怎么办呢?
## 如何使用 RabbitMQ 实现 RPC
基于 [RabbitMQ reply_to](https://www.rabbitmq.com/direct-reply-to.html) 特性,可以很轻易使用 RabbitMQ 实现 RPC 功能。
具体的代码实现,可以看看 [《芋道 Spring Boot 消息队列 RabbitMQ 入门》](http://www.iocoder.cn/Spring-Boot/RabbitMQ/?vip)的[「15. RPC 远程调用」](https://svip.iocoder.cn/RabbitMQ/Interview/#) 小节。
🦅 **使用 RabbitMQ 实现 RPC 有什么好处?**
- 1、将客户端和服务器解耦客户端只是发布一个请求到 MQ 并消费这个请求的响应。并不关心具体由谁来处理这个请求MQ 另一端的请求的消费者可以随意替换成任何可以处理请求的服务器,并不影响到客户端。
> 相当于 RPC 的注册发现功能,交给 RabbitMQ 来实现了。
- 2、减轻服务器的压力传统的 RPC 模式中如果客户端和请求过多,服务器的压力会过大。由 MQ 作为中间件的话,过多的请求而是被 MQ 消化掉,服务器可以控制消费请求的频次,并不会影响到服务器。
- 3、服务器的横向扩展更加容易如果服务器的处理能力不能满足请求的频次只需要增加服务器来消费 MQ 的消息即可MQ会帮我们实现消息消费的负载均衡。
- 4、可以看出 RabbitMQ 对于 RPC 模式的支持也是比较友好地,
```
amq.rabbitmq.reply-to
```
,
```
reply_to
```
,
```
correlation_id
```
这些特性都说明了这一点,再加上
spring-rabbit
的实现,可以让我们很简单的使用消息队列模式的 RPC 调用。
> 例如说:[`rabbitmq-jsonrpc`](https://github.com/rabbitmq/rabbitmq-jsonrpc) 的实现。
当然,虽然有这些优点,实际场景下,我们并不会这么做。😈
🦅 **为什么 heavy RPC 的使用场景下不建议采用 disk node **
heavy RPC 是指在业务逻辑中高频调用 RabbitMQ 提供的 RPC 机制,导致不断创建、销毁 reply queue ,进而造成 disk node 的性能问题(因为会针对元数据不断写盘)。所以在使用 RPC 机制时需要考虑自身的业务场景,一般来说不建议。
## RabbitMQ 是否会弄丢数据?
> 艿艿:这个问题,基本是我们前面看到的几个问题的总结合并。
[![弄丢消息的几种情况](10-RabbitMQ 面试题.assets/6303e69011255831c54d605250a6aa67.png)](http://static.iocoder.cn/6303e69011255831c54d605250a6aa67)弄丢消息的几种情况
🦅 **生产者弄丢了数据?**
生产者将数据发送到 RabbitMQ 的时候,可能数据就在半路给搞丢了,因为网络问题啥的,都有可能。
> 方案一:事务功能
🚀 此时可以选择用 RabbitMQ 提供的【事务功能】,就是生产者**发送数据之前**开启 RabbitMQ 事务`channel.txSelect`,然后发送消息,如果消息没有成功被 RabbitMQ 接收到,那么生产者会收到异常报错,此时就可以回滚事务`channel.txRollback`,然后重试发送消息;如果收到了消息,那么可以提交事务`channel.txCommit`。代码如下:
```
// 开启事务
channel.txSelect
try {
// 这里发送消息
} catch (Exception e) {
channel.txRollback
// 这里再次重发这条消息
}
// 提交事务
channel.txCommit
```
- 但是问题是RabbitMQ 事务机制(同步)一搞,基本上**吞吐量会下来,因为太耗性能**。
具体的代码实现,可以看看 [《芋道 Spring Boot 消息队列 RabbitMQ 入门》](http://www.iocoder.cn/Spring-Boot/RabbitMQ/?vip)的[「12. 事务消息」](https://svip.iocoder.cn/RabbitMQ/Interview/#) 小节。
> 方案二confirm 功能。
🚀 所以一般来说,如果你要确保说写 RabbitMQ 的消息别丢,可以开启 【`confirm` 模式】,在生产者那里设置开启 `confirm` 模式之后,你每次写的消息都会分配一个唯一的 id然后如果写入了 RabbitMQ 中RabbitMQ 会给你回传一个 `ack` 消息,告诉你说这个消息 ok 了。如果 RabbitMQ 没能处理这个消息,会回调你的一个 `nack` 接口,告诉你这个消息接收失败,你可以重试。而且你可以结合这个机制自己在内存里维护每个消息 id 的状态,如果超过一定时间还没接收到这个消息的回调,那么你可以重发。
具体的代码实现,可以看看 [《芋道 Spring Boot 消息队列 RabbitMQ 入门》](http://www.iocoder.cn/Spring-Boot/RabbitMQ/?vip)的[「14. 生产者的发送确认」](https://svip.iocoder.cn/RabbitMQ/Interview/#) 小节。
> 对比总结
🚀 事务机制和 `confirm` 机制最大的不同在于,**事务机制是同步的**,你提交一个事务之后会**阻塞**在那儿,但是 `confirm` 机制是**异步**的,你发送个消息之后就可以发送下一个消息,然后那个消息 RabbitMQ 接收了之后会异步回调你的一个接口通知你这个消息接收到了。
所以一般在生产者这块**避免数据丢失**,都是用 `confirm` 机制的。
> 😈 不过 confirm 功能,也可能存在丢消息的情况。举个例子,如果回调到 `nack` 接口,此时 JVM 挂掉了,那么此消息就丢失了。(这个是艿艿的猜想,还在找胖友探讨中。关于这块,欢迎星球讨论。)
🦅 **Broker 弄丢了数据**
就是 Broker 自己弄丢了数据,这个你必须**开启 Broker 的持久化**,就是消息写入之后会持久化到磁盘,哪怕是 Broker 自己挂了,**恢复之后会自动读取之前存储的数据**一般数据不会丢。除非极其罕见的是Broker 还没持久化,自己就挂了,**可能导致少量数据丢失**,但是这个概率较小。
设置持久化有**两个步骤**
- 创建 queue 的时候将其设置为持久化
这样就可以保证 RabbitMQ 持久化 queue 的元数据,但是它是不会持久化 queue 里的数据的。
- 第二个是发送消息的时候将消息的 `deliveryMode` 设置为 2
就是将消息设置为持久化的,此时 RabbitMQ 就会将消息持久化到磁盘上去。
必须要同时设置这两个持久化才行Broker 哪怕是挂了,再次重启,也会从磁盘上重启恢复 queue恢复这个 queue 里的数据。
注意,哪怕是你给 Broker 开启了持久化机制,也有一种可能,就是这个消息写到了 Broker 中,但是还没来得及持久化到磁盘上,结果不巧,此时 Broker 挂了,就会导致内存里的一点点数据丢失。
所以,持久化可以跟生产者那边的 `confirm` 机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者 `ack` 了所以哪怕是在持久化到磁盘之前Broker 挂了,数据丢了,生产者收不到 `ack`,你也是可以自己重发的。
🦅 **消费端弄丢了数据?**
RabbitMQ 如果丢失了数据,主要是因为你消费的时候,**刚消费到,还没处理,结果进程挂了**比如重启了那么就尴尬了RabbitMQ 认为你都消费了,这数据就丢了。
这个时候得用 RabbitMQ 提供的 `ack` 机制,简单来说,就是你必须关闭 RabbitMQ 的自动 `ack`,可以通过一个 api 来调用就行,然后每次你自己代码里确保处理完的时候,再在程序里 `ack` 一把。这样的话,如果你还没处理完,不就没有 `ack` 了?那 RabbitMQ 就认为你还没处理完,这个时候 RabbitMQ 会把这个消费分配给别的 consumer 去处理,消息是不会丢的。
🦅 **总结**
[![总结](10-RabbitMQ 面试题.assets/83213e2ac79cd8899b09a66a5cf71669.png)](http://static.iocoder.cn/83213e2ac79cd8899b09a66a5cf71669)总结
## RabbitMQ 如何保证消息的顺序性?
和 Kafka 与 RocketMQ 不同Kafka 不存在类似类似 Topic 的概念,而是真正的一条一条队列,并且每个队列可以被多个 Consumer 拉取消息。这个,是非常大的一个差异。
🚀 **来看看 RabbitMQ 顺序错乱的场景**
一个 queue多个 consumer。比如生产者向 RabbitMQ 里发送了三条数据,顺序依次是 data1/data2/data3压入的是 RabbitMQ 的一个内存队列。有三个消费者分别从 MQ 中消费这三条数据中的一条,结果消费者 2 先执行完操作,把 data2 存入数据库,然后是 data1/data3。这不明显乱了。😈 也就是说,乱序消费的问题。
[![乱序](10-RabbitMQ 面试题.assets/29ea655479f826f9a6a5d9d005f46c7c.png)](http://static.iocoder.cn/29ea655479f826f9a6a5d9d005f46c7c)乱序
🚀 **解决方案**
- 方案一,拆分多个 queue每个 queue 一个 consumer就是多一些 queue 而已,确实是麻烦点。
> 这个方式,有点模仿 Kafka 和 RocketMQ 中 Topic 的概念。例如说,原先一个 queue 叫 `"xxx"` ,那么多个 queue ,我们可以叫 `"xxx-01"`、`"xxx-02"` 等,相同前缀,不同后缀。
- 方案二,或者就一个 queue 但是对应一个 consumer然后这个 consumer 内部用内存队列做排队,然后分发给底层不同的 worker 来处理。
> 这种方式,就是讲一个 queue 里的相同的“key” 交给同一个 worker 来执行。因为 RabbitMQ 是可以单条消息来 ack ,所以还是比较方便的。这一点,也是和 RocketMQ 和 Kafka 不同的地方。
[![解决乱序](10-RabbitMQ 面试题.assets/f1bd6c79deaa6fae89a62d0e53f1ad43.png)](http://static.iocoder.cn/f1bd6c79deaa6fae89a62d0e53f1ad43)解决乱序
实际上,我们会发现上述的两个方案,前提都是一个 queue 只能启动一个 consumer 对应。
具体的代码实现,可以看看 [《芋道 Spring Boot 消息队列 RabbitMQ 入门》](http://www.iocoder.cn/Spring-Boot/RabbitMQ/?vip)的[「11. 顺序消息」](https://svip.iocoder.cn/RabbitMQ/Interview/#) 小节
# 666. 彩蛋
写的很糟糕,一度想删除。后来想想,先就酱紫,可能这就是此时自己对 RabbitMQ 掌握的情况。后面等自己业务场景真的开始使用 RabbitMQ 之后在好好倒腾倒腾。Sad But Tree 。
参考与推荐如下文章:
- [《RabbitMQ 面试专题》](https://blog.csdn.net/qq_30764991/article/details/80573352)
- [《如何保证消息队列的高可用?》](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/how-to-ensure-high-availability-of-message-queues.md)
- [《RabbitMQ 面试要点》](https://chaser520.iteye.com/blog/2428253)
- [《如何保证消息的可靠性传输?(如何处理消息丢失的问题)》](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/how-to-ensure-the-reliable-transmission-of-messages.md)

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

View File

@@ -0,0 +1,589 @@
# 精尽 Kafka 面试题「最新更新时间2024-02」
以下面试题,基于网络整理,和自己编辑。具体参考的文章,会在文末给出所有的链接。
如果胖友有自己的疑问,欢迎在星球提问,我们一起整理吊吊的 Kafka 面试题的大保健。
而题目的难度,艿艿尽量按照从容易到困难的顺序,逐步下去。
另外,这个面试题是建立在胖友看过 [《精尽【消息队列 】面试题》](http://svip.iocoder.cn/MQ/Interview) 。
如果可能的话,推荐胖友先阅读了 [《Kafka 权威指南》](https://u.jd.com/jJpbF8) ,更加系统可靠。
## Apache Kafka 是什么?
Kafka 是基于**发布与订阅**的**消息系统**。它最初由 LinkedIn 公司开发,之后成为 Apache 项目的一部分。Kafka 是一个分布式的,可分区的,冗余备份的持久性的日志服务。它主要用于处理活跃的流式数据。
在大数据系统中常常会碰到一个问题整个大数据是由各个子系统组成数据需要在各个子系统中高性能、低延迟的不停流转。传统的企业消息系统并不是非常适合大规模的数据处理。为了同时搞定在线应用消息和离线应用数据文件、日志Kafka 就出现了。Kafka 可以起到两个作用:
- 降低系统组网复杂度。
- 降低编程复杂度各个子系统不在是相互协商接口各个子系统类似插口插在插座上Kafka 承担高速**数据总线**的作用。
🦅 **Kafka 的主要特点?**
- 1、同时为发布和订阅提供高吞吐量。据了解Kafka 每秒可以生产约 25 万消息50MB每秒处理 55 万消息110MB
- 2、可进行持久化操作。将消息持久化到磁盘因此可用于批量消费例如 ETL 以及实时应用程序。通过将数据持久化到硬盘以及replication ,可以防止数据丢失。
- 3、分布式系统易于向外扩展。所有的 Producer、Broker 和Consumer 都会有多个,均为分布式的。并且,无需停机即可扩展机器。
- 4、消息被处理的状态是在 Consumer 端维护,而不是由 Broker 端维护。当失败时,能自动平衡。
> 这段是从网络上找来的。感觉想要表达的意思是
>
> - 消息是否被处理完成,是通过 Consumer 提交消费进度给 Broker ,而不是 Broker 消息被 Consumer 拉取后,就标记为已消费。
> - 当 Consumer 异常崩溃时,可以重新分配消息分区到其它的 Consumer 们,然后继续消费。
- 5、支持 online 和 offline 的场景。
🦅 **聊聊 Kafka 的设计要点?**
1吞吐量
高吞吐是 Kafka 需要实现的核心目标之一,为此 kafka 做了以下一些设计:
- 1、数据磁盘持久化消息不在内存中 Cache ,直接写入到磁盘,充分利用磁盘的顺序读写性能。
> 直接使用 Linux 文件系统的 Cache ,来高效缓存数据。
- 2、zero-copy减少 IO 操作步骤
> 采用 Linux Zero-Copy 提高发送性能。
>
> - 传统的数据发送需要发送 4 次上下文切换。
> - 采用 sendfile 系统调用之后,数据直接在内核态交换,系统上下文切换减少为 2 次。根据测试结果,可以提高 60% 的数据发送性能。Zero-Copy 详细的技术细节可以参考 [《Efficient data transfer through zero copy》](https://developer.ibm.com/articles/j-zerocopy/) 文章。
- 3、数据批量发送
- 4、数据压缩
- 5、Topic 划分为多个 Partition ,提高并行度。
> 数据在磁盘上存取代价为 `O(1)`。
>
> - Kafka 以 Topic 来进行消息管理,每个 Topic 包含多个 Partition ,每个 Partition 对应一个逻辑 log ,有多个 segment 文件组成。
> - 每个 segment 中存储多条消息(见下图),消息 id 由其逻辑位置决定,即从消息 id 可直接定位到消息的存储位置,避免 id 到位置的额外映射。
> - 每个 Partition 在内存中对应一个 index ,记录每个 segment 中的第一条消息偏移。
>
> 发布者发到某个 Topic 的消息会被均匀的分布到多个 Partition 上随机或根据用户指定的回调函数进行分布Broker 收到发布消息往对应 Partition 的最后一个 segment 上添加该消息。
> 当某个 segment上 的消息条数达到配置值或消息发布时间超过阈值时segment上 的消息会被 flush 到磁盘,只有 flush 到磁盘上的消息订阅者才能订阅到segment 达到一定的大小后将不会再往该 segment 写数据Broker 会创建新的 segment 文件。
2负载均衡
- 1、Producer 根据用户指定的算法,将消息发送到指定的 Partition 中。
- 2、Topic 存在多个 Partition ,每个 Partition 有自己的replica ,每个 replica 分布在不同的 Broker 节点上。多个Partition 需要选取出 Leader partition Leader Partition 负责读写,并由 Zookeeper 负责 fail over 。
- 3、相同 Topic 的多个 Partition 会分配给不同的 Consumer 进行拉取消息,进行消费。
3拉取系统
由于 Kafka Broker 会持久化数据Broker 没有内存压力,因此, Consumer 非常适合采取 pull 的方式消费数据,具有以下几点好处:
- 1、简化 Kafka 设计。
- 2、Consumer 根据消费能力自主控制消息拉取速度。
- 3、Consumer 根据自身情况自主选择消费模式,例如批量,重复消费,从尾端开始消费等。
4可扩展性
> 通过 Zookeeper 管理 Broker 与 Consumer 的动态加入与离开。
- 当需要增加 Broker 节点时,新增的 Broker 会向 Zookeeper 注册,而 Producer 及 Consumer 会根据注册在 Zookeeper 上的 watcher 感知这些变化,并及时作出调整。
- 当新增和删除 Consumer 节点时,相同 Topic 的多个 Partition 会分配给剩余的 Consumer 们。
另外,推荐阅读 [《为什么 Kafka 这么快?》](https://mp.weixin.qq.com/s/pzVS7r3QaQPFwob-fY8b4A) 文章,写的更加细致。
## Kafka 的架构是怎么样的?
[![Kafka 架构图](11-Kafka 面试题.assets/ac883ce247c1ff31c7cd4244392dcaed.png)](http://static.iocoder.cn/ac883ce247c1ff31c7cd4244392dcaed)Kafka 架构图
Kafka 的整体架构非常简单是分布式架构Producer、Broker 和Consumer 都可以有多个。
- ProducerConsumer 实现 Kafka 注册的接口。
- 数据从 Producer 发送到 Broker 中Broker 承担一个中间缓存和分发的作用。
- Broker 分发注册到系统中的 Consumer。Broker 的作用类似于缓存,即活跃的数据和离线处理系统之间的缓存。
- 客户端和服务器端的通信,是基于简单,高性能,且与编程语言无关的 TCP 协议。
几个重要的基本概念:
- Topic特指 Kafka 处理的消息源feeds of messages的不同分类。
- PartitionTopic 物理上的分组(分区),一个 Topic 可以分为多个 Partition 。每个 Partition 都是一个有序的队列。Partition 中的每条消息都会被分配一个有序的 idoffset
> - replicasPartition 的副本集,保障 Partition 的高可用。
> - leaderreplicas 中的一个角色Producer 和 Consumer 只跟 Leader 交互。
> - followerreplicas 中的一个角色,从 leader 中复制数据,作为副本,一旦 leader 挂掉,会从它的 followers 中选举出一个新的 leader 继续提供服务。
- Message消息是通信的基本单位每个 Producer 可以向一个Topic主题发布一些消息。
- Producers消息和数据生产者向 Kafka 的一个 Topic 发布消息的过程,叫做 producers 。
- Consumers消息和数据消费者订阅 Topic ,并处理其发布的消息的过程,叫做 consumers 。
> Consumer group每个 Consumer 都属于一个 Consumer group每条消息只能被 Consumer group 中的一个 Consumer 消费,但可以被多个 Consumer group 消费。
- Broker缓存代理Kafka 集群中的一台或多台服务器统称为 broker 。
> ControllerKafka 集群中,通过 Zookeeper 选举某个 Broker 作为 Controller ,用来进行 leader election 以及 各种 failover 。
- ZooKeeperKafka 通过 ZooKeeper 来存储集群的 Topic、Partition 等元信息等。
😈 单纯角色来说Kafka 和 RocketMQ 是基本一致的。比较明显的差异是:
> RocketMQ 从 Kafka 演化而来。
- 1、Kafka 使用 Zookeeper 作为命名服务RocketMQ 自己实现了一个轻量级的 Namesrv 。
- 2、Kafka Broker 的每个分区都有一个首领分区RocketMQ 每个分区的“首领”分区,都在 Broker Master 节点上。
> RocketMQ 没有首领分区一说,所以打上了引号。
- 3、Kafka Consumer 使用 poll 的方式拉取消息RocketMQ Consumer 提供 poll 的方式的同时,封装了一个 push 的方式。
> RocketMQ 的 push 的方式,也是基于 poll 的方式的封装。
- … 当然还有其它 …
🦅 **Kafka 为什么要将 Topic 进行分区?**
正如我们在 [「聊聊 Kafka 的设计要点?」](https://svip.iocoder.cn/Kafka/Interview/#) 问题中所看到的,是为了负载均衡,从而能够水平拓展。
- Topic 只是逻辑概念,面向的是 Producer 和 Consumer ,而 Partition 则是物理概念。如果 Topic 不进行分区,而将 Topic 内的消息存储于一个 Broker那么关于该 Topic 的所有读写请求都将由这一个 Broker 处理,吞吐量很容易陷入瓶颈,这显然是不符合高吞吐量应用场景的。
- 有了 Partition 概念以后,假设一个 Topic 被分为 10 个 Partitions Kafka 会根据一定的算法将 10 个 Partition 尽可能均匀的分布到不同的 Broker服务器上。
- 当 Producer 发布消息时Producer 客户端可以采用 random、key-hash 及轮询等算法选定目标 Partition 若不指定Kafka 也将根据一定算法将其置于某一分区上。
- 当 Consumer 拉取消息时Consumer 客户端可以采用 Range、轮询 等算法分配 Partition ,从而从不同的 Broker 拉取对应的 Partition 的 leader 分区。
所以Partiton 机制可以极大的提高吞吐量,并且使得系统具备良好的水平扩展能力。
## Kafka 的应用场景有哪些?
[![Kafka 的应用场景](11-Kafka 面试题.assets/3636ff4bd554ee1dfcfb92448073b5b8.png)](http://static.iocoder.cn/3636ff4bd554ee1dfcfb92448073b5b8)Kafka 的应用场景
1消息队列
比起大多数的消息系统来说Kafka 有更好的吞吐量,内置的分区,冗余及容错性,这让 Kafka 成为了一个很好的大规模消息处理应用的解决方案。消息系统一般吞吐量相对较低,但是需要更小的端到端延时,并常常依赖于 Kafka 提供的强大的持久性保障。在这个领域Kafka 足以媲美传统消息系统,如 ActiveMQ 或 RabbitMQ 。
2行为跟踪
Kafka 的另一个应用场景,是跟踪用户浏览页面、搜索及其他行为,以发布订阅的模式实时记录到对应的 Topic 里。那么这些结果被订阅者拿到后,就可以做进一步的实时处理,或实时监控,或放到 Hadoop / 离线数据仓库里处理。
3元信息监控
作为操作记录的监控模块来使用,即汇集记录一些操作信息,可以理解为运维性质的数据监控吧。
4日志收集
日志收集方面,其实开源产品有很多,包括 Scribe、Apache Flume 。很多人使用 Kafka 代替日志聚合log aggregation。日志聚合一般来说是从服务器上收集日志文件然后放到一个集中的位置文件服务器或 HDFS进行处理。
然而, Kafka 忽略掉文件的细节,将其更清晰地抽象成一个个日志或事件的消息流。这就让 Kafka 处理过程延迟更低,更容易支持多数据源和分布式数据处理。比起以日志为中心的系统比如 Scribe 或者 Flume 来说Kafka 提供同样高效的性能和因为复制导致的更高的耐用性保证,以及更低的端到端延迟。
5流处理
这个场景可能比较多,也很好理解。保存收集流数据,以提供之后对接的 Storm 或其他流式计算框架进行处理。很多用户会将那些从原始 Topic 来的数据进行阶段性处理,汇总,扩充或者以其他的方式转换到新的 Topic 下再继续后面的处理。
例如一个文章推荐的处理流程,可能是先从 RSS 数据源中抓取文章的内容,然后将其丢入一个叫做“文章”的 Topic 中。后续操作可能是需要对这个内容进行清理,比如回复正常数据或者删除重复数据,最后再将内容匹配的结果返还给用户。这就在一个独立的 Topic 之外产生了一系列的实时数据处理的流程。Strom 和 Samza 是非常著名的实现这种类型数据转换的框架。
6事件源
事件源是一种应用程序设计的方式。该方式的状态转移被记录为按时间顺序排序的记录序列。Kafka 可以存储大量的日志数据这使得它成为一个对这种方式的应用来说绝佳的后台。比如动态汇总News feed
7持久性日志Commit Log
Kafka 可以为一种外部的持久性日志的分布式系统提供服务。这种日志可以在节点间备份数据并为故障节点数据回复提供一种重新同步的机制。Kafka 中日志压缩功能为这种用法提供了条件。在这种用法中Kafka 类似于 Apache BookKeeper 项目。
## Kafka 消息发送和消费的简化流程是什么?
[![Kafka 消息发送和消费](11-Kafka 面试题.assets/456f3adab70714c0a1b1fdbd8a686732.png)](http://static.iocoder.cn/456f3adab70714c0a1b1fdbd8a686732)Kafka 消息发送和消费
- 1、Producer ,根据指定的 partition 方法round-robin、hash等将消息发布到指定 Topic 的 Partition 里面。
- 2、Kafka 集群,接收到 Producer 发过来的消息后,将其持久化到硬盘,并保留消息指定时长(可配置),而不关注消息是否被消费。
- 3、Consumer ,从 Kafka 集群 pull 数据,并控制获取消息的 offset 。至于消费的进度,可手动或者自动提交给 Kafka 集群。
🦅 **1Producer 发送消息**
Producer 采用 push 模式将消息发布到 Broker每条消息都被 append 到 Patition 中,属于顺序写磁盘(顺序写磁盘效率比随机写内存要高,保障 Kafka 吞吐率。Producer 发送消息到 Broker 时,会根据分区算法选择将其存储到哪一个 Partition 。
其路由机制为:
- 1、指定了 Partition ,则直接使用。
- 2、未指定 Partition 但指定 key ,通过对 key 进行 hash 选出一个 Partition 。
- 3、Partition 和 key 都未指定,使用轮询选出一个 Partition 。
写入流程:
- 1、Producer 先从 ZooKeeper 的 `"/brokers/.../state"` 节点找到该 Partition 的 leader 。
> 注意噢Producer 只和 Partition 的 leader 进行交互。
- 2、Producer 将消息发送给该 leader 。
- 3、leader 将消息写入本地 log 。
- 4、followers 从 leader pull 消息,写入本地 log 后 leader 发送 ACK 。
- 5、leader 收到所有 ISR 中的 replica 的 ACK 后,增加 HWhigh watermark ,最后 commit 的 offset 并向 Producer 发送 ACK 。
🦅 **2Broker 存储消息**
物理上把 Topic 分成一个或多个 Patition每个 Patition 物理上对应一个文件夹(该文件夹存储该 Patition 的所有消息和索引文件)。
🦅 **3Consumer 消费消息**
high-level Consumer API 提供了 consumer group 的语义,一个消息只能被 group 内的一个 Consumer 所消费,且 Consumer 消费消息时不关注 offset ,最后一个 offset 由 ZooKeeper 保存(下次消费时,该 group 中的 Consumer 将从 offset 记录的位置开始消费)。
注意:
- 1、如果消费线程大于 Patition 数量,则有些线程将收不到消息。
- 2、如果 Patition 数量大于消费线程数,则有些线程多收到多个 Patition 的消息。
- 3、如果一个线程消费多个 Patition则无法保证你收到的消息的顺序而一个 Patition 内的消息是有序的。
Consumer 采用 pull 模式从 Broker 中读取数据。
- push 模式,很难适应消费速率不同的消费者,因为消息发送速率是由 Broker 决定的。它的目标是尽可能以最快速度传递消息,但是这样很容易造成 Consumer 来不及处理消息,典型的表现就是拒绝服务以及网络拥塞。而 pull 模式,则可以根据 Consumer 的消费能力以适当的速率消费消息。
- 对于 Kafka 而言pull 模式更合适,它可简化 Broker 的设计Consumer 可自主控制消费消息的速率,同时 Consumer 可以自己控制消费方式——即可批量消费也可逐条消费,同时还能选择不同的提交方式从而实现不同的传输语义。
🦅 **Kafka Producer 有哪些发送模式?**
Kafka 的发送模式由 Producer 端的配置参数 `producer.type`来设置。
- 这个参数指定了在后台线程中消息的发送方式是同步的还是异步的,默认是同步的方式,即 `producer.type=sync`
- 如果设置成异步的模式,即 `producer.type=async` ,可以是 Producer 以 batch 的形式 push 数据,这样会极大的提高 Broker的性能但是这样会增加丢失数据的风险。
- 如果需要确保消息的可靠性,必须要将 `producer.type`设置为 sync 。
对于异步模式,还有 4 个配套的参数,如下:
[![参数](11-Kafka 面试题.assets/792a59a9b2b4f271c179e81cf4278322.jpeg)](http://static.iocoder.cn/792a59a9b2b4f271c179e81cf4278322)参数
- 以 batch 的方式推送数据可以极大的提高处理效率Kafka Producer 可以将消息在内存中累计到一定数量后作为一个 batch 发送请求。batch 的数量大小可以通过 Producer 的参数(`batch.num.messages`)控制。通过增加 batch 的大小,可以减少网络请求和磁盘 IO 的次数,当然具体参数设置需要在效率和时效性方面做一个权衡。
- 在比较新的版本中还有
```
batch.size
```
这个参数。Producer 会尝试批量发送属于同一个 Partition 的消息以减少请求的数量. 这样可以提升客户端和服务端的性能。默认大小是 16348 byte (16k).
- 发送到 Broker 的请求可以包含多个 batch ,每个 batch 的数据属于同一个 Partition 。
- 太小的 batch 会降低吞吐. 太大会浪费内存。
具体的代码实现,可以看看 [《芋道 Spring Boot 消息队列 Kafka 入门》](http://www.iocoder.cn/Spring-Boot/RabbitMQ/?vip)的[「3. 快速入门」](https://svip.iocoder.cn/Kafka/Interview/#)和[「4. 批量发送消息」](https://svip.iocoder.cn/Kafka/Interview/#)小节。
🦅 **Kafka Consumer 是否可以消费指定的分区消息?**
Consumer 消费消息时,向 Broker 发出“fetch”请求去消费特定分区的消息Consumer 指定消息在日志中的偏移量(offset)就可以消费从这个位置开始的消息Consumer 拥有了 offset 的控制权,可以向后回滚去重新消费之前的消息,这是很有意义的。
🦅 **Kafka 的 high-level API 和 low-level API 的区别?**
High Level API
- 屏蔽了每个 Topic 的每个 Partition 的 offset 管理自动读取Zookeeper 中该 Consumer group 的 last offset、Broker 失败转移、以及增减 Partition 时 Consumer 时的负载均衡Kafka 自动进行负载均衡)。
- 如果 Consumer 比 Partition 多,是一种浪费。一个 Partition 上是不允许并发的,所以 Consumer 数不要大于 Partition 数。
Low Level API
> Low-level API 也就是 Simple Consumer API ,实际上非常复杂。
- API 控制更灵活,例如消息重复读取,消息 offset 跳读Exactly Once 原语。
- API 更复杂offset 不再透明需要自己管理Broker 自动失败转移需要处理,增加 Consumer、Partition、Broker 需要自己做负载均衡。
## Kafka 的网络模型是怎么样的?
Kafka 基于高吞吐率和效率考虑,并没有使用第三方网络框架,而且自己基于 Java NIO 封装的。
🦅 **1KafkaClient ,单线程 Selector 模型。**
[![KafkaClient](11-Kafka 面试题.assets/00e8ec59cf40c62db53b4d66dc45e17c.jpeg)](http://static.iocoder.cn/00e8ec59cf40c62db53b4d66dc45e17c)KafkaClient
> 实际上,就是 NettyClient 的 NIO 方式。
- 单线程模式适用于并发链接数小,逻辑简单,数据量小。
- 在 Kafka 中Consumer 和 Producer 都是使用的上面的单线程模式。这种模式不适合 Kafka 的服务端,在服务端中请求处理过程比较复杂,会造成线程阻塞,一旦出现后续请求就会无法处理,会造成大量请求超时,引起雪崩。而在服务器中应该充分利用多线程来处理执行逻辑。
🦅 **2KafkaServer ,多线程 Selector 模型。**
> KafkaServer ,指的是 Kafka Broker 。
[![KafkaServer](11-Kafka 面试题.assets/8493bc98876609462fba617d520d1b9a.jpeg)](http://static.iocoder.cn/8493bc98876609462fba617d520d1b9a)KafkaServer
Broker 的内部处理流水线化,分为多个阶段来进行(SEDA),以提高吞吐量和性能,尽量避免 Thead 盲等待,以下为过程说明。
> 实际上,就是 NettyServer 的 NIO 方式。
- Accept Thread 负责与客户端建立连接链路,然后把 Socket 轮转交给Process Thread 。
> 相当于 Netty 的 Boss EventLoop 。
- Process Thread 负责接收请求和响应数据Process Thread 每次基于 Selector 事件循环,首先从 Response Queue 读取响应数据,向客户端回复响应,然后接收到客户端请求后,读取数据放入 Request Queue 。
> 相当于 Netty 的 Worker EventLoop 。
- Work Thread 负责业务逻辑、IO 磁盘处理等,负责从 Request Queue 读取请求,并把处理结果放入 Response Queue 中,待 Process Thread 发送出去。
> 相当于业务线程池。
😈 实际上,艿艿的想法,如果自己实现 MQ ,完全可以直接使用 Netty 作为网络通信框架。包括RocketMQ 就是如此实现的。
🦅 **解释如何提高远程用户的吞吐量?**
如果 Producer、Consumer 位于与 Broker 不同的数据中心,则可能需要调优套接口缓冲区大小,以对长网络延迟进行摊销。
## Kafka 的数据存储模型是怎么样的?
Kafka 每个 Topic 下面的所有消息都是以 Partition 的方式分布式的存储在多个节点上。同时在 Kafka 的机器上,每个 Partition 其实都会对应一个日志目录在目录下面会对应多个日志分段LogSegment
```
MacBook-Pro-5:test-0 yunai$ ls
00000000000000000000.index 00000000000000000000.timeindex leader-epoch-checkpoint
00000000000000000000.log 00000000000000000004.snapshot
```
- Topic 为 `test` Partition 为 0 ,所以文件目录是 `test-0` 。
LogSegment 文件由两部分组成,分别为 `.index` 文件和 `.log` 文件,分别表示为 segment **索引**文件和**数据**文件。这两个文件的命令规则为Partition 全局的第一个 segment 从 0 开始,后续每个 segment 文件名为上一个 segment 文件最后一条消息的 offset 值,数值大小为 64 位20 位数字字符长度,没有数字用 0 填充,如下,假设有 1000 条消息,每个 LogSegment 大小为 100 ,下面展现了 900-1000 的 `.index` 和 `.log` 文件:
[`.index` 和 `.log` 文件](https://user-gold-cdn.xitu.io/2018/7/24/164ca4f7e778be7c?imageView2/0/w/1280/h/960/format/jpeg/ignore-error/1)
- 由于 Kafka 消息数据太大,如果全部建立索引,即占了空间又增加了耗时,所以 Kafka 选择了稀疏索引的方式(通过 `.index` 索引 `.log` 文件),这样的话索引可以直接进入内存,加快偏查询速度。
🦅 **简单介绍一下如何读取数据?**
如果我们要读取第 911 条数据。
- 首先第一步,找到它是属于哪一段的,根据二分法查找到他属于的文件,找到 `0000900.index` 和 `00000900.log` 之后。
- 然后,去 `.index` 中去查找 `(911-900) =11` 这个索引或者小于 11 最近的索引,在这里通过二分法我们找到了索引是 `[10,1367]` 。
> - 10 表示,第 10 条消息开始。
> - 1367 表示,在 `.log` 的第 1367 字节开始。
>
> 😈 所以,本图的第 911 条的“1360”是错的相比“1367” 反倒小了。
- 然后,我们通过这条索引的物理位置 1367 ,开始往后找,直到找到 911 条数据。
上面讲的是如果要找某个 offset 的流程,但是我们大多数时候并不需要查找某个 offset ,只需要按照顺序读即可。而在顺序读中,操作系统会对内存和磁盘之间添加 page cahe ,也就是我们平常见到的预读操作,所以我们的顺序读操作时速度很快。但是 Kafka 有个问题,如果分区过多,那么日志分段也会很多,写的时候由于是批量写,其实就会变成随机写了,随机 I/O 这个时候对性能影响很大。所以一般来说 Kafka 不能有太多的Partition 。针对这一点RocketMQ 把所有的日志都写在一个文件里面,就能变成顺序写,通过一定优化,读也能接近于顺序读。
> 并且,截止到 RocketMQ4 版本,索引文件,对每个数据文件中的消息,都有对应的索引。这个是和 Kafka 的稀疏索引不太一样的地方。
更详尽的,推荐阅读 [《Kafka 之数据存储》](http://matt33.com/2016/03/08/kafka-store/) 文章。
🦅 **为什么不能以 Partition 作为存储单位?**
如果就以 Partition 为最小存储单位,可以想象,当 Kafka Producer 不断发送消息,必然会引起 Partition 文件的无限扩张,将对消息文件的维护以及已消费的消息的清理带来严重的影响,因此,需以 segment 为单位将 Partition 进一步细分。
每个 Partition目录相当于一个巨型文件被平均分配到多个大小相等的 segment数据文件中每个 segment 文件中消息数量不一定相等),这种特性也方便 old segment 的删除,即方便已被消费的消息的清理,提高磁盘的利用率。每个 Partition 只需要支持顺序读写就行segment 的文件生命周期由服务端配置参数(`log.segment.bytes``log.roll.{ms,hours}` 等若干参数)决定。
## Kafka 的消息格式是怎么样的?
message 中的物理结构为:
[![message 物理结构](11-Kafka 面试题.assets/a034ab2df1d5b67520432355e0bbf95b.png)](http://static.iocoder.cn/a034ab2df1d5b67520432355e0bbf95b)message 物理结构
参数说明:
| 关键字 | 解释说明 |
| :------------------ | :----------------------------------------------------------- |
| 8 byte offset | 在parition(分区)内的每条消息都有一个有序的id号这个id号被称为偏移(offset),它可以唯一确定每条消息在parition(分区)内的位置。即offset表示partiion的第多少message |
| 4 byte message size | message大小 |
| 4 byte CRC32 | 用crc32校验message |
| 1 byte “magic” | 表示本次发布Kafka服务程序协议版本号 |
| 1 byte “attributes” | 表示为独立版本、或标识压缩类型、或编码类型 |
| 4 byte key length | 表示key的长度,当key为-1时K byte key字段不填 |
| K byte key | 可选 |
| value bytes payload | 表示实际消息数据 |
不过,这是早期 Kafka 的版本,最新版本的格式,推荐阅读如下两篇文章:
- [《一文看懂 Kafka 消息格式的演变》](http://www.iocoder.cn/Kafka/message-format/?vip)
- [《Kafka 消息格式中的变长字段Varints》](http://www.iocoder.cn/Kafka/varints/)
当然,看懂这个数据格式,基本也能知道消息的大体格式。
## Kafka 的副本机制是怎么样的?
Kafka 的副本机制,是多个 Broker 节点对其他节点的 Topic 分区的日志进行复制。当集群中的某个节点出现故障,访问故障节点的请求会被转移到其他正常节点(这一过程通常叫 Reblance)Kafka 每个主题的每个分区都有一个主副本以及 0 个或者多个副本,副本保持和主副本的数据同步,当主副本出故障时就会被替代。
[![副本机制](11-Kafka 面试题.assets/1f42986437f76c108007e86a51ae1287.jpeg)](http://static.iocoder.cn/1f42986437f76c108007e86a51ae1287)副本机制
> 注意哈,下面说的 Leader 指的是每个 Topic 的某个分区的 Leader ,而不是 Broker 集群中的【集群控制器】。
在 Kafka 中并不是所有的副本都能被拿来替代主副本,所以在 Kafka 的Leader 节点中维护着一个 ISRIn sync Replicas集合翻译过来也叫正在同步中集合在这个集合中的需要满足两个条件:
- 1、节点必须和 Zookeeper 保持连接。
- 2、在同步的过程中这个副本不能落后主副本太多。
另外还有个 ARAssigned Replicas用来标识副本的全集OSR 用来表示由于落后被剔除的副本集合,所以公式如下:
- ISR = Leader + 没有落后太多的副本。
- AR = OSR + ISR 。
这里先要说下两个名词HW 和 LEO 。
- HW高水位 HighWatermark是 Consumer 能够看到的此 Partition 的位置。
- LEOlogEndOffset是每个 Partition 的 log 最后一条 Message 的位置。
- HW 能保证 Leader 所在的 Broker 失效该消息仍然可以从新选举的Leader 中获取,不会造成消息丢失。
当 Producer 向 Leader 发送数据时,可以通过`request.required.acks` 参数来设置数据可靠性的级别:
- 1默认这意味着 Producer 在 ISR 中的 Leader 已成功收到的数据并得到确认后发送下一条 message 。如果 Leader 宕机了,则会丢失数据。
- 0这意味着 Producer 无需等待来自 Broker 的确认而继续发送下一批消息。这种情况下数据传输效率最高,但是数据可靠性确是最低的。
- -1Producer 需要等待 ISR 中的所有 Follower 都确认接收到数据后才算一次发送完成,可靠性最高。但是这样也不能保证数据不丢失,比如当 ISR 中只有 Leader 时(其他节点都和 Zookeeper 断开连接,或者都没追上),这样就变成了 `acks=1` 的情况。
关于这块详详细的内容,推荐阅读
- [《Kafka 数据可靠性深度解读》](https://blog.csdn.net/u013256816/article/details/71091774) 的 [「3 高可靠性存储分析」](https://svip.iocoder.cn/Kafka/Interview/#) 小节
- [《Kafka 集群内复制功能深入剖析》](https://www.jianshu.com/p/03d6a335237f)
## ZooKeeper 在 Kafka 中起到什么作用?
关于 ZooKeeper 是什么,不了解的胖友,直接去看 [《精尽 Zookeeper 面试题》](http://svip.iocoder.cn/Zookeeper/Interview/) 。
在基于 Kafka 的分布式消息队列中ZooKeeper 的作用有:
- 1、Broker 在 ZooKeeper 中的注册。
- 2、Topic 在 ZooKeeper 中的注册。
- 3、Consumer 在 ZooKeeper 中的注册。
- 4、Producer 负载均衡。
> 主要指的是Producer 从 Zookeeper 拉取 Topic 元数据,从而能够将消息发送负载均衡到对应 Topic 的分区中。
- 5、Consumer 负载均衡。
- 6、记录消费进度 Offset 。
> Kafka 已推荐将 consumer 的 Offset 信息保存在 Kafka 内部的 Topic 中。
- 7、记录 Partition 与 Consumer 的关系。
其实,总结起来,就是两类功能:
- Broker、Producer、Consumer 和 Zookeeper 的交互。
> 对应 1、2、3、5 。
- 相应的状态存储到 Zookeeper 中。
> 对应 4、6、7 。
详细的每一点,看 [《再谈基于 Kafka 和 ZooKeeper 的分布式消息队列原理》](https://gitbook.cn/books/5bc446269a9adf54c7ccb8bc/index.html) 的 [「Kafka 架构中 ZooKeeper 以怎样的形式存在?」](https://svip.iocoder.cn/Kafka/Interview/#) 小节。
## Kafka 如何实现高可用?
在 [「Kafka 的架构是怎么样的?」](https://svip.iocoder.cn/Kafka/Interview/#) 问题中,已经基本回答了这个问题。
[Kafka 集群](https://segmentfault.com/img/bVbcmpm?w=776&h=436)
- Zookeeper 部署 2N+1 节点,形成 Zookeeper 集群,保证高可用。
- Kafka Broker 部署集群。每个 Topic 的 Partition ,基于【副本机制】,在 Broker 集群中复制,形成 replica 副本,保证消息存储的可靠性。每个 replica 副本,都会选择出一个 leader 分区Partition提供给客户端Producer 和 Consumer进行读写。
- Kafka Producer 无需考虑集群因为和业务服务部署在一起。Producer 从 Zookeeper 拉取到 Topic 的元数据后,选择对应的 Topic 的 leader 分区,进行消息发送写入。而 Broker 根据 Producer 的 `request.required.acks` 配置,是写入自己完成就响应给 Producer 成功,还是写入所有 Broker 完成再响应。这个,就是胖友自己对消息的可靠性的选择。
- Kafka Consumer 部署集群。每个 Consumer 分配其对应的 Topic Partition 根据对应的分配策略。并且Consumer 只从 leader 分区Partition拉取消息。另外当有新的 Consumer 加入或者老的 Consumer 离开,都会将 Topic Partition 再均衡,重新分配给 Consumer 。
> 注意噢,此处说的都是同一个 Kafka Consumer group 。
总的来说Kafka 和 RocketMQ 的高可用方式是比较类似的,主要的差异在 Kafka Broker 的副本机制,和 RocketMQ Broker 的主从复制,两者的差异,以及差异带来的生产和消费不同。😈 当然,实际上,都是和“主” Broker 做消息的发送和读取不是?!
## 什么是 Kafka 事务?
推荐阅读 [《Kafka 事务简介》](https://blog.csdn.net/ransom0512/article/details/78840042) 文章。
😈 和想象中的,是不是有点差别?!
具体的代码实现,可以看看 [《芋道 Spring Boot 消息队列 Kafka 入门》](http://www.iocoder.cn/Spring-Boot/RabbitMQ/?vip)的[「11. 事务消息」](https://svip.iocoder.cn/Kafka/Interview/#)小节。
## Kafka 是否会弄丢数据?
> 艿艿注意Kafka 是否会丢数据,主要取决于我们如何使用。这点,非常重要噢。
🦅 **消费端弄丢了数据?**
唯一可能导致消费者弄丢数据的情况,就是说,你消费到了这个消息,然后消费者那边自动提交了 offset ,让 Broker 以为你已经消费好了这个消息,但其实你才刚准备处理这个消息,你还没处理,你自己就挂了,此时这条消息就丢咯。
这不是跟 RabbitMQ 差不多吗,大家都知道 Kafka 会自动提交 offset ,那么只要关闭自动提交 offset ,在处理完之后自己手动提交 offset ,就可以保证数据不会丢。但是此时确实还是可能会有重复消费,比如你刚处理完,还没提交 offset ,结果自己挂了,此时肯定会重复消费一次,自己保证幂等性就好了。
> RocketMQ push 模式下,在确认消息被消费完成,才会提交 Offset 给 Broker 。
生产环境碰到的一个问题,就是说我们的 Kafka 消费者消费到了数据之后是写到一个内存的 queue 里先缓冲一下,结果有的时候,你刚把消息写入内存 queue ,然后消费者会自动提交 offset 。然后此时我们重启了系统,就会导致内存 queue 里还没来得及处理的数据就丢失了。
具体的代码实现,可以看看 [《芋道 Spring Boot 消息队列 Kafka 入门》](http://www.iocoder.cn/Spring-Boot/RabbitMQ/?vip)的[「12. 消费进度的提交机制」](https://svip.iocoder.cn/Kafka/Interview/#)小节。
🦅 **Broker 弄丢了数据?**
这块比较常见的一个场景,就是 Kafka 某个 Broker 宕机,然后重新选举 Partition 的 leader。大家想想要是此时其他的 follower 刚好还有些数据没有同步,结果此时 leader 挂了,然后选举某个 follower 成 leader 之后,不就少了一些数据?这就丢了一些数据啊。
生产环境也遇到过,我们也是,之前 Partition 的 leader 机器宕机了,将 follower 切换为 leader 之后,就会发现说这个数据就丢了。
所以此时一般是要求起码设置如下 4 个参数:
- 给 Topic 设置 `replication.factor` 参数:这个值必须大于 1要求每个 partition 必须有至少 2 个副本。
- 在 Kafka 服务端设置 `min.insync.replicas` 参数:这个值必须大于 1 ,这个是要求一个 leader 至少感知到有至少一个 follower 还跟自己保持联系,没掉队,这样才能确保 leader 挂了还有一个 follower 吧。
- 在 Producer 端设置 `acks=all`:这个是要求每条数据,必须是**写入所有 replica 之后,才能认为是写成功了**。
> 不过这个也不一定能够绝对保证例如说Broker 集群里,所有节点都挂了,只剩下一个节点。此时,`acks=all` 和 `acks=1` 就等价了。当然,也可以通过设置 `min.insync.replics` 参数,每次写入要求最小的同步副本数。
>
> 这块也和朋友交流了下,他们金融场景下,`acks=all` 也是这么配置的。原因嘛,因为他们是金融场景呀。
- 在 Producer 端设置 `retries=MAX`(很大很大很大的一个值,无限次重试的意思):这个是**要求一旦写入失败,就无限重试**,卡在这里了。
我们生产环境就是按照上述要求配置的,这样配置之后,至少在 Kafka broker 端就可以保证在 leader 所在 Broker 发生故障,进行 leader 切换时,数据不会丢失。
🦅 **生产者会不会弄丢数据?**
如果按照上述的思路设置了 `acks=all` ,一定不会丢,要求是,你的 leader 接收到消息,所有的 follower 都同步到了消息之后,才认为本次写成功了。如果没满足这个条件,生产者会自动不断的重试,重试无限次。
- 关于 Kafka Producer 重试发送消息的逻辑的源码解析,可以看看 [《Kafka 重试机制解读》](https://www.jianshu.com/p/5bf9c7c02ada) 。
------
😈 另外,在推荐一篇文章 [《360 度测试KAFKA 会丢数据么?其高可用是否满足需求?》](https://juejin.im/post/5bf8f2bee51d4507c12bc722) ,提供了一些测试示例。
关于这一块,可以重点看看 [《Kafka 权威指南》](https://u.jd.com/jJpbF8) 的 [「6.4 在可靠的系统里使用生产者」](https://svip.iocoder.cn/Kafka/Interview/#) 和 [「6.5 在可靠的系统里使用消费者」](https://svip.iocoder.cn/Kafka/Interview/#) 小节。
## Kafka 如何保证消息的顺序性?
Kafka 本身,并不像 RocketMQ 一样,提供顺序性的消息。所以,提供的方案,都是相对有损的。如下:
> 这里的顺序消息,我们更多指的是,单个 Partition 的消息,被顺序消费。
- 方式一Consumer ,对每个 Partition 内部单线程消费,单线程吞吐量太低,一般不会用这个。
- 方式二Consumer ,拉取到消息后,写到 N 个内存 queue具有相同 key 的数据都到同一个内存 queue 。然后,对于 N 个线程,每个线程分别消费一个内存 queue 即可,这样就能保证顺序性。
> 这种方式,相当于对【方式一】的改进,将相同 Partition 的消息进一步拆分,保证相同 key 的数据消费是顺序的。
>
> 不过这种方式,消费进度的更新会比较麻烦。
当然,实际情况也不太需要考虑消息的顺序性,基本没有业务需要。
具体的代码实现,可以看看 [《芋道 Spring Boot 消息队列 Kafka 入门》](http://www.iocoder.cn/Spring-Boot/RabbitMQ/?vip)的[「10. 顺序消息」](https://svip.iocoder.cn/Kafka/Interview/#)小节。
## 666. 彩蛋
😈 略显仓促的一篇文章,后续会重新在梳理一下。如果胖友对 Kafka 有什么疑惑,一定要在星球里提出,我们一起在讨论和解答一波,然后整理到这篇文章中。
同时,期待下厮大的 Kafka 新书。
参考与推荐如下文章:
- [《高并发面试必问:分布式消息系统 Kafka 简介》](https://blog.csdn.net/caisini_vc/article/details/48007297)
- [《14 个最常见的 Kafka 面试题及答案》](https://blog.csdn.net/yjh314/article/details/77568580)
> 这篇博客,有点傻逼。。。。
- [《你需要知道的 Kafka》](https://juejin.im/post/5b573eafe51d45198f5c80a4)
- [《Kafka 内部网络框架模型分析》](https://blog.csdn.net/lizhitao/article/details/52332749)
- [《再谈基于 Kafka 和 ZooKeeper 的分布式消息队列原理》](https://gitbook.cn/books/5bc446269a9adf54c7ccb8bc/index.html)
- [《如何保证消息的可靠性传输?(如何处理消息丢失的问题)》](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/how-to-ensure-the-reliable-transmission-of-messages.md)

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -0,0 +1,487 @@
# 精尽【缓存】面试题「最新更新时间2024-02」
以下面试题,基于网络整理,和自己编辑。具体参考的文章,会在文末给出所有的链接。
如果胖友有自己的疑问,欢迎在星球提问,我们一起整理吊吊的【缓存】面试题的大保健。
而题目的难度,艿艿尽量按照从容易到困难的顺序,逐步下去。
另外,本文只分享通用的【缓存】的面试题,关于 Redis、MemCache 会单独分享。
再另外,本文【缓存】指的更多的是分布式缓存服务,而不是 HTTP 缓存等等。
再再另外,强烈推荐购买 [《Redis 开发与运维》](https://u.jd.com/lDNJa9) ,真心不错,可以完完整整 Redis 。
## 什么是缓存?
> 艿艿:这个问题,理解即可。
缓存,就是数据交换的缓冲区,针对服务对象的不同(本质就是不同的硬件)都可以构建缓存。
目的是,**把读写速度【慢】的介质的数据保存在读写速度【快】的介质中,从而提高读写速度,减少时间消耗**。例如:
- CPU 高速缓存 :高速缓存的读写速度远高于内存。
- CPU 读数据时,如果在高速缓存中找到所需数据,就不需要读内存
- CPU 写数据时,先写到高速缓存,再回写到内存。
- 磁盘缓存:磁盘缓存其实就把常用的磁盘数据保存在内存中,内存读写速度也是远高于磁盘的。
- 读数据,时从内存读取。
- 写数据时,可先写到内存,定时或定量回写到磁盘,或者是同步回写。
在 [《CPU 内存访问速度,磁盘和网络速度,所有人都应该知道的数字》](https://www.cnblogs.com/liqiu/p/3211746.html) 中,胖友可以更好的理解不同介质的速度。
## 为什么要用缓存?
正如在 [「什么是缓存?」](https://svip.iocoder.cn/Cache/Interview/#) 问题中所看到的,使用缓存的目的,就是提升读写性能。而实际业务场景下,更多的是为了提升**读性能**,带来更好的性能,更高的并发量。
日常业务中,我们使用比较多的数据库是 MySQL ,缓存是 Redis 。一起来看看,阿里云提供的性能规格:
- Redis 性能规格https://help.aliyun.com/document_detail/26350.html 。打底 8W QPS ,最高可达千万 QPS 。
- MySQL 性能规格 https://help.aliyun.com/document_detail/53637.html 。打底 1.4K QPS ,最高 7W QPS 。
艿艿自己,也分别进行了下 Redis 和 MySQL 的基准测试,感兴趣的胖友,可以看看,甚至自己上手玩玩。
- [《性能测试 —— Redis 基准测试》](http://www.iocoder.cn/Performance-Testing/Redis-benchmark/?vip)
- [《性能测试 —— MySQL 基准测试》](http://www.iocoder.cn/Performance-Testing/MySQL-benchmark/?vip)
如此一比较Redis 比 MySQL 的读写性能好很多。那么,我们将 MySQL 的热点数据,缓存到 Redis 中,提升读取性能,也减小 MySQL 的读取压力。例如说:
- 论坛帖子的访问频率比较高,且要实时更新阅读量,使用 Redis 记录帖子的阅读量,可以提升性能和并发。
- 商品信息,数据更新的频率不高,但是读取的频率很高,特别是热门商品。
## 请说说有哪些缓存算法?是否能手写一下 LRU 代码的实现?
🦅 **缓存算法**
缓存算法,比较常见的是三种:
- LRUleast recently used ,最近最少使用)
- LFULeast Frequently used ,最不经常使用)
- FIFOfirst in first out ,先进先出)
完整的话,胖友可以看看 [《缓存、缓存算法和缓存框架简介》](http://blog.jobbole.com/30940/) 的 [「缓存算法」](https://svip.iocoder.cn/Cache/Interview/#) 部分。
🦅 **手写 LRU 代码的实现**
手写 LRU 代码的实现,有多种方式。其中,最简单的是基于 LinkedHashMap 来实现,代码如下:
```
class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int CACHE_SIZE;
/**
* 传递进来最多能缓存多少数据
*
* @param cacheSize 缓存大小
*/
public LRUCache(int cacheSize) {
// true 表示让 LinkedHashMap 按照访问顺序来进行排序,最近访问的放在头部,最老访问的放在尾部。
super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true);
CACHE_SIZE = cacheSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
// 当 map 中的数据量大于指定的缓存个数的时候,就自动删除最老的数据。
return size() > CACHE_SIZE;
}
}
```
其它更复杂,更能体现个人编码能力的 LRU 实现方式,可以看看如下两篇文章:
- [《动手实现一个 LRU Cache》](https://crossoverjie.top/2018/04/07/algorithm/LRU-cache/)
- [《缓存、缓存算法和缓存框架简介》](http://blog.jobbole.com/30940/) 文末,并且还提供了 FIFO、LFU 的代码实现。
## 常见的常见的缓存工具和框架有哪些?
在 Java 后端开发中,常见的缓存工具和框架列举如下:
- 本地缓存Guava LocalCache、Ehcache、Caffeine 。
- Ehcache 的功能更加丰富Caffeine 的性能要比 Guava LocalCache 好。
- 分布式缓存Redis、Memcached、Tair 。
- Redis 最为主流和常用。
## 用了缓存之后,有哪些常见问题?
常见的问题,可列举如下:
- 写入问题
- 缓存何时**写入**?并且写时如何避免并发重复写入?
- 缓存如何**失效**
- 缓存和 DB 的**一致性**如何保证?
- 经典三连问
- 如何避免缓存**穿透**的问题?
- 如何避免缓存**击穿**的问题?
- 如果避免缓存**雪崩**的问题?
> 艿艿:重点可以去“记”加粗的六个词。
下面,我们会对每个问题,逐步解析。
## 当查询缓存报错,怎么提高可用性?
缓存可以极大的提高查询性能,但是缓存数据丢失和缓存不可用不能影响应用的正常工作。
因此,一般情况下,如果缓存出现异常,需要手动捕获这个异常,并且记录日志,并且从数据库查询数据返回给用户,而不应该导致业务不可用。
当然,这样做可能会带来缓存雪崩的问题。具体怎么解决,可以看看本文 [「如何避免缓存”雪崩”的问题?」](https://svip.iocoder.cn/Cache/Interview/#) 问题。
## 如果避免缓存”穿透”的问题?
🦅 **缓存穿透**
缓存穿透,是指查询一个一定**不存在**的数据,由于缓存是不命中时被动写,并且处于容错考虑,如果从 DB 查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,失去了缓存的意义。
> 被动写:当从缓存中查不到数据时,然后从数据库查询到该数据,写入该数据到缓存中。
在流量大时,可能 DB 就挂掉了,要是有人利用不存在的 key 频繁攻击我们的应用,这就是漏洞。如下图:[![缓存穿透](12-精尽【缓存】面试题.assets/01.png)](http://static.iocoder.cn/images/Cache/2019-11-21/01.png)缓存穿透
- 在 [「为什么要用缓存?」](https://svip.iocoder.cn/Cache/Interview/#) 中我们已经看到MySQL 的性能是远不如 Redis 的,如果大量的请求直接打到 MySQL ,则会直接打挂 MySQL 。
- 当然,缓存穿透不一定是攻击,也可能是我们自己程序写的问题,疯狂读取不存在的数据,又或者“无脑”的爬虫,顺序爬取数据。
- 另外,一定要注意,**缓存穿透**,指的是查询一个**不存在**的数据,很容器和我们要讲到的**缓存击穿**搞混淆。
🦅 **如何解决**
有两种方案可以解决:
1方案一缓存空对象。
当从 DB 查询数据为空,我们仍然将这个空结果进行缓存,具体的值需要使用**特殊的标识**,能和真正缓存的数据区分开。另外,需要设置较短的过期时间,一般建议不要超过 5 分钟。
> 为什么要较短的过期时间?因为缓存久没有意义,也浪费缓存的内存。
2方案二BloomFilter 布隆过滤器。
在缓存服务的基础上,构建 BloomFilter 数据结构,在 BloomFilter 中存储对应的 KEY 是否存在,如果存在,说明该 KEY 对应的值**不为空**。那么整个逻辑的如下:
- 1、根据 KEY 查询【BloomFilter 缓存】。如果不存在对应的值,直接返回;如果存在,继续向下执行。【后续的流程,就是标准的流程】
- 2、根据 KEY 查询在【数据缓存】的值。如果存在值,直接返回;如果不存在值,继续向下执行。
- 3、查询 DB 对应的值,如果存在,则更新到缓存,并返回该值。
可能有胖友不是很了解 BloomFilter 布隆过滤器,会有疑惑,为什么 BloomFilter 不存储 KEY 是不存在的情况(就是我们方案二反过来)?
- BloomFilter 存在误判。简单来说,**存在的不一定存在,不存在的一定不存在**。这样就会导致,一个存在的 KEY 被误判成不存在。
- 同时BloomFilter 不允许删除。例如说,一个 KEY 一开始是不存在的,后来数据新增了,但是 BloomFilter 不允许删除的特点,就会导致一直会被判断成不存在。
当然,使用 BloomFilter 布隆过滤器的话,需要提前将已存在的 KEY 初始化存储到【BloomFilter 缓存】中。
🦅 **选择**
这两个方案,各有其优缺点。
| | 缓存空对象 | BloomFilter 布隆过滤器 |
| :------- | :-------------------------------------------------- | :---------------------------------------- |
| 适用场景 | 1、数据命中不高 2、保证一致性 | 1、数据命中不高 2、数据相对固定、实时性低 |
| 维护成本 | 1、代码维护简单 2、需要过多的缓存空间 3、数据不一致 | 1、代码维护复杂 2、缓存空间占用小 |
实际情况下,使用方案二比较多。因为,相比方案一来说,更加节省内容,对缓存的负荷更小。
注意,常用的缓存 Redis 默认不支持 BloomFilter 数据结构。具体怎么解决,参考如下文章:
- [RedisBloom](https://github.com/RedisBloom/RedisBloom)
> Redis 4.0 引入 Module 机制,支持 Server 自定义拓展。而 RedisBloom ,就是 Redis BloomFilter 的拓展。
- [Redis-Lua-scaling-bloom-filter](https://github.com/erikdubbelboer/Redis-Lua-scaling-bloom-filter)
> Lua 脚本,实现 BloomFilter 的功能。
- [Redisson BloomFilter](https://github.com/redisson/redisson/wiki/6.-分布式对象#68-布隆过滤器bloom-filter)
> Java Redis 库,实现 BloomFilter 的功能。
- 其它文章
- [《Google Guava之BloomFilter 源码分析及基于 Redis 的重构》](https://segmentfault.com/a/1190000012620152)
- [《基于 Redis 的 BloomFilter 实现》](https://segmentfault.com/a/1190000017370384)
> 艿艿的遐想:因为 BloomFilter 布隆过滤器存在的误判的情况,如果最后去 DB 查询不到数据的情况是不是可以结合方案一缓存空对象到【BloomFilter 缓存】中。后来想想,必要性不大,因为 BloomFilter 布隆过滤器误判率很低,没必要把方案复杂化,大道至简。
------
另外,推荐看下 [《Redis架构之防雪崩设计网站不宕机背后的兵法》](https://mp.weixin.qq.com/s/TBCEwLVAXdsTszRVpXhVug) 文章的 [「一、缓存穿透预防及优化」](https://svip.iocoder.cn/Cache/Interview/#) ,大神解释的更好,且提供相应的图和伪代码。
## 如何避免缓存”雪崩”的问题?
🦅 **缓存雪崩**
缓存雪崩,是指缓存由于某些原因无法提供服务( 例如,缓存挂掉了 ),所有请求全部达到 DB 中,导致 DB 负荷大增,最终挂掉的情况。
🦅 **如何解决**
预防和解决缓存雪崩的问题,可以从以下**多个方面进行共同着手**。
1缓存高可用
通过搭建缓存的高可用,避免缓存挂掉导致无法提供服务的情况,从而降低出现缓存雪崩的情况。
假设我们使用 Redis 作为缓存,则可以使用 Redis Sentinel 或 Redis Cluster 实现高可用。
2本地缓存
如果使用本地缓存时,即使分布式缓存挂了,也可以将 DB 查询到的结果缓存到本地,避免后续请求全部到达 DB 中。
当然,引入本地缓存也会有相应的问题,例如说:
- 本地缓存的实时性怎么保证?
- 方案一,可以引入消息队列。在数据更新时,发布数据更新的消息;而进程中有相应的消费者消费该消息,从而更新本地缓存。
- 方案二,设置较短的过期时间,请求时从 DB 重新拉取。
- 方案三,使用 [「如果避免缓存”击穿”的问题?」](https://svip.iocoder.cn/Cache/Interview/#) 问题的【方案二】,手动过期。
- 每个进程可能会本地缓存相同的数据,导致数据浪费?
- 方案一,需要配置本地缓存的过期策略和缓存数量上限。
> 艿艿:上述的几个方案写的有点笼统,如果有不理解的地方,请在星球给艿艿留言。
如果我们使用 JVM ,则可以使用 Ehcache、Guava Cache 实现本地缓存的功能。
3请求 DB 限流
通过限制 DB 的每秒请求数,避免把 DB 也打挂了。这样至少能有两个好处:
1. 可能有一部分用户,还可以使用,系统还没死透。
2. 未来缓存服务恢复后,系统立即就已经恢复,无需再处理 DB 也挂掉的情况。
当然,被限流的请求,我们最好也要有相应的处理,走【服务降级】,提供一些默认的值,或者友情提示,甚至空白的值也行。
如果我们使用 Java ,则可以使用 Guava RateLimiter、Sentinel、Hystrix 实现限流的功能。
4提前演练
在项目上线前,演练缓存宕掉后,应用以及后端的负载情况以及可能出现的问题,在此基础上做一些预案设定。
------
另外,推荐看下 [《Redis架构之防雪崩设计网站不宕机背后的兵法》](https://mp.weixin.qq.com/s/TBCEwLVAXdsTszRVpXhVug) 文章的 [「二、缓存雪崩问题优化」](https://svip.iocoder.cn/Cache/Interview/#) ,大神解释的更好,且提供相应的图和伪代码。
## 如果避免缓存”击穿”的问题?
🦅 **缓存击穿**
缓存击穿,是指某个**极度“热点”**数据在某个时间点过期时,恰好在这个时间点对这个 KEY 有大量的并发请求过来,这些请求发现缓存过期一般都会从 DB 加载数据并回设到缓存,但是这个时候大并发的请求可能会瞬间 DB 压垮。
- 对于一些设置了过期时间的 KEY ,如果这些 KEY 可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑这个问题。
- 区别:
- 和缓存“雪崩“”的区别在于,前者针对某一 KEY 缓存,后者则是很多 KEY 。
- 和缓存“穿透“”的区别在于,这个 KEY 是真实存在对应的值的。
🦅 **如何解决**
有两种方案可以解决:
1方案一使用互斥锁。
请求发现缓存不存在后,去查询 DB 前,使用分布式锁,保证有且只有一个线程去查询 DB ,并更新到缓存。流程如下:
- 1、获取分布式锁直到成功或超时。如果超时则抛出异常返回。如果成功继续向下执行。
- 2、获取缓存。如果存在值则直接返回如果不存在则继续往下执行。😈 因为,获得到锁,可能已经被“那个”线程去查询过 DB ,并更新到缓存中了。
- 3、查询 DB ,并更新到缓存中,返回值。
2方案二手动过期。
缓存上从不设置过期时间,功能上将过期时间存在 KEY 对应的 VALUE 里。流程如下:
- 1、获取缓存。通过 VALUE 的过期时间,判断是否过期。如果未过期,则直接返回;如果已过期,继续往下执行。
- 2、通过一个后台的异步线程进行缓存的构建也就是“手动”过期。通过后台的异步线程保证有且只有一个线程去查询 DB。
- 3、同时虽然 VALUE 已经过期,还是直接返回。通过这样的方式,保证服务的可用性,虽然损失了一定的时效性。
🦅 **选择**
这两个方案,各有其优缺点。
| | 使用互斥锁 | 手动过期 |
| :--- | :---------------------------------- | :------------------------ |
| 优点 | 1、思路简单 2、保证一致性 | 1、性价最佳用户无需等待 |
| 缺点 | 1、代码复杂度增大 2、存在死锁的风险 | 1、无法保证缓存一致性 |
具体使用哪一种方案,胖友可以根据自己的业务场景去做选择。
- 有一点要注意,上述的两个方案,都是建立在**极度“热点”**数据存在的情况,所以实际场景下,需要结合 [「如果避免缓存”穿透”的问题?」](https://svip.iocoder.cn/Cache/Interview/#) 的方案,一起使用。
------
另外,推荐看下 [《Redis 架构之防雪崩设计:网站不宕机背后的兵法》](https://mp.weixin.qq.com/s/TBCEwLVAXdsTszRVpXhVug) 文章的 [「三、缓存热点 key 重建优化」](https://svip.iocoder.cn/Cache/Interview/#) ,大神解释的更好,且提供相应的图和伪代码。
## 缓存和 DB 的一致性如何保证?
🦅 **产生原因**
主要有两种情况,会导致缓存和 DB 的一致性问题:
1. 并发的场景下,导致读取老的 DB 数据,更新到缓存中。
> 这里,主要指的是,更新 DB 数据之前,先删除 Cache 的数据。在低并发量下没什么问题,但是在高并发下,就会存在问题。在(删除 Cache 的数据, 和更新 DB 数据)时间之间,恰好有一个请求,我们如果使用**被动读**,因为此时 DB 数据还是老的,又会将老的数据写入到 Cache 中。
2. 缓存和 DB 的操作,不在一个事务中,可能只有一个 DB 操作成功,而另一个 Cache 操作失败,导致不一致。
当然,有一点我们要注意,缓存和 DB 的一致性,我们指的更多的是最终一致性。我们使用缓存只要是提高读操作的性能,真正在写操作的业务逻辑,还是以数据库为准。例如说,我们可能缓存用户钱包的余额在缓存中,在前端查询钱包余额时,读取缓存,在使用钱包余额时,读取数据库。
🦅 **解决方案**
在开始说解决方案之前,胖友先看看如下几篇文章,可能有一丢丢多,保持耐心。
- 左耳朵耗子
- [《缓存更新的套路》](https://coolshell.cn/articles/17416.html)
- 沈剑
- [《缓存架构设计细节二三事》](https://www.w3cschool.cn/architectroad/architectroad-cache-architecture-design.html)
- [《缓存与数据库一致性优化》](https://www.w3cschool.cn/architectroad/architectroad-consistency-of-cache-with-database.html) 这篇,我觉得写的方案不太可行。
下面,我们就来看看几种方案。当然无论哪种方案,比较重要的就是解决两个问题:
- 1、将缓存可能存在的并行写实现串行写。
> 注意,这里指的是缓存的并行写。在被动读中,如果缓存不存在,也存在写。
- 2、实现数据的最终一致性。
1先淘汰缓存再写数据库
因为先淘汰缓存,所以数据的最终一致性是可以得到有效的保证的。为什么呢?先淘汰缓存,即使写数据库发生异常,也就是下次缓存读取时,多读取一次数据库。
但是,这种方案会存在缓存和 DB 的数据会不一致的情况,[《缓存与数据库一致性优化》](https://www.w3cschool.cn/architectroad/architectroad-consistency-of-cache-with-database.html) 已经说了。
那么,我们需要解决缓存并行写,实现串行写。比较简单的方式,引入分布式锁。
- 在写请求时,先淘汰缓存之前,先获取该分布式锁。
- 在读请求时,发现缓存不存在时,先获取分布式锁。
这样,缓存的并行写就成功的变成串行写落。实际上,就是 [「如果避免缓存”击穿”的问题?」](https://svip.iocoder.cn/Cache/Interview/#) 的【方案一】互斥锁的加强版。
整体执行,如下草图:
> 艿艿:临时手绘,不要打我。字很丑,哈哈哈哈。
[![草图](12-精尽【缓存】面试题.assets/02.png)](http://static.iocoder.cn/images/Cache/2019-11-21/02.png)草图
- 写请求时,是否主动更新缓存,根据自己业务的需要,是否有,都没问题。
2先写数据库再更新缓存
按照“先写数据库,再更新缓存”,我们要保证 DB 和缓存的操作,能够在“同一个事务”中,从而实现最终一致性。
**基于定时任务来实现**
- 首先,写入数据库。
- 然后,在写入数据库所在的事务中,插入一条记录到任务表。该记录会存储需要更新的缓存 KEY 和 VALUE 。
- 【异步】最后,定时任务每秒扫描任务表,更新到缓存中,之后删除该记录。
**基于消息队列来实现**
- 首先,写入数据库。
- 然后,发送带有缓存 KEY 和 VALUE 的事务消息。此时,需要有支持事务消息特性的消息队列,或者我们自己封装消息队列,支持事务消息。
- 【异步】最后,消费者消费该消息,更新到缓存中。
这两种方式,可以进一步优化,可以先尝试更新缓存,如果失败,则插入任务表,或者事务消息。
另外,极端情况下,如果并发写执行时,先更新成功 DB 的,结果后更新缓存,如下图所示:[![草图](12-精尽【缓存】面试题.assets/03.png)](http://static.iocoder.cn/images/Cache/2019-11-21/03.png)草图
> 艿艿:灵魂画手,哈哈哈哈。
- 理论来说,希望的更新缓存顺序是,线程 1 快于线程 2 但是实际线程1 晚于线程 2 ,导致数据不一致。
- 可能胖友会说,图中不是基于定时任务或消息队列来实现异步更新缓存啊?答案是一直的,如果网络抖动,导致【插入任务表,或者事务消息】的顺序不一致。
- 那么怎么解决呢?需要做如下三件事情:
- 1、在缓存值中拼接上数据版本号或者时间戳。例如说`value = {value: 原值, version: xxx}`
- 2、在任务表的记录或者事务消息中增加上数据版本号或者时间戳的字段。
- 3、在定时任务或消息队列执行更新缓存时先读取缓存对比版本号或时间戳大于才进行更新。😈 当然,此处也会有并发问题,所以还是得引入分布式锁或 CAS 操作。
- 关于 Redis 分布式锁,可以看看 [《精尽 Redis 面试题》](http://svip.iocoder.cn/Redis/Interview) 的 [「如何使用 Redis 实现分布式锁?」](https://svip.iocoder.cn/Cache/Interview/#) 问题。
- 关于 Redis CAS 操作,可以看看 [《精尽 Redis 面试题》](http://svip.iocoder.cn/Redis/Interview) 的 [「什么是 Redis 事务?」](https://svip.iocoder.cn/Cache/Interview/#) 问题。
3基于数据库的 binlog 日志
> 艿艿:如下内容,引用自 [《技术专题讨论第五期:论系统架构设计中缓存的重要性》](http://www.spring4all.com/question/177) 文章,超哥对这个问题的回答。
[![binlog 方案](12-精尽【缓存】面试题.assets/f434927790ae53b4fa955ecd9952f787.png)](http://static.iocoder.cn/f434927790ae53b4fa955ecd9952f787)binlog 方案
- 应用直接写数据到数据库中。
- 数据库更新binlog日志。
- 利用Canal中间件读取binlog日志。
- Canal借助于限流组件按频率将数据发到MQ中。
- 应用监控MQ通道将MQ的数据更新到Redis缓存中。
可以看到这种方案对研发人员来说比较轻量,不用关心缓存层面,而且这个方案虽然比较重,但是却容易形成统一的解决方案。
------
当然,以上种种方案,各有其复杂性,如果胖友心里没底,还是仅仅使用如下任一方案:
- “**先淘汰缓存,再写数据库**”的方案,并且无需引入分布式锁。
> 沈剑大佬,比较支持这种方案,见 [《缓存架构设计细节二三事》](https://www.w3cschool.cn/architectroad/architectroad-cache-architecture-design.html) 。
- “**先写数据库,再更新缓存**”的方案,并且无需引入定时任务或者消息队列。
> 左耳朵耗子,比较支持这种方案,[《缓存更新的套路》](https://coolshell.cn/articles/17416.html)。
原因如下:
> FROM 基友老梁的总结
>
> 使用缓存过程中,经常会遇到缓存数据的不一致性和脏读现象。一般情况下,采取缓存双淘汰机制,在更新数据库的**前**淘汰缓存。此外,设定超时时间,例如三十分钟。
>
> **极端场景下,即使有脏数据进入缓存,这个脏数据也最存在一段时间后自动销毁。**
- 重点,是最后一句话哟。
- 真的,和几个朋友沟通了下,真的出现不一致的情况,靠缓存过期后,重新从 DB 中读取即可。
另外,在 DB 主从架构下,方案会更加复杂。详细可以看看 [《主从 DB 与 cache 一致性优化》](https://www.w3cschool.cn/architectroad/architectroad-consistency-of-cache-with-master-and-slave-database.html) 。
> 艿艿:这是一道相对复杂的问题,重点在于理解为什么产生不一致的原因,然后针对这个原因去解决。
## 什么是缓存预热?如何实现缓存预热?
🦅 **缓存预热**
在刚启动的缓存系统中,如果缓存中没有任何数据,如果依靠用户请求的方式重建缓存数据,那么对数据库的压力非常大,而且系统的性能开销也是巨大的。
此时,最好的策略是启动时就把热点数据加载好。这样,用户请求时,直接读取的就是缓存的数据,而无需去读取 DB 重建缓存数据。
举个例子,热门的或者推荐的商品,需要提前预热到缓存中。
🦅 **如何实现**
一般来说,有如下几种方式来实现:
1. 数据量不大时,项目启动时,自动进行初始化。
2. 写个修复数据脚本,手动执行该脚本。
3. 写个管理界面,可以手动点击,预热对应的数据到缓存中。
## 缓存数据的淘汰策略有哪些?
除了缓存服务器自带的缓存**自动**失效策略之外,我们还可以根据具体的业务需求进行自定义的**“手动”**缓存淘汰,常见的策略有两种:
- 1、定时去清理过期的缓存。
- 2、当有用户请求过来时再判断这个请求所用到的缓存是否过期过期的话就去底层系统得到新数据并更新缓存。
两者各有优劣,第一种的缺点是维护大量缓存的 key 是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!
具体用哪种方案,大家可以根据自己的应用场景来权衡。
## 缓存如何存储 POJO 对象?
实际场景下,缓存值可能是一个 POJO 对象,就需要考虑如何 POJO 对象存储的问题。目前有两种方式:
- 方案一,将 POJO 对象
序列化
进行存储,适合 Redis 和 Memcached 。
- 可参考 [《Redis 序列化方式StringRedisSerializer、FastJsonRedisSerializer 和 KryoRedisSerializer》](https://blog.csdn.net/xiaolyuh123/article/details/78682200) 文章。
- 对于 POJO 对象比较大,可以考虑使用压缩算法,例如说 Snappy、zlib、GZip 等等。
- 方案二,使用 Hash 数据结构,适合 Redis 。
- 可参考 [《Redis 之序列化 POJO》](https://my.oschina.net/yuyidi/blog/499951) 文章。
不过对于 Redis 来说,大多数情况下,会考虑使用 JSON 序列化的方案。想要深入的胖友,可以看看如下两篇文章,很有趣:
- [《Redis 内存压缩实战》](http://www.iocoder.cn/Fight/Redis-memory-compression-combat/?self) Redis HASH 数据结构,可以通过 ziplist 的编码方式,压缩数据。
- [《redis-strings-vs-redis-hashes-to-represent-json-efficiency》](https://stackoverflow.com/questions/16375188/redis-strings-vs-redis-hashes-to-represent-json-efficiency) ,重点看 BMiner 的回答,提供了四种方案,非常有趣。
## 666. 彩蛋
参考与推荐如下文章:
- _痕迹 [《缓存那些事(二)什么是缓存以及缓存的作用》](https://www.jianshu.com/p/118725df0db2)
- yanglbme [《在项目中缓存是如何使用的?缓存如果使用不当会造成什么后果?》](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/why-cache.md)
- boothsun [《缓存常见问题》](https://www.zybuluo.com/boothsun/note/1078466)
- 超神杀戮 [《缓存穿透与缓存雪崩》](https://www.cnblogs.com/fidelQuan/p/4543387.html)

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -0,0 +1,948 @@
# 精尽 Redis 面试题「最新更新时间2024-02」
> 这个面试题是建立在胖友看过 [《精尽【缓存 】面试题》](http://svip.iocoder.cn/Cache/Interview) 。
以下面试题,基于网络整理,和自己编辑。具体参考的文章,会在文末给出所有的链接。
如果胖友有自己的疑问,欢迎在星球提问,我们一起整理吊吊的 Redis 面试题的大保健。
而题目的难度,艿艿尽量按照从容易到困难的顺序,逐步下去。
## 什么是 Redis
[Redis](http://lib.csdn.net/base/redis) ,全称 Remote Dictionary Server ,是一个基于内存的高性能 Key-Value [数据库](http://lib.csdn.net/base/mysql)。
Redis 已经成为互联网公司在缓存组件选择的唯一。例如说,在各种公有云上,缓存服务都是提供的 Redis。再例如说招聘简历要求上都会要求掌握 Redis 。
## Redis 有什么优点?
🦅 **1. 速度快**
因为数据存在内存中,类似于 HashMap HashMap 的优势就是查找和操作的时间复杂度都是O (1) 。
> Redis 本质上是一个 Key-Value 类型的内存数据库,很像 Memcached ,整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据 flush 到硬盘上进行保存。
>
> 因为是纯内存操作Redis 的性能非常出色,每秒可以处理超过 10 万次读写操作,是已知性能最快的 Key-Value 数据库。
- 如果我们查看在[阿里云销售的 Redis 规格](https://help.aliyun.com/document_detail/26350.html),最低的也是 8W QPS 。
🦅 **2. 支持丰富数据类型**
支持 String ListSetSorted SetHash 五种基础的数据结构。
> Redis 的出色之处不仅仅是性能Redis 最大的魅力是支持保存多种数据结构,此外单个 Value 的最大限制是 1GB不像 Memcached只能保存 1MB 的数据,因此 Redis 可以用来实现很多有用的功能。比方说:
>
> - 用他的 List 来做 FIFO 双向链表,实现一个轻量级的高性能消息队列服务。
> - 用他的 Set 可以做高性能的 tag 系统等等。
同时,在基础的数据结构之上,还提供 [Bitmap](http://redisdoc.com/bitmap/index.html)、[HyperLogLog](http://redisdoc.com/hyperloglog/index.html)、[GEO](http://redisdoc.com/geo/index.html) 等高级的数据结构。
如果面试想要加分,胖友一定要去看看这些高级的数据结构,面试与日常开发,必备神器。
🦅 **3. 丰富的特性**
- 订阅发布 Pub / Sub 功能
- Key 过期策略
- 事务
- 支持多个 DB
- 计数
-
并且在 Redis 5.0 增加了 Stream 功能,一个新的强大的支持多播的可持久化的消息队列,提供类似 Kafka 的功能。
🦅 **4. 持久化存储**
Redis 提供 RDB 和 AOF 两种数据的持久化存储方案,解决内存数据库最担心的万一 Redis 挂掉,数据会消失掉。
🦅 **5、高可用**
内置 Redis Sentinel ,提供高可用方案,实现主从故障自动转移。
内置 Redis Cluster ,提供集群方案,实现基于槽的分片方案,从而支持更大的 Redis 规模。
## Redis 有什么缺点?
- 1、由于 Redis 是内存数据库,所以,单台机器,存储的数据量,跟机器本身的内存大小。虽然 Redis 本身有 Key 过期策略,但是还是需要提前预估和节约内存。如果内存增长过快,需要定期删除数据。
> 另外,可使用 Redis Cluster、Codis 等方案,对 Redis 进行分区,从单机 Redis 变成集群 Redis 。
- 2、如果进行完整重同步由于需要生成 RDB 文件,并进行传输,会占用主机的 CPU ,并会消耗现网的带宽。不过 Redis2.8 版本,已经有部分重同步的功能,但是还是有可能有完整重同步的。比如,新上线的备机。
- 3、修改配置文件进行重启将硬盘中的数据加载进内存时间比较久。在这个过程中Redis 不能提供服务。
## Redis 和 Memcached 的区别有哪些?
> 艿艿:随着 Memcached 日渐没落,这个问题问的越来越少了。
🦅 **1. Redis 支持复杂的数据结构**
- Memcached 仅提供简单的字符串。
- Redis 提供复杂的数据结构,丰富的数据操作。
也因为 Redis 支持复杂的数据结构Redis 即使晚于 Memcached 推出,却获得更多开发者的青睐。
Redis 相比 Memcached 来说拥有更多的数据结构能支持更丰富的数据操作。如果需要缓存能够支持更复杂的结构和操作Redis 会是不错的选择。
🦅 **2. Redis 原生支持集群模式**
- 在 Redis3.x 版本中,官方便能支持 Cluster 模式。
- Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据。
🦅 **3. 性能对比**
- Redis 只使用单核,而 Memcached 可以使用多核,所以平均每一个核上 Redis在存储小数据时比 Memcached 性能更高。
- 在 100k 以上的数据中Memcached 性能要高于 Redis 。虽然 Redis 最近也在存储大数据的性能上进行优化,但是比起 Memcached还是稍有逊色。
更多关于性能的对比,可以看看 [《Memcached 与 Redis 的关键性能指标比较》](https://www.jianshu.com/p/34f90813d7c9) 。
🦅 **4. 内存管理机制不同**
相比来说Redis 的内存管理机制,会更加简单。
- Redis 采用的是**包装**的 malloc/free ,使用时现场申请的方式。
- Memcached 采用的是 Slab Allocation 机制管理内存,预分配的内存池的方式。
如果对比两者的内存使用效率:
- 简单的 Key-Value 存储的话Memcached 的内存利用率更高,可以使用类似内存池。
- 如果 Redis 采用 hash 结构来做 key-value 存储,由于其组合式的压缩, 其内存利用率会高于 Memcached 。
🦅 **5. 网络 IO 模型**
- Memcached 是多线程,非阻塞 IO 复用的网络模型,原型上接近 Nignx 。
- Redis 使用单线程的 IO 复用模型,自己封装了一个简单的 AeEvent 事件处理框架,主要实现了 epoll kqueue 和 select ,更接近 Apache 早期的模式。
🦅 **6. 持久化存储**
- Memcached 不支持持久化存储,重启时,数据被清空。
- Redis 支持持久化存储,重启时,可以恢复已持久化的数据。
------
也推荐阅读下 [《脚踏两只船的困惑 - Memcached 与 Redis》](https://www.imooc.com/article/23549) 。
## 请说说 Redis 的线程模型?
> 艿艿:这个是我从网络上找的资料,讲的灰常不错。**一般来说,回答道 Redis 是非阻塞 IO ,多路复用**。
Redis 内部使用文件事件处理器 `file event handler`,这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 Socket根据 Socket 上的事件来选择对应的事件处理器进行处理。
文件事件处理器的结构包含 4 个部分:
- 多个 Socket 。
- IO 多路复用程序。
- 文件事件分派器。
- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)。
多个 Socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。
来看客户端与 redis 的一次通信过程:
[![redis-single-thread-model](13-Redis 面试题.assets/01.png)](http://static.iocoder.cn/images/Redis/2019_11_22/01.png)redis-single-thread-model
- 客户端 Socket01 向 Redis 的 Server Socket 请求建立连接,此时 Server Socket 会产生一个 `AE_READABLE` 事件IO 多路复用程序监听到 server socket 产生的事件后,将该事件压入队列中。文件事件分派器从队列中获取该事件,交给`连接应答处理器`。连接应答处理器会创建一个能与客户端通信的 Socket01并将该 Socket01 的 `AE_READABLE` 事件与命令请求处理器关联。
- 假设此时客户端发送了一个 `set key value` 请求,此时 Redis 中的 Socket01 会产生 `AE_READABLE` 事件IO 多路复用程序将事件压入队列,此时事件分派器从队列中获取到该事件,由于前面 Socket01 的 `AE_READABLE` 事件已经与命令请求处理器关联,因此事件分派器将事件交给命令请求处理器来处理。命令请求处理器读取 Scket01 的 `set key value` 并在自己内存中完成 `set key value` 的设置。操作完成后,它会将 Scket01 的 `AE_WRITABLE` 事件与令回复处理器关联。
- 如果此时客户端准备好接收返回结果了,那么 Redis 中的 Socket01 会产生一个 `AE_WRITABLE` 事件,同样压入队列中,事件分派器找到相关联的命令回复处理器,由命令回复处理器对 socket01 输入本次操作的一个结果,比如 `ok`,之后解除 Socket01 的 `AE_WRITABLE` 事件与命令回复处理器的关联。
这样便完成了一次通信。😈 耐心理解一下,灰常重要。如果还是不能理解,可以在网络上搜一些资料,在理解理解。
## 为什么 Redis 单线程模型也能效率这么高?
- 1、C 语言实现。
> 我们都知道C 语言的执行速度非常快。
- 2、纯内存操作。
> Redis 为了达到最快的读写速度,将数据都读到内存中,并通过异步的方式将数据写入磁盘。所以 Redis 具有快速和数据持久化的特征。
>
> 如果不将数据放在内存中,磁盘 I/O 速度为严重影响 Redis 的性能。
- 3、基于非阻塞的 IO 多路复用机制。
- 4、单线程避免了多线程的频繁上下文切换问题。
> Redis 利用队列技术,将并发访问变为串行访问,消除了传统数据库串行控制的开销。
>
> 实际上Redis 4.0 开始,也开始有了一些异步线程,用于处理一些耗时操作。例如说,异步线程,实现[惰性删除](https://blog.csdn.net/zhanglong_4444/article/details/88350443)(解决大 KEY 删除,阻塞主线程)和异步 AOF (解决磁盘 IO 紧张时fsync 执行一次很慢)等等。
- 5、丰富的数据结构。
> Redis 全程使用 hash 结构,读取速度快,还有一些特殊的数据结构,对数据存储进行了优化。例如,压缩表,对短数据进行压缩存储;再再如,跳表,使用有序的数据结构加快读取的速度。
>
> 也因为 Redis 是单线程的,所以可以实现丰富的数据结构,无需考虑并发的问题。
## Redis 是单线程的,如何提高多核 CPU 的利用率?
可以在同一个服务器部署多个 Redis 的实例,并把他们当作不同的服务器来使用,在某些时候,无论如何一个服务器是不够的, 所以,如果你想使用多个 CPU ,你可以考虑一下分区。
## Redis 有几种持久化方式?
> 艿艿:这个问题有一丢丢长,耐心看完。
>
> 面试的时候,如果不能完整回答出来,也不会有大问题。重点,在于有条理,对 RDB 和 AOF 有理解。
🦅 **持久化方式**
Redis 提供了两种方式,实现数据的持久化到硬盘。
- 1、【全量】RDB 持久化,是指在指定的时间间隔内将内存中的**数据集快照**写入磁盘。实际操作过程是fork 一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
- 2、【增量】AOF持久化以日志的形式记录服务器所处理的每一个**写、删除操作**,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。
🦅 **RDB 优缺点**
① 优点
- 灵活设置备份频率和周期。你可能打算每个小时归档一次最近 24 小时的数据,同时还要每天归档一次最近 30 天的数据。通过这样的备份策略,一旦系统出现灾难性故障,我们可以非常容易的进行恢复。
- 非常适合冷备份对于灾难恢复而言RDB 是非常不错的选择。因为我们可以非常轻松的将一个单独的文件压缩后再转移到其它存储介质上。推荐,可以将这种完整的数据文件发送到一些远程的安全存储上去,比如说 Amazon 的 S3 云服务上去,在国内可以是阿里云的 OSS 分布式存储上。
- 性能最大化。对于 Redis 的服务进程而言,在开始持久化时,它唯一需要做的只是 fork 出子进程,之后再由子进程完成这些持久化的工作,这样就可以极大的避免服务进程执行 IO 操作了。也就是说RDB 对 Redis 对外提供的读写服务,影响非常小,可以让 Redis 保持高性能。
- 恢复更快。相比于 AOF 机制RDB 的恢复速度更更快,更适合恢复数据,特别是在数据集非常大的情况。
② 缺点
- 如果你想保证数据的高可用性,即最大限度的避免数据丢失,那么 RDB 将不是一个很好的选择。因为系统一旦在定时持久化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失。
> 所以RDB 实际场景下,需要和 AOF 一起使用。
- 由于 RDB 是通过 fork 子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是 1 秒钟。
> 所以RDB 建议在业务低估,例如在半夜执行。
🦅 **AOF 优缺点**
① 优点
- 该机制可以带来更高的
数据安全性
即数据持久性。Redis 中提供了 3 种同步策略,即每秒同步、每修改(执行一个命令)同步和不同步。
- 事实上,每秒同步也是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失。
- 而每修改同步,我们可以将其视为同步持久化,即每次发生的数据变化都会被立即记录到磁盘中。可以预见,这种方式在效率上是最低的。
- 至于不同步,无需多言,我想大家都能正确的理解它。
- 由于该机制对日志文件的写入操作采用的是
append
模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。
- 因为以 append-only 模式写入,所以没有任何磁盘寻址的开销,写入性能非常高。
- 另外,如果我们本次操作只是写入了一半数据就出现了系统崩溃问题,不用担心,在 Redis 下一次启动之前,我们可以通过 redis-check-aof 工具来帮助我们解决数据一致性的问题。
- 如果 AOF 日志过大Redis 可以自动启用 **rewrite** 机制。即使出现后台重写操作,也不会影响客户端的读写。因为在 rewrite log 的时候,会对其中的指令进行压缩,创建出一份需要恢复数据的最小日志出来。再创建新日志文件的时候,老的日志文件还是照常写入。当新的 merge 后的日志文件 ready 的时候,再交换新老日志文件即可。
> 注意AOF **rewrite** 机制,和 RDB 一样,也需要 fork 出一次子进程,如果 Redis 内存比较大,可能会因为 fork 阻塞下主进程。
- AOF 包含一个格式清晰、易于理解的日志文件用于记录所有的**修改操作**。事实上,我们也可以通过该文件完成数据的重建。
② 缺点
- 对于相同数量的数据集而言AOF 文件通常要大于 RDB 文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
- 根据同步策略的不同AOF 在运行效率上往往会慢于 RDB 。总之,每秒同步策略的效率是比较高的,同步禁用策略的效率和 RDB 一样高效。
- 以前 AOF 发生过 bug ,就是通过 AOF 记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来。所以说,类似 AOF 这种较为复杂的基于命令日志/merge/回放的方式,比基于 RDB 每次持久化一份完整的数据快照文件的方式,更加脆弱一些,容易有 bug 。不过 AOF 就是为了避免 rewrite 过程导致的 bug ,因此每次 rewrite 并不是基于旧的指令日志进行 merge 的,而是基于当时内存中的数据进行指令的重新构建,这样健壮性会好很多。
🦅 **如何选择**
- 不要仅仅使用 RDB因为那样会导致你丢失很多数据。
- 也不要仅仅使用 AOF因为那样有两个问题第一你通过 AOF 做冷备,没有 RDB 做冷备,来的恢复速度更快; 第二RDB 每次简单粗暴生成数据快照,更加健壮,可以避免 AOF 这种复杂的备份和恢复机制的 bug 。
- Redis 支持同时开启开启两种持久化方式,我们可以综合使用 AOF 和 RDB 两种持久化机制,用 AOF 来保证数据不丢失,作为数据恢复的第一选择; 用 RDB 来做不同程度的冷备,在 AOF 文件都丢失或损坏不可用的时候,还可以使用 RDB 来进行快速的数据恢复。
- 如果同时使用 RDB 和 AOF 两种持久化机制,那么在 Redis 重启的时候,会使用 **AOF** 来重新构建数据,因为 AOF 中的**数据更加完整**。
> 一般来说, 如果想达到足以媲美 PostgreSQL 的数据安全性, 你应该同时使用两种持久化功能。如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失,那么你可以只使用 RDB 持久化。
>
> 有很多用户都只使用 AOF 持久化,但并不推荐这种方式:因为定时生成 RDB 快照snapshot非常便于进行数据库备份 并且 RDB 恢复数据集的速度也要比AOF恢复的速度要快除此之外使用 RDB 还可以避免之前提到的 AOF 程序的 bug。
在 Redis4.0 版本开始,允许你使用 RDB-AOF 混合持久化方式,详细可见 [《Redis4.0 之 RDB-AOF 混合持久化》](https://yq.aliyun.com/articles/193034) 。也因此RDB 和 AOF 同时使用,是希望达到安全的持久化的推荐方式。
------
另外RDB 和 AOF 涉及的知识点蛮多的,可以看看:
- [《Redis 设计与实现 —— RDB》](https://redisbook.readthedocs.io/en/latest/internal/rdb.html)
- [《Redis 设计与实现 —— AOF》](https://redisbook.readthedocs.io/en/latest/internal/aof.html)
如下是老钱对这块的总结,可能更加适合面试的场景:
- bgsave 做镜像全量持久化AOF 做增量持久化。因为 bgsave 会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要 AOF 来配合使用。在 Redis 实例重启时,会使用 bgsave 持久化文件重新构建内存,再使用 AOF 重放近期的操作指令来实现完整恢复重启之前的状态。
> 和老钱沟通了下,最后一句重启恢复,使用的是 RDB-AOF 的混合方案。
- 对方追问那如果突然机器掉电会怎样?取决于 AOF 日志 sync 属性的配置,如果不要求性能,在每条写指令时都 sync 一下磁盘,就不会丢失数据。但是在高性能的要求下每次都 sync 是不现实的,一般都使用定时 sync ,比如 1 秒 1 次,这个时候最多就会丢失 1 秒的数据。
> 实际上,极端情况下,是最多丢失 2 秒的数据。因为 AOF 线程,负责每秒执行一次 fsync 操作,操作完成后,记录最后同步时间。主线程,负责对比上次同步时间,如果超过 2 秒,阻塞等待成功。
- 对方追问 bgsave 的原理是什么你给出两个词汇就可以了fork 和 cow 。fork 是指 Redis 通过创建子进程来进行 bgsave 操作。cow 指的是 copy on write ,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。
> 艿艿:这里 bgsave 操作后,会产生 RDB 快照文件。
为什么不建议在主 Redis 节点开启 RDB 功能呢?因为会带来一定时间的阻塞,特别是数据量大的时候。
> 如下来自球友【jian】的回答感恩~
>
> - 【重点】**子进程 fork 相关的阻塞:在 bgsave 的时候Redis 主进程会 fork 一个子进程,利用操作系统的写时复制技术,这个子进程在拷贝父进程的时候理论上是很快的,因为并不需要全拷贝,比如主进程虽然占了 10G 内存,但子进程拷贝他可能只要 200 毫秒,我认为也就阻塞了 200 毫秒(此耗时基本跟主进程占用的内存是成正比的),这个具体的时间可以通过统计项 info stats 里的 last_fork_usec 查看。**
> - CPU 单线程相关的阻塞Redis 主进程是单线程跑在单核 CPU 上的如果显示绑定了CPU ,则子进程会与主进程共享一个 CPU 而子进程进行持久化的时候是非常占CPU强势 90%因此这种情况也可能导致提供服务的主进程发生阻塞因此如果需要持久化功能不建议绑定CPU
> - 内存相关的阻塞虽然利用写时复制技术可以大大降低进程拷贝的内存消耗但这也导致了父进程在处理写请求时需要维护修改的内存页因此这部分内存过大的话修改页数多或每页占空间大也会导致父进程的写操作阻塞。而不巧的是Linux中TransparentHugePage 会将复制内存页面单位有 4K 变成 2M ,这对于 Redis 来说是比较不友好的,也是建议优化的,具体可百度之)
> - 磁盘相关的阻塞极端情况下假设整个机器的内存已经所剩无几触发了内存交换SWAP则整个 Redis的效率将会非常低下显然这不仅仅针对 save/bgsave ),因此,关注系统的 io 情况,也是定位阻塞问题的一种方法。
>
> 艿艿后来又看了下这个答案,是 [《Redis 开发与运维》](https://u.jd.com/lDNJa9) 的「5.3 持久化 —— 问题定位于优化」小节。
## Redis 有几种数据“过期”策略?
Redis 的过期策略,就是指当 Redis 中缓存的 key 过期了Redis 如何处理。
Redis 提供了 3 种数据过期策略:
- 被动删除:当读/写一个已经过期的 key 时,会触发惰性删除策略,直接删除掉这个过期 key 。
- 主动删除:由于惰性删除策略无法保证冷数据被及时删掉,所以 Redis 会定期主动淘汰一批已过期的 key 。
- 主动删除:当前已用内存超过 maxmemory 限定时,触发主动清理策略,即 [「数据“淘汰”策略」](https://svip.iocoder.cn/Redis/Interview/#) 。
在 Redis 中,同时使用了上述 3 种策略,即它们**非互斥**的。
想要进一步了解,可以看看 [《关于 Redis 数据过期策略》](https://www.cnblogs.com/chenpingzhao/p/5022467.html) 文章。
## Redis 有哪几种数据“淘汰”策略?
Redis 内存数据集大小上升到一定大小的时候,就会进行数据淘汰策略。
Redis 提供了 6 种数据淘汰策略:
1. volatile-lru
2. volatile-ttl
3. volatile-random
4. allkeys-lru
5. allkeys-random
6. 【默认策略】no-enviction
具体的**每种数据淘汰策略的定义**,和**如何选择讨论策略**,可见 [《Redis实战 内存淘汰机制》](http://blog.720ui.com/2016/redis_action_02_maxmemory_policy/) 。
在 Redis 4.0 后,基于 LFULeast Frequently Used最近最少使用算法增加了 2 种淘汰策略:
1. volatile-lfu
2. allkeys-lfu
🦅 **Redis LRU 算法**
另外Redis 的 LRU 算法,**并不是一个严格的 LRU 实现**。这意味着 Redis 不能选择最佳候选键来回收也就是最久未被访问的那些键。相反Redis 会尝试执行一个近似的 LRU 算法,通过采样一小部分键,然后在采样键中回收最适合(拥有最久未被访问时间)的那个。
**Redis 没有使用真正实现严格的 LRU 算是的原因是,因为消耗更多的内存。然而对于使用 Redis 的应用来说,使用近似的 LRU 算法,事实上是等价的。**
具体的可以看看如下文章:
- [《想不到面试官问我Redis 内存满了怎么办?》](http://www.iocoder.cn/Fight/Cannot-think-of-The-interviewer-asked-me-what-if-Redis-runs-out-of-memory/?self)
- [《使用 Redis 作为一个 LRU 缓存》](http://ifeve.com/lru-cache/)
🦅 **MySQL 里有 2000w 数据Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?**
> 艿艿:这个是从网络上找到的一个神奇的问题,并且看了答案之后,觉得有点莫名的对不上。
>
> 所以,感觉这个问题的目的是,如何保证热点数据不要被淘汰。
在 [「Redis 有哪几种数据“淘汰”策略?」](https://svip.iocoder.cn/Redis/Interview/#) 问题中我们已经看到“Redis 内存数据集大小上升到一定 maxmemory 的时候,就会进行数据淘汰策略。” 。
那么,如果我们此时要保证热点数据不被淘汰,那么需要选择 volatile-lru 或 allkeys-lru 这两个基于 LRU 算法的淘汰策略。
相比较来说,最终会选择 allkeys-lru 淘汰策略。原因是,如果我们的应用对缓存的访问符合幂律分布,也就是存在相对热点数据,或者我们不太清楚我们应用的缓存访问分布状况,我们可以选择 allkeys-lru 策略。如果在 Redis 4.0 版本,可以考虑使用 volatile-lfu ,更加符合“热”的概念,频率越高,代表越热。
🦅 **Redis 回收进程如何工作的?**
理解回收进程如何工作是非常重要的:
- 一个客户端运行了新的写命令,添加了新的数据。
- Redis 检查内存使用情况,如果大于 maxmemory 的限制, 则根据设定好的策略进行回收。
- Redis 执行新命令。
所以我们不断地穿越内存限制的边界,通过不断达到边界然后不断地回收回到边界以下(跌宕起伏)。
## 如果有大量的 key 需要设置同一时间过期,一般需要注意什么?
如果大量的 key 过期时间设置的过于集中到过期的那个时间点Redis可能会出现短暂的卡顿现象。
一般需要在时间上加一个随机值,使得过期时间分散一些。
------
上次基友也碰到这个问题,请教了下,他的方案是调大 hz 参数,每次过期的 key 更多,从而最终达到避免一次过期过多。
> 这个定期的频率,由配置文件中的 hz 参数决定代表了一秒钟内后台任务期望被调用的次数。Redis 3.0.0 中的默认值是 10 ,代表每秒钟调用 10 次后台任务。
>
> hz 调大将会提高 Redis 主动淘汰的频率,如果你的 Redis 存储中包含很多冷数据占用内存过大的话,可以考虑将这个值调大,但 Redis 作者建议这个值不要超过 100 。我们实际线上将这个值调大到 100 ,观察到 CPU 会增加 2% 左右,但对冷数据的内存释放速度确实有明显的提高(通过观察 keyspace 个数和 used_memory 大小)。
## Redis 有哪些数据结构?
如果你是 Redis 普通玩家,可能你的回答是如下五种数据结构:
- 字符串 String
- 字典Hash
- 列表List
- 集合Set
- 有序集合 SortedSet
如果你是 Redis 中级玩家,还需要加上下面几种数据结构:
- HyperLogLog
- Geo
- Bitmap
如果你是 Redis 高端玩家,你可能玩过 Redis Module ,可以再加上下面几种数据结构:
- BloomFilter
- RedisSearch
- Redis-ML
- JSON
另外,在 Redis 5.0 增加了 Stream 功能,一个新的强大的支持多播的可持久化的消息队列,提供类似 Kafka 的功能。😈 默默跟面试官在装一波。
## 聊聊 Redis 使用场景
Redis 可用的场景非常之多:
- 数据缓存
- 会话缓存
- 时效性数据
- 访问频率
- 计数器
- 社交列表
- 记录用户判定信息
- 交集、并集和差集
- 热门列表与排行榜
- 最新动态
- 消息队列
- 分布式锁
详细的介绍,可以看看如下文章:
- [《聊聊 Redis 使用场景》](http://blog.720ui.com/2017/redis_core_use/)
- [《Redis 应用场景及实例》](https://www.jianshu.com/p/af277c77b1c9)
- [《Redis 常见的应用场景解析》](https://zhuanlan.zhihu.com/p/29665317)
- [《Redis 和 Memcached 各有什么优缺点,主要的应用场景是什么样的?》](https://www.zhihu.com/question/19829601)
## Redis 支持的 Java 客户端都有哪些?
使用比较广泛的有三个 Java 客户端:
- Redisson
> Redisson ,是一个高级的分布式协调 Redis 客服端,能帮助用户在分布式环境中轻松实现一些 Java 的对象 (Bloom filter, BitSet, Set, SetMultimap, ScoredSortedSet, SortedSet, Map, ConcurrentMap, List, ListMultimap, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, ReadWriteLock, AtomicLong, CountDownLatch, Publish / Subscribe, HyperLogLog)。
- Jedis
> Jedis 是 Redis 的 Java 实现的客户端,其 API 提供了比较全面的 Redis 命令的支持。
>
> Redisson 实现了分布式和可扩展的 Java 数据结构,和 Jedis 相比Jedis 功能较为简单。
>
> Redisson 的宗旨是促进使用者对 Redis 的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。
- Lettuce
> Lettuce 是一个可伸缩线程安全的 Redis 客户端。多个线程可以共享同一个 RedisConnection 。它利用优秀 Netty NIO 框架来高效地管理多个连接。
Redis 官方推荐使用 Redisson 或 Jedis 。
Spring Boot 2.x 内置支持 Jedis 和 Lettuce 。一般情况下,建议:
- 使用 Spring Data Redis ,提供了透明使用 Jedis 和 Lettuce 的封装。也就是说,大多数时候,我们可以通过配置使用 Jedis 或 Lettuce 进行 Redis 的操作,而上层使用 Spring Data Redis 提供的统一 API 。
- 从目前来说Jedis 会比 Lettuce 更加流行,并且更加稳定。虽然说 Jedis 有一段时间,不再进行更新,但是突然又开始更新,可能是诈尸了。
- 如果想要更加丰富的特性,例如说分布式锁,布隆过滤器,可以考虑研究下 Redisson 。
## 如何使用 Redis 实现分布式锁?
Redis 实现分布式锁,需要考虑如下几个方面:
- 1、正确的获得锁
> set 指令附带 nx 参数,保证有且只有一个进程获得到。
- 2、正确的释放锁
> 使用 Lua 脚本,比对锁持有的是不是自己。如果是,则进行删除来释放。
- 3、超时的自动释放锁
> set 指令附带 expire 参数,通过过期机制来实现超时释放。
- 4、未获得到锁的等待机制
> sleep 或者基于 Redis 的订阅 Pub/Sub 机制。
>
> 一些业务场景,可能需要支持获得不到锁,直接返回 false ,不等待。
- 5、【可选】锁的重入性
> 通过 ThreadLocal 记录是第几次获得相同的锁。
>
> 1有且第一次计数为 1 && 获得锁时,才向 Redis 发起获得锁的操作。
> 2有且计数为 0 && 释放锁时,才向 Redis 发起释放锁的操作。
- 6、锁超时的处理
> 一般情况下,可以考虑告警 + 后台线程自动续锁的超时时间。通过这样的机制,保证有且仅有一个线程,正在持有锁。
- 7、Redis 分布式锁丢失问题
> 具体看「方案二Redlock」。
下面,我们来详细说下每个方案。
🦅 **方案一set 指令**
先拿 setnx 来争抢锁,抢到之后,再用 expire 给锁加一个过期时间防止锁忘记了释放。
- 这时候对方会告诉你说你回答得不错,然后接着问如果在 setnx 之后执行 expire 之前进程意外 crash 或者要重启维护了,那会怎么样?
- 这时候你要给予惊讶的反馈:唉,是喔,这个锁就永远得不到释放了。紧接着你需要抓一抓自己得脑袋,故作思考片刻,好像接下来的结果是你主动思考出来的,然后回答:我记得 set 指令有非常复杂的参数,这个应该是可以同时把 setnx 和 expire 合成一条指令来用的!对方这时会显露笑容,心里开始默念:摁,这小子还不错。
所以,我们可以使用 **set** 指令,实现分布式锁。指令如下:
```
SET key value [EX seconds] [PX milliseconds] [NX|XX]
```
- 可以使用 `SET key value EX seconds NX` 命令,尝试获得锁。
- 具体的实现,可以参考如下文章:
- [《精尽 Redisson 源码分析 —— 可重入分布式锁 ReentrantLock》](http://svip.iocoder.cn/Redisson/ReentrantLock/?self)
- [《Redis 分布式锁进化史解读 + 缺陷分析》](http://www.iocoder.cn/Fight/redisfen-bu-shi-suo-jin-hua-shi/?self)
- [《Redis 分布式锁的正确实现方式Java 版)》](http://www.iocoder.cn/Fight/Correct-implementation-of-Redis-distributed-locks-by-Java/?self)
🦅 **方案二Redlock**
set 指令的方案,适合用于在单机 Redis 节点的场景下,在多 Redis 节点的场景下会存在分布式锁丢失的问题。所以Redis 作者 Antirez 基于分布式环境下提出了一种更高级的分布式锁的实现方式Redlock 。
具体的源码解析,可以看看 [《精尽 Redisson 源码分析 —— 可靠分布式锁 RedLock》](http://svip.iocoder.cn/Redisson/RedLock/?self) 文章。
具体的方案,胖友可以看看老友飞哥的两篇博客:
- [《RedlockRedis分布式锁最牛逼的实现》](https://mp.weixin.qq.com/s/JLEzNqQsx-Lec03eAsXFOQ)
- [《Redisson 实现 Redis 分布式锁的 N 种姿势》](https://www.jianshu.com/p/f302aa345ca8)
最近艿艿画了一个 Redisson 实现分布式锁的流程图,胖友可以点击[传送门](https://www.processon.com/view/link/5f4c871d079129356ec6f4d7)阅读。
🦅 **对比 Zookeeper 分布式锁**
- 从可靠性上来说Zookeeper 分布式锁好于 Redis 分布式锁。
- 从性能上来说Redis 分布式锁好于 Zookeeper 分布式锁。
所以,没有绝对的好坏,可以根据自己的业务来具体选择。如果想要更简单,甚至可以考虑基于 MySQL 行锁来实现分布式锁。
## 如何使用 Redis 实现分布式限流?
在 Spring Cloud Gateway 中,提供了 Redis 分布式限流器的实现,具体直接看艿艿写的 [《Spring-Cloud-Gateway 源码解析 —— 过滤器 (4.10) 之 RequestRateLimiterGatewayFilterFactory 请求限流》](http://www.iocoder.cn/Spring-Cloud-Gateway/filter-request-rate-limiter/) 的 [「5.3 Redis Lua 脚本」](https://svip.iocoder.cn/Redis/Interview/#) 部分。
另外Redisson 库中,也提供了 Redis 分布式限流的实现,不过需要使用 Pro 版本。
🦅 **请用 Redis 和任意语言实现一段恶意登录保护的代码,限制 1 小时内每用户 Id 最多只能登录 5 次。**
这个问题,关键点,就是每个用户,每 3600 秒,只能登陆 5 次。这么一想,其实就是一个如何使用 Redis 实现限流的问题。Redis 实现限流,一共有两种方案:
- 使用 zset 实现滑动窗口限流。代码如下:
```
public boolean isActionAllowed(String userId, String actionKey, int period,
int maxCount) {
String key = String.format("hist:%s:%s", userId, actionKey); // 使用用户编号 + 行为作为 KEY 。这样,我们就可以统计某个用户的操作行为。
long nowTs = System.currentTimeMillis(); // 获取当前时间。
Pipeline pipe = jedis.pipelined(); // pipeline 批量操作,提升效率。
pipe.multi(); // 此处启动了事务,可以保证指令的原子性。
pipe.zadd(key, nowTs, "" + nowTs); // zset 添加key value score 要看下。
pipe.zremrangeByScore(key, 0, nowTs - (period * 1000)); // zremrangeByScore ,移除超过周期的 value 。
Response<Long> count = pipe.zcard(key); // zcard ,计算 zset 的数量
pipe.expire(key, period + 1); // 设置过期。这里多 + 1 秒,为了防止网络延迟。
pipe.exec(); // pipeline 执行
pipe.close();
return count.get() <= maxCount; // 是否超过最大次数。
}
```
- 该实现会存在一个问题,可能一个无效的操作,也被记录到次数中。完美的话,可能需要基于 Lua 脚本实现。
- 另外,上述代码是每秒操作的时间,实际需要改成每 N 秒。比较简单,直接上手怼即可。
- 使用 Lua 脚本,实现令牌桶限流算法。具体可以看看艿艿对 [《Spring-Cloud-Gateway 源码解析 —— 过滤器 (4.10) 之 RequestRateLimiterGatewayFilterFactory 请求限流》](http://www.iocoder.cn/Spring-Cloud-Gateway/filter-request-rate-limiter/?self) 的源码解析。
- 使用 Lua 脚本,实现简单的滑动窗口。具体可以看看艿艿对 [《精尽 Redisson 源码分析 —— 限流器 RateLimiter》](http://svip.iocoder.cn/Redisson/RateLimiter/?self) 的源码解析。
## 如何使用 Redis 实现消息队列?
一般使用 list 结构作为队列rpush 生产消息lpop 消费消息。当 lpop 没有消息的时候,要适当 sleep 一会再重试。
- 如果对方追问可不可以不用 sleep 呢list 还有个指令叫 blpop ,在没有消息的时候,它会阻塞住直到消息到来。
- 如果对方追问能不能生产一次消费多次呢?使用 pub / sub 主题订阅者模式,可以实现 1:N 的消息队列。
- 如果对方追问 pub / sub 有什么缺点?在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如 rabbitmq 等。
> 之前生产中,艿艿就碰到因为网络闪断,导致订阅的 pub/sub 消息丢失,导致 JVM 应用的数据字典和系统参数等缓存未刷新,业务受到影响。所以,最好还是使用专业的消息队列的订阅功能(广播消费)。
- 如果对方追问 redis 如何实现延时队列?我估计现在你很想把面试官一棒打死如果你手上有一根棒球棍的话,怎么问的这么详细。但是你很克制,然后神态自若的回答道:使用 sortedset ,拿时间戳作为 score ,消息内容作为 key 调用 zadd 来生产消息,消费者用 zrangebyscore 指令获取 N 秒之前的数据轮询进行处理。
> 可能很多胖友会觉得抽象,可以看看 [《Redis 学习笔记之延时队列》](https://cloud.tencent.com/developer/article/1401122) 。面试中,能回答到 Redis zset 实现延迟队列,还是蛮加分的。
到这里,面试官暗地里已经对你竖起了大拇指。但是他不知道的是此刻你却竖起了中指,在椅子背后。
当然,实际上 Redis 真的真的真的不推荐作为消息队列使用,它最多只是消息队列的存储层,上层的逻辑,还需要做大量的封装和支持。
另外,在 Redis 5.0 增加了 Stream 功能,一个新的强大的支持多播的可持久化的消息队列,提供类似 Kafka 的功能。
## 什么是 Redis Pipelining
一次请求/响应服务器能实现处理新的请求即使旧的请求还未被响应。这样就可以将多个命令发送到服务器,而不用等待回复,最后在一个步骤中读取该答复。
> 注意Redis Pipelining 是 Redis Client 实现的功能,而不是 Redis Server 提供的特性。假设我们有 3 个请求进行下举例子。
>
> - 未使用 Pipeline 时那么整个执行的顺序是req1->resp1->req2->resp2->req3->resp3 。
> - 在使用 Pipeline 时,那么整个执行的顺序是,[req1,req2,req3] 一起发给 Redis Server ,而 Redis Server 收到请求后,一个一个请求进行执行,然后响应,不会进行什么特殊处理。而 Client 在收到 resp1,resp2,resp3 后,进行响应给业务上层。
>
> 所以Pipeline 的作用,是避免每发一个请求,就阻塞等待这个请求的结果。
这就是管道pipelining是一种几十年来广泛使用的技术。例如许多 POP3 协议已经实现支持这个功能,大大加快了从服务器下载新邮件的过程。
Redis 很早就支持管道([pipelining](http://redis.cn/topics/pipelining.html)技术因此无论你运行的是什么版本你都可以使用管道pipelining操作 Redis。
🦅 **Redis 如何做大量数据插入?**
Redis 2.6 开始Redis-cli 支持一种新的被称之为 pipe mode 的新模式用于执行大量数据插入工作。
具体可见 [《Redis 大量数据插入》](http://www.redis.cn/topics/mass-insert.html) 文章。
## 什么是 Redis 事务?
和众多其它数据库一样Redis 作为 NoSQL 数据库也同样提供了事务机制。在 Redis 中MULTI / EXEC / DISCARD / WATCH 这四个命令是我们实现事务的基石。相信对有关系型数据库开发经验的开发者而言这一概念并不陌生,即便如此,我们还是会简要的列出 Redis 中事务的实现特征:
- 1、在事务中的所有命令都将会被串行化的顺序执行事务执行期间Redis 不会再为其它客户端的请求提供任何服务,从而保证了事物中的所有命令被原子的执行。
> Lua 脚本,也能实现该功能。
- 2、和关系型数据库中的事务相比在 Redis 事务中如果有某一条命令执行失败,其后的命令仍然会被继续执行。
> 这一点,非常重要。回答错了,就回家面壁思过,一天不许喝可乐。
>
> 这一点,是 Lua 脚本不具备的。
- 3、我们可以通过 MULTI 命令开启一个事务,有关系型数据库开发经验的人可以将其理解为 `"BEGIN TRANSACTION"` 语句。在该语句之后执行的命令,都将被视为事务之内的操作,最后我们可以通过执行 EXEC / DISCARD 命令来提交 / 回滚该事务内的所有操作。这两个 Redis 命令,可被视为等同于关系型数据库中的 COMMIT / ROLLBACK 语句。
> 开启事务后,所有语句,发送给 Redis Server ,都会暂存在 Server 中。
- 4、在事务开启之前如果客户端与服务器之间出现通讯故障并导致网络断开其后所有待执行的语句都将不会被服务器执行。然而如果网络中断事件是发生在客户端执行 EXEC 命令之后,那么该事务中的所有命令都会被服务器执行。
🦅 **如何实现 Redis CAS 操作?**
在 Redis 的事务中WATCH 命令可用于提供 CAS(check-and-set) 功能。
假设我们通过 WATCH 命令在事务执行之前监控了多个 keys ,倘若在 WATCH 之后有任何 Key 的值发生了变化EXEC 命令执行的事务都将被放弃,同时返回 `nil` 应答以通知调用者事务执行失败。
具体的示例,可以看看 [《Redis 事务锁 CAS 实现以及深入误区》](https://www.jianshu.com/p/0244a875aa26) 。
## Redis 集群都有哪些方案?
Redis 集群方案如下:
- 1、Redis Sentinel
- 2、Redis Cluster
- 3、Twemproxy
- 4、Codis
- 5、客户端分片
关于前四种,可以看看 [《Redis 实战(四)集群机制》](http://blog.720ui.com/2016/redis_action_04_cluster/) 这篇文章。
关于最后一种,客户端分片,在 Redis Cluster 出现之前使用较多,目前已经使用比较少了。实现方式如下:
> 在业务代码层实现,起几个毫无关联的 Redis 实例,在代码层,对 Key 进行 hash 计算,然后去对应的 Redis 实例操作数据。
>
> 这种方式对 hash 层代码要求比较高,考虑部分包括,节点失效后的替代算法方案,数据震荡后的自动脚本恢复,实例的监控,等等。
🦅 **选择**
目前一般在选型上来说:
- 体量较小时,选择 Redis Sentinel ,单主 Redis 足以支撑业务。
- 体量较大时,选择 Redis Cluster ,通过分片,使用更多内存。
> 关于这个问题,多大体量需要使用 Redis Cluster 呢?朋友的建议是 10G+ 的时候。主要原因是:
>
> - 1、一次 RDB 时间随着内存越大,会变大越来越久。同时,一次 fork 的时间也会变久。还有,重启通过 RDB 文件,或者 AOF 日志,恢复时间都会变长。
> - 2、体量大之后读写的 QPS 势必比体量小的时候打的多,那么使用 Redis Cluster 相比 Redis Sentinel ,可以分散读写压力到不同的集群中。
🦅 **Redis 集群如何扩容?**
- ~~如果 Redis 被当做**缓存**使用,使用一致性哈希实现动态扩容缩容。~~
> 删除的原因是,不考虑客户端分片的情况,目前基本已经不在用了。
- 如果 Redis 被当做一个**持久化存**储使用,必须使用固定的 keys-to-nodes 映射关系,节点的数量一旦确定不能变化。否则的话(即 Redis 节点需要动态变化的情况),必须使用可以在运行时进行数据再平衡的一套系统,而当前只有 Redis Cluster、Codis 可以做到这样。
如果是 Redis Cluster 集群的扩容,可以看看 [《Redis 开发与运维》](https://u.jd.com/lDNJa9) 的「10.4 集群 —— 集群伸缩」小节。简单来说,一共三步:
- 1、准备新节点。
- 2、加入集群。
- 3、迁移槽和数据。
## 什么是 Redis 主从同步?
**Redis 主从同步**
Redis 的主从同步(replication)机制,允许 Slave 从 Master 那里,通过网络传输拷贝到完整的数据备份,从而达到主从机制。
- 主数据库可以进行读写操作,当发生写操作的时候自动将数据同步到从数据库,而从数据库一般是只读的,并接收主数据库同步过来的数据。
- 一个主数据库可以有多个从数据库,而一个从数据库只能有一个主数据库。
- 第一次同步时,主节点做一次 bgsave 操作,并同时将后续修改操作记录到内存 buffer ,待完成后将 RDB 文件全量同步到复制节点,复制节点接受完成后将 RDB 镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。
**好处**
通过 Redis 的复制功,能可以很好的实现数据库的读写分离,提高服务器的负载能力。主数据库主要进行写操作,而从数据库负责读操作。
> 实际上,我们不是非常推荐在 Redis 中,使用读写分离。主要有两个原因:
>
> - Redis Sentinel 只保证主节点的故障的失效转移,而例如说 Jedis 库也只监听了主节点的变化但是从节点故障的情况Jedis 是不进行处理的。这就会导致Jedis 读会访问到从节点导致问题。当然Redisson 库的功能比较强大,已经支持从节点的故障监听。
> - 如果到达需要读写分离的体量,一般写操作也不一定会少,可以考虑上 Redis Cluster 方案,更加可靠。
------
Redis 主从同步,是很多 Redis 集群方案的基础,例如 Redis Sentinel、Redis Cluster 等等。
更多详细,可以看看如下:
> 因为主从复制的内容很多,艿艿这里就不详细哔哔了。实际场景下,对于开发的面试,我们也不会特别问,毕竟更偏运维的内容。
- [《Redis 官方文档 —— 复制》](http://redis.cn/topics/replication.html)
- [《Redis 开发与运维》](https://u.jd.com/lDNJa9) 的「6. 复制」章节,更加详细完整。
## 如何使用 Redis Sentinel 实现高可用?
详细,可以看看如下:
> 因为 Redis Sentinel 的内容很多,艿艿这里就不详细哔哔了。实际场景下,对于开发的面试,我们也不会特别问,毕竟更偏运维的内容。
- [《Redis 官方文档 —— Sentinel 高可用》](http://redis.cn/topics/sentinel.html)
- [《Redis 开发与运维》](https://u.jd.com/lDNJa9) 的「9. 哨兵」章节,更加详细完整。
## 如果使用 Redis Cluster 实现高可用?
详细,可以看看如下:
> 因为 Redis Sentinel 的内容很多,艿艿这里就不详细哔哔了。实际场景下,对于开发的面试,我们也不会特别问,毕竟更偏运维的内容。
- [《Redis 官方文档 —— Redis Cluster 集群》](http://redis.cn/topics/cluster-tutorial.html)
- [《Redis 开发与运维》](https://u.jd.com/lDNJa9) 的「10. 集群」章节,更加详细完整。
🦅 **说说 Redis 哈希槽的概念?**
Redis Cluster 没有使用一致性 hash ,而是引入了哈希槽的概念。
Redis 集群有 16384 个哈希槽,每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽,集群的每个节点负责一部分 hash 槽。
因为最大是 16384 个哈希槽,所以考虑 Redis 集群中的每个节点都能分配到一个哈希槽,所以最多支持 16384 个 Redis 节点。
为什么是 16384 呢?主要考虑集群内的网络带宽,而 16384 刚好是 2K 字节大小。
🦅 **Redis Cluster 的主从复制模型是怎样的?**
为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所以集群使用了**主从复制**模型,每个节点都会有 N-1 个复制节点。
所以Redis Cluster 可以说是 Redis Sentinel 带分片的加强版。也可以说:
- Redis Sentinel 着眼于高可用,在 master 宕机时会自动将 slave 提升为 master ,继续提供服务。
- Redis Cluster 着眼于扩展性,在单个 Redis 内存不足时,使用 Cluster 进行分片存储。
🦅 **Redis Cluster 方案什么情况下会导致整个集群不可用?**
有 ABC 三个节点的集群,在没有复制模型的情况下,如果节点 B 宕机了,那么整个集群就会以为缺少 5501-11000 这个范围的槽而不可用。当然,这种情况也可以配置 `cluster-require-full-coverage=no` ,整个集群无需所有槽位覆盖。
🦅 **Redis Cluster 会有写操作丢失吗?为什么?**
Redis 并不能保证数据的强一致性,而是【异步复制】,这意味这在实际中集群在特定的条件下可能会丢失写操作。
> 艿艿:一定一定一定要注意,无论对于 Redis Sentinel 还是 Redis Cluster 方案,都是通过主从复制,所以在数据的复制方面,都存在相同的情况。
🦅 **Redis 集群如何选择数据库?**
Redis 集群目前无法做数据库选择,默认在 0 数据库。
🦅 **请说说生产环境中的 Redis 是怎么部署的?**
> 重点问题,仔细理解。
- Redis Cluster 10 台机器5 台机器部署了 Redis 主实例,另外 5 台机器部署了 Redis 的从实例每个主实例挂了一个从实例5 个节点对外提供读写服务,每个节点的读写高峰 qps 可能可以达到每秒 5 万5 台机器最多是 25 万读写请求每秒。
- 机器是什么配置32G 内存 + 8 核 CPU + 1T 磁盘,但是分配给 Redis 进程的是 10G 内存一般线上生产环境Redis 的内存尽量不要超过 10G超过 10G 可能会有问题。那么5 台机器对外提供读写,一共有 50G 内存。
- 因为每个主实例都挂了一个从实例所以是高可用的任何一个主实例宕机都会自动故障迁移Redis 从实例会自动变成主实例继续提供读写服务。
- 你往内存里写的是什么数据?每条数据的大小是多少?商品数据,每条数据是 10kb 。100 条数据是 1mb 10 万条数据是 1G 。常驻内存的是 200 万条商品数据,占用内存是 20G ,仅仅不到总内存的 50% 。目前高峰期每秒就是 3500 左右的请求量。
> 一般来说,当公司体量大了之后,建议是一个业务线独占一个或多个 Redis Cluster 集群,实现好业务线与业务线之间的隔离。
- 其实大型的公司,会有基础架构的 Team 负责缓存集群的运维。
## 什么是 Redis 分区?
> 这个问题,和 [「Redis 集群都有哪些方案?」](https://svip.iocoder.cn/Redis/Interview/#) 是同类问题。
>
> 简单看看即可,重点还是去理解 Redis Cluster 集群方案。
🦅 关于如下四个问题,直接看 [《Redis 分区》](http://www.runoob.com/redis/redis-partitioning.html) 文章。
- Redis 分区是什么?
- 分区的优势?
- 分区的不足?
- 分区类型?
可能有胖友会懵逼,又是 Redis 主从复制,又是 Redis 分区,又是 Redis 集群。傻傻分不清啊!
- Redis 分区是一种模式,将数据分区到不同的 Redis 节点上,而 Redis 集群的 Redis Cluster、Twemproxy、Codis、客户端分片( 不包括 Redis Sentinel ) 这四种方案,是 Redis 分区的具体实现。
> 注意Redis Sentinel 实现的是 Redis 的高可用,一定要分清楚。实际上,胖友可以对比 MySQL 和 MongoDB 的高可用、集群的方案,发现思路都是一致的。
- Redis 每个分区,如果想要实现高可用,需要使用到 Redis 主从复制。
🦅 **你知道有哪些 Redis 分区实现方案**
Redis 分区方案,主要分成两种类型:
- 客户端分区,就是在客户端就已经决定数据会被存储到哪个 Redis 节点或者从哪个 Redis 节点读取。大多数客户端已经实现了客户端分区。
- 案例Redis Cluster 和客户端分区。
- 代理分区,意味着客户端将请求发送给代理,然后代理决定去哪个节点写数据或者读数据。代理根据分区规则决定请求哪些 Redis 实例,然后根据 Redis 的响应结果返回给客户端。
- 案例Twemproxy 和 Codis 。
查询路由(Query routing)的意思,是客户端随机地请求任意一个 Redis 实例,然后由 Redis 将请求转发给正确的 Redis 节点。Redis Cluster 实现了一种混合形式的查询路由但并不是直接将请求从一个Redis 节点转发到另一个 Redis 节点,而是在客户端的帮助下直接 Redirect 到正确的 Redis 节点。
> Redis Cluster 的重定向,可以认真看看 [《Redis 开发与运维》](https://u.jd.com/lDNJa9) 的「10.5 集群 - 请求路由」章节。
🦅 **分布式 Redis 是前期做还是后期规模上来了再做好?为什么??**
如下是网络上的一个答案:
> 既然 Redis 是如此的轻量,为防止以后的扩容,最好的办法就是一开始就启动较多实例。即便你只有一台服务器,你也可以一开始就让 Redis 以分布式的方式运行,使用分区,在同一台服务器上启动多个实例。
>
> 一开始就多设置几个 Redis 实例,例如 32 或者 64 个实例,对大多数用户来说这操作起来可能比较麻烦,但是从长久来看做这点牺牲是值得的。
>
> 这样的话,当你的数据不断增长,需要更多的 Redis 服务器时,你需要做的就是仅仅将 Redis 实例从一台服务迁移到另外一台服务器而已(而不用考虑重新分区的问题)。一旦你添加了另一台服务器,你需要将你一半的 Redis 实例从第一台机器迁移到第二台机器。
- 和飞哥沟通了下,这个操作不是很合理。
- 无论怎么说,建议,需要搭建下 Redis Sentinel 高可用,至于拓展性,根据自己的情况,是否使用 Redis Cluster 集群。同时, Redis Cluster 集群会有运维的复杂性,同时会存在跨分片操作(例如说 mget 等等)、事务等操作是不支持的。
## Redis 有哪些重要的健康指标?
推荐阅读 [《Redis 几个重要的健康指标》](https://mp.weixin.qq.com/s/D_khsApGkRckEoV75pYpDA)
- 存活情况
- 连接数
- 阻塞客户端数量
- 使用内存峰值
- 内存碎片率
- 缓存命中率
- OPS
- 持久化
- 失效KEY
- 慢日志
**如何提高 Redis 命中率?**
推荐阅读 [《如何提高缓存命中率Redis》](http://www.cnblogs.com/shamo89/p/8383915.html) 。
## 怎么优化 Redis 的内存占用?
推荐阅读 [《Redis 的内存优化》](https://www.jianshu.com/p/8677603d3865)
- redisObject 对象
- 缩减键值对象
- 共享对象池
- 字符串优化
- 编码优化
- 控制 key 的数量
🦅 **一个 Redis 实例最多能存放多少的 keysList、Set、Sorted Set 他们最多能存放多少元素?**
一个 Redis 实例,最多能存放多少的 keys List、Set、Sorted Set 他们最多能存放多少元素。
理论上Redis 可以处理多达 2^32 的 keys ,并且在实际中进行了测试,每个实例至少存放了 2 亿 5 千万的 keys。
任何 list、set、和 sorted set 都可以放 2^32 个元素。
🦅 **假如 Redis 里面有 1 亿个 key其中有 10w 个 key 是以某个固定的已知的前缀开头的,如果将它们全部找出来?**
使用 `keys` 指令可以扫出指定模式的 key 列表。
- 对方接着追问:如果这个 Redis 正在给线上的业务提供服务,那使用 `keys` 指令会有什么问题?
- 这个时候你要回答 Redis 关键的一个特性Redis 的单线程的。`keys` 指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用 `scan` 指令,`scan` 指令可以无阻塞的提取出指定模式的 key 列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用 `keys` 指令长。
## Redis 常见的性能问题都有哪些?如何解决?
- 1、**Master 最好不要做任何持久化工作,如 RDB 内存快照和 AOF 日志文件**。
> 经过和朋友讨论,主节点开启 AOF 日志功能,尽量避免 AOF 重写。
- Master 写内存快照save 命令调度 rdbSave 函数,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务,所以 Master 最好不要写内存快照。
- Master AOF 持久化,如果不重写 AOF 文件,这个持久化方式对性能的影响是最小的,但是 AOF 文件会不断增大AOF 文件过大会影响 Master 重启的恢复速度。
- 所以Master 最好不要做任何持久化工作,包括内存快照和 AOF 日志文件,特别是不要启用内存快照做持久化。如果数据比较关键,某个 Slave 开启AOF备份数据策略为每秒同步一次。
- 2、Master 调用 BGREWRITEAOF 重写 AOF 文件AOF 在重写的时候会占大量的 CPU 和内存资源,导致服务 load 过高,出现短暂服务暂停现象。
- 一般来说,出现这个问题,很多时候是因为 Master 的内存过大,一次 AOF 重写需要占用的 CPU 和内存的资源较多,此时可以考虑 Redis Cluster 方案。
- 3、尽量避免在压力很大的主库上增加过多的从库。
- 可以考虑在从上挂载其它的从。
- 4、主从复制不要用图状结构用单向链表结构更为稳定`Master <- Slave1 <- Slave2 <- Slave3...` 。
- 这样的结构,也方便解决单点故障问题,实现 Slave 对 Master 的替换。如果 Master挂了可以立刻启用 Slave1 做 Master ,其他不变。
> 从节点在切换主节点作为复制源的时候,会重新发起全量复制。所以此处通过 Slave1 挂在 Slave 下,可以规避这个问题。同时,也减少了 Master 的复制压力。当然,坏处就是 Slave1 的延迟可能会高一些些,所以还是需要取舍。
- 5、Redis 主从复制的性能问题为了主从复制的速度和连接的稳定性Slave 和 Master 最好在同一个局域网内。
------
和飞哥沟通过后,他们主节点开启 AOF ,从节点开启 AOF + RDB 。
和晓峰沟通后,他们主节点开启 AOF ,从节点开启 RDB 居多,也有开启 AOF + RDB 的。
## 修改配置不重启 Redis 会实时生效吗?
针对运行实例,有许多配置选项可以通过 `CONFIG SET` 命令进行修改,而无需执行任何形式的重启。
从 Redis 2.2 开始,可以从 AOF 切换到 RDB 的快照持久性或其他方式而不需要重启 Redis。检索 `CONFIG GET *` 命令获取更多信息
但偶尔重新启动是必须的如为升级 Redis 程序到新的版本或者当你需要修改某些目前 CONFIG 命令还不支持的配置参数的时候
## 其他问题
有些比较凶残的面试官可能会问我们一些 Redis 数据结构的问题例如
- Skiplist 插入和查询原理
- 压缩列表的原理
- Redis 底层为什么使用跳跃表而不是红黑树
> 跳跃表在范围查找的时候性能比较高。
想要了解这块需要花一定的时间去撸一撸源码推荐可以看如下两块内容
- [《Redis 深度历险:核心原理与应用实践》](https://juejin.im/book/5afc2e5f6fb9a07a9b362527?referrer=5904c637b123db3ee479d923)
- [《Redis 设计与实现》](https://u.jd.com/Fl5NTt)
推荐先读第一本可以深入浅出的了解 Redis 原理和源码然后在读第二本硬核了解 Redis 的设计与实现源码)。
## 666. 彩蛋
哇哦虽然过程痛苦但是中间请教了蛮多人问题收获颇多哈
嘿嘿在回答问题的过程中胖友会发现一直在推荐 [《Redis 开发与运维》](https://u.jd.com/lDNJa9) 这本书在艿艿整理完第一版 Redis 面试题后发现对有些 Redis 的面试题理解还是有所欠缺当然现在可能也是哈哈哈重新翻看了下这本书发现很多问题都得到了非常不错的解答所以推荐再推荐
参考与推荐如下文章
- JeffreyLcm [《Redis 面试题》](https://segmentfault.com/a/1190000014507534)
- 烙印99 [《史上最全 Redis 面试题及答案》](https://www.imooc.com/article/36399)
- yanglbme [《Redis 和 Memcached 有什么区别Redis 的线程模型是什么?为什么单线程的 Redis 比多线程的 Memcached 效率要高得多?》](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/redis-single-thread-model.md)
- 老钱 [《天下无难试之 Redis 面试题刁难大全》](https://zhuanlan.zhihu.com/p/32540678)
- yanglbme [《Redis 的持久化有哪几种方式?不同的持久化机制都有什么优缺点?持久化机制具体底层是如何实现的?》](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/redis-persistence.md)

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,327 @@
# 精尽【分库分表】面试题「最新更新时间2024-02」
以下面试题,基于网络整理,和自己编辑。具体参考的文章,会在文末给出所有的链接。
如果胖友有自己的疑问,欢迎在星球提问,我们一起整理吊吊的【分库分表】面试题的大保健。
而题目的难度,艿艿尽量按照从容易到困难的顺序,逐步下去。
## 为什么使用分库分表?
> 如下内容,引用自 Sharding Sphere 的文档,写的很大气。
>
> [《ShardingSphere > 概念 & 功能 > 数据分片》](http://shardingsphere.io/document/current/cn/features/sharding/)
传统的将数据集中存储至单一数据节点的解决方案,在**性能、可用性和运维成本**这三方面已经难于满足互联网的海量数据场景。
1性能
从性能方面来说,由于关系型数据库大多采用 B+ 树类型的索引,在数据量超过阈值的情况下,索引深度的增加也将使得磁盘访问的 IO 次数增加,进而导致查询性能的下降。
同时,高并发访问请求也使得集中式数据库成为系统的最大瓶颈。
2可用性
从可用性的方面来讲,服务化的无状态型,能够达到较小成本的随意扩容,这必然导致系统的最终压力都落在数据库之上。而单一的数据节点,或者简单的主从架构,已经越来越难以承担。数据库的可用性,已成为整个系统的关键。
3运维成本
从运维成本方面考虑,当一个数据库实例中的数据达到阈值以上,对于 DBA 的运维压力就会增大。数据备份和恢复的时间成本都将随着数据量的大小而愈发不可控。一般来讲,单一数据库实例的数据的阈值在 1TB 之内,是比较合理的范围。
🦅 **那么为什么不选择 NoSQL 呢?**
在传统的关系型数据库无法满足互联网场景需要的情况下,将数据存储至原生支持分布式的 NoSQL 的尝试越来越多。 但 NoSQL 对 SQL 的不兼容性以及生态圈的不完善,使得它们在与关系型数据库的博弈中始终无法完成致命一击,而关系型数据库的地位却依然不可撼动。
## 什么是分库分表?
数据分片,指按照某个维度将存放在单一数据库中的数据分散地存放至多个数据库或表中以达到提升性能瓶颈以及可用性的效果。数据分片的有效手段是对关系型数据库进行**分库和分表**。
- 分库和分表均可以有效的避免由数据量超过可承受阈值而产生的查询瓶颈。除此之外,分库还能够用于有效的分散对数据库单点的访问量。
- 分表虽然无法缓解数据库压力,但却能够提供尽量将分布式事务转化为本地事务的可能,一旦涉及到跨库的更新操作,分布式事务往往会使问题变得复杂。
- 使用多主多从的分片方式,可以有效的避免数据单点,从而提升数据架构的可用性。
通过分库和分表进行数据的拆分来使得各个表的数据量保持在阈值以下,以及对流量进行疏导应对高访问量,是应对高并发和海量数据系统的有效手段。数据分片的拆分方式又分为垂直分片和水平分片。
🦅 **垂直分片**
按照业务拆分的方式称为垂直分片,又称为纵向拆分,它的核心理念是专库专用。 在拆分之前,一个数据库由多个数据表构成,每个表对应着不同的业务。而拆分之后,则是按照业务将表进行归类,分布到不同的数据库中,从而将压力分散至不同的数据库。
下图展示了根据业务需要,将用户表和订单表垂直分片到不同的数据库的方案:
[![垂直分片](http://shardingsphere.jd.com/document/current/img/sharding/vertical_sharding.png)](http://shardingsphere.jd.com/document/current/img/sharding/vertical_sharding.png)垂直分片
垂直分片往往需要对架构和设计进行调整。通常来讲,是来不及应对互联网业务需求快速变化的;而且,它也并无法真正的解决单点瓶颈。 垂直拆分可以缓解数据量和访问量带来的问题,但无法根治。如果垂直拆分之后,表中的数据量依然超过单节点所能承载的阈值,则需要水平分片来进一步处理。
垂直拆分的优点:
- 库表职责单一,复杂度降低,易于维护。
- 单库或单表压力降低。 相互之间的影响也会降低。
垂直拆分的缺点:
- 部分表关联无法在数据库级别完成,需要在程序中完成。
- 单表大数据量仍然存在性能瓶颈。
- 单表或单库高热点访问依旧对 DB 压力非常大。
- 事务处理相对更为复杂,需要分布式事务的介入。
- 拆分达到一定程度之后,扩展性会遇到限制。
🦅 **水平分片**
水平分片又称为横向拆分。 相对于垂直分片,它不再将数据根据业务逻辑分类,而是通过某个字段(或某几个字段),根据某种规则将数据分散至多个库或表中,每个分片仅包含数据的一部分。
例如:根据主键分片,偶数主键的记录放入 0 库(或表),奇数主键的记录放入 1 库(或表),如下图所示:
[![水平分片](http://shardingsphere.jd.com/document/current/img/sharding/horizontal_sharding.png)](http://shardingsphere.jd.com/document/current/img/sharding/horizontal_sharding.png)水平分片
水平分片从理论上突破了单机数据量处理的瓶颈,并且扩展相对自由,是分库分表的标准解决方案。
水平拆分的优点:
- 解决单表单库大数据量和高热点访问性能遇到瓶颈的问题。
- 应用程序端整体架构改动相对较少。
- 事务处理相对简单。
- 只要切分规则能够定义好,基本上较难遇到扩展性限制。
水平拆分缺点:
- 拆分规则相对更复杂,很难抽象出一个能够满足整个数据库的切分规则。
- 后期数据的维护难度有所增加,人为手工定位数据更困难。
- 产品逻辑将变复杂。比如按年来进行历史数据归档拆分,这个时候在页面设计上就需要约束用户必须要先选择年,然后才能进行查询。
🦅 **总结?**
- 数据表垂直拆分:单表复杂度。
- 数据库垂直拆分:功能拆分。
- 水平拆分
- 分表:解决单表大数据量问题。
- 分库:为了解决单库性能问题。
## 用了分库分表之后,有哪些常见问题?
虽然数据分片解决了性能、可用性以及单点备份恢复等问题,但分布式的架构在获得了收益的同时,也引入了新的问题。
- 面对如此散乱的分库分表之后的数据,应用开发工程师和数据库管理员对数据库的操作变得异常繁重就是其中的重要挑战之一。他们需要知道数据需要从哪个具体的数据库的分表中获取。
- 另一个挑战则是,能够正确的运行在单节点数据库中的 SQL ,在分片之后的数据库中并不一定能够正确运行。
- 例如,分表导致表名称的修改,或者分页、排序、聚合分组等操作的不正确处理。
- 例如,跨节点 join 的问题。
- **跨库事务**也是分布式的数据库集群要面对的棘手事情。
- 合理采用分表,可以在降低单表数据量的情况下,尽量使用本地事务,善于使用同库不同表可有效避免分布式事务带来的麻烦。
> 要达到这个效果,需要尽量把同一组数据放到同一组 DB 服务器上。
>
> 例如说,将同一个用户的订单主表,和订单明细表放到同一个库,那么在创建订单时,还是可以使用相同本地事务。
- 在不能避免跨库事务的场景,有些业务仍然需要保持事务的一致性。而基于 XA 的分布式事务由于在并发度高的场景中性能无法满足需要,并未被互联网巨头大规模使用,他们大多采用最终一致性的柔性事务代替强一致事务。
- 分布式全局唯一 ID 。
- 在单库单表的情况下直接使用数据库自增特性来生成主键ID这样确实比较简单。
- 在分库分表的环境中,数据分布在不同的分表上,不能再借助数据库自增长特性。需要使用全局唯一 ID例如 UUID、GUID等 。
关于这块,也可以看看 [《分库分表》](https://www.yuque.com/lexiangqizhong/java/ckt9uw) 文章。
## 了解和使用过哪些分库分表中间件?
在将数据库进行分库分表之后,我们一般会引入分库分表的中间件,使之能够达到如下目标。
> 尽量透明化分库分表所带来的影响,让使用方尽量像使用一个数据库一样使用水平分片之后的数据库集群,这是分库分表的主要设计目标。
🦅 **分库分表的实现方式?**
目前,市面上提供的分库分表的中间件,主要有两种实现方式:
- Client 模式
- Proxy 模式
🦅 **分库分表中间件?**
比较常见的包括:
- Cobar
- MyCAT
- Atlas
- TDDL
- Sharding Sphere
1Cobar
阿里 b2b 团队开发和开源的,属于 Proxy 层方案。
早些年还可以用,但是最近几年都没更新了,基本没啥人用,差不多算是被抛弃的状态吧。而且不支持读写分离、存储过程、跨库 join 和分页等操作。
2MyCAT
基于 Cobar 改造的,属于 Proxy 层方案,支持的功能非常完善,而且目前应该是非常火的而且不断流行的数据库中间件,社区很活跃,也有一些公司开始在用了。但是确实相比于 Sharding Sphere 来说,年轻一些,经历的锤炼少一些。
3Atlas
360 开源的,属于 Proxy 层方案,以前是有一些公司在用的,但是确实有一个很大的问题就是社区最新的维护都在 5 年前了。所以,现在用的公司基本也很少了。
4TDDL
淘宝团队开发的,属于 client 层方案。支持基本的 crud 语法和读写分离,但不支持 join、多表查询等语法。目前使用的也不多因为还依赖淘宝的 diamond 配置管理系统。
5Sharding Sphere
Sharding Sphere ,可能是目前最好的开源的分库分表解决方案,目前已经进入 Apache 孵化。
Sharding Sphere 提供三种模式:
> 关于每一种模式的介绍,可以看看 [《ShardingSphere > 概览》](http://shardingsphere.io/document/current/cn/overview/)
- Sharding-JDBC
- Sharding-Proxy
- Sharding-Sidecar 计划开发中。
其中Sharding-JDBC 属于 client 层方案,被大量互联网公司所采用。例如,当当、京东金融、中国移动等等。
🦅 **如何选择?**
综上,现在其实建议考量的,就是 Sharding Sphere ,这个可以满足我们的诉求。
Sharding Sphere 的 Sharding-JDBC 方案,这种 Client 层方案的**优点在于不用部署,运维成本低,不需要代理层的二次转发请求,性能很高**,但是如果遇到升级啥的需要各个系统都重新升级版本再发布,各个系统都需要**耦合** sharding-jdbc 的依赖。
> 据艿艿了解到,例如阿里、美团内部,更多使用的是 Client 模式。
Sharding Sphere 的 Sharding-Proxy 方案,这种 Proxy 层方案,可以解决我们平时查询数据库的需求。我们只需要连接一个 Sharding-Proxy ,就可以查询分库分表中的数据。另外,如果我们有跨语言的需求,例如 PHP、GO 等,也可以使用它。
## 如何迁移到分库分表?
一般来说,会有三种方式:
- 1、停止部署法。
- 2、双写部署法基于业务层。
- 3、双写部署法基于 binlog 。
具体的详细方案,可以看看如下两篇文章:
- [《数据库分库分表后,如何部署上线?》](http://www.iocoder.cn/Fight/After-the-database-sharding-how-to-deploy-online/)
- [《【面试宝典】如何把单库数据迁移到分库分表?》](http://www.chaiguanxin.com/articles/2018/11/11/1541923418699.html)
- [《分库分表的面试题3》](https://www.cnblogs.com/daiwei1981/p/9416068.html)
另外,这是另外一个比较相对详细的【双写部署法,基于业务层】的过程:
- 双写 ,老库为主。读操作还是读老库老表,写操作是双写到新老表。
- 历史数据迁移 dts + 新数据对账校验job + 历史数据校验。
- 切读:读写以新表为主,新表成功就成功了。
- 观察几天,下掉写老库操作。
另外,飞哥的 [《不停机分库分表迁移》](https://www.jianshu.com/p/223d71421f49) 文章,也非常推荐看看。
🦅 **如何设计可以动态扩容缩容的分库分表方案?**
可以参看 [《如何设计可以动态扩容缩容的分库分表方案?》](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/database-shard-dynamic-expand.md) 文章。简单的结论是:
- 提前考虑好容量的规划,避免扩容的情况。
- 如果真的需要扩容,走上述的 [「如何迁移到分库分表?」](https://svip.iocoder.cn/database-sharding/Interview/#) 提到的方案。
## 什么是分布式主键?怎么实现?
分布式主键的实现方案有很多,可以看看 [《谈谈 ID》](http://www.iocoder.cn/Architecture/talk-about-global-id/) 的总结。
一般来说,目前采用 SnowFlake 的居多,可以看看 [《Sharding-JDBC 源码分析 —— 分布式主键》](http://www.iocoder.cn/Sharding-JDBC/distributed-id/?vip) 的源码的具体实现,比较简单。
## 分片键的选择?
分库分表后,分片键的选择非常重要。一般来说是这样的:
- 信息表,使用 id 进行分片。例如说,文章、商品信息等等。
- 业务表,使用 user_id 进行分片。例如说,订单表、支付表等等。
- 日志表,使用 create_time 进行分片。例如说,访问日志、登陆日志等等。
🦅 **分片算法的选择?**
选择好分片键之后,还需要考虑分片算法。一般来说,有如下两种:
- 取余分片算法。例如说,有四个库,那么 user_id 为 10 时,分到第
```
10 % 4 = 2
```
个库。
- 当然,如果分片键是字符串,则需要先进行 hash 的方式,转换成整形,这样才可以取余。
- 当然,如果分片键是整数,也可以使用 hash 的方式。
- 范围算法。
- 例如说,时间范围。
上述两种算法,各有优缺点。
- 对于取余来说:
- 好处,可以平均分配每个库的数据量和请求压力。
- 坏处,在于说扩容起来比较麻烦,会有一个数据迁移的过程,之前的数据需要重新计算 hash 值重新分配到不同的库或表。
- 对于 range 来说:
- 好处,扩容的时候很简单,因为你只要预备好,给每个月都准备一个库就可以了,到了一个新的月份的时候,自然而然,就会写新的库了。
- 缺点,但是大部分的请求,都是访问最新的数据。实际生产用 range要看场景。
🦅 **如果查询条件不带分片键,怎么办?**
当查询不带分片键时,则中间件一般会扫描所有库表,然后聚合结果,然后进行返回。
对于大多数情况下,如果每个库表的查询速度还可以,返回结果的速度也是不错的。具体,胖友可以根据自己的业务进行测试。
🦅 **使用 user_id 分库分表后,使用 id 查询怎么办?**
有四种方案。
1不处理
正如在 [「如果查询条件不带分片键,怎么办?」](https://svip.iocoder.cn/database-sharding/Interview/#) 问题所说,如果性能可以接受,可以不去处理。当然,前提是这样的查询不多,不会给系统带来负担。
2映射关系
创建映射表,只有 id、user_id 两列字段。使用 id 查询时,先从映射表获得 id 对应的 user_id ,然后再使用 id + user_id 去查询对应的表。
当然,随着业务量的增长,映射表也会越来越大,后续也可能需要进行分库分表。
对于这方式,也可以有一些优化方案。
- 映射表改成缓存到 Redis 等 KV 缓存中。当然,需要考虑如果 Redis 持久化的情况。
- 将映射表缓存到内存中,减少一次到映射表的查询。
3基因 id
分库基因:假如通过 user_id 分库,分为 8 个库,采用 user_id % 8 的方式进行路由,此时是由 user_id 的最后 3bit 来决定这行 User 数据具体落到哪个库上,那么这 3bit 可以看为分库基因。那么,如果我们将这 3 bit 参考类似 Snowflake 的方式,融入进入到 id 。
> 艿艿:这里的 3 bit 只是举例子,实际需要考虑自己分多少库表,来决定到底使用多少 bit 。
上面的映射关系的方法需要额外存储映射表,按非 user_id 字段查询时,还需要多一次数据库或 Cache 的访问。通过基因 id ,就可以知道数据所在的库表。
详细说明,可以看看 [《用 uid 分库uname 上的查询怎么办?》](http://www.10tiao.com/html/249/201704/2651960032/1.html) 文章。
目前,可以从 [《大众点评订单系统分库分表实践》](https://tech.meituan.com/dianping_order_db_sharding.html) 文章中,看到大众点评订单使用了基因 id 。
4多 sharding column
具体的内容,可以参考 [《分库分表的正确姿势,你 GET 到了么?》](https://yq.aliyun.com/articles/641529) 。当然,这种方案也是比较复杂的方案。
## 如何解决分布式事务?
目前市面上,分布式事务的解决方案还是蛮多的,但是都是基于一个前提,需要保证本地事务。**那么,就对我们在分库分表时,就有相应的要求:数据在分库分表时,需要保证一个逻辑中,能够形成本地事务**。举个例子,创建订单时,我们会插入订单表和订单明细表,那么:
- 如果我们基于这两个表的 id 进行分库分表,将会导致插入的记录被分到不同的库表中,因为创建下单可以购买 n 个商品,那么就会有 1 条订单记录和 n 条 订单明细记录。而这 n 条订单明细记录无法和 1 条订单记录分到一个库表中。
- 如果我们基于这两个表的 user_id 进行分库分表,那么插入的记录被分到相同的库表中。
> 艿艿:这也是为什么业务表一般使用 user_id 进行分库分表的原因之一。
可能会有胖友有疑问,为什么一定要形成本地事务?在有了本地事务的基础上,通过使用分布式事务的解决方案,协调多个本地事务,形成最终一致性。另外,😈 本地事务在这个过程中,能够保证万一执行失败,再重试时,不会产生脏数据。
## 彩蛋
参考与推荐如下文章:
- [《Sharding Sphere 官方文档》](http://shardingsphere.io/document/current/cn/overview/)
- boothsun [《分库分表面试准备》](https://www.zybuluo.com/boothsun/note/1107670)
- butterfly100 [《数据库分库分表思路》](https://www.cnblogs.com/butterfly100/p/9034281.html)

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Some files were not shown because too many files have changed in this diff Show More