简介
RabbitMQ是一个开源的AMQP实现,服务器端用Erlang语言编写,支持多种客户端(语言),存储转发消息
一、RabbitMQ系统架构
1.绿色图示为交换机,红色图示为消息队列 在服务端称作Broker,由RabbitMQ实现
2.蓝色为生产者和消费者两种类型,为客户端
二、RabbitMQ概念
RabbitMQ两大核心组Exchange和Queue
1.Queue-消息队列
Queue是一个不重复,唯一,名字随机的的缓冲区,应用程序在其权限之内可以自由地创建、共享使用和消费消息队列
durability:持久化 durable:是 transient:否
auto delete:如果是yes,最后一个监听被移除后,这个队列自动删除
2.Exchange-交换机
1.Exchange称作交换器,它接收消息和路由消息,然后将消息发送给消息队列。每个交换器都有独一无二的名字。
2.交换机属性:
- name:交换机名称
- type:交换机类型 direct、topic、fanout、headers
- durability:是否需要持久化,true为持久化
- auto delete:当最后一个绑定到exchange上的队列删除后,自动删除该exchange
- internal:当前exchange是否用于rabbitmq内部使用,默认false
3.交换机类型:
- direct(直连式交换机) 所有发送到direct exchange的消息被转发到routekey中指定的queue
(1)Producer:
public class RabbitMQProducer_Direct {
private static final String EXCHANGE_NAME = "RABBITMQ_Direct";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("localhost");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
//指定Exchange的Type = "direct"
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
String routingKey1 = "error";
String message1 = "error infomations....";
String routingKey2 = "warning";
String message2 = "warning infomations....";
String routingKey3 = "info";
String message3 = "info infomations....";
//指定消息的路由参数:routingKey,并发送消息
channel.basicPublish(EXCHANGE_NAME, routingKey1, null, message1.getBytes());
channel.basicPublish(EXCHANGE_NAME, routingKey2, null, message2.getBytes());
channel.basicPublish(EXCHANGE_NAME, routingKey3, null, message3.getBytes());
//发布消息成功提示信息
System.out.println("RABBITMQ客户端成功发送信息:" + message1);
System.out.println("RABBITMQ客户端成功发送信息:" + message2);
System.out.println("RABBITMQ客户端成功发送信息:" + message3);
//关闭连接
channel.close();
connection.close();
}
}
(2)Consumer:
两个Consumer,一个 bindingKey 是”error“;只能收到routing key 是 “error”的消息;
另一个是{“error”, “info”, “warning”},可以收到routing key 是 {“error”, “info”, “warning”}的消息。
//Consumer1
public class RabbitMQConsumer_Direct {
private static final String EXCHANGE_NAME = "RABBITMQ_Direct";
private static String bindingKey = "error";
public static void main(String[] args) throws Exception {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("localhost");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName, EXCHANGE_NAME, bindingKey);
System.out.println(" ---【开始接收消息,退出请按CTRL+C】---");
//RabbitMQConsumer_Topic consumer = new RabbitMQConsumer_Topic();
QueueingConsumer consumer = new QueueingConsumer(channel);
channel.basicConsume(queueName, true, consumer);
while (true){
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
String routingKey = delivery.getEnvelope().getRoutingKey();
System.out.println(" Consumer1接收消息: '" + routingKey + "':'" + message + "'");
}
}
}
//-------------------------------------------------------------------------------------
//Consumer2
public class RabbitMQConsumer2_Direct {
private static final String EXCHANGE_NAME = "RABBITMQ_Direct";
private static String[] bindingKeys = new String[]{"error", "info", "warning"};
public static void main(String[] args) throws Exception {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("localhost");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
String queueName = channel.queueDeclare().getQueue();
System.out.println(">>>>" + queueName);
//绑定:Exchange与Queue绑定
for(String bindingKey : bindingKeys){
channel.queueBind(queueName, EXCHANGE_NAME, bindingKey);
}
System.out.println(" ---【开始接收消息,退出请按CTRL+C】---");
//RabbitMQConsumer_Topic consumer = new RabbitMQConsumer_Topic();
QueueingConsumer consumer = new QueueingConsumer(channel);
channel.basicConsume(queueName, true, consumer);
while (true){
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
String routingKey = delivery.getEnvelope().getRoutingKey();
System.out.println(" Consumer2接收消息: '" + routingKey + "':'" + message + "'");
}
}
}
- topic(主题式交换器) 所有发送到topic exchange的消息被转发到所有关心routekey中指定topic的queue上
- exchange将routekey和某个topic进行模糊匹配,此时队列需要绑定一个topic
注意:可以使用通配符模糊匹配
一个或多个 * 一个
- exchange将routekey和某个topic进行模糊匹配,此时队列需要绑定一个topic
(1)Producer:
public class RabbitMQProducer_Topic {
private static final String EXCHANGE_NAME = "RABBITMQ_Topic";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("localhost");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
//指定Exchange的Type = "Topic"
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
String routingKey1 = "AAA.orange.BBB";
String message1 = "Q1 infomations....";
String routingKey2 = "lazy.orange.fox";
String message2 = "Q1,Q2 infomations....";
String routingKey3 = "lazy.brown.fox";
String message3 = "Q2 infomations....";
//指定消息的路由参数:routingKey,并发送消息
channel.basicPublish(EXCHANGE_NAME, routingKey1, null, message1.getBytes());
channel.basicPublish(EXCHANGE_NAME, routingKey2, null, message2.getBytes());
channel.basicPublish(EXCHANGE_NAME, routingKey3, null, message3.getBytes());
//发布消息成功提示信息
System.out.println("RABBITMQ客户端成功发送信息:" + message1);
System.out.println("RABBITMQ客户端成功发送信息:" + message2);
System.out.println("RABBITMQ客户端成功发送信息:" + message3);
//关闭连接
channel.close();
connection.close();
}
}
(2)Consumer:
Topic模式下模拟了两个Consumer,一个 bindingKey 是”.orange.“,只能收到routing key 匹配“*.orange.”的消息;另一个是{“…rabbit”, “lazy.#”},可以收到routing key 是匹配{“…rabbit”, “lazy.#”}的消息。
//Consumer1:
public class RabbitMQConsumer_Topic {
private static final String EXCHANGE_NAME = "RABBITMQ_Topic";
private static String bindingKey = "*.orange.*";
public static void main(String[] args) throws Exception {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("localhost");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
//queueName是随机产生的
String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName, EXCHANGE_NAME, bindingKey);
System.out.println(" ---【开始接收消息,退出请按CTRL+C】---");
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body, "UTF-8");
System.out.println(" Consumer1 Received '" + envelope.getRoutingKey() + "':'" + message + "'");
}
};
channel.basicConsume(queueName, true, consumer);
}
}
//-------------------------------------------------------------------------------------
//Consumer2:
public class RabbitMQConsumer2_Topic {
private static final String EXCHANGE_NAME = "RABBITMQ_Topic";
private static String[] bindingKeys = new String[]{"*.*.rabbit", "lazy.#"};
public static void main(String[] args) throws Exception {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("localhost");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
String queueName = channel.queueDeclare().getQueue();
//打印出来的queueName:>>>>amq.gen-QjLNUuPTzIHuaBq-TBL6fQ,是随机产生的
System.out.println(">>>>" + queueName);
//绑定:Exchange与Queue绑定
for(String bindingKey : bindingKeys){
channel.queueBind(queueName, EXCHANGE_NAME, bindingKey);
}
System.out.println(" ---【开始接收消息,退出请按CTRL+C】---");
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body, "UTF-8");
System.out.println(" Consumer1 Received '" + envelope.getRoutingKey() + "':'" + message + "'");
}
};
channel.basicConsume(queueName, true, consumer);
}
}
-
Fanout(广播式交换机) 不处理任何路由键,只需要简单的将队列绑定在交换机上
- 发送到交互机的消息都会被转发到该交换机绑定的所有队列上
- fanout交换机转发消息是最快的
(1)produce:
public class RabbitMQProducer_Fanout {
public static final String EXCHANGE_NAME = "RABBITMQ_Fanout";
public static void main(String[] args) throws Exception{
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("localhost");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
String message = "This message is from Fanout mode.特点是Consumer均可获取到消息";
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
System.out.println("---【Producer发送消息】" + message + "---" );
channel.close();;
connection.close();
}
}
(2)Consumer:
//Consumer1
public class RabbitMQConsumer_Fanout {
public static final String EXCHANGE_NAME = "RABBITMQ_Fanout";
public static void main(String[] args) throws Exception {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("localhost");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
//获取Queue随机名
String queueName = channel.queueDeclare().getQueue();
//Binding:绑定Queue与Exchange,此处没有binding key。
channel.queueBind(queueName, EXCHANGE_NAME, "");
System.out.println("---Consumer1 准备接收消息--");
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body, "UTF-8");
System.out.println(" 【Consumer1 接收消息 】'" + message + "'");
}
};
channel.basicConsume(queueName, true, consumer);
}
}
//--------------------------------------------------------------------------------------
//Consumer2
public class RabbitMQConsumer2_Fanout {
public static final String EXCHANGE_NAME = "RABBITMQ_Fanout";
public static void main(String[] args) throws Exception {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("localhost");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
//获取Queue随机名
String queueName = channel.queueDeclare().getQueue();
//Binding:绑定Queue与Exchange,此处没有binding key。
channel.queueBind(queueName, EXCHANGE_NAME, "");
System.out.println("---Consumer2 准备接收消息--");
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body, "UTF-8");
System.out.println(" 【Consumer2 接收消息 】'" + message + "'");
}
};
channel.basicConsume(queueName, true, consumer);
}
}
- Headers:headers类型的Exchange不依赖于routing key与binding key的匹配规则来路由消息,而是根据发送的消息内容中的headers属性进行匹配
3.Routing Key
路由规则,虚拟机可用它来确定如何路由一个特定消息 Queue:也称为Message Queue,消息队列,保存消息并将它们转发给消费者
4.Binding
Exchange和Queue之间的虚拟连接,binding中可以包含routing key( exchange和exchange、queue 之间的连接关系 binding中可以包含routingKey或者参数)
5.Server
又称Broker, 接受客户端的连接,实现AMQP实体服务
6.Connection
连接,应用程序与Broker的网络连接
7.Channel
网络信道,几乎所有的操作都在Channel中进行,Channel是进行消息读写的通道。客户端可建立多个Channel,每个Channel代表一个会话任务。
8.Message
消息,服务器和应用程序之间传送的数据,由Properties和Body组成
服务器和应用程序之间传送的数据
本质上是一段数据,由properties和 payload(body)组成
常用属性:delivery mode、header(自定义属性)
其他属性:content_typea、content_encoding、priority、correlation_id、reply_to、expiration、message_id…
9.Properties
可以对消息进行修饰, 比如消息的优先级、延迟等高级特性; Body则就是消息体内容。
10.Virtual host
虚拟地址,用于进行逻辑隔离,最上层的消息路由。一个virtual host里面可以有若干个exchange和queue。同一个virtual host里面不能有相同名称的exchange和queue。
三、RabbitMQ高级特性
1.消息如何保障100%投递成功
什么是生产端的可靠性投递?
- 保障消息的成功发出
- 保障mq节点成功接收
- 发送端收到mq节点(broker)确认应答
- 完善的消息进行补偿机制
1.1 生产端-可靠性投递(一)
解决方案:
- 消息落库,对消息状态进行打标
BIZ DB (业务数据库) MSG DB(消息数据库)
- step1业务入库和消息入库
- step2:step1成功,生产端的Sender进行消息发送(消息状态初始值为0)
- step3:Broker(Server)收到消息并发送应答给生产端的Confirm Listener
- step4:Confirm Listener异步监听Broker的应答消息并进行判断
假设step2通过,在step3回送响应时,网络突然出现了闪断,导致生产端的Listener收不到这条消息的confirm应答,消息的状态始终为0 - step5:分布式定时任务抓取状态为0的消息
- step6:将状态为0的消息重发
- step7如果尝试了3次(可按实际情况修改)以上则将状态置为2(消息投递失败状态)
1.2 生产端-可靠性投递(二)
在第一种情况下,高并发的情境下,数据库的两次写操作和读取操作会存在数据库IO瓶颈
这种是减少对数据库的操作,提高并发量。但不是关心消息是不是能够100%的投递成功.
解决方案:
- 消息的延迟投递,做二次确认,回调检查
Upstream service:上游服务,可能为生产端
Downstream service:下游服务,可能为消费端
MQ Broker:可能为集群
Callback service:回调服务,监听confirm消息,独立的服务
- step1 业务数据入库,成功后生产端发送消息到Broker
- step2 消息发送成功之后,生产端发送一条延迟消息(Second Send Delay Check),需要设置延迟时间
- step3 消息队列进行指定队列的监听,对收到的消息进行处理
- step4 消费端处理完毕之后,发送Confirm(不是ACK)到Broker
- step5 Callback service是一个单独的服务,其实它扮演了方案一的存储消息的DB角色,它通过Broker去监听消费端发送的Confirm消息,如果收到消息,那么将消息持久化到DB当中.
- step6 一定延迟时间之后再次发送消息给Broker,然后还是Callback Service去监听延迟消息所对应的队列.收到之后去检查MSG DB中是否有这条消息,如果存在,通过.不存在或者是消毒费失败了,那么Callback Service就需要主动发起RPC通信给上游服务,告诉它延迟投递的这条消息没有找到,需要重新发送,生产端收到信息后就会重新查询业务消息然后将消息发送出去,循环第一步。
2.幂等性概念
2.1 what幂等性
一个操作多次执行产生的结果与一次执行产生的结果一致,比如 sql无论执行多少次,结果都是相同的(借鉴乐观锁,加版本),利用加版本号Version的方式来保证幂等性。
2.2 消费端-幂等性保障
在海量订单产生的业务高峰期,如何避免消息的重复消费问题?
在高并发的情况下,会有大量的消息到达MQ,消费端需要监听大量的消息。这样的情况下,难免会出现消息的重复投递,网络闪断等等。如果不去做幂等,则会出现消息的重复消费。
解决: 消费端实现幂等性,就是我们的消息永远不会消费多次,即使收到多条一样的消息,也只会执行一次
方法:
- 唯一id+指纹码机制
- 利用数据库主键去重
- 或者利用redis原子性去实现
2.2.1 唯一ID+指纹码机制
- 唯一ID +指纹码机制,利用数据库主键去重
- select count(1) from t_order where id=唯一ID+指纹码
- 好处:实现简单
- 坏处:高并发下有数据库写入的性能瓶颈
- 解决方案:跟进ID进行分库分表进行路由算法
2.2.2 Redis 原子特性实现
最简单使用Redis的自增。
使用Redis进行幂等,需要考虑的问题。
- 第一:是否需要数据落库,如果落库的话,关键解决的问题是数据库和缓存如何做到原子性?
加事务不行,Redis和数据库的事务不是同一个,无法保证同时成功同时失败。大家有什么更好的方案呢? - 第二:如果不进行落库,那么都存储到缓存中,如何设置定时同步的策略?
怎么做到缓存数据的稳定性?
3. Confirm 确认消息
理解Confirm 消息确认机制:
- 消息的确认,是指生产者投递消息后,如果Broker收到消息,则会给我们生产者一个应答。
- 生产者进行接收应答,用来确定这条消息是否正常的发送到Broker,这种方式也是消息的可靠性投递的核心保障!
蓝色:producer 生产者 红色:MQ Broker 服务器
生产者把消息发送到Broker端,Broker收到消息之后回送给producer。Confirm Listener 监听应答。
操作是异步操作,当生产者发送完消息之后,就不需要管了。Confirm Listener 监听MQ Broker的应答。
3.1 如何实现Confirm 确认消息
第一步:在channel上开启确认模式:channel.confirmSelect()
第二步;在chanel上 添加监听:addConfirmListener,监听成功和失败的返回结果,根据具体的结果对消息进行重新发送、或记录日志等后续处理!
3.2 实现Confirm 确认消息-代码实例
/**
* 生产者
**/
package com.gao.rabbitmq.confirm;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConfirmListener;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Producer {
public static void main(String[] args) throws IOException, TimeoutException {
//1。创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("localhost");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
//2获取connection
Connection connection = connectionFactory.newConnection();
//3通过connection 创建channel
Channel channel = connection.createChannel();
//4.指定消息投递模式:消息确认模式
channel.confirmSelect();
String exchangeName="test_confirm_exchange";
String rouyingKey="confirm.save";
//5.发送消息
String msg="hello";
channel.basicPublish(exchangeName,rouyingKey,null,msg.getBytes());
//6。添加确认监听
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleAck(long l, boolean b) throws IOException {
System.out.println("=--------- ack-----");
}
@Override
public void handleNack(long l, boolean b) throws IOException {
System.out.println("=--------no-ack-----");
}
});
}
}
//------------------------------------------------------------
/**
* 消费者
**/
package com.gao.rabbitmq.confirm;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.QueueingConsumer;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer {
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
//1。创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("localhost");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
//2获取connection
Connection connection = connectionFactory.newConnection();
//3通过connection 获取channel
Channel channel = connection.createChannel();
String exchangeName="test_confirm_exchange";
String rouyingKey="confirm.#";
String queueName="test_confirm_queue";
//4.声明交换机和队列然后进行绑定,指定路由key
channel.exchangeDeclare(exchangeName,"topic",true);
channel.queueDeclare(queueName,true,false,false,null);
channel.queueBind(queueName,exchangeName,rouyingKey);
//5创建消费者
QueueingConsumer queueingConsumer = new QueueingConsumer(channel);
channel.basicConsume(queueName,true,queueingConsumer);
while(true){
Delivery delivery = queueingConsumer.nextDelivery();
String msg=new String( delivery.getBody()) ;
System.out.println("消费端"+msg);
}
}
}
先启动消费端->再启动生产端
3.3 查看管控台
1.Queues下有一个name为test_confirm_queue
的队列
2.Queues页面下拉有一个Bindings绑定的交换机的名字为test_confirm_exchange
3.Exchange页面的Bindings下有具体信息test_confirm_queue
的队列名以及
```confirm.#````的路由key
控制台可以观察到消费端先接收到消息,之后生产端再接收到回调信息。如果出现磁盘已满、RabbitMQ出现异常、queue容量到达上限都可能接收到no ack
如果ack和no ack消息都未接收到,这就是之前所说的。RabbitMQ出现网络闪断,可以采用上面所说的消息补偿。
4. Return消息机制
- Return Listener用于处理一些不可路由的消息!
- 消息生产者通过指定一个Exchange和Routingkey,把消息送达到某一个队列中去,然后消费者监听队列,进行消费处理操作!
- 在某些情况下,如果在发送消息的时候,当前的exchange不存在或者指定的路由key路由不到,这个时候如果需要监听这种不可达的消息,就要使用Return Listener!
在基础API中有一个关键的配置项:
- Mandatory:如果为true,则监听器会接收到路由不可达的消息,然后进行后续处理,如果为false,那么broker端自动删除该消息!
4.1 Return消息机制流程
- Producer生产端将消息发送到MQ Broker端,但是出现NotFind Exchange,
发送的消息的Exchange,在Broker端未能找到
。或者找到了,但是路由key路由不到指定的队列
。因此是一个错误的消息。
-这个时候,生产端应该知道发送的这条消息,并不会被处理。因此MQ Broker提供了这种Return机制
,将这些不可达的消息发送给生产端
,这时候生产端
就需要设置Return Listener
去接收
这些不可达的消息。然后及时记录日志,去处理这些消息。
4.2 Return消息机-代码实例
/**
*生产者
**/
public class Producer {
public static void main(String[] args) throws Exception {
//1 创建ConnectionFactory
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
String exchange = "test_return_exchange";
String routingKey = "return.save";
String routingKeyError = "abc.save";
String msg = "Hello RabbitMQ Return Message";
channel.addReturnListener(new ReturnListener() {
@Override
public void handleReturn(int replyCode, String replyText, String exchange,
String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.err.println("---------handle return----------");
//响应码
System.err.println("replyCode: " + replyCode);
//响应文本
System.err.println("replyText: " + replyText);
System.err.println("exchange: " + exchange);
System.err.println("routingKey: " + routingKey);
System.err.println("properties: " + properties);
System.err.println("body: " + new String(body));
}
});
//第三个参数mandatory=true,意味着路由不到的话mq也不会删除消息,false则会自动删除
channel.basicPublish(exchange, routingKey, true, null, msg.getBytes());
//修改routingkey,测试是否能够收到消息
//channel.basicPublish(exchange, routingKeyError, true, null, msg.getBytes());
}
}
//----------------------------------------------------------------------------
/**
* 消费者
**/
public class Consumer {
public static void main(String[] args) throws Exception {
//1 创建ConnectionFactory
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
String exchangeName = "test_return_exchange";
String routingKey = "return.#";
String queueName = "test_return_queue";
channel.exchangeDeclare(exchangeName, "topic", true, false, null);
channel.queueDeclare(queueName, true, false, false, null);
channel.queueBind(queueName, exchangeName, routingKey);
QueueingConsumer queueingConsumer = new QueueingConsumer(channel);
channel.basicConsume(queueName, true, queueingConsumer);
while(true){
Delivery delivery = queueingConsumer.nextDelivery();
String msg = new String(delivery.getBody());
System.err.println("消费者: " + msg);
}
}
}
//--------------------------------------------------------------------------
/**
* 连接工具类
**/
public class ConnectionUtils {
public static Connection getConnection() throws IOException, TimeoutException {
//定义连接工厂
ConnectionFactory factory = new ConnectionFactory();
//设置服务地址
factory.setHost("localhost");
//端口
factory.setPort(5672);//amqp协议 端口 类似与mysql的3306
//设置账号信息,用户名、密码、vhost
factory.setVirtualHost("/");
factory.setUsername("gn");
factory.setPassword("123456");
// 通过工程获取连接
Connection connection = factory.newConnection();
return connection;
}
}
先启动消费端,在启动生产端
4.3 查看管控台
1.Queues下有一个name为test_return_queue
的队列
2.Queues页面下拉有一个Bindings绑定的交换机的名字为test_return_exchange
3.Exchange页面的Bindings下有具体信息test_confirm_queue
的队列名以及
confirm.#
的路由key
打印结果:
1.正常打印:
放开消费端代码:channel.basicPublish(exchange, routingKey, true, null, msg.getBytes());
消费端打印结果:消费者: Hello RabbitMQ Return Message
2.非正常打印:
改代码为:channel.basicPublish(exchange, routingKeyError, true, null, msg.getBytes());
打印结果:生产端收到不可达消息
5. 自定义消费者
5.1 消费端自定义监听
在代码中编写while循环,进行consumer.nextDelivery方法进行获取下一条消息,然后进行消费处理!这种轮询方式不好,可以使用自定义的Consumer更加方便,解耦性更加强,在实际工作中比较常见。
5.2 代码实例
/**
* 生产者
**/
public class Producer {
public static void main(String[] args) throws Exception {
//1 创建ConnectionFactory
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
String exchange = "test_consumer_exchange";
String routingKey = "consumer.save";
String msg = "Hello RabbitMQ Consumer Message";
for(int i =0; i<5; i ++){
channel.basicPublish(exchange, routingKey, true, null, msg.getBytes());
}
}
}
//------------------------------------------------------------------------------------
/**
*消费者
**/
public class Consumer {
public static void main(String[] args) throws Exception {
// 创建ConnectionFactory
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
String exchangeName = "test_consumer_exchange";
String routingKey = "consumer.#";
String queueName = "test_consumer_queue";
channel.exchangeDeclare(exchangeName, "topic", true, false, null);
channel.queueDeclare(queueName, true, false, false, null);
channel.queueBind(queueName, exchangeName, routingKey);
//实现自己的MyConsumer()
channel.basicConsume(queueName, true, new MyConsumer(channel));
}
}
//------------------------------------------------------------------------------------
/**
* 自定义类:MyConsumer
**/
public class MyConsumer extends DefaultConsumer {
public MyConsumer(Channel channel) {
super(channel);
}
//根据需求,重写自己需要的方法。
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.err.println("-----------consume message----------");
//消费标签
System.err.println("consumerTag: " + consumerTag);
//这个对象包含许多关键信息
System.err.println("envelope: " + envelope);
System.err.println("properties: " + properties);
System.err.println("body: " + new String(body));
}
}
//------------------------------------------------------------------------------------
**
* 连接工具类
**/
public class ConnectionUtils {
public static Connection getConnection() throws IOException, TimeoutException {
//定义连接工厂
ConnectionFactory factory = new ConnectionFactory();
//设置服务地址
factory.setHost("localhost");
//端口
factory.setPort(5672);//amqp协议 端口 类似与mysql的3306
//设置账号信息,用户名、密码、vhost
factory.setVirtualHost("/");
factory.setUsername("gn");
factory.setPassword("123456");
// 通过工程获取连接
Connection connection = factory.newConnection();
return connection;
}
}
5.3 控制台打印结果
6.消费端限流
6.1 what 消费端限流
假设一个场景,首先,Rabbitmq服务器有上万条未处理的消息,随便打开一个消费者客户端,会出现下面情况:
巨量的消息瞬间全部推送过来,但是单个客户端无法同时处理这么多数据!这个时候很容易导致服务器崩溃,出现故障。
为什么不在生产端限流?
因为在高并发
的情况下,客户量就是非常大,所以很难在生产端做限制
。因此我们可以用MQ在消费端做限流
。
- RabbitMQ提供了一种
qos(服务质量保证)
功能,即在非自动确认消息
的前提下,如果一定数目的消息
(通过基于consume或者channel设置Qos的值)未被确认前
,不进行消费
新的消息。
在限流的情况下,千万不要设置自动签收,要设置为手动签收
。 - void BasicQos(uint prfetchSize,ushort prefetchCount,bool global);
参数解释:
- prefetchSize:0
prefetchCount
:会告诉RabbitMQ不要
同时给一个消费者
推送多于N个消息
,即一旦有N个消息
还没有ack
,则该consumer
将block掉
,直到
有消息ack
。global
: true\false 是否将上面设置应用于channel,简单点说,就是上面限制是channel级
别还是consumer级
别。- prefetchSize和global这两项,rabbitmq没有实现,暂且不研究
prefetch_count在no_ask = false
的情况下生效
,即在自动应答
的情况下这两个值
是不生效
的。
第一个参数(prefetchSize):消息的限制大小,消息多少兆。一般不做限制,设置为0
第二个参数(prefetchCount):一次最多处理多少条,实际工作中设置为1就好
第三个参数(global):限流策略在什么上应用。在RabbitMQ一般有两个应用级别:1.通道 2.Consumer级别。一般设置为false,true 表示channel级别,false表示在consumer级别
6.2 代码实例
/**
* 生产者
**/
public class Producer {
public static void main(String[] args) throws Exception {
//1 创建ConnectionFactory
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
String exchange = "test_qos_exchange";
String routingKey = "qos.save";
String msg = "Hello RabbitMQ QOS Message";
for(int i =0; i<5; i ++){
channel.basicPublish(exchange, routingKey, true, null, msg.getBytes());
}
}
}
//------------------------------------------------------------------------------------
/**
* 消费者
**/
public class Consumer {
public static void main(String[] args) throws Exception {
//1 创建ConnectionFactory
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
String exchangeName = "test_qos_exchange";
String queueName = "test_qos_queue";
String routingKey = "qos.#";
channel.exchangeDeclare(exchangeName, "topic", true, false, null);
channel.queueDeclare(queueName, true, false, false, null);
channel.queueBind(queueName, exchangeName, routingKey);
//1 限流方式 第一件事就是 autoAck设置为 false
//设置为1,表示一条一条数据处理
channel.basicQos(0, 1, false);
channel.basicConsume(queueName, false, new MyConsumer(channel));
}
}
//------------------------------------------------------------------------------------
/**
* 自定义类:MyConsumer
**/
public class MyConsumer extends DefaultConsumer {
private Channel channel ;
public MyConsumer(Channel channel) {
super(channel);
this.channel = channel;
}
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.err.println("-----------consume message----------");
System.err.println("consumerTag: " + consumerTag);
System.err.println("envelope: " + envelope);
System.err.println("properties: " + properties);
System.err.println("body: " + new String(body));
//需要做签收,false表示不支持批量签收
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
//------------------------------------------------------------------------------------
**
* 连接工具类
**/
public class ConnectionUtils {
public static Connection getConnection() throws IOException, TimeoutException {
//定义连接工厂
ConnectionFactory factory = new ConnectionFactory();
//设置服务地址
factory.setHost("localhost");
//端口
factory.setPort(5672);//amqp协议 端口 类似与mysql的3306
//设置账号信息,用户名、密码、vhost
factory.setVirtualHost("/");
factory.setUsername("gn");
factory.setPassword("123456");
// 通过工程获取连接
Connection connection = factory.newConnection();
return connection;
}
}
6.3 查看管控台
1.先注释掉:channel.basicAck(envelope.getDeliveryTag(), false);
然后启动Consumer
查看Exchange,交换机名称为test_qos_exchange
查看Queues,绑定名字为test_qos_exchange
的交换机,Routing key 为qos.#
然后启动Producer,们会发现消费端,只收到了一条消息!
原因:
- 在consumer中,
channel.basicConsume(queueName, false, new MyConsumer(channel));
,参数设置为false为手签收 - 在qos中设置只接受一条消息。如果这一条消息不给Broker Ack应答的话,那么Broker会认为你并没有消费完这一条消息,那么就不会继续发送消息
channel.basicQos(0, 1, false);
管控台,unack=1,Ready=4,total=5.
放开注释channel.basicAck(envelope.getDeliveryTag(), false); 进行消息签收
。重启服务。
6.4 控制台打印结果
正常打印5条消息