RabbitMQ 重试机制和消息幂等性

本文探讨了MQ中的幂等性问题,包括如何防止消费者接收重复消息,使用全局ID或唯一标识确保消息处理的一致性。同时,介绍了RabbitMQ的重试机制和可靠消息投递策略,以及如何在异步消息处理中保证数据一致性。

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

幂等性

消费者在消费mq中的消息时,mq已把消息发送给消费者,消费者在给mq返回ack时网络中断,故mq未收到确认信息,该条消息会重新发给其他的消费者,或者在网络重连后再次发送给该消费者,但实际上该消费者已成功消费了该条消息,造成消费者消费了重复的消息;

解决办法

MQ消费者的幂等行的解决一般使用全局ID 或者写个唯一标识比如时间戳 或者UUID 或者订单

消费者消费mq中的消息:也可利用mq的该id来判断,或者可按自己的规则生成一个全局唯一id,每次消费消息时用该id先判断该消息是否已消费过。

幂等性,不仅对MQ有要求,对业务上下游也有要求。

生产者代码:

import java.util.UUID;

import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.alibaba.fastjson.JSONObject;

@Component
public class FanoutProducer {
    @Autowired
    private AmqpTemplate amqpTemplate;

    public void send(String queueName) {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("email", "xx@163.com");
        jsonObject.put("timestamp", System.currentTimeMillis());
        String jsonString = jsonObject.toJSONString();
        System.out.println("jsonString:" + jsonString);
        // 设置消息唯一id 保证每次重试消息id唯一  
        Message message = MessageBuilder.withBody(jsonString.getBytes())
                .setContentType(MessageProperties.CONTENT_TYPE_JSON).setContentEncoding("utf-8")
                .setMessageId(UUID.randomUUID() + "").build(); //消息id设置在请求头里面 用UUID做全局ID 
        amqpTemplate.convertAndSend(queueName, message);
    }
}

消费者代码:

@RabbitListener(queues = "fanout_email_queue")
public void process(Message message) throws Exception {
     // 获取消息Id
     String messageId = message.getMessageProperties().getMessageId();  //id获取之
     if (messageId == null) {
            return;
     }
	 String redisMsgId = ...//根据messageId去redis中查找消息
	 if(未找到reidsMsgId) {
		 String msg = new String(message.getBody(), "UTF-8"); //消息内容获取之
	     System.out.println("-----邮件消费者获取生产者消息-----------------" + "messageId:" + messageId + ",消息内容:" + msg);
	     
	     JSONObject jsonObject = JSONObject.parseObject(msg);
	     // 获取email参数
	     String email = jsonObject.getString("email");
	     // 请求地址
	     String emailUrl = "http://127.0.0.1:8083/sendEmail?email=" + email;
	     JSONObject result = HttpClientUtils.httpGet(emailUrl);
	     if (result == null) {
	     	// 因为网络原因,造成无法访问,继续重试
	     	throw new Exception("调用接口失败!");
	     }
	     System.out.println("执行结束....");
	     //写入到redis中
	}
     
}

给消息分配一个全局id,只要消费过该消息,将 < id,message>以K-V形式写入redis。那消费者开始消费前,先去redis中查询有没消费记录即可。

利用Redis的原子性去实现

我们都知道redis是单线程的,并且性能也非常好,提供了很多原子性的命令。比如可以使用 setnx 命令。
在接收到消息后将消息ID作为key执行 setnx 命令,如果执行成功就表示没有处理过这条消息,可以进行消费了,执行失败表示消息已经被消费了。

rabbitMQ实现可靠消息投递
RabbitMQ消息中间件技术

重试机制

默认情况下,如果消费者程序出现异常情况, Rabbitmq 会自动实现补偿机制 也就是重试机制

@RabbitListener底层使用AOP进行拦截,如果程序没有抛出异常,自动提交事务。 如果Aop使用异常通知拦截获取异常信息的话 , 自动实现补偿机制,该消息会一直缓存在Rabbitmq服务器端进行重放,一直重试到不抛出异常为准。

一般来说默认5s重试一次,可以修改重试策略,消费者配置:

 listener:
      simple:
        retry:
        ####开启消费者重试
          enabled: true
         ####最大重试次数(默认无数次)
          max-attempts: 5
        ####重试间隔次数
          initial-interval: 3000

重试5次,不行就放弃

关于应答模式:

Spring boot 中进行 AOP拦截 自动帮助做重试

手动应答的话 ,如果不告诉服务器已经消费成功,则服务器不会删除消息。告诉消费成功了才会删除。

消费者的yml加入:acknowledge-mode: manual 

rabbitmq可靠发送的自动重试机制

使用异步消息时如何保证数据的一致性

业务逻辑先更新数据库,然后把结果投递到消息队列 RocketMQ 中。如果业务逻辑更新完成,但是消息投递失败,这种情况应该怎么处理?

生产端要保障的是消息发出去,RabbitMQ的Borker收到并且返会确认收到,由于网络的原因消息发送Borker的时候可能失败,另外Borker返回给生产者确认的时候也可能发生闪断,所以为了保障100%投递我们还需要合理的补偿机制。

这种情况其实就是在使用异步消息的时候怎么保证数据的一致性,有一个比较简单的做法,就是借助数据库事务来保证。

使用异步消息怎么还能借助到数据库事务?这需要在数据库中创建一个本地消息表,这样可以通过一个事务来控制本地业务逻辑更新和本地消息表的写入在同一个事务中,一旦消息落库失败,则直接全部回滚。如果消息落库成功,后续就可以根据情况基于本地数据库中的消息数据对消息进行重投了。

关于本地消息表和消息队列中状态如何保持一致,可以采用 2PC 的方式。在发消息之前落库,然后发消息,在得到同步结果或者消息回调的时候更新本地数据库表中消息状态。然后只需要通过定时轮询的方式对状态未已记录但是未发送的消息重新投递就行了。但是这种方案有个前提,就是要求消息的消费者做好幂等控制,这个其实异步消息的消费者一般都需要考虑的。

由于消息投递失败的概率比较低,没有直接设置定时钟来轮询重投,而是配置了一个监控报警,有问题的时候人工处理下就行了。如以下是我们自己使用的本地消息表的服务,其中提供了很多方法。主要包含以下方法:

public interface TransactionMessageService {
    /**
     * 预存储消息.
     */
    TransactionMsgOperateResult saveMessageWaitingConfirm(TransactionMessageDTO transactionMessageDTO);

    /**
     * 确认消息.
     */
    TransactionMsgOperateResult confirmMessage(Long msgDoId);

    /**
     * 确认并且尽最大努力发送消息
     *
     * @param msgDoId
     * @return
     */
    TransactionMsgOperateResult confirmAndTrySendMessage(Long msgDoId);

    /**
     * 存储并确认消息.
     */
    TransactionMsgOperateResult saveAndConfirmMessage(TransactionMessageDTO transactionMessageDTO);

    /**
     * 发送并存储消息。先尝试发消息,然后在落库
     */
    TransactionMsgOperateResult saveAndSendMessage(TransactionMessageDTO transactionMessageDTO);

    /**
     * 存储并且尽最大努力发送消息
     */
    TransactionMsgOperateResult saveAndTrySendMessage(TransactionMessageDTO transactionMessageDTO);

    /**
     * 直接发送消息.
     */
    TransactionMsgOperateResult directSendMessage(TransactionMessageDTO transactionMessageDTO);

    /**
     * 重发消息.
     */
    TransactionMsgOperateResult reSendMessage(Long msgDoId);

    /**
     * 查询
     *
     * @param id
     * @return
     */
    TransactionMessageDO getTransactionMessageById(Long id);

    TransactionMessageDO getByMsgId(String messageId);

    List<TransactionMessageDO> queryByStatus(String status);

}

除了使用数据库以外,还可以使用 Redis 等缓存。这样就是无法利用关系型数据库自带的事务回滚了。

https://www.cnblogs.com/lonelyxmas/p/10668473.html

### RabbitMQ 如何实现消息幂等性 RabbitMQ 本身并不直接提供消息幂等性的实现机制,而是依赖于消费者端的业务逻辑来确保消息幂等性。在分布式系统中,由于消息可能会因为网络中断、消费者故障等原因导致重复消费,因此保证幂等性是避免重复处理、确保业务状态一致的关键。 #### 幂等性的定义 幂等性在程序开发中是指同一个业务执行一次或多次对业务状态的影响是一致的。某些操作如查询删除天生具有幂等性,而像用户下单、退款等操作则不具备这一特性 [^2]。 #### RabbitMQ 中的消息重复场景 当消费者在消费消息后,向 RabbitMQ 发送 `ack` 确认信号时,如果网络中断导致 RabbitMQ 未收到确认,该消息可能会被重新投递给其他消费者或再次投递给当前消费者,造成重复消费的情况 [^4]。 #### 实现消息幂等性的方法 1. **唯一标识 + 去重表** 消费者可以为每条消息设计一个唯一标识(如订单ID、用户ID+业务ID),在处理消息前,先检查该标识是否已经被处理过。可以通过数据库、Redis 或其他持久化存储来记录已处理的消息ID。例如,创建一个幂等性校验表,记录已处理的消息ID处理时间 [^3]。 ```sql CREATE TABLE idempotent_check ( message_id VARCHAR(64) PRIMARY KEY, processed_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); ``` 2. **数据库幂等更新** 在执行业务操作时,使用数据库的唯一约束或乐观锁机制来避免重复处理。例如,在订单状态更新时,使用版本号字段来确保只有第一次更新会成功。 3. **Redis 缓存去重** 使用 Redis 缓存消息ID,利用其高效的写入查询能力进行幂等校验。可以设置与业务处理周期一致的过期时间,避免缓存无限增长。 ```java String messageId = delivery.getProperties().getCorrelationId(); Boolean isProcessed = redisTemplate.opsForValue().setIfAbsent(messageId, "processed", 24, TimeUnit.HOURS); if (Boolean.TRUE.equals(isProcessed)) { // 处理消息 processMessage(delivery.getBody()); } else { // 已处理,跳过 } ``` 4. **事务与补偿机制** 对于复杂的业务操作,可以结合本地事务与消息队列事务机制,确保消息处理与数据库操作的一致性。如果消息处理失败,可以采用补偿机制进行回滚或重试。 5. **死信队列与重试机制** 设置消息重试次数死信队列,当消息多次消费失败后进入死信队列,由专门的处理程序进行后续分析补偿处理。 #### 注意事项 - 在实现幂等性时,应根据业务特性选择合适的去重策略,避免性能瓶颈。 - 消息ID的生成应确保全局唯一性,推荐使用 UUID 或雪花算法等机制。 - 幂等性校验数据应设置合理的过期时间,避免长期存储导致资源浪费。 ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值