消息队列选型:从RabbitMQ到RocketMQ,1688代采系统的实战对比
本文适合后端开发、系统架构师,以及所有被消息队列坑过的同行。如果你只是想了解消息队列的概念,可以跳过代码部分。
需求:一个消息队列解决不了的场景
三年前,我们做了一套1688代采系统。核心流程很简单:用户下单 → 系统自动去1688采购 → 更新订单状态 → 通知用户。一开始用RabbitMQ做异步解耦,几百单的时候跑得挺好。直到有一天,1688 API突然大量报错,订单同步停了两个小时——调用太频繁被限流了。
更糟的是,RabbitMQ的消息积压导致内存溢出,3000多条订单状态没更新。团队花了一整天手动核对,客户群里的催单消息已经刷屏了。
这个教训让我意识到:**消息队列不是装上就能解决问题,选型和架构设计才是关键。**
对比:RabbitMQ vs RocketMQ的核心差异
我们当时在两个方案之间纠结:继续用RabbitMQ做优化,还是换RocketMQ。先看一个简单的性能对比:
| 指标 | RabbitMQ | RocketMQ |
||||
| 单机吞吐量 | 万级/秒 | 十万级/秒 |
| 消息堆积能力 | 内存堆积,容易OOM | 磁盘堆积,几乎无上限 |
| 消息可靠性 | 需手动确认 | 内置事务消息 |
| 延迟 | 微秒级 | 毫秒级 |
| 死信队列 | 需要手动配置 | 内置重试+死信机制 |
光看数字,RocketMQ好像全面碾压。但trade-off在于:**RabbitMQ的延迟更低,适合对实时性要求极高的场景**,比如即时通讯、实时通知。而RocketMQ的延迟虽然高一点,但它的消息堆积能力是RabbitMQ没法比的——这对代采系统至关重要。
Trade-off:为什么选了RocketMQ
我们最终选了RocketMQ,不是因为它的吞吐量高,而是因为三个关键点:
1. 消息堆积不丢数据
代采系统的核心痛点是:**1688 API限流时,消息不能丢**。RabbitMQ的消息默认存在内存里,一旦积压超过内存上限,直接OOM。RocketMQ的消息存在磁盘上,积压几万条也不会崩。
来看一个真实场景的代码对比:
```java
// RabbitMQ消费者(容易出问题)
@RabbitListener(queues = "order.queue")
public void handleOrder(OrderMessage message) {
try {
// 调用1688 API
orderService.purchase(message);
} catch (Exception e) {
// 限流时直接抛异常,消息被丢弃
log.error("处理订单失败", e);
// 没有重试机制
}
}
```
```java
// RocketMQ消费者(带重试)
@RocketMQMessageListener(topic = "order-topic", consumerGroup = "order-consumer")
public class OrderConsumer implements RocketMQListener
@Override
public void onMessage(OrderMessage message) {
try {
orderService.purchase(message);
} catch (ApiLimitException e) {
// 限流异常:延迟重试
throw new RuntimeException("API限流,稍后重试", e);
} catch (Exception e) {
// 其他异常:进入死信队列
log.error("订单处理失败,进入死信", e);
throw e;
}
}
}
```
RocketMQ的重试机制默认16次,每次间隔递增。**重试机制可将临时失败的消息成功率从约85% 提升至99% 以上**。这个数据来自我们线上环境的统计。
2. 消息队列集群的横向扩展
当订单量从每天几百单涨到几千单时,单机RabbitMQ扛不住了。RocketMQ的集群架构天然支持横向扩展:
```yaml
# RocketMQ集群配置(简化版)
brokerClusterName: DefaultCluster
brokerName: broker-a
namesrvAddr: 192.168.1.1:9876;192.168.1.2:9876
# 主从模式,保证高可用
brokerRole: ASYNC_MASTER
flushDiskType: ASYNC_FLUSH
```
我们部署了3个Broker节点,每个节点2个从节点。**系统吞吐量从每秒处理几百单提升到每秒处理数千单**,而且节点故障时消息不丢。
3. 事务消息解决对账问题
代采系统的另一个痛点是:**支付成功但订单没创建,或者订单创建了但支付没到账**。RocketMQ的事务消息可以保证这两步的最终一致性:
```java
// 事务消息生产者
public class OrderTransactionProducer {
@Autowired
private RocketMQTemplate rocketMQTemplate;
public void createOrder(OrderDTO order) {
// 1. 发送半消息
Message
TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction(
"order-topic", message, order
);
// 2. 事务回查:如果半消息发送成功但本地事务失败,RocketMQ会回查
}
@RocketMQTransactionListener
public class OrderTransactionListener implements RocketMQLocalTransactionListener {
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
OrderDTO order = (OrderDTO) arg;
try {
// 执行本地事务:创建订单
orderService.saveOrder(order);
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
return RocketMQLocalTransactionState.ROLLBACK;
}
}
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
// 回查:检查订单是否存在
OrderDTO order = (OrderDTO) msg.getPayload();
if (orderService.exists(order.getId())) {
return RocketMQLocalTransactionState.COMMIT;
}
return RocketMQLocalTransactionState.UNKNOWN;
}
}
}
```
这个机制解决了我们对账的噩梦。以前用RabbitMQ时,支付回调到了但订单没创建,数据对不上,得人工修复。现在事务消息保证了**支付和订单要么一起成功,要么一起失败**。
决策:不是RocketMQ更好,而是它更合适
说回trade-off。RocketMQ也有缺点:
但对我们来说,**消息不丢、能堆积、事务一致性** 是刚需。RocketMQ的缺点可以通过运维工具弥补。比如Taocarts的部署方案里,我们用Docker Compose一键部署RocketMQ集群,降低了运维成本。
**选型的核心不是选最好的,而是选最合适的。** 如果你的场景是实时通知、延迟敏感,RabbitMQ依然是最佳选择。但如果你需要高吞吐、消息堆积、事务支持,RocketMQ值得一试。
最佳实践提炼
从这次踩坑中,我总结了三点:
1. **消息队列不是银弹**:先搞清楚你的场景是实时性优先还是可靠性优先,再选型
2. **压力测试必须做**:模拟1688 API限流、消息积压等极端场景,验证系统能否扛住
3. **监控和告警不能省**:RocketMQ的Console提供消息堆积、消费延迟等指标,必须配置告警
最后,如果你正在做代采或类似的系统,建议先想清楚你的核心痛点是什么。**不要为了用消息队列而用消息队列,而是为了解决具体问题而用。**