1、使用 RabbitMQ 有什么好处?
- 解耦:系统 A 在代码中直接调用系统 B 和系统 C 的代码,如果将来还有 D 系统接入,系统 A 还要修改代码,过于麻烦;
- 异步:将消息写入消息队列,非必要的业务逻辑以异步的方式运行,加快响应速度;
- 削峰:并发量大的时候,所有的请求直接怼到数据库,会造成数据库连接异常。
2、RabbitMQ 有哪些重要的组件?
- Server(broker):服务端,接受客户端连接,实现 AMQP 消息队列和路由功能的进程。
- Virtual Host:虚拟主机,其实就是一个虚拟概念,类似于权限控制组,一个 Virtual Host 里面可以有若干个 Exchange 和 Queue,但是权限控制的最小粒度是 Virtual Host。
- Exchange:交换机,接收生产者发送的消息,并根据 Binding 规则将消息路由给服务器中的队列。ExchangeType 决定了 Exchange 路由消息的行为,例如,在 RabbitMQ 中,ExchangeType 有 direct、Fanout 和 Topic 三种。
- Message Queue:消息队列,用于存储还未被消费者消费的消息。
- Message:消息,由 Header 和 Body 组成,Header 是由生产者添加的各种属性的集合,包括 Message 是否被持久化、由哪个 Queue 接收、优先级多少等属性,而 Body 是真正需要传输的数据。
- Binding:Binding 联系了 Exchange 与 Queue,Exchange 在与多个 Queue 发生 Binding 后会生成一张路由表,路由表中存储着 Queue 所需消息的限制条件即 Binding Key,当 Exchange 接收到 Message 时会解析其 Header 得到 Routing Key,Exchange 根据 Routing Key 与 ExchangeType 将 Message 路由到 Queue。Binding Key 由 Consumer 在 Binding Exchange 与 Queue 时指定,而 Routing Key 由 Producer 发送 Message 时指定,两者的匹配方式由 ExchangeType 决定。
- Connection:连接,对于 RabbitMQ 而言,其实就是一个位于客户端和 Broker 之间的 TCP 连接。
- Channel:信道,仅仅创建了客户端到 Broker 之间的连接后,客户端还是不能发送消息的,需要为每一个 Connection 创建 Channel,AMQP 协议规定只有通过 Channel 才能执行 AMQP 的命令。一个 Connection 可以包含多个 CHannel。之所以需要 Channel,是因为 TCP 连接的建立和释放都是十分昂贵的,如果一个客户端每一个线程都需要与 Broker 交互,如果每一个线程都建立一个 TCP 连接,暂且不考虑 TCP 连接是否浪费,就算操作系统也无法承受每秒建立如此多的 TCP 连接。RabbitMQ 建议客户端线程之间不要共用 Channel,至少要保证共用 Channel 的线程发送消息必须是串行的,但是建议尽量共用 Connection。
- Command:AMQP 的命令,客户端通过 Command 完成与 AMQP 服务器的交互来实现自身的逻辑。例如在 RabbitMQ 中,客户端可以通过 publish 命令来发送消息,txSelect 开启一个事务,txCommit 提交一个事务。
3、RabbitMQ 交换机的种类
- Direct Exchange 直连交换机:默认类型,根据路由键(Routing Key)将消息投递给对应队列。
- Fanout Exchange 发布/订阅:将消息路由给绑定到它身上的所有队列,而不理会绑定的路由键(Routing Key)。
- Topic Exchange 匹配/订阅:通过对消息的路由键(Routing Key)和绑定到交换机的队列,将消息路由给一个或多个队列。
- Headers Exchange 直连交换机:发送消息时匹配 Header 而非 Routing Key,性能很差,几乎不用。
4、RabbitMQ 的消息投递过程
- 生产者连接到消息队列服务器,打开一个 channel。
- 生产者声明一个 exchange,并设置相关属性,比如交换机类型、是否持久化、是否自动删除、是否内置等。
- 生产者声明一个 queue,并设置相关属性,是否排他、是否持久化、是否自动删除、消息最大过期时间、消息最大长度、消息最大字节数等。
- 生产者使用 routing key,在 exchange 和 queue 之间建立好绑定关系。
- 生产者投递消息到 exchange。
- 关闭 channel、关闭 connection。
5、RabbitMQ 的消息消费过程
- 消费者连接到消息队列服务器,打开一个 channel。
- 消费者向 RabbitMQ Broker 请求消费相应队列中的消息,在这个过程中可能会设置消费者标签、是否自动确认、是否排他等。
- 等待 RabbitMQ Broker 回应并投递相应队列中的消息, 消费者接收消息。
- 消费者确认接收到的消息。
- RabbitMQ 从队列中删除相应己经被确认的消息。
- 关闭 channel、关闭 connection。
6、RabbitMQ 怎么保证消息的稳定性?
ACK 确认机制。
消费者在消费完消息后发送一个回执给 RabbitMQ,RabbitMQ 收到消息回执(Message acknowledgment)后才将该消息从 Queue 中移除;如果 RabbitMQ 没有收到回执并检测到消费者的 RabbitMQ 连接断开,则 RabbitMQ 会将该消息发送给其他消费者(如果存在多个消费者)进行处理。这里不存在 timeout 概念,一个消费者处理消息时间再长也不会导致该消息被发送给其他消费者,除非它的 RabbitMQ 连接断开。
7、RabbitMQ 怎么避免消息丢失?
- 消息持久化:RabbitMQ 的消息默认保存在内存中,如果 RabbitMQ 重启或挂掉,消息丢失,此时需要持久化到硬盘。
要做到消息持久化,必须满足以下三个条件:
Exchange 设置持久化;Queue 设置持久化;Message 持久化发送(发送消息时设置发送模式 deliveryMode=2,代表持久化消息) - ACK确认机制:消费端接收到消息发送回执给 RabbitMQ 服务端,服务端才把消息从内存中删除。
- 消息补偿机制:在消息发送、接收时,记录消息到数据库消息日志表,主键就是 msg-id(发送消息时生成msg-id,下面有代码,此举也可以防止重复消费), 定时轮询此表,查明哪些发送消息没有成功消费,启动重新发送消息机制。
message.getMessageProperties().setMessageId(UUID.randomUUID().toString().replace("-", ""));
8、RabbitMQ 的高可用性如何保证?
RabbitMQ 高可用有三种工作模式:单机模式、普通集群模式、镜像集群模式。
- 单机模式:不存在高可用,生产环境不可能使用。
- 普通集群模式:也不存在高可用。即在多个服务器上部署多个 MQ 实例,创建的每一个 queue,只会存在一个 MQ 实例上,但是每一个实例都会同步 queue 的元数据(即queue的标识信息),当在进行消费的时候, 就算连接到了其他的 MQ 实例上,其也会根据内部的 queue 的元数据,从该 queue 所在实例上拉取数据过来。这种方式很麻烦,只是一个简单集群,并没有做到高可用。并且性能开销巨大,容易造成单实例的性能瓶颈,并且如果真正有数据的那个queue的实例宕机了,那么其他的实例就无法进行数据的拉取。这种方式只是通过集群部署的方式提高了消息的吞吐量,但是并没有考虑到高可用。
- 镜像集群模式:真正高可用模式。与普通集群模式的主要区别在于,无论 queue 的元数据还是 queue 中的消息都会同时存在于多个实例上,即数据是同步到所有节点。
要开启镜像集群模式,需要在后台新增镜像集群模式策略,即要求数据同步到所有的节点,也可以指定同步到指定数量的节点。
这种方式的好处就在于,任何一个服务宕机了,都不会影响整个集群数据的完整性,因为其他服务中都有 queue 的完整数据,当进行消息消费的时候,连接其他的服务器节点一样也能获取到数据。
缺点:
性能开销大:因为需要进行整个集群内部所有实例的数据同步。
无法线性扩容:因为每一个服务器中都包含整个集群服务节点中的所有数据,这样如果一旦单个服务器节点的容量无法容纳了怎么办