什么是MQ:
消息队列(Message Queue,简称MQ),从字面意思上看,本质是个队列,FIFO先入先出,只不过队列中存放的内容是message而已。
其主要用途:不同进程Process/线程Thread之间通信。
MQ框架非常之多,比较流行的有RabbitMq、kafka、阿里开源的RocketMQ。本文的主角:RabbitMq
。
特性 | RabbitMQ | RocketMQ | kafka |
---|---|---|---|
开发语言 | erlang | java | scala |
单机吞吐量 | 万级 | 10万级 | 10万级 |
时效性 | us级 | ms级 | ms级以内 |
可用性 | 高(主从架构) | 非常高(分布式架构) | 非常高(分布式架构) |
功能特性 | 基于erlang开发,所以并发能力很强,性能极其好,延时很低;管理界面较丰富 | MQ功能比较完备,扩展性佳 | 只支持主要的MQ功能,像一些消息查询,消息回溯等功能没有提供,毕竟是为大数据准备的,在大数据领域应用广。 |
RabbitMQ:
后台基本操作:
-
connection:连接,生产者、消费者、broker之间的物理网络。
-
channel:消息通道,用于连接生产者和消费者的逻辑结构,一个连接中可以建立多个channel,每个channel代表一个会话任务,通过channel可以隔离同一个连接中的不同交互内容。
-
exchange:消息交换机,是消息第一个到达的地方,消息通过其指定的路由规则,分发到不同的消息队列(queue)中去。
exchange的类型:
- fanout:广播模式,消息会投递到所有队列。
- direct:完全根据key进行投递,如,绑定时设置了routing key为abc,那么客户端发送的消息只有设置了key为abc的才会被投递到队列。
- topic:对key进行模式匹配然后再投递,可以使用#匹配一个或多个词,*匹配正好一个词,如,abc.#可以匹配abc.aac.ddd。abc.*只能匹配abc.xxx。
-
queue:消息队列,消息最终到达的地方,到达queue的消息即进入等待消费状态,一个消息会被发送到一个或多个queue中。
-
binding:绑定,其作用为把exchange和queue按路由规则绑定起来,也就是exchange和queue之间的虚拟连接。
-
routing key:路由关键字,exchange根据这个关键字进行消息投递。
-
virtual host:虚拟主机,一个消息队列服务器的实体中可以存在多个虚拟主机,会存在两个应用程序公用一个服务器实体,为了防止冲突。所以创建两个虚拟主机,每个程序用自己的虚拟主机。
-
broker:消息队列服务器的实体,它是一个中间件应用负责接收消息,然后把消息发送给消费者或其他的broker。
用户设置:
新增用户:
新增虚拟主机:
将新增用户添加到虚拟主机:
查看结果:
学习5种队列:
P = 生产者
Quere = 队列
X = 交换器
C = 消费者
连接工厂:
public class ConnectionUtil {
public static Connection getConnection() throws Exception {
//定义连接工厂
ConnectionFactory factory = new ConnectionFactory();
//设置服务地址
factory.setHost("localhost");
//端口
factory.setPort(5672);
//设置账号信息,用户名、密码、vhost
factory.setVirtualHost("testhost");
factory.setUsername("admin");
factory.setPassword("admin");
// 通过工程获取连接
Connection connection = factory.newConnection();
return connection;
}
}
简单队列:
private final static String QUEUE_NAME = "q_test_01";
// 生产者
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
// 从连接中创建通道
Channel channel = connection.createChannel();
// 声明(创建)队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 消息内容
String message = "Hello World!";
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
//关闭通道和连接
channel.close();
connection.close();
}
----------------------------------------------------------------------------------------------------
// 消费者
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
// 从连接中创建通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 定义队列的消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
// 监听队列
channel.basicConsume(QUEUE_NAME, true, consumer);
// 获取消息
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [x] Received '" + message + "'");
}
}
我们可以看到这个简单的队列没有交换机,其实系统隐式的绑定了一个默认的交换机.
上图可以看到(Exchange = (AMQP default))。
The default exchange is implicitly bound to every queue, with a routing key equal to the queue name. It is not possible to explicitly bind to, or unbind from the default exchange. It also cannot be deleted.
默认交换机隐式绑定到每个队列,其中路由键等于队列名称。不可能显式绑定到,或从缺省交换中解除绑定。它也不能被删除。
系统会为每个队列都隐式的绑定一个默认的交换机,交换机的名称为“(AMQP default)”,类型为直连接direct,当你手动创建一个队列时,后台会自动将这个队列绑定到一个名称为空的Direct类型交换机上,绑定路由名称与队列名称相同,相当于channel.queueBind(queue:”QUEUE_NAME”, exchange:”(AMQP default)”, routingKey:”QUEUE_NAME”); 所以示例虽然没有显示声明交换机,当路由键和队列名称一样时就将消息发送到这个默认的交换机里。有了这个默认的交换机和绑定,我们就可以像其他轻量级的队列,如Redis那样,直接操作队列来处理消息。不过理论上是可以的,但实际上在RabbitMQ里直接操作是不可取的。消息始终都是先发送到交换机,由交换级经过路由传送给队列,消费者再从队列中获取消息的。不过由于这个默认交换机和路由的关系,使我们只关心队列这一层即可,这个比较适合做一些简单的应用,毕竟没有发挥RabbitMQ的最大功能(RabbitMQ可以重量级消息队列),如果都用这种方式去使用的话就真是杀鸡用宰牛刀了。
work模式:
一个生产者,两个消费者
// 生产者
private final static String QUEUE_NAME = "test_queue_work";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
for (int i = 0; i < 100; i++) {
// 消息内容
String message = "" + i;
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
Thread.sleep(i * 10);
}
channel.close();
connection.close();
}
-----------------------------------------------------------------------------------------------------
// 消费者1
private final static String QUEUE_NAME = "test_queue_work";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 同一时刻服务器只会发一条消息给消费者
//channel.basicQos(1);
// 定义队列的消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
// 监听队列,false表示手动返回完成状态,true表示自动
channel.basicConsume(QUEUE_NAME, true, consumer);
// 获取消息
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [y] Received '" + message + "'");
//休眠
Thread.sleep(10);
// 返回确认状态,注释掉表示使用自动确认模式
//channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
-----------------------------------------------------------------------------------------------------
// 消费者2
private final static String QUEUE_NAME = "test_queue_work";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 同一时刻服务器只会发一条消息给消费者
//channel.basicQos(1);
// 定义队列的消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
// 监听队列,false表示手动返回完成状态,true表示自动
channel.basicConsume(QUEUE_NAME, true, consumer);
// 获取消息
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [x] Received '" + message + "'");
// 休眠1秒
Thread.sleep(1000);
//下面这行注释掉表示使用自动确认模式
//channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
测试结果:一个消息只能被一个消费者消费,所以每个消费者消费了相同数量的不同的消息。
这样是不合理的,因为消费者1线程停顿的时间短。应该是消费者1要比消费者2获取到的消息多才对。
RabbitMQ 默认将消息顺序发送给下一个消费者,这样,每个消费者会得到相同数量的消息。即轮询(round-robin)分发消息。
联合使用 Qos 和 Acknowledge 就可以做到能者多劳,basicQos 方法设置了当前信道最大预获取(prefetch)消息数量为1。消息从队列异步推送给消费者,消费者的 ack 也是异步发送给队列,从队列的视角去看,总是会有一批消息已推送但尚未获得 ack 确认,Qos 的 prefetchCount 参数就是用来限制这批未确认消息数量的。设为1时,队列只有在收到消费者发回的上一条消息 ack 确认后,才会向该消费者发送下一条消息。prefetchCount 的默认值为0,即没有限制,队列会将所有消息尽快发给消费者。
我们使用basicQos( prefetchCount = 1)方法,来限制RabbitMQ只发不超过1条的消息给同一个消费者。当消息处理完毕后,有了反馈,才会进行第二次发送,还有一点需要注意,**使用公平分发,必须关闭自动应答,改为手动应答。 **
上边代码开启注释的两行代码:
// 同一时刻服务器只会发一条消息给消费者
channel.basicQos(1);
//开启这行 表示使用手动确认模式
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
测试结果:消费者1消费的比消费者2要多。
订阅模式(Fanout):
一个生产者,多个消费者。
每个队列都需要绑定(Binding)到交换机上。
每个消费者都有自己的一个队列。
生产者生产消息经过交换机到达队列,可以被多个消费者消费。
// 生产者
private final static String EXCHANGE_NAME = "test_exchange_fanout";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明exchange
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
// 消息内容
String message = "Hello World!";
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
channel.close();
connection.close();
}
---------------------------------------------------------------------------------------------------
// 消费者1
private final static String QUEUE_NAME = "test_queue_work1";
private final static String EXCHANGE_NAME = "test_exchange_fanout";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
// 同一时刻服务器只会发一条消息给消费者
channel.basicQos(1);
// 定义队列的消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
// 监听队列,手动返回完成
channel.basicConsume(QUEUE_NAME, false, consumer);
// 获取消息
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [Recv] Received '" + message + "'");
Thread.sleep(10);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
-----------------------------------------------------------------------------------------------------
// 消费者2
private final static String QUEUE_NAME = "test_queue_work2";
private final static String EXCHANGE_NAME = "test_exchange_fanout";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
// 同一时刻服务器只会发一条消息给消费者
channel.basicQos(1);
// 定义队列的消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
// 监听队列,手动返回完成
channel.basicConsume(QUEUE_NAME, false, consumer);
// 获取消息
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [Recv2] Received '" + message + "'");
Thread.sleep(10);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
测试结果: 同一个消息被多个消费者获取 。 **一个消费者队列可以有多个消费者实例,只有其中一个消费者实例会消费到消息。 **

可以看到test_exchange_fanout交换器关联了test_queue_work1,test_queue_work2两个队列。
路由模式(Direct):
// 生产者
private final static String EXCHANGE_NAME = "direct_exchange";
public static void main(String[] args) throws Exception {
//1、获取连接
Connection connection = getConnection();
//2、声明信道
Channel channel = connection.createChannel();
//3、声明交换器
// channel.exchangeDeclare(EXCHANGE_NAME, "fanout");//发布订阅模式
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
//4、创建消息
String message = "hello rabbitmq";
//5、发布消息
channel.basicPublish(EXCHANGE_NAME, "add", null, message.getBytes());
System.out.println("[x] Sent'" + message + "'");
//6、关闭通道
channel.close();
//7、关闭连接
connection.close();
}
-----------------------------------------------------------------------------------------------------
// 消费者1
private final static String QUEUE_NAME = "q_test_01";
private final static String EXCHANGE_NAME = "direct_exchange";
public static void main(String[] args) throws Exception {
Connection connection = getConnection();
// 从连接中创建通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定queue 和 exchange 设置routingkey
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"update");
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"add");
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"select");
channel.basicQos(1);
// 定义队列的消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
// 监听队列
channel.basicConsume(QUEUE_NAME, false, consumer);
// 获取消息
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [x] Received '" + message + "'");
Thread.sleep(10);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
-----------------------------------------------------------------------------------------------------
// 消费者2
private final static String QUEUE_NAME = "q_test_02";
private final static String EXCHANGE_NAME = "direct_exchange";
public static void main(String[] args) throws Exception {
Connection connection = getConnection();
// 从连接中创建通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定queue 和 exchange 设置routingkey
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"update");
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"delete");
channel.basicQos(1);
// 定义队列的消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
// 监听队列
channel.basicConsume(QUEUE_NAME, false, consumer);
// 获取消息
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [x] Received '" + message + "'");
Thread.sleep(1000);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
测试结果:只有消费者1会消费。
因为在队列会通过一个Routing Key和交换机绑定,生产者生产的消息头里会含有Routing Key,交换机会根据消息头的Key匹配队列进行投递
主题模式(Topic):
主题模式也叫通配符模式,和Direct模式类似。他的Routing Key是可以模糊匹配的,#匹配一个或者多个字符,*匹配一个字符。
同一个消息被多个消费者获取。**一个消费者队列可以有多个消费者实例,只有其中一个消费者实例会消费到消息。 **
// 生产者
private final static String EXCHANGE_NAME = "direct_exchange";
public static void main(String[] args) throws Exception {
//1、获取连接
Connection connection = getConnection();
//2、声明信道
Channel channel = connection.createChannel();
//3、声明交换器
// channel.exchangeDeclare(EXCHANGE_NAME, "fanout");//发布订阅模式
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
//4、创建消息
String message = "hello rabbitmq";
//5、发布消息
channel.basicPublish(EXCHANGE_NAME, "delete.a.b", null, message.getBytes());
System.out.println("[x] Sent'" + message + "'");
//6、关闭通道
channel.close();
//7、关闭连接
connection.close();
}
-----------------------------------------------------------------------------------------------------
// 消费者1
private final static String QUEUE_NAME = "q_test_01";
private final static String EXCHANGE_NAME = "direct_exchange";
public static void main(String[] args) throws Exception {
Connection connection = getConnection();
// 从连接中创建通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定queue 和 exchange 设置routingkey
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"update.*");
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"add.*");
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"delete.*");
channel.basicQos(1);
// 定义队列的消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
// 监听队列
channel.basicConsume(QUEUE_NAME, false, consumer);
// 获取消息
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [x] Received '" + message + "'");
Thread.sleep(10);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
-----------------------------------------------------------------------------------------------------
// 消费者2
private final static String QUEUE_NAME = "q_test_02";
private final static String EXCHANGE_NAME = "direct_exchange";
public static void main(String[] args) throws Exception {
Connection connection = getConnection();
// 从连接中创建通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定queue 和 exchange 设置routingkey
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"update.*");
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"delete.#");
channel.basicQos(1);
// 定义队列的消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
// 监听队列
channel.basicConsume(QUEUE_NAME, false, consumer);
// 获取消息
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [x] Received '" + message + "'");
Thread.sleep(1000);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
测试结果:只有消费者2消费。因为消息头的Key = delete.a.b,消费者1Key = delete.*,消费者2Key = delete.#。消费者2的可以匹配delete后多个字符,消费者1只能匹配一个字符。
queueDeclare的参数:第一个参数表示队列名称、第二个参数为是否持久化(true表示是,队列将在服务器重启时生存)、第三个参数为是否是独占队列(创建者可以使用的私有队列,断开后自动删除)、第四个参数为当所有消费者客户端连接断开时是否自动删除队列、第五个参数为队列的其他参数
basicConsume的第二个参数autoAck: 应答模式,true:自动应答,即消费者获取到消息,该消息就会从队列中删除掉,false:手动应答,当从队列中取出消息后,需要程序员手动调用方法应答,如果没有应答,该消息还会再放进队列中,就会出现该消息一直没有被消费掉的现象