RabbitMQ消息中间件(二) RabbitMQ如何保证消息的可靠性投递

RabbitMQ如何保证消息投递的准确性?

    生产端的可靠性投递:

        1.保证消息成功发送

        2.保证MQ节点成功接收

        3.发送端收到MQ节点(Broker)确认应答

        4.完善的消息补偿机制

    BAT等大厂解决方案:

        1.消息落库,对消息状态进行打标(数据信息和消息信息落库)

                2.消息的延迟投递,做二次确认,回调检查(  保证MQ在高并发的场景下)

    幂等性:

        幂等性是什么?

        https://blog.csdn.net/miachen520/article/details/91039661

        借鉴数据库的乐观锁机制:在执行更新库存SQL语句时:UPDATE DB SET COUNT = COUNT - 1,VERSION = VERSION + 1 WHERE VERSION = 1

        消费端-幂等性保障(在海量订单产生的业务高峰期,如何避免消息的重复消费问题):

        消费端实现幂等,就意味着,消息永远不会消费多次,即时我们收到了多条一样的消息

        业界主流的幂等性操作:

        1.唯一性ID+指纹码 机制

            *唯一ID+指纹码机制,利用数据库主键去重

            *SELECT COUNT(1) FROM T_ORDER WHERE ID = 唯一ID + 指纹码

            *好处:实现简单

            *坏处:高并发下有数据库写入的性能瓶颈

            *解决方案:跟进ID进行库分表进行算法路由

        2.利用Redis的原子性去实现

Confirm确认消息:

    理解Confirm消息确认机制:

        消息的确认,是指生产者投递消息后,如果Broker收到消息,则会给我们生产者一个应答,生产者进行接收应答,用来确定这条消息是否正常的发送到Broker,这种方式也是消息的可靠性投递的核心保障!

    如何实现Confirm确认消息?

        1.在channel上开启确认模式:channel.confirmSelect()

        2.在channel上添加监听:addConfirmListener,监听成功和失败的返回结果,根据具体的结果对消息进行重新发送,或记录日志等后续处理。

附源码:

    消费端:ConfirmListenerConsumer.java

package com.xuy.rabbitmq.confirmListener;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.QueueingConsumer;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * @author aric
 * @create 2021-03-02-11:24
 * @fun
 */
public class Consumer {
    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();
        //指定消息投递模式,消息的确认模式
        channel.confirmSelect();
        String exchangeName = "confirm_exchange";
        String routingKey = "confirm.save";
        String queueName = "confirm_queue";
        //声明交换机和队列,然后进行绑定设置,最后指定路由key
        channel.exchangeDeclare(exchangeName,"topic",true);
        channel.queueDeclare(queueName,true,false,false,null);
        channel.queueBind(queueName,exchangeName,routingKey);
        //创建消费者
        QueueingConsumer queueingConsumer = new QueueingConsumer(channel);
        channel.basicConsume(queueName,true,queueingConsumer);
        while(true){
            QueueingConsumer.Delivery delivery = queueingConsumer.nextDelivery();
            String msg = new String(delivery.getBody());
            System.out.println(msg);
        }
    }
}

    生产端:ConfirmListenerProducer.java

package com.xuy.rabbitmq.confirmListener;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConfirmListener;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * @author aric
 * @create 2021-03-02-11:23
 * @fun
 *
 * 添加确认回传机制
 */
public class Producer {
    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();
        //指定消息投递模式,消息的确认模式
        channel.confirmSelect();
        String exchangeName = "confirm_exchange";
        String routingKey = "confirm.save";
        //发送消息
        String msg = "Hello World";
        channel.basicPublish(exchangeName,routingKey,null,msg.getBytes());
        //添加一个确认监听
        channel.addConfirmListener(new ConfirmListener() {
            @Override
            //返回成功ACK
            public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                System.err.println("----no ack!");
            }

            @Override
            //返回失败ACK
            public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                System.err.println("----ack!");
            }
        });
    }
}

Retrun消息机制:

    Return Listener用于处理一些不可路由的消息,消息生产者通过指定一个Exchange和RoutingKey,把消息送达到某一个队列中去,然后消费者监听队列,进行消费处理操作。

    但是在某些情况下,如果我们子啊发送消息的时候,当前的exchange不存在或者指定的路由key路由不到,这个时候如果我们要监听这种不可达的消息,就要使用Return Listener

    在基础API中有一个关键的我配置项:Mandatory:如果为true,则监听器会接收到路由不可达的消息,然后进行后续处理,如果为false,那么broker端自动删除该消息。

附源码:

    消费端:ReturnListenerConsumer.java

package com.xuy.rabbitmq.returnListener;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.QueueingConsumer;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * @author aric
 * @create 2021-03-02-14:44
 * @fun
 */
public class Consumer {
    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();
        String exchangeName = "return_exchange";
        String routingKey = "return.save";
        String queueName = "return_queue";

        channel.exchangeDeclare(exchangeName,"topic",true,false,null);
        channel.queueDeclare(queueName,true,false,false,null);
        channel.queueBind(queueName,exchangeName,routingKey);

        QueueingConsumer queueingConsumer = new QueueingConsumer(channel);
        channel.basicConsume(queueName,true,queueingConsumer);
        while(true){
            QueueingConsumer.Delivery delivery = queueingConsumer.nextDelivery();
            String msg = new String(delivery.getBody());
            System.out.println(msg);
        }
    }
}

    生产端:ReturnListenerProducer.java

package com.xuy.rabbitmq.returnListener;

import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * @author aric
 * @create 2021-03-02-14:44
 * @fun
 *
 * basicPublish中参数Mandatory测试
 */
public class Producer {
    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();
        String exchange = "return_exchange";
        String routingKey = "return.save";
        String routingKeyError = "abc.save";
        String msg = "hello World";
        channel.addReturnListener(new ReturnListener() {
            @Override
            public void handleReturn(int replyCode,
                                     String replyText,
                                     String exchange,
                                     String routingKey,
                                     AMQP.BasicProperties Properties,
                                     byte[] bytes) throws IOException {
                System.err.println("----handle return");
                System.err.println(replyCode);
                System.err.println(replyText);
                System.err.println(exchange);
                System.err.println(routingKey);
                System.err.println(Properties);
                System.err.println(new String(bytes));
            }
        });
        channel.basicPublish(exchange,routingKey,true,null,msg.getBytes());
        channel.basicPublish(exchange,routingKeyError,true,null,msg.getBytes());
    }
}

消费端自定义监听:

    之前在代码中编写while循环,进行consumer.nextDelivery方法进行获取下一条消息,然后进行消费处理。但是使用自定义的Consumer更加的方便,解耦性更加的强。

附源码:

    自定义Consumer:MyConsumer.java

package com.xuy.rabbitmq.consumerBySelf;

import com.rabbitmq.client.*;
import com.rabbitmq.client.Consumer;

import java.io.IOException;

/**
 * @author aric
 * @create 2021-03-02-16:35
 * @fun
 */
public class MyConsumer extends DefaultConsumer {

    public MyConsumer(Channel channel) {
        super(channel);
    }

    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] bytes) throws IOException {
        System.err.println("------consumer message-------");
        System.err.println(consumerTag);
        System.err.println(envelope);
        System.err.println(properties);
        System.err.println(new String(bytes));
    }
}

    消费端:ConsumerBySelfConsumer.java

package com.xuy.rabbitmq.consumerBySelf;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.QueueingConsumer;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * @author aric
 * @create 2021-03-02-16:29
 * @fun
 *
 * 使用自定义的Consumer
 */
public class Consumer {
    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();
        String exchangeName = "consumer_exchange";
        String routingKey = "consumer.save";
        String queueName = "consumer_queue";

        channel.exchangeDeclare(exchangeName,"topic",true,false,null);
        channel.queueDeclare(queueName,true,false,false,null);
        channel.queueBind(queueName,exchangeName,routingKey);

        channel.basicConsume(queueName,true,new MyConsumer(channel));

    }
}

    生产端:ConsumerBySelfProducer.java

package com.xuy.rabbitmq.consumerBySelf;

import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * @author aric
 * @create 2021-03-02-16:30
 * @fun
 */
public class Producer {
    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();
        String exchange = "consumer_exchange";
        String routingKey = "consumer.save";

        String msg = "hello World";

        for(int i = 0 ; i < 5;i++) {
            channel.basicPublish(exchange, routingKey, true, null, msg.getBytes());
        }
    }
}

消费端限流:

    什么是消费端限流?

        假设RabbitMQ服务器有上万条未处理的消息,随便打开一个消费者客户端,会出现下面情况:巨量的消息瞬间全部推送过来,但是单个客户端无法同时处理这么多数据。

    解决:

        RabbitMQ提供了一种qos(服务质量保证)功能,即在非自动确认消息的前提下,如果一定数目的消息(通过基于consumer或者channel设置Qos的值)违背确认前,不进行消费新的消息。

    void BasicQos(uint prefetchSize,ushort prefetchCount,bool global);

        prefetchSize:0消费消息大小限制

        prefetchCount:告诉RabbitMQ不要同时给一个消费者推送多于N个消息,即一旦有N个消息还没有ack,则该consumer将block掉,直到有消息ack

        global:true/false是否将上面设置应用于channel或consumer

        注意:prefetchSize和global这两项,rabbitMQ没有实现,暂且不研究prefetch_count在no_ask=false的情况下生效,即在自动应答的情况下这两个值是不生效的。

附源码:

    自定义消费者模式:MyConsumerByLimit.java

package com.xuy.rabbitmq.consumerLimit;

import com.rabbitmq.client.*;

import java.io.IOException;

/**
 * @author aric
 * @create 2021-03-02-17:22
 * @fun
 */
public class MyConsumerByLimit extends DefaultConsumer {

    private Channel channel;
    public MyConsumerByLimit(Channel channel) {
        super(channel);
    }

    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] bytes) throws IOException {
        System.err.println("------consumer message-------");
        System.err.println(consumerTag);
        System.err.println(envelope);
        System.err.println(properties);
        System.err.println(new String(bytes));

        channel.basicAck(envelope.getDeliveryTag(),false);
    }
}

    消费端:ConsumerBySelfConsumer.java

package com.xuy.rabbitmq.consumerLimit;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * @author aric
 * @create 2021-03-02-17:00
 * @fun
 */
public class Consumer {
    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();
        String exchangeName = "qos_exchange";
        String routingKey = "qos.save";
        String queueName = "qos_queue";

        channel.exchangeDeclare(exchangeName,"topic",true,false,null);
        channel.queueDeclare(queueName,true,false,false,null);
        channel.queueBind(queueName,exchangeName,routingKey);

        channel.basicQos(0,1,false);
        //1.限流方式,第一件事情就是AutoAck设置为false
        channel.basicConsume(queueName,false,new MyConsumerByLimit(channel));

    }
}

    生产端:ConsumerBySelfProducer.java

package com.xuy.rabbitmq.consumerLimit;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * @author aric
 * @create 2021-03-02-17:00
 * @fun
 */
public class Producer {
    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();
        String exchange = "qos_exchange";
        String routingKey = "qos.save";

        String msg = "hello World";

        for(int i = 0 ; i < 5;i++) {
            channel.basicPublish(exchange, routingKey, true, null, msg.getBytes());
        }
    }
}

消费端ACK与重回队列:

    消费端的手工ACK和NACK

        1.消费端进行消费的时候,如果由于业务异常我们可以进行日志的记录,然后进行补偿!

        2.如果由于服务器宕机等严重问题,那我们就需要手工进行ACK保障消费端消费成功!

    消费端的重回队列

        1.消费端重回队列是为了对没有处理成功的消息,把消息重新回递给Broker队列中!

        2.一般我们在实际中都会关闭重回队列,也就是设置为False。

附源码:

    自定义消费者模式:MyConsumerByACK.java

package com.xuy.rabbitmq.ack;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

import java.io.IOException;

/**
 * @author aric
 * @create 2021-03-02-17:36
 * @fun
 *
 * 如果是第0条消息,不签收,重回队列
 *
 */
public class MyConsumerByAck extends DefaultConsumer {

    private Channel channel;
    public MyConsumerByAck(Channel channel) {
        super(channel);
        this.channel = channel;
    }

    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] bytes) throws IOException {
        System.err.println("--------consumer message");
        System.err.println(new String(bytes));

        if((Integer)properties.getHeaders().get("num") == 0){
            channel.basicNack(envelope.getDeliveryTag(),false,true);
        }else{
            channel.basicAck(envelope.getDeliveryTag(),false);
        }
    }
}

    消费端:ConsumerByACKConsumer.java

package com.xuy.rabbitmq.ack;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.xuy.rabbitmq.consumerLimit.MyConsumerByLimit;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * @author aric
 * @create 2021-03-02-17:35
 * @fun
 */
public class Consumer {
    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();
        String exchangeName = "ack_exchange";
        String routingKey = "ack.save";
        String queueName = "ack_queue";

        channel.exchangeDeclare(exchangeName,"topic",true,false,null);
        channel.queueDeclare(queueName,true,false,false,null);
        channel.queueBind(queueName,exchangeName,routingKey);

        //手工签收必须要关闭  AutoACK=false
        channel.basicConsume(queueName,false,new MyConsumerByAck(channel));
    }
}

    生产端:ConsumerByACKProducer.java

package com.xuy.rabbitmq.consumerLimit;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * @author aric
 * @create 2021-03-02-17:00
 * @fun
 */
public class Producer {
    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();
        String exchange = "qos_exchange";
        String routingKey = "qos.save";

        String msg = "hello World";

        for(int i = 0 ; i < 5;i++) {
            channel.basicPublish(exchange, routingKey, true, null, msg.getBytes());
        }
    }
}

TTL队列/消息:

    TTL:TTL是Time To Live的缩写,也就是生存时间,RabbitMQ支持消息的过期时间,在消息发送时可以进行指定,RabbitMQ支持队列的过期时间,从消息入队列开始计算,只要超过了队列的超时时间配置,那么消息会自动的清除

    注意:消息体中Producer的properties的expiration属性也可设置过期时间

死信队列:

    死信队列:

        DLX(Dead-Letter-Exchange):利用DLX,当消息在一个队列中变成死信(dead message)之后,它能被重新publish到另一个Exchange,这个Exchange就是DLX。

        DLX也是一个正常的Exchange,和一般的Exchange没有区别,他能在任何的队列上被指定,实际上就是设置某个队列的属性。

        当这个队列中有死信时,RabbitMQ就会自动的将这个消息重新发布到设置的Exchange上去,进而被路由到另一个队列。

        可以监听这个队列中消息做相应的处理,这个特性可以弥补RabbitMQ3.0以前支持的immediate参数的功能。

    消息变为死信有以下机种情况:

        1.消息被拒绝(basic.reject/basic.nack)并且requeue=false

        2.消息TTL过期

        3.队列达到最大长度

    死信队列设置:

        1.首先需要设置死信队列的exchange和queue,然后进行绑定:

            Exchange:dlx.exchange

            Queue:dlx.queue

            RoutingKey:#

        2.然后进行正常声明交换机,队列,绑定,只不过需要在队列加上一个参数即可:arguments.put("x-dead-letter-exchange","dlx.exchange"); 这样消息在过期,requeue,队列在达到最大长度时,消息就可以i直接路由到死信队列!

附源码:

    消费端:DlxConsumer.java

package com.xuy.rabbitmq.dlx;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.xuy.rabbitmq.ack.MyConsumerByAck;

import java.io.IOException;
import java.util.HashMap;
import java.util.concurrent.TimeoutException;

/**
 * @author aric
 * @create 2021-03-02-18:35
 * @fun
 */
public class Consumer {
    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();
        //这就是一个普通的交换机和队列以及路由
        String exchangeName = "dlx_exchange";
        String routingKey = "dlx.save";
        String queueName = "dlx_queue";

        channel.exchangeDeclare(exchangeName,"topic",true,false,null);

        HashMap<String, Object> agruments = new HashMap<>();
        agruments.put("x-dead-letter-exchange","dlx.exchange");

        //这个agruments属性,要设置到声明队列上
        channel.queueDeclare(queueName,true,false,false,agruments);
        channel.queueBind(queueName,exchangeName,routingKey);

        //要进行死信队列的声明
        channel.exchangeDeclare("dlx.exchange","topic",true,false,null);
        channel.queueDeclare("dlx.queue",true,false,false,null);
        channel.queueBind("dlx.queue","dlx.exchange","#");
        channel.basicConsume(queueName,true,new MyConsumerByAck(channel));

    }
}

    生产端:DlxProducer.java

package com.xuy.rabbitmq.dlx;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * @author aric
 * @create 2021-03-02-18:35
 * @fun
 *
 * expiration : 10s过期 进入死信队列
 */
public class Producer {
    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();
        String exchange = "dlx_exchange";
        String routingKey = "dlx.save";

        String msg = "hello World";

        for(int i = 0 ; i < 5;i++) {
            AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
                    .deliveryMode(2)
                    .contentEncoding("UTF-8")
                    .expiration("10000")
                    .build();
            channel.basicPublish(exchange, routingKey, true, properties, msg.getBytes());
        }
    }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值