RabbitMQ丢失消息和重复消费问题

最近改了改自己的简历,然后试了试水,收到了很多面试邀请,于是去热热身,一来提升一下自己的语言组织能力,二就是给自己做一些查漏补缺,天天码业务代码有时候一些很简单基础的东西反而忘得干净。

今天随便面了一家小公司,经过一个多小时的面试,所有的问题都答的很流畅,但是在一个很简单的地方卡住了,面试官问了我一个问题:

RabbitMQ如何保证消息不重复消费?

这是一个很简单的问题,但是当时我想茬了,我想到了如何防止消息不丢失,然后一下没回忆起来,卡了一会。好在面试官提示了我如何做分布式幂等校验,我就顺着答了下去。

面试完之后,自然是觉得自己十分菜鸡,不得不重新复习了一下MQ的这一块内容,然后整理了一个笔记。

RabbitMQ保证消息不丢失

首先,要搞清楚RabbitMQ在哪些地方可能丢失消息:

  1. 生产者发送给MQ的时候丢失了。
  2. MQ收到了消息,但是发送给消费者的时候丢失了。
  3. 消费者收到了消息但是因为程序异常没有消费消息。

1.解决生产者消息丢失

解决生产者发送给MQ的消息不丢失,首先,我们可以用MQ的事务,在提交失败之后可以做事务回滚,然后重新提交,但是事务是串行的,性能不是很好,所以我们可以使用confirm模式来处理生产者的消息丢失问题,废话不说先上代码:

public class ConfirmProd {

    private static final String QUEUE_NAME = "work_queue_confirm";

    public static void main(String[] args) throws Exception {
        //获取连接
        Connection connection = RabbitConnectionFactory.ConnectionRabbit();
        //从链接中获取一个通道
        Channel channel = connection.createChannel();
        //声明一个队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        channel.confirmSelect(); //将生产者设置为confirm模式
        String msg = "这是Confirm提交的消息";
        channel.basicPublish(" ", QUEUE_NAME, null, msg.getBytes());
       	
        if(!channel.waitForConfirms()){   //此处接收RabbitMQ发送回来的确认消息
            System.out.println("发送失败,做失败处理的逻辑。");
        }else {
            System.out.println("发送成功");
        }
        channel.close();
        connection.close();
    }
}

这是一个使用confirm模式向RabbitMQ推送消息的简单demo,channel.confirmSelect(); 方法为这个queue声明了一个confirm模式,在confirm模式下每个消息都会生成一个唯一的ID,一旦发送到了RabbitMQ且被投递到自己所属的队列之后,MQ就会发送一个确认消息给生产者,生产者可以确认消息是否送达。
需要注意的是,如果队列开启了持久化,那么只有在消息持久化之后才会返回确认消息,RabbitMQ的confirm模式确认是异步的,不会特别影响MQ的性能。

2.解决MQ的消息丢失

如果已经解决了生产者发送给MQ的消息丢失问题,那么解决MQ的消息丢失问题就变的十分简单,只需要在声明队列的时候告诉RabbitMQ这个队列需要做持久化即可,这样即便MQ挂了导致内存里面的消息丢失,那么硬盘上还是会有记录。

//消费者声明一个队列
boolean durable = true; //是否将消息持久化到硬盘
channel.queueDeclare(QUEUE_NAME, durable, false, false, null);

3.解决消费者的消息丢失

public class ConfirmCons {

    private static final String QUEUE_NAME = "work_queue_confirm";

    public static void main(String[] args) throws Exception {
        //获取一个链接
        Connection connection = RabbitConnectionFactory.ConnectionRabbit();
        //从链接中获取一个通道
        Channel channel = connection.createChannel();

        //事件监听新写法,事件模型,一旦有消息进入队列就会触发handleDelivery方法
        DefaultConsumer consumer =  new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, StandardCharsets.UTF_8);
                System.out.println("收到的消息是: " + msg);
                channel.basicAck(envelope.getDeliveryTag(), false);   //这是向RabbitMQ手动应答ack确认消息
            }
        };
        
		boolean autoAck = false;
        channel.basicConsume(QUEUE_NAME, noAutoAck, consumer);
    }
}

channel.basicConsume(QUEUE_NAME, noAutoAck, consumer); 这一行意味着将这个RabbitMQ的自动消息应答改为false,此时需要在消费者代码中在消息消费成功之后手动的调用代码来向RabbitMQ确认 : channel.basicAck(envelope.getDeliveryTag(), false);
当设置为自动应答时,RabbitMQ将消息发送给消费者之后马上就删除消息,如果消费者消费失败就会造成消息丢失。但是如果设置成手动回复ack确认,那么RabbitMQ不会立即删除消息,而是要等到收到ack确认为止。如果RabbitMQ长时间未收到消费者的手动确认,那么会尝试再次发送消息到其他消费者消费,这样就不会丢失消息,但是可能重复消费。

RabbitMQ重复消费问题

上面讲消息丢失的时候提到过有些情况消息会重复消费,除此之外,假设生产者发送了两条一样的消息或者有人伪造了请求构造了多条假的消息,应该如何避免重复消费呢,特别是在一些订单验证的接口中,伪造订单重复消费如果不做验证会被大量刷单,是很严重的情况。

为了防止这种情况,那么我们需要做一个幂等验证,先简单的解释一下幂等:

在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。
例如一个订单号的消费,第一次消费发货成功,那么第二次第三次依旧要是成功,但是却不能发货。

最简单的办法就是有一个唯一的ID,这个ID可以是订单号或者别的什么,每次消费的时候都要验证一下这个ID的数据是否有被消费,消费记录都需要持久化存储起来(一般是MySQL),整个代码的处理逻辑如下:

		//如果是分布式业务,此处验证需要加上分布式锁 setnx()方法,懂的都得。
        if(consumer){   //验证该消息是否被消费,如果被消费了直接返回true,告知生产者不用再发送该消息
             return true;
        }
        //如果上面的代码没有返回true,说明是第一次消费,那么走消费流程
        …………
        
        retrun true;  //消费完成,此时consumer是已消费的状态,给生产者返回消费成功消息,假设该返回丢失,生产者没有收到,即便再次发送了消息,那么在 if(consumer)处验证也会直接再次返回,不会再走业务逻辑。这样就可以保证每个消息纸杯消费一次。

详细的流程已经标注在上面的注释之中了,这整个流程走下来,就解决了RabbitMQ的消息丢失和重复消费的问题。

上面的生产者和消费者代码可以直接运行,只需要引入相应类库,然后改一下包名就可以自己测试了,眼睛会了手不一定会,实践也是十分重要的!

你可能感兴趣的:(杂文)