RocketMQ 怎么保证消息的顺序性?

#中间件#面试八股

RocketMQ 怎么保证消息的顺序性?

结论:RocketMQ 只能保证队列级(Queue-level)顺序,全局顺序需要把 Topic 退化成单队列,代价是并发度为 1。落地要三处配合:生产端按业务键把消息选到同一队列、消费端用顺序监听器对队列加锁串行消费、失败时挂起当前队列而不是跳过。

一、顺序的三个层次

层次做法并发度适用
全局顺序单队列 + 单线程消费1极少,如配置下发、全局流水
分区顺序按业务键选队列,队列内有序队列数订单、支付、库存(绝大多数场景)
无序默认并发消费最高通知、日志、可重试的异步任务

"保证顺序"这句话必须先问清楚:是同一个订单有序,还是所有消息有序?前者用分区顺序,后者才需要全局顺序。

二、三个环节怎么配合

Producer:按 orderId 选队列(同一订单永远落在 queue=hash(orderId)%N)
   │
   ├─ 发送重试:可能把消息重投到同一队列的尾部 → 仍可能乱序,靠业务版本号兜底
   ▼
Broker:同一队列内的 CommitLog 追加顺序 = 消费顺序
   ▼
Consumer:MessageListenerOrderly
   ├─ 对 MessageQueue 加锁,同一队列同一时刻只有一个线程在消费
   ├─ 返回 SUCCESS → 位点前移
   └─ 返回 SUSPEND_CURRENT_QUEUE_A_MOMENT → 当前队列暂停,本地重试,
      后续消息不会越过它被消费(这正是顺序消费与并发消费的最大区别)

并发消费(MessageListenerConcurrently)失败时返回 RECONSUME_LATER,消息会被投到重试队列,后续消息继续消费,顺序就断了;顺序消费失败则"卡住队列",用可用性换顺序性。

三、代码实现

// 生产端:同一个 orderId 必须落到同一个队列
Message msg = new Message("ORDER_STATUS_TOPIC", "PAID", orderId, body);
producer.send(msg, new MessageQueueSelector() {
    @Override
    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
        int idx = Math.abs(arg.toString().hashCode()) % mqs.size();
        return mqs.get(idx);        // 不要用随机或轮询
    }
}, orderId);
// 消费端:顺序监听器 + 幂等 + 版本号防重
consumer.registerMessageListener((MessageListenerOrderly) (msgs, ctx) -> {
    for (MessageExt m : msgs) {
        try {
            if (!orderService.applyStatus(m)) {   // 版本号比当前小则直接跳过
                return ConsumeOrderlyStatus.SUCCESS;
            }
        } catch (TransientException e) {
            return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;  // 稍后重试
        } catch (Exception e) {
            // 业务不可恢复:落库人工处理,避免阻塞整条队列
            deadLetterService.save(m, e);
        }
    }
    return ConsumeOrderlyStatus.SUCCESS;
});

注意 SUSPEND_CURRENT_QUEUE_A_MOMENT 不能滥用:一直挂起会让这条队列彻底停摆,永久性错误必须转人工,而不是无限重试。

四、常见追问

  • 发送重试会不会破坏顺序? 会。send 超时后重发,若第一次实际已成功,两条消息的写入顺序就可能颠倒。解决办法是给消息带版本号或状态机校验,消费端丢弃过期消息。
  • 扩容队列后顺序还成立吗? 不成立。队列数变化会让 hash % N 的结果改变,同一订单的新消息可能落到新队列,而旧消息还在旧队列。顺序敏感的业务要么预留足够队列,要么在扩容时停机或双写过渡。
  • 顺序消费为什么慢? 队列被锁串行处理,线程数再多也只能一个队列一个线程;提升吞吐只能增加队列数或按业务拆分 Topic。
  • 顺序消息还会重复吗? 会。位点每 5 秒持久化一次,重启后从旧位点重新消费,重复无法避免,业务必须幂等。
  • 能不能用并发消费 + 分布式锁实现顺序? 能但代价大:锁粒度、锁超时、死锁都要自己处理,不如直接用顺序监听器。

小结

  • RocketMQ 的顺序是队列级顺序,全局顺序等于单队列单线程,吞吐极低。
  • 生产端必须用业务键路由到固定队列,消费端必须用 MessageListenerOrderly 加队列锁。
  • 顺序消费的失败语义是"挂起队列",永久错误要转死信,否则整条链路堵死。
  • 发送重试与队列扩容都会破坏顺序,需要版本号与容量规划兜底。
笔记加载中…