分布式消息中间件_3 RabbitMQ消息模式-简单&工作点对点

                                         分布式消息中间件

                                    3 RabbitMQ消息模式-简单&工作点对点

                                                                                                                                                                                                田超凡

                                                                                                                                                                                           20191221

 

转载请注明原作者

1 RabbitMQ基本组成和工作模式

RabbitMQ消息中间件实现异步通讯主要由以下三种角色组成:

生产者(Producer):消息的生产方,负责投递消息到MQ服务器

消费者(Consumer):消费的调用方,负责从MQ服务器中获取生产者投递的消息

MQ服务器(MQ Broker):整个消息中间件的核心,负责管理和调度消息队列中的消息,实现RabbitMQ消息异步通讯的正常运作。

虚拟主机(Virtual Host):相对独立的MQ服务器,一个虚拟主机可以理解为是一个RabbitMQ服务器拆分后的一个模块,一个RabbitMQ服务器由多个虚拟主机组成,每一个虚拟主机都是一个相对独立的RabbitMQ服务器,每个虚拟主机内部的交换机、队列、消息都是完全隔离、互不干涉的。

RabbitMQ工作模式是按照生产-发布-消费的过程来实现的,首先生产者需要连接到MQ服务器并把消息投递到MQ服务器中的某一个虚拟主机中的队列,确保消息投递到MQ服务器成功之后,生产者的工作就算做完了。

MQ服务器会对虚拟主机存放的消息队列进行持久化来保证消息完整性,前提是配置了虚拟主机和消息队列开启了持久化模式。

紧接着,消费者会连接到MQ服务器的虚拟主机,通过和生产者绑定相同的队列来获取队列消息进行消费,消费者获取消费消息的实现方式有两种情况:

  1. 消费者第一次连接到MQ服务器,此时会主动从MQ服务器绑定的队列中拉取生产者投递的消息,这个过程叫做主动拉取
  2. 消费者连接到MQ服务器之后,生产者再次投递消息,此时MQ服务器会主动把生产者投递的消息推送到所有已经和MQ服务器建立连接,并且绑定的消息队列匹配的消费者中实现通知消费,这个过程叫做主动推送

 

 

2 RabbitMQ消息模式-简单点对点(Hello World)

   简单点对点队列模式指的是一个生产者投递消息到MQ服务器中的某个队列,一个消费者订阅MQ服务器绑定该队列,然后从队列中获取消息进行消费,简单点对点队列就是由一个生产者、一个消费者、绑定的同一个消息队列组成的消息异步通讯模式最简单的一种实现,生产者和消费者都绑定MQ服务器中的某个队列,然后生产者投递消息到该队列,消费者从该队列中获取消息进行消费。

   简单点对点消息模式的特点就是:可以实现生产者和消费者快速通过MQ服务器来异步通讯,通过绑定相同的队列来实现消息投递和消费。

   简单点对点消息模式的弊端也非常明显:生产者和消费者必须绑定相同的消息队列实现异步通讯,但是大多数业务场景下生产者和消费者都可能存在多个,基本上会根据不同业务场景定义多个消息队列来实现不同生产者和消费者之间的异步通讯,那么简单点对点消息模式就非常不灵活。还有一个致命的问题就是简单点对点消息模式完全不考虑MQ消息投递和消费的风险,容易造成生产者投递消息失败、MQ消费者消费失败等问题造成大量数据丢失,并且如果多个消费者绑定同一个消息队列实现消息消费的时候,会采用均摊消费的方式实现消费,会出现消费者消息分配不公平的情况,这是因为MQ服务器无法区分消费者消费能力的强弱,所以只能均摊消息给每个消费者进行消费。在简单点对点消息队列中,默认视为整个消息投递、MQ服务器缓存和持久化、消费的过程都是正常执行的,容灾性差,所以一般在业务规模稍微大点的项目中就不常用这种消息模式。

 

3 简单点对点消息模式(Hello World)-代码实现

Maven依赖

<dependencies>
    <dependency>
        <groupId>com.rabbitmq</groupId>
        <artifactId>amqp-client</artifactId>
        <version>3.6.5 </version>
    </dependency>
</dependencies>

 

RabbitMQ服务器连接工具类

public class RabitMQConnection {

    /**
     * 获取连接
     *
     * @return
     */
    public static Connection getConnection() throws IOException, TimeoutException {
        // 1.创建连接
        ConnectionFactory connectionFactory = new ConnectionFactory();
        // 2.设置连接地址
        connectionFactory.setHost("127.0.0.1");
        // 3.设置端口号:
        connectionFactory.setPort(5672);
        // 4.设置账号和密码
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("123456");
        // 5.设置VirtualHost
        connectionFactory.setVirtualHost("/virtualhost_first");
        return connectionFactory.newConnection();
    }
}

 

 

 

生产者

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

    public static void main(String[] args) throws IOException, TimeoutException {
        // 1.创建连接
        Connection connection = RabitMQConnection.getConnection();
        // 2.设置通道
        Channel channel = connection.createChannel();
        // 3.设置消息
        String msg = "message";
        System.out.println("msg:" + msg);
        channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
        channel.close();
        connection.close();
    }
}

 

消费者

public class Consumer {
    private static final String QUEUE_NAME = "queue_test";

    public static void main(String[] args) throws IOException, TimeoutException {
        // 1.创建连接
        Connection connection = RabitMQConnection.getConnection();
        // 2.设置通道
        Channel channel = connection.createChannel();
        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);
            }
        };
        // 3.监听队列
        channel.basicConsume(QUEUE_NAME, true, defaultConsumer);

    }
}

 

 

5 RabbitMQ消息模式-工作点对点(Work Queue)

工作点对点队列本质上也还是通过一个生产者、一个消费者绑定同一个消息队列实现消费的,但是工作点对点队列最大的特点就是引入了消息确认、手动ACK公平消费、能者多劳的特性来分阶段、分角色进行消息保护,尽可能保证消息在整个投递和消费过程中不丢失。

(1) 消息确认-生产者

生产者投递消息到MQ服务器之后,生产者会进行消息确认,必须确保消息已经顺利投递到MQ服务器并接收到MQ服务器消息顺利投递响应参数后,才会完成消息发布,确保消息发布后MQ服务器能正确接收到消息并缓存起来。

 

(2) 消息队列持久化机制-MQ服务器

MQ服务器接收到生产者投递的消息后会把消息存入消息队列中,如果开启了持久化则会把消息队列持久化到硬盘。

 

  1. 手动ACK确认消费成功-消费者

消费者进行消费的时候,默认会采用自动ACK确认消费,也就表示默认全部消费者都消费成功了,如果是多个服务器上的消费者同时消费,那么每个消费者消费的消息数量都是平均分摊的,因为MQ服务器无法区分消费者消费能力的强弱,这样做很明显是不公平的,因为不同服务器上的MQ消费者的消费能力肯定是不同的,如果全部都分摊消费的话,可能会造成消费能力能力强的消费的少,消费能力弱的消费的多的情况,典型的分工不均,时间长了可能会导致部分消费能力差的消费者由于分配了超出负荷的消息进行消费,导致消费者服务器处理性能下降,甚至造成消费者服务器宕机,最终整个消费者服务都不可用。

 

使用手动ACK确认消费机制实现消费时,消费者在接收到MQ服务器返回的消费消息之后会再次返回ACK确认标志给MQ服务器,通知MQ服务器消费成功,MQ服务器接收到消费者发送的ACK确认消费标志后就会认为消息消费成功,然后把该消息从消息队列中移除。

MQ服务器的消息队列都是采用队列结构来存放消息的,所以消息队列满足先入先出原则,在MQ服务器上是使用不同消息队列来作为容器存放生产者投递的不同消息的,当消费者消费成功之后就会把消息从对应的消息队列中移除。

 

  1. 公平消费、能者多劳-消费者

当多个消费者绑定同一个消息队列进行消费的时候,如果开启了手动ACK确认消费,那么会默认采用公平消费、能者多劳的方式实现消费,公平消费和能者多劳的基本原则是:哪个消费者消费能力强,那么消费的消息数量就多。

消费者的消费能力强弱是通过消费者消费单个消息的时长来判定的,一般来说,消费者消费消息时间越短,那么返回ACK确认消息的时间就会快,消费能力越强,消费消息数量更多。这是因为对于MQ服务器同一次推送出去的消息而言,哪个消费者返回的ACK确认消费标志越早,那么就会越早的从队列中把这个消息移除,然后越早给这个消费者继续推送队列中的其他消息,以此类推,这个消费者最终消费的消息数量也就越多。

MQ服务器对于同一个消息队列中的多个消息投递给消费者进行消费的过程而言,消息推送出去之后,只有接收到消费者消费成功后返回的ACK确认标志之后才会继续给它推送消息,在收到ACK确认消费标志之前是不会给这个消费者继续投递消息的。所以消费者对于单条消息的消费时间越短,那么返回ACK确认标志的时间就越早,那么MQ服务器接收到消费成功的信号也会越早,MQ服务器接收到消费成功信号之后就会把这个消息从消息队列中移除,然后继续给这个消费者推送队列中的其他消息,从而实现了对于消费者消费消息的能者多劳,对于绑定同一个消息队列的多个消费者而言,这是一种公平的消费模式,采用能者多劳的方式实现公平消费有助于充分利用不同消费能力的消费者服务器的消费资源,提高整体消费效率。

综上所述,工作点对点消息模式可以最大限度保证消息不丢失和实现公平消费(保证不同消费能力的消费者服务器资源的合理利用)。

 

6 工作点对点消息模式(Work Queue)-代码实现

生产者-消息确认

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

    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        System.out.println("生产者启动成功..");
        // 1.创建我们的连接
        Connection connection = RabitMQConnection.getConnection();
        // 2.创建我们通道
        Channel channel = connection.createChannel();
        // 开启了确认消息机制
        channel.confirmSelect();
        String msg = "每特教育第六期突破3万月薪";
        channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
        System.out.println("生产者发送消息成功:" + msg);
        if (channel.waitForConfirms()) {
            System.out.println("发送消息成功");
        } else {
            System.out.println("发送消息失败");
        }
        channel.close();
        connection.close();
    }
}

 

 

消费者-手动ACK公平消费,实现能者多劳

public class Consumer {
    private static final String QUEUE_NAME = "mayikt";
    private static int serviceTimeOut = 1000;

    public static void main(String[] args) throws IOException, TimeoutException {
        System.out.println("消费者01启动,处理业务时间" + serviceTimeOut);
        // 1.创建我们的连接
        Connection connection = RabitMQConnection.getConnection();
        // 2.创建我们通道
        final Channel channel = connection.createChannel();

// 3. 指定MQ服务器单次最少投递的消息数量

channel.basicQos(1);


        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:" + msg);
                // 手动ack应答模式
                channel.basicAck(envelope.getDeliveryTag(), false);
                try {
                    Thread.sleep(serviceTimeOut);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        };
        // 3.创建我们的监听的消息
        channel.basicConsume(QUEUE_NAME, false, defaultConsumer);
    }
}

 

  1. 指定MQ服务器单次向消费者投递的消息数量:channel.basicQos(1);

表示MQ服务器每次会给当前消费者投递1条消息,只有接收到消费者返回的ACK确认消费标志之后才会继续投递1条消息给该消费者,如果没有收到ACK确认消费标志则认为该消费者消费没有成功,不会继续推送消息给该消费者消费,实现公平消费,体现能者多劳。

 

  1. 生产者投递消息之前开始监控消息投递:channel.confirmSelect();

生产者确认消息投递结果:channel.waitForConfirms();

返回true表示消息投递到MQ服务器成功,返回false表示消息投递到MQ服务器失败

 

  1. 消费者手动ACK确认消息,返回ACK确认消费标志给MQ服务器channel.basicAck(envelope.getDeliveryTag(), false);

 

消费者连接到MQ服务器并绑定消息队列,监听MQ服务器返回的响应消息。第二个参数autoAck表示自动还是手动ACK确认消费,默认值是true表示自动ACK确认消费,如果要实现公平消费则需要开启手动ACK确认消费,把该参数设置为false
channel.basicConsume(QUEUE_NAME, false, defaultConsumer);

 

7 RabbitMQ队列持久化设置

登录RabbitMQ服务器管控平台,新增队列的时候会提示指定持久化参数:

 

参数名称详解:

durable表示是否持久化

durable持久化(默认)transient 不持久化

autoDelete 表示当最后一个消费者断开连接之后整个消息队列是否自动被删除默认值是no(不删除消息队列)。

关于每个消息队列中绑定的消费者数量可以通过RabbitMQ Management-Queue队列管理页面查看某个队列绑定的消费者数量,如果autoDelete设置的是true,那么队列绑定的consumers = 0时该消息队列会自动删除

以上参数也可以通过RabbitMQ提供的相关API方法进行设置

 

注意:一般情况下默认新增的虚拟主机、消息队列都是开启了持久化的,建议默认都保持持久化开启状态,这样做的目的是尽可能保证生产者投递的消息在MQ服务器中不会因为MQ服务器宕机、重启等故障导致消息丢失。

 

 

 

 

转载请注明原作者

 

发布了100 篇原创文章 · 获赞 10 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/qq_30056341/article/details/103650103