RocketMQ 消息队列原理与实战

是什么

RocketMQ 是阿里巴巴开源的消息中间件,Apache 顶级项目。它解决的核心问题是通过异步解耦、削峰填谷让分布式系统的各个部分不再直接依赖——上游只管发,下游只管接,互不影响。

在 Spring Cloud Alibaba 生态中的定位: Nacos 管服务发现、Sentinel 管流量防护、Seata 管数据一致性、Gateway 管统一入口,RocketMQ 管异步通信

为什么需要

场景 没有 MQ 的后果 RocketMQ 怎么做
服务间同步耦合 下单必须等发短信、积分、推送到服务全部完成才能返回 下单只发消息到 MQ,消费者异步处理
突发流量压垮下游 秒杀流量打到订单系统,瞬间打满连接池 消息排队缓冲,消费者按自身能力拉取消费
数据最终一致性 跨系统的数据同步需要复杂的分布式事务 RocketMQ 事务消息 + 本地事务保证最终一致
系统解耦 生产者宕机导致消费者无法获取数据 消息持久化到磁盘,消费端恢复后可回溯消费

核心架构

graph TB subgraph 生产者 P1[Producer<br>Clustering] P2[Producer<br>事务消息] end subgraph NameServer 集群 NS1[NameServer<br>无状态<br>路由注册中心] end subgraph Broker 集群 B1[Broker Master<br>消息存储] B2[Broker Slave<br>消息副本] end subgraph 消费者 C1[Consumer<br>集群消费<br>一个消息进一个实例] C2[Consumer<br>广播消费<br>每个实例都收到] end P1 -->|发送消息| NS1 P2 -->|事务消息| B1 B1 -.->|心跳注册<br>每30s| NS1 B1 -->|主从复制| B2 NS1 -->|拉取路由| C1 NS1 -->|拉取路由| C2 B1 -->|Push / Pull| C1 B1 -->|Push / Pull| C2 style P1 fill:#e1f5fe style P2 fill:#e1f5fe style NS1 fill:#fff9c4 style B1 fill:#c8e6c9 style B2 fill:#a5d6a7 style C1 fill:#ffccbc style C2 fill:#ffccbc

四大角色:

角色 职责 关键点
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 发送后不等待任何响应 日志采集等可丢失场景

两种消费模式

graph LR subgraph 集群消费 Clustering B1[一条消息] --> C1[Consumer A-1] B1 --> C2[Consumer A-2] B1 --> C3[Consumer A-3] NOTE1["一条消息只被一个实例消费<br>(负载均衡)"]:::note end subgraph 广播消费 Broadcasting B2[一条消息] --> C4[Consumer B-1] B2 --> C5[Consumer B-2] B2 --> C6[Consumer B-3] NOTE2["每条消息发给所有实例<br>(每个消费端都收到)"]:::note end classDef note fill:#fff9c4

关键特性详解

1. 事务消息(RocketMQ 的杀手锏)

事务消息解决的是"本地事务 + 发消息"的一致性——要么一起成功,要么一起回滚。在跨系统最终一致场景中替代了 Seata 这类重量级分布式事务。

sequenceDiagram participant APP as 业务应用 participant BRO as Broker APP->>BRO: 发送半消息(prepare)<br>消息暂不可见 APP->>APP: 执行本地事务<br>(插入订单表) alt 本地事务成功 APP->>BRO: COMMIT<br>消息变为可见,消费者可消费 else 本地事务失败 APP->>BRO: ROLLBACK<br>消息删除 else 超时无响应 BRO->>APP: 回调反查<br>检查本地事务状态 end

关键点: 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 订阅的 TopicTag 是否与发送时一致(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. 事务消息不执行反查

  • TransactionListenercheckLocalTransaction 方法必须正确实现——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/ ,特别是"最佳实践"章节中对消息去重、顺序消息、事务消息的详细指南。

posted @ 2026-07-27 21:57  念笙  阅读(26)  评论(0)    收藏  举报