mq的常见问题

很多开发同学刚用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问题,别慌,记住三个核心思路:

  1. 消息不丢:发送方要确认、MQ要持久化、消费方要手动ack;
  2. 消息不重:业务要做幂等,用唯一ID或状态机防重;
  3. 顺序不乱:单线程消费或按关键字段分区;
  4. 不积压:监控报警+临时扩容+优化消费速度。

其实MQ的问题都有固定解法,关键是要在上线前做好预案,别等线上出问题了再慌慌张张处理。希望这篇文章能帮你避开MQ的坑,让线上系统更稳定!

相关推荐
屏风走马7 小时前
SpringSecurity的简单想法
java
Y1_again_0_again7 小时前
Java中第三方日志库-Log4J
java·开发语言·log4j
我是华为OD~HR~栗栗呀7 小时前
24届-Python面经(华为OD)
java·前端·c++·python·华为od·华为·面试
Tony_yitao7 小时前
符号运算(华为OD)
java·算法·华为od
柳贯一(逆流河版)8 小时前
Nacos 实战指南:微服务下服务注册与配置管理的完整落地
java·微服务·架构
一叶飘零_sweeeet8 小时前
从轮询到实时推送:将站内消息接口改造为 WebSocket 服务
java·websocket
yinke小琪8 小时前
从秒杀系统崩溃到支撑千万流量:我的Redis分布式锁踩坑实录
java·redis·后端
我登哥MVP8 小时前
Apache Tomcat 详解
java·笔记·tomcat
SXJR8 小时前
Spring前置准备(八)——ConfigurableApplicationContext和DefaultListableBeanFactory的区别
java·后端·spring