RabbitMQ快速上手(一)Rabbit初识以及Work模式

AMQP (Advanced Message Queuing Protocol 高级消息队列协议)是一个消息协议,它支持符合标准的客户端请求程序与符合标准的消息中间件代理进行通信。

RabbitMQ简介

RabbitMQ是一个在AMQP基础上完整的,可复用的企业消息系统。它可以用于大型软件系统各个模块之间的高效通信,支持高并发,支持可扩展。使用Erlang语言编写。

RabbitMQ相关术语

  1. Broker:简单来说就是消息队列服务器实体。
  2. Exchange:消息交换机,它指定消息按什么规则,路由到哪个队列。
  3. Queue:消息队列载体,每个消息都会被投入到一个或多个队列。
  4. Binding:绑定,它的作用就是把exchange和queue按照路由规则绑定起来。
  5. Routing Key:路由关键字,exchange根据这个关键字进行消息投递。
  6. vhost:虚拟主机,一个broker里可以开设多个vhost,用作不同用户的权限分离。
  7. producer:消息生产者,就是投递消息的程序。
  8. consumer:消息消费者,就是接受消息的程序。
  9. channel:消息通道,在客户端的每个连接里,可建立多个channel,每个channel代表一个会话任务。
  10. Virtual host: 出于多租户和安全因素设计的,把AMQP的基本组件划分到一个虚拟的分组中,类似于网络中的namespace概念。当多个不同的用户使用同一个中间件提供的服务时,可以划分出多个vhost,每个用户在自己的vhost创建exchange/queue等。

这里写图片描述

消息队列的使用过程

  1. 客户端连接到消息队列服务器,打开一个channel。
  2. 客户端声明一个exchange,并设置相关属性。
  3. 客户端声明一个queue,并设置相关属性。
  4. 客户端使用routing key,在exchange和queue之间建立好绑定关系。
  5. 客户端投递消息到exchange。
  6. exchange接收到消息后,就根据消息的key和已经设置的binding,进行消息路由,将消息投递到一个或多个队列里

RabbitMQ常用的命令

  • 启动监控管理器:rabbitmq-plugins enable rabbitmq_management
  • 关闭监控管理器:rabbitmq-plugins disable rabbitmq_management
  • 启动rabbitmq:rabbitmq-service start
  • 关闭rabbitmq:rabbitmq-service stop
  • 查看所有的队列:rabbitmqctl list_queues
  • 清除所有的队列:rabbitmqctl reset
  • 关闭应用:rabbitmqctl stop_app
  • 启动应用:rabbitmqctl start_app

用户和权限设置

  • 添加用户:rabbitmqctl add_user username password
  • 分配角色:rabbitmqctl set_user_tags username administrator
  • 新增虚拟主机:rabbitmqctl add_vhost vhost_name
  • 将新虚拟主机授权给新用户:rabbitmqctl set_permissions -p vhost_name username’.’ ‘.’ ‘.*’

角色说明

  • none 最小权限角色
  • management 管理员角色
  • policymaker 决策者
  • monitoring 监控
  • administrator 超级管理员

exchange

RabbitMQ具有五种exchange 模式,接下来我们分两篇文章来介绍,本篇都是采用Default exchange 源码

Default exchange

default exchange是一个没有名称的、被broker预先申明的direct exchange。它所拥有的一个特殊属性使它对于简单的应用程序很有作用:每个创建的queue会与它自动绑定,使用queue名称作为routing key。
举例说,当你申明一个名称为“search-indexing-online”的queue时,AMQP broker使用“search-indexing-online”作为routing key将它绑定到default exchange。因此,一条被发布到default exchange并且routing key为”search-indexing-online”将被路由到名称为”search-indexing-online”的queue。

Direct exchange

direct exchange根据消息的routing key来传送消息。direct exchange是单一传播路由消息的最佳选择(尽管他们也可以用于多路传播路由),以下是它们的工作原理:
一个routing key为K的queue与exchange进行绑定
当一条新的routing key为R的消息到达direct exchange时,exchange 将它路由至该queue如果K=R

Fanout exchange

fanout exchange路由消息到所有的与其绑定的queue中,忽略routing key。如果N个queue被绑定到一个fanout exchange,当一条新消息被发布到exchange时,消息会被复制并且传送到这N个queue。fanout exchange是广播路由的最佳选择。

Topic exchange

根据routing key,通过表达式将消息分配到匹配的队列中,Topic exchange将分发到目标queue中。如, 包含分类与标签的新闻信息推送。

Headers exchange

headers exchanges忽略routing key属性,相反用于路由的属性是从headers属性中获取的。如果消息头的值等于指定的绑定值,则认为消息是匹配的。

这里写图片描述

Hello RabbitMQ

这里写图片描述

public class Producer {

    public final static String QUEUE_NAME="rabbitMQ.test";

    public static void main(String[] args) throws Exception{
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        /*
         * 声明(创建)队列
         * 参数1:队列名称
         * 参数2:为true时server重启队列不会消失(持久化)
         * 参数3:队列是否是独占的,如果为true只能被一个connection使用,其他连接建立时会抛出异常
         * 参数4:队列不再使用时是否自动删除(没有连接,并且没有未处理的消息)
         * 参数5:建立队列时的其他参数
         */
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        String message = "Hello World!";
        /*
         * 向server发布一条消息
         * 参数1:exchange名字,若为空则使用默认的exchange
         * 参数2:routing key
         * 参数3:其他的属性
         * 参数4:消息体
         * RabbitMQ默认有一个exchange,叫default exchange,它用一个空字符串表示,它是direct exchange类型,
         * 任何发往这个exchange的消息都会被路由到routing key的名字对应的队列上,如果没有对应的队列,则消息会被丢弃
         */
        channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
        System.out.println(" [x] Sent '" + message + "'");
        //关闭通道和连接
        channel.close();
        connection.close();
    }
}
public class Customer {

    private final static String QUEUE_NAME = "rabbitMQ.test";

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        // 声明队列(如果你已经明确的知道有这个队列,那么下面这句代码可以注释掉,如果不注释掉的话,也可以理解为消费者必须监听一个队列,如果没有就创建一个)
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                                       AMQP.BasicProperties properties, byte[] body)
                    throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println(" [x] Received '" + message + "'");
            }
        };
        /*
         * 监听队列
         * 参数1:队列名称
         * 参数2:是否发送ack包,不发送ack消息会持续在服务端保存,直到收到ack。  可以通过channel.basicAck手动回复ack
         * 参数3:消费者
         */
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

这里写图片描述
这里写图片描述

work模式

如果仅仅将hello world中的消费者改为两个,生产者多生产一些消息,就会发现一个有趣的现象。消息是按照顺序分发给C1和C2的,并不是按照消费者的消息处理能力来分配的。

如果按照处理能力分发,则需要用到消费者消息应答。也就是告诉broker你处理完了,让broker再分配。
设置basicQos=1,每次只发给消费者一条信息;并把自动应答改为false,并在消费者处理完消息后,给予broker应答,发送者代码如下:
这里写图片描述

/**
 * work模式
 */
public class NewTask {

    public final static String QUEUE_NAME="test";

    public static void main(String[] args) throws Exception{
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        for(int i=0;i<10;i++){
            String message = "Hello World!"+i;
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            System.out.println(" [x] Sent '" + message + "'");

            Thread.sleep(i*100);
        }

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

}
/**
 * 测试结果:
 1、消费者1和消费者2获取到的消息内容是不同的,同一个消息只能被一个消费者获取。
 2、消费者1和消费者2获取到的消息的数量是相同的,一个是奇数一个是偶数。
 其实,这样是不合理的,应该是消费者1要比消费者2获取到的消息多才对。
 Work的能者多劳模式
 需要将上面两个消费者的channel.basicQos(1);这行代码的注释打开,再次执行会发现,休眠时间短的消费者执行的任务多
 消息的确认
 在以上的代码中,已经给出了注释,如何使用自动确认和手动确认,消费者从队列中获取消息,服务端如何知道消息已经被消费呢?
 模式1:自动确认
 只要消息从队列中获取,无论消费者获取到消息后是否成功消息,都认为是消息已经成功消费。
 模式2:手动确认
 消费者从队列中获取消息后,服务器会将该消息标记为不可用状态,等待消费者的反馈,如果消费者一直没有反馈,那么该消息将一直处于不可用状态。
 如果选用自动确认,在消费者拿走消息执行过程中出现宕机时,消息可能就会丢失!!
 */
public class Work1 {

    private static final String TASK_QUEUE_NAME = "test";

    public static void main(String[] args) throws Exception {
        final ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        final Channel channel = connection.createChannel();

        channel.queueDeclare(TASK_QUEUE_NAME, false, false, false, null);
        System.out.println("Worker1  Waiting for messages");

        // 同一时刻服务器只会发一条消息给消费者(能者多劳模式)
        channel.basicQos(1);

        final Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag,
                                       Envelope envelope,
                                       AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println("Worker1  Received '" + message + "'");
                try {
                    Thread.sleep(10); // 暂停1秒钟
                }catch (Exception e){
                    channel.abort();
                }finally {
                    // 手动返回ack包确认状态
                    channel.basicAck(envelope.getDeliveryTag(),false);
                    //channel.basicReject(); channel.basicNack(); //可以通过这两个函数拒绝消息,可以指定消息在服务器删除还是继续投递给其他消费者
                }
            }
        };
        /*
         * 监听队列,不自动返回ack包,下面手动返回
         * 如果不回复,消息不会在服务器删除
         */
        channel.basicConsume(TASK_QUEUE_NAME, false, consumer);
    }
}
public class Work2 {

    private static final String TASK_QUEUE_NAME = "test";

    public static void main(String[] args) throws Exception {
        final ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        final Channel channel = connection.createChannel();

        channel.queueDeclare(TASK_QUEUE_NAME, false, false, false, null);
        System.out.println("Worker2  Waiting for messages");

        //每次从队列获取的数量
        channel.basicQos(1);

        final Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag,
                                       Envelope envelope,
                                       AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println("Worker2  Received '" + message + "'");
                try {
                    Thread.sleep(1000); // 暂停1秒钟
                }catch (Exception e){
                    channel.abort();
                }finally {
                    channel.basicAck(envelope.getDeliveryTag(),false);
                }
            }
        };
        //消息消费完成确认
        channel.basicConsume(TASK_QUEUE_NAME, false, consumer);
    }
}

这里写图片描述
这里写图片描述

可以看到线程休眠时间短的收到了更多的消息。
这里写图片描述
我们注释掉channel.basicQos(1);
这里写图片描述
这里写图片描述

可以看到消息已经被平均消费了

小结

我们上面的HelloWorld和Work模式具有以下特点

  1. 我们在生产者中没有指定exchange 使用默认的交换器default exchange
  2. 我们的队列都是在生产者中声明的,所以先启动谁都可以(注意:在消息生产者发送消息之前应该先声明好队列)

文章参考
https://javaduqing.github.io/2018/05/15/RabbitMQ
http://www.cnblogs.com/LipeiNet/p/5973061.html
https://blog.csdn.net/qq_34021712/article/details/72567801

猜你喜欢

转载自blog.csdn.net/yz357823669/article/details/81482587
今日推荐