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加队列锁。 - 顺序消费的失败语义是"挂起队列",永久错误要转死信,否则整条链路堵死。
- 发送重试与队列扩容都会破坏顺序,需要版本号与容量规划兜底。