1 概述
MQ(Message Queue)消息队列,是一种用来保存消息数据的队列
队列:数据结构的一种,特征为 “先进先出” FIFO
消息中间件:用来在生产者和消费者中间来传输消息。
2 MQ 的作用
-
应用解耦(技术上必须弄好才能使用MQ )
-
快速应用变更维护
-
流量削锋(削峰填谷)
1、任务异步处理
将不需要同步处理的并且耗时长的操作由消息队列通知消息接收方进行异步处理。提高了应用程序的响应时间。
2、应用程序解耦合
MQ相当于一个中介,生产方通过MQ与消费方交互,它将应用程序进行解耦合。
- 系统的耦合性越高,容错性就越低,可维护性就越低。
- 使用 MQ 使得应用间解耦,提升容错性和可维护性。
3、削峰填谷
如订单系统,在下单的时候就会往数据库写数据。但是数据库只能支撑每秒1000左右的并发写入,并发量再高就容易宕机。低峰期的时候并发也就100多个,但是在高峰期时候,并发量会突然激增到5000以上,这个时候数据库肯定卡死了。
消息被MQ保存起来了,然后系统就可以按照自己的消费能力来消费,比如每秒1000个数据,这样慢慢写入数据库,这样就不会卡死数据库了。
但是使用了MQ之后,限制消费消息的速度为1000,但是这样一来,高峰期产生的数据势必会被积压在MQ中,高峰就被“削”掉了。但是因为消息积压,在高峰期过后的一段时间内,消费消息的速度还是会维持在1000QPS,直到消费完积压的消息,这就叫做“填谷”
3 MQ的优缺点
缺点:
1系统可用性降低:集群
2系统复杂度提高:(程序员提升水平)
3异步消息机制(都有解决方案)
消息顺序性
消息丢失
消息一致性
消息重复使用
4 常见产品
ActiveMQ:java语言实现,万级数据吞吐量,处理速度ms级,主从架构,成熟度高
RabbitMQ :erlang语言实现,万级数据吞吐量,处理速度us级,主从架构,
RocketMQ :java语言实现,十万级数据吞吐量,处理速度ms级,分布式架构,功能强大,扩展性强
kafka :scala语言实现,十万级数据吞吐量,处理速度ms级,分布式架构,功能较少,应用于大数据较多
RocketMQ简介
RocketMQ是阿里开源的一款非常优秀中间件产品,脱胎于阿里的另一款队列技术MetaQ,后捐赠给Apache基金会作为一款孵化技术,仅仅经历了一年多的时间就成为Apache基金会的顶级项目。并且它现在已经在阿里内部被广泛的应用,并且经受住了多次双十一的这种极致场景的压力(2017年的双十一,RocketMQ流转的消息量达到了万亿级,峰值TPS达到5600万)
解决所有缺点
5 RocketMQ安装
具体安装查看 RocketMQ安装:https://blog.csdn.net/weixin_45195665/article/details/110351287
角色:
1、NameServer: 命名服务器,主要是协调资源。Broker启动后向NameServer集群的每一个节点注册自己的信息,并保持心跳机制.
NameServer可以做集群:无状态,也就说每个NameServer服务器都是独立的,之间并不通信。
2、Broker: 用来接受消息并转发消息,存储消息使用的。
消息存储使用 Topic进行分类存储:每个Topic 默认有四个消息队列(mq)。Tag是分类的二级分类。
3、Producer: 生产者启动后先和NameServer建立连接,并返回一个分配的Broker服务器的 地址,并将消息发送到对应的Broker服务器上存储。同时,生产者和NameServer保持心跳。
4、Consumer: 消费者启动后先和NameServer建立连接,并保持心跳机制,监听Broker中的对应的Topic中的消息。消费模式:一种推送push,一种拉取pull.
6 使用
发送消息的两 种模式:负载均衡模式(默认)和广播模式
6.1 负载均衡
特点:消费者轮询消费。
环境搭建
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.5.2</version>
</dependency>
消息
- id:mq自动生成
- topic(必须)
- tag
- properties
- 内容
发送
public static void main(String[] args) throws Exception {
//1.创建一个发送消息的对象Producer
DefaultMQProducer producer = new DefaultMQProducer("group1");
//2.设定发送的命名服务器地址
producer.setNamesrvAddr("192.168.38.131:9876");
//3.1启动发送的服务
producer.start();
//4.创建要发送的消息对象,指定topic,指定内容body
Message msg = new Message("topic1","hello rocketmq".getBytes("UTF-8"));
//4.2发送消息
SendResult result = producer.send(msg);
System.out.println("返回结果:"+result);
//5.关闭连接
producer.shutdown();
}
发送多个消息
for (int i = 1; i <= 10; i++) {
Message msg = new Message("topic1",("生产者2: hello rocketmq "+i).getBytes("UTF-8"));
SendResult result = producer.send(msg);
System.out.println("返回结果:"+result);
}
消费者
public static void main(String[] args) throws Exception {
//1.创建一个接收消息的对象Consumer
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
//2.设定接收的命名服务器地址
consumer.setNamesrvAddr("192.168.38.131:9876");
//3.设置接收消息对应的topic,对应的sub标签为任意*
consumer.subscribe("topic1","*");
//默认为负载均衡模式
consumer.setMessageModel(MessageModel.CLUSTERING);
//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;// 成功处理, mq 收到这个 标记后相同的消息讲不会再次发给消费者
}
});
//4.启动接收消息的服务
consumer.start();// 开启多线程 监控消息,持续运行
System.out.println("接收消息服务已开启运行");
}
6.2 广播模式
特点:所有消费者收到同样的消息。
发送者
同上
消费者
//1.创建一个接收消息的对象Consumer
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
System.out.println(consumer.getInstanceName());
//consumer.setInstanceName("instance01");
//2.设定接收的命名服务器地址
consumer.setNamesrvAddr("192.168.38.131: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("消费者1:"+new String(msg.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//4.启动接收消息的服务
consumer.start();
System.out.println("接收消息服务已开启运行");
广播模式的现象
1) 如果 生产者先发送消息, 后启动消费者, 消息只能被消费一次
2) 如果多个消费者先启动(广播模式),后发消息,才有广播的效果
结论:
必须先启动消费者再启动发送者才有广播的效果
6.3 发送者发送消息的类型 三种
同步消息发送:
特点:向MQ发送消息后,生产者同步等待MQ的回信。在此期间什么都不能做。
异步消息发送:
特点:向MQ发送消息后,生产者不必同步等待MQ的回信。在此期间可以干点的别的。如果MQ有回信了,回调函数自动执行得到执行的结果。
单向消息:
特点:只是向单向向MQ发送消息,不接受发送的结果。
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("group1");
producer.setNamesrvAddr("192.168.38.131:9876");
producer.start();
for (int i = 1; i <= 5; i++) {
//同步消息发送
//Message msg = new Message("topic2",("同步消息:hello rocketmq "+i).getBytes("UTF-8"));
//SendResult result = producer.send(msg);
//System.out.println("返回结果:"+result);
//异步消息发送
//Message msg2 = 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);
// }
//});
//单向消息
Message msg3 = new Message("topic2",("单向消息:hello rocketmq "+i).getBytes("UTF-8"));
producer.sendOneway(msg);
}
//添加一个休眠操作,确保异步消息返回后能够输出
TimeUnit.SECONDS.sleep(10);
producer.shutdown();
}
6.4 延时消息
立刻发送,只是告诉MQ,消息隐藏一段时间再暴露
应用场景
下订单时 向mq发一个取消订单消息 (订单号30S演示)
30S后,消费者能看到这个消息,开始处理取消订单(如果没付费)
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("group1");
producer.setNamesrvAddr("192.168.38.131:9876");
producer.start();
for (int i = 1; i <= 5; i++) {
Message msg = new Message("topic3",("非延时消息:hello rocketmq "+i).getBytes("UTF-8"));
// 不是30秒后再发送,而是先发送,但是通知mq , 30s 才对外暴露数据
//设置当前消息的延时效果(比如订单,下订单后,20分钟后,决定这个订单是否删除,)
msg.setDelayTimeLevel(3); //18个延迟级别
// 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
SendResult result = producer.send(msg);
System.out.println("返回结果:"+result);
}
producer.shutdown();
}
6.5 批量消息
List<Message> msgList = new ArrayList<Message>();
SendResult send = producer.send(msgList);
注意:
消息内容总长度不超过4M
消息内容总长度包含如下:
topic(字符串字节数)
body (字节数组长度)
消息追加的属性(key与value对应字符串字节数)
日志(固定20字节)
6.6 Tag
tag: 相当于topic的二级分类
发送者
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("group1");
producer.setNamesrvAddr("192.168.38.131:9876");
producer.start();
//创建消息的时候除了制定topic,还可以指定tag
Message msg = new Message("topic6","tag2",("消息过滤按照tag:hello rocketmq 2").getBytes("UTF-8"));
SendResult send = producer.send(msg);
System.out.println(send);
producer.shutdown();
}
消费者
*
代表任意tag
"tag1 || tag2"
代表两个 tag 哪个都行
//接收消息的时候,除了制定topic,还可以指定接收的tag,*代表任意tag
consumer.subscribe("topic6","tag1 || tag2");
consumer.subscribe("topic6","*");
6.7 sql
生产者
//为消息添加属性
msg.putUserProperty("vip","1");
msg.putUserProperty("age","20");
消费者
//使用消息选择器来过滤对应的属性,语法格式为类SQL语法
consumer.subscribe("topic7", MessageSelector.bySql("age >= 18"));
注意:SQL过滤需要依赖服务器的功能支持,在broker配置文件中添加对应的功能项,并开启对应功能
enablePropertyFilter=true
启动服务器
sh mqbroker -n localhost:9876 -c ../conf/broker.conf
6.7 顺序消息
默认情况下,MQ 开启了多个队列, 同时发送多个消息的的话,发送给哪个队列是不确定的,同时消息的消费者读取消息,每读取一个消息开启一个线程,也不能保证消息的顺序性,
想要保证消息的有序性,需要指定消息的队列,同时 消息的消费者应该在一个队列开启一个线程进行接收而不是一个消息一个线程)
发送者
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("group1");
producer.setNamesrvAddr("192.168.31.80:9876");
producer.start();
//创建要执行的业务队列
List<Order> orderList = new ArrayList<Order>();
Order order11 = new Order();
order11.setId("a");
order11.setMsg("主单-1");
orderList.add(order11);
Order order12 = new Order();
order12.setId("a");
order12.setMsg("子单-2");
orderList.add(order12);
Order order13 = new Order();
order13.setId("a");
order13.setMsg("支付-3");
orderList.add(order13);
Order order14 = new Order();
order14.setId("a");
order14.setMsg("推送-4");
orderList.add(order14);
Order order21 = new Order();
order21.setId("b");
order21.setMsg("主单-1");
orderList.add(order21);
Order order22 = new Order();
order22.setId("b");
order22.setMsg("子单-2");
orderList.add(order22);
Order order31 = new Order();
order31.setId("c");
order31.setMsg("主单-1");
orderList.add(order31);
Order order32 = new Order();
order32.setId("c");
order32.setMsg("子单-2");
orderList.add(order32);
Order order33 = new Order();
order33.setId("c");
order33.setMsg("支付-3");
orderList.add(order33);
//设置消息进入到指定的消息队列中
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) {
System.out.println(list.size());// 数量只能通过修改 mq 的配置 改变(阿里开发团队认为,这个是敏感资源需要服务器管理员控制,而不是编码人员控制)
//根据发送的信息不同,选择不同的消息队列
//根据id来选择一个消息队列的对象,并返回->id得到int值
int mqIndex = order.getId().hashCode() % list.size();
return list.get(mqIndex);
}
}, null);
System.out.println(result);
}
producer.shutdown();
}
接受者
public static void main(String[] args) throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
consumer.setNamesrvAddr("192.168.38.131:9876");
consumer.subscribe("orderTopic","*");
//使用MessageListenerOrderly接口后,仍然是多线程消费,但是它本身能保证一个消息队列中的消息消费是有顺序的,也就说保证第一个消息消费完后才消费第二个,并不能保证是单线程的。
consumer.registerMessageListener(new 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;
}
});
consumer.start();
System.out.println("接收消息服务已开启运行");
}
注:由于每个线程执行的时间不一样,从运行结果中无法准确看出执行的顺序性。
6.8 事务消息
RocketMQ 也允许我们像mysql 一样发送具有事务特征的消息
MQ 的事务流程(本地代码正常执行)
MQ 的消息补偿过程(当本地代码执行失败时)
MQ 消息的三种状态
提交状态:允许进入队列,此消息与非事务消息无区别
回滚状态:不允许进入队列,此消息等同于未发送过
中间状态:完成了 half 消息的发送,未对 MQ 进行二次状态确认(未知状态)
注意:事务消息仅与生产者有关,与消费者无关
生产者代码代码
public static void main1(String[] args) throws Exception {
//事务消息使用的生产者是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.COMMIT_MESSAGE;// 类似于msql 的 commit
}
//事务补偿过程
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
return null;
}
});
producer.start();
Message msg = new Message("topic8",("事务消息:hello rocketmq ").getBytes("UTF-8"));
SendResult result = producer.sendMessageInTransaction(msg,null);
System.out.println("返回结果:"+result);
producer.shutdown();
}
补偿代码
public static void main(String[] args) throws Exception {
//事务消息使用的生产者是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("topic11",("事务消息:hello rocketmq ").getBytes("UTF-8"));
SendResult result = producer.sendMessageInTransaction(msg,null);
System.out.println("返回结果:"+result);
//事务补偿过程必须保障服务器在运行过程中,否则将无法进行正常的事务补偿
//producer.shutdown();
}