RocketMQ 消息队列原理与实战
是什么
RocketMQ 是阿里巴巴开源的消息中间件,Apache 顶级项目。它解决的核心问题是通过异步解耦、削峰填谷让分布式系统的各个部分不再直接依赖——上游只管发,下游只管接,互不影响。
在 Spring Cloud Alibaba 生态中的定位: Nacos 管服务发现、Sentinel 管流量防护、Seata 管数据一致性、Gateway 管统一入口,RocketMQ 管异步通信。
为什么需要
| 场景 | 没有 MQ 的后果 | RocketMQ 怎么做 |
|---|---|---|
| 服务间同步耦合 | 下单必须等发短信、积分、推送到服务全部完成才能返回 | 下单只发消息到 MQ,消费者异步处理 |
| 突发流量压垮下游 | 秒杀流量打到订单系统,瞬间打满连接池 | 消息排队缓冲,消费者按自身能力拉取消费 |
| 数据最终一致性 | 跨系统的数据同步需要复杂的分布式事务 | RocketMQ 事务消息 + 本地事务保证最终一致 |
| 系统解耦 | 生产者宕机导致消费者无法获取数据 | 消息持久化到磁盘,消费端恢复后可回溯消费 |
核心架构
四大角色:
| 角色 | 职责 | 关键点 |
|---|---|---|
| NameServer | 路由注册中心,Broker 每 30s 心跳上报 | 无状态、可集群部署,数据完全一致 |
| Broker | 消息存储引擎,接收生产者消息、持久化、拉取 | 主从架构,单台支持亿级消息堆积 |
| Producer | 消息生产者,选择 Broker 的某个 Queue 发送 | 支持同步/异步/Oneway 三种发送模式 |
| Consumer | 消息消费者,从 Broker 拉取消息消费 | 支持集群/广播两种消费模式 |
核心概念速查
| 概念 | 解释 |
|---|---|
| Topic(主题) | 一类消息的集合。Producer 指定 Topic 发,Consumer 按 Topic 收 |
| Tag(标签) | Topic 下的二级分类。消费者可订阅特定 Tag 过滤消息 |
| Message Queue | Topic 被分为多个 Queue,消息写入时分布在不同的 Queue 实现并发 |
| Consumer Group | 消费组,同一个组内的 Consumer 实例协同消费(集群模式下一条消息只被组内一个实例消费) |
| Offset | 消费偏移量,记录消费者组在某个 Queue 上的消费进度,支持回溯 |
消息发送与接收模式
三种发送方式
| 模式 | 生产者行为 | 返回时机 | 适用场景 |
|---|---|---|---|
| 同步发送 | 等待 Broker 确认后继续 | 收到 SEND_OK | 重要通知、事务消息 |
| 异步发送 | 立即返回,回调通知 | 通过 Callback 异步获知 | 高吞吐场景,对少量丢失不敏感 |
| Oneway | 发送后不等待任何响应 | 无 | 日志采集等可丢失场景 |
两种消费模式
关键特性详解
1. 事务消息(RocketMQ 的杀手锏)
事务消息解决的是"本地事务 + 发消息"的一致性——要么一起成功,要么一起回滚。在跨系统最终一致场景中替代了 Seata 这类重量级分布式事务。
关键点: Broker 在没有收到 COMMIT/ROLLBACK 时会主动反查生产者(回查接口 checkLocalTransaction),这样即使生产者在 COMMIT 前宕机,消息也不会丢。反查是事务消息可靠性的核心。
2. 延时消息
消息发送后不立即投递,到指定时间才可见。RocketMQ 有固定的 18 个延时等级:
// 设置消息延时等级(1=1s, 2=5s, 3=10s, ..., 18=2h)
msg.setDelayTimeLevel(3); // 延时 10s
延时消息先写入 SCHEDULE_TOPIC_XXXX,Broker 定时扫描到期后写入真实 Topic。不支持自定义任意延时时间,只能在预设级别中选择。
3. 消息重试与死信队列
Consumer 消费失败后,RocketMQ 自动重试(默认 16 次),重试间隔逐级递增。16 次仍失败的消息进入死信队列(DLQ),可以在 Dashboard 中查看和重新投递。
// 设置重试次数(默认 16)
consumer.setMaxReconsumeTimes(3);
// 消费时返回重试/成功
// 消费成功
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
// 消费失败,稍后重试
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
注意: 重试消息不是立即重试,会根据重试次数逐渐增加延时。第 1 次重试等待 10s,第 2 次 30s,以此类推。如果业务上不需要重试(如反序列化失败,重试多少次都没用),可以在 catch 中直接返回 SUCCESS 并记录异常到日志——不要依赖死信队列。
4. 流量削峰
RocketMQ 的核心价值之一。生产者以任意速度发消息,Broker 将消息堆积在 CommitLog 中(纯顺序写,性能极高),消费者按自身消费能力拉取。单台 Broker 可支撑数十万 TPS 写入和 TB 级消息堆积。
新人上手指南
第一步:启动 RocketMQ
# 1. 下载(4.9.x 稳定版)
curl -O https://dlcdn.apache.org/rocketmq/4.9.7/rocketmq-all-4.9.7-bin-release.zip
unzip rocketmq-all-4.9.7-bin-release.zip && cd rocketmq-4.9.7/bin
# 2. 启动 NameServer(默认日志路径 ~/logs/rocketmqlogs/namesrv.log)
nohup sh mqnamesrv > /dev/null 2>&1 &
# 3. 启动 Broker(连接 NameServer,自动创建 Topic 方便测试)
nohup sh mqbroker -n localhost:9876 autoCreateTopicEnable=true > /dev/null 2>&1 &
# 4. 验证
tail -f ~/logs/rocketmqlogs/namesrv.log
# 看到 "The Name Server boot success." 表示启动成功
# 5. 可选:启动 Dashboard 控制台
# https://github.com/apache/rocketmq-dashboard 下载后 mvn spring-boot:run
生产集群注意: NameServer 至少 2 节点无状态集群,Broker 主从部署,4.x 用 broker.conf 指定多 Broker,5.x 支持 Controller 模式自动选主。
第二步:添加依赖
<!-- RocketMQ 客户端(纯 Java API) -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.9.7</version>
</dependency>
<!-- Spring Cloud Stream RocketMQ(可选,Spring 集成) -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rocketmq</artifactId>
</dependency>
第三步:生产者代码
@Component
public class OrderEventPublisher {
private DefaultMQProducer producer;
@PostConstruct
public void init() throws MQClientException {
producer = new DefaultMQProducer("order-producer-group");
producer.setNamesrvAddr("localhost:9876");
// 同步发送默认重试 2 次,设置 3 次增加可靠性
producer.setRetryTimesWhenSendFailed(3);
// 消息发送超时,默认 3s,大消息或网络差时调大
producer.setSendMsgTimeout(5000);
producer.start();
}
/** 发送订单创建事件 */
public SendResult publishOrderCreated(OrderEvent event) {
Message msg = new Message();
msg.setTopic("ORDER_TOPIC");
msg.setTags("order_created"); // Tag 做二级分类
msg.setKeys(event.getOrderId()); // Key 用于查消息、去重
msg.setBody(JSON.toJSONBytes(event));
// 同步发送,返回 SendResult 包含 SEND_OK / FLUSH_DISK_TIMEOUT 等状态
SendResult result = producer.send(msg);
if (result.getSendStatus() != SendStatus.SEND_OK) {
log.warn("消息发送状态异常: {}", result.getSendStatus());
}
return result;
}
/** 发送通知消息(异步发送,适合高吞吐) */
public void publishNotification(Notification notification,
SendCallback callback) {
Message msg = new Message("NOTIFY_TOPIC", "notice",
JSON.toJSONBytes(notification));
producer.send(msg, callback);
}
@PreDestroy
public void destroy() {
producer.shutdown();
}
}
第四步:消费者代码
@Component
public class OrderEventConsumer {
private DefaultMQPushConsumer consumer;
@PostConstruct
public void init() throws MQClientException {
consumer = new DefaultMQPushConsumer("order-consumer-group");
consumer.setNamesrvAddr("localhost:9876");
// 订阅 Topic + Tag("*" 表示全部 Tag)
consumer.subscribe("ORDER_TOPIC", "order_created");
// 从头开始消费(新消费者首次上线)
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
// 每次最多拉取 32 条消息
consumer.setConsumeMessageBatchMaxSize(32);
consumer.registerMessageListener(
(MessageListenerConcurrently) (msgs, context) -> {
for (MessageExt msg : msgs) {
try {
String body = new String(msg.getBody(), StandardCharsets.UTF_8);
log.info("消费消息: {}", body);
// 处理业务逻辑...
} catch (Exception e) {
log.error("消息处理失败: keys={}", msg.getKeys(), e);
// 返回 RECONSUME_LATER 触发重试
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
);
consumer.start();
}
@PreDestroy
public void destroy() {
consumer.shutdown();
}
}
第五步:Spring Cloud Stream 集成(生产推荐)
Spring Cloud Stream 是对消息中间件的统一抽象,用注解代替手动构造 Producer/Consumer,生产环境更推荐。
spring:
cloud:
stream:
rocketmq:
binder:
name-server: localhost:9876
bindings:
orderOutput: # 生产者
destination: ORDER_TOPIC
content-type: application/json
orderInput: # 消费者
destination: ORDER_TOPIC
group: order-consumer-group
consumer:
maxAttempts: 3 # 消费失败重试 3 次
// 生产者:注入 StreamBridge
@Service
public class OrderService {
@Autowired
private StreamBridge streamBridge;
public void createOrder(Order order) {
// 业务逻辑...
streamBridge.send("orderOutput", order); // 发消息
}
}
// 消费者:@StreamListener 或 @StreamConsumer
@Component
public class OrderConsumer {
@StreamConsumer("orderInput")
public void onOrder(Order order) {
log.info("收到订单事件: {}", order);
}
}
常见问题
1. 消息发送成功但消费者收不到
- 检查 Consumer 订阅的
Topic和Tag是否与发送时一致(Tag 是精确匹配,"*"匹配全部) - 检查 Consumer Group 是否有冲突(多个不同业务共用同一个 group)
- 检查 Producer 是否发送到了正确的 Broker(
setNamesrvAddr地址正确)
2. 消息重复消费
RocketMQ 的设计保证至少一次(At Least Once) 投递,不天然保证去重。网络超时、重试都可能导致重复。业务方必须自行做幂等:
// 消费时根据消息 Key 做幂等判断
public ConsumeConcurrentlyStatus consume(MessageExt msg) {
String msgId = msg.getKeys();
if (redis.containKey("consumed:" + msgId)) {
// 已消费过,跳过
return CONSUME_SUCCESS;
}
// 执行业务逻辑...
redis.set("consumed:" + msgId, "1", Duration.ofDays(3));
return CONSUME_SUCCESS;
}
3. 消息堆积且消费速度跟不上
- 检查消费端是否有慢操作(数据库慢查询、外部 RPC 超时)导致每条消息处理时间长
- 增加消费者实例数(集群模式下最多不超过 Topic 的 Queue 数)
- 增大每次拉取的消息数(
setConsumeMessageBatchMaxSize) - 如果确实需要扩容:增加 Topic 的 Queue(只对新消息生效),配合增加 Consumer 实例
4. 事务消息不执行反查
TransactionListener的checkLocalTransaction方法必须正确实现——Broker 超时后会多次调用- 确认 Producer 实例没有在发送事务消息后被销毁或重启(事务状态存在内存中,重启后丢失导致无法反查)
面试题
Q1:RocketMQ 的事务消息是如何保证"本地事务和消息发送"一致性的?和 Seata AT 有什么区别?
参考答案:
RocketMQ 事务消息的流程分为三个阶段:
第一阶段: 生产者发送"半消息"(Half Message)到 Broker。此时消息对消费者不可见,存储在 RMQ_SYS_TRANS_HALF_TOPIC 中。
第二阶段: 生产者执行本地事务(如插入订单表)。执行完成后根据结果向 Broker 发送 COMMIT 或 ROLLBACK。COMMIT 后消息变为可见,消费者才能消费;ROLLBACK 后消息被删除。
第三阶段(关键): 如果生产者发完半消息后宕机,Broker 长时间收不到 COMMIT/ROLLBACK,会主动发起回查——调用生产者实现的 checkLocalTransaction 方法。生产者根据本地事务状态返回 COMMIT/ROLLBACK/UNKNOWN。UNKNOWN 会触发下一次回查。
和 Seata AT 的区别:
| 事务消息 | Seata AT | |
|---|---|---|
| 一致性模型 | 最终一致 | 最终一致 |
| 隔离性 | 无法保证隔离,事务提交前消息不可见但直接读库可能读到中间状态 | 全局锁保证写隔离 |
| 适用场景 | 跨系统数据同步、事件驱动 | 同一分布式事务内的多个服务操作同一份数据 |
| 代码入侵 | 低,只需实现回查接口 | 极低,@GlobalTransactional 即可 |
| 性能 | 高(无全局锁) | 高(AT 一阶段直接提交) |
选型建议:如果业务是"发消息给其他系统通知",用事务消息;如果业务是"跨服务操作同一份数据",用 Seata。
Q2:怎么保证 RocketMQ 消息不丢失?从生产、存储、消费三个阶段分别回答。
参考答案:
消息丢失可能发生在三个环节,每个环节有不同的保障手段:
生产阶段(Producer → Broker):
- 使用同步发送并检查 SendResult 的 SendStatus 是否为 SEND_OK。不是则重试
- 开启
retryTimesWhenSendFailed(默认 2 次重试),各用不同的 Broker - 对重要消息开启
retryAnotherBrokerWhenNotStoreOK=true,刷盘失败时换 Broker 重试
存储阶段(Broker 端):
- 刷盘方式:同步刷盘(
flushDiskType=SYNC_FLUSH)确保消息写入物理磁盘后才返回确认——这是最安全的但性能最低 - 主从复制:同步双写(
brokerRole=SYNC_MASTER),消息同时写入 Master 和 Slave 才返回成功。这样一台机器挂了另一台还有完整数据 - 生产环境使用主从架构 + 至少同步刷盘或同步复制的组合
消费阶段(Consumer 处理):
- 消费成功后必须返回
CONSUME_SUCCESS才会更新 Offset。如果消费失败但返回了 SUCCESS,Offset 往前移,消息就"丢失"了 - 消息处理不要先 ACK 再处理业务——应该先处理业务再返回 SUCCESS
- 利用消息 Key 和消息 ID,配合业务幂等表做去重,因为重试、网络抖动可能产生重复消息
总结:消息不丢失不可能在所有环节同时做到极致又不影响性能。生产实践中根据业务重要程度选择组合——资金类用同步刷盘 + 同步双写 + 同步发送;日志类用 Oneway 即可。
Q3:RocketMQ 的顺序消息是怎么实现的?集群模式下如何保证同一订单的消息被同一个消费者消费?
参考答案:
RocketMQ 的顺序消息分为两个层次:
分区顺序(常用): 同一个 Message Queue 内的消息严格 FIFO。生产者将同一业务键(如 orderId)的消息发送到同一个 Queue;消费者使用顺序消费监听器(MessageListenerOrderly)单线程串行处理。
// 生产者:选择相同的 Queue 发送
producer.send(msg, (mqs, msgArg) -> {
// 根据 orderId 选择固定的 Queue
int queueIndex = orderId.hashCode() % mqs.size();
return mqs.get(queueIndex);
}, orderId);
// 消费者:使用顺序监听器
consumer.registerMessageListener((MessageListenerOrderly) (msgs, context) -> {
// 顺序消费:同一个 Queue 的消息串行执行,不会出现并发
for (MessageExt msg : msgs) {
processOrder(new String(msg.getBody()));
}
return ConsumeOrderlyStatus.SUCCESS;
});
全局顺序(不推荐,性能低): Topic 只有一个 Queue,所有消息写入同一个 Queue 消费。对性能影响极大,只有对全局顺序有硬需求时才用。
集群模式下的顺序保证关键点:
- 生产者必须通过自定义 Queue 选择器将同一业务键的消息路由到同一个 Queue——这是分区顺序的前提
- 集群模式下一个 Queue 只能被消费者组内的一个实例消费,RocketMQ 的负载均衡机制保证这一点
- 消费者必须使用
MessageListenerOrderly,它在加锁的基础上单线程处理,并且消费失败时不会跳到下一条——会阻塞等待重试成功或跳过
和 Kafka 的分区顺序对比:两者思路一致(分区内有序),但 RocketMQ 有天然的 MessageListenerOrderly + Queue 锁机制,在消费端处理顺序消息更简单。
延伸阅读: RocketMQ 官方文档 https://rocketmq.apache.org/zh/docs/ ,特别是"最佳实践"章节中对消息去重、顺序消息、事务消息的详细指南。

浙公网安备 33010602011771号