1.1订阅模型分类
在之前的模型中,我们创建了一个工作队列。工作队列背后的假设是:每个任务:只被传递给一个工作人员,而订阅模型则会传递一个消息给多个消费者。这种模式被称为“发布/订阅”。
订阅模型示意图:
解读:
1.一个生产者,多个消费者
2.每个消费者都有自己的一个队列
3.生产者没有将消息直接发送到队列,而是发送到了交换机
4.每个队列都要绑定到交换机
5.生产者发送的消息,经过交换机到达队列,实现一个消息被多个消费者获取的目的
X(Exchanges):交换机一方面:接收生产者发送的消息。另一方面:知道如何处理消息,例如递交给某个特别队列,递交给所有队列,或是将消息丢弃。到底如何操作,取决于Exchange的类型。
Fanout:广播,将消息交给所有绑定到交换机的队列
Direct:定向,把消息交给符合制定routing key 的队列
Topic:通配符,把消息交给符合routing pattern(路由模式)的队列。
Echanges(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchanges绑定,或者没有符合路由规则的队列,那么消息会丢失!
1.2订阅模型-Fanout
Fanout,也称为广播。
代码实现:
生产者:
两个变化:
1.声明Exchange,不再声明Queue
2.发送消息到Exchange,不再发送到Queue
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import util.ConnectionUtil;
public class Send {
private final static String EXCHANGE_NAME = "fanout_exchange_test";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明exchange,指定类型为fanout
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
// 消息内容
String message = "Hello everyone";
// 发布消息到Exchange
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
System.out.println(" [生产者] Sent '" + message + "'");
channel.close();
connection.close();
}
}
消费者1:
import java.io.IOException;
import com.rabbitmq.client.AMQP.BasicProperties;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import util.ConnectionUtil;
//消费者1
public class Recv1 {
private final static String QUEUE_NAME = "fanout_exchange_queue_1";
private final static String EXCHANGE_NAME = "fanout_exchange_test";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println(" [消费者1] received : " + msg + "!");
}
};
// 监听队列,自动返回完成
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
要注意代码中:队列需要和交换机绑定
消费者2:
import com.rabbitmq.client.*;
import util.ConnectionUtil;
import java.io.IOException;
/**
* Created by MM on 2019/3/19.
*/
public class Recv2 {
private final static String QUEUE_NAME="fanout_exchange_queue_2";
private final static String EXCHANGE_NAME="fanout_exchange_test";
public static void main(String[] args)throws Exception {
//获取到连接
Connection connection = ConnectionUtil.getConnection();
//获取通道
Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//绑定队列到交换机
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"");
//定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel){
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//body消息实体
String msg = new String(body);
System.out.println(" [消费者2] received : " + msg + "!");
}
};
// 监听队列,自动返回完成
channel.basicConsume(QUEUE_NAME, false, consumer);
}
}
测试:
1.3订阅模型-Direct
有选择性的接收消息
在订阅模式中,生产者发布消息,所有消费者都可以获取所有消息。
在路由模式中,我们将添加一个功能:只能订阅一部分消息。例如,我们只能将重要的错误消息引导到日志文件(以节省磁盘空间),同事仍然能够在控制台上打印所有日志消息。
但是,在某些场景下,我们希望不同的消息背不痛的队列消费。这是就要用到Direct类型的Exchange.
在Direct模型下,队列与交换机的绑定,不能是任意绑定了,而是要指定一个Routingkey(路由key)
消息的发送方在Exchange发送消息时,也必须指定消息的routing key。
P:生产者,向Exchange发送消息,发送消息时,会指定一个routing key。
X:Exchange(交换机),接收生产者的消息,然后把消息递交给 与routing key完全匹配的队列
C1:消费者,其所在队列指定了需要routing key 为 error 的消息
C2:消费者,其所在队列指定了需要routing key 为 info、error、warning 的消息
代码实现:
生产者:
此处我们模拟商品的增删改,发送消息的RoutingKey 分别是:insert、update、delete
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import util.ConnectionUtil;
/**
* 生产者,模拟为商品服务
* Created by MM on 2019/3/19.
*/
public class Send {
private final static String EXCHANGE_NAME = "direct_exchange_test";
public static void main(String[] args) throws Exception{
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明exchange,指定类型为direct
channel.exchangeDeclare(EXCHANGE_NAME,"direct");
//消息内容
String message1 = "商品删除了,id = 1001";
String message2 = "商品修改了,id = 1001";
String message3 = "商品新增了,id = 1001";
//发送消息,并且指定routing key 为:delete ,代表新增商品
channel.basicPublish(EXCHANGE_NAME,"delete",null,message1.getBytes());
System.out.println(" [商品服务:] Sent '" + message1 + "'");
channel.basicPublish(EXCHANGE_NAME,"update",null,message2.getBytes());
System.out.println(" [商品服务:] Sent '" + message2 + "'");
channel.basicPublish(EXCHANGE_NAME,"insert",null,message3.getBytes());
System.out.println(" [商品服务:] Sent '" + message3 + "'");
channel.close();
connection.close();
}
}
import com.rabbitmq.client.*;
import util.ConnectionUtil;
import java.io.IOException;
/**
* Created by MM on 2019/3/19.
*/
public class Recv {
private final static String QUEUE_NAME="direct_exchange_queue_1";
private final static String EXCHANGE_NAME="direct_exchange_test";
public static void main(String[] args)throws Exception{
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机,同时指定需要订阅的routing key。假设此处需要update和delete消息
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"update");
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"delete");
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel){
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body);
System.out.println(" [消费者1] received : " + msg + "!");
}
};
// 监听队列,自动ACK
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
import java.io.IOException;
import com.rabbitmq.client.*;
import com.rabbitmq.client.AMQP.BasicProperties;
import util.ConnectionUtil;
/**
* Created by MM on 2019/3/19.
*/
public class Recv2 {
private final static String QUEUE_NAME="direct_exchange_queue_2";
private final static String EXCHANGE_NAME="direct_exchange_test";
public static void main(String[] args)throws Exception{
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机,同时指定需要订阅的routing key。假设此处需要update和delete消息
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "insert");
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"update");
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"delete");
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel){
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
byte[] body) throws IOException {
String msg = new String(body);
System.out.println(" [消费者2] received : " + msg + "!");
}
};
// 监听队列,自动ACK
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
测试:
1.4订阅模型-Topic
Topic类型的Exchange与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key 的时候使用通配符!
Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
通配符规则:
#:匹配一个或多个词
*:匹配不多不少恰好1个词
举例:
audit.#:能够匹配audit.irs.corporate 或者 audit.irs
audit.*:只能匹配audit.irs
在这个例子中,我们将发送所有描述动物的消息。消息将使用由三个字(两个点)组成的routing key发送。路由关键字中的第一个单词将描述速度,第二个颜色和第三个种类:“<speed>.<color>.<species>”。
我们创建了三个绑定:Q1绑定了绑定键“* .orange.”,Q2绑定了“.*.rabbit”和“lazy.#”。
Q1匹配所有的橙色动物。
Q2匹配关于兔子以及懒惰动物的消息。
练习,生产者发送如下消息,会进入那个队列:
quick.orange.rabbit Q1 Q2
lazy.orange.elephant Q1 Q2
quick.orange.fox Q1
lazy.pink.rabbit Q2
quick.brown.fox 不匹配任意队列,被丢弃
2.6.1 生产者
使用topic类型的Exchange,发送消息的routing key有3种: item.isnert、item.update、item.delete:
package cn.itcast.rabbitmq.topic;
import cn.itcast.rabbitmq.util.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
/**
* 生产者,模拟为商品服务
*/
public class Send {
private final static String EXCHANGE_NAME = "topic_exchange_test";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明exchange,指定类型为topic
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
// 消息内容
String message = "新增商品 : id = 1001";
// 发送消息,并且指定routing key 为:insert ,代表新增商品
channel.basicPublish(EXCHANGE_NAME, "item.insert", null, message.getBytes());
System.out.println(" [商品服务:] Sent '" + message + "'");
channel.close();
connection.close();
}
}
2.6.2 消费者1
此处假设消费者1只接收两种类型的消息:更新商品和删除商品。
package cn.itcast.rabbitmq.topic;
import java.io.IOException;
import com.rabbitmq.client.AMQP.BasicProperties;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import cn.itcast.rabbitmq.util.ConnectionUtil;
/**
* 消费者1
*/
public class Recv {
private final static String QUEUE_NAME = "topic_exchange_queue_1";
private final static String EXCHANGE_NAME = "topic_exchange_test";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机,同时指定需要订阅的routing key。需要 update、delete
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "item.update");
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "item.delete");
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println(" [消费者1] received : " + msg + "!");
}
};
// 监听队列,自动ACK
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
2.6.3 消费者2
此处假设消费者2接收所有类型的消息:新增商品,更新商品和删除商品。
package cn.itcast.rabbitmq.topic;
import java.io.IOException;
import com.rabbitmq.client.AMQP.BasicProperties;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import cn.itcast.rabbitmq.util.ConnectionUtil;
/**
* 消费者2
*/
public class Recv2 {
private final static String QUEUE_NAME = "topic_exchange_queue_2";
private final static String EXCHANGE_NAME = "topic_exchange_test";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机,同时指定需要订阅的routing key。订阅 insert、update、delete
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "item.*");
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println(" [消费者2] received : " + msg + "!");
}
};
// 监听队列,自动ACK
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
2.6.4 测试
发送三种消息
2.消息持久化
如何避免消息丢失?
1.消费者的ACK机制。可以防止消费者丢失消息。
2.但是如果在消费者消费之前,MQ就宕机了,消息就没了。
那么解决方法肯定就是消息持久化的方向了?
要将消息持久化,前提是:队列、Exchange都持久化
2.1交换机持久化
2.2队列持久化
2.3消息持久化