RabbitMQ笔记

文章中所有的示例代码全部上传到自己的Github中,需要参考请移步RabbitMQ

消息队列

什么是消息队列?

在我真正开始学习消息队列之前就经常听到这个词,常见于各类面试题和高并发场景,一般都会伴随着分布式系统出现,当时自己觉得这个真的好难,好深奥,但是学习一段时间后发现实际上消息队列的使用实际上并不难,难点在于对于业务的理解和模型的建立以及对出现的问题处理这方面。

究竟什么是消息队列?

在计算机科学中,消息队列(英语:Message queue)是一种进程间通信或同一进程的不同线程间的通信方式,软件的贮列用来处理一系列的输入,通常是来自用户。消息队列提供了异步的通信协议,每一个贮列中的纪录包含详细说明的数据,包含发生的时间,输入设备的种类,以及特定的输入参数,也就是说:消息的发送者和接收者不需要同时与消息队列交互。消息会保存在队列中,直到接收者取回它。——摘取自维基百科消息队列

自己对于消息队列的理解实际上可以从两方面入手,消息队列

消息与队列

什么是消息?香农在《信息的数学理论》中定义能够携带信息量的信息都可以称之为消息。简而言之有发送方有接收方并且能够在二者之间传递信息的载体都可以称之为消息。

什么是队列(queue)?一种经典的先入先出的数据结构,后端插入数据,前端获取数据。

为什么需要消息队列,这需要结合一些业务逻辑来说。在计算机中,硬件之间传输速率总是不尽相同,例如使用SATA盘向机械硬盘拷贝数据,无论SATA速率如何高,总要受到机械硬盘的限制。而软件中同样,很多情况下,我们消息在单位时间内获取的速度和软件满足单位时间内写入的速度并不相匹配,例如数据库写入瓶颈,这时候就需要使用到消息队列作为缓冲,还有很多情况之后再总结。

将消息暂存在队列中,保证消息到达的顺序性同时还能够保证消息的准确性,我觉得这是消息队列最可贵的地方。

常见消息队列

RabbitMQ

RabbitMQ是使用Erlang编写的一个开源的消息队列,本身支持很多的协议:AMQP,XMPP, SMTP, STOMP,也正因如此,它非常重量级,更适合于企业级的开发。同时实现了Broker构架,这意味着消息在发送给客户端时先在中心队列排队。对路由,负 载均衡或者数据持久化都有很好的支持。

Redis

Redis是一个基于Key-Value对的NoSQL数据库,开发维护很活跃。虽然它是一个Key-Value数据库存储系统,但它本身支持MQ功能, 所以完全可以当做一个轻量级的队列服务来使用。对于RabbitMQ和Redis的入队和出队操作,各执行100万次,每10万次记录一次执行时间。测试 数据分为128Bytes、512Bytes、1K和10K四个不同大小的数据。实验表明:入队时,当数据比较小时Redis的性能要高于 RabbitMQ,而如果数据大小超过了10K,Redis则慢的无法忍受;出队时,无论数据大小,Redis都表现出非常好的性能,而RabbitMQ 的出队性能则远低于Redis。

Kafka/Jafka

Kafka是Apache下的一个子项目,是一个高性能跨语言分布式Publish/Subscribe消息队列系统,而Jafka是在Kafka之上孵 化而来的,即Kafka的一个升级版。具有以下特性:快速持久化,可以在O(1)的系统开销下进行消息持久化;高吞吐,在一台普通的服务器上既可以达到 10W/s的吞吐速率;完全的分布式系统,Broker、Producer、Consumer都原生自动支持分布式,自动实现复杂均衡;支持Hadoop 数据并行加载,对于像Hadoop的一样的日志数据和离线分析系统,但又要求实时处理的限制,这是一个可行的解决方案。Kafka通过Hadoop的并行 加载机制来统一了在线和离线的消息处理。Apache Kafka相对于ActiveMQ是一个非常轻量级的消息系统,除了性能非常好之外,还是一个工作良好的分布式系统。

ZeroMQ

ZeroMQ号称最快的消息队列系统,尤其针对大吞吐量的需求场景。ZMQ能够实现RabbitMQ不擅长的高级/复杂的队列,但是开发人员需要自己组合 多种技术框架,技术上的复杂度是对这MQ能够应用成功的挑战。ZeroMQ具有一个独特的非中间件的模式,你不需要安装和运行一个消息服务器或中间件,因 为你的应用程序将扮演了这个服务角色。你只需要简单的引用ZeroMQ程序库,可以使用NuGet安装,然后你就可以愉快的在应用程序之间发送消息了。但 是ZeroMQ仅提供非持久性的队列,也就是说如果down机,数据将会丢失。其中,Twitter的Storm中默认使用ZeroMQ作为数据流的传 输。

ActiveMQ

ActiveMQ是Apache下的一个子项目。 类似于ZeroMQ,它能够以代理人和点对点的技术实现队列。同时类似于RabbitMQ,它少量代码就可以高效地实现高级应用场景。

消息队列常见适用场景

首先来说个自己之前的经历吧,在之前的电缆监控管理项目中,需要与很多网关进行数据交换,而这个时间节点是1分钟一次,所以在59秒的时候系统可能会收到非常多的MODBUS数据,通过解析转化为对象后,这些数据大约在3000个/s,但是这种数据高峰只出现在这一秒,其他的时候就非常平缓。由于当时的服务器并不太好,所以在用户使用时,总会在一分钟内卡顿一两秒的时间,用户体验极差。当时的设置的解决方案是将Netty分批次阻塞处理,也就是说让这3000个数据在1分钟内均匀到达。

但是这样有一些缺点,可能某个数据是1分1秒发送而我们将他阻塞到了1分59秒,数据的准确性可能会下降不少。现在看来这种场景实际上非常适合消息队列,可以先将这些数据缓存到消息队列中,再由队列写入数据库。

1.异步处理

假设现在用户需要注册一个账号,注册完成后需要发送注册邮件和短信提示,如果让整个流程按照串行化执行,从注册到系统响应共计150ms。

image

如果让发送邮件和发送短信两个步骤并行执行,需要100ms响应,显然时间已经缩短了1/3.

image

如果加入了消息队列来处理。

image

与之前的并行处理方式所不同的是,在加入消息队列后,发送邮件与发送短信功能变为了异步处理,也就是说当用户将注册信息存入数据库后,数据库会将这个对象存入消息队列,此时就完成了响应。随后发送邮件和短信的模块会自己去消息队列中获取该对象进行发送,此时用户响应为60ms,最为快捷。

2.应用解耦

消息队列同样常见于应用的解耦方面,作为一个中间件,消息队列让两个强耦合的关系变成二者同时依赖于消息队列,二者中任意一个都能够不依赖另一方所进行工作。如图(画图不易,取图请注明)

image

如果不适用消息队列直接让订单系统依赖于库存系统,则会出现二者任意一个崩溃整个系统都无法使用,而在加入消息队列后,如果库存系统崩溃,用户依旧可以正常下单,只是这些订单将存储在订单队列中,待库存系统修复后可从队列中获取进行业务处理,让整个系统更加安全,依赖程度更低。

3.流量削峰

当某个时间点流量过大,会超过服务器负载的时候需要用到流量削峰,例如在秒杀活动中,可以将秒杀订单存放进入消息队列,如果超过队列上限可以丢弃,然后再进行订单的处理,缓解服务器压力。

image

RabbitMQ

RabbitMQ是一套开源(MPL)的消息队列服务软件,是由 LShift 提供的一个 Advanced Message Queuing Protocol (AMQP) 的开源实现,由以高性能、健壮以及可伸缩性出名的 Erlang 写成。

可供选择的消息队列很多,之所以优先学习了RabbitMQ,主要在于其性能强,由Erlang编写,天生对于高并发类型友好,而且其学习资源丰富,遇到问题更加容易排查处理。

安装RabbitMQ

如果有能力最好按照官网的文档进行安装,任何时候官方文档都应该是第一选择。

  • RabbitMQ : Get Started

安装完成之后,打开默认RabbitMQ-Server分配的地址localhost:15672,默认密码和用户名都为guest。

image

Get Started In Java

简单队列

image

既然是消息队列,我们首先来用Java写个Demo往消息队列中存一些MSG,示例为简单队列,由一个消费者与一个生产者组成。

和数据库相同,首先要获取链接,这里抽象处理,编写一个RabbitMQConnectionUtils

package com.magnoliaory;

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

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

/**
 * 工具类,类似JDBC提供MQ的连接
 */
public class RabbitMQUtils {

    public static Connection getRabbitMQConnection
            (String host , Integer port , String virtualHost , String username , String password) {
        //定义一个工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //设置连接参数
        connectionFactory.setHost(host);
        //设置连接方式的端口,例如amqp对应5672
        connectionFactory.setPort(port);
        //设置Vhost , 可以看作数据库
        connectionFactory.setVirtualHost(virtualHost);
        //设置用户名和密码
        connectionFactory.setUsername(username);
        connectionFactory.setPassword(password);

        Connection connection = null;
        //获取连接
        try {
            connection = connectionFactory.newConnection();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
        return connection;
    }
}

然后编写一个生产者类来进行消息的写入。

package com.magnoliaory;


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

public class RabbitMQProduc {

    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 11; i++) {
            prodceMessage();
            Thread.sleep(3000);
        }
    }

    /**
     * 队列名称
     */
    public final static String QUEUE_NAME = "MyProductQueue";

    public static void prodceMessage() throws Exception {
        //获取连接
        Connection connection = RabbitMQUtils.
                getRabbitMQConnection("localhost", 5672,
                        "/magnolia", "guest", "guest");

        //获取通道
        Channel channel = connection.createChannel();
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        //发送消息
        String msg = "HELLO WORLD";
        channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());

        //关闭
        channel.close();
        connection.close();

    }
}

可以看到,此时消息队列中已经有了十一个数据准备就绪,等待我们get

image

编写消费者

package com.magnoliaory;

import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.Date;

public class RabbitMQConsumer {

    public static void main(String[] args) throws IOException {
        receiveMessage();
    }

    public final static String QUEUE_NAME = "MyProductQueue";


    public static void receiveMessage() throws IOException {
        //获取连接
        Connection connection = RabbitMQUtils.
                getRabbitMQConnection("localhost", 5672,
                        "/magnolia", "guest", "guest");

        Channel channel = connection.createChannel();
        //队列声明
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        //事件模型
        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("接收端收到信息 : "+msg + " " + new Date());
            }
        };
        //监听队列
        channel.basicConsume(QUEUE_NAME, true, defaultConsumer);

    }

}

看下控制台效果。

接收端收到信息 : HELLO WORLD Tue Jan 07 14:52:48 CST 2020
接收端收到信息 : HELLO WORLD Tue Jan 07 14:52:51 CST 2020
接收端收到信息 : HELLO WORLD Tue Jan 07 14:52:54 CST 2020
接收端收到信息 : HELLO WORLD Tue Jan 07 14:52:57 CST 2020
接收端收到信息 : HELLO WORLD Tue Jan 07 14:53:00 CST 2020
接收端收到信息 : HELLO WORLD Tue Jan 07 14:53:03 CST 2020
接收端收到信息 : HELLO WORLD Tue Jan 07 14:53:06 CST 2020
接收端收到信息 : HELLO WORLD Tue Jan 07 14:53:09 CST 2020
接收端收到信息 : HELLO WORLD Tue Jan 07 14:53:12 CST 2020
接收端收到信息 : HELLO WORLD Tue Jan 07 14:53:15 CST 2020

控制台中能够看到,此时consumer依旧在监听消息队列。

image

工作模式(work)

工作队列由一个生产者与多个消费者组成。

之前代码示例为简单队列(Simple Queue),其结构为传统的一对一类型,但是再实际生产中,可能存在生产者生产速度远远高于消费者消费速度,此时如果依旧按照一对一模式消费,则会造成消息在队列中积压,所以我们可以根据业务产生的速率来构建工作队列,让消费者和生产者之间速率接近平衡,此时的模型即为工作队列。

image

实际上工作队和简单队列差异很小,更多的是一种业务逻辑上的区别。看下生产者代码。

package workQueue;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import sun.reflect.misc.ConstructorUtil;
import utils.RabbitMQUtils;

import java.io.IOException;

/**
 * @author fanhao
 * 工作队列消息发送者
 */
public class WorkQueueProducer {

    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 100; i++) {
            createMsg(i);
            System.out.println("第"+i+"消息发送完成");
            Thread.sleep(500);
        }
    }


    public static final String QUEUE_NAME = "MSG_PRODUCER";

    public static void createMsg(Integer temp) throws Exception {
        Connection connection = RabbitMQUtils.
                getRabbitMQConnection("localhost", 5672,
                        "/magnolia", "guest", "guest");
        Channel channel = connection.createChannel();

        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        String msg = "消息生产者MSG_PRDUCER第" + temp + "次发送消息";

        channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());

        channel.close();
        connection.close();

    }

}

消费者代码。

package workQueue;

import com.rabbitmq.client.*;
import utils.RabbitMQUtils;

import java.io.IOException;
import java.util.Date;

public class WorkQueueConsumer {

    public static void main(String[] args) throws IOException {
        receiveMessage();
    }


    public final static String QUEUE_NAME = "MSG_PRODUCER";


    public static void receiveMessage() throws IOException {
        //获取连接
        Connection connection = RabbitMQUtils.
                getRabbitMQConnection("localhost", 5672,
                        "/magnolia", "guest", "guest");

        Channel channel = connection.createChannel();
        //队列声明
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        //事件模型
        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("接收端收到信息 : " + msg + "  " + new Date() + Thread.currentThread());
                try {
                    Thread.sleep(1500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        };
        //监听队列
        channel.basicConsume(QUEUE_NAME, true, defaultConsumer);

    }
}

这里启动两个consumer,可以看到一个奇怪的现象,两个消费者均分,一个获取到的是奇数消息,另一个获取到偶数,这种情况称之为轮询分发。

轮询分发 : 无论Consumer负载是否相同,任务消息总是均分处理,即 K%N 处理方式。

针对这种情况可以使用公平分发的模式来处理。即使用basicQos(perfetch=1)。公平分发主要区别在于,每次MQ都会按照fetch数为消费者发送消息,之后消费者处理完成并给与RabbitMQ响应后(手动反馈),才会再次发送下一个消息,这样就保证了性能强的消费者处理更多的消息,即公平分发。

首先要关闭consumer的自动应答。

//监听队列
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME,autoAck , defaultConsumer);

然后在生产者中添加对于公平队列发送的通道设计即可。

//确认响应前只发送一个消息
channel.basicQos(1);

此时启动服务器,发现二者的消费行为就有了差距。

image
image

消息应答

消息应答实际上就是一种沟通机制,在消费者和MQ之间的行为,当消费者获取消息完成后,会告知RabbitMQ,同时消息队列将消息从内存中删除。

image

自动应答

使用自动应答autoack = true时,消息将以轮询的方式发送,此时如果一个消息已经发送,并且目标消费者出现异常,该消息将永远消失,所以这种情况下存在消息丢失的风险。

手动应答

如果使用手动应答,则在消息队列未收到消息处理完成之前,并不会删除该消息,如果消息发送后消费者崩溃,消息队列将会把该消息发送给其他的消费者。

这样我们就解决了消费者异常导致任务丢失的情况,但是还存在RabbitMQ异常导致消息丢失的情况。所以RabbitMQ提供了消息持久化的功能保证我们能够将内存中的数据持久化到磁盘上,防止出现异常情况。

消息持久化

在之前写一些简单的消息队列时都会看到这样一个方法,注意RabbitMQ不允许重新定义不同参数的队列,例如我们刚刚写好的内容修改未true,将无法使用,只能新建一个队列。

channel.queueDeclare(QUEUE_NAME, false, false, false, null);

我们来看看源码中这些参数的内容。

/**
 * Declare a queue
 * @see com.rabbitmq.client.AMQP.Queue.Declare
 * @see com.rabbitmq.client.AMQP.Queue.DeclareOk
 * @param queue the name of the queue
 * @param durable true if we are declaring a durable queue (the queue will survive a server restart)
 * @param exclusive true if we are declaring an exclusive queue (restricted to this connection)
 * @param autoDelete true if we are declaring an autodelete queue (server will delete it when no longer in use)
 * @param arguments other properties (construction arguments) for the queue
 * @return a declaration-confirm method to indicate the queue was successfully declared
 * @throws java.io.IOException if an error is encountered
 */
   
Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete,
                                 Map arguments) throws IOException;

可以看到the queue will survive a server restart这句话,很明显,这个durable即将消息队列持久化到磁盘,让队列信息在服务器重启后得以幸存。持久化只需要将其设置为true即可。

订阅模式(fanout)

消息被发布到通道上。订阅者将收到其订阅的主题上的所有消息,并且所有订阅同一主题的订阅者将接收到同样的消息。

image

和前面两种队列相比较,订阅模式有如下几个特点。

  • 每个消费者拥有自己的消息队列
  • 消息将由生产者发送到交换机,再由交换机转发到消息队列中
  • 每个队列都要被绑定在交换机上
  • 生产者发送的消息经过交换机到达队列后,一个消息可以被多个消费者消费

创建生产者

public class SubscripQueueProducer {

    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 10; i++) {
            createMsg();
        }
    }

    //声明交换机的名称
    public static final String EXCHANGE_NAME = "SUBSCRIPT_QUEUE";

    public static void createMsg() throws Exception {
        //同之前
        Connection connection = RabbitMQUtils.
                getRabbitMQConnection("localhost", 5672,
                        "/magnolia", "guest", "guest");
        Channel channel = connection.createChannel();

        //声明交换机
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");

        //发送消息
        String msg = "这是一条来自交换机的消息。";

        channel.basicPublish(EXCHANGE_NAME,"" , null, msg.getBytes());

        channel.close();
        connection.close();

    }
}

运行结果,查看控制台能够看到在交换机一栏中消息已经被推送进来。

image

但是我们发现,消息却丢失了,这里需要注意,在RabbitMQ中,只有队列有存储能力,交换机无存储能力,在没有队列绑定时,数据将被直接抛弃。

下面我们写一个消费者队列。

private static final String QUEUE_NAME = "subscriptQueueConsumer";

    public static void getMsg() throws Exception {
        //同之前
        Connection connection = RabbitMQUtils.
                getRabbitMQConnection("localhost", 5672,
                        "/magnolia", "guest", "guest");
        Channel channel = connection.createChannel();
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        //将队列绑定在交换机上
        channel.queueBind(QUEUE_NAME, SubscriptQueueProducer.EXCHANGE_NAME, "");

        //事件模型
        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(" 1接收端收到信息 : "+msg + " " + new Date());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        };
        //监听队列
        channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
    }
}

先启动consumer监听,然后启动生产者,这时候可以看到控制台交换机中有绑定的队列。

image

路由模式(direct)

路由模式实际上时对于订阅模式的一个拓展。

image

订阅模式中只要生产者发送消息到交换机中,交换机会将他们推送给所有的消息队列,而路由模式则会在推送消息时携带一个Key,只有与该Key匹配的消息队列才能接收到该消息,也就是说,路由模式将能够决定给哪些消息队列推送消息。

主题模式(topic)

之前我们使用过一个方法 :

channel.queueBind(QUEUE_NAME, SubscriptQueueProducer.EXCHANGE_NAME, "");

其中最后一个参数未rountingKey(路由键),而路由模式则和此值有关。在消费者和生产者中只需要增加对于自身routingKey的绑定,同时修改Exchange的type为direct

简单来讲,主题模式类似于正则表达式或者通配符。

image

如图,其中以CH开头的消息都会发送到CH.#消息队列中,以.News结尾都会发送到#.News消息队列中,以此类推。

下面就直接上代码,生产者 :

public class TopicPouducer {

    //声明交换机的名称
    public static final String EXCHANGE_NAME = "Topic";

    public static void createMsg() throws Exception {
        //同之前
        Connection connection = RabbitMQUtils.
                getRabbitMQConnection("localhost", 5672,
                        "/magnolia", "guest", "guest");
        Channel channel = connection.createChannel();

        //声明交换机
        channel.exchangeDeclare(EXCHANGE_NAME, "topic");

        //发送消息
        String msg = "路由信息";
      
        channel.basicPublish(EXCHANGE_NAME,"goods.car" , null, msg.getBytes());

        channel.close();
        connection.close();
    }
}

消费者

public class TopicConsumer {

    private static final String QUEUE_NAME = "TopicConsumer";

    public static void getMsg() throws Exception {
        //同之前
        Connection connection = RabbitMQUtils.
                getRabbitMQConnection("localhost", 5672,
                        "/magnolia", "guest", "guest");
        Channel channel = connection.createChannel();
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        //将队列绑定在交换机上
        //队列接收一切以goods.开头的消息
        channel.queueBind(QUEUE_NAME, SubscriptQueueProducer.EXCHANGE_NAME, "goods.#");

        //事件模型
        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(" 1接收端收到信息 : "+msg + " " + new Date());
            }
        };
        //监听队列
        channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
    }
}

消息确认机制

之前消息应答保证了消费者与RabbitMQ之间的消息存储安全,但是在生产者一方,我们无法知道我们的消息是否准确发送到了RabbitMQ消息队列中。这里总结两种方式 :

  • AMQP事务机制
  • Confirm模式

AMQP

try {
      channel.txSelect();
      channel.basicPublish(EXCHANGE_NAME,"" , null, msg.getBytes());
      channel.txCommit();
 } catch (IOException e) {
      channel.txRollback();
 }

和数据库常用的事务机制类似,都是以commit和rollback为操作标准。

confirm

在confirm模式中,一旦channel使用该模式,则其发送的每一条消息都将会附带一个唯一ID值,当RabbitMQ收到该消息时,将返回一个确认给生产者,以告知消息安全到达。

由于confirm模式所使用的是异步处理,消息发送与ID确认在不同线程处理,结果通过回调处理,能够有效提高性能。

//开启confim
channel.confirmSelect();

三种编程模式 :

  • 普通模式(串行) : 发送一条confirm信息
  • 批量模式(串行) : 发送一批confirm信息
  • 异步模式 : 提供回调方法

普通模式

if (!channel.waitForConfirms()) {
    System.out.println("消息发送失败");
}

批量模式

for (int i = 0; i < 10; i++) {
     channel.basicPublish(EXCHANGE_NAME,"routingKey" , null, msg.getBytes());
}
if (!channel.waitForConfirms()) {
    System.out.println("消息发送失败");
}

批量实际上就是让所有信息发送完成后再确认即可,大部分情况下批量模式性能更好,但是如果批量模式出现问题,这一批数据都会重新发送,具体使用根据场景而定。

异步模式

在异步模式中,生产者每发送一条消息都会将该消息的ID值放入一个集合当中,然后由另一条线程根据集合进行发送确认。

由于是集合,可能会有多个消息确认收到,这个multiple为true时就是说明有多个符合要求。

package confirm;

public class ConfirmTest {

    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 100; i++) {
            createMsg();
            System.out.println("发送完成");
        }
    }

    //声明交换机的名称
    public static final String EXCHANGE_NAME = "SUBSCRIPT_QUEUE";

    public static void createMsg() throws Exception {
        //同之前
        Connection connection = RabbitMQUtils.
                getRabbitMQConnection("localhost", 5672,
                        "/magnolia", "guest", "guest");
        Channel channel = connection.createChannel();

        //未确认的消息集合
        final SortedSet confirmSet = Collections.synchronizedSortedSet(new TreeSet());

        channel.addConfirmListener(new ConfirmListener() {
            //没有问题
            public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                if (multiple) {
                    confirmSet.headSet(deliveryTag + 1).clear();
                }else {
                    confirmSet.remove(deliveryTag);
                }
            }
            //消息没有确认
            public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                if (multiple) {
                    confirmSet.headSet(deliveryTag + 1).clear();
                }else {
                    confirmSet.remove(deliveryTag);
                }
            }
        });

    }
}

感谢您百忙之中能够看完本篇文章,祝您拥有美好的一天。

参考文章

  • 维基百科 : 消息队列
  • Arvon : 常见消息队列
  • RabbitMQ官方文档

你可能感兴趣的:(RabbitMQ笔记)