一、MQ 的相关概念
1.1、什么是 MQ
MQ(message queue),从字面意思上看,本质是个队列,FIFO 先入先出,只不过队列中存放的内容是message 而已,还是一种跨进程的通信机制,用于上下游传递消息。在互联网架构中,MQ 是一种非常常见的上下游“逻辑解耦+物理解耦”的消息通信服务。使用了 MQ 之后,消息发送上游只需要依赖 MQ,不用依赖其他服务。
1.2、为什么要用 MQ
1.流量消峰
举个例子,如果订单系统最多能处理一万次订单,这个处理能力应付正常时段的下单时绰绰有余,正常时段我们下单一秒后就能返回结果。但是在高峰期,如果有两万次下单操作系统是处理不了的,只能限制订单超过一万后不允许用户下单。使用消息队列做缓冲,我们可以取消这个限制,把一秒内下的订单分散成一段时间来处理,这时有些用户可能在下单十几秒后才能收到下单成功的操作,但是比不能下单的体验要好。
2.应用解耦
以电商应用为例,应用中有订单系统、库存系统、物流系统、支付系统。用户创建订单后,如果耦合调用库存系统、物流系统、支付系统,任何一个子系统出了故障,都会造成下单操作异常。当转变成基于消息队列的方式后,系统间调用的问题会减少很多,比如物流系统因为发生故障,需要几分钟来修复。在这几分钟的时间里,物流系统要处理的内存被缓存在消息队列中,用户的下单操作可以正常完成。当物流系统恢复后,继续处理订单信息即可,中单用户感受不到物流系统的故障,提升系统的可用性
3.异步处理
有些服务间调用是异步的,例如 A 调用 B,B 需要花费很长时间执行,但是 A 需要知道 B 什么时候可以执行完,以前一般有两种方式,A 过一段时间去调用 B 的查询 api 查询。或者 A 提供一个 callback api,B 执行完之后调用 api 通知 A 服务。这两种方式都不是很优雅,使用消息总线,可以很方便解决这个问题,A 调用 B 服务后,只需要监听 B 处理完成的消息,当 B 处理完成后,会发送一条消息给 MQ,MQ 会将此消息转发给 A 服务。这样 A 服务既不用循环调用 B 的查询 api,也不用提供 callback api。同样 B 服务也不用做这些操作。A 服务还能及时的得到异步处理成功的消息。
常见MQ产品
ActiveMQ:基于JMS RabbitMQ:基于AMQP协议,erlang语言开发,稳定性好
RocketMQ:基于JMS,阿里巴巴产品,目前交由Apache基金会
Kafka:分布式消息系统,高吞吐量
RabbitMQ快速入门
RabbitMQ是由erlang语言开发,基于AMQP(Advanced Message Queue 高级消息队列协议)协议实现的消息队列,它是一种应用程序之间的通信方法,消息队列在分布式系统开发中应用非常广泛。RabbitMQ官方地址:http://www.rabbitmq.com
下载与安装
RabbitMQ由Erlang语言开发,需要安装与RabbitMQ版本对应的Erlang语言环境,具体的就不解释了,自行搜索教程。RabbitMQ官网下载地址:http://www.rabbitmq.com/download.html
1.3、RabbitMQ的工作原理
下图是RabbitMQ的基本结构:
1、Message
消息,消息是不具体的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些
属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。
2、Publisher
消息的生产者,也是一个向交换器发布消息的客户端应用程序。
3、Exchange
交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。.
4 Binding
绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以
可以将交换器理解成一个由绑定构成的路由表。
5、Queue
消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。
消息一直在队列里面,等待消费者连接到这个队列将其取走。
如果有多个消费者同时监听同一个队列,会使用轮询的方式把队列中的消息消费掉,如果中间有人退出,则剩下的消费者把队列中的消息消费掉
创建连接工厂
public static ConnectionFactory getFactory(){
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("root");
//tcp连接端口
factory.setPort(5672);
factory.setPassword("root");
factory.setHost("192.168.204.129");
return factory;
}
-
创建连接(不使用交换机直接发送)
生产者:
private static void getConnectionFactory(){ ConnectionFactory factory = FactoryUtil.getFactory(); try { // 创建连接 Connection conn = factory.newConnection(); // 获得信道 Channel channel = conn.createChannel(); // 声明队列 channel.queueDeclare("myQueue",true,false,false,null); String message = "hello,rabbitmq....."+new Date(); // 发送消息到指定队列(交换机,路由键(队列名),属性,消息内容字节流) channel.basicPublish("","myQueue",null,message.getBytes()); System.out.println("消息已经发送"+new Date()); channel.close(); conn.close(); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } }
消费者
public static void getReciver(){ // 连接工厂对象 ConnectionFactory factory = FactoryUtil.getFactory(); //创建连接 try { Connection conn = factory.newConnection(); // 通道 Channel channel = conn.createChannel(); // 声明队列 channel.queueDeclare("myQueue",true,false,false,null); // 消费消息--队列名,自动确认,消费者 channel.basicConsume("myQueue", true, new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("接受到消息:"+new String(body,"utf-8")); } }); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } }
Exchange-使用交换机创建连接
也可以使用该常量声明发送模式——BuiltinExchangeType
-
Direct模式
- **处理路由键。**需要将一个队列绑定到交换机上,**要求该消息与一个特定的路由键完全匹配。**这是一个完整的匹配。如果一个队列绑定到该交换机上要求路由键 “dog”,则只有被标记为“dog”的消息才被转发,不会转发dog.puppy,也不会转发dog.guard,只会转发dog。
生产者:
/** *@Description//TODO Direct模型 *@Date2022/8/8 16:29 **/ private static void getSender(){ ConnectionFactory factory = FactoryUtil.getFactory(); try { Connection conn = factory.newConnection(); Channel channel = conn.createChannel(); //声明交换机--交换机名称,类型,持久化 channel.exchangeDeclare("directExchange","direct",true); // 发送消息 String message = "hello...direct exchagne..."+new Date(); // 交换机,指定的路由键,属性,消息内容字节流 channel.basicPublish("directExchange",**"green"**,null,message.getBytes()); System.out.println("消息发送成功...."); channel.close(); conn.close(); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } }
消费者:
public static void getReciver(){ // 连接工厂对象 ConnectionFactory factory = FactoryUtil.getFactory(); //创建连接 try { Connection conn = factory.newConnection(); // 通道 Channel channel = conn.createChannel(); // 声明队列 channel.queueDeclare("direqueue1",true,false,false,null); // 声明交换机(交换机名,交换类型(需要全小写),是否声明为持久层) channel.exchangeDeclare("directExchange",***"direct"***,true); // 绑定交接机(队列,交换机,用于绑定的路由键(子会接收路由键相同的消息)) channel.queueBind("direqueue1","directExchange",**"green"**); // 消费消息--队列名,自动确认,消费者 channel.basicConsume("direqueue1", true, new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("DirectReciver1接收到消息:"+new String(body,"utf-8")); } }); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } }
-
广播模式-fanout
-
不处理路由键。你只需要简单的将队列绑定到交换机上。一个发送到交换机的消息都会被转发到与该交换机绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。Fanout交换机转发消息是最快的
生产者:
/** *@Description//TODO Fanout模型-会发送给交换机中的所以队列 *@Date2022/8/8 16:29 **/ private static void getSender(){ ConnectionFactory factory = FactoryUtil.getFactory(); try { Connection conn = factory.newConnection(); Channel channel = conn.createChannel(); //声明交换机--交换机名称,类型,持久化 channel.exchangeDeclare("fanoutExchange","fanout",true); // 发送消息 String message = "hello...fanout exchagne..."+new Date(); // 交换机,不用指定路由键,属性,消息内容字节流 channel.basicPublish("fanoutExchange","",null,message.getBytes()); System.out.println("fanoutExchange消息发送成功...."); channel.close(); conn.close(); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } }
消费者:
public static void getReciver(){ // 连接工厂对象 ConnectionFactory factory = FactoryUtil.getFactory(); //创建连接 try { Connection conn = factory.newConnection(); // 通道 Channel channel = conn.createChannel(); // 声明队列 channel.queueDeclare("fanout1",true,false,false,null); // 声明交换机 channel.exchangeDeclare("fanoutExchange","fanout",true); // 绑定交接机(队列,交换机,用于绑定的路由键) channel.queueBind("fanout1","fanoutExchange","green"); // 消费消息--队列名,自动确认,消费者 channel.basicConsume("fanout1", true, new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("FanoutReciver1接收到消息:"+new String(body,"utf-8")); } }); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } }
-
-
topic模式
-
生产者把数据发送到交换机中,设置特定的路由键;消费者根据队列的通配符接收,满足条件的队列中的消息
- :代表1个单词
#:代表0到多个
生产者:
/** *@Description//TODO Topic模型- *@Date2022/8/8 16:29 **/ private static void getSender(){ ConnectionFactory factory = FactoryUtil.getFactory(); try { Connection conn = factory.newConnection(); Channel channel = conn.createChannel(); //声明交换机--交换机名称,类型,持久化 channel.exchangeDeclare("topicExchange", BuiltinExchangeType.TOPIC,true); // 发送消息 String message = "hello...fanout exchagne..."+new Date(); // 交换机,不用指定路由键,属性,消息内容字节流 channel.basicPublish("topicExchange","aa.bb.zez",null,message.getBytes()); System.out.println("fanoutExchange消息发送成功...."); channel.close(); conn.close(); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } }
消费者:
public static void getReciver(){ // 连接工厂对象 ConnectionFactory factory = FactoryUtil.getFactory(); //创建连接 try { Connection conn = factory.newConnection(); // 通道 Channel channel = conn.createChannel(); // 声明队列 channel.queueDeclare("topic3",true,false,false,null); // 声明交换机 channel.exchangeDeclare("topicExchange", BuiltinExchangeType.TOPIC,true); // 绑定交接机(队列,交换机,用于绑定的路由键) channel.queueBind("topic3","topicExchange","*.bb.#"); // 消费消息--队列名,自动确认,消费者 channel.basicConsume("topic3", true, new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("topicExchange3接收到消息:"+new String(body,"utf-8")); } }); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); }
-
事务:
生产者发送消息时,可以使用事务来保证发送的原子性
- 开启事务:
channel.txSelect();
- 提交事务:
channel.txCommit();
启用监听确认:
同步模式:
- 开启监听确认模式:
channel.confirmSelect();
- 执行监听确认:
channel.waitForConfirmsOrDie();
异步模式:
该模式会通过异步的方式去执行监听,判断哪些成功,哪些失败,失败的会去重新执行发送
/**
*@Description//TODO currentTimeMillis启用发布者确认使用异步执行,该
*@Date2022/8/8 16:29
**/
private static void getSender(){
ConnectionFactory factory = FactoryUtil.getFactory();
try {
Connection conn = factory.newConnection();
Channel channel = conn.createChannel();
//声明交换机--交换机名称,类型,持久化
channel.exchangeDeclare("transExchange", BuiltinExchangeType.DIRECT,true);
// 启用发布者确认模式
channel.confirmSelect();
long l = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
// 发送消息
String message1 = "hello...direct exchagne1..."+i;
// 交换机,指定的路由键,属性,消息内容字节流
channel.basicPublish("transExchange","trans",null,message1.getBytes());
}
// 执行监听确认
channel.addConfirmListener(new ConfirmListener() {
// 确认消息
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
System.out.println("确认消息****:"+deliveryTag+",状态:"+multiple);
}
// 未确认
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.err.println("未消息####:"+deliveryTag+",状态:"+multiple);
}
});
long j = System.currentTimeMillis();
System.out.println("消息使用时间---"+(j-l));
// channel.close();
// conn.close();
} catch (Exception e) {
e.printStackTrace();
}
}