Java高频面试题- 每日三连问?【Day9】 — 消息队列篇二

2021-08-16 16:50:07 浏览数 (1)

问题导读

一、重复消费的情况出现过吗?如何解决的?

二、你是如何防止消息丢失的?

三、如何保证消息队列的高可用?

01

重复消费的情况出现过吗?如何解决的?

正经回答:

  在一般网络环境下,都存在一定的网络延迟、网络抖动,网络问题导致消息重复发送的情况是难以避免的,毕竟网络环境无法预知。

因此MQ默认允许消息重复发送。是的,只要通过网络交换数据,就无法避免这个问题。秉承着打不过就加入的原则,解决这个问题的办法就是绕过这个问题。

那么问题就变成了:如果消费端收到两条一样的消息,应该怎样处理?

  RabbitMQ、RocketMQ、Kafka,都有可能会出现消息重复消费的问题。因为这问题通常不是 MQ 自己保证的,而是消费方自己来保证的

  比如说Kafka, 他实际上有个 offset 的概念(偏移量),就是每个消息写进去,都有一个 offset,代表消息的序号,然后 consumer 消费了数据之后,每隔一段时间(定时定期),会把自己消费过的消息的 offset 提交一下

代表我已经消费过了,就算消费者重启,Kafka也会让消费者继上次消费到的offset继续消费

场景示例:

  kafka 中有一条数据:AB,kafka给这条数据分一个 offset(偏移量),offset为: 10011002。消费者从 kafka 去消费的时候,也是按照这个顺序去消费。

当消费者消费到 offset=1002 的这条数据(此时offset=1001还没消费完),刚提交 offset=1002 到 zookeeper,消费者进程就被重启了

此时消费过的数据 A 的 offset 还没有提交,kafka 也就不知道消费者已经消费了1001这条数据。那么重启之后,消费者会找 Kafka 把上次消费到的那个地方后面的数据继续传递过来。数据 A 再次被消费。

 如果消费者是拿到一条数据就往数据库里写一条,就会导致把数据 A 在数据库里插入了 2 次,导致数据不一致。

重复消费其实并不可怕,可怕的是你没考虑到重复消费时,怎么保证幂等性

解决重复消费的方案(保证幂等性)

幂等性,比如一个数据或者一个请求,给后台重复发多次,针对这类情况,你得确保对应的数据结果是不会改变的,不能因为发了多个相同请求导致数据出错。

  • 比如你拿个数据要写库,你先根据主键查一下,如果这数据都有了,你就别插入了,update就行。对了,ES的插入接口是不是就采用了插入并更新的策略?发现相同的数据就直接更新他。
  • 如果是写 Redis,那没问题,反正每次都是set,天然幂等性
  • 比如你不是上面两个场景,那做的稍微复杂一点,你需要让生产者发送每条数据的时候,里面加一个全局唯一的 id,类似订单 id 之类的东西,然后你这里消费到了之后,先根据这个 id 去 Redis 里查一下,之前消费过吗?如果没有消费过,你就处理,然后这个 id 写 进Redis。如果消费过了,那你就别处理了,保证别重复处理相同的消息即可。
  • 比如基于数据库的唯一键来保证重复数据不会重复插入多条。因为有唯一键约束了,重复数据插入只会报错,不会导致数据库中出现脏数据。(类似于第一条,可以通过修改SQL,转成插入或更新的策略)

02

你是如何防止消息丢失的?

正经回答:

第一种:生产者弄丢了数据。生产者将数据发送到 RabbitMQ 的时候,可能数据就在半路给搞丢了,因为网络问题啥的,都有可能。

第二种:RabbitMQ 弄丢了数据。MQ还没有持久化自己挂了 第三种:消费端弄丢了数据。刚消费到,还没处理,结果进程挂了,比如重启了。

解决方案

①:可以选择使用rabbitmq提供是事物功能

就是生产者在发送数据之前开启事物,然后发送消息,如果消息没有成功被rabbitmq接收到,那么生产者会受到异常报错,这时就可以回滚事物,然后尝试重新发送;如果收到了消息,那么就可以提交事物。

代码语言:javascript复制
channel.txSelect();//开启事物
try{
//发送消息
}catch(Exection e){
channel.txRollback();//回滚事物
//重新提交
}

缺点:rabbitmq事物已开启,就会变为同步阻塞操作,生产者会阻塞等待是否发送成功,太耗性能会造成吞吐量的下降。

②:可以开启confirm模式

在生产者哪里设置开启了confirm模式之后,每次写的消息都会分配一个唯一的id,然后如何写入了rabbitmq之中,rabbitmq会给你回传一个ack消息,告诉你这个消息发送OK了;

如果rabbitmq没能处理这个消息,会回调你一个nack接口,告诉你这个消息失败了,你可以进行重试。

而且你可以结合这个机制知道自己在内存里维护每个消息的id,如果超过一定时间还没接收到这个消息的回调,那么你可以进行重发。

代码语言:javascript复制
//开启confirm
channel.confirm();
//发送成功回调
public void ack(String messageId){
}

// 发送失败回调
public void nack(String messageId){
    //重发该消息
}

03

如何保证消息队列的高可用?

正经回答:

可以考虑镜像模式部署消息队列

RabbitMQ 是基于主从做高可用性的,Rabbitmq有三种模式:单机模式、普通集群模式、镜像集群模式。

单机模式一般在生产环境中很少用,普通集群模式只是提高了系统的吞吐量,让集群中多个节点来服务某个 Queue 的读写操作。

那么真正实现 RabbitMQ 高可用的是镜像集群模式。

镜像集群模式跟普通集群模式不一样的是,创建的 Queue,无论元数据还是Queue 里的消息都会存在于多个实例上,然后每次你写消息到 Queue 的时候,都会自动和多个实例的 Queue 进行消息同步。

这样设计,好处在于:任何一个机器宕机不影响其他机器的使用。

坏处在于:1. 性能开销太大:消息同步所有机器,导致网络带宽压力和消耗很重;2. 扩展性差:如果某个 Queue 负载很重,即便加机器,新增的机器也包含了这个 Queue 的所有数据,并没有办法线性扩展你的 Queue。

每日小结

今天我们复习了面试中常考的消息队列三个问题,你做到心中有数了么?

对了,如果你的朋友也在准备面试,请将这个系列扔给他,如果他认真对待,肯定会感谢你的!!

好了,今天就到这里,学废了的同学,记得三连,也会给我继续更新的动力。

0 人点赞