RocketMQ的可靠性

RocketMQ的可靠性

学习目标

  • 了解 RocketMQ 可靠性原理实现

主要内容

一、消息队列简介

二、RocketMQ 的高可用

三、RocketMQ 实践

一、消息队列介绍

1.消息队列是什么?

消息队列是一种基于队列与消息传递技术,用于在分布式系统中进行异步通信的服务。不用知道具体的服务在哪,如何调用。你要做的只是将该发送的消息,向约定好的地址进行发送,任务就完成了。对应的服务能监听到你发送的消息,进行后续的操作。这就是消息队列最大的特点,将同步操作转为异步处理,将多服务共同操作转为职责单一的单服务操作,做到了服务间的解耦。

2.RocketMQ的出现和区别

RocketMQ是阿里开源的消息中间件,它是纯Java开发,具有高吞吐量、高可用性、适合大规模分布式系统应用的特点。RocketMQ实现思路起源于Kafka,但并不是Kafka的一个Copy,它对消息的可靠传输及事务性做了优化

二、RocketMQ的特性原理

1.消息可靠的含义

  • 为什么会导致消息丢失

    rockectMQ集群部署形式,在不可靠网络上通信,生成者发送消息,消费者拉取消息消费,消息持久化三个过程都有可能造成消息丢失。

    在这里插入图片描述

  • 消息丢失的情况会导致什么错误

    1. 事件消息,例如状态的更新,会导致操作未执行

    2. 事务消息,导致业务数据不一致,例如在金融系统中,丢失支付事务消息可能导致资金不一致

    3. 通知消息丢失,订单状态,新消息通知等。消息丢失可能导致用户在界面上看到的信息不准确或不及时

    4. 定时任务消息丢失,定时任务无法按预期执行

    5.日志消息丢失,排查错误困难

    1. rockectMQ发送怎么保证的消息可靠

      1. 实现消息发送高可用的两种机制

      1)消息发送重试机制

      RocketMQ在消息发送时如果出现异常,捕获异常后重试。

      2)故障规避机制

      当消息第一次发送失败时,如果下一次消息还是发送到刚刚失败的Broker上,其消息发送大概率还是会失败,因此为了保证重试的可靠性,在重试时会尽量避开刚刚接收失败的Broker,而是选择其他Broker上的队列进行发送,从而提高消息发送的成功率。

      2. 消息发送流程

      消息发送流程主要的步骤为验证消息、查找路由、消息发送(包含异常处理机制)

      查找路由目的是要知道消息要向那个broker投送,选择消息队列,这时候就有个问题,如果一个topic对应一个Broker(包含maser和slave)的话,每一个topic默认4个消息队列,现在有两个Broker,A和B,如果A失败后,怎么避免再次发送到A,防止重试也失败。

      💡 为什么nameServer中存有故障的Broker的路由?

      nameServer检查Broker可用有延迟
       1. 10S一次的心跳间隔
       2. 检测到也不会马上通知生产者(生产者每隔30s更新路由)
      因此生产者最少30s才知道Broker出现故障(因此,重试间隔应该要大于30S)    
      

      首先消息发送端采用重试机制,同步发送方式由retryTimesWhenSendFailed指定重试次数,在 for 循环中 使用 try catch 将sendKernelImpl 发送方法包裹,就可以保证该方法抛出异常后能继续重试。异步发送的重试机制是在收到消息发送结果时,执行回调之前进行重试,由retryTimesWhenSendAsyncFailed指定异常重试次数。默认会重试两次。循环的执行选择消息队列、发送消息,发送成功则返回,收到异常则重试。

      int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;

      选择消息队列有两种方式。sendLatencyFaultEnable=false,默认不启用Broker故障延迟机制。sendLatencyFaultEnable=true,启用Broker故障延迟机制。

    • 默认的选择流程:

       public MessageQueue selectOneMessageQueue(final String lastBrokerName) {
              if (lastBrokerName == null) {
                  return selectOneMessageQueue();
              } else {
                  for (int i = 0; i < this.messageQueueList.size(); i++) {
                      int index = this.sendWhichQueue.incrementAndGet();
                      int pos = Math.abs(index) % this.messageQueueList.size();
                      if (pos < 0)
                          pos = 0;
                      MessageQueue mq = this.messageQueueList.get(pos);
                      if (!mq.getBrokerName().equals(lastBrokerName)) {
                          return mq;
                      }
                  }
                  return selectOneMessageQueue();
              }
          }
      
          public MessageQueue selectOneMessageQueue() {
              int index = this.sendWhichQueue.incrementAndGet();
              int pos = Math.abs(index) % this.messageQueueList.size();
              if (pos < 0)
                  pos = 0;
              return this.messageQueueList.get(pos);
          }
      

      第一次发送时,lastBrokerName(上一次选择发送的Broker)为null,此时直接用sendWhichQueue自增再与消息队列的总数取模(均衡)获取值

      第二次发送(发送失败重试)时,遍历所有的消息队列,找到不是上一次的Broker(这里的index自增使用了ThreadLocal,让同一线程的index不被其他线程修改)

    • 开启故障规避机制的选择流程:其中判断可用的方法是关键,方法中我们从faultItemTable中获取该Broker的条目,那么是什么时候放进去的呢?

      遍历消息队列获取一个可用的,如果都不可用则从延时等待队列中获取一个,如果延时等待队列中的消息队列可用,则移除latencyFaultTolerance关于该topic的条目,表明该Broker故障已经修复。

          public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
              if (this.sendLatencyFaultEnable) {
                  try {
                      int index = tpInfo.getSendWhichQueue().incrementAndGet();
                      for (int i = 0; i < tpInfo.getMessageQueueList().size(); i++) {
                          int pos = Math.abs(index++) % tpInfo.getMessageQueueList().size();
                          if (pos < 0)
                              pos = 0;
                          MessageQueue mq = tpInfo.getMessageQueueList().get(pos);
                          if (latencyFaultTolerance.isAvailable(mq.getBrokerName()))
                              return mq;
                      }
      
                      final String notBestBroker = latencyFaultTolerance.pickOneAtLeast();
                      int writeQueueNums = tpInfo.getQueueIdByBroker(notBestBroker);
                      if (writeQueueNums > 0) {
                          final MessageQueue mq = tpInfo.selectOneMessageQueue();
                          if (notBestBroker != null) {
                              mq.setBrokerName(notBestBroker);
                              mq.setQueueId(tpInfo.getSendWhichQueue().incrementAndGet() % writeQueueNums);
                          }
                          return mq;
                      } else {
                          latencyFaultTolerance.remove(notBestBroker);
                      }
                  } catch (Exception e) {
                      log.error("Error occurred when selecting message queue", e);
                  }
      
                  return tpInfo.selectOneMessageQueue();
              }
      
              return tpInfo.selectOneMessageQueue(lastBrokerName);
          }
      
      
          @Override
          public boolean isAvailable(final String name) {
              final FaultItem faultItem = this.faultItemTable.get(name);
              if (faultItem != null) {
                  return faultItem.isAvailable();
              }
              return true;
          }
      

      在发送完和在发送过程中抛出了异常,会调用updateFaultItem方法,当isolation为true,则使用30s作为computeNotAvailableDuration方法的参数。如果isolation为false,则使用本次消息发送时延作为computeNotAvailableDuration方法的参数。

      在这里插入图片描述

      computeNotAvailableDuration的作用是计算因本次消息发送故障需要规避Broker的时长,也就是接下来多长的时间内,该Broker将不参与消息发送队列负载。具体算法是,从latencyMax数组尾部开始寻找,找到第一个比currentLatency小的下标,然后从notAvailableDuration数组中获取需要规避的时长

          @Override
          public void updateFaultItem(final String name, final long currentLatency, final long notAvailableDuration) {
              FaultItem old = this.faultItemTable.get(name);
              if (null == old) {
                  final FaultItem faultItem = new FaultItem(name);
                  faultItem.setCurrentLatency(currentLatency);
                  faultItem.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);
      
                  old = this.faultItemTable.putIfAbsent(name, faultItem);
                  if (old != null) {
                      old.setCurrentLatency(currentLatency);
                      old.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);
                  }
              } else {
                  old.setCurrentLatency(currentLatency);
                  old.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);
              }
          }
      
          @Override
          public boolean isAvailable(final String name) {
              final FaultItem faultItem = this.faultItemTable.get(name);
              if (faultItem != null) {
                  return faultItem.isAvailable();
              }
              return true;
          }
      
          public void updateFaultItem(final String brokerName, final long currentLatency, boolean isolation) {
              if (this.sendLatencyFaultEnable) {
                  long duration = computeNotAvailableDuration(isolation ? 30000 : currentLatency);
                  this.latencyFaultTolerance.updateFaultItem(brokerName, currentLatency, duration);
              }
          }
      
          private long computeNotAvailableDuration(final long currentLatency) {
              for (int i = latencyMax.length - 1; i >= 0; i--) {
                  if (currentLatency >= latencyMax[i])
                      return this.notAvailableDuration[i];
              }
      
              return 0;
          }
      
    • 不可访问时间数组是按照时延以此递增,日常业务的降级限流也可按照此算法设计

          private long[] latencyMax = {50L, 100L, 550L, 1000L, 2000L, 3000L, 15000L};
          private long[] notAvailableDuration = {0L, 0L, 30000L, 60000L, 120000L, 180000L, 600000L};
      
  • 开启规避机制,是悲观认为发送异常的原因是Broker不可用

  • 同步发送什么异常时会重试

    • 1.发送超时

    • 2.Broker不存在

    • 3.刷盘出现异常

    • 4.发送返回的状态码

          private final Set<Integer> retryResponseCodes = new CopyOnWriteArraySet<Integer>(Arrays.asList(
                  ResponseCode.TOPIC_NOT_EXIST,
                  ResponseCode.SERVICE_NOT_AVAILABLE,
                  ResponseCode.SYSTEM_ERROR,
                  ResponseCode.NO_PERMISSION,
                  ResponseCode.NO_BUYER_ID,
                  ResponseCode.NOT_IN_CURRENT_UNIT
          ));
      
  • 异步发送在收到服务端(broker)的响应包时进行,因此出现网络问题不会重试

    在这里插入图片描述在这里插入图片描述

    2. rockectMQ持久化的两种方式

    RocketMQ基于内存映射文件机制提供了同步刷盘与异步刷盘两种机制

    同步刷盘指的是在消息追加到内存映射文件的内存中后,立即将数据从内存写入磁盘文件

    异步刷盘是指在消息存储时先追加到内存映射文件,然后启动专门的刷盘线程定时将内存中的数据写入磁盘。

    在这里插入图片描述

    3. rockectMQ怎么保证消息已成功消费

  • 保证消息成功消费的机制—ACK采用ACK确认机制,消费者消费消息时需要给Broker反馈消息消费的情况,成功或失败,对于失败的消息会根据内部算法一段时间后重新消费。最大重新消费次数,默认16次,

    每一个Broker上默认有一个重试队列。

  • 什么时候重试1. 当消费状态返回为RECONSUME_LATER

    消费重试,只有在消息模式为MessageModel.CLUSTERING集群模式时,Broker才会自动进行重试,

    public enum ConsumeConcurrentlyStatus {
        /**
         * Success consumption
         */
        CONSUME_SUCCESS,
        /**
         * Failure consumption,later try to consume
         */
        RECONSUME_LATER;
    }
    

    2.业务抛出异常

    3.Broker接收ACK超时(网络问题)

  • 消息确认和消息重试

    当消息监听器返回RECONSUME_LATER时,消息消费进度也会向前推进(为符合队列先进先出的特性,RocketMQ会创建一条与原消息属性相同的消息,拥有一个唯一的新msgId,并存储原消息ID,该消息会存入CommitLog文件,与原消息没有任何关联,所以该消息也会进入ConsuemeQueue,并拥有一个全新的队列偏移量)

    ACK消息是同步发送的,如果在发送过程中出现错误,将记录所有发送ACK消息失败的消息,然后再次封装成ConsumeRequest,延迟5s执行

    在这里插入图片描述

三、实践

MQ保证消费至少能被消费一次At least once,如何保证不被重复消费?

幂等机制

消费过程幂等

RocketMQ无法避免消息重复(Exactly-Once),从对系统的影响结果来说: At least once+ 幂等消费 = Exactly once。

  • 使用数据库的唯一约束实现

借助表中具有唯一约束的字段,同一字段只执行一次变更,当表中已存有这个字段,则不进行消费,msgId一定是全局唯一标识符,但是实际使用中为什么不能直接用,因为可能会存在相同的消息有两个不同msgId的情况(重试机制),这种情况就需要使业务字段进行重复消费。

在redis中借助SETNX命令。只有不存在的时候才进行变更

  • 更新数据前设置前置信息

给数据变更设置一个前置条件,如果满足条件就更新数据,否则拒绝更新数据,在更新数据的时候,同时变更前置条件中需要判断的数据。这样,重复执行这个操作时,由于第一次更新数据的时候已经变更了前置条件中需要判断的数据,不满足前置条件,则不会重复执行更新数据操作。

例如:给数据加版本号,更新时比较版本号,不符合则不更新

  • 记录并检查

以上两种需要根据具体的业务场景进行设计,有一种通用性最强,适用范围最广的实现幂等性方法: 记录并检查操作,也称为“Token 机制或者 GUID(全局唯一 D)机制",实现的思路特别简单: 发送消息时,生成一个全局唯一的ID,消费消息时,在执行数据更新操作之前,先检查一下是否执行过这个更新操作。

总结:想要性能比较好的实现消息可靠和消息幂等

构建一张本地消息表,消息落库与业务代码放入一个事务中,在事务外异步发起mq,开启同步刷盘

在事务外发起MQ的原因:在事务中进行消息发送时,当出现发送信息异常或超时会进行事务回滚,但是消息会进行重试后再次成功发出,导致数据不一致(解决方法:捕获异常)

  • 10
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
RocketMQ 是一个开源的分布式消息队列系统,它支持可靠消息传输和最终一致性。 RocketMQ 的可靠消息传输是通过消息的持久化和复制来实现的。当生产者发送消息时,消息会被持久化到本地磁盘,并且会根据配置的复制因子将消息复制到其他的 Broker 节点上。这样即使某个 Broker 节点出现故障,消息仍然可以从其他节点获取。 RocketMQ 通过使用主题(Topic)和分区(Partition)的概念来实现消息的负载均衡和扩展性。一个主题可以由多个分区组成,每个分区可以在不同的 Broker 节点上存储。这样可以保证同一个主题的消息在多个节点上进行分布式存储,提高了系统的可靠性和可扩展性。 最终一致性是指当消息被消费者消费后,消息队列系统会保证所有消费者看到的消息顺序是一致的。RocketMQ 使用了消息消费者组(Consumer Group)的概念,每个消费者组内的消费者共同消费一个主题的消息,系统会确保每个消费者按照相同的顺序消费消息。 此外,RocketMQ 还提供了事务消息和顺序消息等特性来满足不同业务场景下的需求,进一步提高了消息传输的可靠性和一致性。 总结来说,RocketMQ 通过持久化、复制、负载均衡、分区和消费者组等机制来实现可靠消息传输和最终一致性。这使得 RocketMQ 在分布式系统中被广泛应用于解决可靠消息传输的需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值