RabbitMQ学习(四)——消息不可达处理以及各种队列

问题

  经过前面几篇的讲述。相信你已经可以简单的写出关于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);
        }
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陈大侠在江湖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值