消息队列面试题
1. 项目中为什么使用mq
(1) 解耦
- 一个模块, 调用其他多个模块的接口, 调用过程很复杂, 但又不是必须同步调用的时候, 就可以将这个调用最为一条消息放在mq里, 让被调用者取订阅相关的消息, 此时被调用的那几个模块就可以从mq里得知有一个模块调用我了
- 不用考虑对方调用是否成功, 超时, 失败重试等
- 主要是应用了消息队列的pub-sub模型
(2) 异步
- 不用mq的同步高延时场景:
还是以上场景, 模块A要分别调用模块B, 模块C, 模块D; 假设BCD三个模块分别处理3条sql, 5条sql, 4条sql, 且分别耗时200ms, 250ms, 700ms, 则模块A要想正常返回, 需要1150ms, 这在用户体验上十分不可接受(一般可接受范围在200ms以内) - 消峰
2. 引入消息队列后, 有什么坏处
- 系统可用性降低
- 系统与MQ之间的交互可能会产生问题
- 消息重复: 比如系统A本来只应该调用系统B一次, 但是由于系统A和MQ交互发生问题 ,使得A存了2此请求在mq中, 则B会被请求2次
- 消息丢失: 少了几次请求
- 消息乱序: 调用次序变了
- 消费者挂掉了: 系统BCD挂掉了, 系统A狂往队列中加数据
- 一致性问题
本来系统ABCD都成在能返回给用户成功, 加入mq后, 调用变成异步的了, 恰恰BCD中某个模块失败了, 但A却已经给用户正确的返回了
3. 如何保证mq的高可用性
(1) Rabbit mq高可用的做法
- 普通集群模式
数据只保存在某1个节点上, 而其它节点只负责响应request请求, 然后查找该请求的数据在那个节点上, 把数据从其他节点传输到本节点再返回给用户端
这种方式没有任何高可用性 - 镜像集群模式(数据副本, n台机器保存n分完整数据, 不存在partition, 只存在replication)
每个机器都是完整的queue的数据, 这几个节点的数据互为备份, 但是仍然不是分布式的queue, 只能用一台机器存储数据
(通过管理平台 增加一条镜像管理策略, 配置所有机器同部数据/某几台机器同步数据)
(2) kafka高可用性的做法
- 数据分区(partition)
- 数据备份(每个partition分为leader和follower)
4. mq consumer如何保证消息不被重复消费or如何保证消息的幂等性
(1) kafka
每条消息有一个offset, 可理解为每个消息的一个编号. consumer消费后返回offset(基于zookeeper里记录消费到了哪个offset) 或记录到kafka的一个特定队列中(__consumer_offsets
下面)
如何保证消息重复消费的幂等性
(1) 为什么会出现数据的重复消费
比如kafka中, 如果consumer在消费后就宕机了, 而且还没来得及到zookeeper中记录自己消费的offset
(2) 如何保证幂等性
比如consumer消费数据时要写入databse, 可以记录一个消息主键, 重复消费时会报错但不会重复保存
5. 如何处理使用mq后消息存在丢失的问题
-
rabbitmq
(1) 生产者把数据发往mq的时候丢了- rabbitmq可以通过开启事务实现避免生产者这端丢数据. 这种模式是同步机制, 只等这条消息发送完毕再发送下一条消息
channel.txSelect() try{ // 发送消息 }catch (异常){ channel.txrollback; // 再次重试发送数据 }
- 或者开启confirm机制.
- 先把channel设置为confirm机制,
- 然后发送消息,
- 发送成功后, rabbitmq接收消息后会回调生产者本地的回调接口, 如果mq保存消息成功回调ack()方法, 如果保存失败则回调unack()方法
这种方法是异步机制, 发送完不用等待回调就可以立刻发送下一条. 一般选用confirm机制
(2) mq存储的数据丢了
- rabbitmq开启持久化
- a. 先把queue的元数据开启持久化
- b. 把diliverymode设置为2, 让queue里的数据持久化
但此时仍存在一个风险就是, 数据存到内存后, 还没来得及存到磁盘, mq就挂了, 导致数据丢失
(3) 消费者处理失败导致消息丢失
消费者从mq收到消息后就挂掉了, 但还没来得及消费, 而mq却认为消费者取走就代表消费完毕
这种一般是开启了消费者的auto ack机制, 这样会让rabbitmq认为收到消息就是处理完毕, 此时若在处理过程中consumer宕机, 就会出现数据丢失问题
所以应该关闭autoack机制, 在consumer处理完消息后在手动进行ack - rabbitmq可以通过开启事务实现避免生产者这端丢数据. 这种模式是同步机制, 只等这条消息发送完毕再发送下一条消息
-
kafka
(1) kafka存储的数据丢失
因为kafka的partition通过leader和follower进行数据高可用, 有时数据只写到了leader的内存, 还没复制到follower时, leader就宕机了, 此后将follower选举成leader的时候数据就丢失了
解决办法: 设置4个参数- kafka服务期设置
- 为topic设置
replication.factors
大于1,
确保数据至少有2个副本 -
min.insync.replicats
:
kafka的leader会对follower进行心跳, 如果超时认为该follower宕机. 该参数必须大于等于2, 表示数据至少有1个follower可以和leader正常通信
- 为topic设置
- producer段设置
-
ack=all
:
每条数据必须写入所有的follower才认为是写入成功了 -
retries=MAX
:
一旦失败, 就自动无限重试的重发数据. 该参数设置后配合ack=all
, 使得当leader未写成功所有follower就宕机后leader切换, producer可以再次把数据发送给新leader
-
(2) kafka消费端丢数据
原理一样. kafka的自动提交offset机制, 导致consumer收到消息成功但处理过程中失败时造成数据丢失. 解决办法也是关闭自动提交offset, 来手动提交offset - kafka服务期设置
6. 如何保证消息的顺序性
(1) 什么时候rabbitMq的数据会出现顺序性问题
当producer只有1个, consumer有多个, 且mq以队列模式存在(每个consumer依次从mq中拿数据), 这种时候, 即使mq中的消息不乱序, 但由于consumer处理消息的速度不同, 最终产生结果的顺序会不同(比如consumer是插入数据到数据库的话)
解决办法: 需要保证顺序的消息都放到1个queue里, 同时只被1个consumer消费
(2) 什么时候kafka出现顺序问题
- kafka可以保证写入一个partition的数据时有序的, 所以可以在写入数据时指定一个key
producer.send(new ProducerRecord<>(topic,partition,key, msg));
比如: 设置订单id
为key, 这个key相关的数据全都会发送到1个partition中
- kafka的另一个原则:
1个partition只能被1个消费者消费. 即如果partition有3个, 而开启了4个consumer, 那有1个consumer会闲置 - 但是, 如果1个consumer内开启了多个线程处理数据, 则还会出现数据顺序的问题.
解决办法:
如果确实1个consumer需要多个线程并发加速处理数据, 那么可以为每个线程开启一个内存queue, 从kafka中得到数据时, 把key按照hash分发到不同的queue. 例如: 1个订单id的消息, 既可以保证生产者生产的数据在partition中有序, 也可以保证消费者在多线程下消费的顺序性
7. 有几百万消息积压在kafka消息队列怎么办
消息积压是因为consumer出问题或下线了, 重启恢复consumer后, 首先创建1个新的topic, 让这个新的topic拥有的partition数量是原来的10倍, 修改原来的consumer代码逻辑为: 把老topic的数据消费后就存入新的topic. 然后再启动10倍的consumer消费这个新topic下的消息.
为什么不能直接启动10倍的consumer消费老topic?
因为kafka里1个partition只能被1个consumer消费, 老topic的partition数量不变的话, 即使新增consumer, 那些consumer也会被闲置
8. 如果让你来开发一个mq中间件, 如何设计这个架构
面试官心理分析:
其实聊到这个问题,一般面试官要考察两块:
你有没有对某一个消息队列做过较为深入的原理的了解,或者从整体了解把握住一个消息队列的架构原理。
看看你的设计能力,给你一个常见的系统,就是消息队列系统,看看你能不能从全局把握一下整体架构设计,给出一些关键点出来。
说实话,问类似问题的时候,大部分人基本都会蒙,因为平时从来没有思考过类似的问题,大多数人就是平时埋头用,从来不去思考背后的一些东西。类似的问题,比如,如果让你来设计一个 Spring 框架你会怎么做?如果让你来设计一个 Dubbo 框架你会怎么做?如果让你来设计一个 MyBatis 框架你会怎么做?
回答思路:
首先, 让mq支持数据的分布式存储, 其次支持高可用(数据副本), 支持数据的零丢失(数据副本的一致性问题)
rabbitMQ基本概念
- 概念:
- Brocker:消息队列服务器实体。
- Exchange:消息交换机,指定消息按什么规则,路由到哪个队列。
- Queue:消息队列,每个消息都会被投入到一个或者多个队列里。
- Binding:绑定,它的作用是把exchange和queue按照路由规则binding起来。
- Routing Key:路由关键字,exchange根据这个关键字进行消息投递。
- Vhost:虚拟主机,一个broker里可以开设多个vhost,用作不同用户的权限分离。
- Producer:消息生产者,就是投递消息的程序。
- Consumer:消息消费者,就是接受消息的程序。
- Channel:消息通道,在客户端的每个连接里,可建立多个channel,每个channel代表一个会话任务。
- 消息队列的使用过程大概如下:
- 消息接收
- 客户端连接到消息队列服务器,打开一个channel。
- 客户端声明一个exchange,并设置相关属性。
- 客户端声明一个queue,并设置相关属性。
- 客户端使用routing key,在exchange和queue之间建立好绑定关系。
- 消息发布
- 客户端投递消息到exchange。
- xchange接收到消息后,就根据消息的key和已经设置的binding,进行消息路由,将消息投递到一个或多个队列里。
- 消息接收
- AMQP 里主要要说两个组件:
Exchange 和 Queue
绿色的 X 就是 Exchange ,红色的是 Queue ,这两者都在 Server 端,又称作 Broker
这部分是 RabbitMQ 实现的,而蓝色的则是客户端,通常有 Producer 和 Consumer 两种类型。 - Exchange通常分为四种:
- fanout:该类型路由规则非常简单,会把所有发送到该Exchange的消息路由到所有与它绑定的Queue中,相当于广播功能
- direct:该类型路由规则会将消息路由到binding key与routing key完全匹配的Queue中
- topic:与direct类型相似,只是规则没有那么严格,可以模糊匹配和多条件匹配
- headers:该类型不依赖于routing key与binding key的匹配规则来路由消息,而是根据发送的消息内容中的headers属性进行匹配
kafka基本概念:
topic: 队列, 相当于rabbitmq的queue
partition: 数据的分片(分布式存储数据), rabbitmq中没有分布式存储的概念