消息中间件—RocketMQ(二)进阶

一、消息发送规则

生产者会选择一个message queue进行发送,类似于partition的分区器,具体代码如下:

public MessageQueue selectOneMessageQueue(TopicPublishInfo tpInfo, String lastBrokerName) {
  if (this.sendLatencyFaultEnable) {
      try {
          int index = tpInfo.getSendWhichQueue().getAndIncrement();
          int i = 0;

          while(true) {
              int writeQueueNums;
              MessageQueue mq;
              if (i >= tpInfo.getMessageQueueList().size()) {
                  String notBestBroker = (String)this.latencyFaultTolerance.pickOneAtLeast();
                  writeQueueNums = tpInfo.getQueueIdByBroker(notBestBroker);
                  if (writeQueueNums > 0) {
                      mq = tpInfo.selectOneMessageQueue();
                      if (notBestBroker != null) {
                          mq.setBrokerName(notBestBroker);
                          mq.setQueueId(tpInfo.getSendWhichQueue().getAndIncrement() % writeQueueNums);
                      }

                      return mq;
                  }

                  this.latencyFaultTolerance.remove(notBestBroker);
                  break;
              }

              writeQueueNums = Math.abs(index++) % tpInfo.getMessageQueueList().size();
              if (writeQueueNums < 0) {
                  writeQueueNums = 0;
              }

              mq = (MessageQueue)tpInfo.getMessageQueueList().get(writeQueueNums);
              if (this.latencyFaultTolerance.isAvailable(mq.getBrokerName()) && (null == lastBrokerName || mq.getBrokerName().equals(lastBrokerName))) {
                  return mq;
              }

              ++i;
          }
      } catch (Exception var7) {
          log.error("Error occurred when selecting message queue", var7);
      }

      return tpInfo.selectOneMessageQueue();
  } else {
      return tpInfo.selectOneMessageQueue(lastBrokerName);
  }
}

关键在于下面这段代码,自增随机数对messageQueue长度取模

writeQueueNums = Math.abs(index++) % tpInfo.getMessageQueueList().size();
if (writeQueueNums < 0) {
    writeQueueNums = 0;
}

还有一些实现MessageQueueSelector接口的算法

  • SelectMessageQueueByHash:哈希取值(默认)
  • SelectMessageQueueByRandom:随机选取
  • SelectMessageQueueByMachineRoom:返回空,没有实现
    若是都不满意,也可以在send时自定义算法。
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
  @Override
  public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
      return null;
  }
}, null);

二、消息有序

消息的有序性在前文中提到过,通常指的是局部有序而非全局有序,因此实现原理如下:

  1. 生产者发送消息时,到达Broker应该是有序的,所以对于生产者而言,不能使用多线程异步发送,只能使用单线程顺序发送。
  2. 写入Broker时,应该是顺序写入的,也就是相同主题的消息应该集中写入,选择同一个Message Queue,而不是分散写入。
  3. 消费者消费的时候只能有一个线程,否则由于消费速率的不同,有可能出现记录到数据库的时候无序。

1、生产者

在producer的send方法中,有一个参数,CommunicationMode.SYNC,其实它包含多组参数

public enum CommunicationMode {
	//同步
    SYNC,
    //异步
    ASYNC,
    //单向
    ONEWAY;

    private CommunicationMode() {
    }
}

在这里,它使用了同步,即必须要同步发送,等待响应。

private SendResult sendMessageSync(String addr, String brokerName, Message msg, long timeoutMillis, RemotingCommand request) throws RemotingException, MQBrokerException, InterruptedException {
   RemotingCommand response = this.remotingClient.invokeSync(addr, request, timeoutMillis);

   assert response != null;

   return this.processSendResponse(brokerName, msg, response);
}

以上是生产者的实现。

2、Broker写入

在之前提到的方法中,sendOneWayOrderly是单向顺序发送,在其内部方法中,有一个关键的参数hashKey,hashKey可以保证对它取模时,最终路由到同一个Message Queue。

public void sendOneWayOrderly(String destination, Message<?> message, String hashKey) {
    if (!Objects.isNull(message) && !Objects.isNull(message.getPayload())) {
        try {
            org.apache.rocketmq.common.message.Message rocketMsg = this.createRocketMqMessage(destination, message);
            this.producer.sendOneway(rocketMsg, this.messageQueueSelector, hashKey);
        } catch (Exception var5) {
            log.error("sendOneWayOrderly failed. destination:{}, message:{}", destination, message);
            throw new MessagingException(var5.getMessage(), var5);
        }
    } else {
        log.error("sendOneWayOrderly failed. destination:{}, message is null ", destination);
        throw new IllegalArgumentException("`message` and `message.payload` cannot be null");
    }
}

3、消费者消费

可在消费者设置中选择顺序消费,这样就可以加锁进行单线程消费保证顺序性。

三、消息事务

和kafka类似,这里的事务也类似分布式事务。
实现思路

  1. 生产者先发送一条消息到Broker,把这个消息状态标记为未确认。
  2. Broker通知生产者消息接收成功,现在可以执行本地事务
  3. 生产者执行本地事务
  4. 确认或放弃

首先,Producer将发送半消息给Broker,Broker将回复消息接收成功,然后Producer执行本地事务,根据事务的成功或失败决定返回结果是提交还是回滚,Broker将根据此次事务结果决定这个消息是投递还是丢弃。如果一直没能得到这次的事务究竟成功还是失败,Broker将主动向Producer发起询问,Producer检查事务情况,将结果告知Broker。
示例图如下:
在这里插入图片描述

生产者将启动事务监听器,这个事务监听器可以自己重定义其中的方法。

public class TransactionProducer {
    public static void main(String[] args) throws MQClientException, InterruptedException {
        TransactionListener transactionListener = new TransactionListenerImpl();
        TransactionMQProducer producer = new TransactionMQProducer("transaction_test_group_name");
        producer.setNamesrvAddr("192.168.44.1:9876;192.168.44.2:9876");
        ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2000), new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r);
                thread.setName("client-transaction-msg-check-thread");
                return thread;
            }
        });

        producer.setExecutorService(executorService);
        producer.setTransactionListener(transactionListener);
        producer.start();

        String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
        for (int i = 0; i < 12; i++) {
            try {
                Message msg =
                    new Message("transaction-test-topic", tags[i % tags.length], "KEY" + i,
                        ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
                SendResult sendResult = producer.sendMessageInTransaction(msg, null);
                System.out.printf("%s%n", sendResult);

                Thread.sleep(10);
            } catch (MQClientException | UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }

        for (int i = 0; i < 100000; i++) {
            Thread.sleep(1000);
        }
        producer.shutdown();
    }
}

重写方法executeLocalTransaction,这个方法将执行本地事务,根据返回结果不同对应事务的成功,失败或未知。
重写方法checkLocalTransaction,这个方法将执行事务回查,在executeLocalTransaction方法超时响应或回复未知时,将主动提供询问事务执行结果,这个方法会执行15次,第一次会回查6秒,后续回查时间间隔1分钟。

public class TransactionListenerImpl implements TransactionListener {
    private AtomicInteger transactionIndex = new AtomicInteger(0);

    private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>();

    /**
     * 执行本地事务
     * @param msg
     * @param arg
     * @return
     */
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        int value = transactionIndex.getAndIncrement();
        int status = value % 3;
        localTrans.put(msg.getTransactionId(), status);
        System.out.println("收到ACK,假装执行本地事务,返回结果:UNKNOW……");
        return LocalTransactionState.UNKNOW;
    }

    /**
     * 事务回查
     * @param msg
     * @return
     */
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        System.out.println("有人没有报告事务执行状态,主动检查!");
        Integer status = localTrans.get(msg.getTransactionId());
        if (null != status) {
            switch (status) {
                case 0:
                    return LocalTransactionState.UNKNOW;
                case 1:
                    return LocalTransactionState.COMMIT_MESSAGE;
                case 2:
                    return LocalTransactionState.ROLLBACK_MESSAGE;
                default:
                    return LocalTransactionState.COMMIT_MESSAGE;
            }
        }
        return LocalTransactionState.COMMIT_MESSAGE;
    }
}

在springboot中有类似的方法,sendMessageInTransaction()
和kafka不同的是,kafka的事务是原子性的,一个失败全部失败,需要协调者协调全部事务预提交成功,RocketMQ则采用半消息,可以同时控制消息事务和数据库事务,确保事务一致性。

四、延时投递

setDelayTimeLevel方法实现延时投递,在开源版本中,延时时间分为18个等级,但实际上能用的只有后面16个。

Message msg = new Message("delay-topic",
"TagA",
"OrderID188",
content.getBytes(RemotingHelper.DEFAULT_CHARSET));
msg.setDelayTimeLevel(3); // 10秒钟

SendResult sendResult = producer.send(msg);

它的原理可以阐述为,producer先把延时消息发送给一个临时Topic,之后由定时任务投递给目标Topic,再由消费者消费。

五、存储方式

1、集中存储

和kafka不同的是,RocketMQ没有partition的概念,它会将所有的消息集中存储到一个文件中。
但是这样做会给消息的查找带来很大的困难,因此RocketMQ使用ConsumerQueue来对消息和消费者的关系进行管理。ConsumerQueue会按照topic来划分目录,和写队列的个数相等,在写入消息时,不仅仅需要在文件中写入,也要在ConsumerQueue中写入。consumer消费消息时,会通过ConsumerQueue读取对应的消息。
各文件作用及大小

  • index:索引,400M空间,存储约2000万条数据
  • consumequeue:消费者队列,600B空间,30万条数据
  • commitlog:消息数据,1G空间。

在这里插入图片描述
为了提升读取速度,所以RocketMQ使用了几种I/O技术。

2、Page CaChe

Page CaChe,页缓存。在操作系统中,内存从磁盘空间中读取数据的时候,会按照一页来读取,一般来说是4KB大小,为了提升磁盘的读取效率,操作系统会将这一页数据缓存起来,当有读取请求时,会先去缓存中进行查找,如果没有,才会去磁盘中再读取。除了正常读取,还会有预读取,也就是将读取页相邻的页面也加载到页缓存中,为将来可能的读取做准备。

3、Memory Map

Memory Map,内存映射,类似于Kafka的零拷贝提升读取速度,但有相当差距,它在用户区和内存区建立了一个共享缓冲区,避免再使用read之类的系统函数读取数据,进一步提高了读取效率。
在这里插入图片描述

六、文件清理

在RocketMQ中,消息的默认保存时间为3天,比Kafka的7天要短,同时会在每天的凌晨4点删除消息。如果磁盘已经快要写满了(超过75%),RocketMQ将立即清除数据,不再等待固定时间,直到低于75%,如果当时占用空间已经超过90%,将拒绝消息写入。

七、消费者负载

在广播模式无所谓负载,因为所有消费者都将得到一样的消息,只有集群模式才有消费者负载。
在集群模式中,类似kafka,RocketMQ也有类似的分配策略,使用方法consumer.setAllocateMessageQueueStrategy();即可完成策略选择

  • AllocateMessageQueueAveragely:连续分配(默认策略)
  • AllocateMessageQueueAveragelyByCirCle:轮流分配
  • AllocateMessageQueueByConfig:通过配置
  • AllocateMessageQueueConsistentHash:一致性哈希
  • AllocateMessageQueueByMachineRoom:指定一个broker的topic中的queue消费
  • AllocateMachineRoomNearby:按broker的机房就近分配

八、重试与死信

当生产者发送消息的返回值为RECONSUME_LATER时,消息将会被重发,重发机制与上文提到过的延时发送有关,有十六个可用等级。发送失败的消息会先进入重试topic,进行延时重试投递,如果最后还是没有投递成功,就会进入死信队列,可以专门监听死信队列的topic,进行进一步的处理。

九、高可用架构

1、集群优点

RocketMQ二主二从集群架构,master节点和可读取的slave节点都可以用作数据读取,确保某一个节点挂掉依旧可以保持可用,生产者和消费者从NameServer中获取全部节点地址。

  1. 数据备份
  2. 高可用性
  3. 提高性能
  4. 消费实时

2、主从通讯

在RocketMQ中,不必配置所有节点的地址,主从节点互相关联通讯依靠的是以下条件

  1. 集群的名字相同,brokerClusterName需要相同。
  2. 连接到相同的NameServer
  3. 在配置文件中,brokerId = 0代表master,brokerId = 1代表slave

3、主从同步

主从同步分为两种,同步和异步

  • ASYNC_MASTER:主从异步复制,当master节点同步数据时就发送成功,效率更高但不可靠
  • SYNC_MASTER:主从同步复制(推荐使用),当master和slave节点都同步数据后发送成功,效率低但可靠

4、刷盘类型

消息存储时,会将数据存入磁盘,存入磁盘的方式就叫刷盘类型

  • ASYNC_FLUSH:异步刷盘(默认),将消息放进缓存后通知成功,消息可能会丢失
  • SYNC_FLUSH:同步刷盘,将消息存入磁盘后通知成功,消息不会丢失。

5、同步流程

  1. slave连接到master,然后每隔5秒向master发送commitLog文件最大偏移量拉取还未同步的消息
  2. master节点收到slave发过来的偏移量进行解析,并返回查找出未同步的消息给slave。
  3. slave收到master节点的消息后,将这批消息写入commitLog文件中,然后更新commitLog拉取偏移量,接着向master拉取未同步的消息。

6、故障转移

在早期版本,RocketMQ并没有主从选举功能,假如某一个节点挂掉之后需要手动重启。在4.5.0之后,出现了一个新功能,故障转移(Dledger),这才解决了主从选举问题。它的本质依旧是主从选举协议,开启Dledger后,宕机节点的数据将会交给Dledger来进行管理,然后选举出新的master节点。这个功能默认是关闭的,需要至少三个节点才能开启这个Dledger功能。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值