RabbitMQ进阶

目录

RabbitMQ进阶_第1张图片

1、RabbitMQ消息何去何从

channel.basicPublish方法两个参数,mandatory和immediate,他们都有当消息传递过程不可达到目的地时会将消息返回给生产者的功能

mandatory参数

当mandatory参数设为true时,交换器无法根据自身的类型和路由键找到一个符合条件的队列,那么RabbitMQ会调用Basic.Return命令将消息返回给生产者。当mandatory参数设置为false时,出现上述情形,则消息直接被丢弃。RabbitMQ提供的备份交换器(Alternate Exchange)可以将未能被交换器路由的消息(没有绑定队列或没有匹配的绑定)存储起来,不返回给客户端

使用mandatory参数的关键代码

channel.basicPublish(EXCHANGE_NAME, "", true,
        MessageProperties.PERSISTENT_TEXT_PLAIN,
        "mandatory test".getBytes()); 
channel.addReturnListener(new ReturnListener() {
public void handleReturn(int replyCode, String replyText,
                         String exchange, String routingKey,
                         AMQP.BasicProperties basicProperties,
                         byte[] body) throws IOException {
    String message = new String(body);
    System.out.println("Basic.Return返回的结果是:"+message);
}
});

上面代码中生产者没有成功地将消息路由到队列,此时RabbitMQ会通过Basic.Return返回“mandatory test”这条消息,之后生产者客户端通过ReturnListener监听到了这个事件,上面代码的最后输出应该是“Basic.Return返回的结果是:mandatory test”。

immediate参数

当immediate参数设为true时,如果交换器在将消息路由到队列时发现队列上并不存在任何消费者,那么这条消息将不会存入队列中。当与路由键匹配的所有队列都没有消费者时,该消息会通过Basic.Return返回至生产者。

RabbitMQ 3.0版本开始去掉了对immediate参数的支持,对此RabbitMQ官方解释是:immediate参数会影响镜像队列的性能,增加了代码复杂性,建议采用TTL和DLX的方法替代。

备份交换器

备份交换器通过在声明交换器(调用channel.exchangeDeclare方法)的时候添加alternate-exchange参数来实现,也可以通过策略(Policy)的方式实现。如果两者同时使用,则前者的优先级更高,会覆盖掉Policy的设置。

Map<String, Object> args = new HashMap<String, Object>(); 
args.put("alternate-exchange", "myAe"); 
channel.exchangeDeclare("normalExchange", "direct", true, false, args); 
channel.exchangeDeclare("myAe", "fanout", true, false, null); 
channel.queueDeclare("normalQueue", true, false, false, null); 
channel.queueBind("normalQueue", "normalExchange", "normalKey"); 
channel.queueDeclare("unroutedQueue", true, false, false, null); 
channel.queueBind("unroutedQueue", "myAe", "");

上面的代码中声明了两个交换器normalExchange和myAe,分别绑定了normalQueue和unroutedQueue这两个队列,同时将myAe设置为normalExchange的备份交换器。注意myAe的交换器类型为fanout。

如果此时发送一条消息到normalExchange上,当路由键等于“normalKey”的时候,消息能正确路由到normalQueue这个队列中。如果路由键设为其他值,比如“errorKey”,即消息不能被正确地路由到与normalExchange绑定的任何队列上,此时就会发送给myAe,进而发送到unroutedQueue这个队列。

对于备份交换器,总结了以下几种特殊情况:

  1. 如果设置的备份交换器不存在,客户端和RabbitMQ服务端都不会有异常出现,此时消息会丢失。
  2. 如果备份交换器没有绑定任何队列,客户端和RabbitMQ服务端都不会有异常出现,此时消息会丢失。
  3. 如果备份交换器没有任何匹配的队列,客户端和RabbitMQ服务端都不会有异常出现,此时消息会丢失。
  4. 如果备份交换器和mandatory参数一起使用,那么mandatory参数无效。

2、过期时间(TTL)

设置消息的TTL

在代码中有两种方法设置某个队列的消息过期时间:

  1. 针对队列来说,可以使用x-message-ttl参数设置当前队列中所有消息的过期时间,即当前队列中所有的消息过期时间都一样;
  2. 针对单个消息来说,在发布消息时,可以使用Expiration参数来设置单个消息的过期时间。

以上两个参数的单位都是毫秒。如果以上两个都设置,则以当前消息最短的那个过期时间为准。

第一种

Map<String, Object> argss = new HashMap<String, Object>();
argss.put("x-message-ttl", 6000);
channel.queueDeclare(QUEUE_NAME, false, false, false, argss);

第二种

AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder();
builder.deliveryMode(2);//持久化消息
builder.expiration("60000");//设置TTL=60000ms
AMQP.BasicProperties properties = builder.build();
channel.basicPublish("exchangeName", "routingKey", true, properties, "ttlTestMessgae".getBytes());

设置队列的TTL

针对队列来说,可以使用x-expires参数设置当前队列(未使用状态,也就是没有任何消费者,没有被重新声明)的过期时间

Map<String, Object> argss = new HashMap<String, Object>();
argss.put("x-expires", 6000);
channel.queueDeclare(QUEUE_NAME, false, false, false, argss);

3、死信队列

死信队列 是 当消息在一个队列 因为下列原因:

  1. 消息被拒绝(basic.reject/ basic.nack)并且不再重新投递 requeue=false
  2. 消息超期 (rabbitmq Time-To-Live -> messageProperties.setExpiration())
  3. 队列超载

变成了 “死信” 后 被重新投递(publish)到另一个Exchange 该Exchange 就是DLX ,然后该Exchange 根据绑定规则 转发到对应的 队列上 监听该队列 就可以重新消费 说白了 就是 没有被消费的消息 换个地方重新被消费

生产者 --> 消息 --> 交换机 --> 队列 --> 变成死信 --> DLX交换机 -->队列 --> 消费者

4、延迟队列

RabbitMQ本身不支持延迟队列,但是我们可以使用死信队列(DLX)和设置有效时间(TTL)两个特性来实现延迟队列。

先新建队列order_query并设置消息有效时间是10分钟,然后绑定一个死信队列order_dead_query,消费者消费order_dead_query队列的消息。生成订单的时候往队列order_query发一条消息,当10分钟后这条消息会进入死信队列order_dead_query里面并被我们消费者消费,这时我们去查询一下该订单的支付状态,如果是已支付不做任何操作,如果是未支付就取消订单。

延迟队列模型

RabbitMQ进阶_第2张图片

@Configuration
public class RabbitConfig {
    /**
     * 方法rabbitAdmin的功能描述:动态声明queue、exchange、routing
     *
     * 声明死信队列 和 发送优惠券的消息队列
     */
    public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory) {
        RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory);
        //声明死信队列(Fanout类型的exchange)
        Queue deadQueue = new Queue(RabbitConstants.QUEUE_NAME_DEAD_QUEUE);
        // 死信队列交换机
        FanoutExchange deadExchange = new FanoutExchange(RabbitConstants.MQ_EXCHANGE_DEAD_QUEUE);
        rabbitAdmin.declareQueue(deadQueue);
        rabbitAdmin.declareExchange(deadExchange);
        rabbitAdmin.declareBinding(BindingBuilder.bind(deadQueue).to(deadExchange));


        //声明发送优惠券的消息队列(Direct类型的exchange)
        Queue couponQueue = queue(RabbitConstants.QUEUE_NAME_SEND_COUPON);
        // 发放奖励队列交换机
        DirectExchange exchange = new DirectExchange(RabbitConstants.MQ_EXCHANGE_SEND_AWARD);
        rabbitAdmin.declareQueue(couponQueue);
        rabbitAdmin.declareExchange(exchange);
        rabbitAdmin.declareBinding(BindingBuilder.bind(couponQueue).to(exchange).with(RabbitConstants.MQ_ROUTING_KEY_SEND_COUPON));
        return rabbitAdmin;
    }

    public Queue queue(String name) {
        Map<String, Object> args = new HashMap<String, Object>();
        //设置死信队列
        args.put("x-dead-letter-exchange", RabbitConstants.MQ_EXCHANGE_DEAD_QUEUE);
        args.put("x-dead-letter-routing-key", RabbitConstants.MQ_ROUTING_KEY_DEAD_QUEUE);
        //设置消息的过期时间,单位是毫秒
        args.put("x-message-ttl", 5000);

        //是否持久化
        boolean durable = true;
        // 仅创建者可以使用的私有队列,断开后自动删除
        boolean exclusive = false;
        //当所有消费者客户端连接断开后,是否自动删除队列
        boolean autoDelete = false;
        return new Queue(name, durable, exclusive, autoDelete, args);
    }
}

消费者消费死信队列 DeadMessageListener

@Service
public class DeadMessageListener {

    private final Logger logger = LoggerFactory.getLogger(DeadMessageListener.class);

    @RabbitListener(queues = RabbitConstants.QUEUE_NAME_DEAD_QUEUE)
    public void process(SendMessage sendMessage, Channel channel, Message message) throws Exception {

        System.out.println(message.getMessageProperties().getDeliveryTag());

        try {
            // 参数校验
            Assert.notNull(sendMessage, "sendMessage 消息体不能为NULL");

            // TODO 处理消息

            // 确认消息已经消费成功
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {

            try {
                // TODO 保存消息到数据库

                // 确认消息已经消费成功
                channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
            } catch (Exception dbe) {
                // 确认消息将消息放到死信队列
                channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
            }
        }
    }

}

5、优先级队列

优先级队列,顾名思义,具有更高优先级的队列具有较高的优先权,优先级高的消息具备优先被消费的特权。
本文主要讲解如何使用RabbitMQ实现队列优先级。

可以通过RabbitMQ管理界面配置队列的优先级属性,如下图的x-max-priority.
RabbitMQ进阶_第3张图片
也可以通过代码去实现,比如:

Map<String,Object> args = new HashMap<String,Object>();
args.put("x-max-priority", 10);
channel.queueDeclare("queue_priority", true, false, false, args);

6、实现RPC

Callback Queue

一般在RabbitMQ中做RPC是很简单的。客户端发送请求消息,服务器回复响应的消息。为了接受响应的消息,我们需要在请求消息中发送一个回调队列。可以使用默认的队列(which is exclusive in the java client.):

callbackQueueName = channel.queueDeclare().getQueue();
BasicProperties props = new BasicProperties.Builder().replyTo(callbackQueueName).build();
channel.basicPublish("", "rpc_queue",props,message.getBytes());
// then code to read a response message from the callback_queue...

Correlation Id

在上述方法中为每个RPC请求创建一个回调队列。这是很低效的。幸运的是,一个解决方案:可以为每个客户端创建一个单一的回调队列。

新的问题被提出,队列收到一条回复消息,但是不清楚是那条请求的回复。这是就需要使用correlationId属性了。我们要为每个请求设置唯一的值。然后,在回调队列中获取消息,查看这个属性,关联response和request就是基于这个属性值的。如果我们看到一个未知的correlationId属性值的消息,可以放心的无视它——它不是我们发送的请求。

RPC的处理流程:

  1. 当客户端启动时,创建一个匿名的回调队列。
  2. 客户端为RPC请求设置2个属性:replyTo,设置回调队列名字;correlationId,标记request。
  3. 请求被发送到rpc_queue队列中。
  4. RPC服务器端监听rpc_queue队列中的请求,当请求到来时,服务器端会处理并且把带有结果的消息发送给客户端。接收的队列就是replyTo设定的回调队列。
  5. 客户端监听回调队列,当有消息时,检查correlationId属性,如果与request中匹配,那就是结果了。

7、消息持久化

1.将queue,exchange, message等都设置了持久化之后就能保证100%保证数据不丢失了嚒?

答案是否定的。
首先,从consumer端来说,如果这时autoAck=true,那么当consumer接收到相关消息之后,还没来得及处理就crash掉了,那么这样也算数据丢失,这种情况也好处理,只需将autoAck设置为false(方法定义如下),然后在正确处理完消息之后进行手动ack(channel.basicAck).

其次,关键的问题是消息在正确存入RabbitMQ之后,还需要有一段时间(这个时间很短,但不可忽视)才能存入磁盘之中,RabbitMQ并不是为每条消息都做fsync的处理,可能仅仅保存到cache中而不是物理磁盘上,在这段时间内RabbitMQ broker发生crash, 消息保存到cache但是还没来得及落盘,那么这些消息将会丢失。那么这个怎么解决呢?首先可以引入RabbitMQ的mirrored-queue即镜像队列,这个相当于配置了副本,当master在此特殊时间内crash掉,可以自动切换到slave,这样有效的保障了HA, 除非整个集群都挂掉,这样也不能完全的100%保障RabbitMQ不丢消息,但比没有mirrored-queue的要好很多,很多现实生产环境下都是配置了mirrored-queue的。还有要在producer引入事务机制或者Confirm机制来确保消息已经正确的发送至broker端,有关RabbitMQ的事务机制或者Confirm机制可以参考:RabbitMQ之消息确认机制(事务+Confirm). 幸亏本文的主题是讨论RabbitMQ的持久化而不是可靠性,不然就一发不可收拾了。RabbitMQ的可靠性涉及producer端的确认机制、broker端的镜像队列的配置以及consumer端的确认机制,要想确保消息的可靠性越高,那么性能也会随之而降,鱼和熊掌不可兼得,关键在于选择和取舍。

2.消息什么时候刷到磁盘?

1、写入文件前会有一个Buffer,大小为1M,数据在写入文件时,首先会写入到这个Buffer,如果Buffer已满,则会将Buffer写入到文件(未必刷到磁盘)。
2、有个固定的刷盘时间:25ms,也就是不管Buffer满不满,每个25ms,Buffer里的数据及未刷新到磁盘的文件内容必定会刷到磁盘。
3、每次消息写入后,如果没有后续写入请求,则会直接将已写入的消息刷到磁盘:使用Erlang的receive x after 0实现,只要进程的信箱里没有消息,则产生一个timeout消息,而timeout会触发刷盘操作。

你可能感兴趣的:(消息队列)