文章目录
摘要:RocketMQ 作为一款高性能、高吞吐量的分布式消息中间件,在微服务架构中扮演着“削峰填谷”和“解耦”的核心角色。然而,在复杂的生产环境中,消息丢失、消息积压、重复消费等问题时有发生。本文将结合源码与实战经验,深入拆解这些痛点,提供可落地的解决方案。
一、 全局视野:生产问题全景图
在深入具体技术细节前,我们先通过一张思维导图建立 RocketMQ 生产问题的分类体系。
二、 核心痛点一:消息丢失 (Message Loss)
消息丢失是业务无法容忍的致命问题。RocketMQ 在 Producer、Broker、Consumer 三个环节都可能出现漏洞。
1. 生产者端 (Producer)
问题现象:业务日志显示发送成功,但 Broker 端查不到消息。
原因分析:
- 使用了
Oneway发送模式(只管发,不等待响应)。 - 异步发送时,未正确处理回调中的异常。
✅ 解决方案:使用同步发送或异步发送带回调,并结合重试机制。
// 生产级代码示例:可靠的异步发送
producer.setRetryTimesWhenSendAsyncFailed(3); // 失败重试次数
producer.send(msg, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
// 业务逻辑:记录发送成功日志
log.info("Send success: {}", sendResult.getMsgId());
}
@Override
public void onException(Throwable e) {
// 关键:异常处理
log.error("Send failed, topic: {}", msg.getTopic(), e);
// 兜底策略:写入本地数据库/文件,由后台线程定时重试(最终一致性)
saveToLocalDeadLetter(msg);
}
});
2. Broker 端
问题现象:Broker 宕机,重启后由于刷盘机制导致部分内存消息丢失。
✅ 解决方案:权衡性能与可靠性,调整 Broker 配置。
flushDiskType: 默认为ASYNC_FLUSH(异步刷盘)。金融级业务建议修改为SYNC_FLUSH(同步刷盘)。brokerRole: 默认为ASYNC_MASTER。建议修改为SYNC_MASTER(同步复制),保证主从数据一致。
3. 消费者端 (Consumer)
问题现象:消费者拉取了消息,业务逻辑未执行完就宕机,但 Offset 已经提交。
原因分析:过早返回 CONSUME_SUCCESS 或代码中使用了自动提交 Offset(RocketMQ 默认是手动逻辑确认)。
✅ 解决方案:确保业务逻辑幂等且执行成功后再返回状态。
三、 核心痛点二:消息积压 (Message Accumulation)
当生产速度远大于消费速度,或者消费者出现 Bug 卡死时,会发生积压。
1. 紧急处理流程
如果线上已经积压了百万条消息,简单的重启或增加消费者可能无效(受限于 Queue 的数量)。
2. 扩容实战代码(临时转发策略)
当 Queue 只有 4 个,但你需要 20 个消费者来加快速度时,可以使用“搬运工”模式:
// 搬运工消费者:只负责将消息转发到 partitions 更多的临时 Topic
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
try {
List<Message> newMsgs = msgs.stream()
.map(m -> new Message("TEMP_TOPIC_EXPANDED", m.getBody())) // 发送到临时 Topic
.collect(Collectors.toList());
// 批量快速转发,不进行耗时业务逻辑
tempProducer.send(newMsgs);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
} catch (Exception e) {
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
}
3. 常见积压原因自查
- 本地锁竞争:消费者内部线程池是否被数据库锁或死锁卡住?
- 消费耗时:是否有外部 API 调用超时未设置上限?
- 并发度设置:
consumeThreadMin和consumeThreadMax是否设置得过小?
四、 核心痛点三:消息重复与幂等 (Idempotency)
RocketMQ 承诺的是 At Least Once(至少一次),不保证 Exactly Once。
产生原因:
- 网络抖动:Broker 收到消息并落盘,但回传 ACK 给 Producer 时失败,Producer 重试导致重复。
- Rebalance:消费者扩缩容或 Broker 重启,导致队列重新分配,部分已消费但未提交 Offset 的消息被重新消费。
✅ 解决方案:业务层必须实现幂等性。
幂等性通用设计模式
Java 伪代码示例:
// 利用数据库主键冲突实现幂等
try {
orderService.createOrder(orderMsg); // orderId 是主键
} catch (DuplicateKeyException e) {
log.warn("Order already exists, ignore. ID: {}", orderMsg.getId());
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
五、 性能优化与隐患排查
1. 启动脚本优化 (runbroker.sh)
RocketMQ 默认的 JVM 配置可能并不适合大内存机器,容易导致 Full GC 导致 Broker 暂停响应。
建议调整:
- 使用 G1 收集器:对于大堆(Heap > 8GB),G1 表现更稳。
- Xms 与 Xmx 相等:防止内存抖动。
# 修改 runbroker.sh
JAVA_OPT="${JAVA_OPT} -server -Xms16g -Xmx16g -XX:+UseG1GC -XX:MaxGCPauseMillis=200"
2. PageCache 与 Busy 问题
现象:日志出现 PAGECACHELOCK_TIMEOUT 或 OsPageCacheBusy。
原因:
Broker 严重依赖 OS 的 PageCache。如果同一台机器上有其他高 IO 进程(如 ElasticSearch, Hadoop),或者 Broker 内存不足导致频繁 Swap,会锁死 PageCache。
排查命令:
# 查看内存交换情况,si/so 必须为 0
vmstat 1
# 查看磁盘 IO 利用率
iostat -x 1
3. 顺序消费的陷阱
问题:顺序消费(Orderly)性能极差。
原因:
RocketMQ 的顺序消费是队列级锁。一个队列同一时刻只能被一个线程消费。如果某个消息处理卡死,整个队列都会堵塞。
策略:
- 尽量减少顺序消息的使用场景。
- 如果必须使用,确保 Partition Key(如 OrderId)散列均匀,避免数据倾斜导致某个 Queue 成为热点。
六、 故障排查工具箱
当线上报警响起时,请按以下步骤操作:
- Dashboard 查看:RocketMQ Dashboard 是第一现场,查看
Consumer Lag(堆积量)和TPS。 - Broker 日志:
store.log: 查看刷盘、存储情况。broker.log: 查看核心运行错误。
- 命令行工具 (mqadmin):
# 查看消费者状态,包含堆积量、Consumer连接情况
sh mqadmin consumerProgress -n 127.0.0.1:9876 -g MyConsumerGroup
# 查看集群状态
sh mqadmin clusterList -n 127.0.0.1:9876
# 查询特定消息轨迹
sh mqadmin queryMsgById -n 127.0.0.1:9876 -i <MsgId>
总结
RocketMQ 的稳定性治理是一个系统工程。
- 防丢失:靠的是同步刷盘、同步复制和优雅的异常处理。
- 防重复:靠的是业务层的幂等设计。
- 防积压:靠的是合理的队列规划、快速的消费逻辑以及紧急情况下的扩容方案。
542



被折叠的 条评论
为什么被折叠?



