文章目录
- 七、实战 RabbitMQ 的六种消息模型
- 1. 基本消息模式
- 2.work消息模式
- 2.1 轮询模式
- 2.2 公平分发模式
- 2.3 订阅模型分类
- 3.Publish/subscribe(交换机类型:Fanout,也称为广播 )
- 4.Routing 路由模型(交换机类型:direct)
- 5.Topics 通配符模式(交换机类型:topics)
- 6.RPC
七、实战 RabbitMQ 的六种消息模型
官方文档写的很详细:官网:RabbitMQ Tutorials — RabbitMQ
导入 amqp-client 依赖
<!--java原生依赖-->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.10.0</version>
</dependency>
1. 基本消息模式
单个消费端情况
- P:生产者:发送消息
- C:消费者:接收消息
- queue:消息队列,图中红色部分。可以缓存消息;生产者向其中投递消息,消费者从其中取出消息。
新建一个连接工具类:
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<String, Object> 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);
}
}
消息被消耗了
2.work消息模式
多个消费端情况
工作队列或者竞争消费者模式
两个消费端竞争获取生产者发布的消息,假设有一个消费端处理速度较慢来模拟实际环境。
2.1 轮询模式
轮询模式:一个消费者一条,按均分配
生产者循环发送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条消息。
2.2 公平分发模式
公平分发:根据消费者的消费能力进行公平分发,处理快的处理的多,处理慢的处理的少;按劳分配
在生产者声明队列的时候,修改队列的参数:
channel.basicQos(1);//同一时刻,消费者只会推送一条消息给消费者
这里在消费端自动ACK 的情况下测试,发现依旧是轮询模式。
注意:只有在 消费端手动ACK 的情况下,公平分发模式才有效。
可以看到,高速度的消费端接收了46个消息。
2.3 订阅模型分类
- 一个生产者,多个消费者
- 每个消费者独占一个队列,每个队列都需要绑定到交换机上
- 由生产者生产的消息发送给交换机
- 由交换机发送个队列,实现一个消息被多个消费者消费
交换机(Exchanges):
- 一方面:接收生产者发送的消息。
- 另一方面:知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。
Exchange 交换机有以下几种类型(接下来三种消息模式分别使用以下前三种交换机):
- Fanout:广播,将消息交给所有绑定到交换机的队列
- Direct:定向,把消息交给符合指定routing key 的队列
- Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列
- Header:header模式与routing不同的地方在于,header模式取消routingkey,使用header中的 key/value(键值对)匹配队列。
3.Publish/subscribe(交换机类型:Fanout,也称为广播 )
- 生产者不声明队列,只声明交换机,将消息发送到交换机
- 消费者声明队列,并将队列绑定到交换机,从而一个消费端对应一条队列
生产者:
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<String, Object> 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消息模式的区别:
- 一个面向交换机发送消息,一个面向队列发送消息(使用的默认交换机)
4.Routing 路由模型(交换机类型:direct)
上一个模式中的交换机将所有消息广播给所有消费者。我们希望扩展它以允许根据消息的严重性过滤消息。
例如,我们可能希望将日志消息写入磁盘的脚本仅接收严重错误,而不会在警告或信息日志消息上浪费磁盘空间
- 在发送消息的时候,加上 RoutingKey
- 消费端可以绑定队列与交换机的时候,设置 RoutingKey ,只有与发送消息附带的 RoutingKey 相同的消费队列才会接收到消息
生产者:
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<String, Object> 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<String, Object> 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 消费端接收到了消息:
5.Topics 通配符模式(交换机类型:topics)
上一个模式的 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());
两个都获取到了:
6.RPC
RPC 框架很出名,但是 RabbitMQ 实现的 RPC 模式很少用到,这里简单介绍一下。
我们的 RPC 将按如下方式工作:
- 当客户端启动时,它会创建一个匿名独占回调队列。
- 对于 RPC 请求,客户端发送一条具有两个属性的消息:reply_to(设置为回调队列)和correlation_id(设置为每个请求的唯一值)。
- 请求将发送到rpc_queue队列。
- RPC 工作线程(也称为:服务器)正在等待该队列上的请求。当出现请求时,它会执行作业,并使用reply_to 字段中的队列将包含结果的消息发送回客户端。
- 客户端等待回调队列上的数据。当出现一条消息时,它会检查correlation_id属性。如果它与请求中的值匹配,则会将响应返回到应用程序。