RabbitMQ消息重复消费场景及解决方案

本文介绍RabbitMQ中消息重复消费的问题及解决方案,通过给每条消息分配唯一ID并在Redis中存储确认来避免重复消费。

前言

上一篇文章介绍了springboot如何整合RabbitMQ:springboot集成RabbitMQ_龙池小生的博客-优快云博客

这里介绍一下RabbitMQ重复消费的场景,以及如何解决消息重复消费的问题。

注:本文只做粗略逻辑实现借鉴,实际业务场景需根据实际情况再做细化处理。

目录

消息重复消费:

MQ的一条消息被消费者消费了多次:

重复消费场景重现测试:

如何解决消息重复消费的问题:

编码:

解决消息重复消费测试:


消息重复消费:

什么是消息重复消费?首先我们来看一下消息的传输流程。消息生产者--》MQ--》消息消费者;消息生产者发送消息到MQ服务器,MQ服务器存储消息,消息消费者监听MQ的消息,发现有消息就消费消息。

所以消息重复也就出现在两个阶段1、生产者多发送了消息给MQ;2、MQ的一条消息被消费者消费了多次。第一种场景很好控制,只要保证消息生成者不重复发送消息给MQ即可。我们着重来看一下第二个场景。

MQ的一条消息被消费者消费了多次

在保证MQ消息不重复的情况下,消费者消费消息成功后,在给MQ发送消息确认的时候出现了网络异常(或者是服务中断),MQ没有接收到确认,此时MQ不会将发送的消息删除,
为了保证消息被消费,当消费者网络稳定后,MQ就会继续给消费者投递之前的消息。这时候消费者就接收到了两条一样的消息。

重复消费场景重现测试

1、消息发送者发送1万条消息给MQ:

    @GetMapping("/rabbitmq/sendToClient")
    public String sendToClient() {
        String message = "server message sendToClient";
        for (int i = 0; i < 10000; i++) {
            amqpTemplate.convertAndSend("queueName3",message+": "+i);

        }
        return message;
    }

启动消息发送服务,调用接口发送消息,mq成功收到1万条消息。

2、消费者监听消费消息:

    @RabbitListener(queues = "queueName3")//发送的队列名称     @RabbitListener注解到类和方法都可以
    @RabbitHandler
    public void receiveMessage(String message) {
        System.out.println("接收者2--接收到queueName3队列的消息为:"+message);
    }

启动消费者服务,然后中断消费服务,此时消费到了第7913个消息:

此时查看MQ的消息,现在MQ队列中应该还有2087个消息,但还有2088个

### RabbitMQ 消息重复消费原因及解决方案 #### 1. 消息重复消费的原因 消息重复消费的根本原因主要包括以下几个方面: - **网络波动或中断**:在消费者处理消息期间,如果网络出现波动或中断,导致消费者的确认消息(ACK)未能成功发送到 RabbitMQ,则 RabbitMQ 会认为消息未被成功处理并重新投递[^2]。 - **消费者故障**:当消费者在处理消息时发生崩溃、超时或其他异常情况,且未发送确认消息时,RabbitMQ 会将消息重新分配给其他消费者或再次推送给当前消费者[^2]。 - **集群脑裂**:在 RabbitMQ 集群中,如果发生脑裂现象,可能导致部分节点之间的通信中断,从而引发消息重复投递[^1]。 - **自动确认机制问题**:如果消费者设置了自动确认机制,并在消息处理完成前发生宕机,则 RabbitMQ 可能会认为消息未被处理并重新发送[^4]。 - **“至少一次传递”策略**:RabbitMQ 的“至少一次传递”策略确保了消息至少会被传递一次,但在某些情况下可能导致消息的多次传递[^2]。 #### 2. 解决方案 ##### 2.1 保证消费者幂等性 核心原则是无论消息消费多少次,结果都应保持一致。以下是几种实现方式: - **数据库唯一约束**:利用数据库主键或唯一索引防止重复写入,例如订单号唯一性。 - **版本号/状态机**:在更新数据之前检查其状态,确保只有符合特定条件的消息才会被处理[^1]。 - **Token 机制**:预生成唯一的 Token,在处理消息时校验该 Token 是否已存在,若存在则跳过处理[^1]。 ##### 2.2 消息去重设计 通过为每条消息生成唯一标识符,并结合外部存储进行去重操作: - **Redis 去重**:使用 Redis 的 `SETNX` 命令记录已处理的消息 ID,并设置合理的过期时间以释放内存空间。 - **数据库去重表**:创建一个专门用于存储消息 ID 和状态的表,在处理消息前查询该表以判断是否已处理过该消息[^1]。 ```python import redis # 初始化 Redis 客户端 redis_client = redis.StrictRedis(host='localhost', port=6379, db=0) def process_message(message_id, body): # 使用 SETNX 实现去重 if not redis_client.setnx(message_id, "processed"): print(f"Message {message_id} already processed, skipping.") return # 设置过期时间(如 1 小时) redis_client.expire(message_id, 3600) # 处理业务逻辑 print(f"Processing message {message_id}: {body.decode()}") ``` ##### 2.3 优化消息确认机制 - **手动确认**:避免使用自动确认机制,改为手动确认,确保仅在消息处理成功后才发送 ACK[^4]。 - **合理设置超时时间**:根据业务需求调整消费者的超时时间,避免因超时导致的重复投递[^2]。 ##### 2.4 控制消息重试 通过限制消息的重试次数来减少重复消费的可能性。例如,可以设置死信队列(DLQ),将超过重试次数的消息转移到 DLQ 中进行后续处理。 ```python import pika connection = pika.BlockingConnection(pika.ConnectionParameters('localhost')) channel = connection.channel() # 声明死信队列和普通队列 dlx_queue_name = 'dlq_queue' queue_name = 'retry_queue' channel.queue_declare(queue=dlx_queue_name) channel.queue_declare( queue=queue_name, arguments={ 'x-dead-letter-exchange': '', 'x-dead-letter-routing-key': dlx_queue_name, 'x-message-ttl': 5000, # 消息 TTL 为 5 秒 'x-max-length': 3 # 最大重试次数为 3 次 } ) ``` ##### 2.5 并发控制 在多消费场景下,可以通过以下方式避免多个消费者同时处理同一条消息: - **单消费者模型**:对于需要严格顺序的消息,确保每个队列只有一个消费者[^3]。 - **分区队列**:根据消息类型或标识符将消息路由到不同的队列,每个队列由独立的消费者负责处理[^3]。 --- ###
评论 21
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值