面向 Java 后端开发,涵盖选型、可靠性、顺序性、幂等、积压、高可用、事务消息等高频面试题。 采用「底层原理 → 源码实现 → 面试标准答案 → 追问 → 业务落地」五段式结构。


目录

  1. 什么是消息队列

  2. 消息队列的使用场景

  3. 如何解决消息丢失

  4. 如何保证消息顺序性

  5. 如何避免重复消费

  6. 什么是幂等

  7. 如何处理消息积压

  8. MQ 技术选型

  9. 高可用架构

  10. 事务消息与数据一致性

  11. 手写 MQ 架构设计

  12. 阻塞队列有界与无界

  13. 为什么放弃 ZooKeeper

  14. 补充高频追问


1. 什么是消息队列

1.1 底层原理

消息队列(Message Queue)本质上是一个保存消息的 FIFO(先进先出)数据结构,跨进程的通信机制。底层核心逻辑是通过"存储-转发"机制,将消息暂存于 Broker(消息服务器),避免生产者与消费者直接耦合,同时提供消息的持久化、路由、重试等能力,解决分布式系统中的通信问题。

核心模型对比

模型 说明 典型实现
点对点(P2P) 一条消息只能被一个消费者消费 RabbitMQ(Queue模式)
发布/订阅(Pub/Sub) 一条消息可被多个消费者组消费 RocketMQ(Topic模式)、Kafka

1.2 RocketMQ 实现

RocketMQ 的核心架构由 4 部分组成,完美实现消息队列的"存储-转发"逻辑:

  • Producer(生产者):负责发送消息,支持同步、异步、单向发送三种模式;

  • Consumer(消费者):负责接收并处理消息,支持集群消费、广播消费两种模式;

  • Broker(消息服务器):核心组件,负责消息的存储、转发、持久化,基于磁盘文件(CommitLog)存储,兼顾性能和可靠性;

  • NameServer(命名服务器):无状态节点,负责路由管理,类似"路由导航",Broker 启动后向其注册自身信息,Producer/Consumer 从中获取路由信息。

架构交互流程

Producer ──注册──→ NameServer ←──注册── Broker (Master/Slave)
   │                  ↑                    ↑
   │──发送消息──→ Broker ──主从同步──→ Slave
   │                  │
Consumer ←──拉取消息──┘

1.3 面试标准答案

消息队列是一种基于发布/订阅模式的异步通信中间件,核心作用是将同步的系统调用转化为异步的消息传递,从而打破系统之间的强耦合关系。RocketMQ 通过 Producer、Consumer、Broker、NameServer 四大组件协作,实现了高性能的解耦、异步和削峰,且自身不依赖 ZooKeeper,部署轻量简便。

1.4 追问

Q:RocketMQ 的 Producer 和 Consumer 分别与哪些组件交互?

A:Producer 启动时从 NameServer 拉取 Topic 路由信息(包含 Broker 地址和 Queue 列表),然后直接与 Broker Master 建立 TCP 长连接发送消息。Consumer 同样从 NameServer 拉取路由,但可以连接 Master 和 Slave 拉取消息(当 slaveReadEnable=true 时,Slave 可分担读压力)。

Q:NameServer 是无状态的,那路由信息存在哪?

A:路由信息只存在于 NameServer 的内存中(RouteInfoManager),不持久化。Broker 每 30 秒向所有 NameServer 发送心跳,NameServer 通过定时任务每 10 秒检测 Broker 存活,超过 120 秒无心跳则剔除。Producer/Consumer 每 30 秒从 NameServer 更新一次本地路由表。

1.5 业务落地

电商下单全链路。用户下单后,如果同步执行"扣减库存、生成订单、发短信、加积分",不仅耗时长,而且任何一环报错都会导致下单失败。引入 RocketMQ 后,订单服务仅需"生成订单"并将后续任务作为消息发给 MQ,立刻返回成功给用户。下游库存、短信、积分服务异步拉取消费,接口响应时间从几百毫秒骤降至几十毫秒,各服务故障互不影响。


2. 消息队列有哪些使用场景

2.1 底层原理

MQ 的核心使用场景可以用三个词概括:解耦、异步、削峰填谷,对应分布式系统中耦合度高、响应慢、流量突发三大痛点。

  • 解耦:系统间不再相互调用 API,而是通过 MQ 传递数据,新增下游业务无需改动上游代码,降低系统耦合度;

  • 异步:主流程以外的非核心链路,扔给 MQ 后台异步处理,无需同步等待,大幅提升主链路响应速度;

  • 削峰填谷:面对瞬时超大流量,利用 MQ 的暂存能力,将流量拦截在数据库之前,后台按数据库能承受的速率平滑拉取处理,避免系统被压垮。

2.2 场景详解与 RocketMQ 适配

场景 问题 RocketMQ 解决方案 典型案例
异步通信 同步调用耗时长 异步发送 + 回调确认 用户注册后异步发短信/邮件
削峰填谷 瞬时流量压垮 DB 高吞吐暂存 + 匀速消费 双11 秒杀请求缓冲
系统解耦 上下游强依赖 Topic 订阅模式 订单→物流/积分/发票
分布式事务 跨服务数据不一致 事务消息(半消息+回查) 转账、支付回调
日志/数据同步 同步传输影响性能 批量发送 + 异步传输 Binlog 同步、埋点上报

2.3 追问

Q:解耦和异步有什么区别?

A:解耦强调的是"我不需要知道你是谁"——通过 MQ 的 Topic 订阅,上游只管发消息,下游可以自由增减,互不感知。异步强调的是"我不需要等你做完"——主流程发完消息就返回,耗时操作交给消费者后台处理。两者经常同时出现,但本质不同。

Q:削峰填谷时,如果消息量持续超过消费能力怎么办?

A:这属于"谷填不满"的情况,需要配合限流 + 扩容 + 降级三板斧:生产端限流控制发送速率;消费端扩容消费者实例和队列数;非核心业务降级,只处理核心消息。

2.4 业务落地

电商秒杀系统。用户点击抢购,网关直接将请求消息发送到 RocketMQ,并立即向用户返回"排队中",无需等待后续业务处理。后台订单服务按照数据库承载力(如每秒落库 2000 单),从 RocketMQ 平滑拉取消息,执行扣减库存、创建订单的核心逻辑,完美保护底层数据库和服务不被瞬时流量压垮。


3. 消息队列如何解决消息丢失问题

3.1 底层原理

一条消息的流转经历三个核心阶段:生产者 → Broker(MQ 服务器) → 消费者。要保证消息不丢失,必须在这三个环节都做到"确认机制 + 持久化 + 重试兜底",覆盖网络抖动、服务宕机等异常场景。

消息丢失的三种情况

环节 丢失原因 后果
生产者→Broker 网络抖动、Broker 宕机 消息根本没到达 MQ
Broker 存储 内存未刷盘、单机宕机 消息到了但没存住
Broker→消费者 消费者处理完但 ACK 前宕机 消息被重复投递或丢失

3.2 RocketMQ 实现(重点)

RocketMQ 可靠性极高,针对三个环节分别设计保障机制:

环节一:生产者(避免发送丢失)
// ✅ 同步发送(默认,推荐)—— 等待 Broker 返回 SEND_OK
SendResult result = producer.send(msg);
​
// ❌ 单向发送(不推荐)—— 无返回确认,消息可能丢失
producer.sendOneway(msg);
​
// ✅ 异步发送 + 回调(高吞吐推荐)
producer.send(msg, new SendCallback() {
    @Override
    public void onSuccess(SendResult result) { /* 发送成功 */ }
    @Override
    public void onException(Throwable e) { /* 发送失败,重试或记录 */ }
});
  • 同步发送(Sync)模式(默认),Producer 发送消息后,必须等待 Broker 返回"发送成功"(SEND_OK)才算成功,失败会自动重试(默认 3 次);

  • 禁止使用单向发送(无返回确认),避免消息发送失败无法感知。

环节二:Broker(避免存储丢失)
# broker.conf 关键配置
flushDiskType=SYNC_FLUSH          # 同步刷盘(默认 ASYNC_FLUSH)
brokerRole=SYNC_MASTER            # 同步复制(默认 ASYNC_MASTER)
  • 配置同步刷盘flushDiskType=SYNC_FLUSH),消息写入 CommitLog 磁盘文件后才返回成功,避免内存中消息因宕机丢失;

  • 配置主从同步复制brokerRole=SYNC_MASTER),Master 收到消息后同步到 Slave,确保主节点宕机后数据不丢失。

环节三:消费者(避免消费丢失)
// ✅ 手动 ACK —— 处理完业务再确认
consumer.registerMessageListener((List<MessageExt> msgs, ConsumeConcurrentlyContext context) -> {
    try {
        // 执行业务逻辑
        processBusiness(msgs);
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; // 确认消费成功
    } catch (Exception e) {
        return ConsumeConcurrentlyStatus.RECONSUME_LATER; // 消费失败,稍后重试
    }
});
  • 关闭自动 ACK,使用手动 ACK 确认机制;

  • 只有业务逻辑处理完毕,才返回 CONSUME_SUCCESS,否则 Broker 会自动重试,避免未处理完消息就确认导致丢失。

3.3 三环节对比表

环节 保障机制 配置/代码 性能影响
生产者 同步发送 + 重试(默认3次) producer.send(msg) 中等(等待响应)
Broker 存储 同步刷盘 flushDiskType=SYNC_FLUSH 高(写磁盘后才返回)
Broker 复制 同步主从复制 brokerRole=SYNC_MASTER 中等(等 Slave 确认)
消费者 手动 ACK RECONSUME_LATER

性能与可靠性的权衡

  • 最高可靠:同步发送 + 同步刷盘 + 同步复制 → 金融级,吞吐量最低

  • 推荐配置:同步发送 + 异步刷盘 + 同步复制 → 平衡方案

  • 最高性能:异步发送 + 异步刷盘 + 异步复制 → 日志级,可能丢少量数据

3.4 追问

Q:同步刷盘和异步刷盘的区别是什么?

A:同步刷盘是消息写入 PageCache 后,立即调用 fsync 强制刷到磁盘物理介质,然后才返回成功,可靠性最高但性能差。异步刷盘是消息写入 PageCache 后就返回成功,由后台线程定时(默认 500ms)批量刷盘,性能高但宕机可能丢失最后一批数据。RocketMQ 还支持异步刷盘+主从同步复制的折中方案,即使主节点宕机,Slave 上还有数据。

Q:RocketMQ 的 CommitLog 是怎么存储的?

A:所有 Topic 的消息都顺序写入同一个 CommitLog 文件(单个 1GB),通过内存映射(mmap)实现高效写入。消费时通过 ConsumeQueue(逻辑队列,类似 Kafka 的 Partition)索引到 CommitLog 的物理偏移量,再读取消息体。这种"集中存储 + 索引分离"的设计,写入是纯粹的顺序 I/O,性能极高。

Q:如果 Broker 开了同步刷盘,性能会下降多少?

A:根据阿里官方压测,同步刷盘相比异步刷盘,TPS 大约下降 50%~70%。例如异步刷盘可达 10 万+ TPS,同步刷盘大约 3~5 万 TPS。金融场景通常采用"异步刷盘 + 同步复制"的折中方案,既保证数据不丢(Slave 有副本),又不至于性能太差。

3.5 业务落地

金融支付转账通知场景(核心要求消息零丢失):资金链路容不得半点丢失,需牺牲部分吞吐量,在 Broker 端强制开启同步刷盘和同步主从复制;消费端(对账服务)处理完对账逻辑(比对支付金额、订单信息)后,再发送手动 ACK;若处理异常,返回消费失败,Broker 触发重试,同时将失败消息打入死信队列(DLQ),后续人工干预排查。


4. 消息队列如何保证消息的顺序性

4.1 底层原理

顺序性分为全局顺序局部(分区)顺序

类型 说明 实现方式 吞吐量 适用场景
全局顺序 所有消息严格 FIFO 单 Queue/Partition 极低 几乎不用
局部顺序 同一业务实体的消息有序 Hash 路由到同一 Queue 主流方案

核心解决思路:将需要保证顺序的消息,路由到同一个队列/分区,且保证该队列/分区的消息"串行发送、串行消费",避免并发导致顺序错乱。

4.2 RocketMQ 实现(重点)

生产端保证
// 使用队列选择器,将同一订单的消息路由到同一个 Queue
producer.send(msg, new MessageQueueSelector() {
    @Override
    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
        Long orderId = (Long) arg;
        int index = (int) (orderId % mqs.size()); // Hash 取模
        return mqs.get(index);
    }
}, orderId); // 传入业务唯一标识
  • 使用队列选择器(MessageQueueSelector),对业务唯一标识(如订单 ID)进行 Hash 取模,相同标识的消息会被路由到同一个 Topic 下的同一个 Queue;

  • 同时采用串行发送,避免同一标识消息并发发送到不同队列。

Broker 端保证
  • 同一 MessageQueue 内的消息,严格按照发送顺序写入 CommitLog;

  • 转发到 ConsumerQueue 时保持顺序不变;

  • 禁止对顺序消息队列进行负载均衡调整,避免顺序错乱。

消费端保证
// 使用顺序消费监听器(而非并发消费监听器)
consumer.registerMessageListener(new MessageListenerOrderly() {
    @Override
    public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
        // 单线程串行消费同一 Queue 的消息
        processOrder(msgs);
        return ConsumeOrderlyStatus.SUCCESS;
    }
});
  • 使用 MessageListenerOrderly 接口(而非 MessageListenerConcurrently);

  • Broker 会给对应的 Queue 加分布式锁,保证同一时间只有一个消费者线程拉取和处理该 Queue 的消息,实现单线程串行消费;

  • 消费失败时,暂停该队列消费,重试成功后再处理下一条,避免顺序错乱。

4.3 顺序消息完整流程图

Producer                          Broker                         Consumer
   │                                │                               │
   │──orderId=1001──→ Queue0 ──→ CommitLog ──→ ConsumerQueue0 ──→ 线程A(串行)
   │──orderId=1001──→ Queue0 ──→     │          │                    │
   │──orderId=1002──→ Queue1 ──→ CommitLog ──→ ConsumerQueue1 ──→ 线程B(串行)
   │                                │                               │
   │  Hash(orderId) % Queue数       │  严格写入顺序                  │  分布式锁保证
   │  相同orderId → 同一Queue       │  不乱序                        │  单线程消费

4.4 追问

Q:全局顺序怎么实现?有必要吗?

A:全局顺序只需将 Topic 的 Queue 数设为 1,所有消息都进同一个 Queue,天然全局有序。但这会将吞吐量降到单线程水平,实际业务中极少使用。真需要全局顺序的场景(如 Binlog 同步),通常用局部顺序就够了——按表名+主键 Hash 路由即可。

Q:顺序消费时,某个消息消费失败怎么办?会不会阻塞整个队列?

A:会的。MessageListenerOrderly 模式下,消费失败会暂停当前 Queue 的消费,进入重试。可以通过 setMaxReconsumeTimes 设置最大重试次数,超过后进入死信队列。这就是顺序消费的代价——一个消息卡住,整个队列都卡住。所以顺序消费的业务逻辑要尽量简单、快速,避免长事务。

Q:RocketMQ 的顺序消息和 Kafka 的顺序消息有什么区别?

A:Kafka 的 Partition 内天然有序(单 Partition 单消费者线程),但没有内置的顺序消费接口,需要业务自己保证单线程消费。RocketMQ 提供了 MessageListenerOrderly 接口,内置了分布式锁和单线程消费机制,对开发者更友好。

4.5 业务落地

MySQL Binlog 同步系统(Canal + RocketMQ):一条数据库记录的 Insert、Update、Delete 操作绝对不能乱序,否则同步到下游数据仓库的数据会出错。解决方案:用"表名+主键 ID"进行 Hash 取模,将同一记录的所有操作消息路由到同一个 Queue,严格顺序消费,确保数据同步准确。


5. 消息队列有可能发生重复消费,如何避免,如何做到幂等

5.1 底层原理

由于网络的不可靠性(网络抖动、闪断),主流 MQ(包括 Kafka 和 RocketMQ)默认提供的都是 At Least Once(至少一次) 交付语义。

重复消费的根本原因

消费者处理完消息 → 准备提交 ACK → 网络闪断 → Broker 未收到 ACK
    → Broker 超时后重新投递 → 消费者再次收到同一条消息 → 重复消费

核心结论:重复消费无法彻底避免,我们要做的不是阻止重复投递,而是保证消费逻辑的幂等性——即多次消费同一消息,业务结果一致,不产生副作用。

三种消息语义对比

语义 说明 实现难度 消息丢失 重复消费
At Most Once 最多一次 简单 可能丢 不重复
At Least Once 至少一次 中等 不丢 可能重复
Exactly Once 恰好一次 极难 不丢 不重复

RocketMQ 和 Kafka 都是 At Least Once。Kafka 的 "Exactly Once" 仅限于 Kafka 内部(幂等 Producer + 事务),跨系统仍需业务幂等。

5.2 RocketMQ 实现(避免 + 幂等)

避免重复消费(辅助手段)
  • 生产者减少重试次数,异步发送时做好回调确认,避免重复触发发送;

  • 消费者及时发送 ACK,优化消费逻辑,减少处理耗时,降低宕机概率;

  • Broker 优化投递机制,重试间隔递增,减少短时间内重复投递。

实现幂等(核心方案)

RocketMQ 每条消息有全局唯一的 msgId,生产者也可通过 msg.setKeys() 设置业务唯一标识(如订单 ID),消费者利用该标识实现幂等:

方案 1:唯一标识 + Redis 缓存记录(高并发常用)

String key = "mq:deduplicate:" + msg.getKeys(); // 业务唯一标识
Boolean isNew = redisTemplate.opsForValue().setIfAbsent(key, "1", 24, TimeUnit.HOURS);
if (Boolean.TRUE.equals(isNew)) {
    // 首次消费,执行业务逻辑
    processBusiness(msg);
} else {
    // 重复消息,直接跳过
    log.warn("重复消息: {}", msg.getKeys());
}

方案 2:数据库唯一约束(最可靠)

-- 将业务唯一标识作为唯一索引
CREATE UNIQUE INDEX uk_pay_no ON t_order(pay_no);
​
-- 重复消费时插入会触发 DuplicateKeyException
-- 捕获异常视为消费成功

方案 3:业务逻辑幂等(最灵活)

-- 更新操作增加条件判断
UPDATE orders SET status = '已支付' 
WHERE order_id = 'xxx' AND status = '未支付';
-- 第二次执行时 status 已变,影响行数为 0,自然幂等

5.3 三种方案对比

方案 适用场景 优点 缺点
Redis 去重 高并发、允许短暂不一致 性能极高、实现简单 Redis 宕机可能失效
DB 唯一约束 金融级、强一致要求 绝对可靠 性能略低
业务幂等 更新类操作 无额外依赖 需要业务配合设计

5.4 追问

Q:Redis 去重的 Key 过期时间怎么设?

A:过期时间应大于消息的最大重试周期。RocketMQ 默认重试 16 次,最长时间约 4.5 小时,所以 Redis Key 过期时间建议设 24 小时以上。如果业务允许,设 72 小时更安全(与 Broker 消息保留时间一致)。

Q:如果 Redis 和数据库方案结合使用,顺序是什么?

A:先 Redis 快速去重(减少 DB 压力),再 DB 兜底(保证最终一致)。流程:Redis setIfAbsent → 成功则执行业务 + 入库 → 失败则跳过。即使 Redis 丢失数据,DB 唯一索引也能兜底防重。

Q:RocketMQ 有没有内置的 Exactly Once 语义?

A:没有。RocketMQ 只保证 At Least Once。所谓的"Exactly Once"在分布式系统中几乎不可能完美实现,业界通用做法是 At Least Once + 业务幂等 来近似实现 Exactly Once 的效果。

5.5 业务落地

电商支付回调处理场景:第三方支付平台回调订单服务时,因网络抖动可能多次发送"支付成功"消息,若不做幂等,会导致订单重复更新状态、重复增加积分。解决方案:生产者(支付平台)将支付流水号作为 Keys;消费者(订单服务)消费前,用 Redis 的 setIfAbsent 判断该流水号是否已消费,未消费则执行更新逻辑并记录状态,同时在订单表的 pay_no 字段设置唯一索引,兜底重复消费。


6. 什么是幂等?如何解决幂等性问题

6.1 底层原理

幂等(Idempotent) 是数学概念,在编程中指:同样的请求发起一次和发起 N 次,系统的状态是一致的,不会产生副作用

HTTP 方法幂等性

方法 幂等 说明
GET 查询操作,天然幂等
PUT 全量更新,天然幂等
DELETE 删除操作,天然幂等
POST 新增操作,需手动实现幂等
PATCH 部分更新,需手动实现幂等

6.2 幂等三大黄金方案

方案 1:数据库唯一约束(强一致最可靠)
try {
    orderMapper.insert(order); // order 中包含唯一业务标识
} catch (DuplicateKeyException e) {
    // 捕获唯一约束异常,视为已消费
    log.info("重复消费,忽略: {}", order.getPayNo());
}
方案 2:Redis 分布式锁(高并发最常用)
String lockKey = "lock:order:" + orderId;
Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.MINUTES);
if (Boolean.TRUE.equals(locked)) {
    try {
        processOrder(order);
    } finally {
        // 业务完成后可选择删除锁或等过期
    }
} else {
    log.info("正在处理或已处理,跳过: {}", orderId);
}
方案 3:状态机乐观锁(更新类最轻量)
-- 前置状态条件,只有状态匹配才更新
UPDATE orders SET status = 'PAID', update_time = NOW() 
WHERE order_id = 'xxx' AND status = 'UNPAID';
-- 第二次执行:status 已是 'PAID',WHERE 条件不匹配,影响行数 = 0

6.3 注意事项

  • 唯一标识需全局唯一(如 UUID、雪花 ID、业务流水号);

  • Redis 缓存需设置过期时间,避免缓存膨胀;

  • 分布式场景下,需保证"校验-执行-记录"的原子性(如用 Lua 脚本或数据库事务);

  • 幂等方案要分层设计:Redis 快速去重 → DB 唯一约束兜底 → 业务条件判断兜底。

6.4 追问

Q:幂等和去重有什么区别?

A:去重是手段,幂等是目标。去重是指"识别并跳过重复消息",幂等是指"即使重复执行,结果也一样"。去重不一定能完全覆盖所有情况(如 Redis 宕机),所以还需要业务层面的幂等设计来兜底。

Q:分布式事务场景下,如何保证幂等?

A:RocketMQ 事务消息的回查机制天然支持幂等——如果本地事务已经执行成功(状态已变更),回查时返回 COMMIT_MESSAGE,消息不会被重复投递。消费端仍需做幂等处理,因为消费者可能收到多次投递。

6.5 业务落地

用户积分发放场景(重复消费会导致资损):采用"唯一标识 + 缓存记录"方案,生产者(订单服务)发送消息时,将"用户 ID + 订单 ID"作为 Keys;消费者(积分服务)消费前,构建 Redis Key=mq:point:{userId}:{orderId},用 setIfAbsent 判断是否已消费;未消费则执行积分增加逻辑,记录消费状态(过期时间 24 小时);同时在积分明细表中,将"用户 ID + 订单 ID"设为唯一索引,兜底重复消费。


7. 如何处理消息队列消息积压问题

7.1 底层原理

消息积压的核心矛盾是:生产者的发送速度远大于消费者的处理速度

积压根因排查清单

原因 表现 排查方法
消费端 Bug 消费进度停滞 检查消费端日志、死信队列
下游依赖超时 消费耗时突增 监控 DB/Redis/API 响应时间
流量突增 消息量暴增 对比生产/消费 TPS
消费者宕机 消费者实例数减少 检查容器/K8s 状态
Queue 数不足 扩容消费者无效 检查 Topic Queue 数量

7.2 处理方案(分两步)

第一步:紧急处理(治标)

10 倍扩容法(积压千万级时使用):

1. 临时新建 Topic,将 Queue 数量扩大 10~20 倍
2. 修改原消费者代码 → 改为"转发机器",只拉取积压消息,直接投递到新 Topic
3. 临时征用机器,部署 10 倍数量的消费者,消费新 Topic 消息
4. 快速清空积压后,恢复原架构

常规紧急操作

  • 扩容消费者实例(但不超过 Queue 数,超出的消费者分配不到 Queue);

  • 调优消费参数:提高批量消费条数(consumeMessageBatchMaxSize)、缩短拉取间隔;

  • 跳过非核心消息,优先消费核心消息;

  • 如果消息已过期(如促销已结束),可直接清空积压。

第二步:根本解决(治本)
  • 优化消费逻辑:排查并修复消费端 Bug,减少长事务、慢 SQL、慢 API 调用,将耗时操作异步化;

  • 提升消费能力:将串行处理改为线程池异步处理,拆分消费链路,增加 Topic Queue 数量,提升并行度;

  • 控制生产速度:高峰期对非核心业务降级,限制生产端发送速率,避免增量积压;

  • 完善监控:监控积压量(消费延迟)、消费成功率,超过阈值触发预警,结合自动扩缩容机制。

7.3 关键公式

消费能力 = 消费者实例数 × 每实例消费线程数 × 单线程 TPS
积压清空时间 = 积压消息数 / (消费能力 - 生产速率)

注意:消费者实例数上限 = Topic 的 Read Queue 数量。如果消费者数超过 Queue 数,多余的消费者会闲置。所以扩容消费者前,先确认 Queue 数是否足够。

7.4 追问

Q:Queue 数量设多少合适?

A:Queue 数量决定了消费者的并行度上限。一般建议 Queue 数 = 消费者实例数 × 2~3,预留扩容空间。但 Queue 也不是越多越好——过多的 Queue 会增加 Broker 的文件句柄和内存开销,通常单 Topic 不超过 100 个 Queue。

Q:消费端用线程池并行消费,会不会影响顺序性?

A:会。如果使用了顺序消费(MessageListenerOrderly),不能用线程池并行消费,否则会破坏顺序。并发消费(MessageListenerConcurrently)可以用线程池,但要确保业务不需要顺序保证。

Q:积压的消息已经过期了怎么办?

A:如果业务上已无意义(如促销已结束),可以直接跳过或清空。可以在消费逻辑中判断消息时间戳,超过有效期直接返回 CONSUME_SUCCESS(确认消费但不处理)。不要让过期消息继续占用消费资源。

7.5 业务落地

某电商大促期间,数据库连接池爆满,导致订单生成消息大量积压,处理步骤如下:

  1. 紧急处理:启动降级预案,用中转程序将几十万条积压消息快速搬运到具有 100 个 Queue 的新 Topic,部署一批临时容器并行消费落库,快速清空积压;

  2. 根本解决:排查代码,给下游数据库增加 Redis 缓存层,减少数据库查询压力;优化消费逻辑,将单条插入改为批量插入,提升消费速度;配置监控,积压量超过 10000 条触发短信预警,避免再次发生积压。


8. 消息队列技术选型:Kafka 还是 RocketMQ,还是 RabbitMQ

8.1 三大 MQ 全方位对比

维度 RabbitMQ RocketMQ Kafka
开发语言 Erlang Java Scala/Java
单机吞吐量 万级 QPS 十万级 QPS 百万级 QPS
消息延迟 微秒级 毫秒级 毫秒级(批量优化后)
消息可靠性 高(需手动持久化) 极高(同步刷盘+复制) 高(ISR 机制)
顺序消息 不保证 原生支持 Partition 内有序
事务消息 不支持 原生支持 支持(0.11+)
延迟消息 插件支持 原生支持 18 级 不支持
死信队列 支持 原生支持 不支持
消息回溯 不支持 支持(按时间/offset) 支持(按 offset)
消息轨迹 不支持 原生支持 不支持
大数据生态 极强(Flink/Spark)
运维复杂度 中等
社区活跃度 高(Apache 顶级) 极高

8.2 选型决策树

你的业务场景是什么?
├── 大数据日志/流计算 → Kafka(极致吞吐 + 生态完善)
├── 电商/金融核心交易 → RocketMQ(事务消息 + 延迟消息 + 高可靠)
├── 中小系统/简单业务 → RabbitMQ(开箱即用 + 低延迟)
└── 物联网/IoT → RocketMQ(支持 MQTT 协议扩展)

8.3 追问

Q:为什么 Kafka 吞吐量比 RocketMQ 高?

A:核心原因有三:① Kafka 使用顺序写磁盘 + 零拷贝(sendfile),单 Partition 写入是纯粹的顺序 I/O;② Kafka 采用批量发送(batch.size),多条消息打包一次网络请求;③ Kafka 的 Partition 是物理分区,每个 Partition 是独立的日志文件,并行度更高。RocketMQ 的 CommitLog 是所有 Topic 共享的单个文件,写入时需要加锁,吞吐量相对低一些。

Q:RabbitMQ 为什么不适合高并发场景?

A:RabbitMQ 基于 Erlang 的 Actor 模型,每条消息都要经过 Exchange 路由、Queue 存储、消息确认等复杂流程,且 Erlang 的 GC 机制在高并发下容易产生长停顿。另外,RabbitMQ 的 Queue 是内存+磁盘混合存储,消息堆积时性能下降明显。

Q:如果团队技术栈是 Go,选哪个 MQ?

A:RocketMQ 有 Go 客户端(rocketmq-client-go),Kafka 有 saramaconfluent-kafka-go。Go 团队做大数据选 Kafka,做业务选 RocketMQ。RabbitMQ 也有 Go 客户端(amqp-go-client),但 Erlang 运维成本高,不推荐。

8.4 业务落地

  • 日志采集系统:选 Kafka,Flink 实时消费日志流,写入 Elasticsearch,吞吐量百万级无压力;

  • 电商订单系统:选 RocketMQ,下单后异步通知库存、物流、积分服务,事务消息保证支付与订单状态一致;

  • 内部管理系统:选 RabbitMQ,团队小、业务简单,开箱即用,维护成本低。


9. 消息中间件如何做到高可用

9.1 底层原理

分布式系统高可用的核心逻辑是:多副本冗余(Replication) + 故障自动转移(Failover),通过集群部署,避免单点故障,确保服务持续可用。

9.2 RocketMQ 高可用架构

NameServer 无状态集群
Producer ──→ NameServer-1
         ──→ NameServer-2    ← 任意节点宕机不影响服务
         ──→ NameServer-3
  • 多个 NameServer 节点相互独立,不通信,任意一台宕机都不影响路由发现;

  • 客户端可切换到其他节点获取路由信息,天然高可用。

Broker 多主多从集群
Broker-A (Master) ──同步复制──→ Broker-A (Slave)
Broker-B (Master) ──同步复制──→ Broker-B (Slave)
​
Producer ──→ Master-A / Master-B(自动切换)
Consumer ──→ Master-A / Slave-A / Master-B / Slave-B
  • 数据分片存储在多个 Master 节点,一个 Master 宕机,其他 Master 的数据不受影响;

  • 生产者可自动切换到正常 Master 发送消息。

DLedger/Raft 自动主从切换(RocketMQ 4.5+)
Broker 组:
  ├── Candidate (竞选 Master)
  ├── Follower (同步数据)
  └── Follower (同步数据)
​
Master 宕机 → Raft 选举 → Follower-A 成为新 Master → 自动切换
  • 基于 Raft 一致性算法,当 Master 宕机时,集群会在秒级自动选举;

  • 将数据最新的 Slave 提升为新 Master,实现无人工干预故障转移。

9.3 生产环境部署模式

模式 说明 可靠性 适用场景
单 Master 单节点部署 低(宕机即丢) 仅开发测试
多 Master 多主无从 中(主宕机丢数据) 非核心业务
多 Master 多 Slave(异步) 主写从异步同步 高(可能丢少量) 一般业务
多 Master 多 Slave(同步) 主写从同步确认 极高(不丢数据) 核心交易
DLedger 模式 Raft 自动选主 极高 + 自动故障转移 金融级

9.4 追问

Q:RocketMQ 的 Broker 主从模式和 Kafka 的 ISR 机制有什么区别?

A:Kafka 的 ISR(In-Sync Replicas)是动态副本集,只有 ISR 中的 Follower 才有资格被选为 Leader,Producer 可以配置 acks=all 要求所有 ISR 确认。RocketMQ 的主从是静态配置,Master 宕机后需要人工切换(4.5 之前)或 DLedger 自动选举(4.5+)。Kafka 的 ISR 机制更灵活,但 RocketMQ 的 DLedger 模式已经追上了。

Q:NameServer 无状态,那 Broker 宕机后,Producer 怎么感知?

A:Producer 每 30 秒从 NameServer 更新路由表。如果 Broker 宕机,NameServer 在 120 秒后剔除该 Broker(心跳超时),Producer 下次更新路由时会发现 Broker 不可用,自动切换到其他 Broker。但在 120 秒窗口期内,发送到宕机 Broker 的消息会失败,触发重试机制(默认 3 次),重试时会选择其他 Broker。

9.5 业务落地

线上生产环境绝对不能单节点部署,标准架构至少是"双主双从(2m-2s)":核心交易系统强制开启主从同步复制(Sync Broker),确保消息同步到 Slave 后才返回成功;即使某台物理机突然断电、烧毁,依靠另一套主从节点,业务依然能正常运行,数据不丢失、服务不中断。


10. 如何保证数据一致性,事务消息如何实现

10.1 底层原理

微服务架构下,本地数据库事务无法覆盖 MQ 消息发送,容易产生分布式事务问题:

场景1:本地库操作成功(如扣钱),但 MQ 消息发送失败 → 下游收不到通知
场景2:MQ 消息发送成功,但本地库操作异常回滚 → 下游收到错误通知

核心需求是保证"本地操作"和"发消息"的最终一致性

10.2 RocketMQ 事务消息实现(面试重点)

RocketMQ 原生支持事务消息(两阶段提交变种),通过"半消息 + 本地事务 + 事务回查"的机制,解决分布式一致性问题:

Producer                           Broker                           Consumer
   │                                 │                                │
   │──1. 发送半消息(Half)──→         │                                │
   │                        对消费者不可见                              │
   │←──返回发送结果──                 │                                │
   │                                 │                                │
   │──2. 执行本地事务──→  MySQL       │                                │
   │   (如:扣减余额)                 │                                │
   │                                 │                                │
   │──3. 提交/回滚──→                │                                │
   │   Commit → 消息可见              │──→ 投递给 Consumer ──→         │
   │   Rollback → 删除消息            │                                │
   │                                 │                                │
   │   [如果3超时/丢失]               │                                │
   │←──4. 事务回查──                  │                                │
   │   Broker 主动询问本地事务状态     │                                │
   │──返回 Commit/Rollback──→        │                                │

详细步骤

  1. 发送半消息(Half Message):生产者向 MQ 发送一条"半消息",消息到达 Broker 后,对消费者不可见,仅记录消息状态;

  2. 执行本地事务:生产者收到半消息发送成功的 ACK 后,执行本地业务逻辑(如写入 MySQL);

  3. 二次确认(Commit/Rollback)

    • 本地事务执行成功 → 向 MQ 发送 Commit → 消息改为对外可见,消费者可拉取消费

    • 本地事务执行失败 → 向 MQ 发送 Rollback → MQ 直接删除半消息

  4. 反查机制(兜底):若步骤 3 发生网络中断,MQ 迟迟未收到二次确认,会主动发起回查(默认 15 次,首次间隔 6s,后续 60s),调用生产者的 TransactionListener 接口,查询本地数据库事务状态,再根据回查结果执行 Commit 或 Rollback。

10.3 代码实现

// 1. 定义事务监听器
public class OrderTransactionListener implements TransactionListener {
    
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        try {
            // 执行本地事务(如更新订单状态)
            orderService.updateOrderStatus(msg.getKeys(), "PAID");
            return LocalTransactionState.COMMIT_MESSAGE; // 提交
        } catch (Exception e) {
            return LocalTransactionState.ROLLBACK_MESSAGE; // 回滚
        }
    }
​
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        // 回查本地事务状态
        Order order = orderService.queryByOrderId(msg.getKeys());
        if (order != null && "PAID".equals(order.getStatus())) {
            return LocalTransactionState.COMMIT_MESSAGE; // 已完成,提交
        }
        return LocalTransactionState.UNKNOW; // 未完成,稍后再查
    }
}
​
// 2. 发送事务消息
TransactionMQProducer producer = new TransactionMQProducer("tx_producer_group");
producer.setTransactionListener(new OrderTransactionListener());
producer.start();
​
Message msg = new Message("order_topic", "OrderID001".getBytes());
producer.sendMessageInTransaction(msg, null);

10.4 追问

Q:半消息存储在哪里?和普通消息一样吗?

A:半消息存储在 Broker 的内部 Topic RMQ_SYS_TRANS_HALF_TOPIC 中,与普通消息分开存储。当收到 Commit 后,Broker 会将半消息从内部 Topic 转移到目标 Topic(如 order_topic),消费者才能看到。如果收到 Rollback,则直接标记删除。

Q:事务回查的次数和间隔是多少?能自定义吗?

A:默认回查 15 次,首次间隔 6 秒,后续每次间隔 60 秒。可以通过 transactionCheckMaxtransactionCheckInterval 配置。超过最大回查次数仍为 UNKNOW 状态,消息会被标记为回滚(删除)。

Q:RocketMQ 的事务消息和 Kafka 的事务有什么区别?

A:Kafka 的事务主要用于保证"跨 Partition 写入的原子性"(如同时写 Topic-A 和 Topic-B),本质是 Kafka 内部事务。RocketMQ 的事务消息解决的是"本地事务与 MQ 消息的一致性"(如数据库操作与发消息的原子性),本质是分布式事务方案。两者解决的问题不同,不能互相替代。

Q:如果回查时本地事务状态是 UNKNOW,会怎样?

A:Broker 会继续回查,直到超过最大回查次数(默认 15 次)。如果始终是 UNKNOW,消息最终会被回滚(删除)。所以本地事务的设计原则是:尽量能在短时间内确定状态,避免长时间处于中间状态。如果业务确实需要长时间处理(如 T+1 对账),建议将事务状态持久化到数据库,回查时读库判断。

10.5 业务落地

订单支付后发货通知场景:用户支付成功后,订单服务需要完成"更新订单状态(本地库操作)"和"向发货系统发送通知(MQ 消息)",两者必须保证一致。使用 RocketMQ 事务消息,在本地事务方法中执行 update order_status='PAID',通过事务回查监听器读取数据库该订单状态,确保本地操作和消息发送一致。这套机制在阿里交易链路中,经历了多次双 11 的高并发考验。


11. 让你写一个消息队列,该如何进行架构设计

11.1 底层原理

这是考察架构视野的系统设计题,设计任何中间件,都需从"网络通信、存储模型、高可用容灾、路由元数据管理"四个核心维度出发,兼顾性能、可靠性和可扩展性。

11.2 架构设计(参考 RocketMQ)

┌─────────────────────────────────────────────────────────────┐
│                      整体架构                                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐              │
│  │Producer-1│    │Producer-2│    │Producer-N│  生产者集群   │
│  └────┬─────┘    └────┬─────┘    └────┬─────┘              │
│       │               │               │                    │
│       └───────────────┼───────────────┘                    │
│                       │                                     │
│  ┌────────────────────▼────────────────────┐               │
│  │          NameServer (无状态集群)          │  路由注册中心  │
│  │   保存 Broker 列表 + Topic → Queue 路由   │               │
│  └────────────────────┬────────────────────┘               │
│                       │                                     │
│  ┌────────────────────▼────────────────────┐               │
│  │           Broker 集群                    │  消息存储核心  │
│  │  ┌─────────────┐  ┌─────────────┐       │               │
│  │  │  Master-A   │  │  Master-B   │       │               │
│  │  │ (CommitLog  │  │ (CommitLog  │       │               │
│  │  │ +CQueue+Idx)│  │ +CQueue+Idx)│       │               │
│  │  └──────┬──────┘  └──────┬──────┘       │               │
│  │         │                │              │               │
│  │  ┌──────▼──────┐  ┌──────▼──────┐       │               │
│  │  │  Slave-A    │  │  Slave-B    │       │               │
│  │  └─────────────┘  └─────────────┘       │               │
│  └────────────────────┬────────────────────┘               │
│                       │                                     │
│  ┌────────────────────▼────────────────────┐               │
│  │         Consumer 集群                    │  消费者集群    │
│  │  Pull 模式 + 长轮询 + 负载均衡           │               │
│  └─────────────────────────────────────────┘               │
│                                                             │
└─────────────────────────────────────────────────────────────┘

11.3 各模块详细设计

1. 元数据管理(类似 NameServer)
  • 设计轻量级注册中心,保存 Broker 节点列表和 Topic 路由信息;

  • 采用无状态集群部署,各节点独立,不通信,保证高可用;

  • Broker 定时发送心跳,客户端定时拉取路由。

2. 网络通信层
  • 使用 Netty 框架构建高性能 NIO 异步通信网络;

  • 自定义报文协议(Header + Body),支持同步、异步发送;

  • 连接池复用,减少 TCP 握手开销。

3. 存储引擎(核心重点)
写入流程:
Producer → 追加写入 CommitLog(顺序 I/O,极快)
         → 同时写入 ConsumeQueue(索引,异步)
         → 同时写入 IndexFile(Key 索引,异步)
​
读取流程:
Consumer → 查 ConsumeQueue 获取 CommitLog 偏移量
         → 用 mmap/Zero-Copy 读取 CommitLog 消息体
  • 不使用关系型数据库,采用磁盘顺序写模式,将所有消息追加到一个大文件(类似 CommitLog);

  • 使用 mmap(内存映射) 实现零拷贝,减少 CPU 上下文切换和内存拷贝;

  • 消息清理:定时删除过期文件(默认 72 小时),磁盘使用率超阈值触发清理。

4. 高可用集群
  • 支持数据分片(Partition),实现横向扩容;

  • 引入基于 Raft 协议的主从复制机制,保证数据强一致;

  • 主节点宕机后自动选举新主,实现故障自动转移。

11.4 追问

Q:为什么不用 Kafka 那样的物理分区,而用 CommitLog 集中存储?

A:两者各有利弊。Kafka 的物理分区(每个 Partition 独立文件)写入并行度高,吞吐量更大,但文件数量多,运维复杂。RocketMQ 的 CommitLog 集中存储写入时需要加锁(内存锁),吞吐量略低,但文件管理简单,且支持消息回溯(因为所有消息在一个文件里)。对于业务消息场景(消息量没日志那么大),CommitLog 方案更合适。

Q:mmap 和 sendfile 的零拷贝有什么区别?

A:mmap 是将磁盘文件映射到用户空间虚拟内存,用户可以直接读写,省去了内核态到用户态的数据拷贝。sendfile 是直接在内核态完成文件到 Socket 的数据传输,完全不经过用户空间。RocketMQ 读消息用 mmap,Kafka 读消息用 sendfile。mmap 适合需要修改数据的场景,sendfile 适合纯转发场景。

11.5 业务落地

设计一个内部消息队列系统时,应优先考虑:① 存储引擎选用顺序写文件 + mmap,不引入外部数据库依赖;② 元数据管理用无状态节点,降低运维成本;③ 高可用基于 Raft 协议实现自动主从切换,减少人工干预。


12. 什么叫做阻塞队列的有界和无界

12.1 底层原理

在 Java 并发包(JUC)和 MQ 消费者内部缓冲设计中,常常用到阻塞队列(BlockingQueue),核心用于缓冲消息、协调生产者和消费者的速度,分为有界和无界两种:

类型 代表实现 容量 队列满时行为
有界队列 ArrayBlockingQueue 需指定 阻塞/抛异常/拒绝策略
无界队列 LinkedBlockingQueue() ~21亿 永远不会满
同步队列 SynchronousQueue 0 必须有消费者等待

12.2 关键避坑

在 MQ 消费端实现或任何高并发缓冲池设计中,绝对禁止使用无界队列!

生产者持续发送 → 无界队列不断堆积 → JVM 堆内存耗尽 → OOM 崩溃
  • 一旦消费者因数据库卡顿、处理缓慢,而生产端仍在持续发送消息,无界队列会不断堆积消息,导致 JVM 内存耗尽,引发 OOM(Out Of Memory)崩溃;

  • 必须使用有界队列,并配置合理的降级和拒绝策略(如 CallerRunsPolicy),避免内存溢出。

12.3 Java 线程池拒绝策略

策略 行为 适用场景
AbortPolicy(默认) 抛出 RejectedExecutionException 需要感知积压
CallerRunsPolicy 由提交任务的线程执行 不丢消息,降速
DiscardPolicy 静默丢弃 可丢消息的场景
DiscardOldestPolicy 丢弃队列最旧的任务 只关心最新数据

12.4 追问

Q:RocketMQ 消费者的内部缓冲队列是有界还是无界?

A:RocketMQ 的 DefaultMQPushConsumer 内部使用的是有界队列ProcessQueue 中的 TreeMap),通过 pullThresholdForQueue(默认 1000 条)控制单个 Queue 的缓存上限,超过阈值会暂停拉取,避免内存溢出。

Q:Kafka 消费者的缓冲队列呢?

A:Kafka 消费者通过 fetch.max.bytesmax.partition.fetch.bytes 控制单次拉取的数据量上限,本质也是有界控制。消费者维护的是一个拉取缓冲区,不是传统的阻塞队列。

12.5 业务落地

MQ 消费端的线程池设计:使用 ArrayBlockingQueue(1000) 作为有界缓冲队列,配合 CallerRunsPolicy 拒绝策略——当队列满时,由调用线程(拉取线程)直接执行消费任务,自然降低拉取速度,形成背压机制,保护系统不被压垮。


额外重点:RocketMQ 为什么要放弃 ZooKeeper

20.1 底层原理

CAP 理论指出,一致性(C)、可用性(A)、分区容错性(P)三者不可兼得:

模型 优先保证 牺牲 代表
CP 一致性 可用性 ZooKeeper、Etcd
AP 可用性 强一致性 NameServer、Eureka

20.2 RocketMQ 放弃 ZooKeeper 原因

早期 RocketMQ 借鉴 Kafka,采用 ZooKeeper 作为注册中心,后来阿里自研 NameServer 将其替换,核心原因有两点:

  1. CAP 理论取舍:ZooKeeper 是 CP 模型,发生网络分区或主节点选举时,会短暂无法提供服务(不可用);而 MQ 路由发现,允许客户端拿到几秒前的旧路由(大不了发送失败重试),但绝不允许注册中心宕机拒绝服务,因此更需要高可用的 AP 模型,NameServer 恰好满足这一需求。

  2. 架构极简:ZooKeeper 过于沉重,需维护独立集群,运维成本高;NameServer 设计极其轻量,各节点相互独立、无状态,部署和维护简单,同时保证了极高的可用性,符合 MQ 路由服务的核心需求。

20.3 追问

Q:Kafka 也在去 ZooKeeper 化(KIP-500),说明什么?

A:Kafka 从 3.0 开始支持 KRaft 模式(基于 Raft 的内置元数据管理),去掉了对 ZooKeeper 的依赖。这说明业界普遍认为 ZooKeeper 作为 MQ 的元数据管理过于"重"了。Kafka 的 KRaft 模式和 RocketMQ 的 NameServer 思路类似——都是自研轻量级方案替代外部依赖。

Q:NameServer 无状态,那 Broker 注册信息重启后会丢吗?

A:会。NameServer 重启后内存中的路由信息全部丢失,但 Broker 每 30 秒会重新发送心跳注册,Producer/Consumer 每 30 秒重新拉取路由,所以最多 30 秒后路由信息就会恢复。这个窗口期内,发送/消费会短暂失败,但会自动重试,不会丢消息。


补充高频追问

Q1:RocketMQ 的消息模型是什么?

RocketMQ 使用发布/订阅模型,核心概念:

  • Topic:消息的逻辑分类(如 order_topic

  • Tag:消息的二级标签(如 order_topic:payorder_topic:refund),用于消费者过滤

  • Queue:Topic 的物理分片(类似 Kafka 的 Partition),实现并行消费

  • ConsumerGroup:消费者组,同一组内的消费者共同消费一个 Topic 的消息(集群模式)

Q2:RocketMQ 的 Pull 和 Push 模式有什么区别?

模式 说明 优点 缺点
Pull 消费者主动拉取 消费者控制速率 实时性差,需轮询
Push Broker 推送(基于 Pull 封装) 实时性好 消费者无法控制速率

RocketMQ 的 Push 模式底层其实是长轮询(Long Polling):消费者发起拉取请求,如果没有新消息,Broker 会 hold 住连接(默认 15 秒),有新消息时立即返回,模拟"推送"效果。

Q3:RocketMQ 的消息积压监控怎么做?

  • 消费延迟ConsumerOffsetMaxOffset 的差值,差值越大积压越严重

  • 消费 TPS:单位时间消费消息条数,突然下降说明消费端有问题

  • 死信队列:监控 %DLQ% Topic 中的消息数量,有消息说明消费多次失败

  • 管理控制台:RocketMQ Dashboard 可视化展示所有指标

Q4:RocketMQ 的 Tag 过滤是在 Broker 端还是 Consumer 端?

Broker 端。Consumer 订阅时将 Tag 表达式发送给 Broker,Broker 在 ConsumeQueue 中通过 Tag 的 HashCode 进行预过滤,只有匹配的消息才会被拉取到 Consumer,减少无效数据传输。

Q5:RocketMQ 支持哪些消息类型?

消息类型 说明 典型场景
普通消息 同步/异步/单向发送 通用场景
顺序消息 同一 Queue 内有序 Binlog 同步
延迟消息 指定延迟等级投递 定时任务、超时取消
事务消息 半消息 + 本地事务 分布式事务
批量消息 多条消息打包发送 高吞吐场景
过滤消息 Tag/SQL 表达式过滤 多消费者按需消费

Q6:RocketMQ 的延迟消息原理?

Producer → 发送延迟消息 → Broker 存入内部 Topic (SCHEDULE_TOPIC_XXXX)
                                    ↓
                        定时任务扫描到期消息
                                    ↓
                        投递到目标 Topic → Consumer 消费
  • 延迟消息暂存在 Broker 的内部 Topic SCHEDULE_TOPIC_XXXX 中;

  • Broker 定时任务每秒扫描一次到期消息,投递到目标 Topic;

  • 开源版支持 18 级固定延迟(1s/5s/10s/30s/1m/2m/3m/4m/5m/6m/7m/8m/9m/10m/20m/30m/1h/2h),商业版支持自定义时间。

Q7:RocketMQ 的死信队列是什么?

当消息消费失败次数超过最大重试次数(默认 16 次)后,消息不会被丢弃,而是进入死信队列(Dead Letter Queue,DLQ),Topic 名称为 %DLQ%{ConsumerGroup}。死信队列中的消息需要人工介入处理——可以手动消费、修复后重发,或直接丢弃。


总结

MQ 技术的深浅,必须抓住其底层核心逻辑:

  1. 存储模型:磁盘顺序读写(CommitLog) + 零拷贝(mmap/sendfile)

  2. 高可用模型:Raft 选举 + 多副本复制 + 无状态注册中心

  3. 分布式环境应对:ACK 确认 + 重试机制 + 半事务消息 + 业务幂等

  4. 性能优化:批量发送 + 异步化 + 长轮询 + 索引分离

面试时,按"底层原理 → RocketMQ 实现 → 对比 Kafka/RabbitMQ → 业务落地"的结构回答,既展示深度,又展示广度。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐