倘若你有实践过有关电商、外面等背景的项目,那一定绕不开消息队列的使用,但是一旦你在简历中书写了消息队列的使用,那这一定是面试官询问的重点,特别是一些电商外面公司。
这篇文章的初衷就是,假设你在简历中写了MQ的使用但是又不想去系统性的学习整个MQ的系统框架,希望通过这篇文章可以让你对MQ有一个初步的认识,并且可以应对大部分相关面试题。
以下先罗列出以下常问的面试题:
1、说说RabbitMq和RockeMQ,Kafka 之间的区别,要如果根据项目进行选择?
2、说说RabbitMQ中的一些主要的组件,并说说他们是如何配合在一起工作的
3、如何保证消息的可靠性
4、如何保证消息不会被重复消费
5、什么是死信队列
6、如何保证高并发的
7、如何在兼顾高并发的场景下实现高可用的
8、消息队列常常用来解决什么问题
以上都是一些比较重要,比较重量级的面试题,如果你看完以下内容,能够自主的回答出以上问题就够了。
1、什么是RabbitMQ
其实简单来说:RabbitMQ是消息队列的一种实现形式,即使用队列来进行组件之间通信的一种中间件。
系统来说:RabbitMQ是实现了高级消息队列协议(AMQP
)的开源消息代理软件(亦称面向消息的中间件)。RabbitMQ服务器是用Erlang语言编写的,而群集和故障转移是构建在开放电信平台框架上的。
2、常见几大消息队列组件的区别
我们知道当前市面上实现消息队列功能的组件很多,比较出名常见的有:ActiveMQ,RabbitMQ,Kafka,RocketMQ等,了解它们之间的区别,如何进行选择也是面试常问的一个问题
总体来说可以从以下纬度进行比较:
特性/系统 | RabbitMQ | Kafka | RocketMQ | ActiveMQ |
---|---|---|---|---|
类型 | 消息中间件(传统 MQ) | 分布式日志系统 + 消息中间件 | 分布式消息中间件 | 老牌消息中间件 |
协议支持 | AMQP、MQTT、STOMP | 自定义协议 | 自定义协议 | JMS、OpenWire、AMQP |
性能 | 万级 TPS(低延迟,吞吐一般) | 高吞吐(百万级 TPS) | 高吞吐(优于 RabbitMQ) | 较低 |
消息投递语义 | At most once / At least once | At least once(默认) / Exactly once(依赖配置) | At least once | At least once |
消息顺序 | 支持(通过队列保证) | 支持(单分区有序) | 支持(通过 MessageQueue) | 支持 |
持久化机制 | 支持,基于磁盘+内存混合 | 强持久化,顺序写磁盘,IO 优化 | 支持,使用文件存储 | 支持 |
延迟消息 | 支持(通过 TTL + DLX 实现) | 原生不支持,需插件或定时任务 | 原生支持 | 支持 |
事务消息 | 支持 | 支持(可靠但复杂) | 支持(实现较完善) | 支持 |
集群模式 | 普通集群、镜像集群 | Broker + Zookeeper(或 KRaft) | 多种 Broker 主从结构 | Master-Slave、网络集群 |
可用性 | 高(镜像队列容错) | 非常高(分布式设计) | 高 | 一般 |
易用性 | ✅非常高(文档丰富,工具完善) | 中等(需理解分区、副本) | 中等 | 一般 |
适合场景 | 任务队列、异步处理、延迟消息等 | 日志采集、海量数据流、高吞吐业务 | 大型业务系统、事务消息、电商等 | 简单系统或早期 Java 项目 |
1. RabbitMQ
-
优点:延迟低成熟稳定,协议丰富,社区活跃,支持延迟消息,易上手
-
缺点:吞吐量较 Kafka 低,消息积压严重时性能下滑
-
适用场景:中小型业务系统、异步任务处理、RPC解耦、延迟任务
2. Kafka
-
优点:高吞吐、高可用、分布式天然支持,适合大数据流
-
缺点:不适合处理对“消息可靠性要求特别高”的场景(默认配置下丢消息风险)
-
适用场景:日志收集、用户行为跟踪、消息流处理(如 Flink)
3. RocketMQ
-
优点:事务消息强、性能高、支持定时/延迟消息,阿里出品,适合大规模业务
-
缺点:学习曲线略高,生态不如 RabbitMQ/Kafka 丰富
-
适用场景:金融、电商等对消息可靠性要求高的系统
4. ActiveMQ
-
优点:老牌,支持 JMS、Spring 生态好
-
缺点:性能落后,社区活跃度下降
-
适用场景:传统 Java 企业系统,轻量级队列需求
简单来说:
🔸 RabbitMQ 适合快速开发与中小型业务,注重功能丰富与易用性。
🔸 Kafka 是海量数据管道首选,适用于实时流处理系统。
🔸 RocketMQ 兼顾高性能与事务能力,是大型企业系统的可靠选项。
🔸 ActiveMQ 更适合传统系统或对 JMS 有兼容要求的项目。
3、RabbitMQ的几大组件
了解了消息队列的背景之后,我们就RabbitMQ来谈谈它的整个业务流程是什么样的,首先我们需要知道它的整个系统结构设计是什么样的。
RabbitMQ的几个重要概念:
- broker :可以简单理解一个broker 节点就是一个rabbitMQ服务器实例
- producer:生产者,负责生产消息并将消息push 到队列中的
- consumer:消费者,负责从队列中拉取消息进行消费
- exchange:交换机,负责对接生产者推送消息,并根据某些规则(后面详细讲)将消息推送到不同的队列。
- queue:队列,实质上存储消息的容器,按顺序派发给不同的消费者
1、broker 节点
主要作用:
- 接收生产者的消息
- 存储消息(通过队列)
- 管理交换机、队列、绑定关系等元数据
- 推送消息给消费者
2、producer 生产者
负责生产消息,并发送给交换机(Exchange)。一般是某些业务的服务端
- Producer 并不会直接将消息送入队列,而是把消息发给 交换机,交换机再根据绑定规则路由到对应的队列。
- 发送的消息可以携带
routingKey
(路由键)来辅助路由。
例如:
channel.basicPublish("order_exchange", "order.create", null, msgBody);
这条语句将消息发送给名为 order_exchange
的交换机,routingKey 为 order.create
。
3、exchange 交换机
RabbitMQ 中的消息“路由中转站”,负责接收生产者消息并决定送往哪个队列。
-
交换机不存储消息,只路由。
-
根据
routingKey
和绑定规则将消息分发到队列。
📦 交换机类型(4种):
类型 | 描述 |
---|---|
direct | 精确匹配 routingKey |
fanout | 广播给所有绑定队列,忽略 routingKey |
topic | 模糊匹配(支持通配符) |
headers | 根据消息 header 匹配(较少用) |
举例:一个 topic
类型的 exchange:
routingKey: order.create
bindingKey:order.* ✅ 匹配成功,消息会路由到绑定的队列
4、queue 队列
RabbitMQ 中真正存储消息的地方,先进先出(FIFO),等待消费者消费
-
每条消息只能被一个消费者消费(除非配置为广播)。
-
队列可以是临时的,也可以设置为持久化。
-
可以和多个交换机绑定,接收来自不同来源的消息。
5、consumer 消费者
从队列中拉取(或被推送)消息进行处理的应用或服务。
消费模式:
- 推模式(push):RabbitMQ 自动把消息推送给 consumer。
- 拉模式(pull):Consumer 主动向队列拉消息(较少用)。
channel.basicConsume("order_queue", false, consumer);
此语句从 order_queue
消费消息,并手动 ack。
以下用一张图来表示一个消息从生产到消费的一整个逻辑大概是什么样的:
4、两个特殊队列
RabbitMQ 中提供两个比较特殊队列来应对一些特殊场景,接下来我们一一介绍
一、死信队列(Dead Letter Queue)
什么是死信队列?
死信队列是RabbitMQ中用于处理无法被正常消费的消息的机制。当消息因为某些原因无法被处理时,这些消息会被转发到预先配置的死信交换机,最终进入死信队列。
消息变成死信的三种情况
- 消息被拒绝(reject/nack)且不重新入队
- 消息过期(TTL超时)
- 队列达到最大长度限制
死信队列的工作原理
当普通队列中的消息变成死信时,RabbitMQ会将这些消息发送到该队列绑定的死信交换机(Dead Letter Exchange),然后根据路由规则将消息路由到死信队列中。
死信队列的配置示例
// 声明普通队列,配置死信交换机
Map<String, Object> args = new HashMap<>();
args.put("x-dead-letter-exchange", "dlx.exchange"); // 死信交换机
args.put("x-dead-letter-routing-key", "dlx.routing"); // 死信路由键
args.put("x-message-ttl", 60000); // 消息TTL(可选)
args.put("x-max-length", 100); // 队列最大长度(可选)
channel.queueDeclare("normal.queue", true, false, false, args);
// 声明死信交换机和死信队列
channel.exchangeDeclare("dlx.exchange", "direct");
channel.queueDeclare("dlx.queue", true, false, false, null);
channel.queueBind("dlx.queue", "dlx.exchange", "dlx.routing");
死信队列的应用场景
- 异常消息处理:收集处理失败的消息进行人工干预
- 消息审计:记录所有无法处理的消息用于后续分析
- 降级处理:为重要业务提供备用处理方案
- 监控告警:当死信队列有消息时触发告警机制
处理流程如下图:
二、延迟队列(Delay Queue)
什么是延迟队列
延迟队列是指消息在发送后不会立即被消费,而是在指定的延迟时间后才能被消费者获取的队列机制。RabbitMQ本身不直接支持延迟队列,但可以通过TTL+死信队列的组合来实现。
延迟队列的实现方式
方式一:TTL + 死信队列
这是最常用的实现方式,利用消息的TTL特性结合死信队列来实现延迟效果。
方式二:RabbitMQ延迟插件
使用rabbitmq_delayed_message_exchange
插件,提供原生的延迟消息支持。
延迟队列实现示例
// 方式一:TTL + 死信队列实现
public class DelayQueueWithTTL {
// 创建延迟队列(实际是TTL队列)
public void createDelayQueue() {
Map<String, Object> args = new HashMap<>();
args.put("x-message-ttl", 30000); // 30秒后过期
args.put("x-dead-letter-exchange", "delay.exchange"); // 过期后发送到目标交换机
args.put("x-dead-letter-routing-key", "delay.process");
channel.queueDeclare("delay.ttl.queue", true, false, false, args);
// 目标队列(真正的业务队列)
channel.exchangeDeclare("delay.exchange", "direct");
channel.queueDeclare("delay.process.queue", true, false, false, null);
channel.queueBind("delay.process.queue", "delay.exchange", "delay.process");
}
}
// 方式二:使用延迟插件
public class DelayQueueWithPlugin {
public void createDelayExchange() {
Map<String, Object> args = new HashMap<>();
args.put("x-delayed-type", "direct");
// 声明延迟交换机
channel.exchangeDeclare("delay.plugin.exchange", "x-delayed-message", true, false, args);
}
public void sendDelayMessage(String message, int delayTime) {
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
.headers(Collections.singletonMap("x-delay", delayTime))
.build();
channel.basicPublish("delay.plugin.exchange", "delay.routing", properties, message.getBytes());
}
}
延迟队列的处理流程大概如下:
死信队列和延迟队列是RabbitMQ中两个非常实用的特性,它们分别解决了异常消息处理和定时任务的需求。在实际应用中,合理使用这些特性可以大大提高系统的健壮性和灵活性。
5、消息可靠性的保证
根据以上知识的铺垫,详细读者已经对RabbitMQ的整体框架有一个比较初步的认识,也明白了它的相应处理逻辑,那我们接下来就应该学习它在真实生产环境中会出现的问题。
一个比较重要的问题就是,如何保证消息的可靠性,举个例子要是生产者没有成功推送一条消息,但你不知道,这条消息就莫名其妙的消失了,这种情况肯定是不允许的,接下来我们就来看看rabbitMQ是如何保证消息的可靠性的。
要解决这个问题,我们首先要知道消息可能在哪几个环节会出问题:
- 1、生产者向 broker 节点推送的过程
- 2、消息路由到队列的过程
- 3、消息在队列中等待消费者消费的过程
这三个过程都有可能出现消息丢失的情况,所以我们需要从这三个阶段出发去保证消息的可靠性
1、生产者向 broker 节点推送的过程
✅ 1.1 Publisher Confirm(发布确认机制)
-
RabbitMQ 提供 发布确认机制 来确保消息是否被 broker 成功接收。
-
开启方式:将 channel 设置为 confirm 模式(
channel.confirmSelect()
)。 -
原理:
-
消息发送后,Broker 接收成功会异步返回一个确认(ack)或失败(nack)响应。
-
生产者可以通过回调函数来处理这些确认结果,进而进行重试或记录日志。
-
以下是一个简单的例子
@Component
public class ReliableProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
// 配置发布确认
@PostConstruct
public void configureReliability() {
// 开启发布确认模式
rabbitTemplate.setConfirmCallback(this::confirmCallback);
// 开启返回模式(消息无法路由时回调)
rabbitTemplate.setReturnsCallback(this::returnsCallback);
rabbitTemplate.setMandatory(true);
}
// 发布确认回调
private void confirmCallback(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
log.info("消息发送成功: {}", correlationData.getId());
// 可以删除本地备份或更新状态
updateMessageStatus(correlationData.getId(), MessageStatus.SENT);
} else {
log.error("消息发送失败: {}, 原因: {}", correlationData.getId(), cause);
// 重新发送或记录失败
handleSendFailure(correlationData);
}
}
// 可靠发送消息
public void sendReliableMessage(String exchange, String routingKey, Object message) {
// 生成唯一ID用于跟踪
String messageId = UUID.randomUUID().toString();
CorrelationData correlationData = new CorrelationData(messageId);
// 本地记录消息(用于重试)
saveMessageLocally(messageId, exchange, routingKey, message);
try {
rabbitTemplate.convertAndSend(exchange, routingKey, message, correlationData);
log.info("消息已发送: {}", messageId);
} catch (Exception e) {
log.error("发送消息异常: {}", messageId, e);
handleSendException(messageId, e);
}
}
}
2、消息是否成功进入队列(路由成功)
✅ 1.2 Mandatory 标志 + Return Callback
-
即使消息送达了交换机(Exchange),如果找不到对应的队列(Queue),消息也可能会“丢失”。
-
解决方案是设置 mandatory = true,并设置 return callback 回调:
-
若消息路由不到任何队列,则触发回调,生产者就可以感知这种异常情况。
-
没有设置 mandatory 时,默认消息会被直接丢弃。
-
再刚刚的配置类添加一个returnCallback函数即可
// 消息无法路由时的回调
private void returnsCallback(ReturnedMessage returned) {
log.error("消息无法路由 - Exchange: {}, RoutingKey: {}, ReplyText: {}",
returned.getExchange(), returned.getRoutingKey(), returned.getReplyText());
// 处理路由失败的消息
handleRoutingFailure(returned);
}
3、消息在队列中持久化
✅ 1.3 消费前前持久化(消息持久性)
-
设置消息的持久性属性(
delivery_mode = 2
),让消息在 Broker 端写入磁盘,避免因 Broker 异常宕机导致消息丢失。 -
注意:只有消息和队列都设置为 durable,且消息真正被写入磁盘时,才算真正持久化成功。
MessageProperties properties = new MessageProperties();
properties.setDeliveryMode(MessageDeliveryMode.PERSISTENT); // 设置为持久化
6、如何保证消息不会被重复消费/或者没被消费
了解了RabbitMQ如何保证消息的可靠性之后,我们有必要了解一下重复消费这个问题,为什么会出现重复消费。
首先,我们先了解一下消息被消费之后会发生什么,消息被消费者接受之后,消费者会向相应的队列返回确认消费的ack标志,此时队列认定消息已经被消费,就会将其从队列中删除。
那试想一下,要是消费者消费了但是由于网络等原因或者消费者端宕机没有返回ack,那么队列会将消息重新发给另一个消费者消费,此时就会出现重复消费问题,如果消费逻辑是修改数据库,最直接后果就是对数据库数据重复修改,导致错误数据。
或者说如果消费者接受之后没有完成相应的消费逻辑,那这条消息的消费就失败了,但是服务端是没办法感知的。
解决第二个问题(没被消费)比较简单,解决方法如下:
✅ 开启消费者确认机制(manual ack)
默认情况下,RabbitMQ 的消费者可能会采用自动 ack(auto-ack),也就是说只要消息投递成功,无论消费者是否处理成功,RabbitMQ 都会立刻认为消费完成并将消息移出队列。
要避免因消费者异常未处理完就丢失 ack,需要启用 手动确认机制:
channel.basicConsume(queueName, false, consumer);
在消费完成后显式调用:
channel.basicAck(deliveryTag, false);
如果消费失败或消费者断连,RabbitMQ 会将这条未确认的消息重新投递给其他消费者,保证“至少一次”投递。但这也正是导致重复消费(第一个问题)的根本原因。
由于 RabbitMQ 是 “至少投递一次” 的模型,无法完全避免重复投递,所以我们需要在消费端实现 幂等性 —— 即同一条消息消费一次和多次,业务效果一样。
✅ 消费端实现幂等性(重点!)
常见幂等性策略包括:
1、利用消息唯一 ID 去重(消息去重表)
- 在消息中携带一个 唯一业务 ID(如订单号、事务 ID、UUID 等)。
- 建立一个消费日志表记录已经消费的id
- 消费前查询是否已处理该 ID,若已处理则跳过;未处理则执行业务并记录处理状态。
-- Pseudo-code
IF NOT EXISTS (SELECT 1 FROM message_log WHERE msg_id = ?) THEN
-- 执行业务逻辑
INSERT INTO message_log (msg_id, status) VALUES (?, 'done')
ELSE
-- 已处理,忽略
END IF
2、利用数据库本身的幂等性操作
让数据库操作本身具有幂等性,比如依赖主键约束,避免重复插入。这样我们就不用使用别的措施去保证。
3、使用Redis + SETNX 实现幂等性
SETNX
是 Redis 提供的 原子操作(Set if Not eXists)。- 它会尝试将一个键设置为某个值,但仅当该键不存在时才会成功设置,并返回
1
,否则返回0
。 - 搭配
EX
(设置过期时间)使用可以防止 key 永久占用内存。
-
消费端接收到消息后:
-
先尝试用 Redis 的
SETNX
设置一个“处理标记” -
如果设置成功,说明第一次处理,就执行业务逻辑
-
如果设置失败,说明这条消息已经被处理过了,直接忽略
-
以上思路跟使用redis 实现分布式锁的逻辑类似
7、实现RabbitmQ的高并发使用
既然RabbitmQ是一个如今几乎必不可少的一个中间件,那么在应对高流量的访问,单机Rabbit MQ 肯定不够满足,所以我们自然而然的想到要进行集群扩展
1、集群架构设计
RabbitMQ 集群是由多个节点(Broker)组成的逻辑整体,节点间通过 Erlang 的分布式特性通信协同处理消息。
🧩 节点类型
RabbitMQ 的节点分为三种:
节点类型 | 简介 | 是否存储消息队列数据 |
---|---|---|
Disk Node(磁盘节点) | 数据持久化到磁盘,集群核心节点 | ✅ |
RAM Node(内存节点) | 仅将元数据保存在内存中 | ❌ |
Management Node(管理节点) | 提供 UI、HTTP API 管理能力 | ⚠️ 不特殊,是普通节点安装了管理插件 |
📌 一般建议集群中至少有两个 Disk Node,其他可以是 RAM Node。
2、消息调度与负载均衡
设计好集群之后,我们当然要想到消息调度与负载均衡的问题,这样才能使集群的每个节点都能发挥好作用,从而实现整个系统的高并发。
以下是两个比较常见的方法
方式一:客户端使用不同节点连接
-
在客户端配置连接多个节点(failover + load balance)
-
使用如 HAProxy / Nginx TCP 代理 做负载均衡
方式二:使用官方 cluster_nodes
参数配置客户端连接多个节点
spring.rabbitmq.addresses=host1:5672,host2:5672,host3:5672
8、实现RabbitMQ的高可用
好了既然实现RabbitMQ的高并发,那接着想到的就是实现其高可用,我们观察上面提到的这种RabbitMQ集群可以实现高并发吗,存储某个消息的队列只位于集群中的一个节点上,那如果这个节点挂掉了,那怎么系统不久崩溃了嘛,这就是缺陷,所以我们要以上以上集群的基础上增加相应措施来避免类似情况的发送,RabbitMQ采用的是镜像队列和主从切换的措施
1、镜像队列
-
主节点(Master)负责接收、存储、分发消息
-
镜像节点(Slave)负责同步主节点的所有消息和状态副本
-
主节点的所有操作都会同步到镜像节点
这样,即使主节点宕机,镜像节点可以快速切换为新的主节点,保证消息不中断、服务持续可用。
2、主从切换
当某个队列的主节点发生故障的时候
- 主节点挂掉或网络隔离时,RabbitMQ 集群会自动选举一个镜像节点成为新的主节点
- 新主节点继续提供队列服务,消费者自动切换,不影响业务消费
- 当原主节点恢复,会自动成为镜像节点,和新主节点同步数据
镜像队列的引入可以极大地提升 RabbitMQ 的可用性及可靠性,提供了数据冗余备份、避免单点故障的功能,因此推荐在实际应用中为每个重要的队列都配置镜像。
那么镜像队列就没有缺点了吗?当然不是,那么镜像集群的缺点是什么呢?
首先镜像队列需要为每一个节点都要同步所有的消息实体,所以会导致网络带宽压力很大。 提供了数据的冗余备份,会导致存储压力变大,可能会出现IO瓶颈。
RabbitMQ 常见使用场景详解
说了那么多,那我们来具体看看消息队列有哪些常见的业务使用场景:
1. 异步处理
目的:将耗时或非关键操作从主业务流程中剥离,实现异步执行,提升系统响应速度和用户体验。
-
功能实现:
-
生产者将任务消息发送到队列,不需等待任务完成立即返回。
-
消费者异步消费消息,处理具体业务逻辑。
-
通过消息持久化保证任务不会丢失。
-
消费者确认(ACK)确保消息成功处理。
-
-
典型应用:
-
用户注册后发送欢迎邮件。
-
订单支付后异步生成发票或物流信息。
-
视频上传后异步转码处理。
-
2. 服务解耦
目的:将复杂系统拆分成多个独立模块,模块间通过消息通信,降低耦合度,增强系统灵活性和可维护性。
-
功能实现:
-
利用交换机(Direct、Topic、Fanout)实现多种路由策略,支持模块间消息灵活分发。
-
发布-订阅模式实现广播通知,多个服务同时接收消息。
-
死信队列处理异常消息,保证系统健壮性。
-
-
典型应用:
-
电商系统中,订单服务、库存服务、支付服务解耦。
-
微服务架构中,各个服务通过消息异步通信。
-
实时通知系统,推送活动信息给不同客户端。
-
3. 流量控制(削峰)
目的:应对突发流量,防止后端系统过载,保证系统稳定运行。
-
功能实现:
-
队列缓存高峰流量,平滑请求处理速度。
-
通过限流和流控策略,控制消息消费速率。
-
优先级队列确保关键任务优先处理。
-
延迟队列实现定时重试和错峰处理。
-
-
典型应用:
-
秒杀、抢购系统峰值请求排队处理。
-
大促活动中订单流量均衡。
-
后台批量任务错峰执行。
-
到此这篇文章也就结束了,希望读者阅读完之后能够对rabbitmq具有初步的认识,这篇文章旨在帮助读者短时间内能够应对面试,有很多没有讲到的知识点,敬请指正!