阅读 208

RabbitMQ消息中间件详解(三)

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

何为生产端的可靠性投递

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

可靠性投递的方案(不加事务)

1. 消息落库(持久化至数据库),对消息状态进行打标,如若消息未响应,进行轮询操作。

image.png

  • 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. 消息的延迟投递,做二次确认,回调检查。(高并发场景)

image.png

  • Upstream service:生产端

  • Downstream service:消费端

  • Step1:业务消息落库后,发送消息至Broker

  • Step2:紧接着发送第二条延迟(设置延迟时间)检查的消息。

  • Step3:消费端监听指定的队列接收到消息进行处理

  • Step4:处理完后,生成一条响应消息发送到Broker

  • Step5:由Callback服务去监听该响应消息,收到该响应消息后持久化至消息DB(记录成功状态)。

  • Step6:到了延迟时间,延迟发送的消息也被Callback服务的监听器监听到后,去检查消息DB。如果未查询到成功的状态,Callback服务需要做补偿,发起RPC通讯,让生产端重新发送。生产端通过介绍到的命令中所带的id去数据库查询该业务消息,再重新发送,即跳转到Step1。

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

消费端的幂等性保障

幂等性的概念

通俗的说就是执行N次操作的结果是相同的。
借鉴数据库的乐观锁机制。 执行一条更新数据库的SQL语句:
(避免并发问题,添加一个版本号,执行过减操作后递增version,就不会重复减)
UPDATE T_REPS SET COUNT = COUNT - 1,VERSION = VERSION + 1 WHERE VERSION = 1

消费端保障幂等性

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

方案:

  • 唯一ID + 指纹码机制
    唯一ID + 指纹码(业务规则、时间戳等拼接)机制,利用数据库主键去重 SELECT COUNT(1) FROM T_ORDER WHERE ID = 唯一ID + 指纹码 未查询到就insert,如有说明已处理过该消息,返回失败。

    • 优点:实现简单
    • 缺点:高并发下有数据库写入的性能瓶颈
    • 解决方案:根据ID进行分库分表、算法路由
  • 利用Redis的原子性
    需要考虑的问题:
    是否要落库数据库,如落库,数据库和缓存如何做到数据的一致性?
    不落库,数据存储在缓存中,如何设置定时同步的策略(可靠性保障)?

Confirm确认消息

Confirm消息确认机制的概念

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

确认机制的流程图

image.png 发送消息与监听应答的消息是异步操作。

确认消息的实现

  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);

    }
}
复制代码

控制台输出:
image.png image.png

Return消息机制

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

基础API

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

流程图

image.png 消息的生产者通过制定ExchangeRoutingKey,把消息投递到某一个队列中,消费者监听队列,进行消费。 但在一些情况下,发送消息时,Exchange不存在或RoutingKey路由不到,Return Listener就会监听这种不可达的消息,然后进行处理。

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的参数输出: image.png 具体的处理可以在该方法下编写。

消费端限流

消费端限流的概念

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

代码演示

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后。

队列

消费端ACK与重回队列机制

  • 消费端的手工ACKNACK

消费端进行消费时,可能由于业务异常,会调用NACK拒绝确认,而到了一定次数,就直接ACK,将异常消息进行日志的记录,然后进行补偿。 由于服务器宕机等严重问题,消费端没消费成功,重发消息后,需要手工ACK保障消费端消费成功。

  • 消费端的重回队列:

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

TTL队列

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

控制台演示:

声明队列,设置TTL时长: image.png

声明交换机:

image.png

添加绑定:

image.png

image.png

image.png

发送消息:

image.png

image.png 十秒后,因为TTL过期,消息消失。

消息的TTL

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

死信队列

DLX:Dead-Letter-Exchange

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

发生死信队列的情况:

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

死信队列的设置:

  • 正常声明交换机,队列并绑定,需要在队列上设置一个参数:arguments.put("x-dead-letter-exchange", "dlx.exchange");
  • 声明死信队列的ExchangeQueue,然后进行绑定:
    • Exchange: dlx.exchange
    • Queue: dlx.queue
    • RoutingKey: #
  • 在消息过期、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);
    }
}
复制代码
文章分类
后端
文章标签