假设我们有一个场景,生产者有消息发到某个直连交换机,这个交换机上有两个队列分别存储两种类型的消息,但是与这两个队列相连的消费者太不争气了,处理消息有点慢,我们想5秒钟这个消息在队列中还没有被消费的话,就给它丢进死信队列里得了(我们平时听到的延时队列其实就可按此方法实现,故意让它过期然后延时处理),后续再处理,但是这俩队列明显存储的消息不一样,我们又不好意思将它都扔到同一个死信队列里去,如果我们想要俩死信队列分别装这两个消费者漏掉的消息,那我们怎么做呢?
下面就是一个简单的例子,如果用spring boot之类的去做也是类似,原理差不多,感兴趣的可以自己改造。
预处理:我们先创建一个工具类用来连接rabbitmq,注意你需要去创建对应的虚拟主机,以及对应的登录账号和密码。
工具类如下:
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
public class ConnectionUtil {
public static Connection getConnection() throws Exception {
//定义连接工厂
ConnectionFactory factory = new ConnectionFactory();
//设置服务地址
factory.setHost("localhost");
//端口
factory.setPort(5672);
//设置账号信息,用户名、密码、vhost
//VirtualHost(虚拟主机)是一个逻辑上独立的RabbitMQ服务实例。每个VirtualHost都有自己的队列、交换机、绑定等对象,并且它们之间是相互隔离的,即exchange、queue、message不能互通。
factory.setVirtualHost("myVirtualHost");
factory.setUsername("mytest");
factory.setPassword("mytest");
// 通过工程获取连接
Connection connection = factory.newConnection();
return connection;
}
}
现在我们有一个直连交换机test_exchange_direct(直连交换机即根据设置的固定键直接路由到对应的队列,注意与主题topic队列的区分),我们往这个交换机里每300毫秒分别发送键为good和bad的数据各30条。
import com.dubbo.study.dubbostudy.utils.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
public class SendToExchange {
private final static String EXCHANGE_NAME = "test_exchange_direct";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明exchange
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
//这里要注意,如果你没有响应的队列的话即交换机还没有绑定队列,发送消息到交换机这些消息会丢失。
for (int i = 0; i < 30; i++) {
// 消息内容
String message = "good " + i;
//会路由到good对应的队列上
channel.basicPublish(EXCHANGE_NAME, "good", null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
Thread.sleep(300);
}
for (int i = 0; i < 30; i++) {
// 消息内容
String message = "bad " + i;
//会路由到bad对应的队列上
channel.basicPublish(EXCHANGE_NAME, "bad", null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
Thread.sleep(300);
}
channel.close();
connection.close();
}
}
我们再创建一个直连死信交换机dead_exchange_direct,和连接到此私信交换机上的两个队列dead_queue,dead_queue1,对应的键分别为dead-good和dead-bad。
import com.dubbo.study.dubbostudy.utils.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
public class DeadExchange {
private final static String EXCHANGE_NAME = "dead_exchange_direct";
private final static String QUEUE_NAME = "dead_queue";
private final static String QUEUE_NAME1 = "dead_queue1";
public static void main(String[] argv) throws Exception {
channel1();
channel2();
}
public static void channel1() throws Exception{
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明exchange
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
// 声明队列 注意:一个消费者队列可以有多个消费者实例,只有其中一个消费者实例会消费
// 队列名称,是否持久化,是否排他,是否自动删除,自定义属性
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
// 绑定队列到交换机 死信路由键为dead
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "dead-good");
channel.close();
connection.close();
}
public static void channel2() throws Exception{
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明exchange
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
// 声明队列 注意:一个消费者队列可以有多个消费者实例,只有其中一个消费者实例会消费
// 队列名称,是否持久化,是否排他,是否自动删除,自定义属性
channel.queueDeclare(QUEUE_NAME1, true, false, false, null);
// 绑定队列到交换机 死信路由键为dead
channel.queueBind(QUEUE_NAME1, EXCHANGE_NAME, "dead-bad");
channel.close();
connection.close();
}
第一个不争气消费者RecvFromExchange,这个消费者对应的队列是good_queue队列,它800毫秒能处理一条消息,给他设置读取队列消息过期时间为5秒,绑定dead_exchange_direct死信交换机,死信队列路由键为dead-good。
import com.dubbo.study.dubbostudy.utils.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import java.util.HashMap;
import java.util.Map;
public class RecvFromExchange {
private final static String QUEUE_NAME = "good_queue";
private final static String EXCHANGE_NAME = "test_exchange_direct";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
Map<String,Object> args = new HashMap<>();
args.put("x-message-ttl",5000);
args.put("x-dead-letter-exchange","dead_exchange_direct");
args.put("x-dead-letter-routing-key","dead-good"); // 死信路由键dead 路由到键为dead的死信队列
// 声明队列 注意:一个消费者队列可以有多个消费者实例,只有其中一个消费者实例会消费
// 队列名称,是否持久化,是否排他,是否自动删除,自定义属性
channel.queueDeclare(QUEUE_NAME, true, false, false, args);
// 绑定队列到交换机
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "good");
// 同一时刻服务器只会发一条消息给消费者
channel.basicQos(1);
// 定义队列的消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
// 监听队列,手动返回完成
channel.basicConsume(QUEUE_NAME, false, consumer);
// 获取消息
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [Recv] Received '" + message + "'");
Thread.sleep(800);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
}
第二个不争气消费者RecvFromExchange2,这个消费者对应的队列是bad_queue队列,它1秒能处理一条消息,它虽然慢一些但是我就是一视同仁给他设置读取队列消息过期时间也为5秒,绑定dead_exchange_direct死信交换机,死信队列路由键为dead-bad。
import com.dubbo.study.dubbostudy.utils.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import java.util.HashMap;
import java.util.Map;
public class RecvFromExchange2 {
private final static String QUEUE_NAME = "bad_queue";
private final static String EXCHANGE_NAME = "test_exchange_direct";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
Map<String,Object> args = new HashMap<>();
args.put("x-message-ttl",5000);
args.put("x-dead-letter-exchange","dead_exchange_direct");
args.put("x-dead-letter-routing-key","dead-bad"); // 死信路由键dead 路由到键为dead的死信队列
// 声明队列 注意:一个消费者队列可以有多个消费者实例,只有其中一个消费者实例会消费
// 队列名称,是否持久化,是否排他,是否自动删除,自定义属性
channel.queueDeclare(QUEUE_NAME, true, false, false, args);
// 绑定队列到交换机
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "bad");
// 同一时刻服务器只会发一条消息给消费者
channel.basicQos(1);
// 定义队列的消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
// 监听队列,手动返回完成
channel.basicConsume(QUEUE_NAME, false, consumer);
// 获取消息
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [Recv] Received '" + message + "'");
Thread.sleep(1000);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
}
按顺序先启动DeadExchange,SendToExchange,RecvFromExchange,RecvFromExchange2。然后再次启动SendToExchange,重新发数据观察发现,这两个不争气的消费者漏掉的数据最后被死信队列接收了。
接下来我们对我们喜欢的绑定键dead-good的好队列给它兜底擦屁股。
import com.dubbo.study.dubbostudy.utils.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
public class FuckDeadQueue {
private final static String EXCHANGE_NAME = "dead_exchange_direct";
private final static String QUEUE_NAME = "dead_queue";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 绑定队列到交换机 死信路由键为dead
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "dead-good");
// 同一时刻服务器只会发一条消息给消费者
channel.basicQos(1);
// 定义队列的消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
// 监听队列,手动返回完成
channel.basicConsume(QUEUE_NAME, false, consumer);
// 获取消息
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
//这里我们可以只给他兜底不包含1的内容
if(message.contains("1")){
System.out.println(" [Recv] Received '" + message + "'");
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
} else{
System.out.println(" 不处理包含1的数据 '" + message + "'");
channel.basicNack(delivery.getEnvelope().getDeliveryTag(),true,false);
}
Thread.sleep(1000);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
}
执行后发现,死信队列里的消息被我们消费掉了。
注:
在RabbitMQ中,basicNack
和basicReject
方法都用于否定确认(即拒绝)从队列中接收到的消息。然而它们之间有一些重要的区别。
-
basicNack 方法:
basicNack
方法用于否定当前消息,并且可以批量拒绝消息。- 它有三个参数:
deliveryTag
: 一个长整型值,表示要否定的消息的标识。multiple
: 一个布尔值。如果设置为true
,则否定所有小于或等于deliveryTag
的消息。如果设置为false
,则仅否定具有特定deliveryTag
的消息。requeue
: 一个布尔值。如果设置为true
,则RabbitMQ会重新将这条消息存入队列,以便发送给下一个订阅的消费者。如果设置为false
,则RabbitMQ会立即从队列中移除这条消息,而不会把它发送给新的消费者。
-
basicReject 方法:
basicReject
方法用于明确拒绝当前的消息而不是确认。- 它有两个参数:
deliveryTag
: 一个长整型值,表示要拒绝的消息的标识。requeue
: 一个布尔值。与basicNack
方法中的requeue
参数具有相同的含义。
还得注意,如果requeue设置成true即重新让这个数据入队列,那么它会一直等待有别的消费者消费这个消息后它才会继续往下走,否则他会一直不停的取这个被拒绝的消息。
接下来,再给一个新场景:假设一个队列上有两个消费者RecvFromExchange和RecvFromExchange22,其中消费者RecvFromExchange比较菜,对某一类型的消息它消费不了,这个消费不了的消息RecvFromExchange22消费者能消费,所以也不能RecvFromExchange不能将这个消息扔进死信队列,RecvFromExchange想把这个消息再重新扔进队列里让RecvFromExchange22去消费,那怎么做呢?
首先生产者:
import com.dubbo.study.dubbostudy.utils.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
public class SendToExchange {
private final static String EXCHANGE_NAME = "test_exchange_direct";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明exchange
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
/*for (int i = 0; i < 30; i++) {
// 消息内容
String message = "删除商品" + i;
channel.basicPublish(EXCHANGE_NAME, "delete", null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
Thread.sleep(100);
}*/
/*for (int i = 0; i < 30; i++) {
// 消息内容
String message = "添加商品" + i;
channel.basicPublish(EXCHANGE_NAME, "insert", null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
Thread.sleep(100);
}*/
for (int i = 0; i < 30; i++) {
// 消息内容
String message = "更新商品" + i;
channel.basicPublish(EXCHANGE_NAME, "update", null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
Thread.sleep(100);
}
channel.close();
connection.close();
}
}
消费者RecvFromExchange。
import com.dubbo.study.dubbostudy.utils.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.MessageProperties;
import com.rabbitmq.client.QueueingConsumer;
import java.util.HashMap;
import java.util.Map;
public class RecvFromExchange {
private final static String QUEUE_NAME = "test_queue_work1";
private final static String EXCHANGE_NAME = "test_exchange_direct";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
Map<String,Object> args = new HashMap<>();
args.put("x-max-retries",3);
args.put("x-dead-letter-exchange","dead_exchange_direct");
args.put("x-dead-letter-routing-key","dead-good"); // 死信路由键dead 路由到键为dead的死信队列
// 声明队列 注意:一个消费者队列可以有多个消费者实例,只有其中一个消费者实例会消费
// 队列名称,是否持久化,是否排他,是否自动删除,自定义属性
channel.queueDeclare(QUEUE_NAME, false, false, false, args);
// 绑定队列到交换机
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "update");
//channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "delete");
// 同一时刻服务器只会发一条消息给消费者
channel.basicQos(1);
// 定义队列的消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
// 监听队列,手动返回完成
channel.basicConsume(QUEUE_NAME, false, consumer);
// 获取消息
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
if(message.contains("1")){
System.out.println(" [Recv] Received '" + message + "'");
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
} else{
System.out.println(" 不包含1的数据回退到队列 '" + message + "'");
//requeue: 一个布尔值。如果设置为true,则RabbitMQ会重新将这条消息存入队列,以便发送给下一个订阅的消费者。如果设置为false,但是要注意的是basicNack是回到队首,可能会被当前消费者又给消费了造成死循环,
// 则RabbitMQ会立即从队列中移除这条消息,而不会把它发送给新的消费者。
channel.basicNack(delivery.getEnvelope().getDeliveryTag(),true,false);
channel.basicPublish(EXCHANGE_NAME,
"update", null,
message.getBytes());
}
Thread.sleep(500);
}
}
}
消费者RecvFromExchange22。
import com.dubbo.study.dubbostudy.utils.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import java.util.HashMap;
import java.util.Map;
public class RecvFromExchange22 {
private final static String QUEUE_NAME = "test_queue_work1";
private final static String EXCHANGE_NAME = "test_exchange_direct";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
Map<String,Object> args = new HashMap<>();
args.put("x-max-retries",3);
args.put("x-dead-letter-exchange","dead_exchange_direct");
args.put("x-dead-letter-routing-key","dead-good"); // 死信路由键dead 路由到键为dead的死信队列
// 声明队列 注意:一个消费者队列可以有多个消费者实例,只有其中一个消费者实例会消费
// 队列名称,是否持久化,是否排他,是否自动删除,自定义属性
channel.queueDeclare(QUEUE_NAME, false, false, false, args);
// 绑定队列到交换机
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "update");
// 同一时刻服务器只会发一条消息给消费者
channel.basicQos(2);
// 定义队列的消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
// 监听队列,手动返回完成
channel.basicConsume(QUEUE_NAME, false, consumer);
// 获取消息
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [Recv2] Received '" + message + "'");
Thread.sleep(500);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
}
俩消费者消费情况。
但是要注意消费者RecvFromExchange如果消费能力比较快,它最后会再次消费到他自己回退的消息,直至这个消息被另一个消费者消费走为止(如下图),这里我们如果要用死信队列就需要用消费重试机制了如:x-max-retries,将这些放入死信队列里进行兜底操作,如果就是不想用死信队列,那就硬着头皮让他重试吧,不过这里重试可能会造成一些业务上的问题需要注意如重试的时候不停的刷数据库或者别的什么占用资源的操作,需要把程序写的健壮些。