一、概述
1.什么是MQ
MQ(Message Queue)消息队列,是基础数据结构中“先进先出”的一种数据结构。一般用来解决应用解耦,异步消息,流量削峰等问题,实现高性能,高可用,可伸缩和最终一致性架构。
MQ就是把要传输的数据(消息)放在队列中,用队列机制来实现消息传递–生产者产生消息并把消息放入队列,然后由消费者去处理。消费者可以到指定队列拉取消息,或者订阅相应的队列,由MQ服务端给其推送消息。
2.什么是AMQP
AMQP,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。Erlang中的实现有RabbitMQ等。
3.什么是JMS
JMS(Java Messaging Service)是Java平台上有关面向消息中间件(MOM)的技术规范,它便于消息系统中的Java应用程序进行消息交换,并且通过提供标准的产生、发送、接收消息的接口简化企业应用的开发,翻译为Java消息服务。
有两种类型:
- 点对点。在点对点的消息系统中,消息分发给一个单独的使用者。点对点消息往往与队列相关联
- 发布/订阅。发布/订阅消息系统支持一个事件驱动模型,消息生产者和消费者都参与消息传递。生产者发布事件,而使用者订阅感兴趣的事件,并使用事件。该类型消息一般与特定的主题关联
4.AMQP与JMS区别
- JMS是定义了统一接口的,对消息操作进行统一;AMQP通过规定协议统一数据交互的格式
- JMS限定了必须使用java语言;AMQP只是协议,不规定实现方式,因此是跨语言的
- JMS规定了两种消息类型(queue,topic);而AMQP的消息类型更加丰富
5.常见的MQ产品
- ActiveMQ:基于JMS
- RabbitMQ:基于AQMP协议,erlang语言开发,稳定性好
- RocketMQ:基于JMS,阿里巴巴产品
- Kafka:分布式消息系统,高通吐量,处理日志,Scala和Java编写
二、RabbitMQ的优势
- 可靠性(Reliablity):使用了一些机制来保证可靠性,比如持久化、传输确认、发布确认。
- 灵活的路由(Flexible Routing):在消息进入队列之前,通过Exchange来路由消息。对于典型的路由功能,Rabbit已经提供了一些内置的Exchange来实现。针对更复杂的路由功能,可以将多个Exchange绑定在一起,也通过插件机制实现自己的Exchange。
- 消息集群(Clustering):多个RabbitMQ服务器可以组成一个集群,形成一个逻辑Broker。
- 高可用(Highly Avaliable Queues):队列可以在集群中的机器上进行镜像,使得在部分节点出问题的情况下队列仍然可用。
多种协议(Multi-protocol):支持多种消息队列协议,如STOMP、MQTT等。 - 多种语言客户端(Many Clients):几乎支持所有常用语言,比如Java、.NET、Ruby等。
- 管理界面(Management UI):提供了易用的用户界面,使得用户可以监控和管理消息Broker的许多方面。
- 跟踪机制(Tracing):如果消息异常,RabbitMQ提供了消息的跟踪机制,使用者可以找出发生了什么。
- 插件机制(Plugin System):提供了许多插件,来从多方面进行扩展,也可以编辑自己的插件。
三、RabbitMQ后台管理系统
先要启动后台管理插件
rabbitmq-plugins enable rabbitmq_management
设置guest可以远程访问
修改rabbitmq的配置/usr/local/rabbitmq/etc/rabbitmq/rabbitmq.config
添加:[{rabbit, [{loopback_users, []}]}].
访问地址
http://192.168.94.142:15672/
username:guest
password:guest
管理界面标签页介绍
- overview:概览
- connections:无论是生产者还是消费者,都需要与-RabbitMQ建立连接后才可以完成消息的生产和消费,在这里可以查看连接情况。
- channels:通道,建立连接后,会形成通道,消息的投递获取依赖通道。
- Exchanges:交换机,用来实现消息的路由。
- Queues:队列,即消息队列,消息存放在队列上,等待消费,消费后被移除。
- 端口:
- 5672:rabbitmq的编程语言客户端连接端口
- 15672:rabbitmq管理界面端口
- 25672: rabbitmq集群的端口
四、消息模式
1.基本消息
P:生产者,一个发送消息的用户应用程序。
C:消费者,消费和接受有类似的意思,消费者是一个主要用来等待接受消息的用户应用程序。
队列(红色区域):队列只受主机的内存和磁盘限制,实质上是一个大的消息缓冲区。许多生产者可以发送消息到一个队列,许多消费者可以尝试从一个队列接受数据。
总之,生产者将消息发送到队列,消费者从队列中获取信息,队列是存储消息的缓冲区。
2、work消息模型
work消息模型又称为竞争消费者模式,生产者将消息发送到队列之后,消息在消费者之间共享,但是一个消息只能被一个消费者获取。并且一旦消息被消费,就会消失。
3.订阅模型(三种)
1.总体框架解释
1、一个生产者,多个消费者;
2、每一个消费者都有自己的一个队列;
3、生产者没有将消息直接发送到队列,而是发送到交换机;
4、每个队列都要绑定到交换机
5、生产者发送的消息,经过交换机到达队列,实现一个消息被多个消费者获取的目的。
交换机(Exchange):交换机一方面接受生产者发送的消息,另一方面进行消息的处理,比如将消息递交给特定队列、全部队列或者将消息丢弃,到底该如何操作,取决于交换机的类型(Fanout,Direct,Topic)。
Exchange(交换机):只负责转发消息,不具备存储消息的能力。因此如果没有任何队列与交换价绑定,或者没有符合路由规则的队列,那么信息会丢失。
1.Fanout(广播式交换机)
这种模式类似于广播的方式,所有发送到Fanout Exchange交换机上的消息,都会被发送到该交换机上面的所有队列上,这样绑定到这些队列的消费者就可以接收到该消息。
- 这种模式不需要指定Routing key路由键,一个交换机可以绑定多个队列queue,一个queue可同时与多个exchange交换机进行绑定。
- 如果消息发送到交换机上,但是这个交换价上面没有绑定的队列,那么这些消息将会被丢弃。
公共获取连接类
public class MQConnectionUtils {
public static Connection getConnection() throws IOException, TimeoutException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.94.142");
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
return connectionFactory.newConnection();
}
}
生产者
public class Producer {
private static final String EXCHANGE_NAME = "fanout_exchange";
//广播式交换机
private static final String EXCHANGE_TYPE = "fanout";
public static void main(String[] args) {
//获取MQ连接
Connection connection = MQConnecitonUtils.getConnection();
//从连接中获取Channel通道对象
Channel channel = null;
try {
channel = connection.createChannel();
//创建交换机对象
channel.exchangeDeclare(EXCHANGE_NAME, EXCHANGE_TYPE);
//发送消息到交换机exchange上
String msg = "hello fanout exchange!!";
channel.basicPublish(EXCHANGE_NAME, "", null, msg.getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != channel) {
try {
channel.close();
} catch (IOException | TimeoutException e) {
e.printStackTrace();
}
}
if (null != connection) {
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
消费者1
public class Consumer01 {
private static final String QUEUE_NAME = "fanout_exchange_queue01";
private static final String EXCHANGE_NAME = "fanout_exchange";
public static void main(String[] args) {
//获取MQ连接对象
Connection connection = MQConnecitonUtils.getConnection();
try {
//创建消息通道对象
final Channel channel = connection.createChannel();
//创建队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//将队列绑定到交换机上
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
//创建消费者对象
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//消息消费者获取消息
String message = new String(body, StandardCharsets.UTF_8);
System.out.println("【Consumer01】receive message: " + message);
}
};
//监听消息队列
channel.basicConsume(QUEUE_NAME, true, consumer);
} catch (IOException e) {
e.printStackTrace();
}
}
}
消费者2
public class Consumer02 {
private static final String QUEUE_NAME = "fanout_exchange_queue02";
private static final String EXCHANGE_NAME = "fanout_exchange";
public static void main(String[] args) {
//获取MQ连接对象
Connection connection = MQConnecitonUtils.getConnection();
try {
//创建消息通道对象
final Channel channel = connection.createChannel();
//创建队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//将队列绑定到交换机上
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
//创建消费者对象
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//消息消费者获取消息
String message = new String(body, StandardCharsets.UTF_8);
System.out.println("【Consumer02】receive message: " + message);
}
};
//监听消息队列
channel.basicConsume(QUEUE_NAME, true, consumer);
} catch (IOException e) {
e.printStackTrace();
}
}
}
因为消费者1和消费者2分别绑定了队列fanout_exchange_queue01和fanout_exchange_queue02,而且这两个队列都给绑定到了交换机fanout_exchange上面,所有两个消费者都能够接受到此消息。
2.Direct(直连交换机)
- 生产者生产消息的时候需要执行Routing Key路由键;
- 队列绑定交换机的时候需要指定Binding Key,只有路由键与绑定键相同的话,才能将消息发送到绑定这个队列的消费者,如果没有相同的,则该消息会被丢弃。
生产者
public class Producer {
private static final String EXCHANGE_NAME = "direct_exchange";
//交换机类型:direct
private static final String EXCHANGE_TYPE = "direct";
//路由键
private static final String EXCHANGE_ROUTE_KEY = "user.add";
public static void main(String[] args) throws IOException, TimeoutException {
//获取MQ连接
Connection connection = MQConnectionUtils.getConnection();
//从连接中获取Channel通道对象
Channel channel = null;
try {
channel = connection.createChannel();
//创建交换机对象
channel.exchangeDeclare(EXCHANGE_NAME, EXCHANGE_TYPE);
//发送消息到交换机exchange上
String msg = "hello direct exchange!!!";
//指定routing key为info
channel.basicPublish(EXCHANGE_NAME, EXCHANGE_ROUTE_KEY, null, msg.getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != channel) {
try {
channel.close();
} catch (IOException | TimeoutException e) {
e.printStackTrace();
}
}
if (null != connection) {
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
消费者1
public class Consumer01 {
private static final String QUEUE_NAME = "direct_exchange_queue01";
private static final String EXCHANGE_NAME = "direct_exchange";
//binding key
private static final String EXCHANGE_ROUTE_KEY = "user.delete";
public static void main(String[] args) {
//获取MQ连接对象
Connection connection = null;
try {
connection = MQConnectionUtils.getConnection();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
try {
//创建消息通道对象
final Channel channel = connection.createChannel();
//创建队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//将队列绑定到交换机上,并且指定routing_key
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, EXCHANGE_ROUTE_KEY);
channel.basicQos(1);
//创建消费者对象
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//消息消费者获取消息
String message = new String(body, StandardCharsets.UTF_8);
System.out.println("【Consumer01】receive message: " + message);
}
};
//监听消息队列
channel.basicConsume(QUEUE_NAME, true, consumer);
} catch (IOException e) {
e.printStackTrace();
}
}
}
消费者2
public class Consumer02 {
private static final String QUEUE_NAME = "direct_exchange_queue02";
private static final String EXCHANGE_NAME = "direct_exchange";
//binding key
private static final String EXCHANGE_ROUTE_KEY01 = "user.add";
private static final String EXCHANGE_ROUTE_KEY02 = "user.delete";
public static void main(String[] args) {
//获取MQ连接对象
Connection connection = null;
try {
connection = MQConnectionUtils.getConnection();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
try {
//创建消息通道对象
final Channel channel = connection.createChannel();
//创建队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//将队列绑定到交换机上,并且指定routing_key
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, EXCHANGE_ROUTE_KEY01);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, EXCHANGE_ROUTE_KEY02);
//创建消费者对象
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//消息消费者获取消息
String message = new String(body, StandardCharsets.UTF_8);
System.out.println("【Consumer02】receive message: " + message);
}
};
//监听消息队列
channel.basicConsume(QUEUE_NAME, true, consumer);
} catch (IOException e) {
e.printStackTrace();
}
}
}
因为生产者生产者消息指定的路由键为user.add,而消费者1绑定的队列对应的绑定键为user.delete,显然消费者1接受不了;而消费者2指定的绑定键位user.add和user.delete,显然消费者2就能成功消费此消息。
3.Topic(通配符交换机)
- 生产者发送消息的时候需要指定Route Key,同时绑定Exchange与Queue的时候也需要指定Binding Key。
- 如果Exchange没有发现能够与RouteKey模糊匹配的队列Queue,则会抛弃此消息。
“ # ” 表示0个或多个关键字, “ * ” 表示匹配一个关键字
生产者
public class Producer {
private static final String EXCHANGE_NAME = "topic_exchange";
//交换机类型:direct
private static final String EXCHANGE_TYPE = "topic";
//路由键
private static final String EXCHANGE_ROUTE_KEY = "user.add.submit";
public static void main(String[] args) throws IOException, TimeoutException {
//获取MQ连接
Connection connection = MQConnectionUtils.getConnection();
//从连接中获取Channel通道对象
Channel channel = null;
try {
channel = connection.createChannel();
//创建交换机对象
channel.exchangeDeclare(EXCHANGE_NAME, EXCHANGE_TYPE);
//发送消息到交换机exchange上
String msg = "hello topic exchange!!!";
//指定routing key为info
channel.basicPublish(EXCHANGE_NAME, EXCHANGE_ROUTE_KEY, null, msg.getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != channel) {
try {
channel.close();
} catch (IOException | TimeoutException e) {
e.printStackTrace();
}
}
if (null != connection) {
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
消费者1
public class Consumer1 {
private static final String QUEUE_NAME = "direct_exchange_queue01";
private static final String EXCHANGE_NAME = "topic_exchange";
//binding key
private static final String EXCHANGE_ROUTE_KEY = "user.#";
public static void main(String[] args) throws IOException, TimeoutException {
//获取MQ连接对象
Connection connection = MQConnectionUtils.getConnection();
try {
//创建消息通道对象
final Channel channel = connection.createChannel();
//创建队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//将队列绑定到交换机上,并且指定routing_key
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, EXCHANGE_ROUTE_KEY);
channel.basicQos(1);
//创建消费者对象
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//消息消费者获取消息
String message = new String(body, StandardCharsets.UTF_8);
System.out.println("【Consumer01】receive message: " + message);
}
};
//监听消息队列
channel.basicConsume(QUEUE_NAME, true, consumer);
} catch (IOException e) {
e.printStackTrace();
}
}
}
消费者2
public class Consumer2 {
private static final String QUEUE_NAME = "direct_exchange_queue02";
private static final String EXCHANGE_NAME = "topic_exchange";
//binding key
private static final String EXCHANGE_ROUTE_KEY01 = "user.*";
public static void main(String[] args) throws IOException, TimeoutException {
//获取MQ连接对象
Connection connection = MQConnectionUtils.getConnection();
try {
//创建消息通道对象
final Channel channel = connection.createChannel();
//创建队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//将队列绑定到交换机上,并且指定routing_key
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, EXCHANGE_ROUTE_KEY01);
//创建消费者对象
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//消息消费者获取消息
String message = new String(body, StandardCharsets.UTF_8);
System.out.println("【Consumer02】receive message: " + message);
}
};
//监听消息队列
channel.basicConsume(QUEUE_NAME, true, consumer);
} catch (IOException e) {
e.printStackTrace();
}
}
}
因为生产者发送消息的时候指定了Routing Key为user.add.submit,而消费者1所在的队列Binding Key为user.#,#能够匹配一个或多个,所以消费者1能够接受到这个消息;但是消费者2指定的Binding Key为user.* ,而“*”只能匹配一个,所有并不能够匹配到user.add.submit这个路由键,所以消费者2不能消费次消息。
五、RabbitMQ保证消息不丢失
RabbitMQ一般情况下消息不会发生丢失的问题,但是不能排除意外,为了保证我们自己的系统高可用,我们必须做出更好完善的措施,保证系统的稳定性。
1.消息持久化
RabbitMQ的消息默认放在内存上面,如果不特别声明设置,消息不会持久化到磁盘上,如果节点重启或者宕机,就会导致消息丢失。
- Exchange持久化:channel.exchangeDeclare(exchangeName, “direct/topic/header/fanout”, true);即在声明的时候讲durable字段设置为true即可。
- Quque持久化:queue的持久化是通过durable=true来实现的。
- Message消息持久化:发送消息设置发送模式deliveryMode=2,代表持久化消息。
2.ACK确认机制(异步)
多个消费者者同时受到消息,比如消息收到一般的时候,一个消费者死掉了(逻辑复杂时间太长,超时或消费被停机或网络断开连接),如何保证消息不丢失
使用Message acknowledgment机制,消费者消费完成要通知服务端,服务端才把消息从内存删除。
举例:
①生产者发生消息丢失:
//设置消息发送确认--回调 开关
connectionFactory.setPublisherConfims(true);
//同时启动一个监听(生产者监听确认):监听生产者是否消息发送成功。
template.setConfirmCallback(confirmCallback());
②消费者信息丢失
消费者获取到消息之后,没有来得及处理完毕,自己直接宕机了,因为消息这默认采用自动ack,此时RabbitMQ的自动ack机制会通知MQ Server这条消息已经处理好了,此时消息就丢了,并不是预期的。
采用手动ACK机制来解决这个问题,消费端处理完逻辑之后在通知MQ Server,这样消费者没处理完消息不会发送ack,如果在消费者拿到消息,没来得及处理的情况下自己挂了,此时MQ集群会自动感知到,它会自觉的重发消息给其他的消费者服务实例。
# 需要两步操作
1、消费者监听设置手动ack
this.channel.basicConsume(queue, false, consumerTag, this);
2、业务完成后手动ack
context.getChannel().basicAck(deliveryTag, false);
3.设置集群镜像模式
RabbitMQ三种部署模式:
- 单节点模式:最简单的情况,节点挂了,消息就不能用了
- 普通模式:默认的集群模式,某个节点挂了,该节点上的消息不能用,有影响的业务瘫痪,只能等待节点恢复重启可用(消息持久化的)
- 镜像模式:把需要的队列做成镜像队列,存在于多个节点,输入RabbitMQ的HA方案
三种HA策略模式:
- 同步所有的
- 同步最多的N个机器
- 只同步符合制定名称的nodes
4.消息补偿机制
持久化消息,保存到硬盘中,当前队列节点挂了,存储节点磁盘又坏了,消息丢失了,怎么办?
产线网络环境太复杂,所以不知数太多,消息补偿机制需要建立在消息要写入DB日志,发送日志,节后日志,两者的状态必须记录
然后根据DB日志记录check消息发送消费是否成功,不成功,进行消息补偿措施,重新发送消息处理
5.路由保障(解决交换机消息丢失问题)
路由保障的出现就是为了解决路由信息不匹配的情况 ,当消息没有成功通过路由键路由到队列中时,那么消息就会发生丢失的情况,主要有两种方式失败通知 和备用交换机
失败通知
这时候就需要有一个失败通知(mandatory+ReturnListener)。
//路由失败通知
template.setMandatory(true);
//回调
template.setReturnListener(returnCallback());
备用交换机
在路由的时候,有可能是路由键路由的范围不够广。
所以在创建交换机的时候可以再创建一个其他路由键的交换机(比如Fanout广播式交换机),以此来确保消息可以路由到另一个队列中,从而保证消息不会丢失
6.生产者生产消息到RabbitMQ Server可靠性保证?
- AMQP协议提供的一个事务机制(用的比较少)
- 发送方确认机制
首先生产者通过调用channel.confirmSelect方法将信道设置为confirm模式,一旦
信道进入confirm模式,所有在该信道上面发布的小都会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,RabbitMQ就会发送一个确认给生产者(包含消息的唯一deliveryTag和multiple参数),这就使得生产者知晓消息已经正确到达了目的地了。
其中Confirm模式有三种方式实现
-
串行confirm模式:生产者每发送一条消息后,调用waitForConfirms()方法,等待broker端confirm,如果服务器端返回false或者在超时时间内未返回,客户端进行消息重传。
-
批量confirm模式:生产者每发送一批消息后,调用waitForConfirms()方法,等待broker端confirm。
-
异步confirm模式:提供一个回调方法,broker confirm了一条或者多条消息后producer端会回调这个方法。