当我在学习使用各种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会记录消息消费进度。
- 消费线程池在处理完一批消息后,会将消息消费进度存储在本地内存中。
- 客户端会启动一个定时线程,每5s将存储在本地内存中的所有队列消息消费偏移量提交到Broker中。
- Broker收到的消息消费进度会存储在内存中,每隔5s将消息消费偏移量持久化到磁盘文件中。
- 在客户端向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的刷盘策略等,只是有了上图这个框架在这里,学习起来更加方便罢了