这里写自定义目录标题
1、MQ的认知
MQ:Message Queue 消息队列
是一个服务,接收消息(数据),先进先出。
作用:
1、把串行工作改为并行进行,提升运行效率
2、流量削峰
都有哪些MQ?
市面上比较火爆的几款MQ:
ActiveMQ,RocketMQ,Kafka,RabbitMQ。
-
语言的支持:ActiveMQ,RocketMQ只支持Java语言,Kafka可以支持多们语言,RabbitMQ支持多种语言。
-
效率方面:ActiveMQ,RocketMQ,Kafka效率都是毫秒级别,RabbitMQ是微秒级别的。
-
消息丢失,消息重复问题: RabbitMQ针对消息的持久化,和重复问题都有比较成熟的解决方案。
-
学习成本:RabbitMQ非常简单。
RabbitMQ是由Rabbit公司去研发和维护的,最终是在Pivotal。
RabbitMQ严格的遵循AMQP协议,高级消息队列协议,帮助我们在进程之间传递异步消息。
2、搭建RabbitMQ的环境
1、先安装RabbitMQ的依赖环境erlang
最好默认路径(非中文),一路下一步即可
2、配置环境变量,新建环境变量
安装rabbitmq,最后默认路径,一路下一步
新建系统环境变量
编辑系统环境变量path
启动监控程序
进入到rabbitmq的安装路径下的sbin文件夹下,cmd打开命令窗口,拖拽rabbitmq-plugins.bat后,输入 enable rabbitmq-management 来启动
到程序下:
启动后,访问网址
3、RabbitMQ的体系结构
4、RabbitMQ的通讯
4、1 简单通讯方式
特点:1、一个生产者
2、一个交换机—默认的
3、一个队列—默认的
4、一个消费者
1、新建生产者
2、添加jar依赖
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.6.0</version>
</dependency>
</dependencies>
3、配置文件、启动类
4、在common中编写工具类:获取rabbitmq服务的连接
package com.qf.health2205common.util;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class RabbitMQUtil {
private static ConnectionFactory factory;
static {
factory=new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
// factory.setUsername("guest");
// factory.setPassword("guest");
// factory.setVirtualHost("/");
}
//获取连接的方法
public static Connection getMQConnection(){
Connection connection=null;
try {
connection=factory.newConnection();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
return connection;
}
}
5、在业务逻辑层写入消息到队列中
package com.qf.health2205publisher.service.impl;
import com.qf.health2205common.util.RabbitMQUtil;
import com.qf.health2205publisher.service.PublisherService;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import org.springframework.stereotype.Service;
import java.io.IOException;
@Service
public class PublisherServiceImpl implements PublisherService {
//声明RabbitMQ的连接对象
private Connection connection= RabbitMQUtil.getMQConnection();
@Override
public String addMessageToMq(String meg) {
//定义管道
Channel channel=null;
String result="";
try {
channel=connection.createChannel();
//把消息通过管道,写入交换机中
//参数1:交换机的名字,“” 使用默认的交换机
//参数2:是队列的名字,在同一服务器内,队列不能重名
//参数3:写入消息时的携带信息,null
//参数4:写入的消息,必须是byte[]类型
channel.basicPublish("","java220501",null,meg.getBytes());
result= "add Message success!";
} catch (IOException e) {
result="add Message failed!";
e.printStackTrace();
}
return result;
}
}
消费者的controller
package com.qf.health2205publisher.controller;
import com.qf.health2205publisher.service.PublisherService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/pmq")
public class PublisherController {
@Autowired
private PublisherService publisherService;
@GetMapping("/add1")
public String add1(String meg){
return publisherService.addMessageToMq(meg);
}
}
创建消费者服务
4、2 work 工作队列通讯方式
特点:一个生产者、一个交换机、一个队列,多个消费者
设置每个消费者的消费能力:一次只消费一个消息
//定义消费者每次只消费1个消息
channel.basicQos(1);
自动消息确认删除的缺点:消费者有可能没有把消息消费成功,但是消息已经从队列删除了,导致消息的丢失
设置手动消息确认删除:当消息消费成功后,才从队列中删除该消息。
又增加一个消费者,两个消费者从同一个队列中消费消息
4、3 发布订阅通讯方式
特点:生产者一个,交换机一个,多个队列,每个队列对应若干个消费者
场景:一个消息,同时投递到多个队列中
下订单: 饿了么 作为生产者,把一个订单信息同时写入 通知商家的队列 ----商家接单程序是消费者
通知骑手的队列 —骑手接单程序是消费者
库存队列 —写入订单到数据库的消费者
生产者:
1、交换机的类型:fanout 广播类型
2、声明多个队列
@Override
public String addMessageToMq2(String meg) {
//获取管道
Channel channel=null;
String result="";
try {
channel=connection.createChannel();
//定义交换机
//参数1:交换机的名字 ,要唯一
//参数2:交换机的类型 FANOUT---广播类型 能把一个消息同时投递到多个队列中
channel.exchangeDeclare("exchange1", BuiltinExchangeType.FANOUT);
//队列绑定
//参数1:声明的队列的名字,不要重名
//参数2:哪个交换机向此队列投递消息,交换机的名字
//参数3:路由规则,"" 无路由规则
channel.queueBind("java220502","exchange1","");
channel.queueBind("java220503","exchange1","");
//写入消息
channel.basicPublish("exchange1","",null,meg.getBytes());
result="add2 success";
} catch (IOException e) {
result="add2 failed";
e.printStackTrace();
}
return result;
}
消费者:让不同的消费者从不同的队列中来消费消息
4、4 route路由通讯方式
特点:一个生产者,一个交换机,多个队列,每个队列对应自己的若干个消费者
交换机根据路由规则,把消息投递到符合路由规则的队列中
生产者端:
1、定义多个队列,指定路由规则
2、交换机的类型是direct定向类型
@Override
public String addMessageToMq3(String meg, String route) {
Channel channel=null;
String result="";
try {
channel=connection.createChannel();
//定义交换机的类型
//参数1:交换机的名字
//参数2:交换机的类型---DIRECT,把消息投递到符合路由规则的队列中,路由规则中不允许有通配符
channel.exchangeDeclare("exchange2",BuiltinExchangeType.DIRECT);
//绑定队列
//参数1:队列的名字
//参数2:指定哪个交换机向此队列写入消息
//参数3:路路由规则
channel.queueBind("java220504","exchange2","food");
channel.queueBind("java220505","exchange2","play");
//写入消息
//参数2:写入消息时传递的路由规则
channel.basicPublish("exchange2",route,null,meg.getBytes());
result="add3 success";
} catch (IOException e) {
result="add3 failed";
e.printStackTrace();
}
return result;
}
消费者端,修改消费者消费的队列即可
交换机把消息写到符合路由规则的队列中。
4、5 topic路由通讯方式
特点:一个生产者,一个交换机,多个队列,每个队列对应消费者
交换机把消息投递到符合路由规则的队列中,但是路由规则支持通配符
支持通配符的路由规则的语法:
字符串1.字符串2.字符串3
同配置只允许出现 * #
*仅代表一组字符串 # 代表若干组字符串
java.part4.* 路由规则中只要是java.part4.任何字符串都可以
java.# java.part1.javase java.part2.javaweb java.part3.framework
#.java
生产者:
1、交换机的类型是topic
2、路由规则中使用通配符
@Override
public String addMessageToMq4(String meg, String route) {
Channel channel=null;
String result="";
try {
channel=connection.createChannel();
//定义交换机的类型
//参数2:交换机的类型---TOPIC,支持陆游与规则中的通配符
channel.exchangeDeclare("exchange3",BuiltinExchangeType.TOPIC);
//队列绑定:交换机 带通配符的路由规则
channel.queueBind("java220506","exchange3","java.part4.*");
channel.queueBind("java220507","exchange3","#.python");
//写入消息
channel.basicPublish("exchange3",route,null,meg.getBytes());
result="add4 success";
} catch (IOException e) {
result="add4 failed";
e.printStackTrace();
}
return result;
}
5、消息的可靠性
MQ属于内存存储消息,需要确保消息不丢失
1、使用confirm机制确保生产者成功的将消息写入队列
2、使用return机制记录交换机写入队列失败的消息
3、让队列持久化,确保队列在MQ当即后,依然不丢失消息
4、使用手动Ack,确保消息被成功消费
5、1:Confirm机制 确保生产者把消息成功的写入交换机
当批量把消息写入交换机时,需要批量确认
@Override
public String addMessageToMq5(String meg) {
Channel channel=null;
String result="";
try {
channel=connection.createChannel();
//开启confirm机制
channel.confirmSelect();
//批量写入
for(int i=0; i<100; i++){
meg=meg+i;
channel.basicPublish("","java220508",null,meg.getBytes());
}
//当批量写入消息到交换机,如果有1个消息写入失败,那么之前写入的消息均撤销,并且抛出异常
channel.waitForConfirmsOrDie();
result="add5 success";
} catch (IOException | InterruptedException e) {
result="add5 failed";
e.printStackTrace();
}
return null;
}
异步确认:添加监听,监听批量操作中的每一个消息,谁写入成功了,谁写入失败,认为对写入失败的消息进行处理,无需对写入成功的消息进行撤销。
@Override
public String addMessageToMq6(String meg) {
Channel channel=null;
String result="";
try {
channel=connection.createChannel();
channel.confirmSelect();
for(int i=0; i<99; i++){
meg=meg+i;
channel.basicPublish("","java220509",null,meg.getBytes());
}
//异步confirm ,添加监听
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleAck(long l, boolean b) throws IOException {
//监听到某个消息写入交换机成功后,回调的方法
System.out.println(l+"消息写入成功!");
}
@Override
public void handleNack(long l, boolean b) throws IOException {
//监听到某个消息写入交换机失败,回调的方法
System.out.println(l+"消息写入失败!");
}
});
result="add6 success";
} catch (IOException e) {
result="add6 failed";
e.printStackTrace();
}
return result;
}
5、2 使用return机制,确保交换机里的消息成功的写入队列中
【面试题】在rabbit mq中,如何确保消息的可靠性?
从如上4点。
6、RabbitMQ的常见问题及解决方案
6、1:消息的重复消费问题
当一个队列有多个消费者时,如果其中一个消费者正在消费某个消息,另一个消费者也来消费此消息,造成同一个消息被多个消费者重复消费。
锁:锁的是线程
在分布式系统下,传统的锁就失效,无法锁进程。
需要分布式锁:使用Redis
1、当某个消费者要消费消息时,先到Redis中读取某个变量(锁)的值
2、如果读取不到或者读取到的是0,意味着当前服务可以消费消息
3、把锁变量的值改为1,同时去消费消息
4、如果读取到的是1,则不去消费消息
5、当消费消息的进程消费结束后,把锁变量的值改为0.
6、【强调】没有获得锁的进程,需要自行每间隔一段时间,去尝试获取锁。
@Override
public void getMessageFromMq() {
//说明,消费者需要一直监控队列,所以是无返回值的
Channel channel=null;
try {
channel=connection.createChannel();
//定义消费者每次只消费1个消息
channel.basicQos(1);
//定义队列
//参数1:从哪个队列中获取消息,队列的名字
//参数2:队列是否支持持久化 需要支持,当MQ服务器宕机重启后,还能加载到消息,防止消息的丢失
//参数3:是否排外,是否只允许一个消费者消费
//参数4:当消费者不存在时,是否删除队列?
//参数5:是否有其他携带消息处理
channel.queueDeclare("java220506",true,false,false,null);
Channel channel1=channel;
//定义监听:监控队列中是否有消息,一旦有消息,就获取消息
DefaultConsumer defaultConsumer=new DefaultConsumer(channel){
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
Jedis jedis= RedisUtil.getJedis();
jedis.select(3);
String lock=jedis.get("mqLock");
if(lock==null){
//加锁
jedis.setex("mqLock",3,"1");
//可以消费
//当监听到队列中有消息时,就调用此方法从队列中获取消息
String meg=new String(body);
//消费
System.out.println("consumer---从队列java220506中获取到消息:"+meg);
//在此处判断消息是否消费成功,如果成功则从队列中删除消息
channel1.basicAck(envelope.getDeliveryTag(),false);
//消费结束 解锁
jedis.set("mqLock","0");
}else{
int intLock=Integer.parseInt(lock);
if(intLock==0){
jedis.set("mLock","1");
//当监听到队列中有消息时,就调用此方法从队列中获取消息
String meg=new String(body);
//消费
System.out.println("consumer---从队列java220506中获取到消息:"+meg);
//在此处判断消息是否消费成功,如果成功则从队列中删除消息
channel1.basicAck(envelope.getDeliveryTag(),false);
jedis.set("mqLock","0");
}
}
}
};
//设置如何删除消息
//参数1:从哪个队列删除
//参数2:true 当消费者从队列取出消息后,立刻删除
//此种删除策略:自动消息确认删除,当消费者把消息取出后,立刻删除消息
//设置手动ACK 参数2设置为false 消息取出后不删除
channel.basicConsume("java220506",false,defaultConsumer);
System.out.println("消费者开始监听java220506队列……");
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
}
【注意】加锁使用setex();给锁设置有效期,防止死锁,防止当前进程加上锁后,宕机了,宕机后就不能解锁,造成死锁,设置时间后,在时间内完成消费。
6、2 如何处理消息堆积问题
消息堆积:队列中的消息存满了
造成原因:
1、生产者生产消息快,消费消息慢
2、消费者宕机,无法消费
解决方案:
1、增加消费者的消费能力—开启当前消费者的多线程模式
—增加消费者服务
2、设置死信交换机,死信就会自动进入死信交换机,死信交换机把死信写入死信队列。
3、使用延迟队列:当队列满后,再有消息,直接写入磁盘,并且记录写入顺序
死信:当队列满后,再有消息写入时,队列头部的消息就是死信