• 如何解决RabbitMQ消息重复消费的问题?
  • 发布于 2个月前
  • 266 热度
    0 评论

今天我给为大家讲讲RabbitMQ中的一个关键问题 - 消息重复消费。我们会了解什么是消息重复消费问题,如何保证消息的幂等性,并讲一讲对非幂等性操作可能带来的数据不一致性问题以及如何解决这些问题。


一、消息重复消费问题

在分布式系统中,消息队列扮演着重要的角色,它能够实现系统间的解耦和异步通信。然而,由于网络、硬件和服务故障等原因,消息在传递过程中可能会发生重复消费的情况。这会导致系统中的数据出现不一致性,并产生预期之外的副作用。


二、幂等性的概念与保证
为了解决消息重复消费问题,我们需要保证消息的幂等性,即无论我们多次处理相同的消息,最终的结果都应该是一致的。幂等性是指对相同的操作进行多次执行所产生的效果与一次执行的效果相同。在实际应用中,我们可以通过以下方式来保证消息的幂等性:
1.生成唯一标识: 每条消息都应该携带一个唯一标识,可以是消息ID或者由消息内容生成的唯一哈希值。接收端在处理消息前,先检查是否已经处理过具有相同唯一标识的消息,如果已经处理过,则直接忽略。

2.幂等操作设计: 我们需要设计操作具备幂等性,也就是说,无论我们对相同的消息进行多次操作,最终的结果都应该是一样的。这可能需要对数据和业务逻辑进行合理的设计,例如使用乐观锁、唯一索引或者版本控制等手段。


三、非幂等性操作导致的数据不一致性

如果我们对非幂等性的操作多次消费消息,将会引发数据不一致性的问题。这是因为每次操作都会对数据进行修改,导致最终结果与预期不符。例如,扣除用户余额是一个非幂等性操作,如果多次消费相同的消息,用户的余额将会被重复扣除,导致错误的结果。


四、保证消息不重复消费的解决方案
为了解决消息在非幂等性情况下的重复消费问题,我们可以采用以下几种方式:
1.消息去重:
每次消费消息时,记录已经消费过的消息ID或者唯一标识。在接收到消息时,先检查是否已经消费过该消息,如果已经消费过,则直接忽略。
2.消息确认机制:
消费端消费消息后,及时向消息队列发送确认消息,以确保消息已经成功处理并已从队列中删除。这可以通过RabbitMQ的确认机制来实现。
3.消息的定期过期:

在消息发送时,为消息设置一定的过期时间,超过该时间的消息将自动过期。这样即便消息被重复消费,也不会对系统产生实质性的影响。


下面我讲讲消息去重这一方案实现保证消息不重复消费的实现思路
生成者发送消息时生成唯一的消息ID,并将其储存到缓存中是一种常见的方案,用于在消费者端实现消息去重。下面是关于这个方案的解释和步骤:
1.生成者在发送每一条消息之前,生成一个唯一的消息ID。可以使用全局唯一标识符(UUID)或者其他唯一生成算法来确保消息ID的唯一性。
2.将消息ID作为消息的一部分,一同发送给RabbitMQ。
3.消费者在接收到消息后,从消息中提取出消息ID。
4.在处理消息之前,消费者将消息ID储存到缓存中,常见的选择是使用分布式缓存,使用Redis的HSETNX命令(Hash Set if Not Exists)尝试设置字段(field)的值。如果返回true,表示设置成功,说明该字段是新的,即键值对是唯一的,如果返回false说明存储失败,这个键值对已存在。
5.然后将消息ID作为键再设置一个值比如0是消息正在消费,1是消息已经消费,最后将这个键值对存储到缓存中。
6.在存储消息ID时,可以设置过期时间,以便自动清理不需要的数据。这可以通过Redis的过期时间设置来实现。
7.如果储存失败,说明这个消息ID已经存在于缓存中,其他消费者已经接收过这个消费可能因为网络异常或其他原因导致这个消息重回队列,但是不知道这个消息是否已经消费过,应该先判断这个消息在缓存中的值是0还是1
    7.1如果是1说明这个消息已经被消费了,所以这个时候可以手动ack确认
    7.2如果是0,说明有其他消费者正在消费这个消息,则可以直接忽略该消息。
8.如果储存到缓存中成功时,说明该消息是新的,可以进行消息处理的逻辑。
9.处理完消息后,将缓存中这个消息的值修改为1,最后手动ack确认消息成功消费

通过将消息ID储存到缓存中,消费者可以在处理每条消息之前,快速检查是否已经处理过该消息,从而实现消息去重。使用缓存能够提供高效的查询和插入操作,并且具备较快的响应速度,适用于高并发的场景。


需要注意的是,缓存中的消息ID可能会占用一定的存储空间,因此需要根据实际情况设置合理的过期时间和缓存大小,以及定期清理不需要的数据,以避免存储空间的过度占用。


消息去重代码演示
生成者
@SpringBootTest
@Slf4j
public class SpringAmqpTest {

    @Autowired
    RabbitTemplate rabbitTemplate;

    /**
     * @throws Exception
     */
    @Test
    public void sendOrderInfoDesc() throws Exception {
        //模拟发送订单信息给消费者扣减库存
        //订单参数
        Order order = Order.builder()
                .id(15L)
                .goodsId(9L)
                .number(2)
                .build();
        String json = JSON.toJSONString(order);
        //消息id 保证消息的唯一性
        String messageId = UUID.randomUUID().toString().replace("-", "");
        //设置消息id
        MessageProperties messageProperties = new MessageProperties();
        messageProperties.setMessageId(messageId);

        // 创建消息对象
        Message message = new Message(json.getBytes(), messageProperties);

        // 发送消息
        rabbitTemplate.send("order.direct", "test", message);

    }

}
消费者
/**
 *
 * @ 堆代码 duidaima.com
 */
@Configuration
public class RabbitConfig {
    //声明队列
    @Bean
    public Queue errorQueue(){
        return QueueBuilder.durable("order.test.queue").build();
    }
    //声明交换机
    @Bean
    public DirectExchange errorDirect(){
        return ExchangeBuilder.directExchange("order.direct").build();
    }
    //绑定队列与交换机
    @Bean
    public Binding errorQueueToErrorDirect(Queue errorQueue, DirectExchange errorDirect){
        return BindingBuilder.bind(errorQueue).to(errorDirect).with("test");
    }
    
}


@Component
@Slf4j
public class MessageListener {

    @Autowired
    private RedisTemplate redisTemplate;


    @RabbitListener(queues = "order.test.queue")
    public void listenerGoodsQueue(Message message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) Long tag) {
        String messageId = message.getMessageProperties().getMessageId();

        if (Objects.nonNull(messageId)){

            //在Redis中设置缓存键,如果已存在,则表示已处理过该消息
            boolean flag = redisTemplate.opsForValue().setIfAbsent(messageId, "0",30, TimeUnit.SECONDS);

            if (flag) {
                //缓存设置成功
                String value = String.valueOf(redisTemplate.opsForValue().get(messageId));
                log.warn("redis缓存设置成功--->key={},value={}",messageId,value);
                try {
                    String json = new String(message.getBody());
                    Order order = JSON.parseObject(json, Order.class);
                    log.warn("处理消息----> 订单id:" + order.getId() + " 商品id:" + order.getGoodsId() + " 扣减库存数量:" + order.getNumber());
                    //消息处理完毕将缓存中messageId的值设置为1
                    redisTemplate.opsForValue().set(messageId, "1");
                    value = String.valueOf(redisTemplate.opsForValue().get(messageId));
                    log.warn("消息处理完毕修改缓存messageId的值为:{}",value);

                    //消息处理完成 手动ack
                    channel.basicAck(tag, false);

                } catch (IOException e) {
                    e.printStackTrace();
                }
            } else {
                //缓存设置失败
                //获取缓存中messageId的值 判断是0还是1
                String value = String.valueOf(redisTemplate.opsForValue().get(messageId));
                if ("1".equals(value)) {
                    // 缓存中的值为1,表示消息已被处理过,手动签收返回ack
                    try {
                        channel.basicAck(tag, false);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                //如果是0 就什么都不做
            }
        }
    }

}

总结:
在使用RabbitMQ时,要牢记解决消息重复消费问题的重要性。我们必须保证消息的幂等性,使用合理的方法来解决非幂等性操作带来的数据不一致性问题。通过消息去重、消息确认机制和定期过期等技术手段,我们可以有效地保证消息在非幂等性情况下不会重复消费。

用户评论