为什么还要看 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 多,但配置和运维复杂度也更高。选型时先看业务是否真的需要顺序和事务,不要为了“看起来高级”把简单系统做复杂。
评论