官方文档写的很详细:官网:RabbitMQ Tutorials — RabbitMQ
导入 amqp-client 依赖
<dependency>
<groupId>com.rabbitmqgroupId>
<artifactId>amqp-clientartifactId>
<version>5.10.0version>
dependency>
单个消费端情况
新建一个连接工具类:
public class ConnectionUtil {
public static Connection getConnection() throws IOException, TimeoutException {
// 所以的中间件技术都是基于 tcp/ip 协议 或者 是在此协议基础之上构建新型的协议规范,rabbitmq 遵循的是 AMQP 协议
// 既然都是以 tcp/ip 协议为基础的,那么都需要设置 ip 和端口port
// 1:创建连接工厂 并配置信息
ConnectionFactory connectionFactory = new ConnectionFactory();
//配置属性
connectionFactory.setHost("xxxx");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
connectionFactory.setPassword("xxxx");
connectionFactory.setVirtualHost("/");//将所有消息发到根目录节点
Connection connection = connectionFactory.newConnection();
return connection;
}
}
生产者发送消息:
public class Send {
public static final String QUEUE_NAME="simple_queue";
public static void main(String[] args) throws IOException, TimeoutException {
// 1.获取连接
Connection connection = ConnectionUtil.getConnection();
// 2.创建通道
Channel channel = connection.createChannel();
// 3.声明队列
/**
* 参数明细
* 1、queue 队列名称
* 2、durable 是否持久化,如果持久化,mq重启后队列还在
* 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
* 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
* 5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
*/
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
// 4.发送消息
String message = "Hello Rabbit!";
// 向指定的队列中发送消息
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
// 5.关闭通道
channel.close();
// 6.关闭连接
connection.close();
}
}
消费者接收消息:
public class Recv {
private final static String QUEUE_NAME = "simple_queue";
public static void main(String[] argv) throws IOException, TimeoutException {
// 1.获取到连接
Connection connection = ConnectionUtil.getConnection();
// 2.创建会话通道,生产者和mq服务所有通信都在channel通道中完成
Channel channel = connection.createChannel();
// 3.声明队列
//参数:String queue, boolean durable, boolean exclusive, boolean autoDelete, Map arguments
/**
* 参数明细
* 1、queue 队列名称
* 2、durable 是否持久化,如果持久化,mq重启后队列还在
* 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
* 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
* 5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
*/
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 4.监听消息
DefaultConsumer consumer = new DefaultConsumer(channel){
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
/**
* 当接收到消息后此方法将被调用
* @param consumerTag 消费者标签,用来标识消费者的,在监听队列时设置channel.basicConsume
* @param envelope 信封,通过envelope
* @param properties 消息属性
* @param body 消息内容
* @throws IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException, UnsupportedEncodingException {
//交换机
String exchange = envelope.getExchange();
//消息id,mq在channel中用来标识消息的id,可用于确认消息已接收
long deliveryTag = envelope.getDeliveryTag();
// body 即消息体
String msg = new String(body,"utf-8");
System.out.println(" [x] received : " + msg + "!");
}
};
// 监听队列,第二个参数:是否自动进行消息确认。
//参数:String queue, boolean autoAck, Consumer callback
/**
* 参数明细:
* 1、queue 队列名称
* 2、autoAck 自动回复,当消费者接收到消息后要告诉mq消息已接收,如果将此参数设置为true表示会自动回复mq,如果设置为false要通过编程实现回复
* 3、callback,消费方法,当消费者接收到消息要执行的方法
*/
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
消息被消耗了
多个消费端情况
工作队列或者竞争消费者模式
两个消费端竞争获取生产者发布的消息,假设有一个消费端处理速度较慢来模拟实际环境。
轮询模式:一个消费者一条,按均分配
生产者循环发送50条消息:
public class Send {
public static final String QUEUE_NAME="simple_queue";
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
// 1.获取连接
Connection connection = ConnectionUtil.getConnection();
// 2.创建通道
Channel channel = connection.createChannel();
// 3.声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
for (int i = 0;i < 50;i++){
// 4.发送消息
String message = "Hello Rabbit!";
// 向指定的队列中发送消息
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
Thread.sleep(i * 2);
}
// 5.关闭通道
channel.close();
// 6.关闭连接
connection.close();
}
}
处理速度较慢的消费者:
public class Recv {
private final static String QUEUE_NAME = "simple_queue";
public final static int i = 0;
public static void main(String[] argv) throws IOException, TimeoutException {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException, UnsupportedEncodingException {
String exchange = envelope.getExchange();
long deliveryTag = envelope.getDeliveryTag();
String msg = new String(body,"utf-8");
System.out.println(" [消费者1] received : " + msg + "!"+"["+(i+1)+"]");
try {
TimeUnit.SECONDS.sleep(1);// 增加耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
提前启动两个消费端,发现,两个消费端都是收到了25条消息。
公平分发:根据消费者的消费能力进行公平分发,处理快的处理的多,处理慢的处理的少;按劳分配
在生产者声明队列的时候,修改队列的参数:
channel.basicQos(1);//同一时刻,消费者只会推送一条消息给消费者
这里在消费端自动ACK 的情况下测试,发现依旧是轮询模式。
注意:只有在 消费端手动ACK 的情况下,公平分发模式才有效。
可以看到,高速度的消费端接收了46个消息。
交换机(Exchanges):
Exchange 交换机有以下几种类型(接下来三种消息模式分别使用以下前三种交换机):
Fanout:广播,将消息交给所有绑定到交换机的队列
Direct:定向,把消息交给符合指定routing key 的队列
Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列
Header:header模式与routing不同的地方在于,header模式取消routingkey,使用header中的 key/value(键值对)匹配队列。
生产者:
public class Send {
public static final String EXCHANGE_NAME="fanout_exchange";
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
// 1.获取连接
Connection connection = ConnectionUtil.getConnection();
// 2.创建通道
Channel channel = connection.createChannel();
// 3.声明交互机
channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
// 4.发送消息
String message = "fanout_exchange send 'hello rabbit!'";
channel.basicPublish(EXCHANGE_NAME,"",null,message.getBytes());
System.out.println("[生产者] send :"+ message);
// 5.关闭通道
channel.close();
// 6.关闭连接
connection.close();
}
}
消费者1、2:
public class Recv {
private final static String QUEUE_NAME = "fanout_queue_01";//消费者2 fanout_queue_02
public static final String EXCHANGE_NAME="fanout_exchange";
public static void main(String[] argv) throws IOException, TimeoutException {
// 1.获取到连接
Connection connection = ConnectionUtil.getConnection();
// 2.创建会话通道,生产者和mq服务所有通信都在channel通道中完成
Channel channel = connection.createChannel();
// 3.声明队列
//参数:String queue, boolean durable, boolean exclusive, boolean autoDelete, Map arguments
/**
* 参数明细
* 1、queue 队列名称
* 2、durable 是否持久化,如果持久化,mq重启后队列还在
* 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
* 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
* 5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
*/
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"");
// 4.监听消息
DefaultConsumer consumer = new DefaultConsumer(channel){
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
/**
* 当接收到消息后此方法将被调用
* @param consumerTag 消费者标签,用来标识消费者的,在监听队列时设置channel.basicConsume
* @param envelope 信封,通过envelope
* @param properties 消息属性
* @param body 消息内容
* @throws IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException, UnsupportedEncodingException {
//交换机
String exchange = envelope.getExchange();
//消息id,mq在channel中用来标识消息的id,可用于确认消息已接收
long deliveryTag = envelope.getDeliveryTag();
// body 即消息体
String msg = new String(body,"utf-8");
System.out.println(" [消费者1] received : " + msg);
channel.basicAck(envelope.getDeliveryTag(),false);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
// 监听队列,第二个参数:是否自动进行消息确认。
//参数:String queue, boolean autoAck, Consumer callback
/**
* 参数明细:
* 1、queue 队列名称
* 2、autoAck 自动回复,当消费者接收到消息后要告诉mq消息已接收,如果将此参数设置为true表示会自动回复mq,如果设置为false要通过编程实现回复
* 3、callback,消费方法,当消费者接收到消息要执行的方法
*/
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
先启动生产者,声明交换机
此时再图形化界面查看交换机,发现并不是像队列一样可以获取到消息,猜测交换机收到的消息如果不能立即发送就会销毁。
此时启动两个消费者,果然没有获取到消息,交换机显示已经绑定了两个队列:
再次启动生产者,成功获取到消息:
Publish/subscribe 与 work消息模式的区别:
上一个模式中的交换机将所有消息广播给所有消费者。我们希望扩展它以允许根据消息的严重性过滤消息。
例如,我们可能希望将日志消息写入磁盘的脚本仅接收严重错误,而不会在警告或信息日志消息上浪费磁盘空间
生产者:
public class Send {
public static final String EXCHANGE_NAME="direct_exchange";
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
// 1.获取连接
Connection connection = ConnectionUtil.getConnection();
// 2.创建通道
Channel channel = connection.createChannel();
// 3.声明交互机
channel.exchangeDeclare(EXCHANGE_NAME,"direct");
// 4.发送消息
String message = "direct_exchange send 'hello rabbit!'";
//发送一条报错信息
channel.basicPublish(EXCHANGE_NAME,"error",null,message.getBytes());
System.out.println("[生产者] send :"+ message);
// 5.关闭通道
channel.close();
// 6.关闭连接
connection.close();
}
}
消费者1(RoutingKey = log):
public class Recv {
private final static String QUEUE_NAME = "direct_queue_log";
public static final String EXCHANGE_NAME="direct_exchange";
public static void main(String[] argv) throws IOException, TimeoutException {
// 1.获取到连接
Connection connection = ConnectionUtil.getConnection();
// 2.创建会话通道,生产者和mq服务所有通信都在channel通道中完成
Channel channel = connection.createChannel();
// 3.声明队列
//参数:String queue, boolean durable, boolean exclusive, boolean autoDelete, Map arguments
/**
* 参数明细
* 1、queue 队列名称
* 2、durable 是否持久化,如果持久化,mq重启后队列还在
* 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
* 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
* 5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
*/
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"log");
// 4.监听消息
DefaultConsumer consumer = new DefaultConsumer(channel){
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
/**
* 当接收到消息后此方法将被调用
* @param consumerTag 消费者标签,用来标识消费者的,在监听队列时设置channel.basicConsume
* @param envelope 信封,通过envelope
* @param properties 消息属性
* @param body 消息内容
* @throws IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException, UnsupportedEncodingException {
//交换机
String exchange = envelope.getExchange();
//消息id,mq在channel中用来标识消息的id,可用于确认消息已接收
long deliveryTag = envelope.getDeliveryTag();
// body 即消息体
String msg = new String(body,"utf-8");
System.out.println(" [log] received : " + msg);
channel.basicAck(envelope.getDeliveryTag(),false);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
// 监听队列,第二个参数:是否自动进行消息确认。
//参数:String queue, boolean autoAck, Consumer callback
/**
* 参数明细:
* 1、queue 队列名称
* 2、autoAck 自动回复,当消费者接收到消息后要告诉mq消息已接收,如果将此参数设置为true表示会自动回复mq,如果设置为false要通过编程实现回复
* 3、callback,消费方法,当消费者接收到消息要执行的方法
*/
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
消费者2(RoutingKey = error):
public class Recv2 {
private final static String QUEUE_NAME = "direct_queue_error";
public static final String EXCHANGE_NAME="direct_exchange";
public static void main(String[] argv) throws IOException, TimeoutException {
// 1.获取到连接
Connection connection = ConnectionUtil.getConnection();
// 2.创建会话通道,生产者和mq服务所有通信都在channel通道中完成
Channel channel = connection.createChannel();
// 3.声明队列
//参数:String queue, boolean durable, boolean exclusive, boolean autoDelete, Map arguments
/**
* 参数明细
* 1、queue 队列名称
* 2、durable 是否持久化,如果持久化,mq重启后队列还在
* 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
* 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
* 5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
*/
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"error");
// 4.监听消息
DefaultConsumer consumer = new DefaultConsumer(channel){
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
/**
* 当接收到消息后此方法将被调用
* @param consumerTag 消费者标签,用来标识消费者的,在监听队列时设置channel.basicConsume
* @param envelope 信封,通过envelope
* @param properties 消息属性
* @param body 消息内容
* @throws IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException, UnsupportedEncodingException {
//交换机
String exchange = envelope.getExchange();
//消息id,mq在channel中用来标识消息的id,可用于确认消息已接收
long deliveryTag = envelope.getDeliveryTag();
// body 即消息体
String msg = new String(body,"utf-8");
System.out.println(" [error] received : " + msg);
channel.basicAck(envelope.getDeliveryTag(),false);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
// 监听队列,第二个参数:是否自动进行消息确认。
//参数:String queue, boolean autoAck, Consumer callback
/**
* 参数明细:
* 1、queue 队列名称
* 2、autoAck 自动回复,当消费者接收到消息后要告诉mq消息已接收,如果将此参数设置为true表示会自动回复mq,如果设置为false要通过编程实现回复
* 3、callback,消费方法,当消费者接收到消息要执行的方法
*/
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
启动生产者 声明交换机:
启动消费者等待消息,显示交换机绑定的队列,同时有 RoutingKey:
再次启动生产者,只有 error 消费端接收到了消息:
上一个模式的 RoutingKey 并不能满足我们的需求,于是我们在 topics 模式中加入了*
、#
等通配符。
发送到主题交换的消息不能具有任意routing_key
- 它必须是单词列表,由点分隔。这些单词可以是任何内容,但通常它们指定与消息相关的一些功能。一些有效的路由关键示例:“stock.usd.nyse
”,“nyse.vmw
”,“quick.orange.rabbit
”。路由密钥中可以有任意数量的单词,最大限制为 255 个字节。
绑定键也必须采用相同的形式。主题交换背后的逻辑类似于直接交换 - 使用特定路由键发送的消息将被传递到使用匹配绑定键绑定的所有队列。但是,绑定键有两种重要的特殊情况:
*
(星号)可以正好代替一个词。#
(哈希)可以代替零个或多个单词。主题交换功能强大,可以像其他交换一样运行。
当队列绑定“#”(哈希)绑定键时 - 它将接收所有消息,而不管路由键如何 - 就像在扇出交换中一样。
当绑定中不使用特殊字符“*”(星号)和“#”(哈希)时,主题交换的行为将类似于直接交换。
在二号队列绑定了两个 RoutingKey:
消费者1 收到了,消费者2没有收到:
修改消息:发送:hot.orange.cat
channel.basicPublish(EXCHANGE_NAME,"hot.orange.cat",null,message.getBytes());
两个都获取到了:
RPC 框架很出名,但是 RabbitMQ 实现的 RPC 模式很少用到,这里简单介绍一下。
我们的 RPC 将按如下方式工作: