• 消息中间件面试常见问题


    说说你们公司线上生产环境用的是什么消息中间件?  

    为什么要使用MQ?

    多个MQ如何选型?

    1. 使用的Rocket MQ  。
    2. 因为项目比较大,做了分布式系统,所有远程服务调用请求都是同步调用,经常出问题,所以引入MQ(解耦)。批量操作需要异步操作,项目中批量打印快递单,批量发货。(解耦,异步,削峰 )
    3. 用的RocketMQ,1.用java开发的,易于开发。2.能够应对大流量场景,经过双十一的检验 
    4. 各种MQ对比,小项目:ActiveMQ  大项目:RocketMQ 或者Kafka ,RabbitMQ 。RabbitMQ :使用erlang开发,延迟低。Kafka :Scala 开发,面向日志功能丰富。ActiveMQ :java开发,简单,稳定。

    Rocket MQ由哪些角色组成,每个角色的作用和特点是什么?

    Rocket MQ 中Topic 和ActiveMQ有什么区别?

    Rocket MQ Broker中的消息被消费后会立即删除吗?

    1. 生产者、消费者、Broker、NameServer     NameServer :注册中心,无状态:启动后不与其他节点有通信,当作路由,一个broker启动后会向所有NameServer发送注册信息,NameServer维护一个动态列表。
    2. ActiveMQ :中有destination的概念,即消息目的地 ,destination分为两类, topic 广播消息,queue 队列消息
    3. RocketMQ :是一组 MessageMQ的集合 ,一条消息是广播消息还是队列消息由客户端消费决定

    RocketMQ消费模式有几种?

    消费消息时使用的是push还是pull?

    为什么要主动拉取消息而不使用事件监听方式?

    说一说几种常用的消息同步机制?

    集群消费

    一组为单位,一组cosumer同时消费一个topic,多个组组成一个集群。

    一组consumer 同时消费一个topic,可以分配消费负载均衡策略分配consumer对应消费topic下的哪些queue。

    多个group同时消费一个topic时,每个group 都会消费到数据

    一条消息只会被一个group中的consumer消费

    广播消费

    消息将一个Consumer Group 下的各个Consumer 实例都消费一遍,即使这些consumer属于同一个 Consumer Group ,消息也会被 Conusumer Group 中的每个Consumer都消费一次

    Broker 中的消息被消费后不会被立即删除,每条消息都会持久化到CommitLog中,每个consumer连接到broker后会维持消费进度信息,当消息消费后只是当前consumer的消费进度(commitLog 的 offeset) 更新了。

    在刚开始的时候就要决定使用那种方式消费

    两种 :DefaultLitePullConsumerImpl 拉  DefaultMQPushConsumerImpl 推 

    两个都实现了MQConsumerInner 接口, 实际底层都实现了长轮询机制,即拉取方式  

    RocketMQ 消息重试机制

    转自:https://blog.csdn.net/belongtocode/article/details/104310781

    消息重试分为两种:Producer发送消息的重试 和 Consumer消息消费的重试。

    一、Producer端重试

    Producer端重试是指: Producer往MQ上发消息没有发送成功,比如网络原因导致生产者发送消息到MQ失败。

    部分源码解析:

     1    /**
     2      * 说明 抽取部分代码
     3      */
     4     private SendResult sendDefaultImpl(Message msg, final CommunicationMode communicationMode, final SendCallback sendCallback, final long timeout) {
     5         
     6         //1、获取当前时间
     7         long beginTimestampFirst = System.currentTimeMillis();
     8         long beginTimestampPrev ;
     9         //2、去服务器看下有没有主题消息
    10         TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
    11         if (topicPublishInfo != null && topicPublishInfo.ok()) {
    12             boolean callTimeout = false;
    13             //3、通过这里可以很明显看出 如果不是同步发送消息 那么消息重试只有1次
    14             int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
    15             //4、根据设置的重试次数,循环再去获取服务器主题消息
    16             for (times = 0; times < timesTotal; times++) {
    17                 MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
    18                 beginTimestampPrev = System.currentTimeMillis();
    19                 long costTime = beginTimestampPrev - beginTimestampFirst;
    20                 //5、前后时间对比 如果前后时间差 大于 设置的等待时间 那么直接跳出for循环了 这就说明连接超时是不进行多次连接重试的
    21                 if (timeout < costTime) {
    22                     callTimeout = true;
    23                     break;
    24 
    25                 }
    26                 //6、如果超时直接报错
    27                 if (callTimeout) {
    28                     throw new RemotingTooMuchRequestException("sendDefaultImpl call timeout");
    29                 }
    30         }
    31     }

    通过这段源码很明显可以看出以下几点

    1. 如果是异步发送 那么重试次数只有1次
    2. 对于同步而言,超时异常也是不会再去重试
    3. 如果发生重试是在一个for 循环里去重试,所以它是立即重试而不是隔一段时间去重试。

    实践出真知!!!

    二、 Consumer端重试

    消费端比较有意思,而且在实际开发过程中,我们也更应该考虑的是消费端的重试。

    消费者端的失败主要分为2种情况Exception 和 Timeout

    1、Exception

     1 @Slf4j
     2 @Component
     3 public class Consumer {
     4     /**
     5      * 消费者实体对象
     6      */
     7     private DefaultMQPushConsumer consumer;
     8     /**
     9      * 消费者组
    10      */
    11     public static final String CONSUMER_GROUP = "test_consumer";
    12     /**
    13      * 通过构造函数 实例化对象
    14      */
    15     public Consumer() throws MQClientException {
    16         consumer = new DefaultMQPushConsumer(CONSUMER_GROUP);
    17         consumer.setNamesrvAddr("47.99.203.55:9876;47.99.203.55:9877");
    18         //订阅topic和 tags( * 代表所有标签)下信息
    19         consumer.subscribe("topic_family", "*");
    20         //注册消费的监听 并在此监听中消费信息,并返回消费的状态信息
    21         consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
    22             //1、获取消息
    23             Message msg = msgs.get(0);
    24             try {
    25                 //2、消费者获取消息
    26                 String body = new String(msg.getBody(), "utf-8");
    27                 //3、获取重试次数
    28                 int count = ((MessageExt) msg).getReconsumeTimes();
    29                 log.info("当前消费重试次数为 = {}", count);
    30                 //4、这里设置重试大于3次 那么通过保存数据库 人工来兜底
    31                 if (count >= 2) {
    32                     log.info("该消息已经重试3次,保存数据库。topic={},keys={},msg={}", msg.getTopic(), msg.getKeys(), body);
    33                     return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    34                 }
    35                 //直接抛出异常
    36                 throw new Exception("=======这里出错了============");
    37                 //return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    38             } catch (Exception e) {
    39                 e.printStackTrace();
    40                 return ConsumeConcurrentlyStatus.RECONSUME_LATER;
    41             }
    42         });
    43         //启动监听
    44         consumer.start();
    45     }
    46 }

    这里的代码意思很明显: 主动抛出一个异常,然后如果超过3次,那么就不继续重试下去,而是将该条记录保存到数据库由人工来兜底

    看下运行结果

    注意 消费者和生产者的重试还是有区别的,主要有两点

    1、默认重试次数:Product默认是2次,而Consumer默认是16次。

    2、重试时间间隔:Product是立刻重试,而Consumer是有一定时间间隔的。它照1S,5S,10S,30S,1M,2M····2H进行重试。
    3、Product在异步情况重试失效,而对于Consumer在广播情况下重试失效。

    2、Timeout

    说明 这里的超时异常并非真正意义上的超时,它指的是指获取消息后,因为某种原因没有给RocketMQ返回消费的状态,即没有return ConsumeConcurrentlyStatus.CONSUME_SUCCESS 或 return ConsumeConcurrentlyStatus.RECONSUME_LATER

    那么 RocketMQ会认为该消息没有发送,会一直发送。因为它会认为该消息根本就没有发送给消费者,所以肯定没消费。

    做这个测试很简单。

     1         //1、消费者获得消息
     2         String body = new String(msg.getBody(), "utf-8");
     3         //2、获取重试次数
     4         int count = ((MessageExt) msg).getReconsumeTimes();
     5         log.info("当前消费重试次数为 = {}", count);
     6         //3、这里睡眠60秒
     7         Thread.sleep(60000);
     8        log.info("休眠60秒 看还能不能走到这里。topic={},keys={},msg={}", msg.getTopic(), msg.getKeys(), body);
     9         //返回成功
    10         return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;

    当获得 当前消费重试次数为 = 0 后 , 关掉该进程。再重新启动该进程,那么依然能够获取该条消息

    consumer消费者 当前消费重试次数为 = 0
    休眠60秒 看还能不能走到这里。topic=topic_family,keys=1a2b3c4d5f,msg=小小今年3岁

    其他理解

    首先,我们需要明确,只有当消费模式为 MessageModel.CLUSTERING(集群模式) 时,Broker 才会自动进行重试,对于广播消息是不会重试的。

    集群消费模式下,当消息消费失败,RocketMQ 会通过消息重试机制重新投递消息,努力使该消息消费成功。

    当消费者消费该重试消息后,需要返回结果给 broker,告知 broker 消费成功(ConsumeConcurrentlyStatus.CONSUME*SUCCESS)或者需要重新消费(ConsumeConcurrentlyStatus.RECONSUME*LATER)

    这里有个问题,如果消费者业务本身故障导致某条消息一直无法消费成功,难道要一直重试下去吗?

    答案是显而易见的,并不会一直重试。

    事实上,对于一直无法消费成功的消息,RocketMQ 会在达到最大重试次数之后,将该消息投递至死信队列。然后我们需要关注死信队列,并对该死信消息业务做人工的补偿操作。

    那如何返回消息消费失败呢?

    RocketMQ 规定,以下三种情况统一按照消费失败处理并会发起重试。

    1. 业务消费方返回 ConsumeConcurrentlyStatus.RECONSUME_LATER
    2. 业务消费方返回null
    3. 业务消费方主动/被动抛出异常

    前两种情况较容易理解,当返回 ConsumeConcurrentlyStatus.RECONSUME_LATER或者 null时,broker 会知道消费失败,后续就会发起消息重试,重新投递该消息。

    注意 对于抛出异常的情况,只要我们在业务逻辑中显式抛出异常或者非显式抛出异常,broker 也会重新投递消息,如果业务对异常做了捕获,那么该消息将不会发起重试。因此对于需要重试的业务,消费方在捕获异常的时候要注意返回 ConsumeConcurrentlyStatus.RECONSUME*LATERnull 并输出异常日志,打印当前重试次数。(推荐返回ConsumeConcurrentlyStatus.RECONSUME*LATER

    死信的业务处理方式

    默认的处理机制中,如果我们只对消息做重复消费,达到最大重试次数之后消息就进入死信队列了。

    我们也可以根据业务的需要,定义消费的最大重试次数,每次消费的时候判断当前消费次数是否等于最大重试次数的阈值。

    如:重试三次就认为当前业务存在异常,继续重试下去也没有意义了,那么我们就可以将当前的这条消息进行提交,返回 broker 状态ConsumeConcurrentlyStatus.CONSUME_SUCCES,让消息不再重发,同时将该消息存入我们业务自定义的死信消息表,将业务参数入库,相关的运营通过查询死信表来进行对应的业务补偿操作。

    RocketMQ 的处理方式为将达到最大重试次数(16 次)的消息标记为死信消息,将该死信消息投递到 DLQ 死信队列中,业务需要进行人工干预。实现的逻辑在 SendMessageProcessor的 consumerSendMsgBack方法中,大致思路为首先判断重试次数是否超过 16 或者消息发送延时级别是否小于 0,如果已经超过 16 或者发送延时级别小于 0,则将消息设置为新的死信。死信 topic 为:%DLQ%+consumerGroup

    我们接着看一下死信的源码实现机制。

     1 private RemotingCommand consumerSendMsgBack(final ChannelHandlerContext ctx, final RemotingCommand request)
     2             throws RemotingCommandException {
     3             final RemotingCommand response = RemotingCommand.createResponseCommand(null);
     4             final ConsumerSendMsgBackRequestHeader requestHeader =
     5                 (ConsumerSendMsgBackRequestHeader)request.decodeCommandCustomHeader(ConsumerSendMsgBackRequestHeader.class);
     6 
     7             ......
     8 
     9             // 0.首先判断重试次数是否大于等于 16,或者消息延迟级别是否小于 0
    10             if (msgExt.getReconsumeTimes() >= maxReconsumeTimes
    11                 || delayLevel < 0) {
    12                 // 1. 如果满足判断条件,设置死信队列 topic= %DLQ%+consumerGroup
    13                 newTopic = MixAll.getDLQTopic(requestHeader.getGroup());
    14                 queueIdInt = Math.abs(this.random.nextInt() % 99999999) % DLQ_NUMS_PER_GROUP;
    15 
    16                 topicConfig = this.brokerController.getTopicConfigManager().createTopicInSendMessageBackMethod(newTopic,
    17                     DLQ_NUMS_PER_GROUP,
    18                     PermName.PERM_WRITE, 0
    19                 );
    20                 if (null == topicConfig) {
    21                     response.setCode(ResponseCode.SYSTEM_ERROR);
    22                     response.setRemark("topic[" + newTopic + "] not exist");
    23                     return response;
    24                 }
    25             } else {
    26                 // 如果延迟级别为 0,则设置下一次延迟级别为 3+当前重试消费次数,达到时间衰减效果
    27                 if (0 == delayLevel) {
    28                     delayLevel = 3 + msgExt.getReconsumeTimes();
    29                 }
    30 
    31                 msgExt.setDelayTimeLevel(delayLevel);
    32             }
    33 
    34             MessageExtBrokerInner msgInner = new MessageExtBrokerInner();
    35             msgInner.setTopic(newTopic);
    36             msgInner.setBody(msgExt.getBody());
    37             msgInner.setFlag(msgExt.getFlag());
    38             MessageAccessor.setProperties(msgInner, msgExt.getProperties());
    39             msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgExt.getProperties()));
    40             msgInner.setTagsCode(MessageExtBrokerInner.tagsString2tagsCode(null, msgExt.getTags()));
    41 
    42             msgInner.setQueueId(queueIdInt);
    43             msgInner.setSysFlag(msgExt.getSysFlag());
    44             msgInner.setBornTimestamp(msgExt.getBornTimestamp());
    45             msgInner.setBornHost(msgExt.getBornHost());
    46             msgInner.setStoreHost(this.getStoreHost());
    47             msgInner.setReconsumeTimes(msgExt.getReconsumeTimes() + 1);
    48 
    49             String originMsgId = MessageAccessor.getOriginMessageId(msgExt);
    50             MessageAccessor.setOriginMessageId(msgInner, UtilAll.isBlank(originMsgId) ? msgExt.getMsgId() : originMsgId);
    51 
    52             // 3.死信消息投递到死信队列中并落盘
    53             PutMessageResult putMessageResult = this.brokerController.getMessageStore().putMessage(msgInner);
    54             ......
    55             return response;
    56         }

    我们总结一下死信的处理逻辑:

    1. 首先判断消息当前重试次数是否大于等于 16,或者消息延迟级别是否小于 0
    2. 只要满足上述的任意一个条件,设置新的 topic(死信 topic)为:%DLQ%+consumerGroup
    3. 进行前置属性的添加
    4. 将死信消息投递到上述步骤 2 建立的死信 topic 对应的死信队列中并落盘,使消息持久化。
  • 相关阅读:
    Linux Context , Interrupts 和 Context Switching 说明
    zabbix监控cpu jumps
    国际时区 TimeZone ID列表
    onenote无法更新,提示无法流式传输、无法登陆等问题解答
    Laravel Lumen 数组操作
    ApiDoc 和 Swagger 接口文档
    现代 PHP 新特性系列
    php 流(Stream)
    laravel Lumen邮箱发送配置
    钉钉开发验证登录功能
  • 原文地址:https://www.cnblogs.com/ScarecrowAnBird/p/14399267.html
Copyright © 2020-2023  润新知