rabbitmq的消息可靠投递 --下篇

文章目录
保证消息100%投递
如何保证生产者可靠性投递消息
方案一 : 消息入库,对消息状态进行打标。
方案二 : 消息延迟投递,进行二次确认,回调检查
confirm确认消息
Return消息机制
消费端限流策略
消费端ACK与重回队列
消费端的手动ACK和NACK
消费端重回队列
TTL队列/消息
死信队列
死信队列设置
保证消息100%投递
如何保证生产者可靠性投递消息
保证消息成功发出
保障MQ节点成功接受
发送端收到MQ节点(Broker)确认应答
对消息进行补偿机制
方案一 : 消息入库,对消息状态进行打标。

step1:业务数据入库,消息数据入库(两次操作DB 高并发可能会受影响)
step2:producer向MQ发送消息
step3:producer异步监听MQ确认消息投递到队列
step4:producer监听到确认送达后,修改消息数据库状态
step5:补偿机制,通过定时任务轮询扫描消息表中,未确认的消息。
step6:补偿机制,将未确认的消息从新发送。
step7:补偿机制,记录重试次数,达到一定数值后,将消息状态该为失败。(通过人工处理)
方案二 : 消息延迟投递,进行二次确认,回调检查
异步操作,性能更高

整个流程需要三个消息队列来完成。

step1:先业务数据入库,然后发送业务消息。
step2:延迟发送第二条check消息。(延迟时间根据业务来确定)
step3:消费端监听队列并消费消息。
step4:消费端发送confirm消息。(此消息为从新发送的新消息在不同的队列中,非ack签收)
step5:回调服务监听消费端的confirm消息队列,并将消息进行持久化存储。
step6:回调服务监听check消息队列,查询数据库,如果业务消息没有消费,回调服务将告诉producer从新发送业务消息
confirm确认消息
消息的确认,是指生产者投递消息后,如果Broker收到消息,则会给我们生产者一个应答。
生产者进行接受应答,用来确认这条消息是否正常的发送到Broker,这种方式是消息可靠性投递的核心保障

第一步:在channel上开启消息确认模式:channel.confirmSelect();
第二步:在channel上添加监听,addConfirmListener,监听成功和失败的返回结果,根据具体的结果对消息进行重新发送、或记录日志等后续操作。
//生产者
//省略 创建连接和channel

    //打开消息确认模式
    channel.confirmSelect();

    String exchange = "test.confirm.exchange";
    String routingKey = "confirm.routing-key";
    String message = "hello rabbitmq";
    channel.basicPublish(exchange, routingKey, null, message.getBytes());
    //添加确认监听
    channel.addConfirmListener(new ConfirmListener() {
        //消息成功发送回调
        public void handleAck(long deliverTag, boolean multiple) throws IOException {
            System.err.println("----发送成功---");
        }
        //消息发送失败回调
        public void handleNack(long deliverTag, boolean multiple) throws IOException {
        	//队列满、磁盘满 等等情况
            System.err.println("----发送失败---");
        }
    });

    //消费者
    //省略 创建连接和channel
    String queueName = "confirm-queue";
    String exchange = "test.confirm.exchange";
    String routingKey = "confirm.#";
    //声明交换机
    channel.exchangeDeclare(exchange, "topic");
    //声明队列
    channel.queueDeclare(queueName, false, false, false, null);
    //队列、交换机、routing key 三者绑定
    channel.queueBind(queueName, exchange, routingKey);

    DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
        @Override
        public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
            String message = new String(body, "UTF-8");
            System.err.println(message);
        }
    };
    channel.basicConsume(queueName, true, defaultConsumer);
}

Return消息机制
Return Listener 用于处理一些不可路由的消息
我们的生产者,通过指定一个Exchange和Routing key,把消息送达到某个消息队列,然后我们的消费者监听队列,进行消费操作。
但是在某些情况下,如果我们在发送消息的时候,当前的Exchange不存在或者指定的Routing key路由不到,此时我们需要监听这种不可达消息,就是用Return Listener。

Mandatory: 如果为true,则监听器会收到路由不可达的消息,然后进行处理,如果为false,Broker会自动删除消息。

    //生产者
    //省略 创建连接和channel
    String exchange = "test.return.exchange";
    String routingKey = "return.routing-key";
    String message = "hello rabbitmq";

    //声明交换机  不绑定队列 触发return 回调
    channel.exchangeDeclare(exchange, "topic");

    //添加 return 监听器
    channel.addReturnListener(new ReturnListener() {
        public void handleReturn(int replyCode, String replyTest, String exchange, String routingKey,
                                 AMQP.BasicProperties basicProperties, byte[] body) throws IOException {
            System.err.println("replyCode: " + replyCode);
            System.err.println("replyTest: " + replyTest);
            System.err.println("exchange: " + exchange);
            System.err.println("routingKey: " + routingKey);
            System.err.println("basicProperties: " + basicProperties);
            System.err.println("body: " + new String(body, "UTF-8"));
        }
    });
    //第三个参数 是否打开 return 机制
    channel.basicPublish(exchange, routingKey, true,null, message.getBytes());
}

消费端限流策略
什么是消费端限流?

假设一个场景,首先,我们RabbitMQ服务器上有上万条未处理的消息,随便打开一个消费端,就会有巨量消息推送过来,压垮客户端。
RabbitMQ提供了一种qos(服务质量保证)功能,即在非自动签收消息的前提下,如果一定数目的消息(通过基于consumer或者channel设置Qos的值)未被确认前,不进行新消息的消费。
在自动签收消息的情况下 不生效。
void BasicQos(int prefetchSize,short prefetchCount,boolean global);
prefetchSize:消息的大小限制,0 不限制。
prefetchCount:推送的消息数目。
global:true\false 是否将以上设置 应用于channel上
简单点说:就是上面限制是channel级别还是consumer级别

    //生产者
    //省略 创建连接和channel

    String exchange = "test.qos.exchange";
    String routingKey = "qos.routing-key";
    String message = "hello rabbitmq";
    for (int i = 0; i < 9; i++) {
        channel.basicPublish(exchange, routingKey, null, message.getBytes());

    }
    channel.close();
    connection.close();

1
2
3
4
5
//消费者
//省略 创建连接和channel
String queueName = “qos-queue”;
String exchange = “test.qos.exchange”;
String routingKey = “qos.routing-key”;
//声明交换机
channel.exchangeDeclare(exchange, “topic”);
//声明队列
channel.queueDeclare(queueName, false, false, false, null);
//队列、交换机、routing key 三者绑定
channel.queueBind(queueName, exchange, routingKey);

    // 限流 第一件事:autoAck 设置为false
    channel.basicQos(0, 3, false);
    channel.basicConsume(queueName, false, new DefaultConsumer(channel){
        @Override
        public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
            String message = new String(body, "UTF-8");
            System.err.println(message);
            //第二个参数  是否批量签收
            channel.basicAck(envelope.getDeliveryTag(), true);
        }
    });      

消费端ACK与重回队列
ACK:签收,NACK不签收

消费端的手动ACK和NACK
消费端进行消费是,如果业务出现异常NACK消息,经历三到四次重试后依然异常,手动ACK,然后进行补偿。
消费端重回队列
为了将没有处理成功的消息重新投递到Queue的队尾中。
实际项目中,应用较少。
//生产者
//省略 创建连接和channel

    String exchange = "test.ack.exchange";
    String routingKey = "ack.routing-key";

    for (int i = 0; i < 5; i++) {
        Map<String, Object> headers = new HashMap<>();
        headers.put("index", i);
        AMQP.BasicProperties properties = new AMQP.BasicProperties().builder()
                .deliveryMode(2) //持久化
                .headers(headers)
                .build();

        String message = "hello rabbitmq " + i;
        channel.basicPublish(exchange, routingKey, properties, message.getBytes());
    }

    //消费者
    //省略 创建连接和channel
    String queueName = "ack-queue";
    String exchange = "test.ack.exchange";
    String routingKey = "ack.routing-key";
    //声明交换机
    channel.exchangeDeclare(exchange, "topic");
    //声明队列
    channel.queueDeclare(queueName, false, false, false, null);
    //队列、交换机、routing key 三者绑定
    channel.queueBind(queueName, exchange, routingKey);

    //必须手工签收  关闭autoACK
    channel.basicConsume(queueName, false, new DefaultConsumer(channel) {
        @Override
        public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
            String message = new String(body, "UTF-8");
            System.err.println(message);
            if ((Integer) properties.getHeaders().get("index") == 0) {
                //第三个参数  是否重回队列
                channel.basicNack(envelope.getDeliveryTag(), false, true);
            }else{
                //第二个参数  是否批量签收
                channel.basicAck(envelope.getDeliveryTag(),false);
            }
        }
    });

TTL队列/消息
TTL 是 Time To Live 的缩写,也就是生存时间
RabbitMQ支持消息的过期时间设置,在消息发送时指定
RabbitMQ支持队列的过期时间设置,从消息入队时开始计算,只要超过队列的超时时间配置,消息自动清除
死信队列
消息变成死信有以下几种情况

消息被拒绝(basic.reject/basic.nack)并且requeue = false。
消息TTL过期
队列达到最大长度
当消息在一个队列中变成私信之后,它会被重新public到另外一个Exchange中,这个Exchange就是DLX(私信队列)
DLX也是一个正常的Exchange,和一般的Exchange没有区别,它可以在任何的队列指定,实际上就是设置队列的属性。
当这个队列中有私信时,RabbitMQ就会自动的将这个消息重新发送到设置的Exchange上去,进而被路由到另一个队列
死信队列设置
首先需要设置私信队列的Exchange和queue,然后进行绑定。
例如:
Exchange:dlx.exchange
Queue:dlx.queue
Routing key:#

然后我们正常声明Exchange、Queue、binding,只不过我们需要在队列上加上一个参数即可:arguments.put(“x-dead-letter-exchange”,“dlx.exchange”)
简单点说:就是在队列上设置私信队列就可以。
//生产者
//省略 创建连接和channel
String exchange = “test.dlx.exchange”;
String routingKey = “dlx.routing-key”;

    //进行私信队列的声明
    channel.exchangeDeclare("dlx.exchange", "topic", true, false, false, null);
    channel.queueDeclare("dlx.queue", false, false, false, null);
    channel.queueBind("dlx.queue", "dlx.exchange", "#");

    //声明普通队列和交换机
    String queueName = "test.dlx-queue";
    channel.exchangeDeclare(exchange, "topic");

    //-----------------------设置队列的私信队列---------------------------------------
    Map<String, Object> arguments = new HashMap<>();
    arguments.put("x-dead-letter-exchange", "dlx.exchange");
    //这个arguments属性要设置到声明队列上
    channel.queueDeclare(queueName, false, false, false, arguments);
    //-----------------------设置队列的私信队列---------------------------------------
    
    channel.queueBind(queueName, exchange, routingKey);

    AMQP.BasicProperties properties = new AMQP.BasicProperties().builder()
            .expiration("1000") //设置过期时间  不启动消费者 使消息变为私信
            .build();
    String message = "hello rabbitmq";
    channel.basicPublish(exchange, routingKey,properties, message.getBytes());
    channel.close();
    connection.close();

猜你喜欢

转载自blog.csdn.net/weixin_30947631/article/details/86499961