【我要找工作_02】学习MQ时,到底在学习个啥?

当我在学习使用各种MQ时,我到底在学习什么?

MQ:消息队列,有Kafka,RocketMQ,ActiveMQ等各种各样的MQ,每一个都有自己的概念或者特效,那么在学习MQ时,到底在学习个啥?

什么时候会使用到MQ?

当我接触到MQ时,是在学习微服务的时候,工作用到的第一个MQ是Kafka,其作用就是就是用来削峰填谷汽车上传的运行数据,解耦程序。其次就是阿里巴巴开源的RocketMQ。这两种MQ在使用上、甚至在一些概念上、都是相近的。不同类别的MQ必然有不同的特性,那么排除掉特殊的使用场景之外,他们的共性是什么?一下是一些概念性内容:

Kafka中的概念

  • 生产者:向kafka服务器发送消息的一方
  • kafka服务器:broker实例、处理生产者发送消息的一方
  • 消费者:从服务器拉取消息、进行消息消费的一方
  • 主题:是一个逻辑概念、是对消息的分类
  • 分区:是对同一个主题的消息进行物理分区的地方、每一个分区存储的消息是不同的、所有分区存储的消息加起来就是一个topic的全部消息
  • segment:一个分区由多个segment组成
  • 副本:对同一个分区数据的拷贝、通常为一主多从,一主:leader、多从 follower
  • ISR:在一定时间间隔内、和主副本信息保持同步的副本、包含leader副本
  • OSR:和leader副本中的信息不同步的副本
  • AR:ISR + OSR,即所有副本的集合
  • LEO(log end offset):分区内、下一条消息要写入的地址
  • HW:通常有同一个分区的副本且在ISR中的最小的LEO确定、消费者只能消费在这之前的信息。
  • 已提交的信息:即高水位之前的信息
  • 未提交的信息:即高水位到LEO之间的消息。
  • 订阅关系:主题的一个分区只能被同一个消费组内的消费者消费,一个消费者可以消费多个分区的消息,不同消费者组订阅关系互不影响。

RocketMQ的概念

  • 主题:消息的逻辑分类

  • 队列:一个主题由一个或多个队列构成、生产者会把消息发送到某一个队列里面【对应kfk的分区】

  • 消息:交互的载体

  • 集群模式:主题下的同一条消息只允被集群内的一个消费者消费。

  • 广播模式:主题下的同一条消息被集群内的每一个消费者消费。

  • namesrv:无状态的服务器,彼此之间不通信、最终一致性,broker在启动时会向namesrv注册信息、并定时进行心跳连接。

    • 维护topic的路由信息:produce在发送消息时、从namesrv获取路由信息、决定最终发送到topic到那个队列上。
    • 和broker建立长连接
    • consumer在启动时也会从namesrv获取topic路由信息。
  • broker

    • 存储和转发消息到地方
    • 每一个broker和所有的namesrv保持着长连接和心跳、定时将topic信息同步到namesrv。
    • 每一个topic到queue都对应一个ConsumeQueue
    • ConsumeQueue存储着消息的索引
    • message实际存储在一个CommitLog中
  • 生产者:消息的发送者

    • 同步发送:指消息发送后、在收到接受方的响应之后在发送下一个数据包。【不会丢失】
    • 异步发送:不管发送结果、发送完之后立马发送下一个、可通过回调函数判断发送结果。【不会丢失】
    • 单向发送:不管发送结果、发送完之后立马发送下一个且没有回调。【可能丢失】
  • 消费者

    • 支持集群消费和广播消费
    • pull拉取消息:消费者主动拉取批量消息、只要拉取到消息、消费者就会消费
    • push拉取消息:基于pull模式实现,在pull模式上包装一层,一个拉取任务完成后开始下一个拉取任务。
  • 订阅关系

    • 不同消费者分组对于同一个主题的订阅相互独立
    • 同一个消费者分组对于不同主题的订阅也相互独

MQ是如何使用的?

不管是 kafka 还是 RocketMQ,在实际的使用过程中,排除掉软件安装以及项目配置(地址、端口号等),我们都需要在客户端发送消息到MQ服务器,消费者从MQ服务器获取发送成功的消息进行消费,均均涉及两次RPC一次持久化,不同的MQ可能消息的发送和消息的消费方式、甚至消息的持久化也不一样,但总的来说就是这么个流程。

那么针对这两次RPC以及一次持久化,我们能得到什么样的问题?

  • 消息的发送过程以及如何保证消息发送成功
  • 消息的格式以及消息的持久化
  • 消息的消费是如何保证的

换言之就是如下常见的面试题:

  • 如何保证消息不丢失?
  • 如何保证消息不会被重复消费?
  • 技术选型的时候为什么选择xxMQ,其优势是什么?

如何保证消息不丢失?

首先回答第一个问题,如何保证消息不丢失?首先思考消息丢失的原因有什么?

  • 网络原因,导致消息丢失
  • 生产者发送成功,但是服务器丢失相关数据
  • 消息者消息消息时,由于某种原因没有正常消费

不管是哪种MQ,生产者一般都会使用callback函数获取当消息发送成功或者发送异常,用于执行对应的回调逻辑。由于网络原因导致的消息丢失,除了重试也没有其他办法。

Java 复制代码
// kafka发送消息 
public static void main(String[] args) {
      Properties properties = new Properties();
      properties.put("key.serializer",
              "org.apache.kafka.common.serialization.StringSerializer");
      properties.put("value.serializer",
              "org.apache.kafka.common.serialization.StringSerializer");
      properties.put("bootstrap.servers", brokerList);
​
​
      KafkaProducer<String, String> producer = new KafkaProducer<>(properties);
      ProducerRecord<String, String> record = new ProducerRecord<>(TOPIC, "hello, Kafka!");
      try {
          producer.send(record, (metadata, exception) -> {
              if (exception != null) {
                  exception.printStackTrace();
              } else {
                  System.out.println("发送成功");
              }
          });
​
      } catch (Exception e) {
          e.printStackTrace();
      }
      producer.close();
  }
// rocketmq发送消息
public static void main(String[] args) throws Exception {
  // 初始化一个producer并设置Producer group name
  DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
  // 设置NameServer地址
  producer.setNamesrvAddr("localhost:9876");
  // 启动producer
  producer.start();
  producer.setRetryTimesWhenSendAsyncFailed(0);
  int messageCount = 100;
  final CountDownLatch countDownLatch = new CountDownLatch(messageCount);
  for (int i = 0; i < messageCount; i++) {
    try {
        final int index = i;
// 创建一条消息,并指定topic、tag、body等信息,tag可以理解成标签,对消息进行再归类,RocketMQ可以在消费端对tag进行过滤
        Message msg = new Message("TopicTest",
          "TagA",
          "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
        // 异步发送消息, 发送结果通过callback返回给客户端
        producer.send(msg, new SendCallback() {
          @Override
          public void onSuccess(SendResult sendResult) {
            System.out.printf("%-10d OK %s %n", index,
              sendResult.getMsgId());
            countDownLatch.countDown();
          }
          @Override
          public void onException(Throwable e) {
            System.out.printf("%-10d Exception %s %n", index, e);
            e.printStackTrace();
            countDownLatch.countDown();
          }
        });
      } catch (Exception e) {
          e.printStackTrace();
          countDownLatch.countDown();
      }
  }
  //异步发送,如果要求可靠传输,必须要等回调接口返回明确结果后才能结束逻辑,否则立即关闭Producer可能导致部分消息尚未传输成功
  countDownLatch.await(5, TimeUnit.SECONDS);
  // 一旦producer不再使用,关闭producer
  producer.shutdown();
}

通过重试解决网络原因导致的消息丢失,接下来就是解决MQ服务器上导致的消息丢失,MQ服务器什么情况下会导致消息丢失呢?单机且没有数据备份或者数据没有来得及持久化。因此就需要保证MQ服务器的可用性,就需要搭建集群。以Kafka为例,为了保证服务器的可靠性,需要进行如下的参数设置

properties 复制代码
replication.factor = 3
min.insync.replicas = 2
acks = -1

消息的副本数为三:即同一条消息有三份,Kafka的ISR集合的个数为2: 即必须保证有两个服务器能够实时的同步消息,acks = -1,必须保证所有的isr集合的服务器将消息保存成功之后再返回确认,即告诉生产者消息已经发送成功。

RocketMQ的集群有多Master + 无Salve 模式和 多Master + Salve模式,很明显的是无Slave模式只要一个主机宕机就会导致部分消息丢失。

合理的参数设置以及集群搭建可以有效解决broker端端消息丢失,那么接下来就需要解决消费者端端消息丢失情况。

同样的以Kafka为例,Kafka在消息消息时,存在一个offset的概念,用于记录消息消费的位置,默认情况下应用是自动提交的,因此我们需要改为手动提交,由于消息的拉取是按照批量拉取的,自动提交存在一批消息没有消息完就已经提交了offset导致消息并没有被消费的情况、这样就会导致消息丢失。

RocketMQ客户端消费一批数据后,需要向Broker反馈消息的消费进度,Broker会记录消息消费进度。

  1. 消费线程池在处理完一批消息后,会将消息消费进度存储在本地内存中。
  2. 客户端会启动一个定时线程,每5s将存储在本地内存中的所有队列消息消费偏移量提交到Broker中。
  3. Broker收到的消息消费进度会存储在内存中,每隔5s将消息消费偏移量持久化到磁盘文件中。
  4. 在客户端向Broker拉取消息时也会将该队列的消息消费偏移量提交到Broker。

如何保证消息不重复消费?

通过数据库/redis + 唯一性约束即可解决。即在消费前根据消息的唯一性标识查询数据库或者redis,看是否已经消费过,根据查询结果进行处理。

如何保证消息的顺序消费?

保证消息的顺序消费需要明白消息是如何持久化的,在Kafka中,消息是存在分区里面的,同一主题的不同分区的消息是不一样的,同一分区的消息是按照先后顺序来存储的,因此在各种因素如网路的影响下,同一分区的消息是有序的,而同一主题的不同分区的消息是无序的。因此为了保证消息的顺序消费,可以在消息发送时,指定消息的key,根据key进行分区,将这个标识的标志的消息都发送到同一分区,这样就能完成单线程下的顺序消费,如果是并发消费,将消息拉取出来之后,在根据消息的标识分组,将其放到内存队列中,每个内存队列指定一个线程消费,这样就大大的提高了消息的消费速度。

RocketMQ的消息是存在队列中的,同样的是消息在队列中局部有序,整体没有顺序,可采用相同的思路进行消费进而保证消息的顺序消费。除此之外,RocketMQ支持顺序消息,发送时需要自己选择队列MessageQueueSelector

Java 复制代码
DefaultMQProducer producer = new DefaultMQProducer("order_producer_group");
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
String orderId = "ORDER_20250408_001"; // 业务标识(如订单ID)
// 发送顺序消息(需指定队列选择策略)
for (int i = 0; i < 10; i++) {
    Message msg = new Message("OrderTopic", "TagA", 
        ("Order Step " + i + ", ID: " + orderId).getBytes(StandardCharsets.UTF_8));
    // 使用 orderId 选择队列(同一订单的消息进入同一队列)
    SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
        @Override
        public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
            String id = (String) arg;
            int index = id.hashCode() % mqs.size();
            return mqs.get(index);
        }
    }, orderId); // 传入业务标识
​
    System.out.println("Send Order Result: " + sendResult);
}
producer.shutdown();

所以MQ到底学习了个啥?大抵是如下这张图:

明白了上述的问题,有了对 MQ 的框架认识,只需要了解不同MQ之间的特性或者说特点了,比如Kafka在发送消息的具体过程,消息如何经过拦截器、序列化器、分区器的?提供了那些函数或者接口供我们自定义使用,RocketMQ相对于Kafka而言,提供了那些新的特性,如延时消息、顺序消息、以及他们是如何实现的。

当然了实际情况远比这个复杂多的多,比如分区或者队列扩容之后的重平衡策略、不同mq消息的格式、消息的分类、不同mq的刷盘策略等,只是有了上图这个框架在这里,学习起来更加方便罢了

相关推荐
我是谁的程序员4 分钟前
Flutter iOS真机调试报错弹窗:不受信任的开发者
后端
蓝宝石Kaze6 分钟前
使用 Viper 读取配置文件
后端
aiopencode8 分钟前
Flutter 开发指南:安卓真机、虚拟机调试及 VS Code 开发环境搭建
后端
开心猴爷11 分钟前
M1搭建flutter环境+真机调试demo
后端
沐道PHP12 分钟前
Go Gin框架安装记录
后端
技术宝哥27 分钟前
解决 Spring Boot 启动报错:数据源配置引发的启动失败
spring boot·后端·mybatis
独立开阀者_FwtCoder31 分钟前
2025年,真心佩服的十大开源工具
前端·后端·面试
喵手32 分钟前
如何快速掌握 Java 反射之获取类的字段?
java·后端·java ee
AronTing34 分钟前
06- 服务网格实战:从 Istio 核心原理到微服务治理升级
java·后端·架构
风生水气37 分钟前
在supabase中实现关键词检索和语义检索
后端·搜索引擎