官方文档
public class Producer {
private static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception{
//创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//工厂IP连接RabbitMQ队列
factory.setHost("192.168.140.129");
//用户名
factory.setUsername("why");
//密码
factory.setPassword("123");
//创建连接
Connection connection = factory.newConnection();
//创建信道
Channel channel = connection.createChannel();
/*
*生成队列
*1、队列名称,若队列不存在就创建队列
*2、队列里的消息是否支持持久化,默认消息存储在内存种
*3、是否独占队列 true表示消费者独占队列
*4、是否自动删除 true表示在消费者消费完队列中的数据并与该队列连接断开时自动删除队列
*5、额外参数设置(本例种暂时不用)
* */
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
String message = "hello world";
/*
*发送消息
* 1、发送到哪个交换机
* 2、路由的key
* 3、其他参数信息
* 4、发送消息的消息体
* */
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
System.out.println("消息发送完毕");
}
}
消费者代码
public class Consumer {
private static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception{
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.140.129");
factory.setUsername("why");
factory.setPassword("123");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
System.out.println("等待消息接收");
DeliverCallback deliverCallback = (consumerTag,delivery)->{
String message = new String(delivery.getBody());
System.out.println("接收到的消息"+message);
};
//取消消费的一个回调接口 如在消费的时候队列被删除掉了
CancelCallback cancelCallback = (consumerTag)->{
System.out.println("消费者被中断");
};
/*
* 消费消息
* 1、消费哪个队列
* 2、消费成功之后是否自动应答 true表示自动应答 false表示手动应答
* 3、消费者成功消费的回调函数
* 4、消费者被中断消费的回调
* */
channel.basicConsume(QUEUE_NAME,false,deliverCallback,cancelCallback);
}
}
启动生产者程序我们发现在消息队列种存在一条未被消费的消息
然后启动消费者程序发现消息已经被消费
public class MQUtil {
public static Channel getChannel() throws Exception{
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.140.129");
factory.setUsername("why");
factory.setPassword("123");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
return channel;
}
}
生产者代码
public class Producer {
private static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception{
Channel channel = MQUtil.getChannel();
/*
*生成队列
*1、队列名称,若队列不存在就创建队列
*2、队列里的消息是否支持持久化,默认消息存储在内存种
*3、是否独占队列 true表示消费者独占队列
*4、是否自动删除 true表示在消费者消费完队列中的数据并与该队列连接断开时自动删除队列
*5、额外参数设置(本例种暂时不用)
* */
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
/*
*一次发送4条消息
* 1、发送到哪个交换机
* 2、路由的key
* 3、其他参数信息
* 4、发送消息的消息体
* */
for (int i = 1; i < 5; i++) {
String message = "message"+i;
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
}
System.out.println("消息发送完毕");
}
}
消费者代码还是上个例子中的代码,启动两个消费者线程
运行结果
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
1号正在等待消息接收
1号消费者接收到的消息message1
1号消费者接收到的消息message3
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
2号正在等待消息接收
2号消费者接收到的消息message2
2号消费者接收到的消息message4
可以看出消息队列是轮询分发消息的
如果消费者在消费消息的过程种突然down掉,会导致消息的丢失。为了保证消息在发送过程中不丢失,RabbitMQ 引入消息应答机制,消息应答就是:消费者在接收到消息并且处理该消息之后,告诉 RabbitMQ它已经处理了,RabbitMQ可以把该消息删除了。
方法 | 描述 |
---|---|
Channel.basicAck(long tag,boolean multiple) | 消息已经成功处理,可以将其丢弃 |
Channel.basicNack(ong tag,boolean multiple,boolean b1) | 否定确认 |
Channel.basicReject(long tag,boolean multiple) | 否定确认,直接拒绝 |
如果消费者由于某些原因失去连接,导致消息未发送 ACK 确认,RabbitMQ 将了解到消息未完全处理,并将对其重新排队。使用手动应答即使消费者在处理消息的过程中宕机也不会造成消息的丢失(注意这里的消息不丢失指的是消息队列分配给消费者过程种消息不会丢失)
消费者代码
public class HandProducer {
private static final String QUEUE_NAME = "hand";
public static void main(String[] args) throws Exception{
Channel channel = MQUtil.getChannel();
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
Scanner scanner = new Scanner(System.in);
while(scanner.hasNext()){
String message = scanner.nextLine();
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
System.out.println("生产者发出消息"+message);
}
}
}
1号消费者代码
public class HandConsumer {
private static final String QUEUE_NAME = "hand";
public static void main(String[] args) throws Exception{
Channel channel = MQUtil.getChannel();
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
DeliverCallback deliverCallback = (consumerTag,delivery)->{
String message = new String(delivery.getBody());
try {
Thread.sleep(3*1000);//假设1号消费者处理消息耗时三秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("1号消费者接收到的消息"+message);
//1、消息标记tag
//2、false表示不启动批应答
channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
};
CancelCallback cancelCallback = (consumerTag)->{};
//手动应答
boolean auto = false;
channel.basicConsume(QUEUE_NAME,auto,deliverCallback,cancelCallback);
System.out.println("1号消费者正在等待接收消息");
}
}
2号消费者代码仅仅在处理时间上有所不同
try {
Thread.sleep(10*1000);//假设2号消费者处理消息耗时十秒
} catch (InterruptedException e) {
e.printStackTrace();
}
首先生产者发送两条消息
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
aaa
生产者发出消息aaa
bbb
生产者发出消息bbb
在10秒内关闭2号消费者程序来模拟宕机
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
2号消费者正在等待接收消息
Process finished with exit code -1
最后两条消息都由1号消费者处理
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
1号消费者正在等待接收消息
1号消费者接收到的消息aaa
1号消费者接收到的消息bbb
每次重启 RabbitMQ ,非持久化的队列都被删除。在声明队列的时候将durable参数设置为true表明创建持久化队列。如果队列存在需要先删除原先的非持久化队列才能生效
boolean durable = true;
//创建持久化队列
channel.queueDeclare(QUEUE_NAME,durable,false,false,null);
将消息标记为持久化并不能完全保证不会丢失消息。在消息保存的过程中依然会出现消息丢失的情况。实现消息持久化只需要将basicPublish方法参数的props改为MessageProperties.PERSISTENT_TEXT_PLAIN即可
//当队列是持久化队列时才能设置消息持久化
channel.basicPublish("",QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());
//写在消费者代码中
int prefetchCount = 1;
channel.basicQos(prefetchCount);
这段代码表示消费者在处理消息的过程中最多接收1个消息。如果消费者还在处理消息的过程中又有新的消息来了,RabbitMQ就会把消息分配给空闲的消费者。prefetchCount是预取值,表示该值定义通道上允许的未确认消息的最大数量,一旦数量达到配置的数量,RabbitMQ 将停止在通道上传递更多消息
之前我们讨论过虽然队列持久化和消息持久化可以使消息不丢失,但是不能完全保证消息不丢失。因为在消息保存到磁盘的过程中依然有可能丢失数据。开启发布确认模式才能保证消息不丢失
开启发布确认
//开启发布确认
channel.confirmSelect();
发布一条消息只有它被确认发布后才继续发送后续消息
public class Producer {
private static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception{
Channel channel = MQUtil.getChannel();
//开启发布确认
channel.confirmSelect();
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
long beign = System.currentTimeMillis();
for (int i = 1; i < 101; i++) {
String message = "message"+i;
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
//单个发布确认
boolean flag = channel.waitForConfirms();
if (flag){
System.out.println("消息发布成功");
}
}
long end = System.currentTimeMillis();
System.out.println("发布100条消息共耗时"+(end-beign)+"ms");
}
}
for (int i = 1; i < 101; i++) {
String message = "message"+i;
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
//批量发布确认:每发布10条消息确认一次
if (i%10 == 0){
boolean flag = channel.waitForConfirms();
if (flag){
System.out.println("消息发布成功");
}
}
}
public class Producer {
private static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception{
Channel channel = MQUtil.getChannel();
//开启发布确认
channel.confirmSelect();
channel.queueDeclare(QUEUE_NAME,true,false,false,null);
//线程安全有序的哈希表,适用于高并发
ConcurrentSkipListMap<Long,String> map = new ConcurrentSkipListMap<>();
long begin = System.currentTimeMillis();
//消息确认收到的回调
ConfirmCallback ackCallback = (deliveryTag,multiple)->{
//处理未确认消息第二步:删除已经确认的消息
if (multiple){
ConcurrentNavigableMap<Long,String> confirmedMap = map.headMap(deliveryTag);
confirmedMap.clear();
}else{
map.remove(deliveryTag);
}
System.out.println("确认收到消息"+deliveryTag);
};
//消息确认失败的回调
ConfirmCallback nackCallback = (deliveryTag,multiple)->{
//map中剩下的就是未确认的消息
System.out.println("未确认消息"+deliveryTag);
};
/*
* 准备监听器,监听哪些消息收到了,哪些消息失效了
* 1、ackCallback:监听哪些消息成功了
* 2、nackCallback:监听哪些消息失败了
* */
channel.addConfirmListener(ackCallback,nackCallback);
//批量发送
for (int i = 1; i < 101; i++) {
String message = "消息"+i;
channel.basicPublish("",QUEUE_NAME,null,message.getBytes("UTF-8"));
//处理未确认消息第一步:记录所有发送的消息
map.put(channel.getNextPublishSeqNo(),message);
}
long end = System.currentTimeMillis();
System.out.println("发布100条消息共耗时"+(end-begin)+"ms");
}
}
实际上生产者生产的消息不会直接发送到消息队列,之前我们案例使用的都是默认交换机。生产者只能将消息发送到交换机,再由交换机将消息发送到队列。
第一个参数是交换机的名称,空字符串表示默认交换机。
channel.basicPublish("",QUEUE_NAME,null,message.getBytes("UTF-8"));
Fanout 是将接收到的所有消息广播到它知道的所有队列中。
消费者代码(2号消费者代码仅仅将字符串"1号消费者"换为"2号消费者"即可)
public class Consumer1 {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws Exception {
Channel channel = MQUtil.getChannel();
//声明交换机
channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
/*
* 声明临时队列
* 临时队列名称是随机的
* 当消费者断开与队列的连接时 队列自动删除
* */
String queueName = channel.queueDeclare().getQueue();
//绑定交换机与队列
channel.queueBind(queueName,EXCHANGE_NAME,"");
System.out.println("1号消费者等待接收消息...");
DeliverCallback deliverCallback = (consumerTag,message)->{
System.out.println("1号消费者接收到的消息:"+new String(message.getBody(),"UTF-8"));
};
CancelCallback cancelCallback = (consumerTag)->{};
channel.basicConsume(queueName,true,deliverCallback,cancelCallback);
}
}
生产者代码
public class Producer {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws Exception{
Channel channel = MQUtil.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String message = scanner.nextLine();
channel.basicPublish(EXCHANGE_NAME,"",null,message.getBytes("UTF-8"));
System.out.println("生产者发送消息:"+message);
}
}
}
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
生产者发送消息:aa
生产者发送消息:bb
1号消费者和2号消费者都会收到消息
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
1号消费者等待接收消息...
1号消费者接收到的消息:aa
1号消费者接收到的消息:bb
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
2号消费者等待接收消息...
2号消费者接收到的消息:aa
2号消费者接收到的消息:bb
直接交换机与Fanout交换机的区别在于绑定的RoutingKey不同
1号消费者代码
public class Consumer1 {
private static final String DIRECT_EXCHANGE_NAME = "direct_logs";
public static void main(String[] args) throws Exception {
Channel channel = MQUtil.getChannel();
//声明交换机
//"direct"的枚举类型为BuiltinExchangeType.DIRECT
channel.exchangeDeclare(DIRECT_EXCHANGE_NAME, "direct");
//声明临时队列
String queueName = channel.queueDeclare().getQueue();
//绑定交换机与队列
channel.queueBind(queueName,DIRECT_EXCHANGE_NAME,"error");
System.out.println("1号消费者等待接收消息...");
DeliverCallback deliverCallback = (consumerTag,message)->{
System.out.println("1号消费者接收到的消息:"+new String(message.getBody(),"UTF-8"));
};
CancelCallback cancelCallback = (consumerTag)->{};
channel.basicConsume(queueName,true,deliverCallback,cancelCallback);
}
}
2号消费者代码(与1号消费者不同的代码片段)
//绑定交换机与队列
channel.queueBind(queueName,DIRECT_EXCHANGE_NAME,"info");
channel.queueBind(queueName,DIRECT_EXCHANGE_NAME,"waring");
生产者代码
public class Producer {
private static final String DIRECT_EXCHANGE_NAME = "direct_logs";
public static void main(String[] args) throws Exception{
Channel channel = MQUtil.getChannel();
channel.exchangeDeclare(DIRECT_EXCHANGE_NAME,"direct");
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String message = scanner.nextLine();
channel.basicPublish(DIRECT_EXCHANGE_NAME,"info",null,message.getBytes("UTF-8"));
System.out.println("生产者发送消息:"+message);
}
}
}
这里我生产者指定给绑定info的信道发送消息
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
2号消费者等待接收消息...
2号消费者接收到的消息:
2号消费者接收到的消息:aa
2号消费者接收到的消息:bb
2号消费者接收到的消息:cc
只有2号消费者能接收到消息而1号消费者不能接收到消息
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
1号消费者等待接收消息...
发送到类型是 topic 交换机的消息的 routing_key 不能随意写,必须满足一定的要求,它必须是一个单词列表,以点号分隔开。例如"quick.mq.rabbit"、"slow.use.nyse"等。这个单词列表最多不能超过255个字节。
如果按照上图的绑定关系:
quick.orange.rabbit会被Q1队列接收
lazy.red.rabbit会被Q2队列接收
lazy.orange.rocket会被Q1和Q2队列接收
quick.red.rocket不会被队列接收会被丢弃
消费者从消息队列中取出消息进行消费,当某些特殊的原因导致队列中的消息无法被消费。这样的消息如果没有后续的处理就成了死信。
普通消费者
public class Consumer1 {
//普通交换机名称
private static final String NORM_EXCHANGE_NAME = "norm_exchange";
//死信交换机名称
private static final String DEAD_EXCHANGE_NAME = "dead_exchange";
//普通队列名称
private static final String NORM_QUEUE_NAME = "norm_queue";
//死信队列名称
private static final String DEAD_QUEUE_NAME = "dead_queue";
public static void main(String[] args) throws Exception{
Channel channel = MQUtil.getChannel();
//声明普通交换机和死信交换机
channel.exchangeDeclare(NORM_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
channel.exchangeDeclare(DEAD_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
Map<String,Object> arguments = new HashMap<>();
//设置消息TTL过期时间的第一种方式(单位毫秒)
//arguments.put("x-message-ttl",10*1000);
//设置普通队列的死信交换机
arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE_NAME);
//设置死信RoutingKey
arguments.put("x-dead-letter-routing-key","lisi");
//声明普通队列
channel.queueDeclare(NORM_QUEUE_NAME,false,false,false,arguments);
//声明死信队列
channel.queueDeclare(DEAD_QUEUE_NAME,false,false,false,null);
//绑定RoutingKey
channel.queueBind(NORM_QUEUE_NAME,NORM_EXCHANGE_NAME,"zhangshang");
channel.queueBind(DEAD_QUEUE_NAME,DEAD_EXCHANGE_NAME,"lisi");
System.out.println("消费者1等待接收消息...");
DeliverCallback deliverCallback = (consumerTag,message)->{
System.out.println("消费者1接收到的消息:"+new String(message.getBody(),"UTF-8"));
};
channel.basicConsume(NORM_QUEUE_NAME,true,deliverCallback,consumerTag ->{});
}
}
死信消费者
public class Consumer2 {
//死信交换机名称
private static final String DEAD_EXCHANGE_NAME = "dead_exchange";
//死信队列名称
private static final String DEAD_QUEUE_NAME = "dead_queue";
public static void main(String[] args) throws Exception{
Channel channel = MQUtil.getChannel();
channel.exchangeDeclare(DEAD_EXCHANGE_NAME,BuiltinExchangeType.DIRECT);
channel.queueDeclare(DEAD_QUEUE_NAME,false,false,false,null);
DeliverCallback deliverCallback = (consumerTag,message)->{
System.out.println("死信消费者接收到的消息:"+new String(message.getBody(),"UTF-8"));
};
channel.basicConsume(DEAD_QUEUE_NAME,true,deliverCallback,consumerTag -> {});
}
}
生产者
public class Producer {
//普通交换机名字
private static final String NORM_EXCHANGE_NAME = "norm_exchange";
private static final String NORM_QUEUE_NAME = "norm_queue";
public static void main(String[] args) throws Exception{
Channel channel = MQUtil.getChannel();
channel.exchangeDeclare(NORM_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
//构建TTL过期时间的第二种方式(单位毫秒)
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();
for (int i = 1; i <11 ; i++) {
String message = "消息"+i;
channel.basicPublish(NORM_EXCHANGE_NAME,"zhangshang",properties,message.getBytes("UTF-8"));
System.out.println("生产者发送信息:"+message);
}
}
}
十秒后普通消费者没有将消息消费,那么就会进入死信队列
然后启动死信消费者程序
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
死信消费者接收到的消息:消息1
死信消费者接收到的消息:消息2
死信消费者接收到的消息:消息3
死信消费者接收到的消息:消息4
死信消费者接收到的消息:消息5
死信消费者接收到的消息:消息6
死信消费者接收到的消息:消息7
死信消费者接收到的消息:消息8
死信消费者接收到的消息:消息9
死信消费者接收到的消息:消息10
生产者代码修改
//将上例生产者代码中的这行代码注释掉
//AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();
普通消费者代码修改
//添加代码
//设置队列最大长度
arguments.put("x-max-length",6);
普通消费者代码
public class Consumer1 {
private static final String NORM_EXCHANGE_NAME = "norm_exchange";
private static final String DEAD_EXCHANGE_NAME = "dead_exchange";
private static final String NORM_QUEUE_NAME = "norm_queue";
private static final String DEAD_QUEUE_NAME = "dead_queue";
public static void main(String[] args) throws Exception{
Channel channel = MQUtil.getChannel();
//声明普通交换机和死信交换机
channel.exchangeDeclare(NORM_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
channel.exchangeDeclare(DEAD_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
Map<String,Object> arguments = new HashMap<>();
//设置普通队列的死信交换机
arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE_NAME);
//设置死信RoutingKey
arguments.put("x-dead-letter-routing-key","lisi");
//声明普通队列
channel.queueDeclare(NORM_QUEUE_NAME,false,false,false,arguments);
//声明死信队列
channel.queueDeclare(DEAD_QUEUE_NAME,false,false,false,null);
//绑定RoutingKey
channel.queueBind(NORM_QUEUE_NAME,NORM_EXCHANGE_NAME,"zhangshang");
channel.queueBind(DEAD_QUEUE_NAME,DEAD_EXCHANGE_NAME,"lisi");
System.out.println("消费者1等待接收消息...");
DeliverCallback deliverCallback = (consumerTag,delivery)->{
String message = new String(delivery.getBody(),"UTF-8");
if("消息5".equals(message)){
//拒绝消息5 参数requeue为false表示拒绝重新入队如果配置了死信交换机就将消息发送到死信队列中
channel.basicReject(delivery.getEnvelope().getDeliveryTag(),false);
}else{
System.out.println("消费者1接收到的消息:"+message);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
}
};
//设置手动应答
channel.basicConsume(NORM_QUEUE_NAME,false,deliverCallback,consumerTag ->{});
}
}
生产者和死信消费者代码不变化
只有被拒绝的消息5进入了死信队列
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
死信消费者正在等待接收消息...
死信消费者接收到的消息:消息5
延时队列就是用来存放需要在指定时间被处理的元素的队列。延时队列的应用场景:
【案例】
创建两个队列 QA 和 QB,两者队列 TTL 分别设置为 10S 和 40S,然后在创建一个交换机 X 和死信交换机 Y,它们的类型都是direct,创建一个死信队列 QD,它们的绑定关系如下:
一、创建SpringBoot工程
2、添加依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.amqpgroupId>
<artifactId>spring-rabbit-testartifactId>
<scope>testscope>
dependency>
三、填写配置文件
spring:
rabbitmq:
host: 192.168.140.129
port: 5672 #15672是客户端端口
username: why
password: 123
四、声明配置类
@Configuration
public class TtlQueueConfig {
public static final String X_EXCHANGE = "X";
public static final String Y_DEAD_LETTER_EXCHANGE = "Y";
public static final String QUEUE_A = "QA";
public static final String QUEUE_B = "QB";
public static final String DEAD_LETTER_QUEUE = "QD";
// 声明 xExchange
@Bean("xExchange")
public DirectExchange xExchange(){
return new DirectExchange(X_EXCHANGE);
}
// 声明 yExchange
@Bean("yExchange")
public DirectExchange yExchange(){
return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);
}
//声明队列QA TTL为 10s 并绑定到对应的死信交换机
@Bean("queueA")
public Queue queueA() {
Map<String, Object> args = new HashMap<>(3);
//声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
//声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key", "YD");
//声明队列的 TTL
args.put("x-message-ttl", 10000);
//构建队列
return QueueBuilder.durable(QUEUE_A).withArguments(args).build();
}
//声明队列QB TTL 为30秒 并绑定到对应的死信交换机
@Bean("queueB")
public Queue queueB(){
Map<String,Object> args = new HashMap<>(3);
args.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);
args.put("x-dead-letter-routing-key","YD");
args.put("x-message-ttl",30000);
return QueueBuilder.durable(QUEUE_B).withArguments(args).build();
}
//声明死信队列 QD
@Bean("queueD")
public Queue queueD(){
return QueueBuilder.durable(DEAD_LETTER_QUEUE).build();
}
//声明队列 QA 绑定 X 交换机
@Bean
public Binding queueQABindingX(@Qualifier("queueA") Queue queueA,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queueA).to(xExchange).with("XA");
}
//声明队列 QB 绑定 X 交换机
@Bean
public Binding queueQBBindingX(@Qualifier("queueB") Queue queueB,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queueB).to(xExchange).with("XB");
}
//声明死信队列 QD 绑定关系
@Bean
public Binding deadLetterBindingQD(@Qualifier("queueD") Queue queueD,
@Qualifier("yExchange") DirectExchange yExchange){
return BindingBuilder.bind(queueD).to(yExchange).with("YD");
}
}
五、生产者代码
@RestController
@RequestMapping("/send")
public class SendMsgController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/{message}")
public void sendMsg(@PathVariable String message){
System.out.println("当前时间"+new Date()+"发送消息个两个TTL队列:"+message);
rabbitTemplate.convertAndSend("X","XA","消息来自TTL为10秒的队列"+message);
rabbitTemplate.convertAndSend("X","XB","消息来自TTL为30秒的队列"+message);
}
}
六、消费者代码
@Component
public class DeadLetterQueueConsumer {
@RabbitListener(queues = "QD")
public void receiveD(Message message, Channel channel) throws Exception {
String msg = new String(message.getBody());
System.out.println("当前时间"+new Date()+"收到死信队列信息"+msg);
}
}
运行结果
当前时间Fri Apr 08 20:01:26 CST 2022发送消息个两个TTL队列:bbb
当前时间Fri Apr 08 20:01:36 CST 2022收到死信队列信息消息来自TTL为10秒的队列bbb
当前时间Fri Apr 08 20:01:56 CST 2022收到死信队列信息消息来自TTL为30秒的队列bbb
上面这种方式创建延时队列把代码写死了。如果要增加一个新的时间需求就要增加一个队列。我们增加一个新的队列,这个队列不指定TTL时长。TTL时长交给生产者来指定
在配置类中添加代码
//声明队列 C 死信交换机
@Bean("queueC")
public Queue queueC(){
Map<String, Object> args = new HashMap<>(3);
//声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
//声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key", "YD");
//没有声明 TTL 属性
return QueueBuilder.durable(QUEUE_C).withArguments(args).build();
}
//声明队列 QC 绑定 X 交换机
@Bean
public Binding queueQCBindingX(@Qualifier("queueC") Queue queueC,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queueC).to(xExchange).with("XC");
}
由生产者提供TTL时长
@GetMapping("/{message}/{ttlTime}")
public void sendMsg(@PathVariable String message,@PathVariable String ttlTime){
System.out.println("当前时间"+new Date()+"发送消息给队列C:"+ttlTime+",ttl时长为"+message+"毫秒");
rabbitTemplate.convertAndSend("X","XC","消息来自TTL自定义的队列"+message,msg -> {
//设置ttl时长
msg.getMessageProperties().setExpiration(ttlTime);
return msg;
});
}
运行结果
initialization in 5 ms
当前时间Fri Apr 08 20:31:37 CST 2022发送消息给队列C:aaa,ttl时长为20000毫秒
当前时间Fri Apr 08 20:31:57 CST 2022收到死信队列信息消息来自TTL自定义的队列aaa
虽然上面的代码经过优化但是存在一个非常严重的问题:一旦发送两条以上的消息延时消息是按照先来后到的顺序处理,而不是按照TTL时间长短处理。
当前时间Fri Apr 08 20:34:28 CST 2022发送消息给队列C:aaa,ttl时长为20000毫秒
当前时间Fri Apr 08 20:34:32 CST 2022发送消息给队列C:aaa,ttl时长为2000毫秒
当前时间Fri Apr 08 20:34:48 CST 2022收到死信队列信息消息来自TTL自定义的队列aaa
当前时间Fri Apr 08 20:34:48 CST 2022收到死信队列信息消息来自TTL自定义的队列aaa
一、将插件复制到容器的plugins目录下
docker cp [插件名] [容器名或ID]:/plugins
docker cp rabbitmq_delayed_message_exchange-3.9.0.ez 51dd8ab5591a:/plugins
二、进入rabbitMQ容器内部
docker exec -it [容器名] bash
docker exec -it mq bash
三、安装插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
四、查看命令
rabbitmq-plugins list
配置类
@Configuration
public class DelayedQueueConfig {
public static final String DELAYED_QUEUE_NAME = "delayed.queue";
public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";
@Bean
public Queue delayedQueue() {
return new Queue(DELAYED_QUEUE_NAME);
}
@Bean
public CustomExchange delayedExchange() {
Map<String, Object> args = new HashMap<>();
//自定义交换机的类型
args.put("x-delayed-type", "direct");
return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message", true, false,args);
}
@Bean
public Binding bindingDelayedQueue(@Qualifier("delayedQueue") Queue queue,
@Qualifier("delayedExchange") CustomExchange
delayedExchange) {
return BindingBuilder.bind(queue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
}
}
生产者
@GetMapping("sendDelayMsg/{message}/{delayTime}")
public void sendMsg(@PathVariable String message, @PathVariable Integer delayTime) {
System.out.println("当前时间"+new Date()+"发送消息给队列C:"+message+"延时为"+delayTime+"毫秒");
rabbitTemplate.convertAndSend(DELAYED_EXCHANGE_NAME, DELAYED_ROUTING_KEY, message, correlationData -> {
correlationData.getMessageProperties().setDelay(delayTime);
return correlationData;
});
}
消费者
@Component
public class DelayedQueueConsumer {
@RabbitListener(queues = DelayedQueueConfig.DELAYED_QUEUE_NAME)
public void receiveD(Message message, Channel channel) throws Exception {
String msg = new String(message.getBody());
System.out.println("当前时间"+new Date()+"收到死信队列信息"+msg);
}
}
当前时间Fri Apr 08 22:45:00 CST 2022发送消息给队列C:aaa延时为20000毫秒
当前时间Fri Apr 08 22:45:03 CST 2022发送消息给队列C:aaa延时为2000毫秒
当前时间Fri Apr 08 22:45:05 CST 2022收到死信队列信息aaa
当前时间Fri Apr 08 22:45:20 CST 2022收到死信队列信息aaa
在生产环境中由于一些不明原因,导致 rabbitmq 重启,在 RabbitMQ 重启期间生产者消息投递失败,导致消息丢失,需要手动处理和恢复。这些无法投递的消息改如何处理?
一、添加配置文件
spring:
rabbitmq:
host: 192.168.140.129
port: 5672
username: why
password: 123
#开启确认发布模式
publisher-confirm-type: correlated
#允许回退消息
publisher-returns: true
二、添加配置类
@Configuration
public class ConformConfig {
public static final String CONFORM_EXCHANGE_NAME = "confirm_exchange";
public static final String CONFORM_QUEUE_NAME = "confirm_queue";
public static final String CONFORM_ROUTING_KEY = "confirm_routing_key";
//交换机
@Bean("conformExchange")
public DirectExchange conformExchange(){
return new DirectExchange(CONFORM_EXCHANGE_NAME);
}
//队列
@Bean("conformQueue")
public Queue conformQueue(){
return QueueBuilder.durable(CONFORM_QUEUE_NAME).build();
}
//绑定RoutingKey
@Bean
public Binding queueBindingExchange(@Qualifier("conformExchange") DirectExchange conformExchange,
@Qualifier("conformQueue") Queue conformQueue){
return BindingBuilder.bind(conformQueue).to(conformExchange).with(CONFORM_ROUTING_KEY);
}
}
三、消息生产者
@GetMapping("/conform/{message}")
public void send(@PathVariable String message){
System.out.println("发送消息:"+message);
CorrelationData correlationData = new CorrelationData("1");
//模拟消息队列收不到消息
rabbitTemplate.convertAndSend(ConformConfig.CONFORM_EXCHANGE_NAME,ConformConfig.CONFORM_ROUTING_KEY+1,message,correlationData);
//模拟交换机接收不到消息
rabbitTemplate.convertAndSend(ConformConfig.CONFORM_EXCHANGE_NAME+1,ConformConfig.CONFORM_ROUTING_KEY,message,correlationData);
}
四、消息消费者
@Component
public class ConformConsumer {
@RabbitListener(queues = ConformConfig.CONFORM_QUEUE_NAME)
public void receiveD(Message message, Channel channel) throws Exception {
String msg = new String(message.getBody());
System.out.println("收到队列信息:"+msg);
}
}
五、实现回调接口
@Component
public class MyCallBack implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
//注入对象到RabbitTemplate
@PostConstruct
public void init(){
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnCallback(this);
}
/*
* 交换机确认回调方法,什么时候触发回调?
* 1、交换机接收到了消息会触发回调
* correlationData:保存回调消息的ID及相关信息
* ack:true表示交换机收到消息
* cause:消息接收失败原因,成功情况下为null
* 2、交换机接收消息失败会触发回调
* correlationData:保存回调消息的ID及相关信息
* ack:false表示交换机接收消息失败
* cause:消息接收失败原因
* */
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
String id = correlationData != null ? correlationData.getId(): "";
if (ack){
System.out.println("交换机收到ID为"+id+"的消息");
}else{
System.out.println("交换机未收到ID为"+id+"的消息"+",原因为"+cause);
}
}
/*如果消息不可达到目的地址就将消息返回给生产者
* 只有消息不可达时触发回调
* */
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("消息"+message.getBody()+",被交换机回退"+exchange+"回退的原因为:"+replyText+",路由key为:"+routingKey);
}
}
运行结果
发送消息:hello
消息[B@3bd5d358,被交换机回退confirm_exchange回退的原因为:NO_ROUTE,路由key为:confirm_routing_key1
交换机收到ID为1的消息
2022-04-09 10:59:27.162 ERROR 17312 --- [68.140.129:5672] o.s.a.r.c.CachingConnectionFactory : Channel shutdown: channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'confirm_exchange1' in vhost '/', class-id=60, method-id=40)
交换机未收到ID为1的消息,原因为channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'confirm_exchange1' in vhost '/', class-id=60, method-id=40)
备份交换机可以理解为 RabbitMQ 中交换机的“备胎”,当我们为某一个交换机声明一个对应的备份交换机时,就是为它创建一个备胎,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由备份交换机来进行转发和处理,通常备份交换机的类型为 Fanout ,这样就能把所有消息都投递到与其绑定的队列中,然后我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都进入这个队列了。当然,我们还可以建立一个报警队列,用独立的消费者来进行监测和报警。
配置类
@Configuration
public class ConformConfig {
public static final String CONFORM_EXCHANGE_NAME = "confirm_exchange";
public static final String CONFORM_QUEUE_NAME = "confirm_queue";
public static final String CONFORM_ROUTING_KEY = "confirm_routing_key";
public static final String BACKUP_EXCHANGE_NAME = "backup_exchange";
public static final String BACKUP_QUEUE_NAME = "backup_queue";
public static final String WARNING_QUEUE_NAME = "warning_queue";
//交换机
@Bean("conformExchange")
public DirectExchange conformExchange(){
//创建确认交换机时指定备份交换机
return ExchangeBuilder.directExchange(CONFORM_EXCHANGE_NAME).durable(true).withArgument("alternate-exchange",BACKUP_EXCHANGE_NAME).build();
}
//队列
@Bean("conformQueue")
public Queue conformQueue(){
return QueueBuilder.durable(CONFORM_QUEUE_NAME).build();
}
//绑定RoutingKey
@Bean
public Binding queueBindingExchange(@Qualifier("conformExchange") DirectExchange conformExchange,
@Qualifier("conformQueue") Queue conformQueue){
return BindingBuilder.bind(conformQueue).to(conformExchange).with(CONFORM_ROUTING_KEY);
}
//备份交换机
@Bean("backupExchange")
public FanoutExchange backupExchange(){
return new FanoutExchange(BACKUP_EXCHANGE_NAME);
}
//备份队列
@Bean("backupQueue")
public Queue backupQueue(){
return QueueBuilder.durable(BACKUP_QUEUE_NAME).build();
}
//警告队列
@Bean("warningQueue")
public Queue warningQueue(){
return QueueBuilder.durable(WARNING_QUEUE_NAME).build();
}
//绑定RoutingKey
@Bean
public Binding backupQueueBindingBackupExchange(@Qualifier("backupExchange") FanoutExchange backupExchange,
@Qualifier("backupQueue") Queue backupQueue){
return BindingBuilder.bind(backupQueue).to(backupExchange);
}
@Bean
public Binding warningQueueBindingBackupExchange(@Qualifier("backupExchange") FanoutExchange backupExchange,
@Qualifier("warningQueue") Queue warningQueue){
return BindingBuilder.bind(warningQueue).to(backupExchange);
}
}
警告消费者(另外两个消费者仅监听队列不同)
@Component
public class WarningConsumer {
@RabbitListener(queues = ConformConfig.WARNING_QUEUE_NAME)
public void receiveD(Message message, Channel channel) throws Exception {
String msg = new String(message.getBody());
System.out.println("收到报警队列不可路由信息:"+msg);
}
}
生产者
@GetMapping("/conform/{message}")
public void send(@PathVariable String message){
System.out.println("发送消息:"+message);
CorrelationData correlationData = new CorrelationData("1");
//模拟消息队列收不到消息
rabbitTemplate.convertAndSend(ConformConfig.CONFORM_EXCHANGE_NAME,ConformConfig.CONFORM_ROUTING_KEY+1,message,correlationData);
}
结果
发送消息:hello
交换机收到ID为1的消息
收到报警队列不可路由信息:hello
如果同时配置了备份交换机和回退消息,备份交换机的优先级要大于消息回退。
Map<String, Object> params = new HashMap();
params.put("x-max-priority", 10);
channel.queueDeclare("hello", true, false, false, params);
要让队列实现优先级需要做的事情有如下事情:队列需要设置为优先级队列,消息需要设置消息的优先级,消费者需要等待消息已经发送到队列中才去消费因为,这样才有机会对消息进行排序
生产者
public class Producer {
private static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception{
Channel channel = MQUtil.getChannel();
Map<String,Object> arguments = new HashMap<>();
arguments.put("x-max-priority",10);
for (int i = 1; i < 11; i++) {
String message = "message"+i;
if (i == 5){
//设置消息优先级为5
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().priority(5).build();
channel.basicPublish("",QUEUE_NAME,properties,message.getBytes());
}else{
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
}
}
System.out.println("消息发送完毕");
}
}
消费者
public class Consumer {
private static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception{
Channel channel = MQUtil.getChannel();
System.out.println("正在等待消息接收");
DeliverCallback deliverCallback = (consumerTag,delivery)->{
String message = new String(delivery.getBody());
System.out.println("消费者接收到的消息"+message);
};
CancelCallback cancelCallback = (consumerTag)->{
System.out.println("消费者被中断");
};
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
结果显示message5最先处理
正在等待消息接收
消费者接收到的消息message5
消费者接收到的消息message1
消费者接收到的消息message2
消费者接收到的消息message3
消费者接收到的消息message4
消费者接收到的消息message6
消费者接收到的消息message7
消费者接收到的消息message8
消费者接收到的消息message9
消费者接收到的消息message10