|
JAVA | 2021最全Java面试题及答案汇总
|
答:broker 是指一个或多个 erlang node 的逻辑分组,且 node 上运行着 RabbitMQ
应用程序。cluster 是在 broker 的基础之上,增加了 node 之间共享元数据的约束。
答:RAM node 仅将 fabric(即 queue、exchange 和 binding等 RabbitMQ基础构件)
相关元数据保存到内存中,但 disk node 会在内存和磁盘中均进行存储。RAM node 上唯一
会存储到磁盘上的元数据是 cluster 中使用的 disk node 的地址。要求在 RabbitMQ
cluster 中至少存在一个 disk node 。
答:可以认为是无限制,因为限制取决于机器的内存,但是消息过多会导致处理效率的
下降。
答:queue 具有自己的 erlang 进程;exchange 内部实现为保存 binding 关系的查找
表;channel 是实际进行路由工作的实体,即负责按照 routing_key 将 message 投递给
queue 。由 AMQP 协议描述可知,channel 是真实 TCP 连接之上的虚拟连接,所有 AMQP 命
令都是通过 channel 发送的,且每一个 channel 有唯一的 ID。一个 channel 只能被单独
一个操作系统线程使用,故投递到特定 channel 上的 message 是有顺序的。但一个操作系
统线程上允许使用多个 channel 。channel 号为 0 的 channel 用于处理所有对于当前
connection 全局有效的帧,而 1-65535 号 channel 用于处理和特定 channel 相关的帧。
AMQP 协议给出的 channel 复用模型如下
其中每一个 channel 运行在一个独立的线程上,多线程共享同一个 socket。
答:vhost 可以理解为虚拟 broker ,即 mini-RabbitMQ server。其内部均含有独立
的 queue、exchange 和 binding 等,但最最重要的是,其拥有独立的权限系统,可以做到
vhost 范围的用户控制。当然,从 RabbitMQ 的全局角度,vhost 可以作为不同权限隔离的
手段(一个典型的例子就是不同的应用可以跑在不同的 vhost 中)。
答:当你在单 node 上声明 queue 时,只要该 node 上相关元数据进行了变更,你就
会得到 Queue.Declare-ok 回应;而在 cluster 上声明 queue ,则要求 cluster 上的全
部 node 都要进行元数据成功更新,才会得到 Queue.Declare-ok 回应。另外,若 node 类
型为 RAM node 则变更的数据仅保存在内存中,若类型为 disk node 则还要变更保存在磁
盘上的数据。
答:是的。客户端感觉不到有何不同。
答:不能。第一,你无法控制所创建的 queue 实际分布在 cluster 里的哪个 node 上
(一般使用 HAProxy + cluster 模型时都是这样),这可能会导致各种跨地域访问时的常
见问题;第二,Erlang 的 OTP 通信框架对延迟的容忍度有限,这可能会触发各种超时,导
致业务疲于处理;第三,在广域网上的连接失效问题将导致经典的“脑裂”问题,而
RabbitMQ 目前无法处理(该问题主要是说 Mnesia)。
答:255 字节。
答:当消息被 RabbitMQ server 投递到 consumer 后,但 consumer 却通过
Basic.Reject 进行了拒绝时(同时设置 requeue=false),那么该消息会被放入“dead
letter”queue 中。该 queue 可用于排查 message 被 reject 或 undeliver 的原因。
答:该信令可用于 consumer 对收到的 message 进行 reject 。若在该信令中设置
requeue=true,则当 RabbitMQ server 收到该拒绝信令后,会将该 message 重新发送到下
一个处于消费状态的消费者处(理论上仍可能将该消息发送给当前 consumer)。若设置
requeue=false ,则 RabbitMQ server 在收到拒绝信令后,将直接将该 message 从 queue
中移除。
而 Basic.Nack 是对 Basic.Reject 的扩展,以支持一次拒绝多条 message 的能力。
答:首先,必然导致性能的下降,因为写磁盘比写 RAM 慢的多,message 的吞吐量可
能有 10 倍的差距。
其次,message 的持久化机制用在 RabbitMQ 的内置 cluster 方案时会出现问题。
矛盾点在于,若消息设置了 persistent 属性,但 queue 未设置 durable 属性,那么
当该 queue 的所属节点出现异常后,在未重建该queue前,发往该 queue 的 消息将被
blackholed;若 消息设置了 persistent 属性,同时 queue 也设置了 durable 属性,那
么当 queue 的所属节点异常且无法重启的情况下,则该 queue 无法在其他节点上重建,只
能等待其所属节点重启后,才能恢复该 queue 的使用,而在这段时间内发送给该 queue 的
message 将被 blackholed 。
所以,是否要对 message 进行持久化,需要综合考虑性能需要,以及可能遇到的问题。
若想达到 100,000 条/秒以上的消息吞吐量(单 RabbitMQ 服务器),则要么使用其他的方
式来确保 message 的可靠 delivery ,要么使用非常快速的存储系统以支持全持久化(例
如使用 SSD)。
另外一种处理原则是:仅对关键消息作持久化处理(根据业务重要程度),且应该保证
关键消息的量不会导致性能瓶颈。
答:cluster 是为了解决当 cluster 中的任意 node 失效后,producer 和 consumer
均可以通过其他 node 继续工作,即提高了可用性;另外可以通过增加 node 数量增加
cluster 的消息吞吐量的目的。cluster 本身不负责 message 的可靠性问题(该问题由
producer 通过各种机制自行解决);cluster 无法解决跨数据中心的问题(即脑裂问题)。
另外,在cluster 前使用 HAProxy 可以解决 node 的选择问题,即业务无需知道 cluster
中多个 node 的 ip 地址。可以利用 HAProxy 进行失效 node 的探测,可以作负载均衡。
下图为 HAProxy + cluster 的模型。
Mirrored queue 是为了解决使用 cluster 时所创建的 queue 的完整信息仅存在于单
一 node 上的问题,从另一个角度增加可用性。
Kafka将消息以topic为单位进行归纳
将向Kafka topic发布消息的程序成为producers.
将预订topics并消费消息的程序成为consumer.
Kafka以集群的方式运行,可以由一个或多个服务组成,每个服务叫做一个broker.
producers通过网络将消息发送到Kafka集群,集群向消费者提供消息
Kafa consumer消费消息时,向broker发出"fetch"请求去消费特定分区的消息,
consumer指定消息在日志中的偏移量(offset),就可以消费从这个位置开始的消息,
customer拥有了offset的控制权,可以向后回滚去重新消费之前的消息。
Kafka最初考虑的问题是,customer应该从brokes拉取消息还是brokers将消息推送到
consumer,也就是pull还push。在这方面,Kafka遵循了一种大部分消息系统共同的传统的
设计:producer将消息推送到broker,consumer从broker拉取消息
采用push模式,将消息推送到下游的consumer。这样做有好处也有坏处:由broker决定
消息推送的速率,对于不同消费速率的consumer就不太好处理了。消息系统都致力于让
consumer以最大的速率最快速的消费消息,但不幸的是,push模式下,当broker推送的速率
远大于consumer消费的速率时,consumer恐怕就要崩溃了。最终Kafka还是选取了传统的pull
模式。
Pull模式的另外一个好处是consumer可以自主决定是否批量的从broker拉取数据。Push
模式必须在不知道下游consumer消费能力和消费策略的情况下决定是立即推送每条消息还
是缓存之后批量推送。如果为了避免consumer崩溃而采用较低的推送速率,将可能导致一次
只推送较少的消息而造成浪费。Pull模式下,consumer就可以根据自己的消费能力去决定这
些策略
Pull有个缺点是,如果broker没有可供消费的消息,将导致consumer不断在循环中轮询,
直到新消息到达。为了避免这点,Kafka有个参数可以让consumer阻塞知道新消息到达。
(1).Kafka把topic中一个parition大文件分成多个小文件段,通过多个小文件段,就容
易定期清除或删除已经消费完文件,减少磁盘占用。
(2).通过索引信息可以快速定位message和确定response的最大大小。
(3).通过index元数据全部映射到memory,可以避免segment file的IO磁盘操作。
(4).通过索引文件稀疏存储,可以大幅降低index文件元数据占用空间大小。
(1).Kafka 持久化日志,这些日志可以被重复读取和无限期保留
(2).Kafka 是一个分布式系统:它以集群的方式运行,可以灵活伸缩,在内部通过复制
数据提升容错能力和高可用性
(3).Kafka 支持实时的流式处理
topic中的多个partition以文件夹的形式保存到broker,每个分区序号从0递增,且消
息有序 。
Partition文件下有多个segment(xxx.index,xxx.log)
segment 文件里的 大小和配置文件大小一致可以根据要求修改 默认为1g
如果大小大于1g时,会滚动一个新的segment并且以上一个segment最后一条消息的偏移
量命名
request.required.acks有三个值 0、 1、 -1
0:生产者不会等待broker的ack,这个延迟最低但是存储的保证最弱当server挂掉的时
候就会丢数据
1:服务端会等待ack值 leader副本确认接收到消息后发送ack但是如果leader挂掉后他
不确保是否复制完成新leader也会导致数据丢失
-1:同样在1的基础上 服务端会等所有的follower的副本受到数据后才会受到leader
发出的ack,这样数据不会丢失
生产者决定数据产生到集群的哪个partition中,每一条消息都是以(key,value)格
式,Key是由生产者发送数据传入。所以生产者(key)决定了数据产生到集群的哪个
partition。
数据传输的事务定义通常有以下三种级别:
(1)最多一次: 消息不会被重复发送,最多被传输一次,但也有可能一次不传输
(2)最少一次: 消息不会被漏发送,最少被传输一次,但也有可能被重复传输.
(3)精确的一次(Exactly once): 不会漏传输也不会重复传输,每个消息都传输被一
次而且仅仅被传输一次,这是大家所期望的
其实就是问问你消息队列都有哪些使用场景,然后你项目里具体是什么场景,说说你在
这个场景里用消息队列是什么?
面试官问你这个问题,期望的一个回答是说,你们公司有个什么业务场景,这个业务场
景有个什么技术挑战,如果不用 MQ 可能会很麻烦,但是你现在用了 MQ 之后带给了你很多的
好处。消息队列的常见使用场景,其实场景有很多,但是比较核心的有 3 个:解耦、异步、
削峰。
解耦:
A 系统发送个数据到 BCD 三个系统,接口调用发送,那如果 E 系统也要这个数据呢?那
如果 C 系统现在不需要了呢?现在 A 系统又要发送第二种数据了呢?而且 A 系统要时时刻刻
考虑 BCDE 四个系统如果挂了咋办?要不要重发?我要不要把消息存起来?
你需要去考虑一下你负责的系统中是否有类似的场景,就是一个系统或者一个模块,调
用了多个系统或者模块,互相之间的调用很复杂,维护起来很麻烦。但是其实这个调用是不
需要直接同步调用接口的,如果用 MQ 给他异步化解耦,也是可以的,你就需要去考虑在你
的项目里,是不是可以运用这个 MQ 去进行系统的解耦。
异步:
A 系统接收一个请求,需要在自己本地写库,还需要在 BCD 三个系统写库,自己本地写
库要 30ms,BCD 三个系统分别写库要 300ms、450ms、200ms。最终请求总延时是 30 + 300 +
450 + 200 = 980ms,接近 1s,异步后,BCD 三个系统分别写库的时间,A 系统就不再考虑
了。
削峰:
每天 0 点到 16 点,A 系统风平浪静,每秒并发请求数量就 100 个。结果每次一到 16 点
~23 点,每秒并发请求数量突然会暴增到 1 万条。但是系统最大的处理能力就只能是每秒钟
处理 1000 个请求啊。怎么办?需要我们进行流量的削峰,让系统可以平缓的处理突增的请
求。
优点上面已经说了,就是在特殊场景下有其对应的好处,解耦、异步、削峰。
缺点呢?
系统可用性降低
系统引入的外部依赖越多,越容易挂掉,本来你就是 A 系统调用 BCD 三个系统的接口就
好了,ABCD 四个系统好好的,没啥问题,你偏加个 MQ 进来,万一 MQ 挂了怎么办?MQ 挂了,
整套系统崩溃了,业务也就停顿了。
系统复杂性提高
硬生生加个 MQ 进来,怎么保证消息没有重复消费?怎么处理消息丢失的情况?怎么保
证消息传递的顺序性?
一致性问题
A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD
三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,你这数据就不一致了。
所以消息队列实际是一种非常复杂的架构,你引入它有很多好处,但是也得针对它带来
的坏处做各种额外的技术方案和架构来规避掉。
第一类原因
消息发送端应用的消息重复发送,有以下几种情况。
可以看到,通过消息发送端产生消息重复的主要原因是消息成功进入消息存储后,因为
各种原因使得消息发送端没有收到“成功”的返回结果,并且又有重试机制,因而导致重复。
第二类原因
消息到达了消息存储,由消息中间件进行向外的投递时产生重复,有以下几种情况。
消息被投递到消息接收者应用进行处理,处理完毕后应用出问题了,消息中间件不知道
消息处理结果,会再次投递。
消息被投递到消息接收者应用进行处理,处理完毕后网络出现问题了,消息中间件没有
收到消息处理结果,会再次投递。
消息被投递到消息接收者应用进行处理,处理时间比较长,消息中间件因为消息超时会
再次投递。
消息被投递到消息接收者应用进行处理,处理完毕后消息中间件出问题了,没能收到消
息结果并处理,会再次投递
消息被投递到消息接收者应用进行处理,处理完毕后消息中间件收到结果但是遇到消息
存储故障,没能更新投递状态,会再次投递。
可以看到,在投递过程中产生的消息重复接收主要是因为消息接收者成功处理完消息后,
消息中间件不能及时更新投递状态造成的。
如何解决重复消费
那么有什么办法可以解决呢?主要是要求消息接收者来处理这种重复的情况,也就是要
求消息接收者的消息处理是幂等操作。
什么是幂等性?
对于消息接收端的情况,幂等的含义是采用同样的输入多次调用处理函数,得到同样的
结果。例如,一个 SQL 操作
update stat_table set count= 10 where id =1
这个操作多次执行,id 等于 1 的记录中的 count 字段的值都为 10,这个操作就是幂等的,
我们不用担心这个操作被重复。
再来看另外一个 SQL 操作
update stat_table set count= count +1 where id= 1;
这样的 SQL 操作就不是幂等的,一旦重复,结果就会产生变化。
常见办法
因此应对消息重复的办法是,使消息接收端的处理是一个幂等操作。这样的做法降低了
消息中间件的整体复杂性,不过也给使用消息中间件的消息接收端应用带来了一定的限制和
门槛。
RabbitMQ
(1)生产者弄丢了数据
生产者将数据发送到 RabbitMQ 的时候,可能数据就在半路给搞丢了,因为网络啥的问
题,都有可能。此时可以选择用 RabbitMQ 提供的事务功能,就是生产者发送数据之前开启
RabbitMQ 事务(channel.txSelect),然后发送消息,如果消息没有成功被 RabbitMQ 接收
到,那么生产者会收到异常报错,此时就可以回滚事务(channel.txRollback),然后重试
发送消息;如果收到了消息,那么可以提交事务(channel.txCommit)。但是问题是,RabbitMQ
事务机制一搞,基本上吞吐量会下来,因为太耗性能。
所以一般来说,如果要确保 RabbitMQ 的消息别丢,可以开启 confirm 模式,在生产者
那里设置开启 confirm 模式之后,你每次写的消息都会分配一个唯一的 id,然后如果写入
了 RabbitMQ 中,RabbitMQ 会给你回传一个 ack 消息,告诉你说这个消息 ok 了。如果 RabbitMQ
没能处理这个消息,会回调你一个 nack 接口,告诉你这个消息接收失败,你可以重试。而
且你可以结合这个机制自己在内存里维护每个消息 id 的状态,如果超过一定时间还没接收
到这个消息的回调,那么你可以重发。
事务机制和 cnofirm 机制最大的不同在于,事务机制是同步的,你提交一个事务之后会
阻塞在那儿,但是 confirm 机制是异步的,你发送个消息之后就可以发送下一个消息,然后
那个消息 RabbitMQ 接收了之后会异步回调你一个接口通知你这个消息接收到了。
所以一般在生产者这块避免数据丢失,都是用 confirm 机制的。
(2)RabbitMQ 弄丢了数据
就是 RabbitMQ 自己弄丢了数据,这个你必须开启 RabbitMQ 的持久化,就是消息写入之
后会持久化到磁盘,哪怕是 RabbitMQ 自己挂了,恢复之后会自动读取之前存储的数据,一
般数据不会丢。除非极其罕见的是,RabbitMQ 还没持久化,自己就挂了,可能导致少量数
据会丢失的,但是这个概率较小。
设置持久化有两个步骤,第一个是创建 queue 和交换器的时候将其设置为持久化的,这
样就可以保证 RabbitMQ 持久化相关的元数据,但是不会持久化 queue 里的数据;第二个是
发送消息的时候将消息的 deliveryMode 设置为 2,就是将消息设置为持久化的,此时
RabbitMQ 就会将消息持久化到磁盘上去。必须要同时设置这两个持久化才行,RabbitMQ 哪
怕是挂了,再次重启,也会从磁盘上重启恢复 queue,恢复这个 queue 里的数据。
而且持久化可以跟生产者那边的 confirm 机制配合起来,只有消息被持久化到磁盘之
后,才会通知生产者 ack 了,所以哪怕是在持久化到磁盘之前,RabbitMQ 挂了,数据丢了,
生产者收不到 ack,你也是可以自己重发的。
哪怕是你给 RabbitMQ 开启了持久化机制,也有一种可能,就是这个消息写到了 RabbitMQ
中,但是还没来得及持久化到磁盘上,结果不巧,此时 RabbitMQ 挂了,就会导致内存里的
一点点数据会丢失。
(3)消费端弄丢了数据
RabbitMQ 如果丢失了数据,主要是因为你消费的时候,刚消费到,还没处理,结果进
程挂了,比如重启了,那么就尴尬了,RabbitMQ 认为你都消费了,这数据就丢了。
这个时候得用 RabbitMQ 提供的 ack 机制,简单来说,就是你关闭 RabbitMQ 自动 ack,
可以通过一个 api 来调用就行,然后每次你自己代码里确保处理完的时候,再程序里 ack
一把。这样的话,如果你还没处理完,不就没有 ack?那 RabbitMQ 就认为你还没处理完,
这个时候 RabbitMQ 会把这个消费分配给别的 consumer 去处理,消息是不会丢的。
Kafka
(1)消费端弄丢了数据
唯一可能导致消费者弄丢数据的情况,就是说,你那个消费到了这个消息,然后消费者
那边自动提交了 offset,让 kafka 以为你已经消费好了这个消息,其实你刚准备处理这个
消息,你还没处理,你自己就挂了,此时这条消息就丢咯。
大家都知道 kafka 会自动提交 offset,那么只要关闭自动提交 offset,在处理完之后
自己手动提交 offset,就可以保证数据不会丢。但是此时确实还是会重复消费,比如你刚
处理完,还没提交 offset,结果自己挂了,此时肯定会重复消费一次,自己保证幂等性就
好了。
生产环境碰到的一个问题,就是说我们的 kafka 消费者消费到了数据之后是写到一个内
存的 queue 里先缓冲一下,结果有的时候,你刚把消息写入内存 queue,然后消费者会自动
提交 offset。
然后此时我们重启了系统,就会导致内存 queue 里还没来得及处理的数据就丢失了
(2)kafka 弄丢了数据
这块比较常见的一个场景,就是 kafka 某个 broker 宕机,然后重新选举 partiton 的
leader 时。大家想想,要是此时其他的 follower 刚好还有些数据没有同步,结果此时 leader
挂了,然后选举某个 follower 成 leader 之后,他不就少了一些数据?这就丢了一些数据啊。
所以此时一般是要求起码设置如下 4 个参数:
给这个 topic 设置 replication.factor 参数:这个值必须大于 1,要求每个 partition
必须有至少 2 个副本。
在 kafka 服务端设置 min.insync.replicas 参数:这个值必须大于 1,这个是要求一个
leader 至少感知到有至少一个 follower 还跟自己保持联系,没掉队,这样才能确保 leader
挂了还有一个 follower 吧。
在 producer 端设置 acks=all:这个是要求每条数据,必须是写入所有 replica 之后,
才能认为是写成功了。
在 producer 端设置 retries=MAX(很大很大很大的一个值,无限次重试的意思):这
个是要求一旦写入失败,就无限重试,卡在这里了。
(3)生产者会不会弄丢数据
如果按照上述的思路设置了 ack=all,一定不会丢,要求是,你的 leader 接收到消息,
所有的 follower 都同步到了消息之后,才认为本次写成功了。如果没满足这个条件,生产
者会自动不断的重试,重试无限次。
从根本上说,异步消息是不应该有顺序依赖的。在 MQ 上估计是没法解决。要实现严格
的顺序消息,简单且可行的办法就是:保证生产者 - MQServer - 消费者是一对一对一的关
系。
RabbitMQ
如果有顺序依赖的消息,要保证消息有一个 hashKey,类似于数据库表分区的的分区 key
列。保证对同一个 key 的消息发送到相同的队列。A 用户产生的消息(包括创建消息和删除
消息)都按 A 的 hashKey 分发到同一个队列。只需要把强相关的两条消息基于相同的路由就
行了,也就是说经过 m1 和 m2 的在路由表里的路由是一样的,那自然 m1 会优先于 m2 去投递。
而且一个 queue 只对应一个 consumer。
Kafka
一个 topic,一个 partition,一个 consumer,内部单线程消费