1、MQ概述
- MQ是消息队列,存储消息的中间件,就是存储小数据的一个容器
- 分布式系统通信有两种方式:直接远程调用和借助第三方完成间接通信
- 发送方称为生产者,接收方称为消费者
MQ的优势:
- 应用解耦:提高系统容错性和可维护性
- 异步提速:提升用户体验和系统吞吐量
- 削峰填谷:提高系统稳定性
MQ的劣势:
- 系统可用性降低:系统引入的外部依赖越多,系统稳定性越差,一旦MQ宕机,就会对业务造成影响。
- 系统复杂度提高
- 一致性问题:A处理完业务,通过MQ给B、C、D三个发送消息数据,如果B、C系统处理成功,D系统处理失败,怎么保证消息处理的一致性。
2、使用MQ需要满足什么条件呢?
1)生产者不需要从消费者处获得反馈。引入消息队列之前的直接调用。
2)容许短暂的数据不一致性。
3)确实是用了有效果,但是要确保这方面的收益超过加入MQ,管理MQ的这些成本。
3、RabbitMQ基本概述
- RabbitMQ是基于AMQP协议使用Erlang语言开发的一款消息队列产品。
- RabbitMQ提供了6种工作模式
- AMQP是协议,类比HTTP
- JMS是API规范接口,类比JDBC
4、Linux系统安装RabbitMQ
1)第一步:使用xftp将压缩包导入rabbitmq文件夹中
2)第二步:安装语言
# 安装
rpm -ivh erlang-18.3-1.el7.centos.x86_64.rpm
3)第三步:安装环境
# 安装
rpm -ivh socat-1.7.3.2-5.el7.lux.x86_64.rpm
4)第四步:安装rabbitmq
# 安装
rpm -ivh rabbitmq-server-3.6.5-1.noarch.rpm
5)第五步:启动rabbitmq
systemctl start rabbitmq-server
6)第六步:安装界面管理工具
# 开启管理界面
rabbitmq-plugins enable rabbitmq_management
7)第七步:修改配置文件
# 修改默认配置信息
vim /usr/lib/rabbitmq/lib/rabbitmq_server-3.6.5/ebin/rabbit.app
# 比如修改密码、配置等等,例如:loopback_users 中的 <<"guest">>,只保留guest
8)第八步:重启服务
systemctl restart rabbitmq-server
9)第九步:测试登录界面管理
http://192.168.192.131:15672/
#账号是:guest
#密码是:guest
4、RabbitMQ快速入门
1)第一步:创建生产者发送消息到RabbitMQ中
public class ProducerTest {
public static void main(String[] args) throws IOException, TimeoutException {
//1.创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//2.设置参数
factory.setHost("192.168.192.131");//ip 默认值是 localhost
factory.setPort(5672);//端口号 默认值是 5672
factory.setVirtualHost("/itcast");//虚拟机 默认值是/
factory.setUsername("long");//用户名 默认是guest
factory.setPassword("123456");//密码 默认是guest
//3.创建连接Connection
Connection connection = factory.newConnection();
//4.创建Channel
Channel channel = connection.createChannel();
/*
queueDeclare(String queue, boolean durable, boolean exclusive,
boolean autoDelete, Map<String, Object> arguments)
参数:
1.queue:队列名称
2.durable:是否持久化,当mq重启之后,还在
3.exclusive:
* 是否独占,只能有一个消费者监听这队列
* 当Connection关闭时,是否删除队列
4.autoDelete:是否自动删除,当没有Consumer时,自动删除掉
5.arguments:参数
*/
//如果没有一个名字叫hello_world的队列,则会创建该队列,如果有则不会创建
//5.创建队列Queue
channel.queueDeclare("hello_world",true,false,false,null);
//6.指定要发送的消息
String body = "hello rabbitmq~~~";
/*
basicPublish(String exchange, String routingKey, AMQP.BasicProperties props, byte[] body)
参数:
1.exchange:交换机名称。简单模式下交换机会使用默认的 ""
2.routingKey:路由名称
3.props:配置信息
4.body:发送消息数据
*/
//7.发送消息
channel.basicPublish("","hello_world",null,body.getBytes());
//8.释放资源
channel.close();
connection.close();
}
}
2)第二步:创建消费者订阅消息
public class ConsumerTest {
public static void main(String[] args) throws IOException, TimeoutException {
//1.创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//2.设置参数
factory.setHost("192.168.192.131");//ip 默认值是 localhost
factory.setPort(5672);//端口号 默认值是 5672
factory.setVirtualHost("/itcast");//虚拟机 默认值是/
factory.setUsername("long");//用户名 默认是guest
factory.setPassword("123456");//密码 默认是guest
//3.创建连接Connection
Connection connection = factory.newConnection();
//4.创建Channel
Channel channel = connection.createChannel();
/*
queueDeclare(String queue, boolean durable, boolean exclusive,
boolean autoDelete, Map<String, Object> arguments)
参数:
1.queue:队列名称
2.durable:是否持久化,当mq重启之后,还在
3.exclusive:
* 是否独占,只能有一个消费者监听这队列
* 当Connection关闭时,是否删除队列
4.autoDelete:是否自动删除,当没有Consumer时,自动删除掉
5.arguments:参数
*/
//如果没有一个名字叫hello_world的队列,则会创建该队列,如果有则不会创建
//5.创建队列Queue
channel.queueDeclare("hello_world",true,false,false,null);
/*
basicConsume(String queue, boolean autoAck, Consumer callback)
参数:
1.queue:队列名称
2.autoAck:是否自动确认
3.callback:回调对象
*/
//接收消息
Consumer consumer = new DefaultConsumer(channel){
/*
回调方法,当收到消息后,会自动执行该方法
参数:
1.consumerTag:标识
2.envelope:获取一些信息,交换机,路由key...
3.properties:配置信息
4.body:数据
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("consumerTag:" + consumerTag);
System.out.println("Exchange:" + envelope.getExchange());
System.out.println("RoutingKey:" + envelope.getRoutingKey());
System.out.println("properties:" + properties);
System.out.println("body:" + new String(body));
}
};
channel.basicConsume("hello_world", true,consumer);
}
}
5、Work Queues工作队列模式
1)在一个队列中如果有多个消费者,那么消费者之间对于同一个消息的关系是竞争的关系。
2)Work Queues对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度。例如:短信服务部署多个,只需要有一个节点成功发送即可。
6、消息应答
为了保证消息在发送过程中不丢失,rabbitmq引入消息应答机制,消息应答就是:消费者在接收到消息并且处理该消息之后,告诉rabbitmq它已经处理了,rabbitmq可以把该消息删除了。
7、消息重新入队
如果消费者由于某些原因失去连接,导致消息未发送ACK确认,RabbitMQ将了解到消息未完全处理,并将其重新排队。如果此时其他消费者可以处理,它将很快将其重新分发到另一个消费者。这样,即使某个消费者偶尔死亡,也可以确保不会丢失任何消息。
8、单个确认发布
这是一种简单的确认方式,它是一种同步确认发布的方式,也就是发布一个消息之后只有它被确认发布之后,后续的消息才能继续发布,缺点是:发布速度特别的慢,如果因为没有确认发布的消息就会阻塞所有后续消息的发布,这种方式最多提供每秒不超过数百条发布消息的吞吐量。
9、批量确认发布
与单个等待确认消息相比,先发布一批消息然后一起确认可以极大地提高吞吐量,当然这种方式的缺点就是,当发生故障导致发布出现问题时,不知道是哪个消息出现问题了,我们必须将整个批处理保存在内存中,以记录重要的信息而后重新发布消息。当然这种方案仍然是同步的,也一样阻塞消息的发布。
10、异步确认发布
异步确认发布虽然变成逻辑比上两个要复杂,但是性价比最高,无论是可靠性还是效率都没得说,它是利用回调函数来达到消息可靠性传递的,这个中间件也是通过函数回调来保证是否投递成功。
11、交换机
RabbitMQ消息传递模型的核心思想是:生产者生产的消息从不会直接发送到队列。实际上,通常生产者甚至都不知道这些消息传递到哪些队列中。
相反,生产者只能将消息发送到交换机,交换机工作的内容非常简单,一方面它接收来自生产者的消息,另一方面将它们推入队列,交换机必须确切知道如何处理收到的消息,是应该把这些消息放到特定队列还是说应该丢弃它们,这都由交换机的类型来决定。
fanout广播模式:
消费者01和02(同理)
/**
* 消费者01:测试交换机类型fanout,发布订阅
*/
public class ReceiveLogs01 {
//交换机名称
public static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws Exception{
//获取信道
Channel channel = RabbitMQUtils.getChannel();
//指定交换机名称
channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
//获取一个临时队列
String queueName = channel.queueDeclare().getQueue();
//绑定交换机
channel.queueBind(queueName,EXCHANGE_NAME,"");
System.out.println("C1开始准备接收消息:");
//成功!回调函数
DeliverCallback deliverCallback = (consumerTag,message)->{
System.out.println("接收到的消息是:" + new String(message.getBody(),"UTF-8"));
};
CancelCallback cancelCallback = consumerTag->{
System.out.println("接收消息失败" + consumerTag);
};
//接收消息
channel.basicConsume(queueName,true,deliverCallback,cancelCallback);
}
}
生产者
/**
* 生产者:测试fanout发布和订阅
*/
public class EmitLog {
//交换机名称
public static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws Exception{
//创建信道
Channel channel = RabbitMQUtils.getChannel();
//发送消息
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String message = scanner.next();
channel.basicPublish(EXCHANGE_NAME,"",null,message.getBytes("UTF-8"));
System.out.println("生产者发送的消息:" + message);
}
}
}
Direct直接模式:
消费者:01和02(同理)
/**
* 消费者01:测试Direct模式
*/
public class ReceiveLogsDirect01 {
//定义交换机名称
public static final String EXCHANGE_NAME = "direct_logs";
//定义队列名称
public static final String QUEUE_NAME = "console";
public static void main(String[] args) throws Exception{
//获取信道
Channel channel = RabbitMQUtils.getChannel();
//声明交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//绑定交换机
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"info");
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"warning");
System.out.println("ReceiveLogsDirect01开始准备接收消息...");
//成功!回调函数
DeliverCallback deliverCallback = (consumerTag, message)->{
System.out.println("接收到的消息是:" + new String(message.getBody(),"UTF-8"));
};
CancelCallback cancelCallback = consumerTag->{
System.out.println("接收消息失败" + consumerTag);
};
//接收消息
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
生产者:
/**
* 生产者:测试Direct模式
*/
public class DirectLogs {
//定义交换机名称
public static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMQUtils.getChannel();
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String message = scanner.next();
channel.basicPublish(EXCHANGE_NAME,"error",null,message.getBytes());
System.out.println("生产发送的消息是:" + message);
}
}
}
Topic模式:
消费者:01和02(同理)
/**
* 消费者01:测试topic模式
*/
public class ReceiveLogsTopic01 {
//定义交换机名称
public static final String EXCHANGE_NAME = "topic_logs";
//定义队列名称
public static final String QUEUE_NAME = "Q1";
public static void main(String[] args) throws Exception{
//获取信道
Channel channel = RabbitMQUtils.getChannel();
//声明交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//绑定交换机和队列
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"*.orange.*");
//接收消息
System.out.println("C1开始准备接收消息......" );
//成功!回调函数
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println("接收到的消息是:" + new String(message.getBody(),"UTF-8"));
System.out.println("队列名称:" + QUEUE_NAME + "绑定键:" + message.getEnvelope().getRoutingKey());
};
//失败!回调函数
CancelCallback cancelCallback = consumerTag -> {
System.out.println("消息接收失败!" + consumerTag);
};
channel.basicConsume(QUEUE_NAME, true, deliverCallback,cancelCallback);
}
}
生产者:
/**
* 生产者:测试topic模式
*/
public class EmitLogTopic {
//定义交换机名称
public static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] args) throws Exception{
//获取信道
Channel channel = RabbitMQUtils.getChannel();
//将数据存入到map集合
Map<String,String> bindingKeyMap = new HashMap<>();
bindingKeyMap.put("quick.orange.rabbit","被队列Q1,Q2接收到");
bindingKeyMap.put("lazy.orange.elephant","被队列Q1,Q2接收到");
bindingKeyMap.put("quick.orange.fox","被队列Q1接收到");
bindingKeyMap.put("lazy.pink.rabbit","虽然满足两个绑定但只被队列Q2接收一次");
bindingKeyMap.put("lazy.brown.fox","被队列Q2接收到");
bindingKeyMap.put("quick.brown.fox","不匹配任何绑定不会被任何队列接收到会被丢弃");
bindingKeyMap.put("quick.orange.male.rabbit","是四个单词不匹配任何绑定会被丢弃");
bindingKeyMap.put("lazy.orange.male.rabbit","是四个单词但匹配Q2");
//发送消息
Set<Map.Entry<String, String>> entries = bindingKeyMap.entrySet();
for (Map.Entry<String, String> entry : entries) {
String key = entry.getKey();
String message = entry.getValue();
channel.basicPublish(EXCHANGE_NAME,key,null,message.getBytes("UTF-8"));
System.out.println("生产者发出消息:" + message);
}
}
}