练手项目2笔记之day05

1. 页面发布

1. 需求分析

在这里插入图片描述

业务流程如下:

  1. 管理员进入管理界面点击“页面发布”,前端请求cms页面发布接口
  2. cms页面发布接口执行页面静态化,并将静态化页面(html文件)存储至GridFS中
  3. 静态化成功后,向消息队列发送页面发布的消息。
  4. 消息队列负责将消息发送给各个服务器上部署的Cms Client(Cms客户端)。
  5. 每个接收到页面发布消息的Cms Client从GridFS获取Html页面文件,并将Html文件存储在本地服务器

2. RabbitMQ研究

消息队列将页面发布的消息通知给各个服务器 ,具体见第二部分

2. RabbitMQ研究

1. 介绍

1. RabbitMQ

MQ全称Message Queue,消息队列,RabbitMQ由erlang语言开发,基于AMQP(Advanced Message Queue,高级消息队列协议)协议实现的消息队列,是一种应用程序之间的通信方法,在分布式开发中应用广泛。

应用场景:1. 任务异步处理,提高应用程序的响应时间;2.应用程序解耦合

RabbitMQ的优势:

  1. 使用简单,功能强大
  2. 基于AMQP协议
  3. 社区活跃,文档完善
  4. 高并发性能好,得益于Erlang语言
  5. Spring Boot默认集成RabbitMQ

2. 其他相关知识

AMQP

AMQP是一套公开的消息队列协议,旨在从协议层定义消息通信数据的标准格式,为了解决MQ市场上协议不统一的问题。RabbitMQ就是遵循AMQP标准协议开发的MQ服务。

JMS

java提供的一套消息服务API标准,为了所有的java应用提供统一的消息通信的标准,类似jdbc,和AMQP的区别:jms是java语言专属的消息服务标准,api层定义标准,只能用于java应用;AMQP是在协议层定义的标准,跨语言。

3. 快速入门

1. RabbitMQ的工作原理

下图是RabbitMQ的基本结构

在这里插入图片描述
组成部分说明:

  • Broker:消息队列服务进程,包括两部分Exchange和Queue
  • Exchange: 消息队列交换机,按一定规则将消息路由转发到某个队列,对消息过滤。
  • Queue:消息队列,存储消息的队列,消息到达队列并转发给指定的消费方
  • Producer:消息生产者,即生产方客户端,生产方客户端将消息发送给MQ
  • Consumer:消息消费者,即消费方客户端,接收MQ转发的消息

消息发布接收流程:

  • 发送消息
    1. 生产者和Broker建立TCP连接
    2. 生产者和Broker建立channel通道
    3. 生产者通过通道消息发送给Broker,由Exchange将消息转发
    4. Exchange将消息转发到指定的Queue(队列)
  • 接收消息
    1. 消费者和Broker建立TCP连接
    2. 消费者和Broker建立channel通道
    3. 消费者监听指定的Queue(队列)
    4. 当有消息到达Queue时Broker默认将消息 推送给消费者
    5. 消费者接收到消息

2. 下载安装

RabbitMQ由Erlang语言开发,Erlang语言用于并发及分布式系统的开发,在电信领域应用广泛,OTP(Open Telecom Platform)作为Erlang语言的一部分,包含了很多基于Erlang开发的中间件及工具库,安装RabbitMQ需要安装Erlang/OTP,并保持版本匹配

本项目使用Erlang/OTP 20.3版本和RabbitMQ3.7.3版本

1. 下载安装

  1. erlang的下载

erlang安装完成需要配置erlang环境变量: ERLANG_HOME=D:\Program Files\erl9.3 在path中添加%ERLANG_HOME%\bin;

  1. RabbitMQ 的安装

2. 启动

安装成功后会自动创建RabbitMQ服务并且启动

  1. 安装并运行服务 rabbitmq-service.bat install 安装服务 rabbitmq-service.bat stop 停止服务 rabbitmq-service.bat start 启动服务
  2. 安装管理插件 安装rabbitMQ的管理插件,方便在浏览器端管理RabbitMQ 管理员身份运行 rabbitmq-plugins.bat enable rabbitmq_management
  3. 启动成功 登录RabbitMQ 进入浏览器,输入:http://localhost:15672 初始账号和密码:guest/guest

2. hello world

1. 搭建环境

  1. 创建maven工程 创建生产者工程和消费者工程,分别加入RabbitMQ java client的依赖

生产者和消费者都属于客户端,rabbitMQ的java客户端如下:

test-rabbitmq-producer 生产者工程

test-rabbitmq-consumer :消费者工程

<dependencies>
    <dependency>
        <!--此版本与spring boot 1.5.9版本匹配-->
        <groupId>com.rabbitmq</groupId>
        <artifactId>amqp-client</artifactId>
        <version>4.0.3</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-logging</artifactId>
    </dependency>
</dependencies>

2. 生产者

在生产者工程的test中创建测试类

public class Producer01 {

  // 队列
  private static final String QUEUE = "helloworld";

  public static void main(String[] args) {
    // 通过连接工厂创建新的连接和mq建立连接
    ConnectionFactory connectionFactory = new ConnectionFactory();
    connectionFactory.setHost("127.0.0.1");
    connectionFactory.setPort(5672);
    connectionFactory.setUsername("guest");
    connectionFactory.setPassword("guest");
    // 设置虚拟机,一个mq服务可以设置多个虚拟机,每个虚拟机相当于一个独立的mq
    connectionFactory.setVirtualHost("/");
    // 建立新连接
    Connection connection = null;
    Channel channel = null;
    try {
      connection = connectionFactory.newConnection();
      // 创建会话通道,生产者和mq服务所有通信都在channel通道中完成
      channel = connection.createChannel();
      // 声明队列
      // 参数
      /**
             * String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
             *      1. queue: 队列名称
             *      2. durable:是否持久化,如果持久化,mq重启后队列还在
             *      3. exclusive:是否独占连接,队列只允许在该链接中访问,如果连接关闭后,队列自动删除,可用于临时队列
             *      4. autoDelete:自动删除,队列不再使用时是否自动删除
             *      5. arguments:参数,设置一个队列的扩展参数,如存活时间
             */
      channel.queueDeclare(QUEUE,true,false,false,null);
      // 发送消息
      /**
             *  String exchange, String routingKey, BasicProperties props, byte[] body
             *      1. exchange:交换机,如果不指定使用mq的默认交换机
             *      2. routingKey:路由key,交换机根据路由key将消息转发到指定的队列,如果使用默认交换机,routingKey设置为队列的名称
             *      3. props:消息属性
             *      4. body:消息内容
             */
      String message = "hello world 黑马程序员";
      channel.basicPublish("",QUEUE,null,message.getBytes());
      System.out.println("send to mq..........");

    } catch (Exception e) {
      e.printStackTrace();
    } finally {

      // 先关闭通道
      try {
        channel.close();
      } catch (IOException e) {
        e.printStackTrace();
      } catch (TimeoutException e) {
        e.printStackTrace();
      }
      try {
        connection.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }
}

3. 消费者

消费者工程下的test创建测试类

public class Consumer01 {
  // 队列
  private static final String QUEUE = "helloworld";

  public static void main(String[] args) throws IOException, TimeoutException {
    // 通过连接工厂创建新的连接和mq建立连接
    ConnectionFactory connectionFactory = new ConnectionFactory();
    connectionFactory.setHost("127.0.0.1");
    connectionFactory.setPort(5672);
    connectionFactory.setUsername("guest");
    connectionFactory.setPassword("guest");
    // 设置虚拟机,一个mq服务可以设置多个虚拟机,每个虚拟机相当于一个独立的mq
    connectionFactory.setVirtualHost("/");
    // 建立新连接
    Connection connection = connectionFactory.newConnection();
    // 创建会话通道,生产者和mq服务所有通信都在channel通道中完成
    Channel channel = connection.createChannel();
    // 监听队列
    // 声明队列
    /**
         * String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
         *      1. queue: 队列名称
         *      2. durable:是否持久化,如果持久化,mq重启后队列还在
         *      3. exclusive:是否独占连接,队列只允许在该链接中访问,如果连接关闭后,队列自动删除,可用于临时队列
         *      4. autoDelete:自动删除,队列不再使用时是否自动删除
         *      5. arguments:参数,设置一个队列的扩展参数,如存活时间
         */
    channel.queueDeclare(QUEUE,true,false,false,null);

    // 监听队列
    // 实现消费方法
    DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
      // 接收到消息,此方法将被调用

      /**
             *
             * @param consumerTag 消费者标签,标识消费者,在监听队列时设置channel.basicConsume
             * @param envelope 信封,通过envelope
             * @param properties 消息属性
             * @param body 消息内容
             * @throws IOException
             */
      @Override
      public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        // 交换机
        String exchange = envelope.getExchange();
        // 消息id,mq在channel中用来标识消息的id,可用于确认消息已接收
        long deliveryTag = envelope.getDeliveryTag();
        // 消息内容
        String message = new String(body,"utf-8");
        System.out.println("receive message: "+message);
      }
    };
    // 参数:
    /**
         *  String queue, boolean autoAck, Consumer callback
         *      1. queue:队列名称
         *      2. autoAck: 自动回复,当消费者接收到消息后要告诉mq消息已接收,如果将此参数设置为true会自动回复mq,false需要编程实现回复
         *      3. callback: 消费方法,当消费者接受到消息要执行的方法
         */
    channel.basicConsume(QUEUE,true,defaultConsumer);
  }
}

4. 总结

  • 发送端操作流程
    1. 创建连接
    2. 创建通道
    3. 声明队列
    4. 发送消息
  • 接收端
    1. 创建连接
    2. 创建通道
    3. 声明队列
    4. 监听队列
    5. 接收消息
    6. ack回复

4. 工作模式

有以下几种工作模式

  1. Work queues
  2. Publish/Subscribe
  3. Routing
  4. Topics
  5. Header
  6. RPC

1. Work queues

在这里插入图片描述
与入门程序相比,多了个消费端,两个消费端共同消费同一个队列中的消息

应用场景:对于任务过重或任务较多情况使用工作队列提高任务处理的速度

测试:启动多个消费者,生产者发送多个消息

结果:

  1. 一条消息只会被一个消费者接收
  2. rabbitMQ采用轮询的方式将消息平均发送给消费者
  3. 消费者在处理完某条消息后,才会收到下一条消息

2. Publish/subscribe

1. 工作模式

在这里插入图片描述
发布订阅模式:

  1. 每个消费者监听自己的队列
  2. 生产者将消息发给broker,由交换机将消息转发到绑定此交换机的每个队列,每个绑定交换机的队列都将收到消息

2. 代码

案例:用户充值成功或转账完成系统通知用户,通知方式有短信和微信公众号等方法。

  1. 生产者
public class Producer02_publish {

  // 队列
  private static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
  private static final String QUEUE_INFORM_SMS = "queue_inform_sms";
  private static final String EXCHANGE_FANOUT_INFORM = "exchange_fanout_inform";

  public static void main(String[] args) {
    // 通过连接工厂创建新的连接和mq建立连接
    ConnectionFactory connectionFactory = new ConnectionFactory();
    connectionFactory.setHost("127.0.0.1");
    connectionFactory.setPort(5672);
    connectionFactory.setUsername("guest");
    connectionFactory.setPassword("guest");
    // 设置虚拟机,一个mq服务可以设置多个虚拟机,每个虚拟机相当于一个独立的mq
    connectionFactory.setVirtualHost("/");
    // 建立新连接
    Connection connection = null;
    Channel channel = null;
    try {
      connection = connectionFactory.newConnection();
      // 创建会话通道,生产者和mq服务所有通信都在channel通道中完成
      channel = connection.createChannel();
      // 声明队列
      // 参数
      /**
             * String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
             *      1. queue: 队列名称
             *      2. durable:是否持久化,如果持久化,mq重启后队列还在
             *      3. exclusive:是否独占连接,队列只允许在该链接中访问,如果连接关闭后,队列自动删除,可用于临时队列
             *      4. autoDelete:自动删除,队列不再使用时是否自动删除
             *      5. arguments:参数,设置一个队列的扩展参数,如存活时间
             */
      channel.queueDeclare(QUEUE_INFORM_EMAIL,true,false,false,null);
      channel.queueDeclare(QUEUE_INFORM_SMS,true,false,false,null);
      // 声明交换机
      /**
             *  参数:
             *  String exchange 交换机的名称
             *  String type 交换机的类型
             *      1. fanout: 对应的rabbitmq的工作模式是 publish/subscribe
             *      2. direct: 对应的routing工作模式
             *      3. topic: 对应的topics工作模式
             *      4. headers:对应headers工作模式
             */
      channel.exchangeDeclare(EXCHANGE_FANOUT_INFORM, BuiltinExchangeType.FANOUT);
      // 交换机和队列绑定
      /**
             *  参数 String queue, String exchange, String routingKey
             *     1. queue 队列名称
             *     2. exchange 交换机名称
             *     3. routingKey 路由key,作用是交换机根据路由key的值将消息转发到指定的队列,发布订阅莫斯设为空字符串
             */
      channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_FANOUT_INFORM,"");
      channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_FANOUT_INFORM,"");
      // 发送消息
      /**
             *  String exchange, String routingKey, BasicProperties props, byte[] body
             *      1. exchange:交换机,如果不指定使用mq的默认交换机
             *      2. routingKey:路由key,交换机根据路由key将消息转发到指定的队列,如果使用默认交换机,routingKey设置为队列的名称
             *      3. props:消息属性
             *      4. body:消息内容
             */
      for (int i = 0; i < 5; i++) {
        String message = "send inform message to user";
        channel.basicPublish(EXCHANGE_FANOUT_INFORM,"",null,message.getBytes());
        System.out.println("send to mq..........");
      }


    } catch (Exception e) {
      e.printStackTrace();
    } finally {

      // 先关闭通道
      try {
        channel.close();
      } catch (IOException e) {
        e.printStackTrace();
      } catch (TimeoutException e) {
        e.printStackTrace();
      }
      try {
        connection.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }
}
  1. 消费者,邮件消费者
public class Consumer02_subscribe_email {
  // 队列
  private static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
  private static final String EXCHANGE_FANOUT_INFORM = "exchange_fanout_inform";

  public static void main(String[] args) throws IOException, TimeoutException {
    // 通过连接工厂创建新的连接和mq建立连接
    ConnectionFactory connectionFactory = new ConnectionFactory();
    connectionFactory.setHost("127.0.0.1");
    connectionFactory.setPort(5672);
    connectionFactory.setUsername("guest");
    connectionFactory.setPassword("guest");
    // 设置虚拟机,一个mq服务可以设置多个虚拟机,每个虚拟机相当于一个独立的mq
    connectionFactory.setVirtualHost("/");
    // 建立新连接
    Connection connection = connectionFactory.newConnection();
    // 创建会话通道,生产者和mq服务所有通信都在channel通道中完成
    Channel channel = connection.createChannel();
    // 监听队列
    // 声明队列
    /**
         * String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
         *      1. queue: 队列名称
         *      2. durable:是否持久化,如果持久化,mq重启后队列还在
         *      3. exclusive:是否独占连接,队列只允许在该链接中访问,如果连接关闭后,队列自动删除,可用于临时队列
         *      4. autoDelete:自动删除,队列不再使用时是否自动删除
         *      5. arguments:参数,设置一个队列的扩展参数,如存活时间
         */
    channel.queueDeclare(QUEUE_INFORM_EMAIL,true,false,false,null);

    // 监听队列
    // 实现消费方法
    DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
      // 接收到消息,此方法将被调用

      /**
             *
             * @param consumerTag 消费者标签,标识消费者,在监听队列时设置channel.basicConsume
             * @param envelope 信封,通过envelope
             * @param properties 消息属性
             * @param body 消息内容
             * @throws IOException
             */
      @Override
      public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        // 交换机
        String exchange = envelope.getExchange();
        // 消息id,mq在channel中用来标识消息的id,可用于确认消息已接收
        long deliveryTag = envelope.getDeliveryTag();
        // 消息内容
        String message = new String(body,"utf-8");
        System.out.println("receive message: "+message);
      }
    };
    // 参数:
    /**
         *  String queue, boolean autoAck, Consumer callback
         *      1. queue:队列名称
         *      2. autoAck: 自动回复,当消费者接收到消息后要告诉mq消息已接收,如果将此参数设置为true会自动回复mq,false需要编程实现回复
         *      3. callback: 消费方法,当消费者接受到消息要执行的方法
         */
    channel.basicConsume(QUEUE_INFORM_EMAIL,true,defaultConsumer);
  }
}
  1. 短信发送消费者,同上面类似

3. 测试

生产者发送的消息,每条都转发到了各个队列,每个消费者都接收到消息

4. publish/subscribe 和 work queues的区别

区别:

  1. work queues不用定义交换机,而publish/subscribe需要定义交换机
  2. publish/subscribe的生产方是面向交换机发送消息,work queues的生产方是面向队列发送消息(底层使用默认交换机)
  3. publish/subscribe需要设置队列和交换机的绑定,work queues不需要设置,实质上work queues会将队列绑定到默认的交换机

相同点:

两者实现的发布/订阅的效果是一样的,多个消费端监听同一个队列不会重复消费消息

**建议使用 publish/subscribe,**发布订阅模式比工作队列模式更强大,并且发布订阅模式可以指定自己专用的交换机。

3. Routing

1. 工作模式

在这里插入图片描述
路由模式:

  1. 每个消费者监听自己的队列,并且设置routingkey
  2. 生产者将消息发给交换机,由交换机根据routingkey转发消息给指定队列

2. 代码

  1. 生产者

声明exchange_routing_inform交换机

声明两个队列并且绑定到此交换机,绑定时需要指定routingkey

发送消息时需要指定routingkey

public class Producer03_routing {

  // 队列
  private static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
  private static final String QUEUE_INFORM_SMS = "queue_inform_sms";
  private static final String EXCHANGE_ROUTING_INFORM = "exchange_routing_inform";
  private static final String ROUTINGKEY_EMAIL = "inform_email";
  private static final String ROUTINGKEY_SMS = "inform_sms";

  public static void main(String[] args) {
    // 通过连接工厂创建新的连接和mq建立连接
    ConnectionFactory connectionFactory = new ConnectionFactory();
    connectionFactory.setHost("127.0.0.1");
    connectionFactory.setPort(5672);
    connectionFactory.setUsername("guest");
    connectionFactory.setPassword("guest");
    // 设置虚拟机,一个mq服务可以设置多个虚拟机,每个虚拟机相当于一个独立的mq
    connectionFactory.setVirtualHost("/");
    // 建立新连接
    Connection connection = null;
    Channel channel = null;
    try {
      connection = connectionFactory.newConnection();
      // 创建会话通道,生产者和mq服务所有通信都在channel通道中完成
      channel = connection.createChannel();
      // 声明队列
      // 参数
      /**
             * String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
             *      1. queue: 队列名称
             *      2. durable:是否持久化,如果持久化,mq重启后队列还在
             *      3. exclusive:是否独占连接,队列只允许在该链接中访问,如果连接关闭后,队列自动删除,可用于临时队列
             *      4. autoDelete:自动删除,队列不再使用时是否自动删除
             *      5. arguments:参数,设置一个队列的扩展参数,如存活时间
             */
      channel.queueDeclare(QUEUE_INFORM_EMAIL,true,false,false,null);
      channel.queueDeclare(QUEUE_INFORM_SMS,true,false,false,null);
      // 声明交换机
      /**
             *  参数:
             *  String exchange 交换机的名称
             *  String type 交换机的类型
             *      1. fanout: 对应的rabbitmq的工作模式是 publish/subscribe
             *      2. direct: 对应的routing工作模式
             *      3. topic: 对应的topics工作模式
             *      4. headers:对应headers工作模式
             */
      channel.exchangeDeclare(EXCHANGE_ROUTING_INFORM, BuiltinExchangeType.DIRECT);
      // 交换机和队列绑定
      /**
             *  参数 String queue, String exchange, String routingKey
             *     1. queue 队列名称
             *     2. exchange 交换机名称
             *     3. routingKey 路由key,作用是交换机根据路由key的值将消息转发到指定的队列,发布订阅莫斯设为空字符串
             */
      channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_ROUTING_INFORM,ROUTINGKEY_SMS);
      channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_ROUTING_INFORM,"inform");
      channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_ROUTING_INFORM,ROUTINGKEY_EMAIL);
      channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_ROUTING_INFORM,"inform");
      // 发送消息
      /**
             *  String exchange, String routingKey, BasicProperties props, byte[] body
             *      1. exchange:交换机,如果不指定使用mq的默认交换机
             *      2. routingKey:路由key,交换机根据路由key将消息转发到指定的队列,如果使用默认交换机,routingKey设置为队列的名称
             *      3. props:消息属性
             *      4. body:消息内容
             */
      /* for (int i = 0; i < 5; i++) {
                String message = "send email inform message to user";
                channel.basicPublish(EXCHANGE_ROUTING_INFORM,ROUTINGKEY_EMAIL,null,message.getBytes());
                System.out.println("send to mq..........");
            }
            for (int i = 0; i < 5; i++) {
                String message = "send sms inform message to user";
                channel.basicPublish(EXCHANGE_ROUTING_INFORM,ROUTINGKEY_SMS,null,message.getBytes());
                System.out.println("send to mq..........");
            }*/
      for (int i = 0; i < 5; i++) {
        String message = "send inform message to user";
        channel.basicPublish(EXCHANGE_ROUTING_INFORM,"inform",null,message.getBytes());
        System.out.println("send to mq..........");
      }

    } catch (Exception e) {
      e.printStackTrace();
    } finally {

      // 先关闭通道
      try {
        channel.close();
      } catch (IOException e) {
        e.printStackTrace();
      } catch (TimeoutException e) {
        e.printStackTrace();
      }
      try {
        connection.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }
}
  1. 邮件发送消费者
public class Consumer03_routing_email {
  // 队列
  private static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
  private static final String EXCHANGE_ROUTING_INFORM = "exchange_routing_inform";
  private static final String ROUTINGKEY_EMAIL = "inform_email";

  public static void main(String[] args) throws IOException, TimeoutException {
    // 通过连接工厂创建新的连接和mq建立连接
    ConnectionFactory connectionFactory = new ConnectionFactory();
    connectionFactory.setHost("127.0.0.1");
    connectionFactory.setPort(5672);
    connectionFactory.setUsername("guest");
    connectionFactory.setPassword("guest");
    // 设置虚拟机,一个mq服务可以设置多个虚拟机,每个虚拟机相当于一个独立的mq
    connectionFactory.setVirtualHost("/");
    // 建立新连接
    Connection connection = connectionFactory.newConnection();
    // 创建会话通道,生产者和mq服务所有通信都在channel通道中完成
    Channel channel = connection.createChannel();
    // 监听队列
    // 声明队列
    /**
         * String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
         *      1. queue: 队列名称
         *      2. durable:是否持久化,如果持久化,mq重启后队列还在
         *      3. exclusive:是否独占连接,队列只允许在该链接中访问,如果连接关闭后,队列自动删除,可用于临时队列
         *      4. autoDelete:自动删除,队列不再使用时是否自动删除
         *      5. arguments:参数,设置一个队列的扩展参数,如存活时间
         */
    channel.queueDeclare(QUEUE_INFORM_EMAIL,true,false,false,null);
    // 声明交换机
    /**
         *  参数:
         *  String exchange 交换机的名称
         *  String type 交换机的类型
         *      1. fanout: 对应的rabbitmq的工作模式是 publish/subscribe
         *      2. direct: 对应的routing工作模式
         *      3. topic: 对应的topics工作模式
         *      4. headers:对应headers工作模式
         */
    channel.exchangeDeclare(EXCHANGE_ROUTING_INFORM, BuiltinExchangeType.DIRECT);
    // 绑定队列
    channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_ROUTING_INFORM,ROUTINGKEY_EMAIL);
    // 监听队列
    // 实现消费方法
    DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
      // 接收到消息,此方法将被调用

      /**
             *
             * @param consumerTag 消费者标签,标识消费者,在监听队列时设置channel.basicConsume
             * @param envelope 信封,通过envelope
             * @param properties 消息属性
             * @param body 消息内容
             * @throws IOException
             */
      @Override
      public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        // 交换机
        String exchange = envelope.getExchange();
        // 消息id,mq在channel中用来标识消息的id,可用于确认消息已接收
        long deliveryTag = envelope.getDeliveryTag();
        // 消息内容
        String message = new String(body,"utf-8");
        System.out.println("receive message: "+message);
      }
    };
    // 参数:
    /**
         *  String queue, boolean autoAck, Consumer callback
         *      1. queue:队列名称
         *      2. autoAck: 自动回复,当消费者接收到消息后要告诉mq消息已接收,如果将此参数设置为true会自动回复mq,false需要编程实现回复
         *      3. callback: 消费方法,当消费者接受到消息要执行的方法
         */
    channel.basicConsume(QUEUE_INFORM_EMAIL,true,defaultConsumer);
  }
}
  1. 短信发送消费者,类似上面邮件的代码

3. 测试

打开RabbitMQ的管理界面,观察交换机绑定情况

有routingkey,根据routingkey转发消息到指定的队列

4. 思考

Routing模式和Publish/subscibe的区别: Routing模式要求队列在绑定交换机时要指定routingkey,消息会转发到符合routingkey的队列。

4. topics

1. 工作模式

在这里插入图片描述
路由模式:

  1. 每个消费者监听自己的队列,并且设置带通配符的routingkey
  2. 生产者将消息发给broker,由交换机根据routingkey转发消息到指定队列

2. 代码

案例:

根据用户的通知设置去通知用户,设置接收Email的用户只接收email,设置接收sms的用户只接收sms,设置两种都接收的则两种都有效。

  1. 生产者

声明交换机,指定topic类型

public class Producer04_topics {

  // 队列
  private static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
  private static final String QUEUE_INFORM_SMS = "queue_inform_sms";
  private static final String EXCHANGE_TOPICS_INFORM = "exchange_topics_inform";
  private static final String ROUTINGKEY_EMAIL = "inform.#.email.#";
  private static final String ROUTINGKEY_SMS = "inform.#.sms.#";

  public static void main(String[] args) {
    // 通过连接工厂创建新的连接和mq建立连接
    ConnectionFactory connectionFactory = new ConnectionFactory();
    connectionFactory.setHost("127.0.0.1");
    connectionFactory.setPort(5672);
    connectionFactory.setUsername("guest");
    connectionFactory.setPassword("guest");
    // 设置虚拟机,一个mq服务可以设置多个虚拟机,每个虚拟机相当于一个独立的mq
    connectionFactory.setVirtualHost("/");
    // 建立新连接
    Connection connection = null;
    Channel channel = null;
    try {
      connection = connectionFactory.newConnection();
      // 创建会话通道,生产者和mq服务所有通信都在channel通道中完成
      channel = connection.createChannel();
      // 声明队列
      // 参数
      /**
             * String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
             *      1. queue: 队列名称
             *      2. durable:是否持久化,如果持久化,mq重启后队列还在
             *      3. exclusive:是否独占连接,队列只允许在该链接中访问,如果连接关闭后,队列自动删除,可用于临时队列
             *      4. autoDelete:自动删除,队列不再使用时是否自动删除
             *      5. arguments:参数,设置一个队列的扩展参数,如存活时间
             */
      channel.queueDeclare(QUEUE_INFORM_EMAIL,true,false,false,null);
      channel.queueDeclare(QUEUE_INFORM_SMS,true,false,false,null);
      // 声明交换机
      /**
             *  参数:
             *  String exchange 交换机的名称
             *  String type 交换机的类型
             *      1. fanout: 对应的rabbitmq的工作模式是 publish/subscribe
             *      2. direct: 对应的routing工作模式
             *      3. topic: 对应的topics工作模式
             *      4. headers:对应headers工作模式
             */
      channel.exchangeDeclare(EXCHANGE_TOPICS_INFORM, BuiltinExchangeType.TOPIC);
      // 交换机和队列绑定
      /**
             *  参数 String queue, String exchange, String routingKey
             *     1. queue 队列名称
             *     2. exchange 交换机名称
             *     3. routingKey 路由key,作用是交换机根据路由key的值将消息转发到指定的队列,发布订阅莫斯设为空字符串
             */
      channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_TOPICS_INFORM,ROUTINGKEY_SMS);
      channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_TOPICS_INFORM,"inform");
      channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_TOPICS_INFORM,ROUTINGKEY_EMAIL);
      channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_TOPICS_INFORM,"inform");
      // 发送消息
      /**
             *  String exchange, String routingKey, BasicProperties props, byte[] body
             *      1. exchange:交换机,如果不指定使用mq的默认交换机
             *      2. routingKey:路由key,交换机根据路由key将消息转发到指定的队列,如果使用默认交换机,routingKey设置为队列的名称
             *      3. props:消息属性
             *      4. body:消息内容
             */
      for (int i = 0; i < 5; i++) {
        String message = "send email inform message to user";
        channel.basicPublish(EXCHANGE_TOPICS_INFORM,"inform.email",null,message.getBytes());
        System.out.println("send to mq..........");
      }
      /* for (int i = 0; i < 5; i++) {
                String message = "send sms inform message to user";
                channel.basicPublish(EXCHANGE_TOPICS_INFORM,"inform.sms",null,message.getBytes());
                System.out.println("send to mq..........");
            }*/
      for (int i = 0; i < 5; i++) {
        String message = "send sms and email inform message to user";
        channel.basicPublish(EXCHANGE_TOPICS_INFORM,"inform.sms.email",null,message.getBytes());
        System.out.println("send to mq..........");
      }

    } catch (Exception e) {
      e.printStackTrace();
    } finally {

      // 先关闭通道
      try {
        channel.close();
      } catch (IOException e) {
        e.printStackTrace();
      } catch (TimeoutException e) {
        e.printStackTrace();
      }
      try {
        connection.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }
}
  1. 消费端

队列绑定交换机指定通配符

通配符规则:

  • 中间以. 分隔
  • 符号# 可以匹配多个词,符号* 可以匹配一个词
public class Consumer04_topics_email {
  // 队列
  private static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
  private static final String EXCHANGE_TOPICS_INFORM = "exchange_topics_inform";
  private static final String ROUTINGKEY_EMAIL = "inform.#.email.#";

  public static void main(String[] args) throws IOException, TimeoutException {
    // 通过连接工厂创建新的连接和mq建立连接
    ConnectionFactory connectionFactory = new ConnectionFactory();
    connectionFactory.setHost("127.0.0.1");
    connectionFactory.setPort(5672);
    connectionFactory.setUsername("guest");
    connectionFactory.setPassword("guest");
    // 设置虚拟机,一个mq服务可以设置多个虚拟机,每个虚拟机相当于一个独立的mq
    connectionFactory.setVirtualHost("/");
    // 建立新连接
    Connection connection = connectionFactory.newConnection();
    // 创建会话通道,生产者和mq服务所有通信都在channel通道中完成
    Channel channel = connection.createChannel();
    // 监听队列
    // 声明队列
    /**
         * String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
         *      1. queue: 队列名称
         *      2. durable:是否持久化,如果持久化,mq重启后队列还在
         *      3. exclusive:是否独占连接,队列只允许在该链接中访问,如果连接关闭后,队列自动删除,可用于临时队列
         *      4. autoDelete:自动删除,队列不再使用时是否自动删除
         *      5. arguments:参数,设置一个队列的扩展参数,如存活时间
         */
    channel.queueDeclare(QUEUE_INFORM_EMAIL,true,false,false,null);
    // 声明交换机
    /**
         *  参数:
         *  String exchange 交换机的名称
         *  String type 交换机的类型
         *      1. fanout: 对应的rabbitmq的工作模式是 publish/subscribe
         *      2. direct: 对应的routing工作模式
         *      3. topic: 对应的topics工作模式
         *      4. headers:对应headers工作模式
         */
    channel.exchangeDeclare(EXCHANGE_TOPICS_INFORM, BuiltinExchangeType.TOPIC);
    // 绑定队列
    channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_TOPICS_INFORM,ROUTINGKEY_EMAIL);
    // 监听队列
    // 实现消费方法
    DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
      // 接收到消息,此方法将被调用

      /**
             *
             * @param consumerTag 消费者标签,标识消费者,在监听队列时设置channel.basicConsume
             * @param envelope 信封,通过envelope
             * @param properties 消息属性
             * @param body 消息内容
             * @throws IOException
             */
      @Override
      public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        // 交换机
        String exchange = envelope.getExchange();
        // 消息id,mq在channel中用来标识消息的id,可用于确认消息已接收
        long deliveryTag = envelope.getDeliveryTag();
        // 消息内容
        String message = new String(body,"utf-8");
        System.out.println("receive message: "+message);
      }
    };
    // 参数:
    /**
         *  String queue, boolean autoAck, Consumer callback
         *      1. queue:队列名称
         *      2. autoAck: 自动回复,当消费者接收到消息后要告诉mq消息已接收,如果将此参数设置为true会自动回复mq,false需要编程实现回复
         *      3. callback: 消费方法,当消费者接受到消息要执行的方法
         */
    channel.basicConsume(QUEUE_INFORM_EMAIL,true,defaultConsumer);
  }
}

3. 测试

生产者发送若干条消息,交换机根据routekey通配符匹配并转发到指定的队列

4. 思考

需求可否由routing工作模式实现?

使用routing模式也可以实现,但是过程比topics复杂

topic模式更加强大,可以实现routing、publish/subscribe模式的功能

5. header模式

header模式取消routingkey,使用header中的key/value匹配队列

案例:

根据用户的通知设置去通知用户,设置接收Email的用户只接收Email,设置接收sms的用户只接收sms,设置两种通知类型都接收的则两种通知都有效。

代码:

  1. 生产者

队列和交换机的绑定的代码

Map<String, Object> headers_email = new Hashtable<String, Object>();
headers_email.put("inform_type", "email");
Map<String, Object> headers_sms = new Hashtable<String, Object>();
headers_sms.put("inform_type", "sms");
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_HEADERS_INFORM,"",headers_email);
channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_HEADERS_INFORM,"",headers_sms);

通知

String message = "email inform to user"+i;
Map<String,Object> headers = new Hashtable<String, Object>();
headers.put("inform_type", "email");//匹配email通知消费者绑定的header
//headers.put("inform_type", "sms");//匹配sms通知消费者绑定的header
AMQP.BasicProperties.Builder properties = new AMQP.BasicProperties.Builder();
properties.headers(headers);
//Email通知
channel.basicPublish(EXCHANGE_HEADERS_INFORM, "", properties.build(), message.getBytes());
  1. 消费者
channel.exchangeDeclare(EXCHANGE_HEADERS_INFORM, BuiltinExchangeType.HEADERS);
Map<String, Object> headers_email = new Hashtable<String, Object>();
headers_email.put("inform_email", "email");
//交换机和队列绑定
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_HEADERS_INFORM,"",headers_email);
//指定消费队列
channel.basicConsume(QUEUE_INFORM_EMAIL, true, consumer);

3. 测试

6. PRC

在这里插入图片描述

RPC即客户端远程调用服务端的方法 ,使用MQ可以实现RPC的异步调用,基于Direct交换机实现 ,流程:

  1. 客户端既是生产者也是消费者,向RPC请求队列发送RPC调用消息,同时监听RPC响应队列
  2. 服务端监听RPC请求队列的消息,收到消息后执行服务端的方法,得到方法返回的结果
  3. 服务端将RPC方法 的结果发送到RPC响应队列
  4. 客户端(RPC调用方)监听RPC响应队列,接收到RPC调用结果。

5. spring整合RabbitMQ

1. 搭建springboot环境

基于springboot操作RabbitMQ

<dependencies>
    <!--<dependency>
        &lt;!&ndash;此版本与spring boot 1.5.9版本匹配&ndash;&gt;
        <groupId>com.rabbitmq</groupId>
        <artifactId>amqp-client</artifactId>
        <version>4.0.3</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>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-logging</artifactId>
    </dependency>
</dependencies>

2. 配置

1. 配置application.yml

server:
  port: 44000
spring:
  application:
    name: test-rabbitmq-producer
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    virtualHost: /

2. 定义RabbitConfig类

配置Exchange Queue以及绑定交换机

package com.xuecheng.test.rabbitmq.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitmqConfig {
  // 队列
  public static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
  public static final String QUEUE_INFORM_SMS = "queue_inform_sms";
  public static final String EXCHANGE_TOPICS_INFORM = "exchange_topics_inform";
  public static final String ROUTINGKEY_EMAIL = "inform.#.email.#";
  public static final String ROUTINGKEY_SMS = "inform.#.sms.#";

  // 声明交换机
  @Bean(EXCHANGE_TOPICS_INFORM)
  public Exchange EXCHANGE_TOPICS_INFORM(){
    // durable 持久化
    return ExchangeBuilder.topicExchange(EXCHANGE_TOPICS_INFORM).durable(false).build();
  }

  // 声明队列QUEUE_INFORM_EMAIL
  @Bean(QUEUE_INFORM_EMAIL)
  public Queue QUEUE_INFORM_EMAIL(){
    return new Queue(QUEUE_INFORM_EMAIL);
  }

  // 声明队列QUEUE_INFORM_SMS
  @Bean(QUEUE_INFORM_SMS)
  public Queue QUEUE_INFORM_SMS(){
    return new Queue(QUEUE_INFORM_SMS);
  }

  // 绑定email交换机和队列,指定routingkey
  @Bean
  public Binding BINDING_QUEUE_INFORM_EMAIL(@Qualifier(QUEUE_INFORM_EMAIL) Queue queue,
                                            @Qualifier(EXCHANGE_TOPICS_INFORM)Exchange exchange){
    return BindingBuilder.bind(queue).to(exchange).with(ROUTINGKEY_EMAIL).noargs();
  }

  // 绑定sms交换机和队列,指定routingkey
  @Bean
  public Binding BINDING_QUEUE_INFORM_SMS(@Qualifier(QUEUE_INFORM_SMS) Queue queue,
                                          @Qualifier(EXCHANGE_TOPICS_INFORM)Exchange exchange){
    return BindingBuilder.bind(queue).to(exchange).with(ROUTINGKEY_SMS).noargs();
  }
}

3. 生产端

使用RabbitTemplate发送消息

@RunWith(SpringRunner.class)
@SpringBootTest
public class Producer05_topics_springboot {
  @Autowired
  private RabbitTemplate rabbitTemplate;

  // 使用rabbittemplate发送消息
  @Test
  public void testSendEmail(){
    String message = "send email message to user";
    /**
         *  参数
         *  1. 交换机名称
         *  2. routingkey
         *  3. 消息内容
         */
  rabbitTemplate.convertAndSend(RabbitmqConfig.EXCHANGE_TOPICS_INFORM,"inform.email",message);
  }
}

4. 消费端

创建消费端工程,添加依赖,和生产端一样

使用@RabbitListener 注解监听队列

@Component
public class ReceiveHandler {

  @RabbitListener(queues = {RabbitmqConfig.QUEUE_INFORM_EMAIL})
  public void send_email(String msg, Message message, Channel channel){
    System.out.println("receive message is: "+msg);
  }
}

5. 测试

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章