MQ如何防止消息被重复消费?

21 篇文章 15 订阅
20 篇文章 3 订阅

被询问如何防止MQ消息被重复消费时,其实是在考察候选人对消息队列、分布式系统设计以及容错机制的理解,通过这些问题,可以全面了解候选人在处理MQ消息重复消费问题时的思考方式、技术能力和实践经验,从而评估其是否适合担任相关岗位。

MQ实现策略

MQ提供了以下几种方式来防止消息被重复消费:

1.消费者手动确认消息

在消费者消费消息后,通过调用basic.ack()方法手动确认消息已被消费。这样一来,RabbitMQ就会从队列中删除该消息,防止消息被重复消费。

java代码示例:

// 创建连接和频道
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
// 设置消息确认模式为手动确认
channel.basicQos(1);
// 定义消息消费者
Consumer consumer = new DefaultConsumer(channel) {
    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        try {
            // 模拟消费消息
            String message = new String(body, "UTF-8");
            System.out.println("Received message: " + message);
            // 手动确认消息消费
            channel.basicAck(envelope.getDeliveryTag(), false);
        } catch (Exception e) {
            // 消息消费失败,进行异常处理
            channel.basicNack(envelope.getDeliveryTag(), false, true);
        }
    }
};
// 开始消费消息
channel.basicConsume(queueName, false, consumer);

2.消息去重(使用乐观锁)

在消费者消费消息前,可以将消息的唯一标识保存在数据库或缓存中。在消费者接收到消息后,先检查数据库或缓存中是否存在该消息的唯一标识,如果存在,则表示该消息已经被消费过,可以忽略;如果不存在,则表示该消息是新的,可以进行消费。

java代码示例:

// 创建连接和频道
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
// 定义消息消费者
Consumer consumer = new DefaultConsumer(channel) {
    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        try {
            // 模拟消费消息
            String message = new String(body, "UTF-8");
            System.out.println("Received message: " + message);
            // 判断消息是否已经消费过,可以通过数据库或缓存进行判断
            if (!isMessageConsumed(message)) {
                // 进行消息消费
                consumeMessage(message);
            }
            // 手动确认消息消费
            channel.basicAck(envelope.getDeliveryTag(), false);
        } catch (Exception e) {
            // 消息消费失败,进行异常处理
            channel.basicNack(envelope.getDeliveryTag(), false, true);
        }
    }
};
// 开始消费消息
channel.basicConsume(queueName, false, consumer);

3.使用消息的全局唯一标识

可以在消息的属性中添加一个全局唯一标识,例如UUID,确保每条消息都具有唯一性。消费者在消费消息时,可以通过检查全局唯一标识来判断消息是否已经被消费过。

java代码示例:

// 创建连接和频道
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
// 定义消息消费者
Consumer consumer = new DefaultConsumer(channel) {
    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        try {
            // 模拟消费消息
            String message = new String(body, "UTF-8");
            System.out.println("Received message: " + message);
            // 判断消息是否已经消费过,可以通过全局唯一标识进行判断
            String messageId = properties.getMessageId();
            if (!isMessageConsumed(messageId)) {
                // 进行消息消费
                consumeMessage(message);
                // 将消息的全局唯一标识保存到数据库或缓存中
                saveMessageId(messageId);
            }
            // 手动确认消息消费
            channel.basicAck(envelope.getDeliveryTag(), false);
        } catch (Exception e) {
            // 消息消费失败,进行异常处理
            channel.basicNack(envelope.getDeliveryTag(), false, true);
        }
    }
};
// 开始消费消息
channel.basicConsume(queueName, false, consumer);

4.设置消息的过期时间

可以为消息设置一个过期时间,在消费者消费消息时,先判断消息是否已经过期,如果已经过期,则不进行消费。

java代码示例:

// 创建连接和频道
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();

// 设置队列的消息过期时间
Map<String, Object> arguments = new HashMap<>();
arguments.put("x-message-ttl", 5000); // 设置消息过期时间为5秒
channel.queueDeclare(queueName, true, false, false, arguments);

// 发布消息
String message = "Hello, RabbitMQ!";
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
    .expiration("5000") // 设置消息的过期时间为5秒
    .build();
channel.basicPublish("", queueName, properties, message.getBytes("UTF-8"));
System.out.println("Sent message: " + message);

// 关闭连接和频道
channel.close();
connection.close();

通过在队列声明时设置x-message-ttl参数来设置队列的消息过期时间。然后,通过发布消息时设置expiration属性来设置消息的过期时间。这里将消息的过期时间设置为5秒。需要注意的是,RabbitMQ的消息过期时间精度是毫秒级别的,可以通过设置整数或字符串形式的时间间隔来指定过期时间。如果同时设置了队列和消息的过期时间,以较小的那个为准。

5.使用幂等操作

对于一些幂等操作,可以将操作的唯一标识保存在数据库或缓存中。在消费者消费消息时,先检查操作的唯一标识是否存在,如果存在,则表示该操作已经执行过,可以忽略;如果不存在,则表示该操作是新的,可以进行执行。

java代码示例:

// 创建连接和频道
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();

// 定义消息消费者
Consumer consumer = new DefaultConsumer(channel) {
    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        try {
            // 模拟消费消息
            String message = new String(body, "UTF-8");
            System.out.println("Received message: " + message);
            
            // 判断消息是否已经处理过,可以通过幂等操作来判断
            if (!isMessageProcessed(message)) {
                // 进行消息处理
                processMessage(message);
                
                // 标记消息已处理,将消息的唯一标识保存到数据库或缓存中
                saveProcessedMessage(message);
            }
            
            // 手动确认消息消费
            channel.basicAck(envelope.getDeliveryTag(), false);
        } catch (Exception e) {
            // 消息消费失败,进行异常处理
            channel.basicNack(envelope.getDeliveryTag(), false, true);
        }
    }
};

// 开始消费消息
channel.basicConsume(queueName, false, consumer);

// 关闭连接和频道
channel.close();
connection.close();

通过isMessageProcessed()方法判断消息是否已经被处理过。如果消息已经被处理过,则忽略该消息;如果消息还未被处理过,则进行消息处理,并将消息的唯一标识保存起来。这里的processMessage()方法是具体的消息处理逻辑,saveProcessedMessage()方法将消息的唯一标识保存到数据库或缓存中,以便后续判断消息是否已经被处理过。需要根据具体的业务逻辑实现isMessageProcessed()、processMessage()saveProcessedMessage()方法来实现幂等操作。

消息重复消费的原因多种多样,不可避免。所以只能从消费者端入手,只要能保证消息处理的幂等性就可以确保消息不被重复消费。SS 而幂等性的保证又有很多方案:

  1. 给每一条消息都添加一个唯一id,在本地记录消息表及消息状态,处理消息时基于数据库表的id唯一性做判断

  2. 消息去重使用乐观锁,同样是记录消息表,利用消息状态字段实现基于乐观锁的判断,保证幂等

  3. 基于业务本身的幂等性。比如根据id的删除、查询业务天生幂等;新增、修改等业务可以考虑基于数据库id唯一性、或者乐观锁机制确保幂等。本质与消息表方案类似。

  4. 设置消息的过期时间,可以为消息设置一个过期时间,在消费者消费消息时,先判断消息是否已经过期,如果已经过期,则不进行消费。

  5. 消费者手动确认消息,在消费者消费消息后,通过调用basic.ack()方法手动确认消息已被消费。这样一来,RabbitMQ就会从队列中删除该消息,防止消息被重复消费。

  • 23
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大梦谁先觉i

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值