RocketMQ详解
MQ简介
为什么要有MQ?
传统项目中,需要请求方去发送请求到响应方,响应方响应结果。但是如果响应方服务器宕机了就会立刻影响到请求方代码。
如果响应方服务器宕机,请求就会不成立,连接相当于就已经断开了。
当请求方和响应方需要进行业务通讯时,请求方不发请求给响应方也就是(调用方),而是发给MQ,再由MQ将消息发送给响应方。如果响应方服务器宕机,而请求方已经将消息发送给MQ了,请求方任务已经结束。此时请求方并不知道响应方是谁,响应方也不知道请求方是谁,它们只靠MQ进行通信。
某种程度来讲是对两个微服务之间的通讯进行了解耦,两台服务器之间没有那么强的耦合性了。
MQ(Message Queue)消息队列
是一种用来保存消息数据的队列(队列是数据结构的一种,特征为“先进先出”)。
消息
服务器之间的业务请求
原始架构:服务器中的A功能需要调用B、C模块才能完成。
微服务架构:服务器A向服务器B发送要执行的操作(视为消息);服务器A向服务器C发送要执行的操作(视为消息)。
MQ的作用
应用解耦(本质:异步消息发送)
服务器A将消息发送给MQ,发起业务方A和接收业务方B和C进行了有效解耦,A不能感知B和C的存在。
本质
原来A服务器去调B、C服务器都是同步的,现在将消息交给MQ,相当于是异步消息发送。
快速应用变更维护(本质:异步消息发送)
当要进行业务变更新增D,业务必须有B、C、D三个服务同时完成。
只需要服务器A多发送一个消息,服务器D接进来,收到消息就可以工作。
当出现业务变更,将其中一台服务替代或者下线时,此时只需要A少发送一个消息到MQ中。
MQ工作的本质
当消息的生产者将消息发送到MQ中时,MQ中有个地方专门用来保存发送过来的消息,每发送一次就记录一次。使用消息的时候是从指定的位置读取对应的消息(类似偏移量的概念),其实并没有消息进来再删除掉这样的过程。只是通过消息的存储偏移量来描述消息的消费。
流量削峰(本质:异步消息发送)
比如,双11流量高峰期,在某个时间点服务器A的qps达到了4000,消息量忽然增大(和业务处理方B相比),当服务器B与MySQL打交道时,MySQL的qps只能达到1000,如果没有MQ,4000个请求一起过来,服务器基本就挂掉了,根本就处理不过来。
如果有MQ,每次发过去1000,让服务器进行处理,处理完再发1000过去,反复完成,服务器就可以解决这个问题。
举例:双11下完订单的时候并不是立刻生成运单和订单,MQ是慢慢消费这些消息的。
MQ的缺点
-
系统可用性降低
两台服务器通过MQ进行通讯,如果MQ挂了,两台服务器都无法工作。解决:集群
-
系统复杂度提高
-
异步消息机制
-
消息顺序性
某些特定场合需要保证消息的顺序性,MQ中存放着大量的消息,如何保证消息的顺序性。
-
消息丢失
MQ挂了,里面的消息丢了。
-
消息一致性
比如一组操作需要两台服务器配合完成,发两个消息,A服务器ok,但是B服务器挂了。相当于,订单生成了,但是运单没了。同样是对于订单的一组东西。异步消息会存在,同步消息不会存在这个问题。
-
消息重复使用
-
潜在问题:
消息的生产者发送给MQ的过程中,消息丢失怎么办?
MQ宕机,消息丢失怎么办?
如果保证消息的顺序性?
MQ在发给消费者的时候,消息丢失怎么办?
如何保证消息不重复消费?
常用MQ产品
- ActiveMQ:java语言实现,万级数据吞吐量,处理速度ms级,主从架构,成熟度高
- RabbitMQ:erlang语言实现,万级数据吞吐量,处理速度us级,主从架构,
- RocketMQ:java语言实现,十万级数据吞吐量,处理速度ms级,分布式架构,功能强大,扩展性强
- kafka:scala语言实现,十万级数据吞吐量,处理速度ms级,分布式架构,功能较少,应用于大数据较多
基本概念
消息的生产者负责生产消息,生产出来的消息给Broker(消息服务器)。有了消息服务器,消息的生产者就可以发送消息给消息服务器,同时返回回去你的消息我接收到了。消息的消费者负责消费消息。
常见的两种模式:1.消息的消费者发送一个请求过去,消息服务器给消费者一个消息,消费者进行消费,这种模式较少。2.监听器监听消息服务器中是否有消息,如果有消息就直接推送给消息的消费者(就像滴滴,直接推送给司机,司机没有拒绝的权利。美团也是同样,经常会听到店家的美团消息:您有新的外卖订单,请及时处理。商家也没有拒绝的权利)。第二种模式较多,是主流的消息消费方式。
生产者和消费者一般都是以集群形式,集群可以让工作更加高效。不然仅有的一台服务器挂了,就不能正常工作了。
消息服务器集群的工作包含:消息的接收、消息持久化、提供消息、过滤消息、高可用。
那生产者、消息服务器和消费者如何找到对方?
消息服务器注册到命名服务器上,这样命名服务器就有了所有的消息服务器的Broker IP。
消息的生产者在发消息时,去连接命名服务器,同时获取所有的Broker信息,消息的消费者同理。
命名服务器怎么才能知道这些服务器存在呢?万一有的机器挂了呢?通过心跳机制来维系,每30秒发送一个心跳包,来保障现在拿到的这个服务器就有效的。
消息包括:消息Message、主题Topic(必备。是对消息进行分类的,这是订单类的,这是会员类的)、标题Tag。一个消息可以分成好多类,提取方式不同,处理的方式不同。服务器中会根据不同的Topic创建队列。对应的Topic主题的消息会放到对应的队列中
环境搭建
命名服务器如果不启动,就没法连,所以命名服务器是要安装的第一个东西。
命名服务器安装好后,Broker就可以注册到命名服务器上了,所以第二步是安装Broker。
接下来就可以发、收消息了。可以先发消息然后再收消息,也可以先收着然后再发消息。
MQ的服务器在启动时会报错,因为配置有问题,需要修改runbroker.sh文件中的配置,调整到与当前机器内存匹配即可,推荐256-128m。
安装RocketMQ
上传压缩包小工具,先安装
yum -y install lrzsz
rz
此时要选择上传的zip(Apache官网自行下载)
解压zip包
unzip rocketmq-all-4.5.2-bin-release.zip
修改名称方便查看
mv rocketmq-all-4.5.2-bin-release rocketmq
进入bin目录中:
启动命名服务器
sh mqnamesrv
启动成功
启动消息服务器时,要指定要注册到哪个命名服务器上
sh mqbroker -n localhost:9876
启动消息服务器时会报错,报错原因是runbroker.sh配置有问题,需要进行修改,修改成符合你当前机器的一个合适大小,当前使用的是虚拟机,修改为256m和128m
修改前:
修改后:
接下来启动服务,启动命名服务器和消息服务器,启动时不会报错,但是运行时会报错,先行说明,如果是超时异常,无法连接,查看:
解决方案
消息发送(重点)
消息的发送主要包括 :
单生产者单消费者&同步消息
消息的发送与接收
1.消息谁来发?
2.消息发给谁?
3.消息怎么发?
4.发什么?
5.发的结果是什么?
6.收尾工作?
单生产者单消费者&同步消息
引入依赖坐标
<dependencies>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.5.2</version>
</dependency>
</dependencies>
消息生产者:
package com.base;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
public class Producer {
public static void main(String[] args) throws Exception {
//创建发送消息对象
DefaultMQProducer producer = new DefaultMQProducer("group1");
//设定命名服务器地址---获取到消息服务器ip
producer.setNamesrvAddr("192.168.200.130:9876");
//启动发送服务
producer.start();
//构建消息,指定topic和body
Message msg = new Message("topic1", "hello base".getBytes());
//发送消息
SendResult sendResult = producer.send(msg, 10000);
System.out.println("sendResult = " + sendResult);
//关闭连接
producer.shutdown();
}
}
打印结果
sendResult = SendResult [sendStatus=SEND_OK, msgId=C0A8006F0FA018B4AAC27A51177E0000, offsetMsgId=C0A8C88200002A9F000000000001343E, messageQueue=MessageQueue [topic=tpoic1, brokerName=broker-a, queueId=3], queueOffset=0]
消息消费者:
package com.base;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
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 Consumer {
public static void main(String[] args) throws Exception {
//创建一个消息接收对象consumer
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
//设定接收消息的命名服务器地址---获取到消息服务器ip
consumer.setNamesrvAddr("192.168.200.130:9876");
//设置接收消息对应的topic,对应的sub标签为任意*,之前producer没有指定tag。如果producer发送的消息指定了tag,那么也必须指定相应的tag
consumer.subscribe("topic1", "*");
//开启监听,用于接收消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
//遍历接收到的消息
for (MessageExt msg : list) {
System.out.println("msg = " + msg);
System.out.println("消息为:" + new String(msg.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//启动消息接收服务
consumer.start();
}
}
msg = MessageExt [queueId=3, storeSize=157, queueOffset=2, sysFlag=0, bornTimestamp=1627132561754, bornHost=/192.168.200.1:10109, storeTimestamp=1627132561761, storeHost=/192.168.200.130:10911, msgId=C0A8C88200002A9F000000000001393A, commitLogOffset=80186, bodyCRC=806332349, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{
topic='topic1', flag=0, properties={
MIN_OFFSET=0, MAX_OFFSET=3, CONSUME_START_TIME=1627132561761, UNIQ_KEY=C0A8006FF2B418B4AAC27B0281590000, WAIT=true}, body=[104, 101, 108, 108, 111, 32, 109, 113], transactionId='null'}]
消息为:hello mq
单生产者多消费者
消息生产者与之前一致,举例改为发送10次消息
package com.one2many;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
public class Producer {
public static void main(String[] args) throws Exception {
//创建发送消息对象
DefaultMQProducer producer = new DefaultMQProducer("group1");
//设定命名服务器地址---获取到消息服务器ip
producer.setNamesrvAddr("192.168.200.130:9876");
//启动发送服务
producer.start();
for (int i = 1; i <= 10; i++) {
//构建消息,指定topic和body
Message msg = new Message("topic1", ("hello base"+i).getBytes());
//发送消息
SendResult sendResult = producer.send(msg, 10000);
System.out.println("sendResult = " + sendResult);
}
//关闭连接
producer.shutdown();
}
}
消息消费者:与 之前一致。
package com.one2many;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
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 org.apache.rocketmq.common.protocol.heartbeat.MessageModel;
import java.util.List;
public class Consumer {
public static void main(String[] args) throws Exception {
//创建一个消息接收对象consumer
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
//设定接收消息的命名服务器地址---获取到消息服务器ip
consumer.setNamesrvAddr("192.168.200.130:9876");
//设置接收消息对应的topic,对应的sub标签为任意*,之前producer没有指定tag。如果producer发送的消息指定了tag,那么也必须指定相应的tag
consumer.subscribe("topic1", "*");
//设置消费者的消费模式:也是默认的模式负载均衡
//consumer.setMessageModel(MessageModel.CLUSTERING);
//设置消费者的消费模式为广播模式:所有客户端接收的消息都是一样的
consumer.setMessageModel(MessageModel.BROADCASTING);
//开启监听,用于接收消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
public ConsumeConcurrentlyStatus <