很多开发同学刚用MQ时,总觉得"发消息、收消息很简单",结果一上线上就出问题:消息丢了、重复消费导致数据错乱、消息顺序不对... 其实MQ的这些问题都有固定解法,关键是要搞懂"问题怎么产生的"和"具体怎么解决"。这篇文章就用口语化的方式,拆解MQ最常遇到的4个问题,每个问题都附真实场景和能落地的方案,新手也能照着做。
一、先明确:MQ 最容易出问题的4个点
不管是RabbitMQ、Kafka还是RocketMQ,线上最常遇到的问题就4类:消息丢失 、消息重复消费 、消息顺序错乱 、消息积压。这4个问题解决了,MQ线上使用基本就稳了。
二、问题1:消息丢了,用户付了钱没订单
1. 先搞懂:消息怎么丢的?
很多人以为"消息发出去就不会丢",其实从"发送方→MQ→消费方"这三步,每一步都可能丢消息:
- 发送方丢:比如订单服务发消息时,网络突然断了,消息没传到MQ就失败了;
- MQ丢:MQ节点突然宕机,消息没存到磁盘(没开持久化),重启后消息没了;
- 消费方丢:通知服务刚拿到消息,还没处理完就崩溃了,MQ以为它处理完了,没重新发。
2. 真实场景:
之前做电商项目,有次线上MQ节点宕机,没开持久化,导致100多笔下单消息丢了------用户付了钱,库存没扣、订单没创建,最后只能靠数据库日志手动恢复,还赔了用户优惠券,损失不少。
3. 怎么解决?三步保障消息不丢
(1)发送方:确保消息发去MQ
别直接发消息就完事,要等MQ的"确认"------比如用RabbitMQ的Confirm机制、Kafka的ack=all配置:
-
RabbitMQ示例(Java):
// 开启Confirm机制
channel.confirmSelect();
// 发消息
channel.basicPublish(exchange, routingKey, null, msg.getBytes());
// 等待MQ确认,超时没确认就重试
if (!channel.waitForConfirms(5000)) {
// 重试发消息(最多重试3次)
retryPublish(msg, 3);
} -
核心逻辑:发消息后,等MQ回"收到了"的确认,没收到就重试,避免网络波动导致消息没发出去。
(2)MQ:确保消息存住
开启"消息持久化",把消息存到磁盘,就算MQ节点宕机,重启后也能恢复:
- RabbitMQ:创建队列和交换机时,设置
durable=true
(持久化); - Kafka:创建Topic时,设置
replication.factor=3
(3个副本),确保至少有一个副本存住消息。
(3)消费方:确保消息处理完再告诉MQ
别用"自动ack"(MQ一给消息就自动确认),要"手动ack"------处理完业务再告诉MQ"我处理完了":
-
RabbitMQ示例(Java):
// 关闭自动ack,手动确认
channel.basicConsume(queueName, false, consumer);// 处理完业务后,手动ack
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) {
try {
// 处理业务(比如扣库存、发通知)
processMsg(body);
// 处理完,手动确认
channel.basicAck(envelope.getDeliveryTag(), false);
} catch (Exception e) {
// 处理失败,让MQ重新发
channel.basicNack(envelope.getDeliveryTag(), false, true);
}
} -
核心逻辑:处理失败就用
basicNack
让MQ重发,处理成功再basicAck
,避免没处理完就确认导致消息丢了。
三、问题2:消息重复消费,库存多扣了
1. 先搞懂:为什么会重复?
MQ为了确保消息不丢,会"重试"------比如消费方处理到一半崩溃,MQ没收到ack,就会重新发一次消息,结果导致同一条消息被消费两次:
- 例子:库存服务收到"扣1件库存"的消息,处理完还没ack就崩溃了,MQ重发一次,结果库存扣了2件。
2. 真实场景:
之前做库存系统,没处理重复消费,有次通知服务崩溃,MQ重发了10条"扣库存"消息,导致10个商品库存多扣,用户下单时显示"库存不足",投诉了好几天。
3. 怎么解决?做"幂等处理"
核心思路:让"重复消费的消息"和"消费一次的消息"结果一样,常用两种方法:
(1)用唯一ID防重
给每条消息加一个唯一ID(比如订单ID、UUID),消费前先查数据库,有这个ID就不处理:
-
示例(扣库存业务):
public void processMsg(String msgId, String goodsId, int num) {
// 1. 查数据库,看这个msgId有没有处理过
if (db.exists("select 1 from msg_log where msg_id = ?", msgId)) {
return; // 已经处理过,直接返回
}
// 2. 没处理过,扣库存
db.update("update goods set stock = stock - ? where goods_id = ?", num, goodsId);
// 3. 记录msgId到数据库,标记已处理
db.insert("insert into msg_log (msg_id) values (?)", msgId);
} -
注意:msg_log表要加唯一索引,避免重复插入。
(2)用状态机防重
比如订单状态从"待扣库存"→"已扣库存",处理前先查状态,不是目标状态就不处理:
-
示例(订单处理):
public void processOrderMsg(String orderId) {
// 1. 查订单当前状态
String status = db.query("select status from order where order_id = ?", orderId);
// 2. 只有"待扣库存"状态才处理
if (!"WAIT_STOCK".equals(status)) {
return;
}
// 3. 扣库存,更新订单状态为"已扣库存"
db.update("update order set status = 'STOCK_DONE' where order_id = ?", orderId);
}
四、问题3:消息顺序乱了,先发货再下单
1. 先搞懂:为什么顺序会乱?
如果一个队列有多个消费线程,或者消息发到多个分区,就可能导致顺序乱:
- 例子:用户下单流程,消息顺序应该是"创建订单"→"扣库存"→"发通知",结果因为多线程消费,变成"扣库存"→"创建订单",导致库存扣了但订单没创建,数据错乱。
2. 真实场景:
之前做物流系统,没保证消息顺序,用户下单后,"发货"消息比"创建订单"消息先处理,导致物流单先创建,订单后创建,物流系统显示"无此订单",只能手动改数据。
3. 怎么解决?两种方案保障顺序
(1)单线程消费
一个队列只开一个消费线程,消息按顺序处理------适合消息量不大的场景:
- RabbitMQ:消费方只启动一个线程消费队列;
- Kafka:一个分区只对应一个消费线程(Kafka的顺序是"分区内有序")。
(2)按关键字段分区
如果消息量太大,单线程处理不过来,就按关键字段(如订单ID)分区,同一个ID的消息发到同一个队列/分区:
-
Kafka示例:
// 按订单ID哈希分区,同一个订单ID的消息发到同一个分区
int partition = Math.abs(orderId.hashCode()) % topicPartitionCount;
ProducerRecord<String, String> record = new ProducerRecord<>(topic, partition, orderId, msg);
producer.send(record); -
核心逻辑:同一个用户的订单消息,都发到同一个分区,由同一个消费线程处理,确保顺序。
五、问题4:消息积压了,系统越用越慢
1. 先搞懂:为什么会积压?
消费方处理速度比发送方慢,消息就堆在MQ里------比如每秒发1000条消息,消费方每秒只处理500条,一天下来就堆了4320万条,MQ磁盘占满,处理越来越慢。
2. 真实场景:
做日志收集系统时,用Kafka收集日志,有次消费方服务器宕机,1小时堆了36万条日志,重启后消费方处理了3小时才清完,期间日志查询根本用不了。
3. 怎么解决?先止损再优化
(1)紧急止损:临时扩容消费方
- 快速启动多个临时消费实例,一起消费积压的消息;
- Kafka:增加消费组的消费者数量(不超过分区数),分摊消费压力。
(2)优化消费方:提升处理速度
- 优化代码:比如把同步数据库操作改成异步,减少处理时间;
- 加索引:数据库查询慢的话,给查询字段加索引;
- 批量处理:比如批量更新数据库,一次处理10条消息,减少数据库交互次数。
(3)预防:监控+限流
- 监控:用Prometheus+Grafana监控消息堆积量,超过1万条就报警;
- 限流:高峰期给发送方限流,比如每秒最多发800条,别让消息发得太快。
六、总结:MQ 问题解决的核心思路
遇到MQ问题,别慌,记住三个核心思路:
- 消息不丢:发送方要确认、MQ要持久化、消费方要手动ack;
- 消息不重:业务要做幂等,用唯一ID或状态机防重;
- 顺序不乱:单线程消费或按关键字段分区;
- 不积压:监控报警+临时扩容+优化消费速度。
其实MQ的问题都有固定解法,关键是要在上线前做好预案,别等线上出问题了再慌慌张张处理。希望这篇文章能帮你避开MQ的坑,让线上系统更稳定!