【RocketMQ】架构原理、消息丢失、重复消费、顺序消费、事务消息
- 1、RocketMQ架构
-
- [1.1 RocketMQ的架构是怎么样的?](#1.1 RocketMQ的架构是怎么样的?)
- [1.2 RocketMQ怎么实现消息分发的?](#1.2 RocketMQ怎么实现消息分发的?)
- [1.3 RocketMQ的消息是推还是拉?](#1.3 RocketMQ的消息是推还是拉?)
-
- [1.3.1 推Push模式、拉Pull模式](#1.3.1 推Push模式、拉Pull模式)
- [1.3.2 pop 模式](#1.3.2 pop 模式)
- [1.4 RocketMQ有几种集群方式?](#1.4 RocketMQ有几种集群方式?)
- 2、消息丢失问题
-
- [2.1 RocketMQ如何保证消息不丢失?](#2.1 RocketMQ如何保证消息不丢失?)
- [2.2 RocketMQ能否100%保证消息不丢失?](#2.2 RocketMQ能否100%保证消息不丢失?)
- 3、重复消费问题
-
- [3.1 RocketMQ如果重复消费了,可能是什么原因导致的?](#3.1 RocketMQ如果重复消费了,可能是什么原因导致的?)
- 4、顺序消费问题
-
- [4.1 RocketMQ如何保证消息的顺序性?](#4.1 RocketMQ如何保证消息的顺序性?)
- [4.2 RocketMQ顺序消费的缺点?](#4.2 RocketMQ顺序消费的缺点?)
- 5、事务消息
-
- [5.1 RocketMQ的事务消息是如何实现的?](#5.1 RocketMQ的事务消息是如何实现的?)
-
- [5.1.1 如果一直没收到COMMIT或者ROLLBACK怎么办?](#5.1.1 如果一直没收到COMMIT或者ROLLBACK怎么办?)
- [5.1.2 为什么要用事务消息?](#5.1.2 为什么要用事务消息?)
- [5.2 RocketMQ的事务消息和Kafka的事务消息有什么区别?](#5.2 RocketMQ的事务消息和Kafka的事务消息有什么区别?)
- 6、重平衡问题
-
- [6.1 RocketMQ和Kafka一样有重平衡的问题吗?](#6.1 RocketMQ和Kafka一样有重平衡的问题吗?)
- 7、延时消息
-
- [7.1 RocketMQ如何实现延时消息?](#7.1 RocketMQ如何实现延时消息?)
- 8、RocketMQ消息堆积了怎么解决?
1、RocketMQ架构
1.1 RocketMQ的架构是怎么样的?
RocketMQ中有这样几个角色:NameServer、Broker、Producer和Consumer。
NameServer:NameServer是RocketMQ的路由和寻址中心,它维护了Broker和Topic的路由信息,提供了Producer和Consumer与正确的Broker建立连接的能力。NameServer还负责监控Broker的状态,并提供自动发现和故障恢复的功能。
Broker:Broker是RocketMQ的核心组件,负责存储、传输和路由消息。它接收Producer发送的消息,并将其存储在内部存储中。并且还负责处理Consumer的订阅请求,将消息推送给订阅了相应Topic的Consumer。
Producer(消息生产者):Producer是消息的生产者,用于将消息发送到RocketMQ系统。
Consumer(消息消费者):Consumer是消息的消费者,用于从RocketMQ系统中订阅和消费消息。
RocketMQ的工作过程大致如下:
1、启动NameServer,他会等待Broker、Producer以及Consumer的链接。
2、启动Broker,会和NameServer建立连接,定时发送心跳包。心跳包中包含当前Broker信息(ip、port等)、Topic信息以及Borker与Topic的映射关系。
3、启动Producer,启动时先随机和NameServer集群中的一台建立长连接,并从NameServer中获取当前发送的Topic所在的所有Broker的地址;然后从队列列表中轮询选择一个队列,与队列所在的Broker建立长连接,进行消息的发送。
4、Broker接收Producer发送的消息,当配置为同步复制时,master需要先将消息复制到slave节点,然后再返回"写成功状态"响应给生产者;当配置为同步刷盘时,则还需要将消息写入磁盘中,再返回"写成功状态";要是配置的是异步刷盘和异步复制,则消息只要发送到master节点,就直接返回"写成功"状态。
5、启动Consumer,过程和Producer类似,先随机和一台NameServer建立连接,获取订阅信息,然后在和需要订阅的Broker建立连接,获取消息。

1.2 RocketMQ怎么实现消息分发的?
RocketMQ 支持两种消息模式:广播消费( Broadcasting )和集群消费( Clustering )
。
1、广播消费
当使用广播消费模式时,RocketMQ 会将每条消息推送给集群内所有的消费者,保证消息至少被每个消费者消费一次。
广播模式下,RocketMQ 保证消息至少被客户端消费一次,但是并不会重投消费失败的消息,因此业务方需要关注消费失败的情况。并且,客户端每一次重启都会从最新消息消费。客户端在被停止期间发送至服务端的消息将会被自动跳过。

2、集群消费(默认)
当使用集群消费模式时,RocketMQ 认为任意一条消息只需要被集群内的任意一个消费者处理即可。
集群模式下,每一条消息都只会被分发到一台机器上处理。但是不保证每一次失败重投的消息路由到同一台机器上。一般来说,用集群消费的更多一些。

1.3 RocketMQ的消息是推还是拉?
MQ的消费模式可以一共有3种,分别是推Push、拉Pull以及5.0中推出的 POP 模式。
1.3.1 推Push模式、拉Pull模式
Push是服务端主动推送消息给客户端,Pull是客户端需要主动到服务端轮询获取数据。
他们各自有各自的优缺点,推优点是及时性较好,但如果客户端没有做好流控,一旦服务端推送大量消息到客户端时,就会导致客户端消息堆积甚至崩溃。
拉优点是客户端可以依据自己的消费能力进行消费,但是频繁拉取会给服务端造成压力,并且可能会导致消息消费不及时。
RocketMQ既提供了Push模式也提供了Pull模式,开发者可以自行选择,主要有两个Consumer可以供开发者选择:
java
public class DefaultMQPullConsumer extends ClientConfig implements MQPullConsumer {
// https://github.com/apache/rocketmq/blob/develop/client/src/main/java/org/apache/rocketmq/client/consumer/DefaultMQPullConsumer.java
}
public class DefaultMQPushConsumer extends ClientConfig implements MQPushConsumer {
//https://github.com/apache/rocketmq/blob/develop/client/src/main/java/org/apache/rocketmq/client/consumer/DefaultMQPushConsumer.java
}
需要注意的是,RocketMQ的push模式其实底层的实现还是基于pull实现的,只不过他把pull给封装的比较好,让你以为是在push
。RocketMQ的push就是通过长轮询来实现的。
1.3.2 pop 模式
Pop 模式其实也是一种拉的模式,主要是来代替原来的 push 模式的。
在5.0以前的 push 模式中(其实也是基于拉实现的)。客户端在开始消费消息前,会需要通过负载均衡算法计算出自己需要消费哪些 Queue,每当 Consumer 数量发生变化时就会触发ReBalance。
这个负载均衡是在客户端做的,也就是消费者这里,这也就意味着如果负载均衡时间过长会影响消费者的消费。
但是,这种模式有个问题,那就是无法通过一直增加客户端数量的方式来提升消费能力。因为 Queue 数量有限,客户端数量一旦达到 Queue 数量,再扩容的话,也会因为无法分配到 Queue而无法消费。这也就是传统的 push 模式的性能瓶颈。

除了负载均衡以外,push 模式中,消费者除了要做负载均衡以外,还有很多其他的事情要做,比如消息拉取,消息消费位点管理等等。这使得客户端的职责很大,出错的概率也比较大。
还有一个问题,那就是如果某个消费者hang住,会导致分配到该消费者的消息队列中的消息无法消费,导致消息积压;
于是在5.0中推出了一个新的 POP模式,来解决这些问题。
在 POP 模式中,消费者不需要感知到分区,即MessageQueue 和消费者不再进行绑定了,并且POP的消费位点也由Broker保存和控制。消费者直接通过 POP 模型提供的接口去获取到数据,消费成功后 ACK 数据。

这样做好处就是消费者只需要负责POP 消息,不再需要进行负载均衡以及消息的进度管理。并且即便某个消费者hang住,其他消费者依旧可以继续消费队列中的数据,不会造成消息堆积。
1.4 RocketMQ有几种集群方式?
3种集群方式:分别是单Master模式、多Master模式以及多Master多Slave模式。
单Master集群,这是一种最简单的集群方式,只包含一个Master节点和若干个Slave节点。所有的写入操作都由Master节点负责处理,Slave节点主要用于提供读取服务。当Master节点宕机时,集群将无法继续工作。
多Master集群:这种集群方式包含多个Master节点,不部署Slave节点。这种方式的优点是配置简单,单个Master宕机或重启维护对应用无影响,在磁盘配置为RAID10时,即使机器宕机不可恢复情况下,由于RAID10磁盘非常可靠,消息也不会丢(异步刷盘丢失少量消息,同步刷盘一条不丢),性能最高;缺点是单台机器宕机期间,这台机器上未被消费的消息在机器恢复之前不可订阅,消息实时性会受到影响。
多Master多Slave集群:这种集群方式包含多个Master节点和多个Slave节点。每个Master节点都可以处理写入操作,并且有自己的一组Slave节点。当其中一个Master节点宕机时,消费者仍然可以从Slave消费。优点是数据与服务都无单点故障,Master宕机情况下,消息无延迟,服务可用性与数据可用性都非常高;缺点是性能比异步复制模式略低(大约低10%左右),发送单个消息的RT会略高,且目前版本在主节点宕机后,备机不能自动切换为主机。
2、消息丢失问题
2.1 RocketMQ如何保证消息不丢失?
RocketMQ的消息想要确保不丢失,需要生产者、消费者以及Broker的共同努力,缺一不可。
1、生产者
首先在生产者端,消息的发送分为同步、异步两种和单向发送(单向发送不保证成功,不建议使用),在同步发送消息的情况下,消息的发送会同步阻塞等待Broker返回结果,在Broker确认收到消息之后,生产者才会拿到SendResult。如果这个过程中发生异常,那么就说明消息发送可能失败了,就需要生产者进行重新发送消息。
java
try {
SendResult sendResult = producer.send(msg);
// 同步发送消息,只要不抛异常就是成功。
if (sendResult != null) {
//重试逻辑
}
}
catch (Exception e) {
//重试逻辑
}
异步发送的时候,会有成功和失败的回调,这还是需要在失败回调中处理重试确保成功。
java
// 异步发送消息, 发送结果通过callback返回给客户端。
producer.sendAsync(msg, new SendCallback() {
@Override
public void onSuccess(final SendResult sendResult) {
// 消息发送成功。
System.out.println("send message success. topic=" + sendResult.getTopic() + ", msgId=" + sendResult.getMessageId());
}
@Override
public void onException(OnExceptionContext context) {
// 消息发送失败
//重试逻辑
}
});
但是Broker其实并不会立即把消息存储到磁盘上,而是先存储到内存中,内存存储成功之后,就返回给确认结果给生产者了。然后再通过异步刷盘的方式将内存中的数据存储到磁盘上。但是这个过程中,如果机器挂了,那么就可能会导致数据丢失。
如果想要保证消息不丢失,可以将消息保存机制修改为同步刷盘,这样,Broker会在同步请求中把数据保存在磁盘上,确保保存成功后再返回确认结果给生产者。
bash
## 默认情况为 ASYNC_FLUSH
flushDiskType = SYNC_FLUSH
2、broker
为了保证消息不丢失,RocketMQ肯定要通过集群方式进行部署,Broker 通常采用一主多从部署方式,并且采用主从同步的方式做数据复制。
当主Broker宕机时,从Broker会接管主Broker的工作,保证消息不丢失。同时,RocketMQ的Broker还可以配置多个实例,消息会在多个Broker之间进行冗余备份,从而保证数据的可靠性。
默认方式下,Broker在接收消息后,写入 master 成功,就可以返回确认响应给生产者了,接着消息将会异步复制到 slave 节点。但是如果这个过程中,Master的磁盘损坏了。那就会导致数据丢失了。
如果想要解决这个问题,可以配置同步复制的方式,即Master在将数据同步到Slave节点后,再返回给生产者确认结果。
bash
## 默认为 ASYNC_MASTER
brokerRole=SYNC_MASTER
3、消费者
在消费者端,需要确保在消息拉取并消费成功之后再给Broker返回ACK,就可以保证消息不丢失了,如果这个过程中Broker一直没收到ACK,那么就可以重试。
所以,在消费者的代码中,一定要在业务逻辑的最后一步return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; 当然,也可以先把数据保存在数据库中,就返回,然后自己再慢慢处理。
2.2 RocketMQ能否100%保证消息不丢失?
RocketMQ和Kafka一样,只能最大限度的保证消息不丢失,但是没办法做到100%保证不丢失。原理也类似,在生产者发送消息后,broker集群发生崩溃后,可能导致消息丢失。
如果生产者在发送消息之后,RocketMQ的集群发生故障或崩溃,而消息尚未被完全写入RocketMQ的日志中,那么这些消息可能会丢失。虽然后续有可能会重试,但是,如果重试也失败了呢?如果这个过程中刚好生产者也崩溃了呢?那就可能会导致没有人知道这个消息失败了,就导致不会重试了。
3、重复消费问题
3.1 RocketMQ如果重复消费了,可能是什么原因导致的?
1、Consumer返回给Broker消费失败(常见)
不管是因为什么情况了,是真的消费失败了,还是出现了异常了,还是明明消费成功了,但是你错误的返回了失败等等情况,只要你给RocketMQ返回的是 RECONSUME_LATER ,那么消息就会重投,有重投就会有重复消费。
2、Consumer消费处理超时了(常见)
不只是返回失败的情况,如果消费方法执行时间过长,RocketMQ 可能判定消费者失联,也一样会重投消息。 那就和上面的情况一样了。
3、消息发重了(常见)
这种比较常见的,因为有的时候我们调用MQ发送消息的时候,因为网络抖动或者异常,我们会把一些实际成功的消息重发一遍,那么就会有两条一模一样的消息,那么对于消费者来说就可能会重复消费了。
4、顺序消费问题
4.1 RocketMQ如何保证消息的顺序性?
和Kafka只支持同一个Partition内消息的顺序性一样,RocketMQ中也提供了基于队列(分区)的顺序消费。即同一个队列内的消息可以做到有序,但是不同队列内的消息是无序的!
1、生产者
当我们作为MQ的生产者需要发送顺序消息时,需要在send方法中,传入一个MessageQueueSelector。
MessageQueueSelector中需要实现一个select方法,这个方法就是用来定义要把消息发送到哪个MessageQueue的,通常可以使用取模法进行路由:
java
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
@Override
//mqs:该Topic下所有可选的MessageQueue
//msg:待发送的消息
//arg:发送消息时传递的参数
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
Integer id = (Integer) arg;
//根据参数,计算出一个要接收消息的MessageQueue的下标
int index = id % mqs.size();
//返回这个MessageQueue
return mqs.get(index);
}
}, orderId);
通过以上形式就可以将需要有序的消息发送到同一个队列中。需要注意的时候,这里需要使用同步发送的方式!
2、消费者
消息按照顺序发送的消息队列中之后,那么,消费者如何按照发送顺序进行消费呢?
RocketMQ的MessageListener回调函数提供了两种消费模式,有序消费模式MessageListenerOrderly和并发消费模式MessageListenerConcurrently。所以,想要实现顺序消费,需要使用MessageListenerOrderly模式接收消息:
java
consumer.registerMessageListener(new MessageListenerOrderly() {
Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs ,ConsumeOrderlyContext context) {
System.out.printf("Receive order msg:" + new String(msgs.get(0).getBody()));
return ConsumeOrderlyStatus.SUCCESS ;
}
});
当我们用以上方式注册一个消费之后,为了保证同一个队列中的有序消息可以被顺序消费,就要保证RocketMQ的Broker只会把消息发送到同一个消费者上,这时候就需要加锁了。
在实现中,ConsumeMessageOrderlyService 初始化的时候,会启动一个定时任务,会尝试向 Broker 为当前消费者客户端申请分布式锁。如果获取成功,那么后续消息将会只发给这个Consumer。
接下来在消息拉取的过程中,消费者会一次性拉取多条消息的,并且会将拉取到的消息放入 ProcessQueue,同时将消息提交到消费线程池进行执行。
那么拉取之后的消费过程,怎么保证顺序消费呢?这里就需要更多的锁了。
RocketMQ在消费的过程中,需要申请 MessageQueue 锁,确保在同一时间,一个队列中只有一个线程能处理队列中的消息。
获取到 MessageQueue 的锁后,就可以从ProcessQueue中依次拉取一批消息处理了,但是这个过程中,为了保证消息不会出现重复消费,还需要对ProcessQueue进行加锁。然后就可以开始处理业务逻辑了。
总结下来就是三次加锁:
1、先锁定Broker上的MessageQueue,确保消息只会投递到唯一的消费者
2、消费者对本地的MessageQueue加锁,确保只有一个线程能处理这个消息队列
3、对存储消息的ProcessQueue加锁(broker中的topic加锁),确保在重平衡的过程中不会出现消息的重复消费。
当消费者集群,新增了一些消费者,发生重平衡的时候,某个队列可能会原来属于客户端A消费的,但是现在要重新分配给客户端B了。
这时候客户端A就需要把自己加在Broker上的锁解掉,而在这个解锁的过程中,就需要确保消息不能在消费过程中就被移除了,因为如果客户端A可能正在处理一部分消息,但是位点信息还没有提交,如果客户端B立马去消费队列中的消息,那存在一部分数据会被重复消费。
那么如何判断消息是否正在消费中呢,就需要通过这个ProcessQueue上面的锁来判断了,也就是说在解锁的线程也需要尝试对ProcessQueue进行加锁,加锁成功才能进行解锁操作。以避免过程中有消息消费。
4.2 RocketMQ顺序消费的缺点?
1、并发能力受限
为了保证顺序,通常会把消息按照 key 分配到固定队列或分区。并且要求同一个 key 的消息串行消费。
这就导致消息无法并发消费,那么处理速度就会大大下降,尤其是当某个消息处理慢的话,会拖慢整个队列的速度。
2、发消息复杂
如果顺序消息消费失败,需要重试,且必须保证重试后顺序仍然正确。所以这种容错策略设计起来都比较复杂。
而且一旦一个消息处理失败,可能会阻塞后续消息的发送和消费。
3、可能导致数据倾斜
为了保证顺序,消息必须按照 相同 key 分区到同一个队列。如果业务 key 太多或者分布不均匀,会导致队列热点和消费不平衡。
5、事务消息
5.1 RocketMQ的事务消息是如何实现的?
RocketMQ的事务消息是通过TransactionListener接口来实现的。
在发送事务消息时,首先向RocketMQ Broker发送一条"half消息"(即半消息),半消息将被存储在Broker端的事务消息日志中,但是这个消息还不能被消费者消费。
接下来,在半消息发送成功后,应用程序通过执行本地事务来确定是否要提交该事务消息。
如果本地事务执行成功,就会通知RocketMQ Broker提交该事务消息,使得该消息可以被消费者消费;否则,就会通知RocketMQ Broker回滚该事务消息,该消息将被删除,从而保证消息不会被消费者消费。

主要有以下4个步骤:
-
发送半消息:应用程序向RocketMQ Broker发送一条半消息,该消息在Broker端的事务消息日志中被标记为"prepared"状态。
-
执行本地事务:RocketMQ会通知应用程序执行本地事务。如果本地事务执行成功,应用程序通知RocketMQ Broker提交该事务消息。
-
提交事务消息:RocketMQ收到提交消息以后,会将该消息的状态从"prepared"改为"committed",并使该消息可以被消费者消费。
-
回滚事务消息:如果本地事务执行失败,应用程序通知RocketMQ Broker回滚该事务消息,RocketMQ将该消息的状态从"prepared"改为"rollback",并将该消息从事务消息日志中删除,从而保证该消息不会被消费者消费。
5.1.1 如果一直没收到COMMIT或者ROLLBACK怎么办?
在RocketMQ的事务消息中,如果半消息发送成功后,RocketMQ Broker在规定时间内没有收到COMMIT或者ROLLBACK消息。
RocketMQ会向应用程序发送一条检查请求,应用程序可以通过回调方法返回是否要提交或回滚该事务消息。如果应用程序在规定时间内未能返回响应,RocketMQ会将该消息标记为"UNKNOW"状态。
在标记为"UNKNOW"状态的事务消息中,如果应用程序有了明确的结果,还可以向MQ发送COMMIT或者ROLLBACK。
但是MQ不会一直等下去,如果过期时间已到,RocketMQ会自动回滚该事务消息,将其从事务消息日志中删除。
5.1.2 为什么要用事务消息?
主要是因为:本地事务执行完成之后再发送消息可能会发消息失败。
一旦发送消息失败了,那么本地事务提交了,但是消息没成功,那么监听者就收不到消息,那么就产生数据不一致了。
那如果用事务消息。先提交一个半消息,然后执行本地事务,再发送一个commit的半消息。如果后面这个commit半消息失败了,MQ是可以基于第一个半消息不断反查来推进状态的。这样只要本地事务提交成功,最终MQ也会成功。如果本地事务rolllback,那么MQ的消息也会rollback。保证了一致性。
5.2 RocketMQ的事务消息和Kafka的事务消息有什么区别?
Kafka的事务消息,确保的是一组发送操作要么全部成功,要么全部失败,以保证消息处理的原子性。
RocketMQ的事务消息,确保的是发送方的本地事务和消息发送以原子性方式执行,即要么都成功,要么都失败。
所以,RocketMQ的事务消息,经常被用来解决分布式场景下的数据一致性问题,是分布式事务的一种常见方案。
而Kafka的事务消息,主要是用来来配合 Kafka 的幂等机制来实现 Kafka 的 Exactly Once 语义(每条消息只会被精确地传递一次,既不会多,也不会少)。
6、重平衡问题
6.1 RocketMQ和Kafka一样有重平衡的问题吗?
广播模式下,每个消费者都会消费所有消息,不存在重平衡问题。 但是如果是默认的集群模式下,消费者在一个消费组中,多个消费者会均摊消费,这时候就涉及重平衡的问题。
不过和Kafka不同的是,RocketMQ他只有个定时重平衡的机制,他会自动的每 20s 进行一次重平衡检查,如果发现有消费者新增或离开时,会触发重新分配队列。
java
// 默认20秒,可以通过rocketmq.client.rebalance.waitInterval配置调整
private static long waitInterval =
Long.parseLong(System.getProperty(
"rocketmq.client.rebalance.waitInterval", "20000"));
@Override
public void run() {
log.info(this.getServiceName() + " service started");
while (!this.isStopped()) {
// 等待20秒
this.waitForRunning(waitInterval);
// 执行重平衡
this.mqClientFactory.doRebalance();
}
log.info(this.getServiceName() + " service end");
}
正是因为RocketMQ 定时进行重平衡的,而不是像 Kafka 依赖心跳机制做实时重平衡,那么就会出现如果一个消费者宕机,最多需要 20s 才能触发重平衡,导致这段时间内消息堆积在已宕机的消费者上,影响吞吐。不过定时也有个好处就是避免很多网络抖动,或者频繁增加、退出消费者等导致的频繁的重平衡。
但是相比于Kafka,RocketMQ的重平衡机制最大的好处是STW的影响很小。
由于 RocketMQ 的消费者是通过 异步拉取然后再放到本地队列处理消息的,即使重平衡发生,每个消费者仍然可以继续消费它当前的队列中的消息,只要重平衡的时间足够短,就可以完全消除STW的发生,因为这段时间本地队列中消息还是在正常处理的。一旦重平衡好了,拉取的时候拉取新的队列的消息就行了。
还有就是RocketMQ 在消费者重平衡时是通过默认就是通过局部调整来完成的。当消费者变化时,只有受影响的消费者会重新分配消息队列,其他消费者不受影响。 (类似kafka的渐进式重平衡,但是RocketMQ默认就是这样的)
7、延时消息
7.1 RocketMQ如何实现延时消息?
RocketMQ是支持延迟消息的,延迟消息写入到Broker后,不会立刻被消费者消费,需要等待指定的时长后才可被消费处理的消息,称为延时消息。
当消息发送到Broker后,Broker会将消息根据延迟级别进行存储。RocketMQ的延迟消息实现方式是:将消息先存储在内存中,然后使用Timer定时器进行消息的延迟,到达指定的时间后再存储到磁盘中,最后投递给消费者。
但是,RocketMQ的延迟消息并不是支持任意时长的延迟的,它只支持(5.0之前):1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h这几个时长。
另外,RocketMQ 5.0中新增了基于时间轮实现的定时消息。
前面提到的延迟消息,并使用Timer定时器来实现延迟投递。但是,由于Timer定时器有一定的缺陷,比如在定时器中有大量任务时,会导致定时器的性能下降,从而影响消息投递。
因此,在RocketMQ 5.0中,采用了一种新的实现方式:基于时间轮的定时消息。时间轮是一种高效的定时器算法,能够处理大量的定时任务,并且能够在O(1)时间内找到下一个即将要执行的任务,因此能够提高消息的投递性能。
并且,基于时间轮的定时消息能够支持更高的消息精度,可以实现秒级、毫秒级甚至更小时间粒度的定时消息。
具体实现方式如下:
-
RocketMQ在Broker端使用一个时间轮来管理定时消息,将消息按照过期时间放置在不同的槽位中,这样可以大幅减少定时器任务的数量。
-
时间轮的每个槽位对应一个时间间隔,比如1秒、5秒、10秒等,每次时间轮的滴答,槽位向前移动一个时间间隔。
-
当Broker接收到定时消息时,根据消息的过期时间计算出需要投递的槽位,并将消息放置到对应的槽位中。
-
当时间轮的滴答到达消息的过期时间时,时间轮会将该槽位中的所有消息投递给消费者。
8、RocketMQ消息堆积了怎么解决?
RocketMQ的消息堆积,一般都是因为客户端本地消费过程中,由于消费耗时过长或消费并发度较小等原因,导致客户端消费能力不足,出现消息堆积的问题。
首先,需要明确的是,MQ堆积是正常的,因为MQ有个重要的作用就是削峰填谷,既然他能起到削峰填谷的作用,那就意味着他需要帮你去接收更多的消息,然后放到自己的队列里面,下游再慢慢消费。所以,出现堆积的情况不要慌,也不一定要立刻就进去解决。
所以,MQ堆积的这个问题一旦发生了,比如线上有告警了,处理过程应该是:
1、先去定位具体什么场景,哪个topic的消息堆积了
2、看下当前的堆积情况是否严重,是否在减缓
3、查看上游流量情况,是否有营销活动,或者定时任务在运行
4、分析下堆积导致的延迟是否可以接受
5、考虑扩容增加消费者提升消费速度
6、优化代码,进一步解决堆积的问题
常见的解决方式如下:
1、考虑换成批量拉消息
如果是消息的投递速度慢的话,可能是MQ用的不对,使用了单条拉消息的方案,其实RocketMQ是支持批量拉消息的,可以考虑换成批量拉消息的方案,通过调整ConsumeMessageBatchMaxSize的值来拉取批量消息,默认32条一次,可以调整到更高。还可以调整BatchConsumeMaxAwaitDurationInSeconds来设置批量消费的最大等待时长。
2、引入线程池并发消费
单线程存在瓶颈,这种也比较常见,那比较典型的方案就是引入线程池来进行并发消费,让多线程一起来干活。这种要配合批量消息来做,并且需要考虑如果有某个线程失败了,导致消息丢失的问题。
3、查看是否有慢SQL
针对慢SQL的情况,有的时候线上跑的好好地,一直都没有慢SQL,但是突然有一天就有了慢SQL了,大部分原因是因为数据量积累变多了,导致表变大了,CRUD都变慢了。这时候就需要从数据库层面优化了,比如做数据归档、分库分表、增加索引等方式,提升SQL速度。
4、查看是否下游RT变长
下游RT变长,这种一般是在MQ消费过程中需要调外部服务,而外部服务的RT比较长导致的, 这种一方面是让下游做优化,提升他的RT。要不然就可以和下一个问题解决方案一样。
5、先将消息存本地,定时任务慢慢处理
最后一个就是整个MQ消费的处理流程长,可能是因为SQL慢但是又不好治理,或者前面说的下游RT长,或者就是要干的事情多,这种情况,可以采用一种方案 ,那就是先收单,然后再通过定时任务慢慢处理的方式。
参考链接:
1、https://www.yuque.com/hollis666/wk6won/fkx1hga7xlpbfbuv
2、https://www.yuque.com/hollis666/wk6won/abxh7z
3、https://www.yuque.com/hollis666/wk6won/nt1ishhbunfo0g86
4、https://www.yuque.com/hollis666/wk6won/txw2gxr6utxggu60
5、https://www.yuque.com/hollis666/wk6won/vo0eif0x171805pt