RabbitMQ如何保证消息99.99%被发送成功?

1. 本篇概要

RabbitMQ针对这个问题,提供了以下几个机制来解决:

  1. 生产者确认
  2. 持久化
  3. 手动Ack

本篇博客我们先讲解下生产者确认机制,剩余的机制后续单独写博客进行讲解。

2. 生产者确认

要想保证消息不丢失,首先我们得保证生产者能成功的将消息发送到RabbitMQ服务器。

但在之前的示例中,当生产者将消息发送出去之后,消息到底有没有正确地到达服务器呢?如果不进行特殊配置,默认情况下发送消息的操作是不会返回任何消息给生产者的,也就是默认情况下生产者是不知道消息有没有正确的到达服务器。

从basicPublish方法的返回类型我们也能知晓:

public void basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) throws IOException {
    this.basicPublish(exchange, routingKey, false, props, body);
}

为了更好理解,我们将之前的生产者Producer类中的channel.queueDeclare(QUEUE_NAME, false, false, false, null);注释:

package com.zwwhnly.springbootaction.rabbitmq.helloworld;

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

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

public class Producer {
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] args) throws IOException, TimeoutException {
        // 创建连接
        ConnectionFactory factory = new ConnectionFactory();
        // 设置 RabbitMQ 的主机名
        factory.setHost("localhost");
        // 创建一个连接
        Connection connection = factory.newConnection();
        // 创建一个通道
        Channel channel = connection.createChannel();
        // 指定一个队列,不存在的话自动创建
        //channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 发送消息
        String message = "Hello World!";
        channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
        System.out.println(" [x] Sent ‘" + message + "‘");
        // 关闭频道和连接
        channel.close();
        connection.close();
    }
}

此时运行代码,因为队列不存在,消息肯定没地方存储,但是程序却并未出错,也就是消息丢失了但是我们却并不知晓。

RabblitMQ针对这个问题,提供了两种解决方案:

  1. 通过事务机制实现
  2. 通过发送方确认(publisher confirm)机制实现

3. 事务机制

RabblitMQ客户端中与事务机制相关的方法有以下3个:

  1. channel.txSelect:用于将当前的信道设置成事务模式
  2. channel.txCommit:用于提交事务
  3. channel.txRollback:用于回滚事务

新建事务生产者类TransactionProducer,代码如下:

package com.zwwhnly.springbootaction.rabbitmq.producerconfirm;

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

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

public class TransactionProducer {
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] args) throws IOException, TimeoutException {
        // 创建连接
        ConnectionFactory factory = new ConnectionFactory();
        // 设置 RabbitMQ 的主机名
        factory.setHost("localhost");
        // 创建一个连接
        Connection connection = factory.newConnection();
        // 创建一个通道
        Channel channel = connection.createChannel();
        // 指定一个队列,不存在的话自动创建
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        channel.txSelect();

        // 发送消息
        String message = "Hello World!";
        channel.basicPublish("", QUEUE_NAME, null, message.getBytes());

        channel.txCommit();
        System.out.println(" [x] Sent ‘" + message + "‘");

        // 关闭频道和连接
        channel.close();
        connection.close();
    }
}

运行代码,发现队列新增成功,消息发送成功:

稍微修改下代码,看下异常机制的事务回滚:

try {
    channel.txSelect();

    // 发送消息
    String message = "Hello World!";
    channel.basicPublish("", QUEUE_NAME, null, message.getBytes());

    int result = 1 / 0;

    channel.txCommit();
    System.out.println(" [x] Sent ‘" + message + "‘");
} catch (IOException e) {
    e.printStackTrace();
    channel.txRollback();
}

因为int result = 1 / 0;肯定会触发java.lang.ArithmeticException异常,所以事务会回滚,消息发送失败:

如果要发送多条消息,可以将channel.basicPublish,channel.txCommit等方法放在循环体内,如下所示:

channel.txSelect();
int loopTimes = 10;

for (int i = 0; i < loopTimes; i++) {
    try {
        // 发送消息
        String message = "Hello World!" + i;
        channel.basicPublish("", QUEUE_NAME, null, message.getBytes());

        channel.txCommit();
        System.out.println(" [x] Sent ‘" + message + "‘");
    } catch (IOException e) {
        e.printStackTrace();
        channel.txRollback();
    }
}

虽然事务能够解决消息发送方和RabbitMQ之间消息确认的问题,只有消息成功被RabbitMQ接收,事务才能提交成功,否则便可在捕获异常之后进行事务回滚。但是使用事务机制会“吸干”RabbitMQ的性能,因此建议使用下面讲到的发送方确认机制。

4. 发送方确认机制

发送方确认机制是指生产者将信道设置成confirm(确认)模式,一旦信道进入confirm模式,所有在该信道上面发布的消息都会被指派一个唯一的ID(从1开始),一旦消息被投递到RabbitMQ服务器之后,RabbitMQ就会发送一个确认(Basic.Ack)给生产者(包含消息的唯一ID),这就使得生产者知晓消息已经正确到达了目的地了。

如果RabbitMQ因为自身内部错误导致消息丢失,就会发送一条nack(Basic.Nack)命令,生产者应用程序同样可以在回调方法中处理该nack指令。

如果消息和队列是可持久化的,那么确认消息会在消息写入磁盘之后发出。

事务机制在一条消息发送之后会使发送端阻塞,以等待RabbitMQ的回应,之后才能继续发送下一条消息。

相比之下,发送方确认机制最大的好处在于它是异步的,一旦发布一条消息。生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认后,生产者应用程序便可以通过回调方法来处理该确认消息。

5.1 普通confirm

新建确认生产类NormalConfirmProducer,代码如下:

package com.zwwhnly.springbootaction.rabbitmq.producerconfirm;

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

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

public class NormalConfirmProducer {
    private final static String EXCHANGE_NAME = "normal-confirm-exchange";

    public static void main(String[] args) throws IOException, TimeoutException {
        // 创建连接
        ConnectionFactory factory = new ConnectionFactory();
        // 设置 RabbitMQ 的主机名
        factory.setHost("localhost");
        // 创建一个连接
        Connection connection = factory.newConnection();
        // 创建一个通道
        Channel channel = connection.createChannel();
        // 创建一个Exchange
        channel.exchangeDeclare(EXCHANGE_NAME, "direct");

        try {
            channel.confirmSelect();
            // 发送消息
            String message = "normal confirm test";
            channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
            if (channel.waitForConfirms()) {
                System.out.println("send message success");
            } else {
                System.out.println("send message failed");
                // do something else...
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 关闭频道和连接
        channel.close();
        connection.close();
    }
}

channel.confirmSelect();将信道设置成confirm模式。

channel.waitForConfirms();等待发送消息的确认消息,如果发送成功,则返回ture,如果发送失败,则返回false。

如果要发送多条消息,可以将channel.basicPublish,channel.waitForConfirms等方法放在循环体内,如下所示:

channel.confirmSelect();
int loopTimes = 10;

for (int i = 0; i < loopTimes; i++) {
    try {
        // 发送消息
        String message = "normal confirm test" + i;
        channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
        if (channel.waitForConfirms()) {
            System.out.println("send message success");
        } else {
            System.out.println("send message failed");
            // do something else...
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

运行结果:

send message success

send message success

send message success

send message success

send message success

send message success

send message success

send message success

send message success

send message success

如果不开启信道的confirm模式,调用channel.waitForConfirms()会报错:

注意事项:

1)事务机制和publisher confirm机制是互斥的,不能共存。

如果企图将已开启事务模式的信道再设置为publisher confirm模式,RabbitMQ会报错:

channel.txSelect();
channel.confirmSelect();

如果企图将已开启publisher confirm模式的信道再设置为事务模式,RabbitMQ也会报错:

channel.confirmSelect();
channel.txSelect();

2)事务机制和publisher confirm机制确保的是消息能够正确地发送至RabbitMQ,这里的“发送至RabbitMQ”的含义是指消息被正确地发往至RabbitMQ的交换器,如果此交换器没有匹配的队列,那么消息也会丢失。所以在使用这两种机制的时候要确保所涉及的交换器能够有匹配的队列。

比如上面的NormalConfirmProducer类发送的消息,发送到了交换器normal-confirm-exchange,但是该交换器并没有绑定任何队列,从业务角度来讲,消息仍然是丢失了。

普通confirm模式是每发送一条消息后就调用channel.waitForConfirms()方法,之后等待服务端的确认,这实际上是一种串行同步等待的方式。因此相比于事务机制,性能提升的并不多。

5.2 批量confirm

批量confirm模式是每发送一批消息后,调用channel.waitForConfirms()方法,等待服务器的确认返回,因此相比于5.1中的普通confirm模式,性能更好。

但是不好的地方在于,如果出现返回Basic.Nack或者超时情况,生产者客户端需要将这一批次的消息全部重发,这样会带来明显的重复消息数量,如果消息经常丢失,批量confirm模式的性能应该是不升反降的。

代码示例:

package com.zwwhnly.springbootaction.rabbitmq.producerconfirm;

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

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

public class BatchConfirmProducer {
    private final static String EXCHANGE_NAME = "batch-confirm-exchange";

    public static void main(String[] args) throws IOException, TimeoutException {
        // 创建连接
        ConnectionFactory factory = new ConnectionFactory();
        // 设置 RabbitMQ 的主机名
        factory.setHost("localhost");
        // 创建一个连接
        Connection connection = factory.newConnection();
        // 创建一个通道
        Channel channel = connection.createChannel();
        // 创建一个Exchange
        channel.exchangeDeclare(EXCHANGE_NAME, "direct");

        int batchCount = 100;
        int msgCount = 0;
        BlockingQueue blockingQueue = new ArrayBlockingQueue(100);
        try {
            channel.confirmSelect();
            while (msgCount <= batchCount) {
                String message = "batch confirm test";
                channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
                // 将发送出去的消息存入缓存中,缓存可以是一个ArrayList或者BlockingQueue之类的
                blockingQueue.add(message);
                if (++msgCount >= batchCount) {
                    try {
                        if (channel.waitForConfirms()) {
                            // 将缓存中的消息清空
                            blockingQueue.clear();
                        } else {
                            // 将缓存中的消息重新发送
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        // 将缓存中的消息重新发送
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 关闭频道和连接
        channel.close();
        connection.close();
    }
}

5.3 异步confirm

异步confirm模式是在生产者客户端添加ConfirmListener回调接口,重写接口的handAck()和handNack()方法,分别用来处理RabblitMQ回传的Basic.Ack和Basic.Nack。

这两个方法都有两个参数,第1个参数deliveryTag用来标记消息的唯一序列号,第2个参数multiple表示的是是否为多条确认,值为true代表是多个确认,值为false代表是单个确认。

示例代码:

package com.zwwhnly.springbootaction.rabbitmq.producerconfirm;

import com.rabbitmq.client.*;

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

public class AsyncConfirmProducer {

    private final static String EXCHANGE_NAME = "async-confirm-exchange";

    public static void main(String[] args) throws IOException, TimeoutException {
        // 创建连接
        ConnectionFactory factory = new ConnectionFactory();
        // 设置 RabbitMQ 的主机名
        factory.setHost("localhost");
        // 创建一个连接
        Connection connection = factory.newConnection();
        // 创建一个通道
        Channel channel = connection.createChannel();
        // 创建一个Exchange
        channel.exchangeDeclare(EXCHANGE_NAME, "direct");

        int batchCount = 100;
        long msgCount = 1;
        SortedSet<Long> confirmSet = new TreeSet<Long>();
        channel.confirmSelect();
        channel.addConfirmListener(new ConfirmListener() {
            @Override
            public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                System.out.println("Ack,SeqNo:" + deliveryTag + ",multiple:" + multiple);
                if (multiple) {
                    confirmSet.headSet(deliveryTag - 1).clear();
                } else {
                    confirmSet.remove(deliveryTag);
                }
            }

            @Override
            public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                System.out.println("Nack,SeqNo:" + deliveryTag + ",multiple:" + multiple);
                if (multiple) {
                    confirmSet.headSet(deliveryTag - 1).clear();
                } else {
                    confirmSet.remove(deliveryTag);
                }
                // 注意这里需要添加处理消息重发的场景
            }
        });
        // 演示发送100个消息
        while (msgCount <= batchCount) {
            long nextSeqNo = channel.getNextPublishSeqNo();
            channel.basicPublish(EXCHANGE_NAME, "", null, "async confirm test".getBytes());
            confirmSet.add(nextSeqNo);
            msgCount = nextSeqNo;
        }

        // 关闭频道和连接
        channel.close();
        connection.close();
    }
}

运行结果:

Ack,SeqNo:1,multiple:false

Ack,SeqNo:2,multiple:false

Ack,SeqNo:3,multiple:false

Ack,SeqNo:4,multiple:false

Ack,SeqNo:5,multiple:false

Ack,SeqNo:6,multiple:false

Ack,SeqNo:7,multiple:false

Ack,SeqNo:8,multiple:false

Ack,SeqNo:9,multiple:false

Ack,SeqNo:10,multiple:false

注意:多次运行,发现每次运行的输出结果是不一样的,说明RabbitMQ端回传给生产者的ack消息并不是以固定的批量大小回传的。

5. 性能比较

到目前为止,我们了解到4种模式(事务机制,普通confirm,批量confirm,异步confirm)可以实现生产者确认,让我们来对比下它们的性能,简单修改下以上示例代码中发送消息的数量,比如10000条,以下为4种模式的耗时:

发送10000条消息,事务机制耗时:2103

发送10000条消息,普通confirm机制耗时:1483

发送10000条消息,批量confirm机制耗时:281

发送10000条消息,异步confirm机制耗时:214

可以看出,事务机制最慢,普通confirm机制虽有提升但是不多,批量confirm和异步confirm性能最好,大家可以根据自己喜好自行选择使用哪种机制,个人建议使用异步confirm机制。

原文地址:https://blog.51cto.com/14230003/2404213

时间: 2024-11-11 01:26:14

RabbitMQ如何保证消息99.99%被发送成功?的相关文章

RabbitMq+Spring boot 消息生产者向队列发送消息 (一)

本人学习新框架方法. 一.先学习框架基本知识,也就是看这本书的前三章,了解基本概念.比如这个Rabbitmq,我会先看一些概念,比如,交换机,路由器,队列,虚拟机. 二.然后写代码,写demo,有哪些不懂的地方直接再去翻书或者google找资料,带着问题去学习,学的更快更扎实一些. 三.然后再看这个框架的应用场景,自己能否独立的写一些简单的项目,来验证自己的成果. 四.实际项目积累经验. RabbitMq 消息生产者向队列发送消息 (一) MQ分为消息生产者和消息消费者,这次做的主要是消息的生产

解决RabbitMQ消息丢失问题和保证消息可靠性(一)

原文链接(作者一个人):https://juejin.im/post/5d468591f265da03b810427e 工作中经常用到消息中间件来解决系统间的解耦问题或者高并发消峰问题,但是消息的可靠性如何保证一直是个很大的问题,什么情况下消息就不见了?如何防止消息丢失?下面通过这篇文章,我们就聊聊RabbitMQ 消息可靠性如何解决的? 本文分三部分说明 RabbitMQ 消息丢失场景有哪些? 如何避免消息丢失? 如何设计部署消息中间件保证消息可靠性? RabbitMQ 消息丢失场景有哪些?

RabbitMQ消息丢失问题和保证消息可靠性-消费端不丢消息和HA(二)

继续上篇文章解决RabbitMQ消息丢失问题和保证消息可靠性(一) 未完成部分,我们聊聊MQ Server端的高可用和消费端如何保证消息不丢的问题? 回归上篇的内容,我们知道消息从生产端到服务端,为了保证消息不丢,我们必须做哪些事情? 发送端采用Confirm模式,注意Server端没成功通知发送端,需要重发操作需要额外处理 消息的持久化处理 上面两个操作保证消息到服务端不丢,但是非高可用状态,如果节点挂掉,服务暂时不可用,需要重启后,消息恢复,消息不会丢失,因为有磁盘存储. 本文先从消费端讲起

【转】Twitter Storm如何保证消息不丢失

Twitter Storm如何保证消息不丢失 发表于 2011 年 09 月 30 日 由 xumingming 作者: xumingming | 可以转载, 但必须以超链接形式标明文章原始出处和作者信息及版权声明网址: http://xumingming.sinaapp.com/127/twitter-storm如何保证消息不丢失/ 本文翻译自: https://github.com/nathanmarz/storm/wiki/Guaranteeing-message-processing s

Twitter Storm如何保证消息不丢失

转自:http://xumingming.sinaapp.com/127/twitter-storm如何保证消息不丢失/ storm保证从spout发出的每个tuple都会被完全处理.这篇文章介绍storm是怎么做到这个保证的,以及我们使用者怎么做才能充分利用storm的可靠性特点. 一个tuple被”完全处理”是什么意思? 就如同蝴蝶效应一样,从spout发射的一个tuple可以引起其它成千上万个tuple因它而产生, 想想那个计算一篇文章中每个单词出现次数的topology. 帮助 1 2

Strom 消息处理机制 中英对照翻译 (Storm如何保证消息被完全处理)

官方链接: http://storm.incubator.apache.org/documentation/Guaranteeing-message-processing.html What does it mean for a message to be “fully processed”? A tuple coming off a spout can trigger thousands of tuples to be created based on it. Consider, for ex

【RabbitMQ】6、rabbitmq生产者的消息确认

通过Publisher Confirms and Returns机制,生产者可以判断消息是否发送到了exchange及queue,而通过消费者确认机制,Rabbitmq可以决定是否重发消息给消费者,以保证消息被处理. 1.什么是Publisher Confirms and Returns? Delivery processing acknowledgements from consumers to RabbitMQ are known as acknowledgements in AMQP 0-

RabbitMQ (十二) 消息确认机制 - 发布者确认

消费者确认解决的问题是确认消息是否被消费者"成功消费". 它有个前提条件,那就是生产者发布的消息已经"成功"发送出去了. 因此还需要一个机制来告诉生产者,你发送的消息真的"成功"发送了. 在标准的AMQP 0-9-1,保证消息不会丢失的唯一方法是使用事务:在通道上开启事务,发布消息,提交事务.但是事务是非常重量级的,它使得RabbitMQ的吞吐量降低250倍.为了解决这个问题,RabbitMQ 引入了 发布者确认(Publisher Confir

如何保证消息的顺序性

1.面试官心里分析 其实这个也是用MQ的时候必问的话题,第一看看你了解不了解顺序这个事儿?第二看看你有没有办法保证消息是有顺序的?这个生产系统中常见的问题. 2.面试题剖析 我举个例子,我们以前做过一个mysql binlog同步的系统,压力还是非常大的,日同步数据要达到上亿.mysql -> mysql,常见的一点在于说大数据team,就需要同步一个mysql库过来,对公司的业务系统的数据做各种复杂的操作. 你在mysql里增删改一条数据,对应出来了增删改3条binlog,接着这三条binlo