1 为什么需要消息队列
从同步阻塞到异步解耦 —— MQ 改变了系统间通信的根本方式

同步 vs 异步:没有 MQ vs 有 MQ

没有消息队列 — 同步阻塞调用
服务 A
用户下单
服务 B
扣减库存
服务 C
发送通知
服务 D
更新统计

总耗时 = A + B + C + D  |  任何服务挂掉,整条链路失败

有消息队列 — 异步解耦
服务 A
用户下单
消息队列
Message Queue
服务 B扣减库存
服务 C发送通知
服务 D更新统计

总耗时 ≈ A + 写入MQ  |  服务独立运行,互不影响

生活类比

没有 MQ = 你亲自跑银行排队。你要转账、理财、办卡,每个业务都要排队等前一个办完才能办下一个。你被「阻塞」了,一步卡住后面全卡住。

有 MQ = 用快递。你把包裹丢到快递柜(队列),就可以去做别的事了。快递员(消费者)按自己的节奏取件、配送。你不需要等快递员,快递员也不需要等你。双方完全解耦。

三大核心价值

解耦
订单服务
消息队列
↓ ↓ ↓
库存
通知
统计

新增服务只需订阅队列,
无需修改上游代码

异步
下单请求
5ms
写入MQ,立即返回
后台处理
50ms
100ms
200ms

用户等待: 355ms → 5ms
响应速度提升 70 倍

削峰
5000 请求/秒 (高峰)
MQ 缓冲区
500 请求/秒 (匀速消费)

高峰请求全部接住,
下游按能力平稳消费

生产场景

PRODUCTION 订单系统高峰期

场景:双十一秒杀,订单系统瞬间涌入 5000 订单/秒,但数据库写入能力只有 500/秒

不使用 MQ:数据库连接池耗尽 → 请求超时 → 雪崩 → 整个系统崩溃。

使用 MQ:订单先写入消息队列(内存级速度),消费者以数据库能承受的速度匀速消费。高峰期积压的消息在平峰期慢慢消化完。

// 伪代码:削峰保护
app.post('/order', async (req, res) => {
  // 秒级响应 — 只写MQ
  await mq.send('order_queue', orderData);
  res.json({ msg: '订单已接受,处理中' });
});

// 消费者匀速消费
mq.consume('order_queue', async (msg) => {
  await db.insert('orders', msg);  // 数据库不扛压
}, { prefetch: 50 });  // 限制消费速率
2 消息模型
理解消息传递的两种基本模式

Point-to-Point (点对点)

点对点模型 — 一条消息只被一个消费者消费
Producer
生产者
Queue
消息 A | B | C | D
Consumer 1A, C
Consumer 2B, D

消息被分配给不同消费者,每条消息只被消费一次

Publish-Subscribe (发布订阅)

发布订阅模型 — 一条消息被所有订阅者消费
Publisher
发布者
Topic
主题
Subscriber 1收到 A, B, C
Subscriber 2收到 A, B, C
Subscriber 3收到 A, B, C

每条消息被所有订阅者各消费一次

Topic vs Queue 对比

维度Queue (队列)Topic (主题)
消费模式竞争消费,一条消息一个消费者每条消息广播给所有订阅者
消费者关系互斥,同组只有一个能收到独立,每个订阅者各收到一份
典型场景任务分发、工作队列事件通知、日志分发
代表产品RabbitMQ QueueKafka Topic

Consumer Groups (消费者组)

消费者组 = 点对点 + 发布订阅的结合

同组内:竞争消费,一条消息只被组内一个消费者处理(负载均衡)。

不同组间:每条消息被每个组各消费一次(发布订阅)。

这是 Kafka 的核心模型。比如 "订单创建" 消息 → 库存组 减库存,通知组 发短信,统计组 更新报表。各组独立消费、互不影响。

3 RabbitMQ
最成熟的开源消息代理 — Erlang 实现,支持复杂路由

架构总览

RabbitMQ 架构:Producer → Exchange → Queue → Consumer
Producer
生产者
Exchange
交换机 (路由)
Queue 1
Queue 2
Queue 3
Consumer A
Consumer B
Consumer C

Exchange 负责路由规则,消息不会直接发到 Queue,而是先经过 Exchange

Exchange 类型 — 四种路由策略

Direct Exchange (直连)

Exchange
key=order
Queue-order
key=log
Queue-log

精确匹配 routing key,一对一路由

Fanout Exchange (广播)

Exchange
Queue A
Queue B
Queue C

忽略 routing key,广播到所有绑定队列

Topic Exchange (主题)

Exchange
order.*
Queue-order
#.error
Queue-err

支持通配符:* 一个词,# 零或多个词

Headers Exchange (头部)

Exchange
header匹配
Queue

根据消息 headers 属性匹配,忽略 routing key

消息确认 (ACK / NACK)

确保消息不丢失的机制

ACK (确认):消费者处理完消息后发送 ack,RabbitMQ 才将消息从队列删除。

NACK (拒绝):消费者处理失败发送 nack,可指定是否重新入队 requeue=true

关键配置:开启手动确认 { noAck: false },否则消息发出后立刻删除(可能丢失)。

// 手动 ACK 模式
channel.consume('orders', (msg) => {
  try {
    processOrder(msg);
    channel.ack(msg);    // 处理成功 → 确认
  } catch (err) {
    channel.nack(msg, false, true);  // 失败 → 重新入队
  }
}, { noAck: false });

死信队列 (Dead Letter Queue)

死信队列流程图
正常队列 (order_queue)
TTL=30min, max-length=1000
▼ 消息变成「死信」(以下任一条件)
消费者 NACK
且不重新入队
消息 TTL
过期
队列超过
最大长度
▼ 自动转发
死信交换机 (dlx_exchange)
DLX (Dead Letter Exchange)
▼ 路由到
死信队列 (dead_letter_queue)
人工处理 / 告警 / 重试

生产案例:订单支付超时自动取消

PRODUCTION TTL + 死信队列实现延迟取消

需求:用户下单后 30 分钟未支付,自动取消订单。

实现方案:

  1. 创建普通队列 order_ttl_queue,设置 TTL = 30 分钟,绑定死信交换机
  2. 创建死信队列 order_cancel_queue,消费者监听此队列
  3. 用户下单 → 消息发到 order_ttl_queue(没有消费者)
  4. 30 分钟后消息过期 → 自动进入死信队列 → 消费者处理取消逻辑
// Java (Spring AMQP) 实现:订单超时自动取消
@Configuration
public class RabbitMQConfig {

    // 死信交换机
    @Bean
    public DirectExchange dlxExchange() {
        return new DirectExchange("dlx_exchange");
    }

    // 死信队列 — 消费者监听此队列
    @Bean
    public Queue orderCancelQueue() {
        return QueueBuilder.durable("order_cancel_queue")
                .withArgument("x-dead-letter-exchange", "")
                .build();
    }

    @Bean
    public Binding dlxBinding() {
        return BindingBuilder.bind(orderCancelQueue())
                .to(dlxExchange()).with("order_cancel");
    }

    // TTL 队列 — 消息30分钟后过期,转入死信
    @Bean
    public Queue orderTtlQueue() {
        return QueueBuilder.durable("order_ttl_queue")
                .withArgument("x-message-ttl", 1800000)           // 30分钟
                .withArgument("x-dead-letter-exchange", "dlx_exchange")
                .withArgument("x-dead-letter-routing-key", "order_cancel")
                .build();
    }
}

// 生产者:下单时发送消息到TTL队列
@Service
public class OrderService {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void placeOrder(String orderId) {
        rabbitTemplate.convertAndSend("",
                "order_ttl_queue",
                new OrderMessage(orderId, System.currentTimeMillis()));
        // 消息进入TTL队列 → 30分钟后过期 → 自动转入死信队列
    }
}

// 消费者:监听死信队列,执行取消
@Component
public class OrderCancelConsumer {

    @RabbitListener(queues = "order_cancel_queue")
    public void handleCancel(OrderMessage msg, Channel channel,
                             Message message) throws IOException {
        try {
            cancelOrder(msg.getOrderId());   // 取消订单
            channel.basicAck(message.getMessageProperties()
                    .getDeliveryTag(), false);  // 手动ACK
        } catch (Exception e) {
            channel.basicNack(message.getMessageProperties()
                    .getDeliveryTag(), false, true);  // 重新入队
        }
    }
}

完整 Java (Spring AMQP) 示例

发送消息 (Producer)
// application.yml 配置
spring:
  rabbitmq:
    host: localhost
    port: 5672
    publisher-confirm-type: correlated  # 发布确认
    publisher-returns: true

// Producer — 发送消息
@Service
public class TaskProducer {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void sendTask(String task) {
        // 持久化消息:MessageDeliveryMode.PERSISTENT
        rabbitTemplate.convertAndSend("task_exchange",
                "task.routing.key", task, message -> {
            message.getMessageProperties()
                   .setDeliveryMode(MessageDeliveryMode.PERSISTENT);
            return message;
        });

        System.out.println(" [x] Sent: " + task);
    }
}
消费消息 (Consumer)
// Consumer — 消费消息
@Component
public class TaskConsumer {

    // prefetch=1:公平分发,同时只处理1条
    @RabbitListener(
        queues = "task_queue",
        concurrency = "1",          // 单消费者
        ackMode = "MANUAL"          // 手动ACK
    )
    public void handleTask(String task, Channel channel,
                           Message message) throws IOException {
        long tag = message.getMessageProperties()
                          .getDeliveryTag();

        System.out.println(" [x] Received: " + task);

        try {
            // 模拟处理耗时
            Thread.sleep(1000);
            System.out.println(" [x] Done");
            channel.basicAck(tag, false);    // 确认

        } catch (Exception e) {
            // 处理失败 → 拒绝 → 重新入队
            channel.basicNack(tag, false, true);
        }
    }
}
4 Kafka
分布式流处理平台 — 高吞吐、持久化、水平扩展

架构总览

Kafka 架构:Producer → Topic → Partition → Consumer Group
Producer 1
Producer 2
↓ ↓
Topic: orders (3 Partitions)
Partition 0
0
1
2
3
↑ consumer offset
Partition 1
0
1
2
↑ consumer offset
Partition 2
0
1
2
↑ consumer offset
↓ ↓ ↓
Consumer Group: order-processors
Consumer A
P0
Consumer B
P1
Consumer C
P2

每个 Partition 只能被组内一个 Consumer 消费 → 负载均衡 + 有序性保证

Partition & Offset

Partition 是 Kafka 并发和有序的核心

Partition(分区):一个 Topic 被分成多个 Partition,分布在不同的 Broker 上。分区是并行度的基本单位。

Offset(偏移量):每条消息在 Partition 内的唯一递增编号。Consumer 通过 offset 记录消费进度。

有序性保证:同一 Partition 内消息严格有序;不同 Partition 之间不保证顺序。

关键公式:
Consumer 数量 ≤ Partition 数量(否则多余的 Consumer 空闲)
吞吐量 ∝ Partition 数量(更多分区 = 更多并行)

为什么 Kafka 这么快?

Kafka 高性能三板斧
顺序写磁盘
600 MB/s (顺序写)
vs 100KB/s (随机写)
零拷贝
跳过用户态拷贝
4次 → 2次拷贝
批量压缩
批量发送 + 压缩
网络 IO 减少 70%
零拷贝原理

传统方式:磁盘 → 内核缓冲区 → 用户缓冲区 → Socket 缓冲区 → 网卡 (4 次拷贝)

零拷贝 (sendfile):磁盘 → 内核缓冲区 → 网卡 (2 次拷贝,CPU 不参与数据搬运)

Kafka 使用 Linux 的 sendfile() 系统调用,直接在内核空间完成数据传输。

Kafka vs RabbitMQ

维度RabbitMQKafka
定位消息代理 (Broker)分布式流平台
语言实现ErlangScala / Java
吞吐量万级/秒百万级/秒
延迟微秒级毫秒级
消息模型Exchange → QueueTopic → Partition
消息顺序单队列有序单分区有序
路由能力非常灵活 (4 种 Exchange)基于 Topic 分区
消息持久化可选 (内存/磁盘)默认持久化 (日志文件)
回溯消费不支持 (消费即删除)支持 (通过 offset 重置)
适用场景任务队列、复杂路由日志收集、流处理

生产案例:日志收集系统

PRODUCTION 每天 10 亿条日志消息

架构:

1000 台服务器
Filebeat
Kafka Cluster
10+ 亿条/天
Flink
实时计算
Elasticsearch
日志检索
HDFS
归档存储

为什么选 Kafka:每天 10 亿条 ≈ 12,000 条/秒(平均),高峰可达 50,000+ 条/秒。需要高吞吐 + 持久化 + 多消费者组。RabbitMQ 在这个量级下扛不住。

什么时候用什么?

选 RabbitMQ 的场景
  • 复杂路由需求(多种 Exchange)
  • 任务队列 / 工作队列
  • 需要消息确认、优先级队列
  • 中小规模 (万级 TPS 足够)
  • 延迟敏感(微秒级)
  • 需要 TTL、死信队列
选 Kafka 的场景
  • 高吞吐量需求 (百万级 TPS)
  • 日志收集、事件流处理
  • 需要消息回溯 (重新消费)
  • 大数据生态 (Flink, Spark)
  • 事件溯源 / CQRS
  • 消息持久化 + 长期存储
5 Redis Streams (轻量级方案)
如果你已经在用 Redis,小规模场景不需要引入新的中间件

Redis 作为消息队列

Redis 5.0+ 的 Streams 数据类型

Redis Streams 提供了类似 Kafka 的消费者组模型,适合轻量级消息队列场景。

# 添加消息 (相当于 Kafka Producer)
XADD orders * orderId ORD-001 userId U-1001 amount 99.9
# 返回: "1704067200000-0"  (自动生成的时间戳 ID)

# 创建消费者组
XGROUP CREATE orders order-group $ MKSTREAM

# 消费者读取消息
XREADGROUP GROUP order-group consumer-1 COUNT 1 BLOCK 5000 STREAMS orders >
# > 表示只读取未消费的新消息

# 确认消息 (相当于 ACK)
XACK orders order-group 1704067200000-0

# 查看待处理消息 (查看积压)
XPENDING orders order-group

Redis Streams 的局限

知道什么时候不够用
维度Redis StreamsRabbitMQ / Kafka
吞吐量上限受 Redis 单线程限制分布式水平扩展
持久化内存为主,RDB/AOF 可能丢数据磁盘持久化,数据安全
消息堆积受内存限制磁盘存储,可堆积 TB 级
路由能力无 (只有 Stream 名)RabbitMQ: 丰富 / Kafka: Topic
监控运维简单成熟的监控生态

什么时候够用?什么时候升级?

Redis MQ 足够
  • QPS < 5000,消息量级小
  • 团队已经在用 Redis,不想加中间件
  • 不需要复杂路由
  • 消息丢失可容忍或业务可补偿
  • 短期项目 / MVP
该升级了
  • QPS > 10000 或快速增长中
  • 消息不能丢 (金融、订单)
  • 需要消息堆积能力
  • 需要复杂路由规则
  • 需要多团队共享消息集群
6 关键设计问题
面试高频考点 — 每个问题都有对应的解决方案

消息幂等性

高频题 为什么需要幂等?

网络抖动导致 ack 丢失 → MQ 重发消息 → 同一笔订单扣了两次库存。

不保证幂等 = 重复消费 = 数据错误 = 生产事故

幂等性解决方案:唯一ID + 去重表
1. 消费消息
收到 msg_id=A001
2. 检查去重表
SELECT * FROM dedup WHERE id='A001'
不存在 →
执行业务
INSERT 去重表
→ ACK
已存在 →
跳过 (已处理)
→ ACK
// 幂等消费伪代码
async function consumeMessage(msg) {
  const msgId = msg.headers['message-id'];

  // 去重检查 (Redis 或数据库)
  const exists = await redis.set(`dedup:${msgId}`, 1, 'NX', 'EX', 86400);
  if (!exists) {
    console.log('重复消息,跳过', msgId);
    return ack(msg);
  }

  // 执行业务逻辑
  await processOrder(msg.body);
  return ack(msg);
}

消息顺序性

分区有序 vs 全局有序

分区有序:同一个 Partition 内消息严格按写入顺序消费。这是 Kafka 默认保证的。

全局有序:整个 Topic 所有消息有序。代价极大 — 只能有一个 Partition,完全丧失并行性。

最佳实践:将需要有序的消息发到同一个 Partition(通过 key hash 路由)。

保证顺序:相同 Key → 相同 Partition
用户A: 创建→修改→删除
Partition 0 (hash(A))
创建
修改
删除
顺序消费 ✅

消息可靠性

消息不丢失的三重保障
生产者端 confirm 模式 → 等待 Broker 确认写入成功
Broker 端 队列持久化 + 消息持久化 → 重启不丢数据
消费者端 手动 ACK → 处理成功才确认,失败则重新入队
// RabbitMQ 生产者确认
ch.confirmSelect();  // 开启 confirm 模式
ch.waitForConfirms(); // 等待 Broker 确认

// 队列持久化
ch.assertQueue('orders', { durable: true });

// 消息持久化
ch.sendToQueue('orders', Buffer.from(msg), { persistent: true });

延迟消息

常见延迟队列实现方案
方案原理适用
RabbitMQ TTL + DLX消息过期后进入死信队列精确延迟,量级中等
Kafka 时间轮内部时间轮定时器高吞吐延迟
Redis ZSETscore 为执行时间,定时轮询轻量级,简单场景
数据库轮询定时扫表找过期记录最简单,性能差
RocketMQ 延迟级别内置 18 个延迟级别Java 生态首选

消息积压

生产事故 消费太慢怎么办?

凌晨发现队列积压了 500 万条消息,消费者处理速度远远跟不上。

消息积压应急方案
积压 500 万条
原因:消费者 Bug / 流量突增
方案1: 紧急扩容
临时加 10 倍 Consumer
方案2: 转发分流
写程序快速转发到新队列
方案3: 丢弃低优先级
保核心业务,丢日志类
长期方案
增加 Partition / 优化消费逻辑 / 预警监控
预防积压的最佳实践
  • 监控告警:队列深度 > 阈值时立即告警 (如积压 > 10000 条)
  • 合理的 prefetch:RabbitMQ 设置 prefetch,防止消费者被压垮
  • 批量消费:消费者一次拉取多条,减少网络往返
  • 容量规划:提前评估高峰流量,预留消费能力
  • 降级方案:积压严重时丢弃非核心消息,保证核心业务
7 选型决策树
根据业务场景快速选择合适的消息队列
MQ 选型决策流程图
需要消息队列?业务场景分析
吞吐量需求?QPS 量级
QPS > 10万
Kafka高吞吐 + 持久化
QPS < 1万
已有 Redis?基础判断
Redis Streams轻量方案
RabbitMQ功能全面
1万~10万
需要复杂路由?Exchange 模型
RabbitMQ4种Exchange
Kafka流处理

三方对比总表

维度RabbitMQKafkaRedis Streams
定位消息代理分布式流平台轻量消息队列
语言ErlangScala/JavaC
吞吐量~万级 TPS~百万级 TPS~万级 TPS
延迟微秒级毫秒级微秒级
持久化可选默认磁盘内存为主
路由能力★★★★★★★☆☆☆★☆☆☆☆
消息回溯不支持支持有限支持
运维复杂度中等较高
客户端生态丰富丰富Redis 生态
适用场景任务队列/路由日志/流处理轻量队列
8 面试题精选
25 道高频面试题 — 点击展开答案
1为什么需要消息队列?能解决什么问题?

三大核心价值:

  1. 解耦:生产者和消费者通过队列解耦,新增消费者无需修改生产者代码。
  2. 异步:耗时操作异步处理,用户请求无需等待全部完成,响应更快。
  3. 削峰:高峰期请求先入队列缓冲,下游按自身能力匀速消费,防止系统崩溃。

类比:没有 MQ 像亲自跑银行排队(同步阻塞),有 MQ 像用快递(异步解耦)。

2RabbitMQ 和 Kafka 的核心区别是什么?怎么选?

RabbitMQ:消息代理,支持复杂路由(4 种 Exchange),延迟低(微秒级),适合任务队列、复杂路由场景。吞吐量万级。

Kafka:分布式流平台,高吞吐(百万级 TPS),默认持久化,支持消息回溯。适合日志收集、流处理、大数据场景。

选型原则:需要复杂路由 → RabbitMQ;需要高吞吐 → Kafka;轻量级 → Redis Streams。

3什么是消息幂等性?为什么需要?怎么实现?

定义:同一条消息被消费多次,结果和消费一次一样。

为什么需要:网络抖动导致 ACK 丢失,MQ 会重发消息。不保证幂等 → 重复扣款、重复发货。

实现方案:

  1. 唯一 ID + 去重表:每条消息带唯一 ID,消费前查去重表,已存在则跳过。
  2. 乐观锁/版本号:更新时带版本号条件 UPDATE ... WHERE version=v
  3. Redis SET NX:SET dedup:msgId 1 NX EX 86400,设置成功才执行。
4如何保证消息的顺序性?

Kafka:同一 Partition 内消息严格有序。将需要有序的消息用相同的 key 发送(路由到同一 Partition)。全局有序只需一个 Partition(牺牲并行性)。

RabbitMQ:单队列有序。但多消费者竞争消费会破坏顺序,需确保顺序消息只由一个消费者处理。

最佳实践:分区有序(按 key 路由),而非全局有序。

5如何保证消息不丢失?

三个环节都要保障:

  1. 生产者:使用 confirm 模式(RabbitMQ)或 acks=all(Kafka),确保消息成功写入 Broker。
  2. Broker:队列持久化 + 消息持久化。Kafka 默认持久化到磁盘。
  3. 消费者:手动 ACK,处理成功后才确认。失败则 NACK 重新入队。
6什么是死信队列?什么场景下使用?

定义:无法被正常消费的消息会被转发到死信队列(Dead Letter Queue),用于统一处理异常消息。

消息变成死信的条件:

  1. 消费者 NACK 且不重新入队
  2. 消息 TTL 过期
  3. 队列超过最大长度

典型场景:订单支付超时自动取消(TTL 30 分钟 + 死信队列)、异常消息人工处理、告警通知。

7消息积压了怎么办?

应急方案:

  1. 紧急扩容:临时增加消费者实例(10 倍),快速消化积压。
  2. 转发分流:写程序将积压消息快速转发到多个新队列,多队列并行消费。
  3. 丢弃低优先级:非核心消息(如日志)直接丢弃,保核心业务。

长期预防:监控告警 + 合理 prefetch + 批量消费 + 容量规划。

8RabbitMQ 的 Exchange 有哪几种类型?
  1. Direct:精确匹配 routing key,点对点路由。
  2. Fanout:广播到所有绑定队列,忽略 routing key。
  3. Topic:支持通配符匹配(* 匹配一个词,# 匹配零或多个词)。
  4. Headers:根据消息 headers 属性匹配,不常用。
9Kafka 为什么这么快?
  1. 顺序写磁盘:追加写入日志文件,速度可达 600MB/s(随机写只有 100KB/s)。
  2. 零拷贝 (sendfile):数据从磁盘直接到网卡,跳过用户态拷贝(4 次 → 2 次)。
  3. 批量发送 + 压缩:Producer 批量聚合消息,减少网络 I/O。
  4. PageCache:利用操作系统页缓存,热点数据直接从内存读取。
  5. 分区并行:Topic 分成多个 Partition,分布在不同 Broker 上并行读写。
10Kafka 的 Partition 和 Consumer Group 是什么关系?

Consumer Group 内:一个 Partition 只能被组内一个 Consumer 消费(负载均衡)。

Consumer Group 间:每个组独立消费同一 Topic 的全部消息(发布订阅)。

关键限制:Consumer 数量 > Partition 数量时,多余的 Consumer 会空闲。扩容消费者前先扩容 Partition。

11如何实现延迟消息?

常见方案:

  1. RabbitMQ TTL + DLX:消息设 TTL,过期后进入死信队列,消费者监听死信队列。
  2. Redis ZSET:score 为执行时间戳,定时轮询 score <= now 的消息。
  3. Kafka 时间轮:内部 DelayQueue + TimingWheel 实现。
  4. 数据库轮询:最简单但性能差,适合低频场景。
  5. RocketMQ:内置延迟级别(18 级),开箱即用。
12什么是消息的背压 (Backpressure)?

定义:当消费者处理速度跟不上生产速度时,通过反馈机制告知生产者降低发送速率,防止系统过载。

RabbitMQ:通过 prefetch 限制未确认消息数量。达到上限后不再推送,天然背压。

Kafka:消费者自己控制拉取速率(pull 模式),可以暂停消费。但没有自动背压机制,需在应用层实现。

13RabbitMQ 怎么实现消息持久化?

三个条件缺一不可:

  1. Exchange 持久化:assertExchange(name, type, { durable: true })
  2. Queue 持久化:assertQueue(name, { durable: true })
  3. Message 持久化:sendToQueue(name, msg, { persistent: true })

注意:持久化不保证 100% 不丢。消息先到 PageCache,还没刷盘就宕机会丢。需要更高可靠性用 publisher confirms

14Kafka 的 offset 是什么?怎么管理?

Offset:消息在 Partition 内的唯一递增编号,标识消费进度。

管理方式:

  1. 自动提交:enable.auto.commit=true,定期提交,可能丢消息或重复消费。
  2. 手动提交:commitSync()commitAsync(),处理完再提交,更精确。
  3. 指定 offset:seek(partition, offset),可以回溯重新消费。
15RabbitMQ 的 ACK 和 NACK 有什么区别?

ACK (ack):告诉 Broker 消息处理成功,可以删除。消息的生命周期结束。

NACK (nack):告诉 Broker 消息处理失败。

  • requeue=true:消息重新入队,重新投递给消费者。
  • requeue=false:消息变成死信(如果配置了 DLX)或被丢弃。

注意:NACK + requeue=true 可能导致消息无限循环重试。建议设置最大重试次数。

16消息队列有哪些常见问题?
  1. 消息丢失:ACK 机制 + 持久化 + 生产者确认
  2. 消息重复:幂等消费(唯一 ID + 去重表)
  3. 消息积压:扩容消费者 + 监控告警
  4. 消息乱序:相同 Key 路由到同一 Partition
  5. 系统复杂度:引入 MQ 增加了运维成本,需要监控、 HA、故障恢复
17点对点和发布订阅有什么区别?

点对点 (PTP):一条消息只被一个消费者消费。多个消费者竞争消费,消息在消费者间分配。适合任务分发。

发布订阅 (Pub/Sub):一条消息被所有订阅者各消费一次。每个订阅者独立接收完整消息。适合事件广播。

Kafka 的 Consumer Group:结合两者优势 — 组内竞争消费(PTP),组间广播(Pub/Sub)。

18Kafka 如何保证高可用?
  1. 副本机制 (Replica):每个 Partition 有多个副本,分布在不同 Broker 上。
  2. Leader/Follower:读写都走 Leader,Follower 同步数据。Leader 宕机时 Follower 自动晋升。
  3. ISR (In-Sync Replicas):与 Leader 保持同步的副本集合。只有 ISR 中的副本才能成为新 Leader。
  4. Controller:Kafka 集群中有一个 Broker 担任 Controller,负责分区 Leader 选举、Broker 故障检测。
19Redis Streams 能替代 RabbitMQ 吗?

小规模场景可以:QPS < 5000、消息量小、已在用 Redis、不需要复杂路由时,Redis Streams 是轻量好选择。

不能替代的场景:

  • 需要复杂路由(多种 Exchange)
  • 消息不能丢失(Redis 内存为主,RDB/AOF 可能丢数据)
  • 需要大量消息堆积(Redis 受内存限制)
  • 需要成熟的运维监控生态
20RabbitMQ 的 vhost 是什么?

VHost (Virtual Host):RabbitMQ 中的逻辑隔离单位,类似于数据库中的"数据库"概念。

  • 每个 VHost 有独立的 Exchange、Queue、Binding、权限
  • 不同项目/环境可以用不同的 VHost 隔离
  • 默认 VHost 是 /
  • 用户可以被授权访问特定的 VHost
21如何实现消息的优先级?

RabbitMQ:声明队列时设置 x-max-priority(如 10),发送消息时设置 priority 字段。高优先级消息优先被消费。

Kafka:没有原生优先级支持。可以通过不同 Topic 模拟(高优先级 Topic 优先消费),或在应用层实现。

// RabbitMQ 优先级队列
ch.assertQueue('tasks', {
  arguments: { 'x-max-priority': 10 }
});
ch.sendToQueue('tasks', msg, { priority: 5 });
22消息重复消费的根本原因是什么?

根本原因:消费者处理成功,但 ACK 未到达 Broker(网络超时、消费者宕机、Broker 重启)。Broker 认为消息未消费,重新投递。

常见场景:

  1. 消费者处理完消息后,发送 ACK 前宕机
  2. 网络抖动导致 ACK 丢失
  3. 消费者处理超时,Broker 认为消费失败重新投递

结论:任何消息队列都可能重复投递,消费者必须保证幂等。

23Kafka 的消费者 Rebalance 是什么?有什么问题?

Rebalance:Consumer Group 中有消费者加入或退出时,Partition 需要重新分配给剩余消费者。

问题:

  1. Rebalance 期间所有消费者暂停消费(Stop The World)
  2. 频繁 Rebalance 导致消费延迟
  3. 可能触发连锁 Rebalance(消费者超时 → Rebalance → 更多超时)

优化:

  • 调大 session.timeout.msheartbeat.interval.ms
  • 使用 StickyAssignor 减少分区迁移
  • 确保消费者处理逻辑高效
24消息队列会带来哪些系统复杂度?
  1. 运维成本:MQ 本身是分布式系统,需要 HA、监控、升级、故障恢复。
  2. 一致性问题:分布式事务难处理。生产者写库成功但 MQ 发送失败怎么办?需要补偿机制。
  3. 调试困难:异步链路比同步链路更难追踪。需要分布式追踪(如 Jaeger)。
  4. 消息堆积风险:消费者故障时消息堆积,可能导致内存溢出或磁盘满。
  5. 系统可用性:MQ 挂了 = 系统不可用。MQ 成了新的单点。
25设计一个消息系统需要考虑哪些问题?

核心问题清单:

  1. 消息丢失:生产者确认 + Broker 持久化 + 消费者手动 ACK
  2. 消息重复:唯一 ID + 去重表 + 幂等消费
  3. 消息顺序:相同 Key → 相同 Partition,分区有序
  4. 消息延迟:TTL + 死信队列 / 时间轮 / Redis ZSET
  5. 消息积压:扩容消费者 + 监控告警 + 降级方案
  6. 消息回溯:Kafka offset 重置
  7. 高可用:RabbitMQ 镜像队列 / Kafka 副本机制
  8. 安全:认证、授权、VPC 隔离、消息加密

总结公式:消息不丢 + 不重 + 有序 + 可回溯 = 可靠消息系统。

RabbitMQ 集群高可用架构

三种集群模式对比

模式原理优缺点
普通集群 队列只存在一个节点,其他节点可转发请求 性能好但节点挂了队列不可用
镜像队列集群
(Mirrored Queue)
队列在多个节点间同步复制 高可用,节点挂了自动切换 ✅
但性能有损耗(同步开销)
仲裁队列
(Quorum Queue, 3.8+)
基于 Raft 协议的强一致队列 推荐新方案 ✅✅
替代镜像队列,性能更好

面试回答技巧:先说"三种模式",再强调"3.8+ 推荐用 Quorum Queue 替代镜像队列",体现对新特性的关注。

消息队列实战 | 全栈面试备战 #03  |  Last updated: 2026-06