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 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();
}
}
经过多次发送多次接收发现:消息被规律性接收,是因为RocketMQ有默认的负载均衡策略。
RocketMQ的策略有两种,其中一种是负载均衡(平均分配)。另外一种是广播模式(即:每个消费者都接收全部消息)。
//设置消费者的消费模式:也是默认的模式负载均衡
//consumer.setMessageModel(MessageModel.CLUSTERING);
//设置消费者的消费模式为广播模式:所有客户端接收的消息都是一样的
consumer.setMessageModel(MessageModel.BROADCASTING);
广播模式:所有消费者接收到的消息都是一致的
多生产者多消费者
生产者与消费者代码与上面一致。
多生产者产生的消息可以被同一个消费者消费也可以被多个消费者消费。
消息类型
同步消息
即时性较强,重要的消息,且必须有回执的消息,例如短信,通知(转账成功)
异步消息
即时性较弱,但需要有回执的消息,例如订单中的某些信息
比如生成订单后,支付成功后返回支付成功的消息就可以,你支付完告诉我一声就行,并不需要立刻支付。
单向消息
不需要有回执的消息,例如日志类消息
消息的消费者代码同上,消息生产者代码:
package com.messageType;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import java.util.concurrent.TimeUnit;
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"+i).getBytes());
//发送消息
SendResult sendResult = producer.send(msg);
System.out.println("sendResult = " + sendResult);
}*/
//异步消息
/* for (int i = 1; i <= 10; i++) {
//构建消息,指定topic和body
Message msg = new Message("topic1", ("异步消息:hello" + i).getBytes());
producer.send(msg, new SendCallback() {
//消息发送成功
public void onSuccess(SendResult sendResult) {
System.out.println("sendResult = " + sendResult);
}
//消息发送失败
public void onException(Throwable t) {
System.out.println("t = " + t);
}
});
}*/
//单向消息
//构建消息,指定topic和body
Message msg = new Message("topic1", ("单向消息:hello").getBytes());
producer.sendOneway(msg);
//程序休眠10秒钟,确保异步消息返回后能够输出
TimeUnit.SECONDS.sleep(10);
//关闭连接
producer.shutdown();
}
}
特殊消息发送
延时消息(和消息自身有关的特性)
消息生产者代码:
package com.delayMessage;
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", ("延时消息:" + i).getBytes());
//设置延迟消息等级
msg.setDelayTimeLevel(3);
//发送消息
SendResult sendResult = producer.send(msg);
System.out.println("sendResult = " + sendResult);
}
//关闭连接
producer.shutdown();
}
}
关键代码
//构建消息,指定topic和body
Message msg = new Message("topic1", ("延时消息:" + i).getBytes());
//设置延迟消息等级
msg.setDelayTimeLevel(3);
//发送消息
SendResult sendResult = producer.send(msg);
System.out.println("sendResult = " + sendResult);
- 目前支持的消息时间
-
- 秒级:1,5,10,30
- 分级:1~10,20,30
- 时级:1,2
- 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2
所以上面的level水平对应就是30s。
批量消息
生产者代码:消费者代码与之前一致
package com.mul;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import java.util.ArrayList;
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();
ArrayList<Message> messageList = new ArrayList<Message>();
//构建消息,指定topic和body
Message msg1 = new Message("topic1", ("批量消息:" + 1).getBytes());
Message msg2 = new Message("topic1", ("批量消息:" + 2).getBytes());
Message msg3 = new Message("topic1", ("批量消息:" + 3).getBytes());
Message msg4 = new Message("topic1", ("批量消息:" + 4).getBytes());
messageList.add(msg1);
messageList.add(msg2);
messageList.add(msg3);
messageList.add(msg4);
//发送消息
SendResult sendResult = producer.send(messageList);
System.out.println("sendResult = " + sendResult);
//关闭连接
producer.shutdown();
}
}
关键代码
ArrayList<Message> messageList = new ArrayList<Message>();
//构建消息,指定topic和body
Message msg1 = new Message("topic1", ("批量消息:" + 1).getBytes());
Message msg2 = new Message("topic1", ("批量消息:" + 2).getBytes());
Message msg3 = new Message("topic1", ("批量消息:" + 3).getBytes());
Message msg4 = new Message("topic1", ("批量消息:" + 4).getBytes());
messageList.add(msg1);
messageList.add(msg2);
messageList.add(msg3);
messageList.add(msg4);
//发送消息
SendResult sendResult = producer.send(messageList);
- 消息内容总长度不超过4M
- 消息内容总长度包含如下:
-
- topic(字符串字节数)
- body (字节数组长度)
- 消息追加的属性(key与value对应字符串字节数)
- 日志(固定20字节)
消息内容总长度超过4M就用不了了。
消息过滤
分类过滤
消息上面有主题Topic还有标题Tag。大的可以通过Topic过滤,小的可以从Tag过滤。
消息生产者代码
package com.filterByTag;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import java.util.ArrayList;
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、tag
Message msg = new Message("topic1", "tag1", "消息过滤tag1消息".getBytes());
Message msg2 = new Message("topic1", "tag2", "消息过滤tag2消息".getBytes());
//发送消息
SendResult sendResult = producer.send(msg);
SendResult sendResult2 = producer.send(msg2);
System.out.println("sendResult = " + sendResult);
//关闭连接
producer.shutdown();
}
}
关键代码
//构建消息,指定topic和body、tag
Message msg = new Message("topic1", "tag1", "消息过滤tag1消息".getBytes());
Message msg2 = new Message("topic1", "tag2", "消息过滤tag2消息".getBytes());
消息消费者
package com.filterByTag;
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", "tag1||tag2");
//设置消费者的消费模式:也是默认的模式负载均衡
//consumer.setMessageModel(MessageModel.CLUSTERING);
//设置消费者的消费模式为广播模式:所有客户端接收的消息都是一样的
consumer.setMessageModel(MessageModel.BROADCASTING);
//开启监听,用于接收消息
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();
}
}
关键代码
//设置接收消息对应的topic,对应的sub标签为任意*,之前producer没有指定tag。如果producer发送的消息指 定了tag,那么也必须指定相应的tag
consumer.subscribe("topic1", "tag1||tag2");
//consumer.subscribe("topic1", "*"); //*表示任意tag
在接收消息的时候,除了指定topic。还可以指定tag,*表示任意tag。
语法过滤(属性过滤/语法过滤/SQL过滤)
消息生产者代码
package com.filterBySql;
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", "tag1", "消息过滤tag1消息".getBytes());
//为消息添加属性
msg.putUserProperty("vip","1");
msg.putUserProperty("age","20");
//发送消息
SendResult sendResult = producer.send(msg);
System.out.println("sendResult = " + sendResult);
//关闭连接
producer.shutdown();
}
}
关键代码
//为消息添加属性
msg.putUserProperty("vip","1");
msg.putUserProperty("age","20");
消息消费者
package com.filterBySql;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.MessageSelector;
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", "*");
//使用消息选择器来过滤对应的属性,语法格式为类SQL语法
//consumer.subscribe("topic1", MessageSelector.bySql("vip=1"));
consumer.subscribe("topic1", MessageSelector.bySql("age>18"));
//开启监听,用于接收消息
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();
}
}
关键代码
//使用消息选择器来过滤对应的属性,语法格式为类SQL语法
//consumer.subscribe("topic1", MessageSelector.bySql("vip=1"));
consumer.subscribe("topic1", MessageSelector.bySql("age>18"));
注意:SQL过滤需要依赖服务器功能支持,在broker配置文件中添加对应的功能项,并开启对应功能。
当启动服务时会报错
报错信息如下
Exception in thread "main" org.apache.rocketmq.client.exception.MQClientException: CODE: 1 DESC: The broker does not support consumer to filter message by SQL92
For more information, please visit the url, http://rocketmq.apache.org/docs/faq/
截图如下:
这是因为默认的broker并没有开启对SQL语法的支持,需要修改配置:
打开broker服务器下的broker.conf文件
cd rocketmq/bin
ll
vim broker.conf
当前配置项
需要添加配置项
enablePropertyFilter=true
修改后的broker.conf配置文件
然后重新启动broker
sh mqbroker -n localhost:9876 -c ../conf/broker.conf
再次发送消息启动程序,就不会报错。
解决方案
特殊现象
错乱的消息顺序
某些情况下业务是有一定的顺序性的,没有订单就先生成子单,很容易出问题。如果消息是单个的还好,如果是具有一定业务顺序的,就一定会有问题。
取消息的时候,比如1是A线程、2是B线程、3是C线程,线程之间是靠抢夺CPU资源的,无法保证一定是先1再2后3的顺序。
解决方案就是消息可以选择进入哪一个队列。
消息生产者代码
package com.order;
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", ("顺序消息"+i).getBytes());
//发送消息
SendResult sendResult = producer.send(msg, 10000);
System.out.println("sendResult = " + sendResult);
}
//关闭连接
producer.shutdown();
}
}
消息消费者代码
package com.order;
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.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();
}
}
消息的发送:是放入4个消息队列中的。消息队列是在topic中的。
消息的接收顺序
消息的接收顺序与消息的发送的顺序并不一致。
解决方案就是在消息的生产者在发送消息时,同一组消息指定发送到一个消息队列。
消息生产者:
package com.order;
import com.order.domain.Order;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.MessageQueueSelector;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageQueue;
import java.util.ArrayList;
import java.util.List;
/**
* 测试消息的顺序性
*/
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();
ArrayList<Order> orderList = new ArrayList<Order>();
Order order1 = new Order();
order1.setId("a");
order1.setMsg("主订单-1");
orderList.add(order1);
Order order2 = new Order();
order2.setId("a");
order2.setMsg("子订单-2");
orderList.add(order2);
Order order3 = new Order();
order3.setId("a");
order3.setMsg("支付订单-3");
orderList.add(order3);
Order order4 = new Order();
order4.setId("a");
order4.setMsg("推送消息-4");
orderList.add(order4);
Order order5 = new Order();
order5.setId("b");
order5.setMsg("主订单-1");
orderList.add(order5);
Order order6 = new Order();
order6.setId("b");
order6.setMsg("子订单-2");
orderList.add(order6);
Order order7 = new Order();
order7.setId("c");
order7.setMsg("主订单-1");
orderList.add(order7);
Order order8 = new Order();
order8.setId("c");
order8.setMsg("子订单-2");
orderList.add(order8);
Order order9 = new Order();
order9.setId("c");
order9.setMsg("支付订单-3");
orderList.add(order9);
//设置消息进入到指定的消息队列中
for (final Order order : orderList) {
//构建消息
Message msg = new Message("orderTopic", order.toString().getBytes());
//发送时要指定对应的消息队列选择器
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
//根据发送的消息的不同,选择不同的消息队列
//根据id来选择一个消息队列的对象,并返回->id得到int值,作为索引
int index = order.getId().hashCode() % list.size();
//返回一个消息队列MessageQueue
return list.get(index);
}
}, null);
System.out.println("sendResult = " + sendResult);
}
//关闭连接
producer.shutdown();
}
}
关键代码
//设置消息进入到指定的消息队列中
for (final Order order : orderList) {
//构建消息
Message msg = new Message("orderTopic", order.toString().getBytes());
//发送时要指定对应的消息队列选择器
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
//根据发送的消息的不同,选择不同的消息队列
//根据id来选择一个消息队列的对象,并返回->id得到int值,作为索引
int index = order.getId().hashCode() % list.size();
//返回一个消息队列MessageQueue
return list.get(index);
}
}, null);
System.out.println("sendResult = " + sendResult);
}
发送消息,发现此时同一组消息发送到了指定的消息队列中:
这个时候消息确实是指定了要发送的消息队列。
但是消息消费者取到的消息却依然没有按照预想的顺序取出来。
见图:原因是是由多个线程去执行的。好比说,消息确实是按照多顺序放的,但是取的时候却是多个线程去取出来的,然后每个线程各自执行各自的,自然没有顺序而言。
package com.order.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@AllArgsConstructor
@NoArgsConstructor
@Data
@ToString
public class Order {
private String id;
private String msg;
}
package com.order;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.*;
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("orderTopic", "*");
//开启监听,用于接收消息
/*consumer.registerMessageListener(new MessageListenerConcurrently() {
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
//遍历接收到的消息
for (MessageExt msg : list) {
// System.out.println("msg = " + msg);
System.out.println(Thread.currentThread().getName() + "消息为:" + new String(msg.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});*/
//使用单线程的模式从消息队列中取数据,一个线程绑定一个消息队列
consumer.registerMessageListener(new MessageListenerOrderly() {
//使用ConsumeListenerOrderly后,对消息队列的处理由一个消息队列多个线程服务,转换为一个消息队列一个线程服务
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> list, ConsumeOrderlyContext consumeOrderlyContext) {
//遍历接收到的消息
for (MessageExt msg : list) {
System.out.println(Thread.currentThread().getName() + "消息为:" + new String(msg.getBody()));
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
//启动消息接收服务
consumer.start();
}
}
关键代码
//使用单线程的模式从消息队列中取数据,一个线程绑定一个消息队列
consumer.registerMessageListener(new MessageListenerOrderly() {
//使用ConsumeListenerOrderly后,对消息队列的处理由一个消息队列多个线程服务,转换为一个消息队列一个线程服务
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> list, ConsumeOrderlyContext consumeOrderlyContext) {
//遍历接收到的消息
for (MessageExt msg : list) {
System.out.println(Thread.currentThread().getName() + "消息为:" + new String(msg.getBody()));
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
此时的消息消费顺序:
这个时候,每个线程多运行几次,可以保证同一个业务的消息可以顺着全部取出来,不会被别人插队。目前是单线程去绑定每一个MQ队列,消息处理的速度将会成为整个消息处理的瓶颈。
事务消息
正常事务过程
1-2-3-4是正常事务过程。
1、消息发送到消息服务器;
2、消息服务器返回确认状态;
3、然后生产者去确认本地事务状态;
4、提交或者回滚事务,提交事务就是将消息放入消息队列中,回滚就是将消息对列中的消息删除掉。
事务补偿过程
5、6、7是事务补偿过程
5、当消息服务器没收到确认消息时,主动向生产者做结果确认;
6、生产者再去确认本地事务状态;
7、然后根据事务状态进行提交或回滚。
事务消息状态
- **提交状态:**允许进入队列,此消息与非事务消息无区别
- **回滚状态:**不允许进入队列,此消息等同于未发送过
- **中间状态:**完成了half消息的发送,未对MQ进行二次状态确认
- 注意:事务消息仅与生产者有关,与消费者无关
消息的消费者只是负责消费消息,不管这些。
消息生产者
package com.transtion;
import org.apache.rocketmq.client.producer.*;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;
public class Producer {
/**
* 事务补偿过程
*
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
//事务消息使用的生产者是TransactionMQProducer
TransactionMQProducer producer = new TransactionMQProducer("group1");
//设定命名服务器地址---获取到消息服务器ip
producer.setNamesrvAddr("192.168.200.130:9876");
//启动发送服务
producer.start();
//添加本地事务对应的监听
producer.setTransactionListener(new TransactionListener() {
long start;
//正常事务过程
public LocalTransactionState executeLocalTransaction(Message message, Object o) {
start = System.currentTimeMillis();
//提交事务
return LocalTransactionState.UNKNOW;
}
//事务补偿过程,如果事务补偿过程状态返回仍然是不断进行事务补偿,测试结论每60秒发起一次
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
long time = (System.currentTimeMillis() - start) / 1000;
System.out.println("time = " + time);
System.out.println("事务补偿过程执行了");
return LocalTransactionState.COMMIT_MESSAGE;//提交
}
});
//构建消息,指定topic和body
Message msg = new Message("topic10", "hello rocketmq".getBytes());
//发送事务消息
TransactionSendResult sendResult = producer.sendMessageInTransaction(msg, null);
System.out.println("sendResult = " + sendResult);
//关闭连接
// producer.shutdown();
}
/**
* 正常事务提交事务,无事务补偿过程
*
* @param args
* @throws Exception
*/
public static void main1(String[] args) throws Exception {
//事务消息使用的生产者是TransactionMQProducer
TransactionMQProducer producer = new TransactionMQProducer("group1");
//设定命名服务器地址---获取到消息服务器ip
producer.setNamesrvAddr("192.168.200.130:9876");
//启动发送服务
producer.start();
//添加本地事务对应的监听
producer.setTransactionListener(new TransactionListener() {
//正常事务过程
public LocalTransactionState executeLocalTransaction(Message message, Object o) {
//提交事务
return LocalTransactionState.COMMIT_MESSAGE;
}
//事务补偿过程
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
return null;
}
});
//构建消息,指定topic和body
Message msg = new Message("topic10", "hello rocketmq".getBytes());
//发送事务消息
TransactionSendResult sendResult = producer.sendMessageInTransaction(msg, null);
System.out.println("sendResult = " + sendResult);
//关闭连接
producer.shutdown();
}
/**
* 正常事务事务回滚过程
*
* @param args
* @throws Exception
*/
public static void main2(String[] args) throws Exception {
//事务消息使用的生产者是TransactionMQProducer
TransactionMQProducer producer = new TransactionMQProducer("group1");
//设定命名服务器地址---获取到消息服务器ip
producer.setNamesrvAddr("192.168.200.130:9876");
//启动发送服务
producer.start();
//添加本地事务对应的监听
producer.setTransactionListener(new TransactionListener() {
//正常事务过程
public LocalTransactionState executeLocalTransaction(Message message, Object o) {
//回滚事务
return LocalTransactionState.ROLLBACK_MESSAGE;
}
//事务补偿过程
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
return null;
}
});
//构建消息,指定topic和body
Message msg = new Message("topic10", "hello rocketmq".getBytes());
//发送事务消息
TransactionSendResult sendResult = producer.sendMessageInTransaction(msg, null);
System.out.println("sendResult = " + sendResult);
//关闭连接
producer.shutdown();
}
}
关键代码
//事务消息使用的生产者是TransactionMQProducer
TransactionMQProducer producer = new TransactionMQProducer("group1");
//添加本地事务对应的监听
producer.setTransactionListener(new TransactionListener() {
long start;
//正常事务过程
public LocalTransactionState executeLocalTransaction(Message message, Object o) {
start = System.currentTimeMillis();
//提交事务
return LocalTransactionState.UNKNOW;
}
//事务补偿过程,如果事务补偿过程状态返回仍然是不断进行事务补偿,测试结论每60秒发起一次
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
long time = (System.currentTimeMillis() - start) / 1000;
System.out.println("time = " + time);
System.out.println("事务补偿过程执行了");
return LocalTransactionState.COMMIT_MESSAGE;//提交
}
});
//发送事务消息
TransactionSendResult sendResult = producer.sendMessageInTransaction(msg, null);
测试:如果事务补偿过程返回的状态仍然是 return LocalTransactionState.UNKNOW;那么还会一直进行事务补偿,测试结论为每60秒发起一次事务补偿。
集群搭建
RocketMQ集群分类
-
单机
-
-
一个broker提供服务(宕机后服务瘫痪)
——要是唯一的一台机器宕机来了,就会
-
-
集群
-
- 多个broker提供服务(单机宕机后消息无法及时被消费)
- 多个master多个slave
-
-
-
master到slave消息同步方式为同步(较异步方式性能略低,消息无延迟)
——同步,收到 就要发,这个时候没有得到数据同步的结果之前,不能继续往下走。
-
master到slave消息同步方式为异步(较同步方式性能略高,数据略有延迟)
——异步,不会那么卡
NameServ之间是不进行通讯的,无状态的。
-
-
工作流程:
-
步骤1:NameServer启动,开启监听,等待broker、producer与consumer连接
-
步骤2:broker启动,根据配置信息,连接所有的NameServer,并保持长连接
-
步骤2补充:如果broker中有现存数据, NameServer将保存topic与broker关系
-
步骤3:producer发信息,连接某个NameServer,并建立长连接
-
步骤4:producer发消息
-
- 步骤4.1若果topic存在,由NameServer直接分配
- 步骤4.2如果topic不存在,由NameServer创建topic与broker关系,并分配
-
步骤5:producer在broker的topic选择一个消息队列(从列表中选择)
-
步骤6:producer与broker建立长连接,用于发送消息
-
步骤7:producer发送消息
-
comsumer工作流程同producer
搭建过程(双主双从)
1) 配置主机名称(未来就可以根据主机名找到对应的服务器了)
vim /etc/hosts
# nameserver
192.168.184.128 rocketmq-nameserver1
192.168.184.129 rocketmq-nameserver2
# broker
192.168.184.128 rocketmq-master1
192.168.184.129 rocketmq-slave2
192.168.184.129 rocketmq-master2
192.168.184.128 rocketmq-slave1
配置完毕后重启网卡,应用配置
systemctl restart network
2) 关闭防火墙
# 关闭防火墙
systemctl stop firewalld.service
# 查看防火墙的状态
firewall-cmd --state
# 禁止firewall开机启动
systemctl disable firewalld.service
3) 配置jdk
详见 2.1.1) 步骤1
4) 配置服务器环境
将rocketmq 解压至跟目录 /
# 解压
unzip rocketmq-all-4.5.2-bin-release.zip
# 修改目录名称
mv rocketmq-all-4.5.2-bin-release rocketmq
vim /etc/profile
#set rocketmq
ROCKETMQ_HOME=/rocketmq
PATH=$PATH:$ROCKETMQ_HOME/bin
export ROCKETMQ_HOME PATH
配置完毕后重启网卡,应用配置
source /etc/profile
5) 创建集群服务器的数据存储目录
主节点创建四个目录/ 从节点四个目录
mkdir /rocketmq/store
mkdir /rocketmq/store/commitlog
mkdir /rocketmq/store/consumequeue
mkdir /rocketmq/store/index
mkdir /rocketmq-slave/store
mkdir /rocketmq-slave/store/commitlog
mkdir /rocketmq-slave/store/consumequeue
mkdir /rocketmq-slave/store/index
注意master与slave如果在同一个虚拟机中部署,需要将存储目录区分开
6) 修改配置
不同的节点,应该修改不同的配置,文件夹也应该不一样
cd r/ocketmq/conf/2m-2s-sync
vim broker-a.proerties
#所属集群名字
brokerClusterName=rocketmq-cluster
#broker名字,注意此处不同的配置文件填写的不一样
brokerName=broker-a
#0 表示 Master,>0 表示 Slave
brokerId=1
#nameServer地址,分号分割
namesrvAddr=rocketmq-nameserver1:9876;rocketmq-nameserver2:9876
#在发送消息时,自动创建服务器不存在的topic,默认创建的队列数
defaultTopicQueueNums=4
#是否允许 Broker 自动创建Topic,建议线下开启,线上关闭
autoCreateTopicEnable=true
#是否允许 Broker 自动创建订阅组,建议线下开启,线上关闭
autoCreateSubscriptionGroup=true
#Broker 对外服务的监听端口
listenPort=11011
#删除文件时间点,默认凌晨 4点
deleteWhen=04
#文件保留时间,默认 48 小时
fileReservedTime=48
#commitLog每个文件的大小默认1G
mapedFileSizeCommitLog=1073741824
#ConsumeQueue每个文件默认存30W条,根据业务情况调整
mapedFileSizeConsumeQueue=300000
#destroyMapedFileIntervalForcibly=120000
#redeleteHangedFileInterval=120000
#检测物理文件磁盘空间
diskMaxUsedSpaceRatio=88
#存储路径
storePathRootDir=/rocketmq/store-slave
#commitLog 存储路径
storePathCommitLog=/rocketmq/store-slave/commitlog
#消费队列存储路径存储路径
storePathConsumeQueue=/rocketmq/store-slave/consumequeue
#消息索引存储路径
storePathIndex=/rocketmq/store-slave/index
#checkpoint 文件存储路径
storeCheckpoint=/rocketmq/store-slave/checkpoint
#abort 文件存储路径
abortFile=/rocketmq
#限制的消息大小
maxMessageSize=65536
#flushCommitLogLeastPages=4
#flushConsumeQueueLeastPages=2
#flushCommitLogThoroughInterval=10000
#flushConsumeQueueThoroughInterval=60000
#Broker 的角色
#- ASYNC_MASTER 异步复制Master
#- SYNC_MASTER 同步双写Master
#- SLAVE
brokerRole=SLAVE
#刷盘方式
#- ASYNC_FLUSH 异步刷盘
#- SYNC_FLUSH 同步刷盘
flushDiskType=SYNC_FLUSH
#checkTransactionMessageEnable=false
#发消息线程池数量
#sendMessageThreadPoolNums=128
#拉消息线程池数量
#pullMessageThreadPoolNums=128
检查启动内存 (nameserver 和broker 均需要修改)
vim /rocketmq/bin/runbroker.sh
vim /rocketmq/bin/runserver.sh
# 开发环境配置 JVM Configuration
JAVA_OPT="${JAVA_OPT} -server -Xms256m -Xmx256m -Xmn128m"
启动(bin 目录)
nohup sh mqnamesrv &
nohup sh mqbroker -c ../conf/2m-2s-syncbroker-a.properties &
nohup sh mqbroker -c ../conf/2m-2s-sync/broker-b-s.properties &
rocketmq-console
rocketmq-console是一款基于java环境开发的(springboot)的管理控制台工具:获取地址
高级特性(重点)
持久化与持久化介质
数据库持久化
ActiveMQ 使用了数据库的消息存储
缺点:数据库瓶颈将成为MQ瓶颈
消息保存到数据库中,数据库会通过自己的内部逻辑去保存到文件系统中,数据库将会成为整个MQ过程中的瓶颈。早期的ActiveMQ使用的就是这种方式。
文件系统持久化
不用数据库,直接用文件存储
(RocketMQ/Kafka/RabbitMQ)
硬盘的存储方式:
复制完再删除,就会留下很多的磁盘碎片,这个时候,你去存储数据的时候,就会被打的很散,当数据存储的越满,磁盘碎片越多,存储数据的效率就会越低。
顺序写与零拷贝
MQ 高效的消息存储与读写方式
- SSD(Solid State Disk)
-
- 随机写(100KB/s)
- 顺序写(600MB/s)1秒1部电影
利用了顺序写来提高读写的性能。
零拷贝技术
1) 通过启动时初始化话文件大小来保证 占用固定的磁盘空间,保证磁盘读写速度
2) 零拷贝”技术
数据传输由传统的4次复制简化成3次复制(如下图),减少1次复制过程
Java语言中使用MappedByteBuffer类实现了该技术
要求:预留存储空间,用于保存数据(1G存储空间起步)
不管是什么样的数据,都是MQ内部的数据,跳过了用户态,linux零拷贝技术,四次拷贝简化为三次拷贝,减少了一次复制过程。
消息存储结构
消息数据存储区域
topic
queueId
message
消费逻辑队列
minOffset
maxOffset
consumerOffset
索引
key索引
创建时间索引
……
刷盘机制
持久化到硬盘的过程称为刷盘。
同步刷盘
1)生产者发送消息到MQ,MQ接到消息数据
2)MQ挂起生产者发送消息的线程
3)MQ将消息数据写入内存
4)内存数据写入硬盘
5)磁盘存储后返回SUCCESS
6)MQ恢复挂起的生产者线程
7)发送ACK到生产者
异步刷盘
1)生产者发送消息到MQ,MQ接到消息数据
2)MQ将消息数据写入内存
3)发送ACK到生产者
–等消息量多了–
4)内存数据写入硬盘
同步刷盘/ 异步刷盘 优缺点对比
同步刷盘:安全性高,效率低,速度慢(适用于对数据安全要求较高的业务)
异步刷盘:安全性低,效率高,速度快(适用于对数据处理速度要求较高的业务)
配置方式
#刷盘方式
#- ASYNC_FLUSH 异步刷盘
#- SYNC_FLUSH 同步刷盘
flushDiskType=SYNC_FLUSH
高可用与主从复制方案
高可用方案
nameserver
nameserver ,通过无状态+全服务器注册 来保证即使一个宕机了也能提供所有的服务
消息服务器
主从架构(2M-2S) ,即使又一台服务器宕机, 服务依旧可以正常提供
注意: master 一旦宕机,slave 只提供消费服务,不能写入新的消息(slave 不会升级为master)
消息生产(开发人员写代码时保障)
生产者将相同的topic绑定到多个group组,保障master挂掉后,其他master仍可正常进行消息接收
消息消费
RocketMQ自身会根据master的压力确认是否由master承担消息读取的功能,当master繁忙时候,自动切换由slave承担数据读取的工作
主从复制方案
同步复制
master接到消息后,先复制到slave,然后反馈给生产者写操作成功
优点:数据安全,不丢数据,出现故障容易恢复
缺点:影响数据吞吐量,整体性能低
异步复制
master接到消息后,立即返回给生产者写操作成功,当消息达到一定量后再异步复制到slave
优点:数据吞吐量大,操作延迟低,性能高
缺点:数据不安全,会出现数据丢失的现象,一旦master出现故障,从上次数据同步到故障时间的数据将丢失
配置(配置在启动时 -c 指定的配置文件中 broker.conf)
#Broker 的角色
#- ASYNC_MASTER 异步复制Master
#- SYNC_MASTER 同步双写Master
#- SLAVE
brokerRole=SYNC_MASTER
负载均衡
RocketMQ在进行工作的时候是自动进行负载均衡的。
-
Producer负载均衡
内部实现了不同broker集群中对同一topic对应消息队列的负载均衡 -
Consumer负载均衡
平均分配
循环平均分配平均分配:
如果哦broker-A宕机了,那么蓝色消费者就没有工作了,橙色有一个,绿色正常。
循环平均分配:
广播模式不参与负载均衡。
消息重试
当消息消费后未正常返回消费成功的信息将启动消息重试机制
顺序消息重试
当消费者消费消息失败后,RocketMQ会自动进行消息重试(每次间隔时间为 1 秒)
注意:应用会出现消息消费被阻塞的情况,因此,要对顺序消息的消费情况进行监控,避免阻塞现象的发生
无序消息重试
无序消息包括普通消息、定时消息、延时消息、事务消息
无序消息重试仅适用于负载均衡(集群)模型下的消息消费,不适用于广播模式下的消息消费
为保障无序消息的消费,MQ设定了合理的消息重试间隔时长
无序消息未被消费,到达了重试总次数以后,这种消息会被重新标记叫做死信。
死信队列
概念
当消息消费重试到达了指定次数(默认16次)后,MQ将无法被正常消费的消息称为死信消息(Dead-Letter Message)
死信消息不会被直接抛弃,而是保存到了一个全新的队列中,该队列称为死信队列(Dead-Letter Queue)
死信队列特征
- 归属某一个组(Gourp Id),而不归属Topic,也不归属消费者
- 一个死信队列中可以包含同一个组下的多个Topic中的死信消息
- 死信队列不会进行默认初始化,当第一个死信出现后,此队列首次初始化
死信队列中消息特征
- 不会被再次重复消费
- 死信队列中的消息有效期为3天,达到时限后将被清除
死信处理
在监控平台中,通过查找死信,获取死信的messageId,然后通过id对死信进行精准消费
重复消费与消息幂等
重复消费原因
1 生产者发送了重复的消息
网络闪断
生产者宕机
2 消息服务器投递了重复的消息
网络闪断
3 动态的负载均衡过程
网络闪断/抖动
broker重启
订阅方应用重启(消费者)
客户端扩容
客户端缩容
消息幂等
对同一条消息,无论消费多少次,结果保持一致,称为消息幂等性
解决方案
- 使用业务id作为消息的key
- 在消费消息时,客户端对key做判定,未使用过放行,使用过抛弃
注意:messageId由RocketMQ产生,messageId并不具有唯一性,不能作用幂等判定条件