什么是 RocketMQ?
RocketMQ 是众多 MQ 中的一种,属于Alibaba旗下,使用JAVA语言开发的一款消息中间件,具有高性能、高可靠、高实时、分布式特点。MQ的全称是 Message Queue 消息队列,使用较多的消息队列有ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ。
RocketMQ的重要组成部分
现实生活中的邮政系统要正常运行,离不开下面这四个角色, 一是发信者, 二是收信者, 三是负责暂存、传输的邮局, 四是负责协调各个地方邮局的管理机构。对应到 RocketMQ 中,这四个角色就是Producer、Consumer、Broker和NameServer。
启动 RocketMQ的顺序是先启动 NameServer ,再启动 Broker ,这时候消息队列已可以提供服务了,想发送消息就使用 Producer 来发送,想接收消息 就使用 Consumer 来接收。
NameServer可以看做注册中心,它主要建立起了Producer、Consumer和Broker之间的连接。
Broker可以看做暂存、传输消息的容器,它启动时会向NameServer注册自己的信息。
Producer就是发信者了,它需要先连接NameServer,然后从上面获取到Broker的信息并发送消息到Broker,如果存在多个Broker,NameServer会通过负载均衡的拉取Broker的信息。
Consumer是收信者,它同样需要先连接NameServer,然后从上面获取到Broker的信息并消费容器中的消息。
轮询
轮询是按照某种算法进行顺序触发,轮询时会保存下一个待分配任务的索引,以便于下次执行时可以拿到开始索引位置,以达到负载均衡的目的。
轮询算法是一种无状态的负载均衡策略,它不需要了解每个节点的当前负载或性能状态。这意味着即使某些节点可能比其他节点更繁忙或性能更低,轮询算法仍然会按照固定的顺序分配任务。
比如有 1,2,3,4四个数值,现在轮询输出这四个数值,具体从哪一个开始是随机的,可能是1也可能是3,但是一旦开始以后就是按照顺序执行了。
代码实现:
public static void main(String[] args) {
polling(); // 简单的轮询
}
private static void polling() {
int[] ints = {
1,2,3,4};
// index 的区间[0,4)
int index = new Random().nextInt(4);
// 这里为了测试写死总共轮询10次
for (int i = index; i < 10; i++) {
// 下一个轮询索引(开始位置)
int nextIndex = (index + i) % 4; // 取模后nextIndex的区间只会在[0,4)
System.out.println("依次输出:" + ints[nextIndex]);
}
}
负载均衡
这里主要通俗易懂的讲一下负载均衡的概念,可以实现负载均衡的方式有很多,这里不做赘述。
假如你现在只有一个服务器运行着项目,本来没有什么问题,但是因为用户量不停的增加,导致服务器承受不住了,再这样下去项目要垮了。这个时候就可以多买几台服务器分别部署项目,分别部署了以后还要保证用户的请求可以均匀的分发到每一台服务器上。
这个时候就有人发明了负载均衡机制,它可以通过某种算法将用户的请求均匀的发布到每一台服务器上面,并且如果其中某台服务器宕机了,它会在尝试发送几次无果以后跳过该服务器发送到还在存活的服务器上。
比如Nginx就是专门处理负载均衡的软件,在Nginx上面可以部署多个服务器,用户发送的请求会先到Nginx服务,然后Nginx再通过负载均衡算法请求到对应服务器上面去。
NameServer/Broker
启动RocketMQ时需要先启动NameServer,它相当于一个Broker平台,生产者和消费者想要发送或者消费消息时都需要到Broker平台上面获取Broker容器的IP地址建立连接。
NameServer主要是通过负载均衡算法去获取Broker的信息, NameServer 与每台 Broker 服务保持长连接,并间隔 30S 检查 Broker 是否存活,如果检测到 Broker 宕机,则从路由注册表中将其移除。
Topic(主题)
标识一类消息的逻辑名字,消息的逻辑管理单位。无论消息生产还是消费,都需要指定 Topic。 区分消息的种类;一个发送者可以发送消息给一个或者多个 Topic;一个消息的接收者可以订阅一个或者多个 Topic 消息 。
Topic 保存在 Broker 容器内,而每个Topic下都会有一个或多个队列,队列的数量可以通过客户端进行配置,这个队列就是用来存放消息的。
Message Queue(消息队列)
简称 Queue 或 Q。消息物理管理单位。一个 Topic 将有若干个 Q。若一个 Topic 创建在不同的 Broker,则不同的 broker 上都有若干 Q,消息将物理地 存储落在不同 Broker 结点上,具有水平扩展的能力。
无论生产者还是消费者,实际的生产和消费都是针对 Q 级别。例如 Producer 发送消息的时候,会预先选择(默认轮询)好该 Topic 下面的某一条 Q 发送;Consumer 消费的时候也会负载均衡地分配若干个 Q,只拉取对应 Q 的消息。 每一条 message queue 均对应一个文件,这个文件存储了实际消息的索引信息。并且即使文件被删除,也能通过实际纯粹的消息文件(commit log) 恢复回来
commit log文件
生产者发送消息并不是直接发送到消息队列中去的,而是以顺序写入的方式发送到commit log文件,commit log文件是MQ创建的一个大概1G的连续内存空间,如果一个commit log文件满了以后会再次分配1G创建一个commit log文件。
生产者发送消息到commit log文件,commit log文件保存消息的具体内容,然后把消息的offset、msgSize、tags给到消息队列。当像消费者投递消息时Broker会根据offset找到commit log文件对应消息的具体位置将消息投递给消费者。
- offset:是消息在Commit Log中的起始位置,用于定位消息。
- msgSize:表示消息的大小,用于读取消息时确定读取的长度。
- tags:是消息的标签,用于消息的过滤和查询。
Tags(标签)
虽然消息可以根据不同Topic进行了分类,但是有时候业务可能还需要根据同一个Topic下的消息再进行更细化的分类,这个时候就可以使用Tags属性,Tags可以理解为为每个Topic下的消息设一个标签,生产者和消费者可以订阅某个主题下的某个Tags下的所有消息,而不是某个主题下的全部消息。
Producer(生产者)
生产者也称为消息发布者,负责生产并发送消息至 RocketMQ。
下面是一个生产者发送消息的过程图解:
Consumer(消费者)
消费者也称为消息订阅者,负责从 RocketMQ 拉取消息。
当只有一个消费者订阅某个主题的消息进行消费时,这个消费者会消费这个主题下所有队列的消息,如下图:
当有两个消费者订阅某个主题的消息进行消费时,Broker会采用负载均衡的方式给消费者重新分配队列,如下图:
可以看到,broker会均匀的分配队列给消费者,Consumer1只消费0、2队列里面的消息,Consumer2只消费1、3队列里面的消息,这也会导致0、2队列中的消息Consumer2一直也消费不到。
如果再新增一个消费者那么就总会有一个消费者只能消费一个队列中的消息,以此类推。这里需要注意,假如现在的队列是四个,而同一个消费组里面的消费者有五个,那么会有一个队列被两个消费者消费吗?答案并不会。第五个消费者会一直都无法消费到消息。所以同一消费者组下的消费者的数量要小于等于消费主题下队列的数量。
ConsumerGroup(消费者组)
当想要创建一个消费者的时候,必须要给消费者定义一个组名(生产者也一样),多个消费者可以使用同一个组名进行消费。同一个消费者组中的Consumer和不同消费者组的Consumer消费消息的方式是不太一样的。
从上面图示可以看到,同一消费者组下的Consumer消费方式是负载均衡的,它还有一个名称叫做集群模式,一条消息只能被一个Consumer消费,而不是投递给每一个Consumer。
在面对不同消费组订阅同一个Topic时,Broker会把消息分别投送给消费者组,然后再根据消费者组内Consumer的分配关系进行消息分配,每一个消费者组都会消费全量消息。
集群模式和广播模式
消费者可以分为两种消费模式,分别是集群模式和广播模式,默认情况下统一消费者组下的消费者采取的是集群模式,消费者分担消费消息。
- 集群模式:我们可以通过以下代码来设置采用集群模式,RocketMQ Push Consumer默认为集群模式,同一个消费组内的消费者分担消费。
consumer.setMessageModel(MessageModel.CLUSTERING);
- 广播模式:通过以下代码来设置采用广播模式,广播模式下,消费组内的每一个消费者都会消费全量消息。
consumer.setMessageModel(MessageModel.BROADCASTING);
生产者代码:
@Test
void contextLoads() throws Exception {
// 创建生产者并设置组名
DefaultMQProducer mqProducer = new DefaultMQProducer("rocketmq-producer");
// 连接 name server
mqProducer.setNamesrvAddr(MQConstants.NAME_SERVER);
// 启动生产者
mqProducer.start();
List<Order> orders = createOrder();
for (Order order : orders) {
// 创建消息
Message message = new Message("test-topic", order.toString().getBytes());
message.setKeys(String.valueOf(order.getOrderId()));
// 发送携带Key值的消息
SendResult send = mqProducer.send(message);
System.out.println("发送消息状态:" + send.getSendStatus());
}
// 关闭生产者
mqProducer.shutdown();
}
public List<Order> createOrder(){
List<Order> orders = Arrays.asList(
new Order(1001, "新增订单"),
new Order(1002, "新增订单"),
new Order(1003, "新增订单")
);
return orders;
}
/**
* 订单类
**/
class Order{
private Integer orderId; // 订单号
private String orderDesc; // 订单描述
public Order(Integer orderId, String orderDesc) {
this.orderId = orderId;
this.orderDesc = orderDesc;
}
public Integer getOrderId() {
return orderId;
}
public void setOrderId(Integer orderId) {
this.orderId = orderId;
}
public String getOrderDesc() {
return orderDesc;
}
public void setOrderDesc(String orderDesc) {
this.orderDesc = orderDesc;
}
@Override
public String toString() {
return "Order{" +
"orderId=" + orderId +
", orderDesc='" + orderDesc + '\'' +
'}';
}
}
消费者设置为集成模式,在相同消费者组内的多个消费者场景:
@Test
void consumer1() throws Exception {
// 创建一个消费者并设置组名
DefaultMQPushConsumer mqPushConsumer = new DefaultMQPushConsumer("rocketmq-consumer");
// 连接 name server
mqPushConsumer.setNamesrvAddr(MQConstants.NAME_SERVER);
// 订阅主题 "*" 表示订阅所有消息
mqPushConsumer.subscribe("test-topic", "*");
// 消费者模式:集群模式
mqPushConsumer.setMessageModel(MessageModel.CLUSTERING);
// 设置监听器用来监听消息
mqPushConsumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
MessageExt messageExt = list.get(0);
String body = new String(messageExt.getBody()); // 获取消息体
System.out.println("consumer1 消费到的消息:=> " + body);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 启动消费者服务
mqPushConsumer.start();
// jvm 挂载
System.in.read();
}
控制台输出:
consumer1 消费到的消息:=> Order{
orderId=1003, orderDesc='新增订单'}
@Test
void consumer2() throws Exception {
// 创建一个消费者并设置组名
DefaultMQPushConsumer mqPushConsumer = new DefaultMQPushConsumer("rocketmq-consumer");
// 连接 name server
mqPushConsumer.setNamesrvAddr(MQConstants.NAME_SERVER);
// 订阅主题 "*" 表示订阅所有消息
mqPushConsumer.subscribe("test-topic", "*");
// 消费者模式:集群模式
mqPushConsumer.setMessageModel(MessageModel.CLUSTERING);
// 设置监听器用来监听消息
mqPushConsumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
msgs.stream().forEach(messageExt -> {
System.out.println("consumer2 消费到的消息 => " + new String(messageExt.getBody()));
});
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
});
// 启动消费者服务
mqPushConsumer.start();
// jvm 挂载
System.in.read();
}
控制台输出:
consumer2 消费到的消息 => Order{
orderId=1001, orderDesc='新增订单'}
consumer2 消费到的消息 => Order{
orderId=1002, orderDesc='新增订单'}
消费者设置为广播模式,在相同消费者组内的多个消费者场景:
@Test
void consumer1() throws Exception {
// 创建一个消费者并设置组名
DefaultMQPushConsumer mqPushConsumer = new DefaultMQPushConsumer("rocketmq-consumer");
// 连接 name server
mqPushConsumer.setNamesrvAddr(MQConstants.NAME_SERVER);
// 订阅主题 "*" 表示订阅所有消息
mqPushConsumer.subscribe("test-topic", "*");
// 消费者模式:广播模式
mqPushConsumer.setMessageModel(MessageModel.BROADCASTING);
// 设置监听器用来监听消息
mqPushConsumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
MessageExt messageExt = list.get(0);
String body = new String(messageExt.getBody()); // 获取消息体
System.out.println("consumer1 消费到的消息:=> " + body);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 启动消费者服务
mqPushConsumer.start();
// jvm 挂载
System.in.read();
}
控制台输出:
consumer1 消费到的消息:=> Order{
orderId=1001, orderDesc='新增订单'}
consumer1 消费到的消息:=> Order{
orderId=1002, orderDesc='新增订单'}
consumer1 消费到的消息:=> Order{
orderId=1003, orderDesc='新增订单'}
@Test
void consumer2() throws Exception {
// 创建一个消费者并设置组名
DefaultMQPushConsumer mqPushConsumer = new DefaultMQPushConsumer("rocketmq-consumer");
// 连接 name server
mqPushConsumer.setNamesrvAddr(MQConstants.NAME_SERVER);
// 订阅主题 "*" 表示订阅所有消息
mqPushConsumer.subscribe("test-topic", "*");
// 消费者模式:广播模式
mqPushConsumer.setMessageModel(MessageModel.BROADCASTING);
// 设置监听器用来监听消息
mqPushConsumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
msgs.stream().forEach(messageExt -> {
System.out.println("consumer2 消费到的消息 => " + new String(messageExt.getBody()));
});
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 启动消费者服务
mqPushConsumer.start();
// jvm 挂载
System.in.read();
}
控制台输出:
consumer2 消费到的消息 => Order{
orderId=1001, orderDesc='新增订单'}
consumer2 消费到的消息 => Order{
orderId=1002, orderDesc='新增订单'}
consumer2 消费到的消息 => Order{
orderId=1003, orderDesc='新增订单'}
订阅关系一致
订阅关系:一个消费者组订阅一个 Topic 的某一个 Tag,这种记录被称为订阅关系。
订阅关系一致:同一个消费者组下所有消费者实例所订阅的Topic、Tag必须完全一致。如果订阅关系(消费者组名-Topic-Tag)不一致,会导致消费消息紊乱,甚至消息丢失。
RocketMQ 可以解决什么问题?
- 削峰限流
比如我们把项目部署到Tomcat服务上启动,假如Tomcat最大并发量只有四百,这个时候如果有上千个请求并发过来,那么超过服务器限制的请求就会被放弃,也就是我们常见的浏览器抛出503提示服务不可用。
那么使用RocketMQ就可以设置一个阈值,将超出的请求缓存起来,等后面服务器可以接受新的请求后再进行处理。
- 异步
在复杂的业务逻辑中,有些操作可能不需要立即返回结果,或者它们的执行时间较长,可能会阻塞主流程的执行。
通过RocketMQ,我们可以将这些耗时操作以异步的方式发送到消息队列中,让后台线程或独立的服务去处理它们。这样,主流程可以迅速返回,提高系统的响应速度。同时,异步处理还可以提高系统的吞吐量,因为多个操作可以并行执行,而不是串行等待。
- 解耦
在微服务架构或分布式系统中,服务之间的依赖关系可能非常复杂。当某个服务出现故障时,可能会影响到其他服务的正常运行。RocketMQ通过消息队列实现服务之间的松耦合通信。
发送方将消息发送到队列,而不需要关心接收方的具体实现和状态。接收方可以根据自己的需求订阅相应的队列,并独立处理接收到的消息。这种方式降低了服务之间的耦合度,提高了系统的灵活性和可维护性。
生产者
同步消息
同步发送是最常用的方式,是指消息发送方发出一条消息后,会在收到服务端同步响应之后才发下一条消息的通讯方式,可靠的同步传输被广泛应用于各种场景,如重要的通知消息、短消息通知等。
生命周期:
- 初始化:消息被生产者构建并完成初始化,待发送到服务端的状态。
- 待消费:消息被发送到服务端,对消费者可见,等待消费者消费的状态。
- 消费中:消息被消费者获取,并按照消费者本地的业务逻辑进行处理的过程。 此时服务端会等待消费者完成消费并提交消费结果,如果一定时间后没有收到消费者的响应,Apache RocketMQ会对消息进行重试处理。具体信息,请参见消费重试。
- 消费提交:消费者完成消费处理,并向服务端提交消费结果,服务端标记当前消息已经被处理(包括消费成功和失败)。 Apache RocketMQ默认支持保留所有消息,此时消息数据并不会立即被删除,只是逻辑标记已消费。消息在保存时间到期或存储空间不足被删除前,消费者仍然可以回溯消息重新消费。
- 消息删除:Apache RocketMQ按照消息保存机制滚动清理最早的消息数据,将消息从物理文件中删除。更多信息,请参见消息存储和清理机制。
使用示例:
@Test
void producer() throws Exception{
// 1.创建生产者
DefaultMQProducer defaultMQProducer = new DefaultMQProducer("test-producer-group");
// 2.连接 name server
defaultMQProducer.setNamesrvAddr("localhost:9876");
// 3.启动生产者
defaultMQProducer.start();
// 4.创建消息并设置主题和消息体
Message message = new Message("test-topic", "测试消息".getBytes());
// 5.同步发送消息
for (int i = 0; i < 3; i++) {
System.out.println("开始发送第"+i+"条消息");
SendResult send = defaultMQProducer.send(message);
// 等待发送结果才能继续向下执行......
System.out.println("第"+i+"条消息发送状态:" + send.getSendStatus());
}
// 等待发送结果才能继续向下执行......
System.out.println("发送消息状态:" + send.getSendStatus());
System.out.println("继续下面业务逻辑...");
// 6.关闭生产者
defaultMQProducer.shutdown();
}
控制台输出:
发送原理:
- 生产者向MQ服务器发送消息
- MQ服务器收到消息后返回处理结果
- 生产者继续下一步…
❗备注 同步发送方式请务必捕获发送异常,并做业务侧失败兜底逻辑,如果忽略异常则可能会导致消息未成功发送的情况。
异步消息
异步发送是指发送方发出一条消息后,不等服务端返回响应,接着发送下一条消息的通讯方式。
❗备注 异步发送需要实现异步发送回调接口(SendCallback)。
消息发送方在发送了一条消息后,不需要等待服务端响应即可发送第二条消息,发送方通过回调接口接收服务端响应,并处理响应结果。异步发送一般用于链路耗时较长,对响应时间较为敏感的业务场景。例如,视频上传后通知启动转码服务,转码完成后通知推送转码结果等。
使用示例:
@Test
void producer() throws Exception{
// 1.创建生产者
DefaultMQProducer defaultMQProducer = new DefaultMQProducer("test-producer-group");
// 2.连接 name server
defaultMQProducer.setNamesrvAddr("localhost:9876");
// 3.启动生产者
defaultMQProducer.start();
// 在这里定义一个程序计数器,初始值为线程的数量
int messageCount = 3;
final CountDownLatch countDownLatch = new CountDownLatch(messageCount);
// 4.创建消息并设置主题和消息体
Message message = new Message("test-topic", "测试消息".getBytes());
// 5.异步发送
for (int i = 0; i < messageCount; i++) {
defaultMQProducer.send(message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println("线程["+Thread.currentThread().getName()+"]发送消息成功:" + sendResult.getSendStatus());
countDownLatch.countDown();
}