RabbitMQ 之 四种使用方式

大兔子大兔子 提交于 2020-01-12 04:21:18

、介绍
RabbitMQ是消息代理:它接受并转发消息.假想RabbitMQ是一个邮局:消息提供者把消息交给邮局,邮局在将消息分发给消息消费者.
RabbitMQ与邮局的区别在于,它处理的不是信件,而是接受,存储和转发数据消息的二进制数.

这里会出现几个名词:
生产者:生产者意味着发送消息.而发送消息的程序称为生产者.
队列:RabbitMQ内部的邮件名称.尽管消息流经RabbitMQ和您的应用程序,但它们只有存储在队列中.队列仅仅由主机的存储器&磁盘
限制约束,它本质上是一个大的消息缓存器.许多生产者可以发送一个队列的消息,许多消费者可以尝试从一个队列接收数据.
在这里插入图片描述消费者:消费与接受具有相似的含义.一个消费者是一个程序,主要是等待接受信息:

注意:生产者和经济人不必位于同一主机上。实际上,大多数应用程序中却没有.一个应用程序可以是生产者,也可以是消费者.

生产者提供消息 → RabbitMQ → 消费者接受消息
在这里插入图片描述下面我们用 java 得 maven 工程来 演示一下

声明 RabbitMQ 提供了两个 端口号 一个是 5672(TCP协议)用于程序之间的访问 和 一个15672(HTTP协议)用于网页的访问

.pom文件 导入依赖

 <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.2.RELEASE</version>
    </parent>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.3.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>

方式一:一对一队列模型

消息提供者:

package a_rabbitMQ_hello;

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

public class ProducerDemo {

    private final static String QUEUE_NAME = "test_demo_1";//定义消息队列名称
    public static void main(String[] args) throws Exception {
        //定义连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置服务地址
        factory.setHost("127.0.0.1");
        //端口
        factory.setPort(5672);
        //设置账号信息,用户名、密码、vhost
        factory.setVirtualHost("csTest");
        factory.setUsername("congshuo");
        factory.setPassword("congshuo");
        // 通过工程获取连接
        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();
    }
}

控制面板
在这里插入图片描述
看看我们 RabbitMQ 里的信息
在这里插入图片描述指当前 RabbitMQ里有 一条信息待消费

然后我们 打开消费者 进行消费
消费者代码

package a_rabbitMQ_hello;

import com.rabbitmq.client.*;

import java.io.IOException;


public class ConsumerDemo {
    private final static String QUEUE_NAME = "test_demo_1";//队列名称
    public static void main(String[] args) throws Exception {
        //定义连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置服务地址
        factory.setHost("127.0.0.1");
        //端口
        factory.setPort(5672);
        //设置账号信息,用户名、密码、vhost
        factory.setVirtualHost("csTest");
        factory.setUsername("congshuo");
        factory.setPassword("congshuo");
        // 通过工程获取连接
        Connection connection = factory.newConnection();
        // 从连接中创建通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);

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

    }
}
//消费者消费消息
class QueueingConsumer extends DefaultConsumer{
    //构造接收通道
    public QueueingConsumer(Channel channel) {
        super(channel);
    }

    //获取消息的方法,当监听到队列中有消息的时候 默认执行
    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        //super.handleDelivery(consumerTag, envelope, properties, body);
        String msg = new String(body);
        System.out.println("获得数据:"+msg);
    }
}

控制台
在这里插入图片描述显然消费者从RabbitMQ获取到了信息

在让我们看看RabbitMQ 里面是否还存在未消费的信息
在这里插入图片描述显然信息也被消费

从上面我们可以看出创建一对一 队列的步骤
生产者:
1.创建连接工厂(工厂模式)
 - 通过用户名(username)
 - 密码(password)
 - 交换机(Exchange)
 - ip地址(ip)
 - 端口号(port)
2.创建连接频道(Channel)
3.创建队列(queueDeclare)
4.向队列存入信息
5.关闭各种流

消费者:
1.创建连接工厂(工厂模式)
 - 通过用户名(username)
 - 密码(password)
 - 交换机(Exchange)
 - ip地址(ip)
 - 端口号(port)
2.创建连接频道(Channel)
3.定义队列的消费者
4.监听队列(basicConsume)
5.如果队列中有信息则读取出来
6.关闭各种流

消息确认机制
通过刚才的案例可以看出,消息一旦被消费者接受,队列中的消息就会删除.
那我让我们来想一下:RabbitMQ怎么知道消息被接收了呢?

这就需要让我们通过确认机制(Acknowlege)来实现了.当消费者获取消息后,会向RabbitMQ发送回执ACK,告知消息已经被接收.
不过这种回执ACK分为两种情况:

  •  自动ACK:消息一旦被接收,消息会自动发送ACK
    
  •  手动ACK:消息接收后,不会发送ACK,需要手动调用
    

到底是手动ACK好,还是自动ACK好呢?
这种情况就要根据不同的情况来定了:

  •  如果消息不太重要,丢失也没有影响,那么自动ACK会比较方便
    
  •  如果消息非常重要,不容易丢失.那么最好在消费完成之后手动ACK,否则解溲消息后就自动ACK,RabbitMQ就会把消息从
    
    列表中删除.如果此时消费者宕机,那么消息就丢失了.

手动ACK:

package a_rabbitMQ_hello;

import com.rabbitmq.client.*;

import java.io.IOException;


public class ConsumerDemo {
    private final static String QUEUE_NAME = "test_demo_1";//队列名称
    public static void main(String[] args) throws Exception {
        //定义连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置服务地址
        factory.setHost("127.0.0.1");
        //端口
        factory.setPort(5672);
        //设置账号信息,用户名、密码、vhost
        factory.setVirtualHost("csTest");
        factory.setUsername("congshuo");
        factory.setPassword("congshuo");
        // 通过工程获取连接
        Connection connection = factory.newConnection();
        // 从连接中创建通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);

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

    }
}
//消费者消费消息
class QueueingConsumer extends DefaultConsumer{
	//通过频道手动ACK
    private Channel channel;
    //构造接收通道
    public QueueingConsumer(Channel channel) {
        super(channel);
        this.channel =channel;
    }

    //获取消息的方法,当监听到队列中有消息的时候 默认执行
    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        //super.handleDelivery(consumerTag, envelope, properties, body);
        String msg = new String(body);
        channel.basicAck(envelope.getDeliveryTag(), false); //手动ACK
        System.out.println("获得数据:"+msg);
    }
}

方式二:工作队列模式:(Work Queues)(任务模型)

该队列将用于在多个消费者分配任务

当消息处理比较耗时的时候,可能生产消息的速度会远远大于消费者的消费速度.长此以往,消息就会堆积的越来越多,无法及时处理.
此时就可以使用任务模型:让多个消费者绑定到一个队列,共同消费队列中的消息.队列中的消息一旦消费,就会消失,因此任务是不
会被重复执行的.

这个概念在WEB应用程序中特别有用,因为在Web应用程序中,不可能在较短的HTTP请求窗口内处理复杂的任务.
在这里插入图片描述

p:生产者:任务发布者
c1:消费者,领取任务并完成任务,
c2:消费者,领取任务并消费任务,

生产者代码

package c_rabbitMQ_work;

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

public class ProducerDemo {

    private final static String QUEUE_NAME = "test_demo_3";

    public static void main(String[] args) throws Exception {
        //定义连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置服务地址
        factory.setHost("127.0.0.1");
        //端口
        factory.setPort(5672);
        //设置账号信息,用户名、密码、vhost
        factory.setVirtualHost("csTest");
        factory.setUsername("congshuo");
        factory.setPassword("congshuo");
        // 通过工程获取连接
        Connection connection = factory.newConnection();

        // 从连接中创建通道
        Channel channel = connection.createChannel();
        // 声明(创建)队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        for(int i = 0 ; i < 100 ; i ++){
            // 消息内容
            String message = "发送消息:" + i;
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            System.out.println(message);
            Thread.sleep(30);
        }

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

因为这里我们模拟 2个消费者在消费 所以 我们提供的消息为100条
在这里插入图片描述再让我们 看看 RabbitMQ里面的信息(100条数据)
在这里插入图片描述

消费者代码:(这里涉及到循环调用 博主采用线程 使其循环)

package c_rabbitMQ_work;
import com.rabbitmq.client.*;
import java.io.IOException;

public class ConsumerDemoSleep {
    
    private final static String QUEUE_NAME = "test_demo_3";
    
    public static void main(String[] args) throws Exception {
        //定义连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置服务地址
        factory.setHost("127.0.0.1");
        //端口
        factory.setPort(5672);
        //设置账号信息,用户名、密码、vhost
        factory.setVirtualHost("csTest");
        factory.setUsername("congshuo");
        factory.setPassword("congshuo");
        // 通过工程获取连接
        Connection connection = factory.newConnection();
        // 从连接中创建通道
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        //设置每次取的数量
        channel.basicQos(1);
        // 定义队列的消费者

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                QueueingConsumerSleep consumer = new QueueingConsumerSleep(channel);
                try {
                    channel.basicConsume(QUEUE_NAME, false, consumer);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                QueueingConsumerSleep1 consumer1 = new QueueingConsumerSleep1(channel);
                try {
                    channel.basicConsume(QUEUE_NAME, false, consumer1);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
        thread2.start();
        // 监听队列
        //参数2表示为是否需要自动回执


    }
}

class QueueingConsumerSleep extends DefaultConsumer {
    private final static String QUEUE_NAME = "test_demo_3";
    private Channel channel;

    //构造接收通道
    public QueueingConsumerSleep(Channel channel) {
        super(channel);
        this.channel = channel;
    }

    //获取消息的方法,当监听到队列中有消息的时候 默认执行
    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        String msg = new String(body);
        System.out.println("获得数据xx:" + msg);
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //确认收到一个或多个消息
        channel.basicAck(envelope.getDeliveryTag(), false);
    }
}

class QueueingConsumerSleep1 extends DefaultConsumer {
    private final static String QUEUE_NAME = "test_demo_3";
    private Channel channel;

    //构造接收通道
    public QueueingConsumerSleep1(Channel channel) {
        super(channel);
        this.channel = channel;
    }

    //获取消息的方法,当监听到队列中有消息的时候 默认执行
    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        String msg = new String(body);
        System.out.println("获得数据yy:" + msg);
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //确认收到一个或多个消息
        channel.basicAck(envelope.getDeliveryTag(), false);
    }
}

执行消费者 让其消费

在这里插入图片描述再来看看RabbitMQ里面的信息:
在这里插入图片描述显然信息 被消费者1和消费者2平均消费了

那么在实际工作中也是这样吗?
显然不是,这根据实际而定,如果消费者1消费的比较快那么消费者1相对消费的信息就会多一些,相反消费者2消费的信息就会少一些.

循环调度:

使用任务队列的优点之一是能够轻松并行化工作。如果我们在积压工作,我们可以增更多的消费者,这样就可以轻松扩展.
就像上面一样运行两个消费者,或者更多的消费者.
默认情况下,RabbitMQ将每个消息依次发送给下一个消费者。平均而言,每个消费者都会收到相同数量的消息。这种分发消息的方式
称为循环.与两个消费者或者更过的消费者一起尝试.

消息确认:

执行任务可能需要几秒种,有的同学想知道,如果其中一个消费者开始一项漫长的任务而仅部分完成而死掉,会发生什么情况.使用我
们当前的代码,RabbitMQ一旦将消息传递给消费者,便立即将标记为删除。这种情况下,如果你杀死一个消费者,我们将丢失正在处
理的消息。我们还将丢失的信息但尚未处理的消息发送给特定消费者.

但是我们都不想丢失任何一个信息,如果一个消费者死亡,我们将消息交付给另一个消费者.

为了确保消息永不丢失,RabbitMQ支持消息确认。消费者发送发送回一个确认,以告知RabbitMQ已经接受,处理了特定的消息.并且
RabbitMQ可以自由删除它.

如果消费者死了(其通道关闭,连接已关闭或TCP连接丢失)而没有发送确认,RabbitMQ将知道信息未完全处理,并将重新排队。如
果同时有其他消费者在线,它很快将其从新分发给另一个消费者。这样,您可以确保即使消费者偶尔死亡也不会丢失任何消息.

讯息持久性:
我们已经学会了如何确保即使消费者死亡,任务也不会丢失。但是,如果RabbitMQ服务器停止,让我们的任务丢失.
RabbitMQ退出或崩溃时,他将忘记队列和消息。要确保信息不丢失,需要做两件事:我们需要将队列和消息都标记为持久.
我们要确保RabbitMQ永远不会丢失我们的队列.为此,我们要将其生命为持久的.

//第二个参数为永久保存  true:永久保存  false:不永久保存
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
关于消息持久性的说明
将消息标记为持久性,并不能完全保证不会丢失信息。尽管它告诉RabbitMQ将消息保存到磁盘,但是RabbitMQ接收消息并且尚未保存信息时,还有很短的时间。
而且,RabbitMQ不会对每条消息都执行。-他可能只是保存到缓存中,而没有真正的写入磁盘。持久性保证并不强,但是对于我们简单任务队列而言,这已经
绰绰有余了.如果您需要更有利的保证,则可以使用发布者确认.

发布者确认:

网略可能会以不那么明显的方式出现故障,并且检测某些故障需要时间。因此,将协议帧或一组帧(例如:已发布的消息)写入其套接字的客户端不能假定该
消息已到达服务器并已成功处理.这个过程中可能会丢失它,或者会非常延迟交付.
使用标准 AMQP 0-9-1,确保消息不会丢失的唯一方法就是使用事务-使通道具有事务性,然后对每个消息或者消息集进行发布,提交.在这种情况下,交易会
不必要地增加重量,并使吞吐量降低250.为此,引入了一种确认机制.他模仿了协议中已经存在的消费者确认机制.
Channel channel = connection.createChannel();
channel.waitForConfirmsOrDie(1000L);
为了启动确认,客户端在访问频道的时候设置一个waitForConfirmsOrDie(Long OutTime)方法,取决于是否设置了等待,代理可以响应一个

 public void waitForConfirmsOrDie(long timeout) throws IOException, InterruptedException, TimeoutException {
        try {
            if (!this.waitForConfirms(timeout)) {
                this.close(200, "NACKS RECEIVED", true, (Throwable)null, false);
                throw new IOException("nacks received");
            }
        } catch (TimeoutException var4) {
            this.close(406, "TIMEOUT WAITING FOR ACK");
            throw var4;
        }
    }
状态码 200,一旦在通道上使用了waitForConfirmsOrDie(Long OutTime)方法,就称处于确认者模式.无法将事务性通道置于确认者模式,并且一旦通道处
于确认者模式,就无法使其成为事务性.
通道进入确认者模式后,代理和客户端都会对消息进行计数(在第一个waitForConfirmsOrDie(Long OutTime)上开始从1计数).然后,代理通过在同一通道
上发送ACK来确认消息,以便处理他们.所述字段包含确认消息的序列号,代理还可以在ACK中设置“multiple”字段,以指示所有消息(包含序列号的信息)已处理.

方式三:发布/订阅模式:(Publish/Subscribe)

发布/订阅:工作队列背后的假设是,每个任务都恰好交付给一个工人.在这一部分中,我们将做一些完全不同的事情-我们将消息
传达给多个消费者.(简单来说就是一个收音机,一面发,多面接收)
在这里插入图片描述
1.一个生产者对应多个消费者
2.每个消费者都有自己的队列
3.生产者并没有直接把消息发送给消费者,而是先发送给交换机。
4.每个队列都要绑定到交换机上.
5.消息是由生产者发送给交换机,由于队列绑定了交换机,在由交换机发送给队列.

生产者,发布消息

package d_rabbitMQ_fanout_exchange;

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

public class ProducerDemo {
    private final static String EXCHANGE_NAME = "test_exchange_fanout";

    public static void main(String[] argv) throws Exception {
        //定义连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置服务地址
        factory.setHost("127.0.0.1");
        //端口
        factory.setPort(5672);
        //设置账号信息,用户名、密码、vhost
        factory.setVirtualHost("csTest");
        factory.setUsername("congshuo");
        factory.setPassword("congshuo");
        // 通过工程获取连接
        Connection connection = factory.newConnection();
        //创建通道
        Channel channel = connection.createChannel();

        // 声明exchange
        //参数1: 交换机名称
        //参数2: 交换机类型
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");

        // 消息内容
        String message = "双十二折上折";
        //channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
        channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
        System.out.println(" 大家好 '" + message + "'");

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

结果:
在这里插入图片描述消费者一进行订阅:

package d_rabbitMQ_fanout_exchange;

import com.rabbitmq.client.*;

import java.io.IOException;

public class ConsumerDemo {
    private final static String QUEUE_NAME = "test_queue_work1";
    private final static String EXCHANGE_NAME = "test_exchange_fanout";

    public static void main(String[] args) throws Exception {
        //定义连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置服务地址
        factory.setHost("127.0.0.1");
        //端口
        factory.setPort(5672);
        //设置账号信息,用户名、密码、vhost
        factory.setVirtualHost("csTest");
        factory.setUsername("congshuo");
        factory.setPassword("congshuo");
        // 通过工程获取连接
        Connection connection = factory.newConnection();
        // 从连接中创建通道
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 绑定队列到交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");

        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);

        // 监听队列
        //参数2表示为是否需要自动回执
        channel.basicConsume(QUEUE_NAME, false, consumer);

    }
}

class QueueingConsumer extends DefaultConsumer{

    private Channel channel;
    //构造接收通道
    public QueueingConsumer(Channel channel) {
        super(channel);
        this.channel=channel;
    }

    //获取消息的方法,当监听到队列中有消息的时候 默认执行
    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        String msg = new String(body);
        System.out.println("消费者张三获得数据:"+msg);
        //确认收到一个或多个消息
        channel.basicAck(envelope.getDeliveryTag() , false);
    }
}

结果:

在这里插入图片描述
消费者二进行订阅

package d_rabbitMQ_fanout_exchange;

import com.rabbitmq.client.*;

import java.io.IOException;

public class ConsumerDemo2 {
    private final static String QUEUE_NAME = "test_queue_work2";
    private final static String EXCHANGE_NAME = "test_exchange_fanout";

    public static void main(String[] args) throws Exception {
        //定义连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置服务地址
        factory.setHost("localhost");
        //端口
        factory.setPort(5672);
        //设置账号信息,用户名、密码、vhost
        factory.setVirtualHost("csTest");
        factory.setUsername("congshuo");
        factory.setPassword("congshuo");
        // 通过工程获取连接
        Connection connection = factory.newConnection();
        // 从连接中创建通道
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 绑定队列到交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");

        // 定义队列的消费者
        QueueingConsumer2 consumer = new QueueingConsumer2(channel);

        // 监听队列
        //参数2表示为是否需要自动回执
        channel.basicConsume(QUEUE_NAME, false, consumer);

    }
}

class QueueingConsumer2 extends DefaultConsumer{

    private Channel channel;
    //构造接收通道
    public QueueingConsumer2(Channel channel) {
        super(channel);
        this.channel=channel;
    }

    //获取消息的方法,当监听到队列中有消息的时候 默认执行
    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        //super.handleDelivery(consumerTag, envelope, properties, body);
        //System.out.println(1/0);
        String msg = new String(body);
        System.out.println("消费者李四获得数据:"+msg);
        //确认收到一个或多个消息
        channel.basicAck(envelope.getDeliveryTag() , false);
    }
}

结果:
在这里插入图片描述
看到以上结果可以发现 我们可以通过发布/订阅模式可以对日志进行处理.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!