消息队列(Message Queue)MQ 产品从模型抽象上来说都是一样的过程:
消费者(consumer)订阅某个队列,生产者(producer)创建消息,然后发布到队列(queue)中,最后将消息发送到监听的消费者
AMQP(高级消息队列协议)是一个网络协议。它支持符合要求的客户端应用(application)和消息中间件代理(messaging middleware broker)之间进行通信
AMQP 协议中的核心思想就是生产者和消费者的解耦,生产者从不直接将消息发送给队列
AMQP的机制如下图所示:
消息(message)被发布者(publisher)发送给交换机(exchange),然后交换机将收到的消息根据路由规则分发给绑定的队列(queue)。最后AMQP代理会将消息投递给订阅了此队列的消费者,或者消费者按照需求自行获取。
RabbitMQ系统最核心的组件是Exchange(交换机)和Queue(队列),Exchange和Queue是在rabbitmq server(又叫做Broker)端,Producer(生产者)和Consumer(消费者)在应用端
1、Message
消息,消息是不具体的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。
2、Publisher
消息的生产者,也是一个向交换器发布消息的客户端应用程序。
3、Exchange
交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。
4、Binding
绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。
5、Queue
消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。
6、Connection
网络连接,比如一个TCP连接。
7、Channel
信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内地虚拟连接,AMQP 命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁 TCP 都是非常昂贵的开销,所以引入了信道的概念,以复用一条 TCP 连接。
8、Consumer
消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。
9、Virtual Host
虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个 vhost 本质上就是一个 mini 版的RabbitMQ 服务器,拥有自己的队列、交换器、绑定和权限机制。vhost 是 AMQP概念的基础,必须在连接时指定,RabbitMQ 默认的 vhost 是 / 。
10、Broker
表示消息队列服务器实体。
队列,交换机和绑定统称为AMQP实体(AMQP entities)
交换机是用来发送消息的AMQP实体。交换机拿到一个消息之后将它路由给一个或零个队列。它使用哪种路由算法是由交换机类型和被称作绑定(bindings)的规则所决定的。
Exchange分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct(直连交换机)、fanout(扇型交换机)、topic(主题交换机)、headers(头交换机)
headers 交换器和 direct 交换器完全一致,但性能差很多,目前几乎不用,主要使用direct、fanout、topic
direct、fanout、topic
1、direct(直连交换机)
消息中的路由键(routing key)如果和 Binding 中的 binding key 一致, 交换器就将消息发到对应的队列中。路由键与队列名完全匹配,如果一个队列绑定到交换机要求路由键为“hello”,则只转发 routing key 标记为“hello”的消息。它是完全匹配、单播的模式。
2、fanout(扇型交换机)
发到 fanout 类型交换器的消息都会分到所有绑定的队列上去。fanout 交换器不处理路由键,只是简单的将队列绑定到交换器上,每个发送到交换器的消息都会被转发到与该交换器绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。fanout 类型转发消息是最快的
3、topic(主题交换机)
topic 交换器通过模式匹配分配消息的路由键属性,将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上。
它将路由键和绑定键的字符串切分成单词,这些单词之间用点隔开。它同样也会识别两个通配符:符号“#”和符号“*”
#匹配0个或多个单词
“*”匹配不多不少一个单词
分别创建两个maven Java普通工程,一个命名java-send,一个命名java-receive
发送消息类Send
/**
* 普通发送消息,无交换机
* 消费者(consumer)订阅某个队列,生产者(producer)创建消息,然后发布到队列(queue)中,最后将消息发送到监听的消费者
*/
public class Send {
public static void main(String[] args) {
//创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
/**
* 配置RabbitMQ连接信息
*/
//指定IP
connectionFactory.setHost("192.168.133.129");
//指定端口(控制台端口15672)
connectionFactory.setPort(5672);
//指定账号
connectionFactory.setUsername("root");
//指定密码
connectionFactory.setPassword("root");
//定义连接
Connection connection = null;
//定义通道
Channel channel = null;
try {
//获取连接
connection = connectionFactory.newConnection();
//获取通道
channel = connection.createChannel();
/**
* 声明一个队列
* 参数1 队列名称取值任意
* 参数2 是否为持久化的队列
* 参数3 是否排外,如果排外这个队列只允许一个消费者监听
* 参数4 是否自动删除队列,如果为true表示当前队列中没有消息,也没有消费者连接时就会自动删除这个队列
* 参数5 队列的一些属性设置,通常为null
*
* 注:
* 1、声明队列时,这个队列名称如果已经存在则放弃声明,如果队列不存在则会声明一个新的队列
* 2、队列名可以取值任意,但是要与消息接收时的队列名称完全一致
* 3、一定要在发送消息前确认队列名已经存在在RabbitMQ中,否则就会出现问题(如下代码可有可无)
*/
String queueName = "myQueue";
channel.queueDeclare(queueName,true,false,false,null);
String message = "我的RabbitMQ消息,无交换机";
/**
* 发送消息到RabbitMQ
* 参数1 交换机名称,空字符串表示不使用交换机
* 参数2 队列名或RoutingKey(当指定了交换机名称,这个值就是RoutingKey)
* 参数3 消息属性信息,通常为null
* 参数4 具体的消息数据的字节数组
*
* 注:发送消息的队列名与接收消息的队列名要保持完全一致
*/
channel.basicPublish("",queueName,null,message.getBytes("UTF-8"));
System.out.println("消息发送成功!(无交换机)");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} finally {
if(channel != null){
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
if(connection != null){
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
接收消息类Receive
/**
* 普通接收消息,无交换机
* 消费者(consumer)订阅某个队列,生产者(producer)创建消息,然后发布到队列(queue)中,最后将消息发送到监听的消费者
*/
public class Receive {
public static void main(String[] args) {
//创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
/**
* 配置RabbitMQ连接信息
*/
//指定IP
connectionFactory.setHost("192.168.133.129");
//指定端口(控制台端口15672)
connectionFactory.setPort(5672);
//指定账号
connectionFactory.setUsername("root");
//指定密码
connectionFactory.setPassword("root");
//定义连接
Connection connection = null;
//定义通道
Channel channel = null;
try {
//获取连接
connection = connectionFactory.newConnection();
//获取通道
channel = connection.createChannel();
/**
* 声明一个队列
* 参数1 队列名称取值任意
* 参数2 是否为持久化的队列
* 参数3 是否排外,如果排外这个队列只允许一个消费者监听
* 参数4 是否自动删除队列,如果为true表示当前队列中没有消息,也没有消费者连接时就会自动删除这个队列
* 参数5 队列的一些属性设置,通常为null
*
* 注:
* 1、声明队列时,这个队列名称如果已经存在则放弃声明,如果队列不存在则会声明一个新的队列
* 2、队列名可以取值任意,但是要与消息接收时的队列名称完全一致
* 3、一定要在发送消息前确认队列名已经存在在RabbitMQ中,否则就会出现问题(如下代码可有可无)
*/
String queueName = "myQueue";
channel.queueDeclare(queueName,true,false,false,null);
String message = "我的RabbitMQ消息,无交换机";
/**
* 接收消息
* 参数1 当前消费者监听的队列名(接收消息的队列名必须要和发送消息的队列名完全保持一致,否则接收不到消息)
* 参数2 消息是否自动确认;true表示自动确认接收,且接收完消息以后会自动将消息从队列中移除
* 参数3 消息接收者标签,用于多个消费者同时监听一个队列时,用于确认不同的消费者,通常为空字符串
* 参数4 消息接收的回调方法,完成对消息的处理
*
* 注:使用了basiccnsume()方法后,会自动开启一个线程持续监听队列,如果队列中有消息数据进入会自动接收消息
* 因此不能关闭连接和通道对象
*/
channel.basicConsume(queueName,true,"",new DefaultConsumer(channel){
//消息具体接收方法和处理方法
public void handleDelivery(String consumerTag, Envelope envelope,AMQP.BasicProperties properties,byte[] body) throws IOException {
String message = new String(body,"UTF-8");
System.out.println("消费者----" + message);
}
});
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} finally {
//不能关闭通道和链接,如果一旦关闭可能会造成接收时抛出异常,或无法接收到消息
// if(channel != null){
// try {
// channel.close();
// } catch (IOException e) {
// e.printStackTrace();
// } catch (TimeoutException e) {
// e.printStackTrace();
// }
// }
// if(connection != null){
// try {
// connection.close();
// } catch (IOException e) {
// e.printStackTrace();
// }
// }
}
}
}
运行程序,从RabbitMQ管理控制台查看消息,如下,之后再刷新查看,消息就为0条
注:
1、Queue的消息只能被同一个消费者消费,如果没有消费监听队列那么消息会存放到队列中持久化保存,直到有消费者来消费这个消息,如果已有消费者监听队列则立即消费发送到队列中的消息
2、Queue的消息可以保证每个消息都一定能被消费
1、direct交换机
发送消息
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 发送消息,direct交换机(完全匹配、单播的模式)
*
* 消息中的路由键(routing key)如果和 Binding 中的 binding key 一致,
* 交换器就将消息发到对应的队列中。路由键与队列名完全匹配,
* 如果一个队列绑定到交换机要求路由键为“dog”,则只转发 routing key 标记为“dog”的消息,不会转发“dog.puppy”,也不会转发“dog.guard”等等
*/
public class DirectSend {
public static void main(String[] args) {
//创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
/**
* 配置RabbitMQ连接信息
*/
//指定IP
connectionFactory.setHost("192.168.133.129");
//指定端口(控制台端口15672)
connectionFactory.setPort(5672);
//指定账号
connectionFactory.setUsername("root");
//指定密码
connectionFactory.setPassword("root");
//定义连接
Connection connection = null;
//定义通道
Channel channel = null;
try {
//获取连接
connection = connectionFactory.newConnection();
//获取通道
channel = connection.createChannel();
/**
* 声明一个队列
* 参数1 队列名称取值任意
* 参数2 是否为持久化的队列
* 参数3 是否排外,如果排外这个队列只允许一个消费者监听
* 参数4 是否自动删除队列,如果为true表示当前队列中没有消息,也没有消费者连接时就会自动删除这个队列
* 参数5 队列的一些属性设置,通常为null
*
* 注:
* 1、声明队列时,这个队列名称如果已经存在则放弃声明,如果队列不存在则会声明一个新的队列
* 2、队列名可以取值任意,但是要与消息接收时的队列名称完全一致
* 3、一定要在发送消息前确认队列名已经存在在RabbitMQ中,否则就会出现问题(如下代码可有可无)
*/
String queueName = "myDirectQueue";
channel.queueDeclare(queueName,true,false,false,null);
/**
* 声明一个交换机
* 参数1 交换机名称
* 参数2 交换机类型,取值:direct,fanout,topic,headers
* 参数3 为是否为持久化交换机
*
* 注:
* 1、声明交换机时如果这个交换机应存在则会放弃声明,如果交换机不存在则声明交换机
* 2、在使用前必须要确保这个交换机被声明(如下代码可有可无)
*/
String exchageName = "directExchange";
String exchageType = "direct";
channel.exchangeDeclare(exchageName,exchageType,true);
/**
* 将队列绑定到交换机
* 参数1 队列名称
* 参数2 交换机名称
* 参数3 消息的RoutingKey(也是BindingKey)
*
* 注:在进行队列和交换机绑定时必须确保队列和交换机已经成功声明
*/
String routingkey = "directRoutingKey";
channel.queueBind(queueName,exchageName,routingkey);
String message="direct交换机的消息!";
/**
* 发送消息到指定队列
* 参数1 交换机名称
* 参数2 消息的RoutingKey,如果这个消息的RoutingKey和某个队列与交换机绑定的RoutingKey一致,
* 那么这个消息就会发送的指定的队列中
* 参数3 消息属性信息,通常为null
* 参数4 具体的消息数据的字节数组
*
* 注:发送消息时必须确保交换机已经创建,并且已经绑定到了某个队列
*/
channel.basicPublish(exchageName,routingkey,null,message.getBytes("UTF-8"));
System.out.println("direct交换机消息发送成功!");
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} finally {
if(channel!=null){
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
if(connection!=null){
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
运行程序,从RabbitMQ管理控制台查看消息,如下
接收消息
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 接收消息,direct交换机
*/
public class DirectReceive {
public static void main(String[] args) {
//创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
/**
* 配置RabbitMQ连接信息
*/
//指定IP
connectionFactory.setHost("192.168.133.129");
//指定端口(控制台端口15672)
connectionFactory.setPort(5672);
//指定账号
connectionFactory.setUsername("root");
//指定密码
connectionFactory.setPassword("root");
//定义连接
Connection connection = null;
//定义通道
Channel channel = null;
try {
//获取连接
connection = connectionFactory.newConnection();
//获取通道
channel = connection.createChannel();
/**
* 声明一个队列
* 参数1 队列名称取值任意
* 参数2 是否为持久化的队列
* 参数3 是否排外,如果排外这个队列只允许一个消费者监听
* 参数4 是否自动删除队列,如果为true表示当前队列中没有消息,也没有消费者连接时就会自动删除这个队列
* 参数5 队列的一些属性设置,通常为null
*
* 注:
* 1、声明队列时,这个队列名称如果已经存在则放弃声明,如果队列不存在则会声明一个新的队列
* 2、队列名可以取值任意,但是要与消息接收时的队列名称完全一致
* 3、一定要在发送消息前确认队列名已经存在在RabbitMQ中,否则就会出现问题(如下代码可有可无)
*/
String queueName = "myDirectQueue";
channel.queueDeclare(queueName,true,false,false,null);
/**
* 声明一个交换机
* 参数1 交换机名称
* 参数2 交换机类型,取值:direct,fanout,topic,headers
* 参数3 为是否为持久化交换机
*
* 注:
* 1、声明交换机时如果这个交换机应存在则会放弃声明,如果交换机不存在则声明交换机
* 2、在使用前必须要确保这个交换机被声明(如下代码可有可无)
*/
String exchageName = "directExchange";
String exchageType = "direct";
channel.exchangeDeclare(exchageName,exchageType,true);
/**
* 将队列绑定到交换机
* 参数1 队列名称
* 参数2 交换机名称
* 参数3 消息的RoutingKey(也是BindingKey)
*
* 注:在进行队列和交换机绑定时必须确保队列和交换机已经成功声明
*/
String routingkey = "directRoutingKey";
channel.queueBind(queueName,exchageName,routingkey);
/**
* 监听某个队列并获取队列中的数据
* 参数1 当前消费者监听的队列名(接收消息的队列名必须要和发送消息的队列名完全保持一致,否则接收不到消息)
* 参数2 消息是否自动确认;true表示自动确认接收,且接收完消息以后会自动将消息从队列中移除
* 参数3 消息接收者标签,用于多个消费者同时监听一个队列时,用于确认不同的消费者,通常为空字符串
* 参数4 消息接收的回调方法,完成对消息的处理
*
* 注:当前被监听的队列必须已经存在,并绑定到了某个交换机中
*/
channel.basicConsume(queueName,true,"",new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body);
System.out.println("direct消费者----" + message);
}
});
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} finally {
}
}
}
注:
1、使用direct消息模式时必须要指定RoutingKey(路由键),将指定的消息绑定到指定的路由键上
2、使用Exchange的direct模式时接收者的RoutingKey必须要与发送时的RoutingKey完全一致否则无法获取消息
3、接收消息时队列名也必须要发送消息时的完全一致
2、fanout交换机
发送消息
/**
* 发送消息,fanout交换机(一对多,广播)
*
* 注:
* 1、Fanout模式必须先启动消息接收者队列监听消息,否则会丢失消息
* 2、使用fanout模式获取消息时不需要绑定特定的队列名称,只需使用channel.queueDeclare().getQueue();
* 获取一个随机的队列名称,然后绑定到指定的Exchange即可获取消息。
* 3、这种模式中可以同时启动多个接收者只要都绑定到同一个Exchange即可
* 让所有接收者同时接收同一个消息,是一种广播的消息机制
*/
public class FanoutSend {
public static void main(String[] args) {
//创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
/**
* 配置RabbitMQ连接信息
*/
//指定IP
connectionFactory.setHost("192.168.133.129");
//指定端口(控制台端口15672)
connectionFactory.setPort(5672);
//指定账号
connectionFactory.setUsername("root");
//指定密码
connectionFactory.setPassword("root");
//定义连接
Connection connection = null;
//定义通道
Channel channel = null;
try {
//获取连接
connection = connectionFactory.newConnection();
//获取通道
channel = connection.createChannel();
/**
* 声明一个交换机
* 参数1 交换机名称
* 参数2 交换机类型,取值:direct,fanout,topic,headers
* 参数3 为是否为持久化交换机
*
* 由于使用Fanout类型的交换机,因此消息的接收方可能会有多个因此不建议在消息发送时来创建队列
* 以及绑定交换机,建议在消费者中创建队列并绑定交换机
* 但是发送消息时至少应该确保交换机存在
*/
String exchangeName = "fanoutExchange";
String exchangeType = "fanout";
channel.exchangeDeclare(exchangeName,exchangeType,true);
// channel.queueDeclare("fanoutQueue",true,false,false,null);
// channel.queueBind("fanoutQueue","fanoutExchange","");
String message="fanout的测试消息!";
/**
* 发送消息到指定队列
* 参数1 交换机名称
* 参数2 消息的RoutingKey,如果这个消息的RoutingKey和某个队列与交换机绑定的RoutingKey一致,
* 那么这个消息就会发送的指定的队列中
* 参数3 消息属性信息,通常为null
* 参数4 具体的消息数据的字节数组
*
* 注:发送消息时必须确保交换机已经创建,并且已经绑定到了某个队列
*/
channel.basicPublish(exchangeName,"",null,message.getBytes("UTF-8"));
System.out.println("fanout交换机消息发送成功!");
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} finally {
if(channel!=null){
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
if(connection!=null){
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
接收消息,可以有多个接收消息者,因此可以复制多个 FanoutReceive 类
/**
* 接收消息,fanout交换机
*
* 注:
* 1、Fanout模式必须先启动消息接收者队列监听消息,否则会丢失消息
* 2、使用fanout模式获取消息时不需要绑定特定的队列名称,只需使用channel.queueDeclare().getQueue();
* 获取一个随机的队列名称,然后绑定到指定的Exchange即可获取消息。
* 3、这种模式中可以同时启动多个接收者只要都绑定到同一个Exchange即可
* 让所有接收者同时接收同一个消息,是一种广播的消息机制
*/
public class FanoutReceive {
public static void main(String[] args) {
//创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
/**
* 配置RabbitMQ连接信息
*/
//指定IP
connectionFactory.setHost("192.168.133.129");
//指定端口(控制台端口15672)
connectionFactory.setPort(5672);
//指定账号
connectionFactory.setUsername("root");
//指定密码
connectionFactory.setPassword("root");
//定义连接
Connection connection = null;
//定义通道
Channel channel = null;
try {
//获取连接
connection = connectionFactory.newConnection();
//获取通道
channel = connection.createChannel();
/**
* 由于Fanout类型的交换机的消息时类似于广播的模式,它不需要绑定RoutingKey
* 而又可能会有很多个消费来接收这个交换机中的数据,因此创建队列时要创建一个随机的队列
*
* 没有参数的channel.queueDeclare()方法会创建一个名字为随机的一个队列
* 这个队列的数据
* 非持久化
* 排外(同时最多只允许有一个消费者监听当前队列)
* 自动删除(当没有任何消费者监听队列时这个队列会自动删除)
*
* getQueue() 方法用于获取这个随机的队列名
*/
String queueName = channel.queueDeclare().getQueue();
String exchangeName = "fanoutExchange";
String exchangeType = "fanout";
channel.exchangeDeclare(exchangeName,exchangeType,true);
//将这个随机的队列绑定到交换机中, 由于是fanout类型的交换机因此不需指定RoutingKey进行绑定
channel.queueBind(queueName,exchangeName,"");
//接收消息
channel.basicConsume(queueName,true,"",new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body);
System.out.println("fanout消费者----" + message);
}
});
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
注:
1、fanout模式的消息需要将一个消息同时绑定到多个队列中因此这里不能创建并指定某个队列
2、Fanout模式必须先启动消息接收者队列监听消息,否则会丢失消息
3、使用fanout模式获取消息时不需要绑定特定的队列名称,只需使用channel.queueDeclare().getQueue();获取一个随机的队列名称,然后绑定到指定的Exchange即可获取消息。
4、这种模式中可以同时启动多个接收者只要都绑定到同一个Exchang即可让所有接收者同时接收同一个消息是一种广播的消息机制
3、topic交换机
发送消息
/**
* 发送消息,topic交换机(一对多)
*
* Topic模式的消息接收时必须要指定RoutingKey并且可以使用# 和 *来做统配符号,
* #表示通配任意一个单词 *表示通配任意多个单词,
* 例如消费者的RoutingKey为test.#或#.myRoutingKey都可以获取RoutingKey为test.myRoutingKey发送者发送的消息
*/
public class TopicSend {
public static void main(String[] args) {
//创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
/**
* 配置RabbitMQ连接信息
*/
//指定IP
connectionFactory.setHost("192.168.133.129");
//指定端口(控制台端口15672)
connectionFactory.setPort(5672);
//指定账号
connectionFactory.setUsername("root");
//指定密码
connectionFactory.setPassword("root");
//定义连接
Connection connection = null;
//定义通道
Channel channel = null;
try {
//获取连接
connection = connectionFactory.newConnection();
//获取通道
channel = connection.createChannel();
/**
* 声明一个交换机
* 参数1 交换机名称
* 参数2 交换机类型,取值:direct,fanout,topic,headers
* 参数3 为是否为持久化交换机
*
* 由于使用topic类型的交换机,因此消息的接收方可能会有多个因此不建议在消息发送时来创建队列
* 以及绑定交换机,建议在消费者中创建队列并绑定交换机
* 但是发送消息时至少应该确保交换机存在
*/
String exchangeName = "topicExchange";
String exchangeType = "topic";
channel.exchangeDeclare(exchangeName,exchangeType,true);
String message="topic的测试消息aa.bb.cc!";
/**
* 发送消息到指定队列
* 参数1 交换机名称
* 参数2 消息的RoutingKey,如果这个消息的RoutingKey和某个队列与交换机绑定的RoutingKey一致,
* 那么这个消息就会发送的指定的队列中
* 参数3 消息属性信息,通常为null
* 参数4 具体的消息数据的字节数组
*
* Topic模式的消息接收时必须要指定RoutingKey并且可以使用# 和 *来做统配符号,
* #表示通配任意一个单词 *表示通配任意多个单词,
* 例如消费者的RoutingKey为test.#或#.myRoutingKey都可以获取RoutingKey为test.myRoutingKey发送者发送的消息
* aa;aa.bb;aa.bb.cc
*
* 注:发送消息时必须确保交换机已经创建,并且已经绑定到了某个队列
*/
channel.basicPublish(exchangeName,"aa.bb.cc",null,message.getBytes("UTF-8"));
System.out.println("topic交换机消息发送成功!");
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} finally {
if(channel!=null){
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
if(connection!=null){
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
接收消息,接收消息可以有多个类
/**
* 接收消息,topic交换机
*/
public class TopicReceive {
public static void main(String[] args) {
//创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
/**
* 配置RabbitMQ连接信息
*/
//指定IP
connectionFactory.setHost("192.168.133.129");
//指定端口(控制台端口15672)
connectionFactory.setPort(5672);
//指定账号
connectionFactory.setUsername("root");
//指定密码
connectionFactory.setPassword("root");
//定义连接
Connection connection = null;
//定义通道
Channel channel = null;
try {
//获取连接
connection = connectionFactory.newConnection();
//获取通道
channel = connection.createChannel();
/**
* Topic 类型的交换机也是消息一对多的一种交换机类型,它和fanout都能实现一个消息同时发送给多个队列
*
* fanout更适合于使用在一个功能不同的进程来获取数据,例如手机App中的消息推送,一个App可能会还有很
* 多个用户来进行安装然后他们都会启动一个随机的队列来接收着自己的数据
*
* Topic更适合不同的功能模块来接收同一个消息,例如商城下单成功以后需要发送消息到队列中。例如RoutingKey
* 为 的order.success,物流系统监听订单order.* 发票系统监听order.*
*
* Topic可以使用随机的队列名也可以使用一个明确的队列名,但是如果应用在和订单有关的功能中,建议是有个
* 名取的队列名并且要求为持久化的队列
*/
//声明队列
String queueName = "topicQueue";
channel.queueDeclare(queueName,true,false,false,null);
//声明交换机
String exchageName = "topicExchange";
String exchageType = "topic";
channel.exchangeDeclare(exchageName,exchageType,true);
//绑定队列和交换机
channel.queueBind(queueName,exchageName,"aa");
//接收消息
channel.basicConsume(queueName,true,"",new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message=new String(body);
System.out.println("TopicReceive消费者aa ---" + message);
}
});
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
注:
1、在topic模式中必须要指定Routingkey,并且可以同时指定多层的RoutingKey,每个层次之间使用 点分隔即可 例如 test.myRoutingKey
2、Topic模式的消息接收时必须要指定RoutingKey并且可以使用# 和 *来做统配符号,#表示通配任意一个单词 *表示通配任意多个单词,例如消费者的RoutingKey为test.#或#.myRoutingKey都可以获取RoutingKey为test.myRoutingKey发送者发送的消息
事务消息与数据库的事务类似,MQ中的消息是要保证消息是否会全部发送成功,防止丢失消息的一种策略。
RabbitMQ有两种方式:
1、通过AMQP提供的事务机制实现
2、使用发送者确认模式实现
事务的实现主要是对信道(Channel)的设置,主要的方法有三个:
channel.txSelect()声明启动事务模式
channel.txCommint()提交事务
channel.txRollback()回滚事务
消息发送类
/**
* 事务发送消息
*
* 事务消息与数据库的事务类似,只是MQ中的消息是要保证消息是否会全部发送成功,防止丢失消息的一种策略
* RabbitMQ有两种方式来解决这个问题:
* 1、通过AMQP提供的事务机制实现;
* 2、使用发送者确认模式实现;
*
* 事务的实现主要是对信道(Channel)的设置,主要的方法有三个:
* 1、channel.txSelect()声明启动事务模式;
* 2、channel.txCommint()提交事务;
* 3、channel.txRollback()回滚事务;
*/
public class TransactionSend {
public static void main(String[] args) {
//创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
/**
* 配置RabbitMQ连接信息
*/
//指定IP
connectionFactory.setHost("192.168.133.129");
//指定端口(控制台端口15672)
connectionFactory.setPort(5672);
//指定账号
connectionFactory.setUsername("root");
//指定密码
connectionFactory.setPassword("root");
//定义连接
Connection connection = null;
//定义通道
Channel channel = null;
try {
//获取连接
connection = connectionFactory.newConnection();
//获取通道
channel = connection.createChannel();
//声明队列
String queueName = "transactionQueue";
channel.queueDeclare(queueName,true,false,false,null);
//声明交换机
String exchangeName = "transactionExchange";
String exchangeType = "direct";
channel.exchangeDeclare(exchangeName,exchangeType,true);
//绑定队列和交换机
String routingKey = "transactionRoutingKey";
channel.queueBind(queueName,exchangeName,routingKey);
String message="事务的测试消息的测试消息!";
//启动一个事务,启动事务以后所有消息写入到队列中;必须显示的调用 txCommit()提交事务或txRollback()回滚事务
channel.txSelect();
//发送消息
channel.basicPublish(exchangeName,routingKey,null,message.getBytes("UTF-8"));
//提交事务,如果调用txSelect()方法启动了事务,必须显示调用事务的提交;否则消息不会真正的写入到队列,提交时以后会将内存中的消息写入队列并释放内存
channel.txCommit();
System.out.println("事务消息发送成功");
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} finally {
if(channel!=null){
try {
//回滚事务,放弃当前事务中所有没有提交的消息,释放内存
channel.txRollback();
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
if(connection!=null){
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
消息接收类
/**
* 事务接收消息
*/
public class TransactionReceive {
public static void main(String[] args) {
//创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
/**
* 配置RabbitMQ连接信息
*/
//指定IP
connectionFactory.setHost("192.168.133.129");
//指定端口(控制台端口15672)
connectionFactory.setPort(5672);
//指定账号
connectionFactory.setUsername("root");
//指定密码
connectionFactory.setPassword("root");
//定义连接
Connection connection = null;
//定义通道
Channel channel = null;
try {
//获取连接
connection = connectionFactory.newConnection();
//获取通道
channel = connection.createChannel();
//声明队列
String queueName = "transactionQueue";
channel.queueDeclare(queueName,true,false,false,null);
//声明交换机
String exchangeName = "transactionExchange";
String exchangeType = "direct";
channel.exchangeDeclare(exchangeName,exchangeType,true);
//绑定队列和交换机
String routingKey = "transactionRoutingKey";
channel.queueBind(queueName,exchangeName,routingKey);
/**
* 开启事务
* 当消费者开启事务以后,即使不作为事务的提交,那么依然可以获取队列中的消息并且将消息从队列中移除掉
* 注:
* 暂时事务队列接收者没有任何的影响
*/
channel.txSelect();
channel.basicConsume(queueName,true,"",new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body);
System.out.println("事务消费者 ----" + message);
}
});
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
消息确认有三种方式
1、channel.waitForConfirms()普通发送方确认模式
/**
* Confirm的三种实现方式:
* 方式一:channel.waitForConfirms()普通发送方确认模式
*/
public class WaitForConfirmsSend {
public static void main(String[] args) {
//创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
/**
* 配置RabbitMQ连接信息
*/
//指定IP
connectionFactory.setHost("192.168.133.129");
//指定端口(控制台端口15672)
connectionFactory.setPort(5672);
//指定账号
connectionFactory.setUsername("root");
//指定密码
connectionFactory.setPassword("root");
//定义连接
Connection connection = null;
//定义通道
Channel channel = null;
try {
//获取连接
connection = connectionFactory.newConnection();
//获取通道
channel = connection.createChannel();
//声明队列
String queueName = "confirmsQueue";
channel.queueDeclare(queueName,true,false,false,null);
//声明交换机
String exchangeName = "confirmsExchange";
String exchangeType = "direct";
channel.exchangeDeclare(exchangeName,exchangeType,true);
//绑定队列和交换机
String routingKey = "confirmsRoutingKey";
channel.queueBind(queueName,exchangeName,routingKey);
String message="普通发送者确认模式waitForConfirms的测试消息!";
//启动普通发送者确认模式
channel.confirmSelect();
channel.basicPublish(exchangeName,routingKey,null,message.getBytes("UTF-8"));
/*
阻塞线程等待服务返回响应 ,用于是否消费发送成功,如果服务确认消费已经发送完成则返回true 否则返回false
可以为这个方法指定一个毫秒用于确定我们的需要等待服务确认的超时时间,
如果超过了指定的时间以后则会抛出异常InterruptedException 表示服务器出现问题了需要补发消息或
将消息缓存到Redis中稍后利用定时任务补发
无论是返回false还是抛出异常消息都有可能发送成功有可能没有发送成功
如果我们要求这个消息一定要发送到队列例如订单数据,那怎么我们可以采用消息补发(重新发送一次消息,可以使用递归或利用Redis+定时任务来完成补发)
* */
boolean flag = channel.waitForConfirms();
System.out.println("消息发送成功" + flag);
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if(channel!=null){
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
if(connection!=null){
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
2、channel.waitForConfirmsOrDie()批量确认模式
/**
* Confirm的三种实现方式:
* 方式二:channel.waitForConfirmsOrDie()批量确认模式
*/
public class WaitForConfirmsOrDieSend {
public static void main(String[] args) {
//创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
/**
* 配置RabbitMQ连接信息
*/
//指定IP
connectionFactory.setHost("192.168.133.129");
//指定端口(控制台端口15672)
connectionFactory.setPort(5672);
//指定账号
connectionFactory.setUsername("root");
//指定密码
connectionFactory.setPassword("root");
//定义连接
Connection connection = null;
//定义通道
Channel channel = null;
try {
//获取连接
connection = connectionFactory.newConnection();
//获取通道
channel = connection.createChannel();
//声明队列
String queueName = "confirmsQueue";
channel.queueDeclare(queueName,true,false,false,null);
//声明交换机
String exchangeName = "confirmsExchange";
String exchangeType = "direct";
channel.exchangeDeclare(exchangeName,exchangeType,true);
//绑定队列和交换机
String routingKey = "confirmsRoutingKey";
channel.queueBind(queueName,exchangeName,routingKey);
String message="普通发送者确认模式waitForConfirms的测试消息!";
//启动普通发送者确认模式
channel.confirmSelect();
channel.basicPublish(exchangeName,routingKey,null,message.getBytes("UTF-8"));
/**
* waitForConfirmsOrDie 批量消息确认,它会同时向服务中确认之前当前通道中发送的所有的消息是否已经全部成功写入
* 这个方法没有任何的返回值,如果服务器中有一条消息没有能够成功或向服务器发送确认时服务不可访问都被认定为
* 消息确认失败,可能有消息没有发送成功,我们需要进行消费的补发。
* 如果无法向服务器获取确认信息那么方法就会抛出InterruptedException异常,这时就需要补发消息到队列
* waitForConfirmsOrDie方法可以指定一个参数timeout 用于等待服务器的确认时间,如果超过这个时间也会
* 抛出异常,表示确认失败需要补发消息
*
* 注:
* 批量消息确认的速度比普通的消息确认要快,但是如果一旦出现了消息补发的情况,我们不能确定具体
* 是哪条消息没有完成发送,需要将本次的发送的所有消息全部进行补发
*
*/
channel.waitForConfirmsOrDie();
System.out.println("消息发送成功");
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if(channel!=null){
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
if(connection!=null){
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
3、channel.addConfirmListener()异步监听发送方确认模式
/**
* Confirm的三种实现方式:
* 方式三:channel.addConfirmListener()异步监听发送方确认模式
*/
public class AddConfirmListenerSend {
public static void main(String[] args) {
//创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
/**
* 配置RabbitMQ连接信息
*/
//指定IP
connectionFactory.setHost("192.168.133.129");
//指定端口(控制台端口15672)
connectionFactory.setPort(5672);
//指定账号
connectionFactory.setUsername("root");
//指定密码
connectionFactory.setPassword("root");
//定义连接
Connection connection = null;
//定义通道
Channel channel = null;
try {
//获取连接
connection = connectionFactory.newConnection();
//获取通道
channel = connection.createChannel();
//声明队列
String queueName = "confirmsQueue";
channel.queueDeclare(queueName, true, false, false, null);
//声明交换机
String exchangeName = "confirmsExchange";
String exchangeType = "direct";
channel.exchangeDeclare(exchangeName, exchangeType, true);
//绑定队列和交换机
String routingKey = "confirmsRoutingKey";
channel.queueBind(queueName, exchangeName, routingKey);
String message = "普通发送者确认模式waitForConfirms的测试消息!";
//启动普通发送者确认模式
channel.confirmSelect();
/**
* 异步消息确认监听器,需要在发送消息前启动
*/
channel.addConfirmListener(new ConfirmListener() {
/**
消息确认以后的回调方法
参数 1 为被确认的消息的编号 从 1 开始自动递增用于标记当前是第几个消息
参数 2 为当前消息是否同时确认了多个
注:如果参数 2 为true 则表示本次确认同时确认了多条消息,消息等于当前参数1(消息编号)的所有消息
全部被确认 如果为false 则表示只确认多了当前编号的消息
* */
public void handleAck(long l, boolean b) throws IOException {
System.out.println("消息被确认了 --- 消息编号:" + l +" 是否确认了多条:" + b);
}
/**
消息没有确认的回调方法
如果这个方法被执行表示当前的消息没有被确认 需要进行消息补发
参数 1 为没有被确认的消息的编号 从 1 开始自动递增用于标记当前是第几个消息
参数 2 为当前消息是否同时没有确认多个
注: 如果参数2 为true 则表示小于当前编号的所有的消息可能都没有发送成功需要进行消息的补发
如果参数2 为false则表示当前编号的消息没法发送成功需要进行补发
*/
public void handleNack(long l, boolean b) throws IOException {
System.out.println("消息没有被确认-----消息编号:" + l + " 是否没有确认多条:" + b);
}
});
for(int i = 0 ;i < 1000; i++){
channel.basicPublish(exchangeName,routingKey,null,message.getBytes("UTF-8"));
}
System.out.println("消息发送成功");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} finally {
//在发批量多条消息时,关闭会导致消息编号错误
// if(channel!=null){
// try {
// channel.close();
// } catch (IOException e) {
// e.printStackTrace();
// } catch (TimeoutException e) {
// e.printStackTrace();
// }
// }
// if(connection!=null){
// try {
// connection.close();
// } catch (IOException e) {
// e.printStackTrace();
// }
// }
}
}
}
消息接收
/**
* 确认模式接收消息
*/
public class ConfirmReceive {
public static void main(String[] args) {
//创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
/**
* 配置RabbitMQ连接信息
*/
//指定IP
connectionFactory.setHost("192.168.133.129");
//指定端口(控制台端口15672)
connectionFactory.setPort(5672);
//指定账号
connectionFactory.setUsername("root");
//指定密码
connectionFactory.setPassword("root");
//定义连接
Connection connection = null;
//定义通道
Channel channel = null;
try {
//获取连接
connection = connectionFactory.newConnection();
//获取通道
channel = connection.createChannel();
//声明队列
String queueName = "confirmsQueue";
channel.queueDeclare(queueName,true,false,false,null);
//声明交换机
String exchangeName = "confirmsExchange";
String exchangeType = "direct";
channel.exchangeDeclare(exchangeName,exchangeType,true);
//绑定队列和交换机
String routingKey = "confirmsRoutingKey";
channel.queueBind(queueName,exchangeName,routingKey);
//启动事务
channel.txSelect();
/**
* 接收消息
* 参数 2 为消息的确认机制,true表示自动消息确认,确认以后消息会从队列中被移除 ,当读取完消息以后就会自动确认
* 如果为false 表示手动确认消息
* 注:
* 如果我们只是接收的消息但是还没有来得处理,当前应用就崩溃或在进行处理的时候例如像数据库中
* 写数据但是数据库这时不可用,那么由于消息是自动确认的那么这个消息就会在接收完成以后自动从队列中
* 被删除,这就会丢失消息
*/
channel.basicConsume(queueName,false,"",new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//获取当前当前消息是否被接收过一次如果返回值为false表示消息之前没有被接收过,如果返回值为true
//则表示之前这个消息被接收过,可能也处理完成,因此我们要进行消息的防重复处理
Boolean isReceived = envelope.isRedeliver();
//获取当前内部类中的通道
Channel chan = this.getChannel();
if (!isReceived){
String message = new String(body);
System.out.println("确认模式消费者 处理了消息---"+message);
//获取消息的编号,我们需要根据消息的编号来确认消息
long tag = envelope.getDeliveryTag();
/**
手动确认消息,确认以后表示当前消息已经成功处理了,需要从队列中移除掉
这个方法应该在当前消息的处理程序全部完成以后执行
参数 1 为消息的序号
参数 2 为是否确认多个,如果为true则表示需要确认小等于当前编号的所有消息,false就是单个确认值确认当前消息
* */
chan.basicAck(tag,true);
} else {
/*
这个消息之前已经被接收过需要进行防重复处理
如查询数据库中是否已经添加了记录或已经修改过了记录
如果经过判断这条没有被处理完成则需要重新处理消息然后确认掉这条消息
如果已经处理过了则直接确认消息即可不需要进行其他处理操作
* */
//获取消息的编号,我们需要根据消息的编号来确认消息
long tag = envelope.getDeliveryTag();
chan.basicAck(tag,false);
}
//注:如果启动了事务,而消息消费者确认模式为手动确认,那么必须要提交事务否则即使调用了确认方法
//那么消息也不会从队列中被移除掉
chan.txCommit();
}
});
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}