RabbitMQ消息中间件技术精讲(三)—— 深入RabbitMQ高级特性

一. 生产端消息的可靠性投递方案

1.1何为生产端的可靠性投递

  • 保障消息的成功发出
  • 保障MQ节点成功接收
  • 发送端收到MQ节点(Broker)确认应答(已收到)
  • 完善消息进行补偿机制

1.2可靠性投递的方案

(不加事务)

1.消息落库(持久化至数据库),对消息状态进行打标,如若消息未响应,进行轮询操作。
RabbitMQ消息中间件技术精讲(三)—— 深入RabbitMQ高级特性_第1张图片

Step1:把业务消息落库,再生成一条消息落库到消息DB用来记录(譬如消息刚创建,正在发送中 status: 0)。(缺点:对数据库进行两次持久化)
Step2:生产端发送消息。
Step3:Broker端收到后,应答至生产端。Confirm Listener异步监听Broker的应答。
Step4:应答表明消息投递成功后,去消息DB中抓取到指定的消息记录,更新状态,如status: 1

Step5:
如在Step3中出现网络不稳定等情况,导致Listener未收到消息成功确认的应答。
那么消息数据库中的status就还是0,而Broker可能是接收到消息的状态。
因此设定一个规则(定时任务),例如消息在落库5分钟后(超时)还是0的状态,就把该条记录抽取出来。

Step6:重新投递
Step7:限制一个重试的次数,譬如3次,如果大于3次,即为投递失败,更新status的值。(用补偿机制去查询消息失败的原因,人工)

2.消息的延迟投递,做二次确认,回调检查。(高并发场景)
RabbitMQ消息中间件技术精讲(三)—— 深入RabbitMQ高级特性_第2张图片
Upstream service:生产端
Downstream service:消费端

Step1:业务消息落库后,发送消息至Broker。
Step2:紧接着发送第二条延迟(设置延迟时间)检查的消息。
Step3:消费端监听指定的队列接收到消息进行处理
Step4:处理完后,生成一条响应消息发送到Broker。
Step5:由Callback服务去监听该响应消息,收到该响应消息后持久化至消息DB(记录成功状态)。
Step6:到了延迟时间,延迟发送的消息也被Callback服务的监听器监听到后,去检查消息DB。如果未查询到成功的状态,Callback服务需要做补偿,发起RPC通讯,让生产端重新发送。生产端通过介绍到的命令中所带的id去数据库查询该业务消息,再重新发送,即跳转到Step1。

该方案减少了对数据库的存储,保证了性能。

二.消费端的幂等性保障

2.1幂等性的概念

通俗的说就是执行N次操作的结果是相同的。

借鉴数据库的乐观锁机制。
执行一条更新数据库的SQL语句:
(避免并发问题,添加一个版本号,执行过减操作后递增version,就不会重复减)

UPDATE T_REPS SET COUNT = COUNT - 1,VERSION = VERSION + 1
WHERE VERSION = 1

2.2消费端保障幂等性

避免消息的重复消费:
消费端实现幂等性,接收到多条相同的消息,但不会重复消费,即收到多条一样的消息。

方案:

1.唯一ID + 指纹码机制

  • 唯一ID + 指纹码(业务规则、时间戳等拼接)机制,利用数据库主键去重
  • SELECT COUNT(1) FROM T_ORDER WHERE ID = 唯一ID + 指纹码 未查询到就insert,如有说明已处理过该消息,返回失败
  • 优点:实现简单
  • 缺点:高并发下有数据库写入的性能瓶颈
  • 解决方案:根据ID进行分库分表、算法路由

2.利用Redis的原子性

需要考虑的问题:

  • 是否要落库数据库,如落库,数据库和缓存如何做到数据的一致性?
  • 不落库,数据存储在缓存中,如何设置定时同步的策略(可靠性保障)?

三.Confirm确认消息

3.1Confirm消息确认机制的概念

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

3.2确认机制的流程图

RabbitMQ消息中间件技术精讲(三)—— 深入RabbitMQ高级特性_第3张图片
发送消息与监听应答的消息是异步操作。

3.2确认消息的实现

  1. 在channel开启确认模式:channel.confirmSelect();
  2. 在channel添加监听:channel.addConfirmListener(ConfirmListener listener); 返回监听成功和失败的结果,对具体结果进行相应的处理(重新发送、记录日志等待后续处理等)

具体代码:

Producer

public class ConfirmProducer {

    private static final String EXCHANGE_NAME = "confirm_exchange";
    private static final String ROUTING_KEY = "confirm.key";

    public static void main(String[] args) throws IOException, TimeoutException {

        ConnectionFactory connectionFactory = new ConnectionFactory();

        connectionFactory.setHost("192.168.58.129");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/test");

        Connection connection = connectionFactory.newConnection();

        Channel channel = connection.createChannel();

        // 指定消息的投递模式: 确认模式
        channel.confirmSelect();

        // 发送消息
        String msg = "Send message of confirm demo";
        channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, null, msg.getBytes());

        // 添加确认监听
        channel.addConfirmListener(new ConfirmListener() {
            // 成功
            @Override
            public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                System.out.println("========= Ack ========");
            }

            // 失败
            @Override
            public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                System.out.println("========= Nack ========");
            }
        });
    }
}

Consumer

public class ConfirmConsumer {

    private static final String EXCHANGE_NAME = "confirm_exchange";
    private static final String ROUTING_KEY = "confirm.#";
    private static final String QUEUE_NAME = "confirm_queue";


    public static void main(String[] args) throws IOException, TimeoutException {

        ConnectionFactory connectionFactory = new ConnectionFactory();

        connectionFactory.setHost("192.168.58.129");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/test");

        Connection connection = connectionFactory.newConnection();

        Channel channel = connection.createChannel();

        // 绑定交换机与队列, 指定路由键
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC, true);
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);

        DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("Received message : " + msg);
            }
        };

        channel.basicConsume(QUEUE_NAME, true, defaultConsumer);

    }
}

控制台输出:
RabbitMQ消息中间件技术精讲(三)—— 深入RabbitMQ高级特性_第4张图片
RabbitMQ消息中间件技术精讲(三)—— 深入RabbitMQ高级特性_第5张图片

四.Return消息机制

用于处理一些不可路由的消息。

4.1 基础API

有一个关键配置项:
Mandatory:true,则监听器会接收到路由不可达的消息,然后进行处理;false,Broker会自动删除该消息。默认是false。

4.2 流程图

RabbitMQ消息中间件技术精讲(三)—— 深入RabbitMQ高级特性_第6张图片
消息的生产者通过制定Exchange和RoutingKey,把消息投递到某一个队列中,消费者监听队列,进行消费。

但在一些情况下,发送消息时,Exchange不存在或RoutingKey路由不到,Return Listener就会监听这种不可达的消息,然后进行处理。

4.3 Return Listener代码

Consumer

public class ReturnConsumer {

    private static final String EXCHANGE_NAME = "return_exchange";
    private static final String ROUTING_KEY = "return.#";
    private static final String QUEUE_NAME = "return_queue";


    public static void main(String[] args) throws IOException, TimeoutException {

        ConnectionFactory connectionFactory = new ConnectionFactory();

        connectionFactory.setHost("192.168.58.129");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/test");

        Connection connection = connectionFactory.newConnection();

        Channel channel = connection.createChannel();

        // 绑定交换机与队列, 指定路由键
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC, true);
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);

        DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("Receive Message —— " + new String(body));
            }
        };

        channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
    }
}

Producer

public class ReturnProducer {

    private static final String EXCHANGE_NAME = "return_exchange";
    private static final String ROUTING_KEY = "return.key";
    private static final String ROUTING_KEY_ERROR = "wrong.key";

    public static void main(String[] args) throws IOException, TimeoutException {

        ConnectionFactory connectionFactory = new ConnectionFactory();

        connectionFactory.setHost("192.168.58.129");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/test");

        Connection connection = connectionFactory.newConnection();

        Channel channel = connection.createChannel();


        // 消息
        String msg = "Send message of return demo";
        // 添加并设置Return监听器
        channel.addReturnListener(new ReturnListener() {
            @Override
            public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.err.println("============ handleReturn ============");
                System.err.println("replyCode —— " + replyCode);
                System.err.println("replyText —— " + replyText);
                System.err.println("exchange —— " + exchange);
                System.err.println("routingKey —— " + routingKey);
                System.err.println("properties —— " + properties);
                System.err.println("body —— " + new String(body));
            }
        });

        // 设置Mandatory为true, 可以进行后续处理, 不会删除消息。
        // channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, true,null, msg.getBytes());
        // 发送消息
        channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY_ERROR, true, null, msg.getBytes());

    }
}

handleReturn的参数输出:
RabbitMQ消息中间件技术精讲(三)—— 深入RabbitMQ高级特性_第7张图片
具体的处理可以在该方法下编写。

五.消费端限流

5.1消费端限流的概念

当巨量消息瞬间全部推送时,单个客户端无法同时处理这些数据,服务器容易故障。因此要进行消费端限流

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

    /**
     * Request specific "quality of service" settings.
     *
     * These settings impose limits on the amount of data the server
     * will deliver to consumers before requiring acknowledgements.
     * Thus they provide a means of consumer-initiated flow control.
     * @see com.rabbitmq.client.AMQP.Basic.Qos
     * @param prefetchSize maximum amount of content (measured in
     * octets) that the server will deliver, 0 if unlimited
     * @param prefetchCount maximum number of messages that the server
     * will deliver, 0 if unlimited
     * @param global true if the settings should be applied to the
     * entire channel rather than each consumer
     * @throws java.io.IOException if an error is encountered
     */
    void basicQos(int prefetchSize, int prefetchCount, boolean global) throws IOException;

prefetchSize:消息限制大小,一般为0,不做限制。
prefetchCount:一次处理消息的个数,一般设置为1
global:一般为false。true,在channel级别做限制;false,在consumer级别做限制

(要手动ack)

5.2 代码演示

Consumer

public class QosConsumer {

    private static final String EXCHANGE_NAME = "qos_exchange";
    private static final String ROUTING_KEY = "qos.#";
    private static final String QUEUE_NAME = "qos_queue";


    public static void main(String[] args) throws IOException, TimeoutException {

        ConnectionFactory connectionFactory = new ConnectionFactory();

        connectionFactory.setHost("192.168.58.129");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/test");
        connectionFactory.setUsername("orcas");
        connectionFactory.setPassword("1224");

        Connection connection = connectionFactory.newConnection();

        Channel channel = connection.createChannel();

        // 绑定交换机与队列, 指定路由键
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC, true);
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);

        DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("Receive Message —— " + new String(body));
                // 手动ack签收
                channel.basicAck(envelope.getDeliveryTag(), false); // 不批量签收
            }
        };

        /**
         *  prefetchSize: 0 不限制消息大小
         *  prefetchCount: 一次处理消息的个数, ack后继续推送
         *  global: false 应用在consumer级别
         */
        channel.basicQos(0, 1, false);
        //限流:autoAck需设置为false, 关闭自动签收
        channel.basicConsume(QUEUE_NAME, false, defaultConsumer);
    }
}

Producer

public class QosProducer {

    private static final String EXCHANGE_NAME = "qos_exchange";
    private static final String ROUTING_KEY = "qos.key";

    public static void main(String[] args) throws IOException, TimeoutException {

        ConnectionFactory connectionFactory = new ConnectionFactory();

        connectionFactory.setHost("192.168.58.129");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/test");
        connectionFactory.setUsername("orcas");
        connectionFactory.setPassword("1224");

        Connection connection = connectionFactory.newConnection();

        Channel channel = connection.createChannel();


        String msg = "Send message of QOS demo";
        for (int i = 0; i < 5; i ++) {
            channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, true, null, msg.getBytes());
        }

    }
}

限流需要设置channel.basicQos(0, 1, false);
关闭autoAck,且需要手动签收。
在重写的handleDelivery方法中,如果没有进行手动签收channel.basicAck(),
那么消费端在接收消息时,因为prefetchCount设置为1,只会接收1条消息,剩下的消息的等待中,并不会被推送,直到手动ack后。

六.队列

6.1 消费端ACK与重回队列机制

消费端的手工ACK和NACK:

消费端进行消费时,可能由于业务异常,会调用NACK拒绝确认,而到了一定次数,就直接ACK,将异常消息进行日志的记录,然后进行补偿。

由于服务器宕机等严重问题,消费端没消费成功,重发消息后,需要手工ACK保障消费端消费成功。

消费端的重回队列:
将没有处理成功的消息重新回递给Broker。
一般在实际应用中,会关闭重回队列。

6.2 TTL队列

TTL:Time To Live,生存时间。
可以指定消息的过期时间。
可以指定队列的过期时间,从消息入队列开始计算,超过了队列的超时时间设置,那么消息会自动清除。

控制台演示:
声明队列,设置TTL时长:
RabbitMQ消息中间件技术精讲(三)—— 深入RabbitMQ高级特性_第8张图片
声明交换机:
RabbitMQ消息中间件技术精讲(三)—— 深入RabbitMQ高级特性_第9张图片
添加绑定:
RabbitMQ消息中间件技术精讲(三)—— 深入RabbitMQ高级特性_第10张图片
RabbitMQ消息中间件技术精讲(三)—— 深入RabbitMQ高级特性_第11张图片
RabbitMQ消息中间件技术精讲(三)—— 深入RabbitMQ高级特性_第12张图片
发送消息:
RabbitMQ消息中间件技术精讲(三)—— 深入RabbitMQ高级特性_第13张图片
RabbitMQ消息中间件技术精讲(三)—— 深入RabbitMQ高级特性_第14张图片
十秒后,因为TTL过期,消息消失。

消息的TTL:

AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
                    .deliveryMode(2)
                    .expiration("10000")
                    .build();

6.3 死信队列

DLX:Dead-Letter-Exchange
当消息在队列中变成死信时,能被重新publish到另一个Exchange,该Exchange就是DLX。

发生死信队列的情况:

  • 消息被拒绝(basic.reject/ basic.nack)并且requeue=false(没有重回队列)
  • 消息TTL过期
  • 队列达到最大长度

死信队列的设置:

  1. 正常声明交换机,队列并绑定,需要在队列上设置一个参数:arguments.put("x-dead-letter-exchange", "dlx.exchange");
  2. 声明死信队列的Exchange和Queue,然后进行绑定:
    Exchange: dlx.exchange
    Queue: dlx.queue
    RoutingKey: #
  3. 在消息过期、requeue、队列达到最大长度时(即为死信),消息会发送到指定的dlx.exchange交换机上,消费者会监听该交换机所绑定的死信队列。

代码演示:

public class DlxConsumer {

    private static final String EXCHANGE_NAME = "dlx_exchange";
    private static final String ROUTING_KEY = "dlx.#";
    private static final String QUEUE_NAME = "dlx_queue";

	// DLX
    private static final String DLX_EXCHANGE = "dlx.exchange";
    private static final String DLX_QUEUE = "dlx.queue";
    private static final String DLX_ROUTING_KEY = "#";


    public static void main(String[] args) throws IOException, TimeoutException {

        ConnectionFactory connectionFactory = new ConnectionFactory();

        connectionFactory.setHost("192.168.58.129");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/test");
        connectionFactory.setUsername("orcas");
        connectionFactory.setPassword("1224");

        Connection connection = connectionFactory.newConnection();

        Channel channel = connection.createChannel();

        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC, true);
        // 1. 设置死信队列的参数
        Map<String, Object> arguments = new HashMap<>();
        arguments.put("x-dead-letter-exchange", DLX_EXCHANGE);
        channel.queueDeclare(QUEUE_NAME, true, false, false, arguments);
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);

        // 2. 声明死信队列
        channel.exchangeDeclare(DLX_EXCHANGE, BuiltinExchangeType.TOPIC, true, false, null);
        channel.queueDeclare(DLX_QUEUE, true, false, false, null);
        channel.queueBind(DLX_QUEUE, DLX_EXCHANGE, DLX_ROUTING_KEY);

        DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("Receive Message —— " + new String(body));
                // 手动ack签收
                channel.basicAck(envelope.getDeliveryTag(), false); // false 不批量签收
            }
        };
        channel.basicConsume(QUEUE_NAME, false, defaultConsumer);
    }
}

你可能感兴趣的:(消息中间件,学习笔记)