4. RabbitMQ之生产者发布确认原理


在进行生产者发布确认之前先看下队列和消息持久化问题。

1. 队列持久化

队列持久化指将创建的队列持久化到磁盘中。如果创建的队列是非持久化的,当RabbitMQ服务重启后,非持久化的队列将会被删除掉,导致消息丢失。
设置队列为持久化方式很简单,只需在调queueDeclare方法声明一个队列时指定durable参数为持久化即可,下面会具体用到。
如果之前已经创建的队列是非持久化的,如果改成持久化的,需要把原来非持久化队列先删除掉然后创建一个新的持久化的队列,否则会报错。

2. 消息持久化

消息持久化指将消息持久化到磁盘中。
设置消息持久化方式也非常简单,只需要在调用basicPublish发布消息时添加属性MessageProperties.PERSISTENT_TEXT_PLAIN即可,后续会具体用到。
但是即使消息持久化了,依然不能保证消息百分百的不能丢失。假如消息将要持久化到磁盘还没持久化成功的情况下,服务器发生宕机,服务器重启后还是丢失的,因为消息没有持久化成功。只要服务器不发生宕机一些特殊情况,消息持久化已能保证基本不丢失的问题。

3. 发布确认

3.1 发布确认原理

发布确认指生产者生产消息经信道传递到RabbitMQ服务器的队列中后,服务器经过信道给生产者的一种发布确认答复。生产者将信道设置成Confirm发布确认模式(表示生产者不是发完消息就结束了,还要等待服务器的确认通知),一旦信道被设置成Confirm模式后,所有在该信道上发布的消息都会被分配一个信道中唯一的ID(从1开始),一旦消息被成功发送到服务器的队列中后,服务器就会发送一个确认应答(包含消息的唯一ID)给生产者,此时生产者才可以确定消息已经成功发送到队列中。当然队列和消息可以是持久化的也可以非持久化的,如果消息和队列都是非持久化的情况也遵从上面的发布确认原理,发布确认的目的是为了保证消息不丢失,虽然非持久情况下也是服务器发确认通知到生产者后才会认为消息被发送成功,但如果服务器出现宕机后保存在内存中的队列和消息还是会丢失的;如果消息和队列是持久化的,队列在创建时会被持久化到磁盘,那么消息在发送到服务器队列后,服务器将消息持久化到磁盘后,服务器才会发送确认应答给生产者,否则即使消息发送到了服务器队列中,如果消息还未被持久化到磁盘中,如果服务器发生宕机也会被认为消息未发送成功的。

开启发布确认很简单,只需要在channel上调用confirmSelect方法开启即可,默认是不开启的。

Confirm发布确认分为消息单个确认模式、消息批量确认模式、消息异步确认模式。

3.2 消息单个确认发布

消息单个发布确认是一种同步确认发布方式,也就是一个消息经过生产者发布后并且收到服务器返回的确认发布答复后才会被认为一条消息成功发布,然后才可以继续进行下面一条消息发布。
消息单个确认发布时要首先调用confirmSelect()方法开启发布确认,然后调用basicPublish发布消息,最后调用waitForConfirms确认消息是否发送成功。

下面案例为消息单个确认发布1000条消息的demo,顺便统计出了确认发布1000条消息的时间,为了后面对比用。其中用到的RabbitmqUtil工具类参考此文章

import com.lzj.rabbitmq.RabbitmqUtil;
import com.rabbitmq.client.Channel;
import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class producer {
    private final static String QUEUE_NAME = "hello_queue";
    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        /*创建信道*/
        Channel channel = RabbitmqUtil.getChannel();
        /*开启发布确认模式,只对当前channel生效*/
        channel.confirmSelect();
        /*
        * 声明一个队列
        * 1. 第一个参数表示队列的名字
        * 2. 第二个参数表示队列中消息是否要持久化, false表示不持久化存储在内存中, 默认为false
        * 3. 第三个参数表示该队列是否只供一个消费者消费, 不与其它消费者共享, false表示不共享
        * 4. 最后一个消费者断开链接后是否自动删除队列, true表示自动删除
        * 5. 其它参数
        * */
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);

        long beginTime = System.currentTimeMillis();    /*记录开始时间*/
        String message = null;
        for(int i=0; i<1000; i++){
            message = "hello world " + i;
            /*
             * 发送消息
             * 1. 发送到哪个交换机
             * 2. 指定路由的key是哪个
             * 3. 其它参数信息
             * 4. 消息体
             * */
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            /*
            * 等待服务器回复确认发布成功,true表示服务器回复确认发布成功,false表示发布失败
            * waitForConfirms()是阻塞的,如果信道中没有收到服务器答复就一直阻塞
            * 也可以用waitForConfirms(long)表示如果等待long时间后还未收到服务器应答
            * */
            boolean confirmFlag = channel.waitForConfirms();
            if (confirmFlag){
                System.out.println("消息发布确认成功");
            }else {
                System.out.println("消息发布失败");
            }
        }
        long endTime = System.currentTimeMillis();  /*记录结束时间*/
        System.out.println("消息单独确认时,1000个消息发布成功耗时:" + (endTime - beginTime));
    }
}

等所有消息发布完后会输出下面一句话,表示单个消息确认发布1000条耗时1672ms。

消息单独确认时,1000个消息发布成功耗时:1672

这种单个消息确认发布的方式一大弊端就是速度非常慢,因为只有收到消息的确认发布的指令后才会继续发布下一条消息,利用这种方式吞吐量不过百条左右。

3.3 消息批量确认发布

与消息单个确认发布相比,消息批量确认发布可以极大的提高系统的吞吐量,生产者发布一批消息后然后一起确认这批消息成功应答。
案例:同样对于1000条消息,每生产者每发布100条后确认一次

import com.lzj.rabbitmq.RabbitmqUtil;
import com.rabbitmq.client.Channel;
import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class producer {
    private final static String QUEUE_NAME = "hello_queue";
    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        /*创建信道*/
        Channel channel = RabbitmqUtil.getChannel();
        /*开启发布确认模式,只对当前channel生效*/
        channel.confirmSelect();
        /*
        * 声明一个队列
        * 1. 第一个参数表示队列的名字
        * 2. 第二个参数表示队列中消息是否要持久化, false表示不持久化存储在内存中, 默认为false
        * 3. 第三个参数表示该队列是否只供一个消费者消费, 不与其它消费者共享, false表示不共享
        * 4. 最后一个消费者断开链接后是否自动删除队列, true表示自动删除
        * 5. 其它参数
        * */
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);

        long beginTime = System.currentTimeMillis();    /*记录开始时间*/
        String message = null;
        int confirmSize = 0;
        for(int i=0; i<1000; i++){
            message = "hello world " + i;
            /*
             * 发送消息
             * 1. 发送到哪个交换机
             * 2. 指定路由的key是哪个
             * 3. 其它参数信息
             * 4. 消息体
             * */
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            confirmSize ++;
            if (confirmSize % 100 == 0){
                /*
                 * 等待服务器回复确认发布成功,true表示服务器回复确认发布成功,false表示发布失败
                 * waitForConfirms()是阻塞的,如果信道中没有收到服务器答复就一直阻塞
                 * 也可以用waitForConfirms(long)表示如果等待long时间后还未收到服务器应答
                 * */
                channel.waitForConfirms();
                System.out.println("一批消息发布成功");
            }
        }
        long endTime = System.currentTimeMillis();  /*记录结束时间*/
        System.out.println("消息批量确认发布时,1000个消息发布成功耗时:" + (endTime - beginTime));
    }
}

运行该demo后会输出如下一句话,表示批量发布1000个消息耗费284ms,远远小于上面每个消息都要确认发布情况的耗时。

消息批量确认发布时,1000个消息发布成功耗时:284

虽然批量确认发布提高了系统的吞吐量,但这种模式也是有一定弊端的,那就是一旦发布异常时不知道是批量消息中哪条消息异常了,这时候就要将该批次的消息都要保存后然后重新发布消息。另一种弊端是批量确认发布也是要阻塞的,就是每批次阻塞一次,但效率已经远远高于单个消息确认发布的效率了。

当然批量确认发布也是要阻塞的,每批量消息时还要阻塞一次的。

3.4 消息异步确认发布

异步确认发布是指消息异步确认发布与生产者发布消息两个步骤是异步的,不同的线程之间执行的。RabbitMQ就是通过为生产者发消息时添加一个异步监听器,生产者只负责发送消息,不负责对服务器对服务器返回的应答处理,而这个确认处理过程由监听器去回调专门的确认接口去处理,也即回调确认处理的线程去执行。
还是以上面案例,发布1000条消息观察情况

public class ProducerAsync {
    private final static String QUEUE_NAME = "hello_queue";
    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        /*创建信道*/
        Channel channel = RabbitmqUtil.getChannel();
        /*
         * 声明一个队列
         * 1. 第一个参数表示队列的名字
         * 2. 第二个参数表示队列中消息是否要持久化, false表示不持久化存储在内存中, 默认为false
         * 3. 第三个参数表示该队列是否只供一个消费者消费, 不与其它消费者共享, false表示不共享
         * 4. 最后一个消费者断开链接后是否自动删除队列, true表示自动删除
         * 5. 其它参数
         * */
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);
        /*开启发布确认模式,只对当前channel生效*/
        channel.confirmSelect();
        /*
        * 成功收到服务器确认发布应答时的回调
        * */
        ConfirmCallback ackCallback = (deliveryTag, multiple) -> {
            System.out.println(deliveryTag + " 确认发布");
        };

        /* 成功收到服务器确认发布应答时的回调 */
        ConfirmCallback nackCallback = (deliveryTag, multiple) -> {
            System.out.println("标识为: " + deliveryTag + "的消息发布失败");
        };

        /*
        * 添加监听器,用于异步监听服务器确认发布应答
        *ackCallback 表示成功确认发布回调的函数,nackCallback表示未成功确认发布回调的函数
        * */
        channel.addConfirmListener(ackCallback, nackCallback);

        long beginTime = System.currentTimeMillis();    /*记录开始时间*/
        String message = null;
        for(int i=0; i<1000; i++){
            message = "hello world " + i;
            /*
             * 发送消息
             * 1. 发送到哪个交换机
             * 2. 指定路由的key是哪个
             * 3. 其它参数信息
             * 4. 消息体
             * */
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());

        }
        long endTime = System.currentTimeMillis();  /*记录结束时间*/
        System.out.println("异步确认发布时,1000个消息发布成功耗时:" + (endTime - beginTime) + "ms");
    }

运行上面案例,打印出如下信息,表示异步发布1000条数据耗时118ms左右,可见在以上3种消息发布中,异步发布耗时最短。

步确认发布时,1000个消息发布成功耗时:118ms

虽然上面异步发布耗时非常短,一旦有未成功发布的消息时并没有保存下来未成功发布的消息,也会造成消息丢失,目前网上非常多解决办法都是先通过ConcurrentSkipListMap容器先存储消息,确认发布的消息从容器中删除,保留在容器中的就是未成功确认的消息。ConcurrentSkipListMap可用于多线程之间共享,用于高并发。代码如下所示

public class ProducerAsync {
    private final static String QUEUE_NAME = "hello_queue";
    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        /*创建信道*/
        Channel channel = RabbitmqUtil.getChannel();
        /*
         * 声明一个队列
         * 1. 第一个参数表示队列的名字
         * 2. 第二个参数表示队列中消息是否要持久化, false表示不持久化存储在内存中, 默认为false
         * 3. 第三个参数表示该队列是否只供一个消费者消费, 不与其它消费者共享, false表示不共享
         * 4. 最后一个消费者断开链接后是否自动删除队列, true表示自动删除
         * 5. 其它参数
         * */
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);
        /*开启发布确认模式,只对当前channel生效*/
        channel.confirmSelect();
        /*
        * 用ConcurrentSkipListMap储存生产者已经发过还未被服务器返回确认的消息,消息被确认后从容器中删除掉
        * 用ConcurrentSkipListMap好处就是
        *   1.支持高并发,
        *   2.并且容器内部的键值是根据消息的key排好顺序的,如果批量异步确认时可以把确认时消息的key之前的都删除掉
        * */
        ConcurrentSkipListMap<Long,String> datas = new ConcurrentSkipListMap<>();

        /*
        * 成功收到服务器确认发布应答时的回调
        * */
        ConfirmCallback ackCallback = (deliveryTag, multiple) -> {
            if (multiple){
                /*
                * 批量确认发布时, 返回信道中已经确认发布的所有消息 ,deliveryTag表示当前消息的标识 ,现在通过headMap返回信道中小于该标识且已被确认的消息
                * true表示从signals集合中截取key值小于等于deliveryTag的消息 ,相当于截取结合的左开右闭数据 , 如果fasle则表示截取的key值小于deliveryTab不包括此标识
                * 注意生成的mutiSignals并不是一个新的集合 ,只是signals集合的一个快照 ,共享一块内存 ,相当于从mutiSignals集合中删除一条数据还是会把signals中对应的数据删除掉的。
                * */
                ConcurrentNavigableMap<Long, String> mutiSignals = datas.headMap(deliveryTag, true);
                /*从集合中移除被确认的消息*/
                mutiSignals.clear();
            }else {
                /*单个消息确认发布时只删除当前标识对应的消息*/
                datas.remove(deliveryTag);
            }
        };

        /* 成功收到服务器确认发布应答时的回调 */
        ConfirmCallback nackCallback = (deliveryTag, multiple) -> {
            String message = datas.get(deliveryTag);
            System.out.println("标识为: " + deliveryTag + " ,消息内容为:" + message + " 未发布成功");
        };

        /*
        * 添加监听器,用于异步监听服务器确认发布应答
        *ackCallback 表示成功确认发布回调的函数,nackCallback表示未成功确认发布回调的函数
        * */
        channel.addConfirmListener(ackCallback, nackCallback);

        long beginTime = System.currentTimeMillis();    /*记录开始时间*/
        long time1;
        long time2;
        long costTime = 0;
        String message = null;
        for(int i=0; i<1000; i++){
            message = "hello world " + i;
            time1 = System.currentTimeMillis();
            datas.put(channel.getNextPublishSeqNo(), message);
            costTime = costTime + (System.currentTimeMillis() - time1);
            /*
             * 发送消息
             * 1. 发送到哪个交换机
             * 2. 指定路由的key是哪个
             * 3. 其它参数信息
             * 4. 消息体
             * */
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());

        }
        System.out.println("存放容器耗时 : " + costTime + "ms");
        long endTime = System.currentTimeMillis();  /*记录结束时间*/
        System.out.println("异步确认发布时,1000个消息发布成功耗时:" + (endTime - beginTime) + "ms");
    }
}

运行上面代码输出如下所示,可以看出消息保存容器操作非常耗时,占据了绝大部分时间约608ms左右,而发布消息只占用了大约734-608=126ms。

存放容器耗时 : 608ms
异步确认发布时,1000个消息发布成功耗时:734ms

虽然异步确认发布耗时有所增加,但依然远远小于单个消息确认发布的耗时,并且还能保证消息不丢失。

总结:综合对比以上3中情况

  • 单独消息确认发布:同步确认简单,消息不丢失,但吞吐量有限;
  • 批量消息确认发布:批量确认简单,吞吐量高,一旦出现问题很难确认是哪条消息出了问题;
  • 异步消息确认发布:吞吐量也比较高,消息出了问题可控,但代码相对前两种较复杂。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值