官方网站:https://rocketmq.apache.org/
GitHub地址:https://github.com/apache/rocketmq
RocketMQ介绍
Apache RocketMQ是一个分布式消息传递和流媒体平台,具有低延迟,高性能和可靠性,万亿级容量和灵活的可伸缩性。
它具有多种功能:
- 消息模式包括发布/订阅,请求/答复和流式传输
- 财务级交易消息
- 基于DLedger的内置容错和高可用性配置选项
- 各种跨语言客户端,例如Java,C / C ++,Python,Go
- 可插拔的传输协议,例如TCP,SSL,AIO
- 内置消息跟踪功能,还支持开放式跟踪
- 多功能的大数据和流生态系统集成
- 按时间或偏移量追溯消息
- 可靠的FIFO和严格有序的消息传递在同一队列中
- 高效的推拉消费模型
- 单个队列中的百万级消息累积容量
- 多种消息传递协议,例如JMS和OpenMessaging
- 灵活的分布式横向扩展部署架构
- 快如闪电的批量消息交换系统
- 各种消息过滤器机制,例如SQL和Tag
- 用于隔离测试和云隔离群集的Docker映像
- 功能丰富的管理仪表板,用于配置,指标和监视
- 认证与授权
- 免费的开源连接器,适用于源和接收器
RocketMQ服务端安装
这里仅仅演示在window平台下的RocketMQ安装
- 首先是下载RocketMQ,4.7.0版本的下载地址在此
下面第一个链接是源码,第二个链接是可执行文件,因为我们是在window下做测试,可以直接下载第二个压缩包
- 解压压缩包,打开文件夹的bin目录
- 设置环境变量
创建一个名为ROCKETMQ_HOME的变量,值就是我们刚刚解压的压缩包的地址
如果没有配置这个环境变量,那么在启动rocketmq时就会报如下的错误
- 启动nameserver服务端
在bin目录中双击mqnamesrv.cmd就可以启动nameserver服务端
- 启动broker服务端
同样是在bin目录下通过如下的命令就可以启动broker服务端
start mqbroker.cmd -n 127.0.0.1:9876 -c ../conf/broker.conf
至此,RocketMQ就已经完全启动了
管理控制台的安装
同样RocketMQ也存在管理控制台,下面就是它的github地址
- 下载github上的rocketmq-externals项目
- 打开文件夹中的rocketmq-console项目,修改配置
找到resources文件夹中的application.properties配置文件
主要是将我们刚刚启动配置的地址写上
- 通过maven将项目打包
在项目的根目录执行下面的命令,注意一定要加上-DskipTests用来跳过测试,因为测试很有可能会报错。
mvn package -DskipTests
下面就是我们打包好后的jar包
- 运行jar包,启动管理台
通过下面的命令就可以启动管理台
java -jar rocketmq-console-ng-1.0.1.jar
可以看到这个管理台本身也是个spring boot项目
- 访问管理台页面
直接通过浏览器访问http://localhost:8080就可以访问RocketMQ管理台的页面
至此,RocketMQ管理台就安装好了,下面也不会过多的使用这个管理台,所以就不再详细介绍了。
RocketMQ架构
下面就是RocketMQ的基本架构,前面启动的时候分别启动的两个服务端就是下面图中的nameserver和broker服务端。
- nameserver:此服务端负责接收生产者发送过来的消息,并把消息通过topic值再发送给broker服务端,同时nameserver会和broker进行心跳连接,保证可用性。相当于注册中心,负责路由消费者和生产者的请求。
- broker:负责接收从nameserver发送过来的消息,并把消息存储在本地。
下面是topic的基本结构,topic就是用来区分消息的种类的,在我们生产消息或者消费消息时都需要指定,这些topic都是存储在broker中的,而且namesever则会通过topic来指定存储在那个broker中。
不过需要注意的是topic中并不是单一的队列,其中默认存在八个队列,4个读队列,4个写队列,写入的数据会随机放入队列,也可以指定某个队列,而且在消费端都是通过线程池来消费消息的,默认是开启20个线程去以平均的方式消费队列,所以一般情况下我们消费消息时它们都是乱序的。当然我们可以通过api使消息按顺序被消费。
在消息中间件中消费者有一个重要的概念,就是消息的消费方式,一般存在两种,分别使推和拉
- 推:由服务端主动推送给客户端,实现核心需要客户端与服务端保持长连接。因为这是由服务端直接推送给客户端,对于客户端的状态服务端是不知道的,有可能客户端并不需要获取消息,但是服务端还在推送,甚至是客户端处于接收不到消息的状态。
- 拉:客户端自己主动去服务端获取消息,一般会定时获取。因为需要定时获取,可能多数情况下服务端并没有接收到消息,而消费者还在拉取消息,这样就获取不到任何消息,还会浪费带宽。
这两种方式都有各自的缺陷,所以RocketMQ推出了一种推加拉的机制获取数据,也是定时从服务端拉取消息,但是会和服务端保持的长连接,默认是5秒钟。
RocketMQ客户端的基本使用
官方案例
https://rocketmq.apache.org/docs/simple-example/
这里只介绍RocketMQ的java客户端的使用
引入依赖
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.3.0</version>
</dependency>
简单示例
- 生产者
public class SyncProducer {
public static void main(String[] args) throws Exception {
//Instantiate with a producer group name.
DefaultMQProducer producer = new
DefaultMQProducer("please_rename_unique_group_name");
// Specify name server addresses.
producer.setNamesrvAddr("localhost:9876");
//Launch the instance.
producer.start();
for (int i = 0; i < 100; i++) {
//Create a message instance, specifying topic, tag and message body.
Message msg = new Message("TopicTest" /* Topic */,
"TagA" /* Tag */,
("Hello RocketMQ " +
i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
);
//Call send message to deliver message to one of brokers.
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
}
//Shut down once the producer instance is not longer in use.
producer.shutdown();
}
}
- 消费者
public class Consumer {
public static void main(String[] args) throws InterruptedException, MQClientException {
// Instantiate with specified consumer group name.
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name");
// Specify name server addresses.
consumer.setNamesrvAddr("localhost:9876");
// Subscribe one more more topics to consume.
consumer.subscribe("TopicTest", "*");
// Register callback to execute on arrival of messages fetched from brokers.
consumer.registerMessageListener(new MessageListenerConcurrently() {
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,ConsumeConcurrentlyContext context) {
for (MessageExt msg : msgs){
System.out.printf("%s Receive New Messages: %s %n",Thread.currentThread().getName(), msg);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//Launch the consumer instance.
consumer.start();
System.out.printf("Consumer Started.%n");
}
}
RocketMQ的应用
实现消息有序消费
在上面就已经说了因为生产者和消费者的关系,所以消息一般都是乱序的,但是我们也可以通过一些api使得消息变得有序,其实我们只要满足以下两个条件就可以使得消息队列变得有序。
- 生产者在将消息发送给服务端时指定将消息发送到指定的topic中的某一个写队列中。
- 消费者在消费消息时只用单线程去获取消息。
上面两个条件都可以通过api实现
- 生产者
public static void main(String[] args) throws Exception {
//Instantiate with a producer group name.
DefaultMQProducer producer = new
DefaultMQProducer("please_rename_unique_group_name");
// Specify name server addresses.
producer.setNamesrvAddr("localhost:9876");
//Launch the instance.
producer.start();
for (int i = 0; i < 100; i++) {
//Create a message instance, specifying topic, tag and message body.
Message msg = new Message("TopicTest" /* Topic */,
"TagA" /* Tag */,
("Hello RocketMQ " +
i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
);
//Call send message to deliver message to one of brokers.
// 第二个参数可以指定如何选择把消息存储在哪一个队列中,这里时传递了一个根据hash选择的对象,最后一个参数就是需要hash的数据
// 这里因为要把消息全部指定到同一个队列中,所以可以直接传递固定值
SendResult sendResult = producer.send(msg,new SelectMessageQueueByHash(),"1");
System.out.printf("%s%n", sendResult);
}
//Shut down once the producer instance is not longer in use.
producer.shutdown();
}
指定一个选择队列的规则,而不是让消息通过默认的随机分配队列了,这里使用的是通过hash的方式分配队列。
- 消费者
public static void main(String[] args) throws InterruptedException, MQClientException {
// Instantiate with specified consumer group name.
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name");
// Specify name server addresses.
consumer.setNamesrvAddr("localhost:9876");
// Subscribe one more more topics to consume.
consumer.subscribe("TopicTest", "*");
// Register callback to execute on arrival of messages fetched from brokers.
// 单线程有序消费
consumer.registerMessageListener(new MessageListenerOrderly() {
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext consumeOrderlyContext) {
for (MessageExt msg : msgs){
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msg);
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
// 多线程无序消费
// consumer.registerMessageListener(new MessageListenerConcurrently() {
//
// public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
// ConsumeConcurrentlyContext context) {
// for (MessageExt msg : msgs){
// System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msg);
// }
// return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
// }
// });
//Launch the consumer instance.
consumer.start();
System.out.printf("Consumer Started.%n");
}
消费者最重要的就是对于消费者对象注册的消息监听器,之前我们注册的是MessageListenerConcurrently,这个采用的是多线程消费消息,所以是无序的。如果我们注册的是MessageListenerOrderly,它就是单线程消费消息的,所以能保证消息的顺序。
需要注意的是当我们吧消费者停掉然后在启动的时候就会出现延迟(此时消费者组里面发生了异常),当消费者组发生变化会触发队列重平衡(每隔20s去检查一下是否需要重平衡当队列重平衡的时候是不能消费),只有用单线程单队列的消费模式的时候才会出现延迟。
这是因为消费者每隔20s去锁定一次属于自己的队列,当队列锁定了之后就不会给其他消费消费,当他本次拉取消息消费完之后解锁。