目录
1、MQ引言
1.1 什么是MQ
1.2 MQ有哪些
1.3不同MQ的特点
2、RabbitMQ的引言
2.1 RabbitMQ
2.2 RabbitMQ的访问地址
3、RabbitMQ的第一个程序
3.1 添加用户、虚拟主机并绑定
3.2添加新用户
3.3将用户绑定对应的虚拟机
3.4完成绑定
3.5 引入依赖
4、第一种模型:直连
4.1 封装简单工具类
4.2 生产者Provider
4.3 消费者Consumer
5、第二种模型:work queues
5.1 生产者Provider
5.2 消费者Consumer1
5.3 消费者Consumer2
5.4 消息自动确认机制
5.5 改造消费者1、消费者2
5.6 结论
6 第三种模型:Fanout
6.1 生产者Provider
6.2 消费者Consumer
6.3 临时队列
7、第四种模式:routing
7.1 生产者Provider
7.2 消费者Consumer
7.3 结论
8、第五种模型:Topic
8.1 生产者Provider
8.2消费者Consumer
8.3 结论
MQ(Message Queue):翻译为消息队列,通过典型的生产者和消费者模型,生产者不断从消息队列中产生消息,消费者不断从消息队列中获取消息。因为消息的生产和消费都是异步的,而且只关心消息的发送和接收,没有业务逻辑的侵入,轻松的实现业务间的解耦。别名消息中间件通过利用高效可靠的消息传递机制进行平台无关的数据交流,并基于数据通信来进行分布式系统的集成。
当今市面上有很多主流的消息中间件,如老牌的ActiveMQ、RabbitMQ、炙手可热的Kafka、阿里巴巴自主开发RocketMQ等。
ActiveMQ | Apache出品,最流行的,能力强劲的开源消息总线。他是一个完全支持JMS规范的消息中间件。丰富的API。多种集群架构模式让ActiveMQ在业界称为老牌的消息中间件,在中小企业颇受欢迎。 |
Kafka | Kafka是LinkedIn开源的分布式发布-订阅消息系统,目前归属于Apache顶级项目。Kafka主要特点是基于Pull的模式来处理消息消费,追求高吞吐量,一开始的目的就是用于日志收集和传输。0.8版本开始支持复制,不支持事务,对消息的重复,丢失、错误没有严格要求,适合产生大量数据的互联网服务的数据收集业务。 |
RocketMQ | RocketMQ是阿里开源的消息中间件,它是纯Java开发,具有高吞吐量、高可用性、适合大规模分布式系统应用的特点。RocketMQ思路起源于Kafka,但并不是Kafka的一个Copy,它对消息的可靠传输及事务性做了优化。目前在阿里集团被广泛应用于交易、充值、流计算、消息推送、日志流式处理、binglog分发等场景。 |
RabbitMQ | RabbitMQ是使用Erlang语言开发的开源消息队列系统,基于AMQP协议来实现。AMQP的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。AMQP协议更多用在企业系统内对数据一致性、稳定性和可靠性要求很高的场景、对性能和吞吐量的要求还在其次。 |
RabbitMQ 比 Kafka 可靠,Kafka 更适合 IO 高吞吐量的处理,一般应用在大数据日志处理或对实时性(少量延迟),可靠性(少量丢数据)要求稍低的场景使用,比如ELK日志收集。
基于AMQP协议,erlang语言开发,是部署最广泛的开源消息中间件,是最受欢迎的开源消息中间件之一。
AMQP:即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。Erlang中的实现有RabbitMQ等。
在安装(Linux不好装的话可以装在Windows里)并启动好RabbitMQ之后访问localhost:15672(账号和密码都是guest)
注意:必须 / 开头
com.rabbitmq
amqp-client
5.7.2
junit
junit
4.13.2
package rabbitMq.utils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class RabbitMQUtils {
private static ConnectionFactory connectionFactory;
// 类加载时只执行一次
static {
connectionFactory = new ConnectionFactory();
}
// 定义提供连接对象的方法
public static Connection getConnection(){
// 连接mq主机
connectionFactory.setHost("localhost");
// 设置端口号
connectionFactory.setPort(5672);
// 设置连接哪个虚拟主机
connectionFactory.setVirtualHost("/ems");
// 设置访问虚拟主机的用户名和密码
connectionFactory.setUsername("ems");
connectionFactory.setPassword("123");
// 通道绑定对应的消息队列
try {
return connectionFactory.newConnection();
} catch (IOException | TimeoutException e) {
e.printStackTrace();
}
return null;
}
// 关闭通道和关闭连的接方法
public static void closeChanelAndConnection(Channel channel, Connection connection){
try {
if(channel != null){
channel.close();
}
if(connection != null){
connection.close();
}
} catch (IOException | TimeoutException e) {
e.printStackTrace();
}
}
}
package rabbitMq;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import org.junit.Test;
import rabbitMq.utils.RabbitMQUtils;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Provider {
@Test
public void testSendMessage() throws IOException, TimeoutException {
// // 获取连接
Connection connection = RabbitMQUtils.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 通道绑定对应的消息队列
// 参数1:队列名称,如果队列不存在会自动创建
// 参数2:用来定义队列特性是否要持久化
// 参数3:是否独占队列,表示只有当前连接可用该队列
// 参数4:是否在消费完成后删除队列
// 参数5:额外附加参数
channel.queueDeclare("hello", false, false, false, null );
// 发布消息
// 参数1:交换机名称
// 参数2:队列名称
// 参数3:传递消息额外设置
// 参数4:消息的具体内容
channel.basicPublish("","hello", null, "hello rabbitmq".getBytes());
// // 关闭
RabbitMQUtils.closeChanelAndConnection(channel,RabbitMQUtils.getConnection());
}
}
运行生产者:
查看消息详情
package rabbitMq;
import com.rabbitmq.client.*;
import rabbitMq.utils.RabbitMQUtils;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer {
public static void main(String[] args) throws IOException, TimeoutException {
// // 获取连接
Connection connection = RabbitMQUtils.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 通道绑定对应的消息队列
// 参数1:队列名称,如果队列不存在会自动创建
// 参数2:用来定义队列特性是否要持久化
// 参数3:是否独占队列,表示只有当前连接可用该队列
// 参数4:是否在消费完成后删除队列
// 参数5:额外附加参数
channel.queueDeclare("hello", false, false, false, null);
// 消费消息
// 参数1:消费哪个队列的消息
// 参数2:开启消息的自动确认机制
// 参数3:消费时回调的接口
channel.basicConsume("hello", true, new DefaultConsumer(channel){
// 参数:body消息队列里取出的消息
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
super.handleDelivery(consumerTag, envelope, properties, body);
System.out.println("输出队列里的内容--->>>" + new String(body));
}
});
// 如果没关闭 消费者会一直等待
// 如果关闭 不会等待结果出来
// RabbitMQUtils.closeChanelAndConnection(channel,RabbitMQUtils.getConnection());
}
}
运行消费者
成功消费刚刚生产的消息消息
回到地址查看
Provider生产的消息确实被成功消费
Work queues
,也被称为(Task queues
),任务模型。当消息处理比较耗时的时候,可能生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多,无法及时处理。此时就可以使用work模型:让多个消费者绑定到一个队列,共同消费队列中的消息。队列中的消息一旦消费,就会消失,因此任务是不会被重复执行的
package rabbitmq.workqueue;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import rabbitmq.utils.RabbitMQUtils;
import java.io.IOException;
public class Provider {
public static void main(String[] args) throws IOException {
// 获取连接
Connection connection = RabbitMQUtils.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 通过通道声明队列
// 参数1:队列名称,如果队列不存在会自动创建
// 参数2:用来定义队列特性是否要持久化
// 参数3:是否独占队列,表示只有当前连接可用该队列
// 参数4:是否在消费完成后删除队列
// 参数5:额外附加参数
channel.queueDeclare("work", true, false, false, null);
for (int i = 1; i <= 20; i++) {
// 生产消息
String message = "hello work queue-" + i;
// 参数1:交换机名称
// 参数2:队列名称
// 参数3:传递消息额外设置
// 参数4:消息的具体内容
channel.basicPublish("", "work", null, message.getBytes());
}
// 关闭
RabbitMQUtils.closeChanelAndConnection(channel, connection);
}
}
package rabbitmq.workqueue;
import com.rabbitmq.client.*;
import rabbitmq.utils.RabbitMQUtils;
import java.io.IOException;
public class Consumer1 {
public static void main(String[] args) throws IOException {
// 获取连接
Connection connection = RabbitMQUtils.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 通过通道声明队列
// 参数1:队列名称,如果队列不存在会自动创建
// 参数2:用来定义队列特性是否要持久化
// 参数3:是否独占队列,表示只有当前连接可用该队列
// 参数4:是否在消费完成后删除队列
// 参数5:额外附加参数
channel.queueDeclare("work", true, false, false, null);
// 消费消息
channel.basicConsume("work", true, new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者1---" + new String(body));
}
});
}
}
package rabbitmq.workqueue;
import com.rabbitmq.client.*;
import rabbitmq.utils.RabbitMQUtils;
import java.io.IOException;
public class Consumer2 {
public static void main(String[] args) throws IOException {
// 获取连接
Connection connection = RabbitMQUtils.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 通过通道声明队列
channel.queueDeclare("work", true, false, false, null);
// 消费消息
channel.basicConsume("work", true, new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费者2---" + new String(body));
}
});
}
}
运行两个消费者Consumer1、Consumer2,再运行消息的生产者Provider
消费者1瞬间完成任务,消费者2要隔两秒完成一个任务,但任务是平均分配的,不满足我们日常应用,应该能者多劳。
完成一项任务可能需要几秒钟。 您可能想知道如果 一位消费者开始了一项漫长的任务,而死于其中的一部分。使用我们当前的代码,RabbitMQ一旦向消费者传递了一条消息 立即将其标记为删除。 在这种情况下,如果您杀死工人 我们将丢失正在处理的消息。我们也会失去所有 发送给该特定工作人员但未发送的消息 尚未处理。 但是我们不想丢失任何任务。 如果一个工人死了,我们希望 任务要交付给另一名工人。 为了确保消息永不丢失,RabbitMQ支持 消息确认 。 确认由主机发回。消费者告诉RabbitMQ已经收到了特定的消息,处理后,RabbitMQ可以自由删除它。
// 每一次只能消费一个消息
channel.basicQos(1);
channel.basicConsume("work", false, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者1 --- " + new String(body));
// 手动确认 参数1:手动确认消息标识 参数2:false每次确认一个
channel.basicAck(envelope.getDeliveryTag(), false);
}
});
如果我们将其中一个手动确认注释掉,观察MQ管理器会发生什么情况呢?
// channel.basicAck(envelope.getDeliveryTag(), false);
所以通常我们都会在消费者完成消费后,手动确认消息已消费。
默认情况下,RabbitMQ会将每条消息按顺序发送给下一个使用者。 平均每个消费者将获得相同数量的消息。 这种分发消息的方式称为循环。
如果我们想要做到能者多劳,就要改变消息自动确认机制,改为手动确认消息。
Fanout 也称为广播
RabbitMQ消息传递模型中的核心思想是生产者 从不直接将任何消息发送到队列。
在广播模式下,消息发送流程是这样的:
- 可以有多个消费者
- 每个消费者有自己的queue(队列)
- 每个队列都要绑定到Exchange(交换机)
- 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定
- 交换机把消息发送给绑定过的所有队列
- 队列的消费者都能拿到消息。实现一条消息被多个消费者消费
package rabbitmq.fanout;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import rabbitmq.utils.RabbitMQUtils;
import java.io.IOException;
public class Provider {
public static void main(String[] args) throws IOException {
// 获取连接对象
Connection connection = RabbitMQUtils.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 将通道声明指定的交换机
// 参数1:交换机名称
// 参数2:交换机类型 fanout标识广播
channel.exchangeDeclare("logs", "fanout");
// 发送消息
// 参数1:交换机名称
String message = "fanout type message";
channel.basicPublish("logs", "", null, message.getBytes());
// 关闭
RabbitMQUtils.closeChanelAndConnection(channel, connection);
}
}
注意:交换机类型有几种可用: direct , topic , headers 和 fanout 。 我们将演示最后一个-fanout。
fanout交换非常简单。 您可能会从中猜到名称,它只是将收到的所有消息广播给所有知道的队列。
假设有三个相同的消费者1、2、3
package rabbitmq.fanout;
import com.rabbitmq.client.*;
import rabbitmq.utils.RabbitMQUtils;
import java.io.IOException;
public class Consumer1 {
public static void main(String[] args) throws IOException {
// 获取连接
Connection connection = RabbitMQUtils.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 通道绑定交换机
channel.exchangeDeclare("logs", "fanout");
// 临时队列
String queueName = channel.queueDeclare().getQueue();
// 绑定交换机和队列
// 参数1:临时队列名
// 参数2:交换机名称
channel.queueBind(queueName, "logs", "");
// 消费消息
channel.basicConsume(queueName, true, new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者1---" + new String(body));
System.out.println(queueName);
}
});
}
}
首先,无论何时我们连接到Rabbit,我们都需要一个全新的空队列。 为此,我们可以使用随机名称创建一个队列,或者甚至更好-让服务器为我们选择一个随机队列名称。
其次,一旦我们断开与消费者的联系,队列应该是自动删除。
在Java客户端中,当我们不向提供任何参数时 queueDeclare() 我们使用生成的名称创建一个非持久的,排他的,自动删除队列:
String queueName = channel.queueDeclare().getQueue();
此时, queueName 包含一个随机队列名称。
Routing之订阅模型-Direct(直连)
在Fanout(广播)模式中,一条消息,会被所有订阅的队列都消费。但是,在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。
在Direct模型下:
- 队列与交换机的绑定,不能是任意绑定了,而是要指定一个
RoutingKey
(路由key)- 消息的发送方在向Exchange发送消息时,也必须指定消息的
RoutingKey
- Exchange不再把消息交给每一个绑定的队列,而是根据消息的
RoutingKey
进行判断,只有队列的RoutingKey
与消息的RoutingKey
完全一致,才会接收到消息
生产者需要通过通道声明交换机并指定交换机类型,通过routingKey
决定消息发送到哪个消息队列
package rabbitmq.direct;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import rabbitmq.utils.RabbitMQUtils;
import java.io.IOException;
public class Provider {
public static void main(String[] args) throws IOException {
// 获取连接
Connection connection = RabbitMQUtils.getConnection();
// 获取通道
Channel channel = connection.createChannel();
String exchangeName = "logs_direct";
// 通过通道声明交换机
// 参数1:交换机名称
// 参数2:交换机类型 direct表示路由模式
channel.exchangeDeclare(exchangeName, "direct");
// 发送消息
String routingKey = "waring";
String message = "这是direct模型发布的基于routerKey: [" + routingKey + "] 发送的消息";
channel.basicPublish(exchangeName, routingKey, null, message.getBytes());
// 关闭
RabbitMQUtils.closeChanelAndConnection(channel, connection);
}
}
消费者1:基于routerKey绑定队列和交换机,只接收error消息。
package rabbitmq.direct;
import com.rabbitmq.client.*;
import rabbitmq.utils.RabbitMQUtils;
import java.io.IOException;
public class Consumer1 {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
String exchangeName = "logs_direct";
// 通道声明交换机以及交换机的类型
channel.exchangeDeclare(exchangeName, "direct");
// 创建一个临时队列
String queueName = channel.queueDeclare().getQueue();
// 基于routerKey绑定队列和交换机
channel.queueBind(queueName, exchangeName, "error");
// 获取消息
channel.basicConsume(queueName, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者1 >>>>> " + new String(body));
}
});
}
}
消费者2:基于routerKey绑定队列和交换机,接收info、error、waring消息。
package rabbitmq.direct;
import com.rabbitmq.client.*;
import rabbitmq.utils.RabbitMQUtils;
import java.io.IOException;
public class Consumer2 {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
String exchangeName = "logs_direct";
// 通道声明交换机以及交换机的类型
channel.exchangeDeclare(exchangeName, "direct");
// 创建一个临时队列
String queueName = channel.queueDeclare().getQueue();
// 基于routerKey绑定队列和交换机
channel.queueBind(queueName, exchangeName, "info");
channel.queueBind(queueName, exchangeName, "error");
channel.queueBind(queueName, exchangeName, "warning");
// 获取消息
channel.basicConsume(queueName, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者2 >>>>> " + new String(body));
}
});
}
}
生产者发送三次消息,分别是info、error和warning消息
可以看到,消费者1只关心error消息,不关心其他消息,而消费者2关心info、error和warning消息。假设发送的是debug,则两个消费者都不会关心。
Routing之订阅模型-Topic
Topic类型的Exchange与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定RoutingKey的时候使用通配符。
这种类型RoutingKey一般都是由一个或多个单词组成,多个单词之间以“.”分隔,例如:
item.insert
。
# 通配符:
1. * (star) can substitute for exactly one word. 匹配一个词
2. # (hash) can substitute for zero or more words. 匹配一个或多个词
# 例如:
audit.* 只能匹配audit.irs
audit.# 可以匹配audit.irs.corporate 或者 audit.irs 等
生产者发送消息时,routerKey可以指定规则。
package rabbitmq.topic;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import rabbitmq.utils.RabbitMQUtils;
import java.io.IOException;
public class Provider {
public static void main(String[] args) throws IOException {
// 获取连接
Connection connection = RabbitMQUtils.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明交换机以及交换机类型
channel.exchangeDeclare("topics", "topic");
// 发布消息
String routerKey = "user.save.findAll";
//String routerKey = "user.save.findAll";
String message = "这里是topic动态路由模型,routerKey: [" + routerKey + "] 发送的消息";
channel.basicPublish("topics", routerKey, null, message.getBytes());
// 关闭
RabbitMQUtils.closeChanelAndConnection(channel, connection);
}
}
消费者1:使用“*”匹配,只能匹配一个词
package rabbitmq.topic;
import com.rabbitmq.client.*;
import rabbitmq.utils.RabbitMQUtils;
import java.io.IOException;
public class Consumer1 {
public static void main(String[] args) throws IOException {
// 获取连接
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
// 声明交换机以及交换机类型
channel.exchangeDeclare("topics", "topic");
// 创建一个临时队列
String queueName = channel.queueDeclare().getQueue();
// 绑定队列和交换机,基于动态通配符形式
// * 只能匹配一个词
channel.queueBind(queueName, "topics", "user.*");
// 消费消息
channel.basicConsume(queueName, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者1 >>>> " + new String(body));
}
});
}
}
消费者2:使用“#”匹配,可以匹配一个或多个词
package rabbitmq.topic;
import com.rabbitmq.client.*;
import rabbitmq.utils.RabbitMQUtils;
import java.io.IOException;
public class Consumer2 {
public static void main(String[] args) throws IOException {
// 获取连接
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
// 声明交换机以及交换机类型
channel.exchangeDeclare("topics", "topic");
// 创建一个临时队列
String queueName = channel.queueDeclare().getQueue();
// 绑定队列和交换机,基于动态通配符形式
// # 匹配一个或多个词
channel.queueBind(queueName, "topics", "user.#");
// 消费消息
channel.basicConsume(queueName, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者2 >>>> " + new String(body));
}
});
}
}
开启Consumer1、Consumer2并使用Provider发送user.save以及user.save.findAll
可以看到,这相当于是第四种模型的增强版,通过不同的匹配规则,消费者1和消费者2接收的消息是不一样的。