1. 为什么要用MQ?
解耦,异步,削峰
2. MQ的优点和缺点?
优点:
解耦、异步、削峰
缺点:
1. 系统可用性降低。 外部依赖越多,越容易挂。如果MQ挂了,怎么处理?
2. 系统复杂度提高。 怎么保证没有重复消费,怎么处理消费丢失,怎么保证消费顺序等等。
3. 一致性问题。通过MQ的消息,如果一个系统时子系统A通过MQ传消息给子系统BCD,某个处理需要ABCD处理都完成才算成功。如果子系统D失败,怎么办。
3. 几种MQ的技术选型和适用场景?
ActiveMQ、RabbitMQ、RocketMQ、Kafka各自优缺点。
ActiveMQ: 吞吐量万级,大家用的不多,没有大规模吞吐量场景的业内实践。
RabbitMQ: 吞吐量万级,MQ功能完善,管理界面好,可做集群但不是分布式,开源社区活跃,国内使用公司较多。
缺点Erlang国内少,很少看懂源码,公司对这个掌控很弱,只能依赖开源社区。
RocketMQ: 单机吞吐量10万级,topic可以达到几百,几千个,分布式架构,MQ功能完善,社区活跃度还不错,可以支持业务复杂吞吐量高的MQ场景。Java源码,可以阅读源码,可定制。
缺点:万一项目被阿里抛弃,没人维护。
Kafka: 单机吞吐量10万级,功能只能支持简单MQ,在大数据实时计算以及日志采集被大规模使用。易于扩展,加机器。
总结:
ActiveMQ: 不推荐。
中小公司,技术实力一般,技术挑战不高,用RabbitMQ。
中大公司,基础架构研发实力强,Java方面的,用RocketMQ。
大数据,Kafka业界标准。
4. 如何保证消息队列的高可用?
4.1. RabbitMQ高可用
RabbitMQ 三种模式:单机,普通集群, 镜像集群模式
单机:
做demo,学习用
普通集群:
多台机器启动多个rabbitmq实例,每个机器一个,但是创建的queue只会放在一个rabbitmq实例上,每个实例都同步queue的元数据。
元数据:配置信息,其他节点会拉queue的元数据。
这个方式很麻烦,不是分布式,是普通集群。会导致消费者每次随机连接一个实例后拉取数据,可能只有元数据,没有实际数据。
唯一优点,可以从多个机器消费数据,提高消费者吞吐量。
缺点:1. 可能会在rabbitmq集群内部产生大量数据传输;2. 可用性没有保证,如果queue的数据所在节点挂了,数据也就丢失了。
镜像集群:
queue的元数据和实际数据会在每个节点,任何一个节点宕机,其他节点还有queue的完整数据,别的消费者可以在其他节点消费。
缺点:不是分布式,如果queue的数据量大,大到机器上的容量无法容纳是,怎么办?
怎么开启镜像集群:管理平台新增一个策略,要求数据同步到所有节点,也可以同步指定节点,创建queue时应用这个策略。
4.2 Kafka高可用
天然分布式,多个broker组成,每个broker一个节点,创建一个topic,划分为多个partition,每个partition可存在与不同的broker,每个partition放一部分数据。
Kafka 0.8以前没有HA。之后每个节点有relpica副本。选举leader,写leader时会将数据同步到follower。生产者消费者都是在leader上。
一个节点挂了时,kafka自动感知leader挂了,会将follower选择为leader。生产者消费者会读写新的leader。
5. 如何保证消息不被重复消费?(保证消息消费时的幂等性)
5.1. kafka可能出现重复消息的问题
按照数据进入kafka的顺序,kafka会给每条消息分配一个offset(如offset=N),代表这个数据的序号。
消费者从kafka消费时候,是按照这个顺序去消费的。消费者会提交offset,告诉kafka我已经消费到offset=N的数据了。Zookeeper会记录消费者当前消费到offset=N的那条消息。
消费者如果重启,告诉kafka把上次消费到的那条数据之后给传递过来。
坑:消费者不是消费完一条数据就提交offset,而是定时定期提交一次offset。假设消费者提交之前挂了,就没有提交offset。重启后kafka会把上一次offset的数据发来,就会有重复数据。
5.2. RabbitMQ类似
解决方案:
1. 生产者发送的每条消息包含一个全局唯一ID, 消费者拿到消息先判断用这个ID去Redis(或者内存的Set)查一下,
如果不存在,就处理(插入DB等等),然后再把这个ID写入Redis。如果存在的,就不处理了,保证了幂等性。
2. 还有基于数据库唯一键也可以。如果重复数据插入会报错。
6. 如何处理消息丢失的问题?
丢数据,MQ分两种,要么是MQ自动弄丢了,要么是消费的时候丢了。
6.1. 对于RabbitMQ消息丢失: (3种丢失情况)
生产者 -----> MQ -----> 消费者
6.1.1 生产者写消息时丢失
解决方案:1. 选择用RabbitMQ的事务功能。生产者发送之前开启RabbitMQ事务(channel.txSelect), 如果消息没有被RabbitMQ成功收到,那么生产者会收到异常报错,就可以回滚,重试发送消息。如果RabbitMQ收到消息,就提交事务。
// 开启事务 channel.txSelect try { // 这里发送消息 } catch (Exception e) { channel.txRollback // 这里再次重发这条消息 } // 提交事务 channel.txCommit
这种事务的缺陷:生产者发送的消息会同步阻塞等待RabbitMQ的成功失败,吞吐量下降。
2. 生产者先把Channel设置为Confirm模式,发送消息后不管。RabbitMQ如果收到消息,会回调生产者本地接口,通知消息成功与否。失败通知重发。
一般用Confirm模式,异步,不阻塞,吞吐量高。
6.1.2. RabbitMQ接收消息后消费者还没来得及消费时, MQ故障消息丢失:
解决方案:设置持久化。第一,创建queue时设置为持久化,保证rabbitmq持久化queue的元数据,但是不会持久化queue的实际数据。
第二,发送消息时将消息的deliveryMode设置为2(持久化消息)。这样才能将消息持久化到磁盘。
还有一种小概率情况,即使开启了持久化,消息已经写入rabbitmq内存,但还没有来得及写入到磁盘上挂了,会导致内存里的数据丢失。
6.1.3. 消费者消费到了消息但是还没来得及处理时挂了,MQ以为消费者已经处理完:
问题产生原因:消费者打开了autoAck,消费者收到数据后会自动通知RabbitMQ已经消费了,这时如果消费者宕机了,没有处理完,会丢失这条消息,
但是rabbitmq以为这个消息已经处理了。
解决方案:消费者关闭autoAck,自己确定处理完消息后才发送ack到Rabbit。
6.2. 对于Kafka消息丢失
6.2.1. 消费端丢失消息
和RabbitMQ类似,kafka会自动提交offset,那么关闭自动提交offset,在处理完之后自己手动提交offset,可以保证数据不丢失。
6.2.2. kafka丢失消息
产生原因:生产者发送消息给某个broker,这个leader还没有来得及把数据同步到follower就挂了,这个之后选举产生的leader就少了这条数据。
设置4个参数:
1. topic设置replication.factor > 1。要求每个partition必须有至少2个副本
2. kafka服务端min.insync.replicas > 1。要求一个leader至少感知一个follower与自己保持联系。
3. 生产者 ack=all 。要求每条数据必须写入所有replica之后,才能认为时写入成功。
4. 生产者retries=MAX。一旦写入失败 ,无限重试,阻塞写入。
6.2.3. 生产者会不会弄丢消息
如果丢了,会自动重发,不会丢。
7. 如何保证消息的顺序性?
7.1. RabbitMQ: 一个queue,多个消费者,顺序乱了
解决方案:拆分多个queue,需要按顺序处理的消费放入一个queue,由一个消费处理,消费者内部用内存队列排队,然后处理,比如顺序插入库
7.2. kafka: 一个topic,一个partition,一个消费者,但一个消费者内部多线程,顺序乱了
解决方案:首先kafka中生产者写入一个partition中的数据一定是有序的。
生产者写数据时指定一个key,比如订单id为key,这个订单相关数据一定会被分发到一个partition上,而且这个parttion中数据一定时有序的。
消费者从partition中取数据一定时有序的。
8. 如何解决消息队列的延时以及过去失效问题?消息队列满了以后怎么处理? 有几百万消息持续积压几个小时,怎么解决?
8.1. 第一个坑,大量消息在MQ里积压几个小时还没有解决?
临时紧急扩容。
1. 先修复消费者程序的问题,确保恢复消费者速度
2. 临时建好原先10倍或20倍queue的数量
3. 写临时分发数据的消费者程序,让这个程序去消费积压消息,均匀轮询到临时建好的10倍queue
4. 用临时创建的10倍机器来部署修复后的消费者程序,每一个消费者消费一个临时queue的数据
5. 等快速消费完积压数据之后,恢复原先的部署架构,重新利用原先的消费者机器来消费消息
8.2. 第二个坑,如果RabbitMQ设置了过期时间(TTL),(实际中应该禁止),消息在queue中积压超过过期时间后被RabbitMQ丢弃了,怎么办?
批量重导。等高峰期过后,写程序将丢失的数据一点点查出来(总有日志可以追踪),重新写入MQ,重新走一遍流程。
8.3. 第三个坑,消息积压MQ里,MQ快写满磁盘,怎么办?
写个临时程序,接入数据来消费,消费一个丢一个,快速消费积压的消息,减轻磁盘容量压力。
峰值过后,采用上述7.2的方案,到低峰期再补数据重走流程。
9. 如果让你来开发一个消息队列,该如何进行架构设计?说一下思路。
考点:1. 有没有对某个消息队列原理做过较深入了解,或整体把握一个MQ的架构
2. 查看设计能力,能不能从全局的把握一下整体架构设计,给出一些关键点
类似问题:让你来设计一个Spring框架/Mybatis框架/Dubbo框架/XXX框架,你怎么做?
1. 考虑可扩展,可伸缩。
设计分布式MQ,参考kafka,每个broker是机器上面一个节点,一个topic拆分多个partition, 每个partition放一台机器上,只放topic一部分数据。
资源不够就给topic增加partition, 做数据迁移,增加机器,扩容提高吞吐量。
2. 考虑消息持久化。
落磁盘才能保证进程挂了数据不丢。落磁盘怎么写?参考kafka,顺序写,就没有随机写的寻址开销,顺序写性能更高。
3. 考虑高可用。
参考kafka,多副本,leader 和follower,只有leader读写,follower同步leader数据,leader挂了选follower。
4. 考虑支持数据0丢失。
参考kafka如何设计数据0丢失。要求每个partition必须有至少2个副本。要求一个leader至少感知一个follower与自己保持联系。
要求每条数据必须写入所有replica之后,才能认为时写入成功。一旦写入失败 ,无限重试,阻塞写入。
5. 考虑消息顺序。
消费者如果多线程处理,加入内存队列分发,既能保证顺序又能增加吞吐量。
参考资料:
《互联网Java进阶面试训练营》的笔记 -- 中华石杉