一、概述
MQ ( Message Queue ):消息队列,是一种用来保存消息数据的队列
MQ的作用:应用解耦、快速应用变更维护、流量削峰(削峰填谷)
MQ的缺点:消息顺序性、消息丢失、消息一致性、消息重复使用(都有解决方案)
二、RocketMQ 框架流程
生产者(Producer Cluster)发送消息,消息包括Message、Topic(主分类)、Tag(小分类)。该消息到达消息服务器(Broker Cluster),消息服务器根据不同的topic创建队列,相应topic的消息会存放到对应的队列中。由监听器监督Broker中是否有消息,有消息的话Broker直接向消费者(Consumer Cluster)推送消息(还可向Broker发送请求,获取消息)。
Broker除了接收消息、发送消息以外,还支持消息的持久化,即存储消息。过滤消息,实现高可用,一台服务器挂了,还可以继续工作(集群的方式)。
命名服务器(NameServer Cluster)(集群)统筹生产者、消费者以及消息服务器,建立相应的连接。将消息服务器注册到命名服务器,命名服务器得到所有的Broker IPs,生产者在发送消息时连接命名服务器,同时获取所有的Broker信息(在没有Broker信息时不可工作)。消费者在接收消息时,同样同时拉取Broker消息。由此,三者相互之间建立连接。
由心跳机制保障命名服务器实时感知生产者、消息服务器、消费者的存在,每一个服务器每30s向命名服务器发送请求,由此命名服务器感知到他们的存在。只要有一段时间没有收到心跳连接数据,就视相应的服务器宕机,视作下线。通过心跳机制来保障服务器状态的有效性。如果因为网路的原因而被视作宕机,当重新发送心跳连接数据后,再次上线。到此命名服务器就能够识别生产者、消费者以及消息服务器了。
三、消息发送与消息接收的API使用
1. 消息发送与接收
生产者:Producer.java
//生产者,产生消息
public class Producer {
public static void main(String[] args) throws Exception {
//1.创建一个发送消息的对象Producer
DefaultMQProducer producer = new DefaultMQProducer("group1");
//2.设定发送的命名服务器地址
producer.setNamesrvAddr("192.168.23.129:9876");
//3.1启动发送的服务
producer.start();
//4.创建要发送的消息对象,指定topic,指定内容body
Message msg = new Message("topic1","hello rocketmq".getBytes("UTF-8"));
//3.2发送消息
SendResult result = producer.send(msg);
System.out.println("返回结果:"+result);
//5.关闭连接
producer.shutdown();
}
}
消费者:Consumer.java
//消费者,接收消息
public class Consumer {
public static void main(String[] args) throws Exception {
//1.创建一个接收消息的对象Consumer
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
//2.设定接收的命名服务器地址
consumer.setNamesrvAddr("192.168.23.129:9876");
//3.设置接收消息对应的topic,对应的sub标签为任意*
consumer.subscribe("topic1", "*");
//设置当前消费者的消费模式(默认模式:负载均衡)
//consumer.setMessageModel(MessageModel.CLUSTERING);
//设置当前消费者的消费模式为广播模式:所有客户端接收的消息都是一样的
consumer.setMessageModel(MessageModel.BROADCASTING);
//3.开启监听,用于接收消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
//遍历消息
for (MessageExt msg : list) {
System.out.println("收到消息:"+msg);
System.out.println("消息:" + new String(msg.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//4.启动接收消息的服务
consumer.start();
System.out.println("接收消息服务已开启运行");
}
}
默认模式:负载均衡,利用MQ内部的负载均衡模式来平均分配消息
广播模式:所有客户端接收的消息都是一样的
广播模式的现象:
(1):如果生产者先发送消息,后启动消费者,消息只能被消费一次
(2):如果多个消费者先启动(广播模式),后发消息,才有广播效果
2. 发送者发送消息的三种类型
- 同步消息:即时性较强,重要的消息,且必须有回执的消息,例如短信,通知
prodecer.send ( Message msg )
// 同步消息发送
Message msg = new Message("topic2",("同步消息:hello rocketmq "+i).getBytes("UTF-8"));
SendResult result = producer.send(msg);
System.out.println("返回结果:"+result);
- 异步消息:即时性较弱,但需要有回执的信息,例如订单中的某些信息
producer.send( Message msg, SendCallback sendCallback )
//异步消息发送(回调处理结果必须在生产者进程结束前执行,否则回调无法正确执行)
Message msg = new Message("topic2",("异步消息:hello rocketmq "+i).getBytes("UTF-8"));
producer.send(msg, new SendCallback() {
//表示成功返回结果
public void onSuccess(SendResult sendResult) {
System.out.println(sendResult);
}
//表示发送消息失败
public void onException(Throwable t) {
System.out.println(t);
}
});
- 单向消息:不需要回执的信息,例如日志类消息
sendOneway( Message msg )
//单向消息
Message msg = new Message("topic2",("单向消息:hello rocketmq "+i).getBytes("UTF-8"));
producer.sendOneway(msg);
3. 延时消息:msg.setDelayTimeLevel(3);
消息发送时并不直接发送到消息服务器,而是根据设定的等待时间到达,起到延时到达的缓冲作用
Message msg = new Message("topic3",("非延时消息:hello rocketmq "+i).getBytes("UTF-8"));
//设置当前消息的延时效果,顺序3表示30s
//消息时间等级依次为
//1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
msg.setDelayTimeLevel(3);
SendResult result = producer.send(msg);
System.out.println("返回结果:"+result);
4. 批量消息:send ( Collection<Message> msgs )
//创建一个集合保存多个消息
List<Message> msgList = new ArrayList<Message>();
Message msg1 = new Message("topic5",("批量消息:hello rocketmq "+1).getBytes("UTF-8"));
Message msg2 = new Message("topic5",("批量消息:hello rocketmq "+2).getBytes("UTF-8"));
Message msg3 = new Message("topic5",("批量消息:hello rocketmq "+3).getBytes("UTF-8"));
msgList.add(msg1);
msgList.add(msg2);
msgList.add(msg3);
//发送批量消息(每次发送的消息总量不得超过4M)
//消息的总长度包含4个信息:
//topic(字符串长度),body(字符串长度)
//消息的属性(key与value对应字符串字节数),日志(20字节)
SendResult send = producer.send(msgList);
5. 消息过滤(分类过滤)
生产者:Message( String topic, String tags,byte[] body )
//创建消息的时候除了制定topic,还可以指定tag
Message msg = new Message("topic6","tag2",
("消息过滤按照tag:hello rocketmq 2").getBytes("UTF-8"));
消费者
//接收消息的时候,除了制定topic,还可以指定接收的tag,*代表任意tag
consumer.subscribe("topic6","tag1 || tag2");
6. 消息过滤(SQL过滤、属性过滤、语法过滤)
生产者
//为消息添加属性
msg.putUserProperty("vip","1");
msg.putUserProperty("age","20");
消费者
//使用消息选择器来过滤对应的属性,语法格式为类SQL语法
consumer.subscribe("topic7", MessageSelector.bySql("age >= 18"));
注意:在开启Broker服务时,要开启对SQL语法的支持
配置文件中添加属性:enablePropertyFilter=true
启动命令:sh mqbroker -n localhost:9876 -c ../conf/broker.conf
7. 顺序消息
问题引出:生产者发出具有顺序的业务消息,存放于不同的消息队列中,消费者接收的消息会发生顺序错乱,导致业务前置工作没做完,后置工作已经启动了。
解决方案:指定相同业务的消息按照先后顺序存储在同一队列中,消息的消费者应该一个队列开启一个线程进行接收(会影响速度)。
生产者:producer.send( Message msg, MessageQueueSelector selector, Object arg )
//设置消息进入到指定的消息队列中
for(final Order order : orderList){
Message msg = new Message("orderTopic",order.toString().getBytes());
//发送时要指定对应的消息队列选择器
SendResult result = producer.send(msg, new MessageQueueSelector() {
//设置当前消息发送时使用哪一个消息队列
public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
//根据发送的信息不同,选择不同的消息队列
//根据id来选择一个消息队列的对象,并返回->id得到int值
int mqIndex = order.getId().hashCode() % list.size();
return list.get(mqIndex);
}
}, null);
System.out.println(result);
}
消费者:consumer.registerMessageListener( MessageListenerOrderly messageListener)
//使用单线程的模式从消息队列中取数据,一个线程绑定一个消息队列
consumer.registerMessageListener(new MessageListenerOrderly() {
//使用MessageListenerOrderly接口后,
//对消息队列的处理由一个消息队列多个线程服务,转化为一个消息队列一个线程服务
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;
}
});
8. 事务消息
MQ的事务流程(本地代码正常执行)
生产者首先发送一半的消息(half消息),消息服务器返回接收状态ok,由本地事务的处理结果来决定消息是否发送。如若发送,则生产者提交完整消息进入队列;如果不发送则回滚,half消息被删除。
MQ的消息补偿过程(当本地代码执行失败时)
事务消息状态:
提交状态:允许进入队列,此消息与非事务消息没有区别
回滚状态:不允许进入队列,此消息等同于未发送过
中间状态:完成了half消息的发送,未对MQ进行二次状态确认
生产者
- 事务消息使用的生产者是TransactionMQProducer
- 事务发送使用的是 producer.sendMessageInTransaction( Message msg, Object arg )
- 对于事务的处理过程,需要添加本地事务监听器
//事务消息使用的生产者是TransactionMQProducer
TransactionMQProducer producer = new TransactionMQProducer("group1");
producer.setNamesrvAddr("192.168.184.128:9876");
//添加本地事务对应的监听
producer.setTransactionListener(new TransactionListener() {
//正常事务过程
public LocalTransactionState executeLocalTransaction(Message message, Object o) {
//事务提交状态
return LocalTransactionState.UNKNOW;
}
//事务补偿过程
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
System.out.println("事务补偿过程执行");
return LocalTransactionState.COMMIT_MESSAGE;
}
});
producer.start();
Message msg = new Message("topic8",("事务消息:hello rocketmq ").getBytes("UTF-8"));
SendResult result = producer.sendMessageInTransaction(msg,null);
System.out.println("返回结果:"+result);
producer.shutdown();
由正常事务过程的返回值来决定事务状态
提交状态:LocalTransactionState.COMMIT_MESSAGE
回滚状态:LocalTransactionState.ROLLBACK_MESSAGE
中间状态:LocalTransactionState.UNKNOW
四、集群
单机:一个broker提供服务(宕机后服务瘫痪)
多个broker提供服务:有效解决了宕机后服务瘫痪的问题,但单机宕机后消息无法及时消费
多个master多个slave:(根据数据同步的时机不同,会有两种方式)
master到slave消息同步方式为同步(较异步方式性能略低,消息无延迟)
master到slave消息同步方式为异步(较同步方式性能略高,数据略有延迟)
1. 集群特征
命名服务器(NameServer)集群部署:无状态节点,即各命名服务器之间互不通信;全服务器注册,即所有的broker在注册时注册了所有的命名服务器,命名服务器之间不存在信息差,通过任一服务器都能找到对应topic的broker及其所在队列。
Broker集群部署:Broker分为Master与Slave,一个Master可以对应多个Slave,但是一个Slave只能对应一个Master;BrokerId为0表示Master,非0表示Slave,Master与Slave的对应关系通过指定相同的BrokerName。Master也可以部署多个。由master进行消息的读写操作,只有当master负载过大时,才由slave读取消息。每个Broker与NameServer集群中的所有节点建立长连接,定时注册Topic信息到所有NameServer。
Producer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer取Topic路由信息,并向提供Topic服务的Master建立长连接,且定时向Master发送心跳。Producer完全无状态,可集群部署。
Consumer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer取Topic路由信息,并向提供Topic服务的Master、Slave建立长连接,且定时向Master、Slave发送心跳。Consumer既可以从Master订阅消息,也可以从Slave订阅消息,订阅规则由Broker配置决定。
2. 集群的工作流程
- 步骤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
读取消息时,判定broker当前是否处于高负载状态,broker负载状态低,读取消息,broker负载状态高,连接对应的slave读取消息,降低master工作压力
五、MQ内部存储及消息传输
1. 文件系统存储方案
问题引入:broker中的数据如果不进行持久化存储,一旦broker停止服务,消息就丢失了。消息存储有两种方式,一种是数据库存储,但是数据库瓶颈将成为MQ瓶颈;一种是文件系统存储方案,采用消息刷盘机制进行数据存储。
2. MQ 高效的消息存储与读写方式
1)磁盘的获取方式:通过启动时初始化文件大小来保证占用固定的磁盘空间,保证磁盘读写速度,在预留空间中实现顺序写。
2)零拷贝技术:数据传输由传统的4次复制简化成3次复制,减少1次复制过程,加快数据传输过程。
Linux网络数据传输过程:
- 文件读写的重要指标,硬盘的读写速度,除了可以使用SSD之外,还可以通过顺序写来提高读写速度。由于磁盘存储有占用,会采取随机写的方式(100kb/s)。顺序写(600M/s)是在现有的内容尾部追加内容,因此要保证有足够的空余磁盘空间。
- Java语言中使用 MappedByteBuffer 类实现了零拷贝技术
3. 消息的存储结构
消费逻辑队列:记录每个队列的使用情况,每个消费逻辑队列与每个消息队列一一绑定。
索引:便于快速查找,每个消息队列都要创建自己的索引
4. 刷盘机制
消息存储最终存放在磁盘上,也就是文件系统中,这个过程被称为刷盘。
1)同步刷盘
- 1) 生产者发送消息到MQ,MQ接到消息数据
- 2) MQ挂起生产者发送消息的线程
- 3) MQ将消息数据写入内存
- 4) 内存数据写入硬盘
- 5) 磁盘存储后返回SUCCESS
- 6) MQ恢复挂起的生产者线程
- 7) 发送ACK到生产者
2)异步刷盘
- 1) 生产者发送消息到MQ,MQ接到消息数据
- 2) MQ将消息数据写入内存
- 3) 发送ACK到生产者
- --等消息量多了--
- 4) 内存数据写入硬盘
5. 主从数据复制
同步复制:master接到消息后,先复制到slave,然后反馈给生产者写操作成功
异步复制:master接到消息后,立即返回给生产者写操作成功,当消息达到一定量后再异步复制到slave
6. 负载均衡
Producer负载均衡:内部实现了不同broker集群中对同一topic对应消息队列的负载均衡
Consumer负载均衡:
平均分配
循环平均分配
7. 消息重试
当消息消费后未正常返回消费成功的信息将启动消息重试机制
1. 顺序消息重试
当消费者消费消息失败后,RocketMQ会自动进行消息重试(每次间隔时间为 1 秒)
注意:应用会出现消息消费被阻塞的情况,因此,要对顺序消息的消费情况进行监控,避免阻塞现象的发生
2. 无序消息重试
无序消息包括普通消息、定时消息、延时消息、事务消息
无序消息重试仅适用于负载均衡(集群)模型下的消息消费,不适用于广播模式下的消息消费
为保障无序消息的消费,MQ设定了合理的消息重试间隔时长