AMQP简介
AMQP概念
AMQP全称高级消息队列协议,是一种标准,兼容JMS协议。类似HTTP协议,前端不管后台是什么语言,只要通过HTTP协议调用就可以了。
- Broker: 接收和分发消息的应用,我们在介绍消息中间件的时候所说的消息系统就是Message Broker。一个具体的MQ服务实例
- Virtual host: 出于多租户和安全因素设计的,把AMQP的基本组件划分到一个虚拟的分组中,类似于网络中的namespace概念。当多个不同的用户使用同一个RabbitMQ server提供的服务时,可以划分出多个vhost,每个用户在自己的vhost创建exchange/queue等。
- Connection:publisher/consumer和broker之间的TCP连接。断开连接的操作只会在client端进行,Broker不会断开连接,除非出现网络故障或broker服务出现问题。
- Channel: 如果每一次访问RabbitMQ都建立一个Connection,在消息量大的时候建立TCP Connection的开销将是巨大的,效率也较低。Channel是在connection内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的channel进行通讯,AMQP method包含了channel id帮助客户端和message broker识别channel,所以channel之间是完全隔离的。Channel作为轻量级的Connection极大减少了操作系统建立TCP connection的开销。实际发送消息是用的NIO中的selector方式。
- Exchange: message到达broker的第一站,根据分发规则,匹配查询表中的routing key,分发消息到queue中去。
- direct (路由模式): 这种类型的交换机的路由规则是根据一个routingKey的标识,交换机通过一个routingKey与队列绑定 ,在生产者生产消息的时候 指定一个routingKey 当绑定的队列的routingKey 与生产者发送的一样 那么交换机会吧这个消息发送给对应的队列。
- fanout(发布订阅): 这种类型的交换机路由规则很简单,只要与他绑定了的队列, 他就会吧消息发送给对应队列(与routingKey没关系)
- topic(主题模式): 这种类型的交换机路由规则也是和routingKey有关 只不过 topic他可以根据:星,#( 星号代表过滤一单词,#代表过滤后面所有单词, 用.隔开)来识别routingKey 我打个比方 假设 我绑定的routingKey 有队列A和B A的routingKey是:星.user B的routingKey是: #.user那么我生产一条消息routingKey 为: error.user 那么此时 2个队列都能接受到, 如果改为 topic.error.user 那么这时候 只有B能接受到了
- headers(RPC): 这个类型的交换机很少用到,他的路由规则 与routingKey无关 而是通过判断header参数来识别的, 基本上没有应用场景,因为上面的三种类型已经能应付了。
- Queue: 消息最终被送到这里等待consumer取走。一个message可以被同时拷贝到多个queue中。
- Binding: exchange和queue之间的虚拟连接,binding中可以包含routing key。Binding信息被保存到exchange中的查询表中,用于message的分发依据。
- Routing key: 路由键,用于指定消息路由规则(Exchange将消息路由到具体的queue中),通常需要和具体的Exchange类型、Binding的Routing key结合起来使用。
AMQP传输层架构
AMQP是一个二进制的协议,信息被组织成数据帧,有很多类型。数据帧携带协议方法和其他信息。所有数据帧都拥有基本相同的格式:帧头,负载,帧尾。数据帧负载的格式依赖于数据帧的类型。
我们假定有一个可靠的面向流的网络传输层(TCP/IP或等价的协议)。
在一个单一的socket连接中,可能有多个相互独立的控制线程,称为“channel”。每个数据帧使用通道号码编号。通过数据帧的交织,不同的通道共享一个连接。对于任意给定通道,数据帧严格按照序列传输。
我们使用小的数据类型来构造数据帧,如bit,integer,string以及字段表。数据帧的字段做了轻微的封装,不会让传输变慢或解析困难。根据协议规范机械地生成成数据帧层相对简单。
线级别的格式被设计为可伸缩和足够通用,以支持任意的高层协议(不仅是AMQP)。我们假定AMQP会扩展,改进以及随时间的其他变化,并要求wire-level格式支持这些变化。
AMQP数据类型
- Integers(数值范围1-8的十进制数字):用于表示大小,数量,限制等,整数类型无符号的,可以在帧内不对齐。
- Bits(统一为8个字节):用于表示开/关值。
- Short strings:用于保存简短的文本属性,字符串个数限制为255,8个字节
- Long strings:用于保存二进制数据块。
- Field tables:包含键值对,字段值一般为字符串,整数等。
RabbitMQ相关概念
- RabbitMQ是一个Erlang开发的AMQP(Advanced Message Queuing Protocol,高级消息队列协议)的开源实现。是应用层协议的一个开放标准,为面向消息的中间件设计。消息中间件主要用于组件之间的解耦,消息的发送者无需知道消息使用者的存在。
- 主要特征
- 可靠性:持久化、传输确认、发布确认等机制来保证可靠性。
- 扩展性:支持动态扩展集群中的节点
- 高可用:队列可在集群中设置镜像,部分节点出现问题仍然可用
- 多协议:AMQP协议、STOMP、MOTT等多种消息中间件协议
- 多语言:java、Python、Ruby、PHP、C#、JavaScript、Go、Object-C等
- 支持插件:如web管理端。
- 消息队列有三个基本概念: 发送方、消息队列、消费方。RabbitMQ 在这个基本概念之上, 多做了一层抽象, 在发消息者和队列之间, 加入了交换器 (Exchange)。这样发消息者和消息队列就没有直接联系,转而变成发消息者把消息发给交换器,交换器根据调度策略再把消息转发给消息队列。消息生产者并没有直接将消息发送给消息队列,而是通过建立与Exchange的Channel,将消息发送给Exchange。Exchange根据路由规则,将消息转发给指定的消息队列。消息队列储存消息,等待消费者取出消息。消费者通过建立与消息队列相连的Channel,从消息队列中获取消息。
RabbitMQ几种应用模式
工作流程
生产者发送消息的流程
- 生产者连接RabbitMQ,建立TCP连接( Connection),开启信道(Channel)
- 生产者声明一个Exchange(交换器),并设置相关属性,比如交换器类型、是否持久化等
- 生产者声明一个队列井设置相关属性,比如是否排他、是否持久化、是否自动删除等
- 生产者通过 bindingKey (绑定Key)将交换器和队列绑定( binding )起来
- 生产者发送消息至RabbitMQ Broker,其中包含 routingKey (路由键)、交换器等信息
- 相应的交换器根据接收到的 routingKey 查找相匹配的队列。
- 如果找到,则将从生产者发送过来的消息存入相应的队列中。
- 如果没有找到,则根据生产者配置的属性选择丢弃还是回退给生产者
- 关闭信道。
- 关闭连接
消费者接收消息的过程
- 消费者连接到RabbitMQ Broker ,建立一个连接(Connection ) ,开启一个信道(Channel) 。
- 消费者向RabbitMQ Broker 请求消费相应队列中的消息,可能会设置相应的回调函数, 以及做一些准备工作
- 等待RabbitMQ Broker 回应并投递相应队列中的消息, 消费者接收消息。
- 消费者确认( ack) 接收到的消息。
- RabbitMQ 从队列中删除相应己经被确认的消息。
- 关闭信道。
- 关闭连接。
消息类型Demo
RabbitMQ几种应用模式
工作流程
生产者发送消息的流程
- 生产者连接RabbitMQ,建立TCP连接( Connection),开启信道(Channel)
- 生产者声明一个Exchange(交换器),并设置相关属性,比如交换器类型、是否持久化等
- 生产者声明一个队列井设置相关属性,比如是否排他、是否持久化、是否自动删除等
- 生产者通过 bindingKey (绑定Key)将交换器和队列绑定( binding )起来
- 生产者发送消息至RabbitMQ Broker,其中包含 routingKey (路由键)、交换器等信息
- 相应的交换器根据接收到的 routingKey 查找相匹配的队列。
- 如果找到,则将从生产者发送过来的消息存入相应的队列中。
- 如果没有找到,则根据生产者配置的属性选择丢弃还是回退给生产者
- 关闭信道。
- 关闭连接
消费者接收消息的过程
- 消费者连接到RabbitMQ Broker ,建立一个连接(Connection ) ,开启一个信道(Channel) 。
- 消费者向RabbitMQ Broker 请求消费相应队列中的消息,可能会设置相应的回调函数, 以及做一些准备工作
- 等待RabbitMQ Broker 回应并投递相应队列中的消息, 消费者接收消息。
- 消费者确认( ack) 接收到的消息。
- RabbitMQ 从队列中删除相应己经被确认的消息。
- 关闭信道。
- 关闭连接。
消息类型Demo
public class MqConnectionFactory {
private static final Logger logger = LoggerFactory.getLogger(MqConnectionFactory.class);
public static Connection getConnection() throws IOException, TimeoutException {
System.out.println("开始创建MQ连接");
// 1.创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//连接是否设置自动恢复
// factory.setAutomaticRecoveryEnabled(false);
// 2.设置连接地址
factory.setHost("192.168.100.102");
// 3.设置用户名称
factory.setUsername("admin");
// 4.设置用户密码
factory.setPassword("admin");
// 5.设置amqp协议端口号
factory.setPort(5672);
// 6.设置VirtualHost地址
factory.setVirtualHost("test");
//建立TCP连接
Connection connection = factory.newConnection();
System.out.println("创建connection成功");
return connection;
}
public static void close(Connection connection, Channel channel) {
try {
if (channel != null) {
System.out.println("关闭信道");
channel.close();
}
if (connection != null) {
System.out.println("关闭连接");
connection.close();
}
} catch (Exception e) {
logger.error("关闭连接出现异常", e);
}
}
}
简单模式
- 消息产生消息,将消息放入队列
- 消息的消费者(consumer) 监听(while) 消息队列,如果队列中有消息,就消费掉,消息被拿走后,自动从队列中删除。
- Demo演示
private static String QUEUE_NAME = "demo_hello";
public static void main(String[] args) {
try {
System.out.println("开始发送消息");
Connection connection = MqConnectionFactory.getConnection();
Channel channel = connection.createChannel();
// 创建一个队列:队列名称,是否持久化,是否排外,是否自动删除队列,消息队列的属性(使用默认的)
/**
* 1.队列名称
* 2.是否持久化
* false,队列非持久化。因为队列是存放在内存中的,所以当RabbitMQ重启或者服务器重启时该队列就会丢失
* true,队列持久化。当RabbitMQ重启后队列不会丢失。RabbitMQ退出时它会将队列信息保存到 Erlang自带的Mnesia数据库 中,当RabbitMQ重启之后会读取该数据库
* 3.是否排外
* true则设置队列为排他的。如果一个队列被声明为排他队列,该队列仅对首次声明它的连接(Connection)可见,是该Connection私有的,类似于加锁,并在连接断开connection.close()时自动删除
* 首次是指某个连接(Connection)已经声明了排他队列,其他连接是不允许建立同名的排他队列的
* false则设置队列为非排他的,此时不同连接(Connection)的管道Channel可以使用该队列 ;
* 4. 是否自动删除 ;如果autoDelete = true,当所有消费者都与这个队列断开连接时,这个队列会自动删除。注意: 不是说该队列没有消费者连接时该队列就会自动删除,因为当生产者声明了该队列且没有消费者连接消费时,该队列是不会自动删除的
* 5.设置队列的其他一些参数,如 x-rnessage-ttl 、x-expires 、x-rnax-length 、x-rnax-length-bytes、 x-dead-letter-exchange、 x-deadletter-routing-key 、 x-rnax-priority 等。
*/
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
for (int i = 1; i <= 25; i++) {
// 创建 msg
String msg = "生成 ---" + i;
System.out.println("发送消息:" + msg);
// 生产者发送消息者 MessageProperties.PERSISTENT_TEXT_PLAIN 设置消息的持久化(消息没有接收到也不会丢失)
channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes("UTF-8"));
}
System.out.println("发送消息完成");
MqConnectionFactory.close(connection,channel);
} catch (Exception e) {
e.getStackTrace();
}
}
private static String QUEUE_NAME = "demo_hello";
public static void main(String[] args) {
try {
System.out.println("开始接收消息");
Connection connection = MqConnectionFactory.getConnection();
Channel channel = connection.createChannel();
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
// 监听获取消息
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("消费消息:" + msg);
}
};
// 设置应答模式 如果为true情况下 表示为自动应答模式 false 表示为手动应答
channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
System.out.println("接收消息完成");
// MqConnectionFactory.close(connection,channel);
} catch (Exception e) {
e.getStackTrace();
}
}
work queues(工作模式)
- 多个消费者可以订阅同一个队列,这时队列中的消息会被平均分摊给多个消费者进行处理,而不是每个消费
者都收到所有的消息并处理。 - 这时如果每个消息的处理时间不同,就有可能会导致某些消费者一直在忙,而另外一些消费者很快就处理完手头工作并一直空闲的情况。我们可以通过设置prefetchCount来限制Queue每次发送给每个消费者的消息数,比如我们设置prefetchCount=1,则Queue每次给每个消费者发送一条消息;消费者处理完这条消息后Queue会再给该消费者发送一条消息。
- Demo演示
工作队列只需要将上面的消费者多复制几分就可以了。
如果发现消息分发不均匀可以设置
/**
* 公平队列原理:队列服务器向消费者发送消息的时候,消费者采用手动应答模式,
* 队列服务器必须要收到消费者发送ack结果通知,才会继续发送一下一个消息
* 此处设置一次只消费1个
*/
channel.basicQos(1);
默认情况下,rabbitmq开启了消息的自动应答。此时,一旦rabbitmq将消息分发给了消费者,就会将消息从内存中删除。这种情况下,如果正在执行的消费者被“杀死”或“崩溃”,就会丢失正在处理的消息。 如果想要确保消息不丢失,我们需要设置消息应答方式为手动应答。设置为手工应答后,消费者接受并处理完一个消息后,会发送应答给rabbitmq,rabbitmq收到应答后,会将该条消息从内存中删除。如果一个消费者在处理消息的过程中“崩溃”,rabbitmq没有收到应答,那么”崩溃“前正在处理的这条消息会重新被分发到别的消费者。
public class WorkQueueProducer {
private static String QUEUE_NAME = "demo_hello_rabbitmq";
public static void main(String[] args) {
try {
System.out.println("开始发送消息");
Connection connection = MqConnectionFactory.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
for (int i = 1; i <= 25; i++) {
// 创建 msg
String msg = "生成 ---" + i;
System.out.println("发送消息:" + msg);
// 生产者发送消息者 MessageProperties.PERSISTENT_TEXT_PLAIN 设置消息的持久化(消息没有接收到也不会丢失)
channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes("UTF-8"));
Thread.sleep(i * 10);
}
System.out.println("发送消息完成");
MqConnectionFactory.close(connection,channel);
} catch (Exception e) {
e.getStackTrace();
}
}
}
public class WorkQueueConsumer1 {
private static String QUEUE_NAME = "demo_hello_rabbitmq";
public static void main(String[] args) {
try {
System.out.println("开始接收消息");
Connection connection = MqConnectionFactory.getConnection();
Channel channel = connection.createChannel();
channel.basicQos(1);
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
System.out.println("WorkQueueConsumer1消费消息:" + new String(delivery.getBody(), "UTF-8"));
};
// 设置应答模式 如果为true情况下 表示为自动应答模式, false 表示为手动应答
channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> {
});
// MqConnectionFactory.close(connection,channel);
} catch (Exception e) {
e.getStackTrace();
}
}
}
public class WorkQueueConsumer2 {
private static String QUEUE_NAME = "demo_hello_rabbitmq";
public static void main(String[] args) {
try {
System.out.println("开始接收消息");
Connection connection = MqConnectionFactory.getConnection();
Channel channel = connection.createChannel();
channel.basicQos(1);
// 设置应答模式 如果为true情况下 表示为自动应答模式, false 表示为手动应答
channel.basicConsume(QUEUE_NAME, false, new Recivew(channel));
// MqConnectionFactory.close(connection,channel);
} catch (Exception e) {
e.getStackTrace();
}
}
}
class Recivew extends DefaultConsumer {
private Channel channel;
public Recivew(Channel channel) {
super(channel);
this.channel = channel;
}
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String s = new String(body);
System.out.println("WorkQueueConsumer2消费消息 " + new String(body, "UTF-8"));
// 手动应答 模式 告诉给队列服务器 已经处理成功 multiple false表示只确认当前的消息,true代表签收高消费者所有未签收的消息
channel.basicAck(envelope.getDeliveryTag(), false);
//消息标签,不确认是多个消息还是一个消息,是否重新入列。用于拒绝多条消息
//channel.basicNack(envelope.getDeliveryTag(), false, true);
//用于拒绝一条消息,对于不确认的消息是否入列然后重发
// channel.basicReject(envelope.getDeliveryTag(), true);
}
}
publish/subscribe(发布订阅)
- 1个生产者,多个消费者。每一个消费者都有自己的一个队列。生产者没有将消息直接发送到队列,而是发送到了交换机。每个队列都要绑定到交换机。生产者发送的消息,经过交换机到达队列,实现一个消息被多个消费者获取的目的
- X(Exchanges)接收生产者发送的消息。知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。
- 这种模式不需要RouteKey
- 广播消息到所有队列,没有任何处理,速度最快
- Demo演示
private static final String DESTINATION_NAME = "rabbitMq_fanout";
public static void main(String[] args) {
try {
Connection connection = MqConnectionFactory.getConnection();
Channel channel = connection.createChannel();
String queueName = channel.queueDeclare().getQueue();
System.out.println("随机生成的队列名称:" + queueName);
//绑定队列和交换机
//队列名称、交换机、routingKek目前用不到
channel.queueBind(queueName, DESTINATION_NAME, "");
// 订阅消息的回调函数
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
System.out.println(queueName + "消费者获取生产消息:" + new String(delivery.getBody(), "UTF-8"));
};
// 消费者,有消息时出发订阅回调函数 自动应答
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
System.out.println("消费者回调" + consumerTag);
});
// MqConnectionFactory.close(connection, channel);
} catch (Exception e) {
e.printStackTrace();
}
}
private static final String DESTINATION_NAME = "rabbitMq_fanout";
public static void main(String[] args) {
try {
Connection connection = MqConnectionFactory.getConnection();
Channel channel = connection.createChannel();
String queueName = channel.queueDeclare().getQueue();
System.out.println("随机生成的队列名称:" + queueName);
channel.queueBind(queueName, DESTINATION_NAME, "");
// 订阅消息的回调函数
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
System.out.println(queueName + "消费者获取生产消息:" + new String(delivery.getBody(), "UTF-8"));
};
// 消费者,有消息时出发订阅回调函数 自动应答
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
System.out.println("消费者回调" + consumerTag);
});
// MqConnectionFactory.close(connection, channel);
} catch (Exception e) {
e.printStackTrace();
}
}
public class FanoutProducer {
private static final String DESTINATION_NAME = "rabbitMq_fanout";
public static void main(String[] args) {
try {
System.out.println("开始发送消息");
Connection connection = MqConnectionFactory.getConnection();
Channel channel = connection.createChannel();
// 3.生产者绑定交换机 参数1:交换机名称 参数2:交换机类型,是否持久化,是否排外,是否自动删除队列,消息队列的属性(使用默认的)
channel.exchangeDeclare(DESTINATION_NAME, BuiltinExchangeType.FANOUT, true, false, false, null);
// 4.创建消息
for (int i = 0; i < 10; i++) {
String msg = "rabbitMq_fanout" + i;
System.out.println("生产者投递消息:" + msg);
// 5.发送消息
channel.basicPublish(DESTINATION_NAME, "", null, msg.getBytes());
System.out.println("发送消息完成");
}
MqConnectionFactory.close(connection, channel);
} catch (Exception e) {
e.printStackTrace();
}
}
}
routing(路由模式)
- 处理路由键。需要将一个队列绑定到交换机上,要求该消息与一个特定的路由键完全匹配。这是一个完整的匹配。如果一个队列绑定到该交换机上要求路由键 “dog”,则只有被标记为“dog”的消息才被转发,不会转发dog.puppy,也不会转发dog.guard,只会转发dog。
- 任何发送到Direct Exchange的消息都会被转发到RouteKey中指定的Queue。
- 一般情况可以使用rabbitMQ自带的Exchange:”"(该Exchange的名字为空字符串,下文称其为default Exchange)。
- 这种模式下不需要将Exchange进行任何绑定(binding)操作。
- 消息传递时需要一个“RouteKey”,可以简单的理解为要发送到的队列名字。
- 如果vhost中不存在RouteKey中指定的队列名,则该消息会被抛弃。
- Demo演示
private static final String DESTINATION_NAME = "rabbitMq_direct";
private static final String SMS_QUEUE = "Sms_msg";
public static void main(String[] args) {
try {
Connection connection = MqConnectionFactory.getConnection();
Channel channel = connection.createChannel();
// 消费声明队列
channel.queueDeclare(SMS_QUEUE, false, false, false, null);
// 消费者队列绑定交换机
channel.queueBind(SMS_QUEUE, DESTINATION_NAME, "sms");
channel.queueBind(SMS_QUEUE, DESTINATION_NAME, "email");
channel.basicQos(1);
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
System.out.println(SMS_QUEUE + "消费者获取生产消息:" + new String(delivery.getBody(), "UTF-8"));
};
channel.basicConsume(SMS_QUEUE, true, deliverCallback, consumerTag -> {
});
} catch (Exception e) {
e.printStackTrace();
}
}
private static final String DESTINATION_NAME = "rabbitMq_direct";
private static final String EMAIL_QUEUE = "Email_msg";
public static void main(String[] args) {
try {
Connection connection = MqConnectionFactory.getConnection();
Channel channel = connection.createChannel();
// 消费声明队列
channel.queueDeclare(EMAIL_QUEUE, false, false, false, null);
// 消费者队列绑定交换机
channel.queueBind(EMAIL_QUEUE, DESTINATION_NAME, "email");
channel.basicQos(1);
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
System.out.println(EMAIL_QUEUE + "消费者获取生产消息:" + new String(delivery.getBody(), "UTF-8"));
};
channel.basicConsume(EMAIL_QUEUE, true, deliverCallback, consumerTag -> {
});
} catch (Exception e) {
e.printStackTrace();
}
}
// 交换机名称
private static final String DESTINATION_NAME = "rabbitMq_direct";
public static void main(String[] args) {
try {
System.out.println("开始发送消息");
Connection connection = MqConnectionFactory.getConnection();
Channel channel = connection.createChannel();
// 3.生产者绑定交换机 参数1:交换机名称 参数2:交换机类型
channel.exchangeDeclare(DESTINATION_NAME, BuiltinExchangeType.DIRECT);
for (int i = 1; i < 10; i++) {
String routingKey = "";
//生成路由键
if (i % 2 == 0) {
routingKey = "email";
} else {
routingKey = "sms";
}
String msg = "rabbitMq_direct---:" + routingKey;
System.out.println("生产者投递消息:" + msg);
channel.basicPublish(DESTINATION_NAME, routingKey, null, msg.getBytes());
}
System.out.println("发送消息完成");
MqConnectionFactory.close(connection, channel);
} catch (Exception e) {
e.printStackTrace();
}
}
topic(主题模式)
- Topic Exchange – 将路由键和某模式进行匹配。此时队列需要绑定要一个模式上。符号“#”匹配一个或多个词,符号“ * ”匹配不多不少一个词。因此“audit.#”能够匹配到“audit.irs.corporate”,但是“audit.*” 只会匹配到“audit.irs”
- 任何发送到Topic Exchange的消息都会被转发到所有关心RouteKey中指定话题的Queue上
- 这种模式较为复杂,简单来说,就是每个队列都有其关心的主题,所有的消息都带有一个“标题”(RouteKey),Exchange会将消息转发到所有关注主题能与RouteKey模糊匹配的队列。
- 这种模式需要RouteKey,也许要提前绑定Exchange与Queue。
- 在进行绑定时,要提供一个该队列关心的主题,如“#.log.#”表示该队列关心所有涉及log的消息(一个RouteKey为”MQ.log.error”的消息会被转发到该队列)。
- “#”表示0个或若干个关键字,“ * ”表示一个关键字。如“log.*”能与“log.warn”匹配,无法与“log.warn.timeout”匹配;但是“log.#”能与上述两者匹配。
- 同样,如果Exchange没有发现能够与RouteKey匹配的Queue,则会抛弃此消息。
- Demo演示
private static final String DESTINATION_NAME = "rabbitMq_topic";
private static final String SMS_QUEUE = "Sms_msg_topic";
public static void main(String[] args) {
try {
Connection connection = MqConnectionFactory.getConnection();
Channel channel = connection.createChannel();
// 消费声明队列
channel.queueDeclare(SMS_QUEUE, false, false, false, null);
// 消费者队列绑定交换机
//只要前缀相同都能收到
channel.queueBind(SMS_QUEUE, DESTINATION_NAME, "sms.*");
//只能匹配到email.demo
channel.queueBind(SMS_QUEUE, DESTINATION_NAME, "email.demo");
channel.basicQos(1);
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
System.out.println(SMS_QUEUE + "消费者获取生产消息:" + new String(delivery.getBody(), "UTF-8"));
};
channel.basicConsume(SMS_QUEUE, true, deliverCallback, consumerTag -> {
});
} catch (Exception e) {
e.printStackTrace();
}
}
private static final String DESTINATION_NAME = "rabbitMq_topic";
private static final String EMAIL_QUEUE = "Email_msg_topic";
public static void main(String[] args) {
try {
Connection connection = MqConnectionFactory.getConnection();
Channel channel = connection.createChannel();
// 消费声明队列
channel.queueDeclare(EMAIL_QUEUE, false, false, false, null);
// 消费者队列绑定交换机
channel.queueBind(EMAIL_QUEUE, DESTINATION_NAME, "email.*");
channel.basicQos(1);
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
System.out.println(EMAIL_QUEUE + "消费者获取生产消息:" + new String(delivery.getBody(), "UTF-8"));
};
channel.basicConsume(EMAIL_QUEUE, true, deliverCallback, consumerTag -> {
});
} catch (Exception e) {
e.printStackTrace();
}
}
private static final String DESTINATION_NAME = "rabbitMq_topic";
private static final String SMS_QUEUE_1 = "Sms_msg_topic_1";
public static void main(String[] args) {
try {
Connection connection = MqConnectionFactory.getConnection();
Channel channel = connection.createChannel();
// 消费声明队列
channel.queueDeclare(SMS_QUEUE_1, false, false, false, null);
// 消费者队列绑定交换机
//可以匹配后面所有的词
channel.queueBind(SMS_QUEUE_1, DESTINATION_NAME, "sms.#");
channel.basicQos(1);
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
System.out.println(SMS_QUEUE_1 + "消费者获取生产消息:" + new String(delivery.getBody(), "UTF-8"));
};
channel.basicConsume(SMS_QUEUE_1, true, deliverCallback, consumerTag -> {
});
} catch (Exception e) {
e.printStackTrace();
}
}
private static final String DESTINATION_NAME = "rabbitMq_topic";
public static void main(String[] args) {
try {
System.out.println("开始发送消息");
Connection connection = MqConnectionFactory.getConnection();
Channel channel = connection.createChannel();
// 3.生产者绑定交换机 参数1:交换机名称 参数2:交换机类型
channel.exchangeDeclare(DESTINATION_NAME, BuiltinExchangeType.TOPIC);
String routingKey1="sms.1";
System.out.println("生产者投递消息:" + routingKey1);
channel.basicPublish(DESTINATION_NAME, routingKey1, null, routingKey1.getBytes());
String routingKey2="sms.2";
System.out.println("生产者投递消息:" + routingKey2);
channel.basicPublish(DESTINATION_NAME, routingKey2, null, routingKey2.getBytes());
String routingKey3="sms.1.1";
System.out.println("生产者投递消息:" + routingKey3);
channel.basicPublish(DESTINATION_NAME, routingKey3, null, routingKey3.getBytes());
String routingKey4="sms.1.3";
System.out.println("生产者投递消息:" + routingKey4);
channel.basicPublish(DESTINATION_NAME, routingKey4, null, routingKey4.getBytes());
String routingKey5="email.demo";
System.out.println("生产者投递消息:" + routingKey5);
channel.basicPublish(DESTINATION_NAME, routingKey5, null, routingKey5.getBytes());
String routingKey6="email.123";
System.out.println("生产者投递消息:" + routingKey6);
channel.basicPublish(DESTINATION_NAME, routingKey6, null, routingKey6.getBytes());
System.out.println("发送消息完成");
MqConnectionFactory.close(connection, channel);
} catch (Exception e) {
e.printStackTrace();
}
}
RPC
死信队列的作用
死信交换机有什么用呢? 在创建队列的时候 可以给这个队列附带一个交换机, 那么这个队列作废的消息就会被重新发到附带的交换机,然后让这个交换机重新路由这条消息。
死信消息产生的来源
- 消息被拒绝(basic.reject或basic.nack)并且requeue=false
- 消息TTL过期
- 队列达到最大长度(队列满了,无法再添加数据到mq中)
死信队列处理的方式
- 丢弃,如果不是很重要,可以选择丢弃
- 记录死信入库,然后做后续的业务分析或处理
- 通过死信队列,由负责监听死信的应用程序进行处理
消息超时进入死信队列
通俗的说,就是消息产生之后,因为设置了超时时间,在这段时间内消息没有被消费就会被扔到死信队列里面。
// 交换机名称
private static final String DESTINATION_NAME = "rabbitMq_topic_dlx";
//消息队列
private static final String queueName = "topic_queue_dlx";
//routingKey
private static final String routingKey = "topic.#";
//配置死信队列
private static final String dlxExchangeName = "dlx.exchange";
private static final String dlxQueueName = "dlx.queue";
private static final String dlxRoutingKey = "#";
public static void main(String[] args) {
try {
System.out.println("开始发送消息");
Connection connection = MqConnectionFactory.getConnection();
Channel channel = connection.createChannel();
Map<String, Object> arguments = new HashMap<String, Object>(16);
// 为队列设置队列交换器
arguments.put("x-dead-letter-exchange", dlxExchangeName);
// 设置队列中的消息 60s 钟后过期
arguments.put("x-message-ttl", 60000);
//消费声明队列
channel.exchangeDeclare(DESTINATION_NAME, BuiltinExchangeType.TOPIC);
channel.queueDeclare(queueName, true, false, false, arguments);
//消费者队列绑定交换机 绑定路由件 路由键
channel.queueBind(queueName, DESTINATION_NAME, routingKey);
String message = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " 测试消息超时,传递到死信队列";
// 创建死信交换器和队列
channel.exchangeDeclare(dlxExchangeName, BuiltinExchangeType.TOPIC, true, false, null);
channel.queueDeclare(dlxQueueName, true, false, false, null);
channel.queueBind(dlxQueueName, dlxExchangeName, dlxRoutingKey);
channel.basicPublish(DESTINATION_NAME, routingKey, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
System.out.println("发送消息完成");
MqConnectionFactory.close(connection, channel);
} catch (Exception e) {
e.printStackTrace();
}
}
只监听了死信队列的消息,正常消息无需监听接收
private static final String queueName = "topic_queue_dlx";
public static void main(String[] args) {
try {
Connection connection = MqConnectionFactory.getConnection();
Channel channel = connection.createChannel();
System.out.println("正常消费者启动 ..........");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
System.out.println("正常消费者生产消息:" + new String(delivery.getBody(), "UTF-8"));
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
});
} catch (Exception e) {
e.printStackTrace();
}
}
private static final String dlxQueueName = "dlx.queue";
public static void main(String[] args) {
try {
Connection connection = MqConnectionFactory.getConnection();
Channel channel = connection.createChannel();
System.out.println("死信消费者启动 ..........");
Thread.sleep(65000);
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
System.out.println("消死信消费者生产消息:" + new String(delivery.getBody(), "UTF-8"));
};
channel.basicConsume(dlxQueueName, true, deliverCallback, consumerTag -> {
});
} catch (Exception e) {
e.printStackTrace();
}
}
消息被退回
channel.basicNack(envelope.getDeliveryTag(),false,false);
队列达到最大长度
这个和消息超时差不多,只不过是设置了队列的最大容量而已。
只需要把上面的代码修改一下就可以了。
生产者需要修改
// 设置队列中的消息 60s 钟后过期
// arguments.put("x-message-ttl", 60000);
//设置队列长度为3
arguments.put("x-max-length", 3);
然后消息多发送几次
private static final String queueName = "topic_queue_dlx";
public static void main(String[] args) {
try {
Connection connection = MqConnectionFactory.getConnection();
Channel channel = connection.createChannel();
channel.basicQos(1);
System.out.println("正常消费者启动 ..........");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
System.out.println("正常消费者生产消息:" + new String(delivery.getBody(), "UTF-8"));
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
};
channel.basicConsume(queueName, false, deliverCallback, consumerTag -> {
});
} catch (Exception e) {
e.printStackTrace();
}
}
private static final String dlxQueueName = "dlx.queue";
public static void main(String[] args) {
try {
Connection connection = MqConnectionFactory.getConnection();
Channel channel = connection.createChannel();
System.out.println("死信消费者启动 ..........");
// Thread.sleep(65000);
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
System.out.println("消死信消费者生产消息:" + new String(delivery.getBody(), "UTF-8"));
};
channel.basicConsume(dlxQueueName, true, deliverCallback, consumerTag -> {
});
} catch (Exception e) {
e.printStackTrace();
}
}