ElIauk

RocketMQ 入门与顺序消息

为什么还要看 RocketMQ RabbitMQ 适合业务解耦和复杂路由,但遇到“每秒几十万条”“必须按订单顺序消费”“发送和本地事务要绑定”这类需求,RocketMQ 更合适。它原生支持顺序消息、事务消息、延时消息和消息轨迹,Topic 下的队列天然支持水平扩展,阿里系和国内不少互联网公司都在用。

评论
RocketMQ 入门与顺序消息

为什么还要看 RocketMQ

RabbitMQ 适合业务解耦和复杂路由,但遇到“每秒几十万条”“必须按订单顺序消费”“发送和本地事务要绑定”这类需求,RocketMQ 更合适。它原生支持顺序消息、事务消息、延时消息和消息轨迹,Topic 下的队列天然支持水平扩展,阿里系和国内不少互联网公司都在用。

RocketMQ 的模型比 RabbitMQ 多一层:NameServer 负责路由,Broker 负责存储,Topic 是逻辑概念,真正落盘的是 Topic 下的多个 Queue。生产者把消息发到某个 Queue,消费者按 Queue 拉取,顺序性就建立在“同一个 Queue 内有序”这个前提上。

核心模型

概念 作用
NameServer 无状态路由中心,生产者和消费者从这里拿 Broker 地址
Broker 存储和转发消息,主从部署保证可用性
Topic 消息主题,一个 Topic 默认有多个 Queue
Queue 物理分片,顺序消息的边界
Tag Topic 下的二级分类,消费端可按 Tag 过滤
ConsumerGroup 消费组,组内竞争消费,组间广播

消费模式分集群消费和广播消费。集群消费下,一条消息只会被组内一个消费者处理;广播消费下,组内每个消费者都收到一份。顺序消息只在集群消费 + 同一个 Queue 的前提下成立。

代码:按订单号顺序发送与消费

Spring Boot 3.2 对应 rocketmq-spring-boot-starter 2.3.x,2.2.x 及更早版本还依赖 javax,在 Boot 3 下会直接启动失败,这是升级时最容易踩的版本坑。

rocketmq:
  name-server: 127.0.0.1:9876
  producer:
    group: order-producer
    send-message-timeout: 3000

发送顺序消息用 syncSendOrderly,第三个参数是分片键。RocketMQ 会对它做哈希,保证同一订单的消息进入同一个 Queue:

@Service
public class OrderProducer {

    private final RocketMQTemplate rocketMQTemplate;

    public OrderProducer(RocketMQTemplate rocketMQTemplate) {
        this.rocketMQTemplate = rocketMQTemplate;
    }

    public void send(OrderEvent event) {
        SendResult result = rocketMQTemplate.syncSendOrderly(
                "order-topic:created",
                event,
                String.valueOf(event.orderId()));
        if (result.getSendStatus() != SendStatus.SEND_OK) {
            throw new IllegalStateException("订单消息发送失败: " + result.getSendStatus());
        }
    }
}

消费端声明 consumeMode = ConsumeMode.ORDERLY,RocketMQ 会对 Queue 加锁,保证同一个 Queue 内的消息串行处理:

@Component
@RocketMQMessageListener(
        topic = "order-topic",
        selectorExpression = "created",
        consumerGroup = "order-created-consumer",
        consumeMode = ConsumeMode.ORDERLY)
public class OrderCreatedConsumer implements RocketMQListener<OrderEvent> {

    private static final Logger log = LoggerFactory.getLogger(OrderCreatedConsumer.class);

    @Override
    public void onMessage(OrderEvent event) {
        log.info("按顺序处理订单事件, orderId={}, type={}", event.orderId(), event.type());
        // 业务处理必须幂等,重试会导致重复投递
    }
}

顺序消息的边界

顺序消息不是“全局有序”,而是“同一个分片键有序”。订单 A 和订单 B 之间没有顺序保证,只有同一订单的创建、支付、发货事件会按发送顺序消费。分片键选错,比如用随机数或用户 ID 而不是订单 ID,同一订单的消息可能落到不同 Queue,顺序直接失效。

消费端用 ORDERLY 模式时,一条消息处理失败会阻塞当前 Queue,后续消息等待重试。这是“顺序”换来的代价:一个毒消息可能拖住整个 Queue。所以顺序消费的业务要控制单条耗时,把可异步的部分拆出去,并设置合理的重试次数,超过后进入死信队列。

Topic 的 Queue 数量创建后可以增加,但增加后分片键的哈希映射会变化,历史消息和新消息的顺序关系可能被打乱。顺序消息的 Topic 要在创建时规划好 Queue 数量,不要等线上再加。

常见坑

1. 用了普通 syncSend

普通发送默认轮询选择 Queue,同一订单的消息会散落到不同 Queue,消费端再标 ORDERLY 也没用。顺序消息必须用 syncSendOrderly 或 sendOrderly,并且分片键保持一致。

2. 消费模式和发送方式不匹配

生产端顺序发送,消费端却用默认的并发消费,多个线程同时处理同一 Queue,顺序仍然丢失。两边要同时保证:发送按分片键进同一 Queue,消费用 ConsumeMode.ORDERLY。

3. 重试导致重复

顺序消息失败后会在本地重试,业务可能已经执行了一半。消费逻辑要幂等,用订单号 + 事件类型做唯一键,或者用状态机判断当前状态是否允许该事件。

4. 自动创建 Topic 掩盖了拼写错误

开发环境 autoCreateTopicEnable=true 很方便,生产环境建议关闭,统一通过运维平台建 Topic。否则代码里 Topic 名拼错,RocketMQ 会自动建一个新 Topic,消息发进去了但没人消费,排查很久才发现。

5. 消息体过大

RocketMQ 默认单条消息上限 4MB,超过会发送失败。大报文应该存对象存储,消息里只放地址;不要通过调大 Broker 参数硬扛,会影响存储和网络。

排查步骤

先用命令行确认 Topic、Queue 和消费进度:

mqadmin topicList -n 127.0.0.1:9876
mqadmin topicStatus -n 127.0.0.1:9876 -t order-topic
mqadmin consumerProgress -n 127.0.0.1:9876 -g order-created-consumer
mqadmin brokerStatus -n 127.0.0.1:9876 -b 127.0.0.1:10911

consumerProgress 里看 Diff Total,也就是积压量;持续增长说明消费能力不足或消费卡住。顺序消费卡住时,重点看是不是某个 Queue 的消息一直重试,日志里搜 reconsumeTimes 和 RECONSUME_LATER。客户端日志在 ~/logs/rocketmqlogs/rocketmq_client.log,连接失败、路由找不到 Broker 都会记在这里。

小结

RocketMQ 的顺序消息靠“同一分片键进同一 Queue + 消费端串行处理”实现,两个条件缺一不可。它的能力比 RabbitMQ 多,但配置和运维复杂度也更高。选型时先看业务是否真的需要顺序和事务,不要为了“看起来高级”把简单系统做复杂。

参考

评论

推荐阅读

订阅邮件

订阅更新,及时收到最新文章。

作者