解决RabbitMQ消息丢失与重复消费问题

针对用户提交的SQL查询长时间无响应的问题,本文分析了查询请求丢失的原因,涉及到消息队列和消费者处理流程,并提出了解决方案,包括改进消费者架构及处理消息队列中的消息重复消费等问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 背景

最近用户反馈提交的SQL查询一直处于长时间等待状态,经过排查观察,发现部分查询请求丢失,导致用户提交的查询未被正常接收,继而长时间无响应。

现象:即使SQL控制台提交10个简单SQL查询 -> 消息发送方:发送10条消息至消息队列 -> 消息消费方:只消费了7条消息

2. 现状

2.1. 当前SQL查询的整体流程

这里写图片描述

  • 生产者:PHP
    • 将用户的SQL查询记录在DB表,标识查询任务状态(f_status)为运行中;
    • 将DB表中的任务id、提交人等信息发送到RabbitMQ
  • 消息队列:RabbitMQ
    • PHP消息提交到了交换机;
    • 交换机再把消息分发给指定的消息队列;
  • 消费者:Python
    • 主进程监听消息队列,一旦有消息就不停拉取;
    • 拉取一条消息,就从进程池调起一个空闲进程来处理消息;
    • 随后反馈ACK给消息队列,将消息从消息队列中移除;

2.2. 消息发送方:Web端

结论:消息发送正常
排查步骤:查看log

2.3. 消息队列

结论:消息数量正常
诊断步骤:
执行机安装rabbitmq-dump-queue插件,用于dump队列的消息;
1. 执行机:停止服务;
2. 用户:提交10个SQL查询:
3. 发送方:查看Web服务端的输出日志,确定10个消息已经往消息队列写;
4. 执行机:通过rabbitmq-dump-queue查看队列的消息,确认是正常10个消息写入;

watch -n 1 '$GOPATH/src/rabbitmq-dump-queue/rabbitmq-dump-queue -uri="amqp://guest:guest@xxxxx:5672" -queue ph_open_task'

5. 执行机:启动服务,消息队列中的消息全部被接收;

2.4. 消息接收方

代码逻辑:

try:
    pool = Pool(processes=40)

    def callback(ch, method, properties, body):
        try:
            doSomething...
            pool.apply_async(process)
        except Exception as e:
            print traceback.format_exc()
            logger_msg.info(traceback.format_exc())
        finally:
            // 这里会有问题,即使消息未被处理也会反馈ACK给RabbitMQ
            ch.basic_ack(delivery_tag=method.delivery_tag)

    while True:
        try:
            connection = pika.BlockingConnection(
                pika.ConnectionParameters(host='xxxxxxxx'))
            channel = connection.channel()
            channel.queue_declare(queue=queue_name, durable=True)
            channel.basic_qos(prefetch_count=1)
            channel.basic_consume(callback, queue=queue_name, no_ack=False)
            channel.start_consuming()
        except pika.exceptions.ConnectionClosed as e:
            continue
except Exception as e:
    logger_msg.info(traceback.format_exc())
finally:
    channel.basic_ack(delivery_tag=method.delivery_tag)

    pool.close()
    pool.join()

结论:本例中消费者主进程将持续监听MQ,一旦MQ有消息将会拉取,随后从进程池中启动子进程来处理消息,但是从进程池启动子进程的过程并不一定成功(若当前进程池没有空闲子进程),而主进程不管任何情况下都给MQ发送ACK状态码,从而MQ将未处理的消息移除掉,导致消息丢失

3. 方案

问题是在消费者环节产生,因此对消费者做改动,需要调整消费者的架构:

  • 原来逻辑:使用进程池技术,主进程负责监听、接收MQ的消息,子进程负责执行MQ的消息,缺点是单一的主进程无法简单处理ACK状态码,不易维护;
  • 现有逻辑:使用RabbitMQ自身特性(work_queue),消费者不再维护进程池,是单进程,负责监听、接收、处理MQ的消息,处理完了以后再反馈ACK状态码,进程与进程之间互不干扰,易维护,并发量大时可随时增加消费者进程;

目前方案的问题以及解决方案:

  • 问题1:消息重复消费
    描述:用户在页面停止查询时,会导致消费者进程被杀死,因此ACK状态码未反馈至MQ,从而消息一直存留在MQ中,当新的消费者启动时会重新消费;
    解决方案:消费者每次执行查询前,首先在DB上查询任务的执行状态,若处于「取消/失败/成功」则表示已经由其它消费者消费过,那么直接返回ACK状态码给MQ,将消息从MQ中移除;
  • 问题2:进程池如何维护?
    描述:用户在页面停止查询时,会导致消费者进程被杀死,导致消费者数量减少;
    解决方案:维护一个监控脚本,每分钟轮询消费者进程数,若少于40个进程,则新启动一个消费者,直到数量足够;

这里写图片描述

### RabbitMQ消息丢失解决方案 针对RabbitMQ中可能出现的消息丢失问题,存在多种有效的预防措施。生产者确认机制确保了每条消息确实被发送至目标队列;一旦生产者接收到Broker返回的成功通知,则表明消息已被正确接收[^3]。 #### 生产者确认机制 通过启用发布确认功能,生产者能够得知其发布的消息是否已经被代理接受并记录下来。这一步骤至关重要,因为只有当消息安全抵达Broker后才能继续执行下一步操作。具体实现方式是在Channel上设置`confirm.select()`方法来激活此特性,并监听相应的回调事件以捕捉确认状态变化。 ```java channel.confirmSelect(); // 发送消息... if (!channel.waitForConfirms()) { System.out.println("Message was not confirmed"); } ``` #### 持久化配置 为了进一步保障数据的安全性,在定义Queue时需指定消息属性为持久化的形式(`durable=true`),使得即使遇到意外断电等情况也能恢复未处理完毕的信息流。同时,ExchangeBinding也应当遵循相同的设定原则,从而构建起完整的可靠传输链路[^5]。 ```python channel.queue_declare(queue='task_queue', durable=True) properties=pika.BasicProperties(delivery_mode=2,) # make message persistent channel.basic_publish(exchange='', routing_key='task_queue', body=message, properties=properties) ``` --- ### 防止重复消费的方法 为了避免同一份内容被多个实例不当读取而造成逻辑错误的现象发生,引入了消费者级别的手动ACK机制作为核心手段之一。每当客户端成功解析一条指令之后再向服务器反馈确认信号,这样就能有效避免由于网络波动等原因引起的误判情形出现[^4]。 #### 手动应答模式 默认情况下,自动签收可能会引发潜在风险——即尚未真正完成的任务就被标记为已完成。因此建议采用显式的回执策略,允许应用程序自行控制何时才算正式结束一轮交互过程。下面给出了一段Python代码片段用于展示如何切换成这种更稳妥的方式: ```python def callback(ch, method, properties, body): try: process_message(body.decode()) ch.basic_ack(delivery_tag=method.delivery_tag) # 显式确认 except Exception as e: print(f"Failed to process message: {e}") ch.basic_nack(delivery_tag=method.delivery_tag) channel.basic_consume(queue='my_queue', on_message_callback=callback, auto_ack=False) ``` 此外,利用幂等性设计思路同样有助于缓解此类困扰。所谓幂等是指无论调用多少次相同的操作最终结果保持一致不变。例如在电商场景下更新商品库存数量时可以通过原子计数器或者版本号校验等方式达成目的。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值