文章目录
问题
经过前面几篇的讲述。相信你已经可以简单的写出关于mq的程序了,但是还存在很多的问题,如果你仔细思考的话。这篇讲述其中的一个问题,后面可能会专门出一个遇到的问题。见名知意,当消息不可到达,此时消息应该怎么办呢?
mandatory
当没有消息队列与交换器绑定时,此时消息不可到达队列,无法被消费。先看前面一篇 的代码
/**
* 发布消息
* exchange:发布的交换器名称
* routing key:路由键
* mandatory:后面有单独出一篇,不是本篇重点
* immediate:后面有单独出一篇,不是本篇重点
* BasicProperties:设置属性,比如消息类型,持久化等等,可以new AMQP.BasicProperties().builder()自定义建造也可以使用提供的。
* body:消息,是个字节数组
*/
channel.basicPublish("ex1", "rk1", false, false, MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes());
此时我们看第三个参数mandatory,如果设置为true,则代表当没有队列与其交换器绑定时,返回消息到生产者客户端。如果false,则代表丢弃该消息。具体实现
channel.exchangeDeclare("ex4", BuiltinExchangeType.DIRECT, true, false, false, null);
//通过设置ReturnListener监听器来监听返回的消息
channel.addReturnListener(new ReturnListener() {
@Override
public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) {
log.info("{}:{}:{}:{}:{}", replyCode, replyText, exchange, routingKey, new String(body));
}
});
channel.basicPublish("ex4", "rk4", true, false, MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes());
Thread.sleep(3 * 1000);
备份交换器(alternate exchange)
备份交换器简称AE,我们可以通过设置一个AE,达到消息保存并不把消息返回给客户端的目的,好处是消息不会丢失,可以在需要的时候去消费它。在使用mandatory时,当你消息发出去,但是因为网络原因或生产者宕机了,Basic.return到达不了生产者,那么就监听不到消息的返回,此时就有问题了。
public class AEProducer {
Connection connection = RabbitmqUtil.conn();
public void send(String msg) {
assert connection != null;
try (Channel channel = connection.createChannel()) {
Map<String, Object> args = new HashMap<>();
args.put("alternate-exchange", "ae1");
channel.exchangeDeclare("ex4", BuiltinExchangeType.DIRECT, true, false, args);
channel.exchangeDeclare("ae1", BuiltinExchangeType.FANOUT, true);
channel.queueDeclare("q4", true, false, false, null);
channel.queueBind("q4", "ae1", "rk4", null);
channel.basicPublish("ex4", "rk4", false, false, MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes());
} catch (Exception e) {
e.printStackTrace();
} finally {
RabbitmqUtil.close(connection);
}
}
}
这里声明了两个交换器ex4和ae1,ex4是一个类型为direct的交换器,为ex4添加alternate-exchange参数,指定ae1为其备份交换器。ae1是类型为fanout的交换器。绑定队列q4,如果ae1是direct类型,则需要设置其绑定键为生产者与ex4的路由键。此时我们运行发现,因为ex4没有找到绑定的队列,把消息传到ae1,继而传到队列q4,至于为什么这里的ae1设置了fanout类型,而不是direct类型,因为防止有多个路由键导致消息丢失,根据实际情况来定义类型吧。
immediate
如果一个队列没有任何一个消费者去消费消息,此时设置immediate为true则代表直接将消息返回给生产者。但是RabbitMQ 3.0.0之后就去掉了该参数的作用,因为会影响镜像的性能。关于镜像后面会有出其它的博客。
官方对此参数的注解也表示不支持该注解
* @param immediate true if the 'immediate' flag is to be
* set. Note that the RabbitMQ server does not support this flag.
那么没有immediate参数,我们又应该怎么办呢?可以设置过期时间存放到死信队列中。
消息和队列的TTL
消息TTL
什么是TTL呢?全称是time to live,翻译过来就是存活时间。当超过了消息在队列中存在的TTL还没有被消费,那么,该消息就会变成死信。我们先看如何设置消息的存活时间。
public class DeadMsgProducer {
Connection connection = RabbitmqUtil.conn();
public void send(String msg) {
assert connection != null;
try (Channel channel = connection.createChannel()) {
channel.exchangeDeclare("ex7", BuiltinExchangeType.DIRECT, true);
channel.queueDeclare("q7", true, false, false, null);
channel.queueBind("q7", "ex7", "rk7", null);
AMQP.BasicProperties basicProperties=new AMQP.BasicProperties().builder()
.contentType(MessageProperties.TEXT_PLAIN.getContentType())
.deliveryMode(2)//设置持久化
.expiration("10000")//设置消息的过期时间,单位ms
.build();
channel.basicPublish("ex7", "rk7", false, false,basicProperties, msg.getBytes());
} catch (Exception e) {
e.printStackTrace();
} finally {
RabbitmqUtil.close(connection);
}
}
}
通过在发布消息的时候设置expiration()来设置过期时间,单位是ms。那么,如果这个时间设置成0会出现什么呢?其实是该消息要立即被消费,即绑定的队列存在消费者。如果不存在会立即丢弃。这点体现了immediate参数的特性之一 ,当队列没有消费者时,不存消息到队列中。
//创建线程池保证消费者先订阅,然后生成者发送过期时间是0的消息
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.submit(() -> {
new Consumer().recieve("consumer1");
});
executorService.submit(() -> {
new DeadMsgProducer().send("hello world!");
//new Consumer().recieve("consumer2");
});
try {
Thread.sleep(5*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
通过以上代码模拟证明能够接受到消息,但是当我们串行执行,即生产者发送消息,但是没有消费者订阅,此时接收不到消息。我们通过管理界面也看不到该队列有消息。
上面是我们可以对每一个发布的消息进行控制,当然我们也可以统一设置队列中所有的消息TTL。如下
Map<String,Object> args=new HashMap<>();
args.put("x-message-ttl",10000);//设置该队列中消息的过期时间,单位ms
channel.queueDeclare("q7", true, false, false, args);
当然,如果你两种方法同时用的话,那么,将取过期时间最小的为准。
过期消息删除过程
消息过期即代表没有用了,那么又是如何删除的呢?如果你单独给消息设置TTL,则当其将被消费的时候检测是否过期,如果过期则删除。而当我们在声明队列的时候去设置TTL,RabbitMQ会定时检测队列中的过期消息然后删除,因为过期的消息先存到队列,所以过期的消息都在队列头部,这样不需要遍历检测所有的队列。这也是两种不同删除实现方式的原因,都是避免遍历检测。
队列TTL
设置队列的TTL其实和设置消息的TTL很相似,只需在声明队列的时候指定参数"x-expires"即可,单位ms,但是设置的值必须大于0。
Map<String,Object> args=new HashMap<>();
args.put("x-expires",10000);//设置该队列过期时间,单位ms,该值>0
channel.queueDeclare("q8", true, false, false, args);
死信交换器(DLX)与死信队列(DLQ)
DLX(dead letter exchange)当队列中存在死信时。此时消息可以被重发到死信交换器上。
绑定DLX的队列就是死信队列(dead letter queue)。
消息遇到下列情况会变成死信:
- 消费者拒绝消息,并且设置消息重入队列,即requeue参数为true
- 消息过期
- 队列消息数达到最大
public class DeadMsgProducer {
Connection connection = RabbitmqUtil.conn();
public void send(String msg) {
assert connection != null;
try (Channel channel = connection.createChannel()) {
channel.exchangeDeclare("ex8", BuiltinExchangeType.DIRECT, true);
//声明死信交换器dlx1
channel.exchangeDeclare("dlx1",BuiltinExchangeType.DIRECT,true);
Map<String,Object> args=new HashMap<>();
//设置该队列中消息的过期时间,单位ms
args.put("x-message-ttl",0);
//设置dlx
args.put("x-dead-letter-exchange","dlx1");
//设置dlx1与dlq1的路由键,如果不设置就是发布消息时的路由键
args.put("x-dead-letter-routing-key","dlrk1");
channel.queueDeclare("q8", true, false, false, args);
channel.queueBind("q8", "ex8", "rk8", null);
channel.queueDeclare("dlq1", true, false, false, null);
channel.queueBind("dlq1", "dlx1", "dlrk1", null);
AMQP.BasicProperties basicProperties=new AMQP.BasicProperties().builder()
.contentType(MessageProperties.TEXT_PLAIN.getContentType())
.deliveryMode(2)//设置持久化
//.expiration("0")//设置消息的过期时间,单位ms
.build();
channel.basicPublish("ex8", "rk8", false, false,basicProperties, msg.getBytes());
} catch (Exception e) {
e.printStackTrace();
} finally {
RabbitmqUtil.close(connection);
}
}
}
通过设置TTL为0和设置死信队列进行消息存储。我们实现了代替immediate参数,可能有人会问,你这也没有返回数据到生产者。其实你在生产者订阅死信队列不就ok了。
延迟队列
延迟队列就是指当一个消息并不想立即被消费,而是等到一定的时间在被消费。比如我们下订单,但是订单会在一定时间内支付或不支付操作。此时可以通过延时队列进行实现,当然如果你的系统并没有加入消息,其实也是有其它方法实现的。毕竟我们不可能因为某一个需求就加入消息中间件,其维护和开发成本还是要考虑的。
其实延迟队列就是通过死信队列+TTL实现的。我们只需设置TTL的值就可以做到延迟队列。
优先级队列
优先级队列就是给队里设置一个最大优先级。在发布消息的时候指定消息的优先级,不指定则是默认0,消费者会优先消费优先级高的消息。这是队列存在多个消息时才会生效。
public class PriorityProducer {
Connection connection = RabbitmqUtil.conn();
public void send(String msg) {
assert connection != null;
try (Channel channel = connection.createChannel()) {
channel.exchangeDeclare("ex9", BuiltinExchangeType.DIRECT, true);
Map<String, Object> args = new HashMap<>();
args.put("x-max-priority", 10);
channel.queueDeclare("q9", true, false, false, args);
channel.queueBind("q9", "ex9", "rk9", null);
for (int i = 0; i < 10; i++) {
AMQP.BasicProperties basicProperties = new AMQP.BasicProperties().builder()
.contentType(MessageProperties.TEXT_PLAIN.getContentType())
.deliveryMode(2)//设置持久化
.priority(i)//设置优先级,优先级最大值为队列声明时的最大值
.build();
channel.basicPublish("ex9", "rk9", false, false, basicProperties, (msg+i).getBytes());
}
} catch (Exception e) {
e.printStackTrace();
} finally {
RabbitmqUtil.close(connection);
}
}
}