前言
前言简单介绍一下什么是消息中间件,消息中间件又叫消息队列。它是在网络环境中为不同应用系统之间同步、异步提供稳定的消息传输系统。 常见的消息中间件有:RocketMQ、Kafka、RabbitMQ、ActiveMQ……等。 这里主要介绍RocketMQ。
参考官方中文文档
后文附简单示例
一、RocketMQ
消息模型
RocketMQ的消息模型是由:Producer、Broker、Consumer 三部分组成,其中Producer 负责生产消息,Consumer 负责消费消息,Broker 负责存储、投递、查询消息。
架构设计
RocketMQ的整体架构设计是由:Producer、Broker、Consumer、NameServer 四种角色组成。额外多出来一个NameServer相当于一个简易的“注册配置中心”,类似于kafka和zookeeper的关系
BrokeServer启动后会将自己的元信息注册到NameServer上,Product和Consumer会去NameServer上拉去Broke节点信息。然后进行相应的生产消息和消费消息的动作
消息存储
RocketMQ的消息存储是由Broke节点存储,它接收Product传递的消息后以同步或者异步刷盘的方式将消息顺序写入磁盘的commitLog文件中。只要消息持久化到commotLog后那么Product发送的消息就不会丢失
同步刷盘
只有在消息真正持久化至磁盘后RocketMQ的Broker端才会真正返回给Producer端一个成功的ACK响应。同步刷盘对MQ消息可靠性来说是一种不错的保障,但是性能上会有较大影响,一般适用于金融业务应用该模式较多。
异步刷盘
能够充分利用OS的PageCache的优势,只要消息写入PageCache即可将成功的ACK返回给Producer端。消息刷盘采用后台异步线程提交的方式进行,降低了读写延迟,提高了MQ的性能和吞吐量。
通信机制
RocketMQ消息队列集群主要包括NameServer、Broker(Master/Slave)、Producer、Consumer4个角色,这4个角色之间的相互通信时基于Netty框架的基础上封装的通信能力(rocketmq-remoting 模块)。
RocketMQ主要的通信流程为:
-
Broker启动后需要完成一次将自己注册至NameServer的操作;随后每隔30s时间定时向NameServer上报Topic路由信息。
-
消息生产者Producer作为客户端发送消息时候,需要根据消息的Topic从本地缓存的TopicPublishInfoTable获取路由信息。如果没有则更新路由信息会从NameServer上重新拉取,同时Producer会默认每隔30s向NameServer拉取一次路由信息。
-
消息生产者Producer根据2)中获取的路由信息选择一个队列(MessageQueue)进行消息发送;Broker作为消息的接收者接收消息并落盘存储。
-
消息消费者Consumer根据2)中获取的路由信息,并再完成客户端的负载均衡后,选择其中的某一个或者某几个消息队列来拉取消息并进行消费。
消息过滤
RocketMQ分布式消息队列的消息过滤方式有别于其它MQ中间件,是在Consumer端订阅消息时再做消息过滤的。主要支持如下2种的过滤方式
-
Tag过滤方式:Consumer端在订阅消息时除了指定Topic还可以指定TAG,如果一个消息有多个TAG,可以用||分隔。其中,Consumer端会将这个订阅请求构建成一个 SubscriptionData,发送一个Pull消息的请求给Broker端。Broker端从RocketMQ的文件存储层—Store读取数据之前,会用这些数据先构建一个MessageFilter,然后传给Store。Store从 ConsumeQueue读取到一条记录后,会用它记录的消息tag hash值去做过滤,由于在服务端只是根据hashcode进行判断,无法精确对tag原始字符串进行过滤,故在消息消费端拉取到消息后,还需要对消息的原始tag字符串进行比对,如果不同,则丢弃该消息,不进行消息消费。
-
SQL92的过滤方式:这种方式的大致做法和上面的Tag过滤方式一样,只是在Store层的具体过滤过程不太一样,真正的 SQL expression 的构建和执行由rocketmq-filter模块负责的。每次过滤都去执行SQL表达式会影响效率,所以RocketMQ使用了BloomFilter避免了每次都去执行。SQL92的表达式上下文为消息的属性。
负载均衡
RocketMQ中的负载均衡都在Client端完成,具体来说的话,主要可以分为Producer端发送消息时候的负载均衡和Consumer端订阅消息的负载均衡。
事务消息
RocketMQ采用了2PC的思想来实现了提交事务消息,同时增加一个补偿逻辑来处理二阶段超时或者失败的消息。
RocketMQ实现事务消息的大致方案,其中分为两个流程:正常事务消息的发送及提交、事务消息的补偿流程。
1.事务消息发送及提交:
(1) 发送消息(half消息)。
(2) 服务端响应消息写入结果。
(3) 根据发送结果执行本地事务(如果写入失败,此时half消息对业务不可见,本地逻辑不执行)。
(4) 根据本地事务状态执行Commit或者Rollback(Commit操作生成消息索引,消息对消费者可见)
2.补偿流程:
(1) 对没有Commit/Rollback的事务消息(pending状态的消息),从服务端发起一次“回查”
(2) Producer收到回查消息,检查回查消息对应的本地事务的状态
(3) 根据本地事务状态,重新Commit或者Rollback
其中,补偿阶段用于解决消息Commit或者Rollback发生超时或者失败的情况。
RocketMQ事务消息设计
1.事务消息在一阶段对用户不可见
在RocketMQ事务消息的主要流程中,一阶段的消息如何对用户不可见。其中,事务消息相对普通消息最大的特点就是一阶段发送的消息对用户是不可见的。那么,如何做到写入消息但是对用户不可见呢?RocketMQ事务消息的做法是:如果消息是half消息,将备份原消息的主题与消息消费队列,然后改变主题为RMQ_SYS_TRANS_HALF_TOPIC。由于消费组未订阅该主题,故消费端无法消费half类型的消息,然后RocketMQ会开启一个定时任务,从Topic为RMQ_SYS_TRANS_HALF_TOPIC中拉取消息进行消费,根据生产者组获取一个服务提供者发送回查事务状态请求,根据事务状态来决定是提交或回滚消息。
在RocketMQ中,消息在服务端的存储结构如下,每条消息都会有对应的索引信息,Consumer通过ConsumeQueue这个二级索引来读取消息实体内容,其流程如下:
RocketMQ的具体实现策略是:写入的如果事务消息,对消息的Topic和Queue等属性进行替换,同时将原来的Topic和Queue信息存储到消息的属性中,正因为消息主题被替换,故消息并不会转发到该原主题的消息消费队列,消费者无法感知消息的存在,不会消费。其实改变消息主题是RocketMQ的常用“套路”,回想一下延时消息的实现机制。
2.Commit和Rollback操作以及Op消息的引入
在完成一阶段写入一条对用户不可见的消息后,二阶段如果是Commit操作,则需要让消息对用户可见;如果是Rollback则需要撤销一阶段的消息。先说Rollback的情况。对于Rollback,本身一阶段的消息对用户是不可见的,其实不需要真正撤销消息(实际上RocketMQ也无法去真正的删除一条消息,因为是顺序写文件的)。但是区别于这条消息没有确定状态(Pending状态,事务悬而未决),需要一个操作来标识这条消息的最终状态。RocketMQ事务消息方案中引入了Op消息的概念,用Op消息标识事务消息已经确定的状态(Commit或者Rollback)。如果一条事务消息没有对应的Op消息,说明这个事务的状态还无法确定(可能是二阶段失败了)。引入Op消息后,事务消息无论是Commit或者Rollback都会记录一个Op操作。Commit相对于Rollback只是在写入Op消息前创建Half消息的索引。
3.Op消息的存储和对应关系
RocketMQ将Op消息写入到全局一个特定的Topic中通过源码中的方法—TransactionalMessageUtil.buildOpTopic();这个Topic是一个内部的Topic(像Half消息的Topic一样),不会被用户消费。Op消息的内容为对应的Half消息的存储的Offset,这样通过Op消息能索引到Half消息进行后续的回查操作。
4.Half消息的索引构建
在执行二阶段Commit操作时,需要构建出Half消息的索引。一阶段的Half消息由于是写到一个特殊的Topic,所以二阶段构建索引时需要读取出Half消息,并将Topic和Queue替换成真正的目标的Topic和Queue,之后通过一次普通消息的写入操作来生成一条对用户可见的消息。所以RocketMQ事务消息二阶段其实是利用了一阶段存储的消息的内容,在二阶段时恢复出一条完整的普通消息,然后走一遍消息写入流程。
5.如何处理二阶段失败的消息?
如果在RocketMQ事务消息的二阶段过程中失败了,例如在做Commit操作时,出现网络问题导致Commit失败,那么需要通过一定的策略使这条消息最终被Commit。RocketMQ采用了一种补偿机制,称为“回查”。Broker端对未确定状态的消息发起回查,将消息发送到对应的Producer端(同一个Group的Producer),由Producer根据消息来检查本地事务的状态,进而执行Commit或者Rollback。Broker端通过对比Half消息和Op消息进行事务消息的回查并且推进CheckPoint(记录那些事务消息的状态是确定的)。
值得注意的是,rocketmq并不会无休止的的信息事务状态回查,默认回查15次,如果15次回查还是无法得知事务状态,rocketmq默认回滚该消息。
简单示例
RocketMQ下载地址
先下载RocketMQ,先 nameSrv 后 mqbroke 的方式将两个组件运行起来
windows环境第一次下载启动namesrv会报错,提示你需要先配置环境变量
这里需要将rocketmq的配置,配到系统环境变量中
ROCKETMQ_HOME="rocketmq下载存放目录,例如:D:\rocketmq"
NAMESRV_ADDR="localhost:9876"
配置完成后正常启动namesrv
mqnamesrv.cmd
启动mqbroke
mqbroker.cmd -n localhost:9876 autoCreateTopicEnable=true
autoCreateTopicEnable=true 这个配置可以自动创建topic,否则生产者在发消息前需要手动添加topic
如启动broke时报错:加载不到主类。
此时找到bin目录下的runbroker.cmd文件修改。给%CLASSPATH%加一个引号就行
set "JAVA_OPT=%JAVA_OPT% -cp %CLASSPATH%"
改为
set "JAVA_OPT=%JAVA_OPT% -cp "%CLASSPATH%""
服务启动好了,现在编写代码。
创建一个项目,内含有product和consumer两个模块,并引入pom依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.9.1</version>
</dependency>
</dependencies>
RocketMQ发送一条消息至少要明确nameServer地址、生产者组、topic三个配置
rocketmq:
nameSrv: localhost:9876 #rockermq 的 nameserver默认端口号是9876
group: demoProductGroup
topic: demoTopic
消费者同理
rocketmq:
nameSrv: localhost:9876 #rockermq 的 nameserver默认端口号是9876
group: demoConsumerGroup
topic: demoTopic
编写生产者代码
package com.yxj.product.config;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RocketConfig {
@Value("${rocketmq.nameSrv}")
private String nameSrv;
@Value("${rocketmq.group}")
private String group;
@Value("${rocketmq.topic}")
private String topic;
@Bean
public DefaultMQProducer defaultMQProducer(){
DefaultMQProducer defaultMQProducer = new DefaultMQProducer(group);
defaultMQProducer.setNamesrvAddr(nameSrv);
try {
defaultMQProducer.start();
} catch (MQClientException e) {
e.printStackTrace();
}
return defaultMQProducer;
}
public String getTopic() {
return topic;
}
}
编写RocketMQTemplate
package com.yxj.product.config;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.client.producer.SendStatus;
import org.apache.rocketmq.common.message.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class RocketMQTemplate {
@Autowired
private RocketConfig rocketConfig;
@Autowired
private DefaultMQProducer defaultMQProducer;
public boolean sendNormalMessage(Message message) throws Exception{
SendResult send = defaultMQProducer.send(message);
return send.getSendStatus() == SendStatus.SEND_OK;
}
}
package com.yxj.product;
import com.yxj.product.config.RocketMQTemplate;
import org.apache.rocketmq.common.message.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.nio.charset.StandardCharsets;
@RestController
@RequestMapping("test")
public class TestController {
@Autowired
public RocketMQTemplate rocketMQTemplate;
@Value("${rocketmq.topic}")
private String topic;
@GetMapping("normalMessage")
public ResponseEntity sendNormalMessage(){
Message message = new Message(topic,"发生一条普通消息".getBytes(StandardCharsets.UTF_8));
try {
rocketMQTemplate.sendNormalMessage(message);
} catch (Exception e) {
e.printStackTrace();
return new ResponseEntity("send fail",HttpStatus.BAD_REQUEST);
}
return new ResponseEntity("send ok",HttpStatus.OK);
}
}
编写消费者代码
package com.yxj.consumer;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RocketConfig {
@Value("${rocketmq.nameSrv}")
private String nameSrv;
@Value("${rocketmq.group}")
private String group;
@Value("${rocketmq.topic}")
private String topic;
@Bean
public DefaultMQPushConsumer defaultMQPushConsumer(){
DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer(group);
defaultMQPushConsumer.setNamesrvAddr(nameSrv);
defaultMQPushConsumer.setConsumerGroup(group);
defaultMQPushConsumer.setMessageListener(new DefaultMessageListener());
try {
//消费者订阅topic
defaultMQPushConsumer.subscribe(topic,"*");
//启动消费者
defaultMQPushConsumer.start();
} catch (MQClientException e) {
e.printStackTrace();
}
return defaultMQPushConsumer;
}
}
消费者需要设置消费监听,rocketmq默认支持两种消费方式
顺序消费(MessageListenerOrderly)和随机消费(MessageListenerConcurrently)两种接口
我们需要自定义消费逻辑,所以要实现其中一个监听器实现自己的逻辑
package com.yxj.consumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.MessageExt;
import java.util.List;
public class DefaultMessageListener implements MessageListenerConcurrently {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), list);
for (int i = 0; i < list.size(); i++) {
MessageExt messageExt = list.get(i);
System.out.println("接收消息:"+new String(messageExt.getBody()));
}
// 标记该消息已经被成功消费
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
}
消费者代码比较简单,配置一下监听然后启动消费者就行。
然后分别启动生产者和消费者后,调用接口发送消息
消费结果:
至此,一个简单的消息发送和接收示例就完成了