RabbitMQ

  • 1. RabbitMQ的介绍和安装
    • 1.1 介绍
      • 1.1.1 是什么?
      • 1.1.2 使用场景
      • 1.1.3 市面上常见消息队列
      • 1.1.4 相关介绍
    • 1.2 安装
      • 1.2.1 工作原理
      • 1.2.2 安装
  • 2. Java操作RabbitMQ
    • 2.1 RabbitMQ的消息模型
    • 2.2 搭建环境
      • 2.2.1 "Hello World!"------基本消息模型
        • 2.2.1.1 生产者
        • 2.2.1.2 消费者
        • 2.2.1.3 简单测试
        • 2.2.3.4 消息确认机制(ACK)
        • 2.2.1.4 自动确认存在的问题
        • 2.2.1.5 手动确认实现
        • 2.2.1.6 小结
      • 2.2.2 Work queues
        • 2.2.2.1 生产者
        • 2.2.2.2 消费者1
        • 2.2.2.3 消费者2
        • 2.2.2.4 测试
        • 2.2.2.5 能者多劳
        • 2.2.2.6 再次测试
      • 2.2.3 订阅模型分类
        • 2.2.3.1 概述
        • 2.2.3.2 分类
      • 2.2.4 订阅模型-FANOUT
        • 2.2.4.1 工作模式
        • 2.2.4.2 生产者
        • 2.2.4.3 消费者
        • 2.2.4.4 测试
      • 2.2.5 订阅模型-Direct
        • 2.2.5.1 工作模式
        • 2.2.5.2 生产者
        • 2.2.5.3 消费者
        • 2.2.5.4 测试
      • 2.2.6 订阅模型-Topics
        • 2.2.6.1 工作模式
        • 2.2.6.2 生产者
        • 2.2.6.3 消费者
        • 2.2.6.4 测试
      • 2.2.7 Header模式
      • 2.2.7 RPC
    • 2.3 持久化-解决数据安全
      • 2.3.1 交换机持久化
      • 2.3.2 队列持久化
      • 2.3.3 消息持久化
  • 3. SpringBoot整合RabbitMQ
    • 3.1 步骤分析
    • 3.2 步骤实现
      • 3.2.1 创建项目
      • 3.2.2 pom导包
      • 3.2.3 入口类
      • 3.2.4 application.yml配置
      • 3.2.5 配置类
      • 3.2.6 生产者
      • 3.2.7 消费者
      • 3.2.8 测试

1. RabbitMQ的介绍和安装

1.1 介绍

1.1.1 是什么?

MQ全称为Message Queue,即消息队列. 它也是一个队列,遵循FIFO原则.(先进先出)

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

RabbitMQ官网地址:https://www.rabbitmq.com/

RabbitMQ是基于AMQP协议实现一种MQ

1.1.2 使用场景

开发中消息队列通常有如下应用场景:

  1. 提高系统响应速度

    任务异步处理。 将不需要同步处理的并且耗时长的操作由消息队列通知消息接收方进行异步处理。提高了应用程序的响应时间

  2. 提高系统稳定性

    系统挂了关系,操作内容放到消息队列

  3. 异步化

  4. 解耦

    应用程序解耦合MQ相当于一个中介,生产方通过MQ与消费方交互,它将应用程序进行解耦合。

  5. 排序保证 FIFO(比如应用于秒杀场景)

  6. 消除峰值

1.1.3 市面上常见消息队列

ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ、Redis。

ActiveMQ是老牌,不太好用,有点过时了!
RabbitMQ,Kafka,Redis现在这三个比较好用!

为什么使用RabbitMQ呢?

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

1.1.4 相关介绍

  1. AMQP
    • (1)概念:AMQP,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。Erlang中的实现有RabbitMQ等。
    • (2)官网地址:http://www.amqp.org/
    • (3)总结:AMQP是一套公开的消息队列协议,最早在2003年被提出,它旨在从协议层定义消息通信数据的标准格式,为的就是解决MQ市场上协议不统一的问题,RabbitMQ就是遵循AMQP标准协议开发的MQ服务
  2. JMS
    • (1)概念:JMS即Java消息服务(Java Message Service)应用程序接口,是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。Java消息服务是一个与具体平台无关的API,绝大多数MOM提供商都对JMS提供支持。
    • (2)官网:https://www.oracle.com/technetwork/java/jms/index.html
    • (3)总结: JMS是java提供的一套消息服务API标准,其目的是为所有的java应用程序提供统一的消息通信的标准,类似java的jdbc,只要遵循jms标准的应用程序之间都可以进行消息通信。它和AMQP有什么不同,jms是java语言专属的消息服务标准,它是在api层定义标准,并且只能用于java应用;而AMQP是在协议层定义的标准,是跨语言的 。

1.2 安装

1.2.1 工作原理

组成部分说明如下:

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

消息发送流程:

  1. 生产者和Broker建立TCP连接。
  2. 生产者和Broker建立通道。
  3. 生产者通过通道消息发送给Broker,由Exchange将消息进行转发。
  4. Exchange将消息转发到指定的Queue(队列)

消息接收流程:

  1. 消费者和Broker建立TCP连接
  2. 消费者和Broker建立通道
  3. 消费者监听指定的Queue(队列)
  4. 当有消息到达Queue时Broker默认将消息推送给消费者。
  5. 消费者接收到消息。

1.2.2 安装

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

RabbitMQ的下载地址:http://www.rabbitmq.com/download.html

我这里使用Erlang/OTP 20.3版本和RabbitMQ3.7.4版本。

  1. 安装erlang

    下载地址:https://www.erlang.org/downloads

    我这里下载了OTP 20.3 Windows 64位二进制文​​件 (99142192)-->otp_win64_20.3.exe.运行此文件进行安装。

    安装完以后配置环境变量!

  2. 安装RabbitMQ

    下载地址:https://github.com/rabbitmq/rabbitmq-server/releases/tag/v3.7.4

    下载rabbitmq-server-3.7.4.exe

  3. 启动

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

    • (启动方式1)从开始菜单启动RabbitMQ完成在开始菜单找到RabbitMQ的菜单。或者去任务管理器的服务那里手动搞一下!

    • (启动方式2)进入安装目录下sbin目录手动启动:rabbitmq-service.bat install 安装服务 rabbitmq-service.bat stop 停止服务 rabbitmq-service.bat start 启动服务

  4. 安装管理插件

    安装rabbitMQ的管理插件,方便在浏览器端管理RabbitMQ

    管理员身份运行 rabbitmq-plugins.bat enable rabbitmq_management

    重新启动RabbitMQ服务

  5. 浏览器访问测试

    进入浏览器,访问http://localhost:15672

    初始账号和密码:guest/guest

注意:

  1. 安装erlang和rabbitMQ以管理员身份运行。
  2. 当卸载重新安装时会出现RabbitMQ服务注册失败,此时需要进入注册表清理erlang。搜索RabbitMQ、ErlSrv,将对应的项全部删除。

2. Java操作RabbitMQ

2.1 RabbitMQ的消息模型

RabbitMQ提供了6种消息模型,但是第6种其实是RPC,并不是MQ!那么也就剩下5种。

3、4、5这三种都属于订阅模型,只不过进行路由的方式不同。

消息模型如下图所示:
[图片上传失败...(image-b28b51-1569803506043)]

2.2 搭建环境

  1. 创建maven工程!

    我这里取名RabbitMQTest

  2. 导包

        
            com.rabbitmq
            amqp-client
            
            5.4.1
        
  1. 工具类
package cn.wangningbo.util;

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

public class ConnectionUtil {
    /**
     * 建立与RabbitMQ的连接
     *
     * @return
     * @throws Exception
     */
    public static Connection getConnection() throws Exception {
        //定义连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置服务地址
        factory.setHost("127.0.0.1");
        //端口
        factory.setPort(5672);
        //设置账号信息,用户名、密码、vhost
        factory.setVirtualHost("/");
        factory.setUsername("guest");
        factory.setPassword("guest");
        // 通过工程获取连接
        Connection connection = factory.newConnection();
        return connection;
    }
}

注意:我下面为了测试方便,就没有处理异常都抛出去了!

2.2.1 "Hello World!"------基本消息模型

2.2.1.1 生产者

package cn.wangningbo._01helloworld;

import cn.wangningbo.util.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

public class Producer {
    //队列名称
    private static final String QUEUE = "helloworld";

    //使用main方法进行测试
    public static void main(String[] args) throws Exception {
        //获取连接
        Connection connection = ConnectionUtil.getConnection();
        //创建与Exchange的通道,每个连接可以创建多个通道,每个通道代表一个会话任务
        Channel channel = connection.createChannel();
        /**
         * 声明队列,如果Rabbit中没有此队列将自动创建
         * param1:队列名称
         * param2:是否持久化
         * param3:队列是否独占此连接
         * param4:队列不再使用时是否自动删除此队列
         * param5:队列参数
         */
        channel.queueDeclare(QUEUE, true, false, false, null);
        // 准备一条消息 // 字符串+当前系统时间的时间戳
        String message = "helloworld小明" + System.currentTimeMillis();
        /**
         * 消息发布方法
         * param1:Exchange的名称,如果没有指定,则使用Default Exchange
         * param2:routingKey,消息的路由Key,是用于Exchange(交换机)将消息转发到指定的消息队列
         * param3:消息包含的属性
         * param4:消息体
         */
        /**
         * 这里没有指定交换机,消息将发送给默认交换机,每个队列也会绑定那个默认的交换机,但是不能显
         示绑定或解除绑定
         * 默认的交换机,routingKey等于队列名称
         */
        channel.basicPublish("", QUEUE, null, message.getBytes());
        System.out.println("Send Message is:'" + message + "'");
    }
}

2.2.1.2 消费者

package cn.wangningbo._01helloworld;

import cn.wangningbo.util.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;

public class Consumer {
    //队列名称 //消费哪个生产者队列的消息
    private static final String QUEUE = "helloworld";

    public static void main(String[] args) throws Exception {
        //获取连接
        Connection connection = ConnectionUtil.getConnection();
        //创建与Exchange的通道,每个连接可以创建多个通道,每个通道代表一个会话任务
        Channel channel = connection.createChannel();
        //声明队列
        channel.queueDeclare(QUEUE, true, false, false, null);
        //定义消费方法
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            /**
             * 消费者接收消息调用此方法
             * @param consumerTag 消费者的标签,在channel.basicConsume()去指定
             * @param envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重传标志
            (收到消息失败后是否需要重新发送)
             * @param properties
             * @param body
             */
            @Override
            public void handleDelivery(String consumerTag,
                                       Envelope envelope,
                                       AMQP.BasicProperties properties,
                                       byte[] body)
                    throws IOException {
                //交换机
                String exchange = envelope.getExchange();
                //路由key
                String routingKey = envelope.getRoutingKey();
                //消息id
                long deliveryTag = envelope.getDeliveryTag();
                //消息内容
                String msg = new String(body, "utf8");
                System.out.println("receive message.." + msg);
            }
        };
        /**
         * 监听队列String queue, boolean autoAck,Consumer callback
         * 参数明细
         * 1、队列名称
         * 2、是否自动回复,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置
         为false则需要手动回复
         * 3、消费消息的方法,消费者接收到消息后调用此方法
         */
        channel.basicConsume(QUEUE, true, consumer);
    }
}

2.2.1.3 简单测试

工作流程简述:

  1. 先启动生产者,这时候会有一个消息被丢到默认的交换机里面,这个交换机会把消息丢到指定的队列里面,等待被消费者消费掉!
  2. 再启动消费者,这时候发现消费者消费掉了刚才生产者发送的那条消息,由于消费者设置了自动回复,所以队列里的那个消息就被消费掉了,队里的消息被删除了!但是程序没有停止,一直在监听队列中是否有新的消息。一旦有新的消息进入队列,就会立即打印!

2.2.3.4 消息确认机制(ACK)

通过刚才的案例可以看出,消息一旦被消费者接收,队列中的消息就会被删除。

那么问题来了:RabbitMQ怎么知道消息被接收了呢?

如果消费者领取消息后,还没执行操作就挂掉了呢?或者抛出了异常?消息消费失败,但是RabbitMQ无从得知,这样消息就丢失了!

因此,RabbitMQ有一个ACK机制。当消费者获取消息后,会向RabbitMQ发送回执ACK,告知消息已经被接收。不过这种回执ACK分两种情况:

  1. 自动ACK:消息一旦被接收,消费者自动发送ACK
  2. 手动ACK:消息接收后,不会发送ACK,需要手动调用

只有发送了回执ACK,队列的消息就会被删除掉!

但是哪种更好呢?

这还需要看消息的重要性!

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

我上面的测试都是自动ACK的,如果要手动ACK,需要改动消费者的代码!

2.2.1.4 自动确认存在的问题

修改消费者里面的这个方法,让它1/0发生异常!

            /**
             * 消费者接收消息调用此方法
             * @param consumerTag 消费者的标签,在channel.basicConsume()去指定
             * @param envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重传标志
            (收到消息失败后是否需要重新发送)
             * @param properties
             * @param body
             */
            @Override
            public void handleDelivery(String consumerTag,
                                       Envelope envelope,
                                       AMQP.BasicProperties properties,
                                       byte[] body)
                    throws IOException {
                int i = 1 / 0;
                //交换机
                String exchange = envelope.getExchange();
                //路由key
                String routingKey = envelope.getRoutingKey();
                //消息id
                long deliveryTag = envelope.getDeliveryTag();
                //消息内容
                String msg = new String(body, "utf8");
                System.out.println("receive message.." + msg);
            }
        };

生产者不做任何修改,直接运行,消息发送成功!

运行消费者,程序抛出异常。但是消息依然被消费!

这时候就存在问题,这个消息对我很重要,不允许丢失,这时候就需要消费者没报错的时候才回执ACK!这就需要手动回执了!

2.2.1.5 手动确认实现

修改消费者,把自动改成手动(去掉之前制造的异常)

        /**
         * 监听队列String queue, boolean autoAck,Consumer callback
         * 参数明细
         * 1、队列名称
         * 2、是否自动回复,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置
         为false则需要手动回复
         * 3、消费消息的方法,消费者接收到消息后调用此方法
         */
        channel.basicConsume(QUEUE, false, consumer);

生产者不变,再次运行!

运行消费者!消费者没有异常,并且也获取了消息!但是通过可视化界面看到队列中依然存在那个消息!

这是因为虽然我们设置了手动ACK,但是代码中并没有进行消息确认!所以消息并未被真正消费掉。

当我们关掉这个消费者,消息的状态再次称为Ready!

手动ACK设置:channel.basicAck(envelope.getDeliveryTag(), false);当消费完以后才发送回执ACK!

        //定义消费方法
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            /**
             * 消费者接收消息调用此方法
             * @param consumerTag 消费者的标签,在channel.basicConsume()去指定
             * @param envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重传标志
            (收到消息失败后是否需要重新发送)
             * @param properties
             * @param body
             */
            @Override
            public void handleDelivery(String consumerTag,
                                       Envelope envelope,
                                       AMQP.BasicProperties properties,
                                       byte[] body)
                    throws IOException {
//                int i = 1 / 0;
                //交换机
                String exchange = envelope.getExchange();
                //路由key
                String routingKey = envelope.getRoutingKey();
                //消息id
                long deliveryTag = envelope.getDeliveryTag();
                //消息内容
                String msg = new String(body, "utf8");
                System.out.println("receive message.." + msg);
                //手动进行ACK
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };

2.2.1.6 小结

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

2.2.2 Work queues

work queues与入门程序相比,多了一个消费端,两个消费端共同消费同一个队列中的消息。

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

测试:

  1. 一个生产者,多个消费者
  2. 生产者发送多个消息!

结果:

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

工作队列,又称任务队列。主要思想就是避免执行资源密集型任务时,必须等待它执行完成。相反我们稍后完成任务,我们将任务封装为消息并将其发送到队列。 在后台运行的工作进程将获取任务并最终执行作业。当你运行许多工人时,任务将在他们之间共享,但是一个消息只能被一个消费者获取。

模拟流程:

  • P:生产者:任务的发布者
  • C1:消费者,领取任务并且完成任务,假设完成速度较快
  • C2:消费者2:领取任务并完成任务,假设完成速度慢

2.2.2.1 生产者

对生产者做一点的小小加工,让它发送50个消息到队列,而且是发送的别太快!

加工部分

        //生产者发送50个消息
        for (int i = 1; i <= 50; i++) {
            Thread.sleep(i * 2);
            channel.basicPublish("", QUEUE, null, message.getBytes());
            System.out.println(i + "Send Message is:'" + message + "'");
        }

完整生产者

package cn.wangningbo._02workqueues;

import cn.wangningbo.util.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

public class Producer {
    //队列名称
    private static final String QUEUE = "workqueues";

    //使用main方法进行测试
    public static void main(String[] args) throws Exception {
        //获取连接
        Connection connection = ConnectionUtil.getConnection();
        //创建与Exchange的通道,每个连接可以创建多个通道,每个通道代表一个会话任务
        Channel channel = connection.createChannel();
        /**
         * 声明队列,如果Rabbit中没有此队列将自动创建
         * param1:队列名称
         * param2:是否持久化
         * param3:队列是否独占此连接
         * param4:队列不再使用时是否自动删除此队列
         * param5:队列参数
         */
        channel.queueDeclare(QUEUE, true, false, false, null);
        // 准备一条消息 // 字符串+当前系统时间的时间戳
        String message = "helloworld小明" + System.currentTimeMillis();
        /**
         * 消息发布方法
         * param1:Exchange的名称,如果没有指定,则使用Default Exchange
         * param2:routingKey,消息的路由Key,是用于Exchange(交换机)将消息转发到指定的消息队列
         * param3:消息包含的属性
         * param4:消息体
         */
        /**
         * 这里没有指定交换机,消息将发送给默认交换机,每个队列也会绑定那个默认的交换机,但是不能显
         示绑定或解除绑定
         * 默认的交换机,routingKey等于队列名称
         */
        //生产者发送50个消息
        for (int i = 1; i <= 50; i++) {
            Thread.sleep(i * 2);
            channel.basicPublish("", QUEUE, null, message.getBytes());
            System.out.println(i + "Send Message is:'" + message + "'");
        }
    }
}

2.2.2.2 消费者1

让消费者1处理的能力强

消费者1不需要做什么改变!正常消费即可!只是多个计数器统计消费了多少条消息!

package cn.wangningbo._02workqueues;

import cn.wangningbo.util.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;

public class Consumer {
    //队列名称 //消费哪个生产者队列的消息
    private static final String QUEUE = "workqueues";

    public static void main(String[] args) throws Exception {
        //获取连接
        Connection connection = ConnectionUtil.getConnection();
        //创建与Exchange的通道,每个连接可以创建多个通道,每个通道代表一个会话任务
        Channel channel = connection.createChannel();
        //声明队列
        channel.queueDeclare(QUEUE, true, false, false, null);
        //定义消费方法
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            //计数器
            int index = 1;
            /**
             * 消费者接收消息调用此方法
             * @param consumerTag 消费者的标签,在channel.basicConsume()去指定
             * @param envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重传标志
            (收到消息失败后是否需要重新发送)
             * @param properties
             * @param body
             */
            @Override
            public void handleDelivery(String consumerTag,
                                       Envelope envelope,
                                       AMQP.BasicProperties properties,
                                       byte[] body)
                    throws IOException {
                //交换机
                String exchange = envelope.getExchange();
                //路由key
                String routingKey = envelope.getRoutingKey();
                //消息id
                long deliveryTag = envelope.getDeliveryTag();
                //消息内容
                String msg = new String(body, "utf8");
                //index用来统计消费了多少条消息
                System.out.println(index+++"receive message.." + msg);
                //手动进行ACK
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        /**
         * 监听队列String queue, boolean autoAck,Consumer callback
         * 参数明细
         * 1、队列名称
         * 2、是否自动回复,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置
         为false则需要手动回复
         * 3、消费消息的方法,消费者接收到消息后调用此方法
         */
        channel.basicConsume(QUEUE, false, consumer);
    }
}

2.2.2.3 消费者2

让消费者2处理的能力弱!这里模拟消费耗时!

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

消费者2完整代码

package cn.wangningbo._02workqueues;

import cn.wangningbo.util.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;

public class Consumer2 {
    //队列名称 //消费哪个生产者队列的消息
    private static final String QUEUE = "workqueues";

    public static void main(String[] args) throws Exception {
        //获取连接
        Connection connection = ConnectionUtil.getConnection();
        //创建与Exchange的通道,每个连接可以创建多个通道,每个通道代表一个会话任务
        Channel channel = connection.createChannel();
        //声明队列
        channel.queueDeclare(QUEUE, true, false, false, null);
        //定义消费方法
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            //计数器
            int index = 1;
            /**
             * 消费者接收消息调用此方法
             * @param consumerTag 消费者的标签,在channel.basicConsume()去指定
             * @param envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重传标志
            (收到消息失败后是否需要重新发送)
             * @param properties
             * @param body
             */
            @Override
            public void handleDelivery(String consumerTag,
                                       Envelope envelope,
                                       AMQP.BasicProperties properties,
                                       byte[] body)
                    throws IOException {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //交换机
                String exchange = envelope.getExchange();
                //路由key
                String routingKey = envelope.getRoutingKey();
                //消息id
                long deliveryTag = envelope.getDeliveryTag();
                //消息内容
                String msg = new String(body, "utf8");
                //index用来统计消费了多少条消息
                System.out.println(index+++"receive message.." + msg);
                //手动进行ACK
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        /**
         * 监听队列String queue, boolean autoAck,Consumer callback
         * 参数明细
         * 1、队列名称
         * 2、是否自动回复,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置
         为false则需要手动回复
         * 3、消费消息的方法,消费者接收到消息后调用此方法
         */
        channel.basicConsume(QUEUE, false, consumer);
    }
}

2.2.2.4 测试

先启动两个消费者,再启动生产者!

测试结果:

  1. 可以发现两个消费者各自消费了25条消息,而且各不相同,这就实现了任务分发!
  2. 而消费者1能力比较强,很快就处理完了25条消息,然后处于空闲的监听状态。
  3. 而消费者2的能力比较弱,一直在缓慢的处理,一条一条的,直到25条处理完毕,然后处于空闲监听状态!

2.2.2.5 能者多劳

刚才产生的问题?

  1. 消费者1比消费者2的效率要高,一次任务的耗时较短
  2. 然而两人最终消费的消息数量是一样的
  3. 消费者1大量时间处于空闲状态,消费者2一直忙碌

现在的状态属于是把任务平均分配,正确的做法应该是消费越快的人,消费的越多。

我希望是能者多劳,效率高的多消费一些,效率低的少消费一些,这样把所有消息消费完所需要的时间最短!

实现方法:
使用basicQos方法和prefetchCount = 1设置。 这告诉RabbitMQ一次不要向工作人员发送多于一条消息。 或者换句话说,不要向工作人员发送新消息,直到它处理并确认了前一个消息。相反,它会将其分派给不是仍然忙碌的下一个工作人员。

在两个消费者那里分别设置

//声明一次发1条消息,等到处理完了再发
            channel.basicQos(1);

2.2.2.6 再次测试

生产者发送了50条消息,消费者1能力较强,消费了32条消息,消费者2能力较弱,消费了18条!

实现了能者多劳!

2.2.3 订阅模型分类

2.2.3.1 概述

在之前的模式中,我们创建了一个工作队列。 工作队列背后的假设是:每个任务只被传递给一个工作人员。 在这一部分,我们将做一些完全不同的事情 - 我们将会传递一个信息给多个消费者。 这种模式被称为“发布/订阅”。

  1. 1个生产者,多个消费者
  2. 每一个消费者都有自己的一个队列
  3. 生产者没有将消息直接发送到队列,而是发送到了交换机
  4. 每个队列都要绑定到交换机
  5. 生产者发送的消息,经过交换机到达队列,实现一个消息被多个消费者获取的目的

X(Exchanges):交换机一方面:接收生产者发送的消息。另一方面:知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。

2.2.3.2 分类

Exchange类型有以下几种:

  1. Fanout:广播,将消息交给所有绑定到交换机的队列 all
  2. Direct:定向,把消息交给符合指定routing key 的队列 一堆或一个
  3. Topic:通配符,把消息交给符合routing pattern(路由模式)的队列 一堆或者一个

Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!

2.2.4 订阅模型-FANOUT

2.2.4.1 工作模式

发布订阅模式:

在广播模式下,消息发送流程是这样的:

  • 1) 可以有多个消费者
  • 2) 每个消费者有自己的queue(队列)
  • 3) 每个队列都要绑定到Exchange(交换机)
  • 4) 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定。
  • 5) 交换机把消息发送给绑定过的所有队列
  • 6) 队列的消费者都能拿到消息。实现一条消息被多个消费者消费

2.2.4.2 生产者

与上面那种非订阅模式有两种变化:

  1. 声明Exchange,不再声明Queue
  2. 发送消息到Exchange,不再发送到Queue
package cn.wangningbo._03fanout;

import cn.wangningbo.util.ConnectionUtil;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

public class Producer {
    //交换机名称
    private final static String EXCHANGE_NAME = "fanout_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明exchange的类型,指定类型为fanout
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
        // 消息内容
        String message = "Hello everyone";
        // 发布消息到Exchange
        channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
        System.out.println(" [生产者] Sent '" + message + "'");
        //关闭连接
        channel.close();
        connection.close();
    }
}

2.2.4.3 消费者

消费者1

package cn.wangningbo._03fanout;

import cn.wangningbo.util.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;

public class Consumer {
    // 队列名称
    private final static String QUEUE_NAME = "fanout_exchange_queue_1";
    //交换机名称
    private final static String EXCHANGE_NAME = "fanout_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 绑定队列到交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [消费者1] received : " + msg + "!");
            }
        };
        // 监听队列,自动返回完成
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

消费者2

package cn.wangningbo._03fanout;

import cn.wangningbo.util.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;

public class Consumer2 {
    // 队列名称
    private final static String QUEUE_NAME = "fanout_exchange_queue_2";
    //交换机名称
    private final static String EXCHANGE_NAME = "fanout_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 绑定队列到交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [消费者2] received : " + msg + "!");
            }
        };
        // 监听队列,手动返回完成
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

2.2.4.4 测试

先启动一下生产者,来创建交换机。再启动消费者1和消费者2。这时候再启动一次生产者,可以看到两个消费者都收到了消息!

因为生产者把消息丢给了交换机,而交换机又制定了交换机类型是fanout类型,两个消费者的队列都绑定到了生产者的交换机上面!所以两个消费者都收到了同名交换机发出的消息!

应用场景:

  1. 注册成功后发送短信和邮件
  2. 消息推送

2.2.5 订阅模型-Direct

2.2.5.1 工作模式

有选择性的接收消息

在订阅模式中,生产者发布消息,所有消费者都可以获取所有消息。

在路由模式中,我们将添加一个功能,我们将只能订阅一部分消息。 例如,我们只能将重要的错误消息引导到日志文件(以节省磁盘空间),同时仍然能够在控制台上打印所有日志消息。

但是,在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。

在Direct模型下,队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)

消息的发送方在向Exchange发送消息时,也必须指定消息的routing key。如下:

  1. P:生产者,向Exchange发送消息,发送消息时,会指定一个routing key。
  2. X:Exchange(交换机),接收生产者的消息,然后把消息递交给 与routing key完全匹配的队列
  3. C1:消费者,其所在队列指定了需要routing key 为 error 的消息
  4. C2:消费者,其所在队列指定了需要routing key 为 info、error、warning 的消息

2.2.5.2 生产者

此处模拟商品的增删改,发送消息的RoutingKey分别是:insert、update、delete

生产者

package cn.wangningbo._04direct;

import cn.wangningbo.util.ConnectionUtil;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

public class Producer {
    //交换机名称
    private final static String EXCHANGE_NAME = "direct_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明exchange,指定类型为direct
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        // 消息内容
//        String message = "商品新增了, id = 1001";
//        String message = "商品修改了, id = 1001";
        String message = "商品删除了, id = 1001";
        // 发送消息,并且指定routing key 为:insert ,代表新增商品
        channel.basicPublish(EXCHANGE_NAME, "delete", null, message.getBytes());
        System.out.println(" [商品服务:] Sent '" + message + "'");
        // 关闭
        channel.close();
        connection.close();
    }
}

2.2.5.3 消费者

消费者1:处假设消费者1只接收两种类型的消息:更新商品和删除商品

package cn.wangningbo._04direct;

import cn.wangningbo.util.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;

public class Consumer {
    // 队列名称
    private final static String QUEUE_NAME = "direct_exchange_queue_1";
    //交换机名称
    private final static String EXCHANGE_NAME = "direct_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 绑定队列到交换机,同时指定需要订阅的routing key。假设此处需要update和delete消息
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "update");
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "delete");
        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [消费者1] received : " + msg + "!");
            }
        };
        // 监听队列,自动ACK
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

消费者2:此处假设消费者2接收所有类型的消息:新增商品,更新商品和删除商品。

package cn.wangningbo._04direct;

import cn.wangningbo.util.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;

public class Consumer2 {
    // 队列名称
    private final static String QUEUE_NAME = "direct_exchange_queue_2";
    // 交换机名称
    private final static String EXCHANGE_NAME = "direct_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 绑定队列到交换机,同时指定需要订阅的routing key。订阅 insert、update、delete
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "insert");
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "update");
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "delete");

        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [消费者2] received : " + msg + "!");
            }
        };
        // 监听队列,自动ACK
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

2.2.5.4 测试

分别发送增、删、改的RoutingKey。结果:消费者1只可以收到修改和删除的消息,消费者2都可以收到!

我们项目中就要用,特定站点静态页面,只能发到特定站点服务器

sendMsg routingKey:sitesn   rev:绑定到交换机要以sitesn来作为routingkey

2.2.6 订阅模型-Topics

2.2.6.1 工作模式

Topic类型的Exchange与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routingkey的时候使用通配符

Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: goods.insert

通配符规则:

  • :匹配一个或多个词

  • *:匹配不多不少恰好1个词

举例:

  • audit.#:能够匹配audit.irs.corporate 或者 audit.irs
  • audit.*:只能匹配audit.irs

路由模式:

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

2.2.6.2 生产者

package cn.wangningbo._05topics;

import cn.wangningbo.util.ConnectionUtil;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

public class Producer {
    // 交换机名称
    private final static String EXCHANGE_NAME = "topic_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明exchange,指定类型为topic
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
        // 消息内容
        String message = "新增商品 : id = 1001";
//        String message = "修改商品 : id = 1001";
//        String message = "删除商品 : id = 1001";
        // 发送消息,并且指定routing key 为:insert ,代表新增商品
        channel.basicPublish(EXCHANGE_NAME, "item.insert", null, message.getBytes());
        System.out.println(" [商品服务:] Sent '" + message + "'");
        // 关闭连接
        channel.close();
        connection.close();
    }
}

2.2.6.3 消费者

消费者1:此处假设消费者1只接收两种类型的消息:更新商品和删除商品

package cn.wangningbo._05topics;

import cn.wangningbo.util.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;

public class Consumer {
    // 队列名称
    private final static String QUEUE_NAME = "topic_exchange_queue_1";
    // 交换机名称
    private final static String EXCHANGE_NAME = "topic_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 绑定队列到交换机,同时指定需要订阅的routing key。需要 update、delete
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "item.update");
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "item.delete");

        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [消费者1] received : " + msg + "!");
            }
        };
        // 监听队列,自动ACK
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

消费者2:此处假设消费者2接收所有类型的消息:新增商品,更新商品和删除商品。

package cn.wangningbo._05topics;

import cn.wangningbo.util.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;

public class Consumer2 {
    // 队列名称
    private final static String QUEUE_NAME = "topic_exchange_queue_2";
    // 交换机名称
    private final static String EXCHANGE_NAME = "topic_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 绑定队列到交换机,同时指定需要订阅的routing key。订阅 insert、update、delete
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "item.*");

        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [消费者2] received : " + msg + "!");
            }
        };
        // 监听队列,自动ACK
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

2.2.6.4 测试

测试:先启动生产者,再启动两个消费者,再启动生产者!分别发送增、删、改的RoutingKey。

测试结果:结果:消费者1只可以收到修改和删除的消息,消费者2都可以收到!消费者使用的是通配符接收!

2.2.7 Header模式

header模式与routing不同的地方在于,header模式取消routingkey,使用header中的 key/value(键值对)匹配 队列。

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

  1. 生产者

队列与交换机绑定的代码与之前不同,如下:

Map headers_email = new Hashtable(); 
headers_email.put("inform_type", "email"); 
Map headers_sms = new Hashtable(); 
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 headers = new Hashtable(); 
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 headers_email = new Hashtable(); 
headers_email.put("inform_email", "email"); 
//交换机和队列绑定 
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_HEADERS_INFORM,"",headers_email); 
//指定消费队列 
channel.basicConsume(QUEUE_INFORM_EMAIL, true, consumer); 

============实战演示================

生产者:只发送给"inform_type", "email"

package cn.wangningbo._06header;

import cn.wangningbo.util.ConnectionUtil;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

import java.util.Hashtable;
import java.util.Map;

public class Producer {
    // 交换机名称
    private final static String EXCHANGE_NAME = "headers_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明exchange,指定类型为headers
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.HEADERS);
        // 消息内容
        String message = "发送了一个email消息";
        Map headers = new Hashtable();
        headers.put("inform_type", "email");//匹配email通知消费者绑定的header
//        headers.put("inform_type2", "sms");//匹配sms通知消费者绑定的header
        AMQP.BasicProperties.Builder properties = new AMQP.BasicProperties.Builder();
        properties.headers(headers);
        // 发送消息,并绑定到指定的交换机,并指定发送到指定类型
        channel.basicPublish(EXCHANGE_NAME, "", properties.build(), message.getBytes());
        System.out.println(" [商品服务:] Sent '" + message + "'");
        // 关闭
        channel.close();
        connection.close();
    }
}

消费者1:只接收"inform_type", "email"

package cn.wangningbo._06header;

import cn.wangningbo.util.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.Hashtable;
import java.util.Map;

public class Consumer {
    // 队列名称
    private final static String QUEUE_NAME = "headers_exchange_queue_1";
    // 交换机名称
    private final static String EXCHANGE_NAME = "headers_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        Map headersEmail = new Hashtable();
        // 指定接受的类型
        headersEmail.put("inform_type", "email");
//        headersEmail.put("inform_type2", "sms");
        // 绑定队列到交换机,同时指定需要接收消息的类型
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "", headersEmail);

        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [消费者1] received : " + msg + "!");
            }
        };
        // 监听队列,自动ACK
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

消费者2:只接收"inform_type2", "sms"

package cn.wangningbo._06header;

import cn.wangningbo.util.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.Hashtable;
import java.util.Map;

public class Consumer2 {
    // 队列名称
    private final static String QUEUE_NAME = "headers_exchange_queue_2";
    // 交换机名称
    private final static String EXCHANGE_NAME = "headers_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        Map headersSms = new Hashtable();
//        headersSms.put("inform_type", "email");
        headersSms.put("inform_type2", "sms");
        // 绑定队列到交换机,同时指定需要订阅的routing key。假设此处需要update和delete消息
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "", headersSms);

        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [消费者2] received : " + msg + "!");
            }
        };
        // 监听队列,自动ACK
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

测试:先启动生产者,再启动2个消费者,再启动生产者!

测试结果:只有消费者1接收到了"inform_type", "email"

2.2.7 RPC

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

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

2.3 持久化-解决数据安全

如何避免消息丢失?

  1. 消费者的ACK机制。可以防止消费者丢失消息。
  2. 但是,如果在消费者消费之前,MQ就宕机了,消息就没了。

是可以将消息进行持久化呢?

要将消息持久化,前提是:队列、Exchange都持久化

2.3.1 交换机持久化

生产者那边指定一下,第三个参数那里true就是持久化

// 声明exchange,指定类型为topic, 是否持久化
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC,true);

2.3.2 队列持久化

在消费者那里指定一下队列持久化

        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 参数1:声明队列,参数2:队列是否持久化
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

2.3.3 消息持久化

生产者发送消息的第三个参数

        // 发送消息,并且指定routing key 为:insert ,代表新增商品,参数3:消息持久化
        channel.basicPublish(EXCHANGE_NAME, "item.insert", MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());

3. SpringBoot整合RabbitMQ

3.1 步骤分析

  1. 创建项目
  2. pom导包
  3. 入口类
  4. application.yml配置
  5. 配置类
  6. 生产者
  7. 消费者
  8. 测试

3.2 步骤实现

3.2.1 创建项目

创建一个maven项目

3.2.2 pom导包


        
            
                org.springframework.boot
                spring-boot-dependencies
                2.0.5.RELEASE
                pom
                import
            
        
    

    
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.springframework.boot
            spring-boot-starter-test
        
        
        
            org.springframework.boot
            spring-boot-starter-amqp
        
    

3.2.3 入口类

package cn.wangningbo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class APP {
    public static void main(String[] args) {
        SpringApplication.run(APP.class, args);
    }
}

3.2.4 application.yml配置

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

3.2.5 配置类

package cn.wangningbo.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";

    /**
     * 交换机配置
     * ExchangeBuilder提供了fanout、direct、topic、header交换机类型的配置
     *
     * @return the exchange
     */
    @Bean(EXCHANGE_TOPICS_INFORM)
    public Exchange EXCHANGE_TOPICS_INFORM() {
        //durable(true)持久化,消息队列重启后交换机仍然存在
        return ExchangeBuilder.topicExchange(EXCHANGE_TOPICS_INFORM).durable(true).build();
    }

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

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

    /**
     * channel.queueBind(INFORM_QUEUE_SMS,"inform_exchange_topic","inform.#.sms.#");
     * 绑定队列到交换机 .
     *
     * @param queue    the queue
     * @param exchange the exchange
     * @return the binding
     */
    @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("inform.#.sms.#").noargs();
    }

    @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("inform.#.email.#").noargs();
    }
}

3.2.6 生产者

使用RarbbitTemplate发送消息

package cn.wangningbo;

import cn.wangningbo.config.RabbitmqConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@SpringBootTest(classes = APP.class)
@RunWith(SpringRunner.class)
public class Producer_topics_springboot {

    @Autowired
    RabbitTemplate rabbitTemplate;

    // 生产者
    @Test
    public void testSendByTopics() {
        // 使用for循环发送5条消息
        for (int i = 0; i < 5; i++) {
            // 准备消息
            String message = "sms email inform to user" + i;
            // 参数1:交换机名称,参数2:使用通配符类型的routingKey,参数3:要发送的消息
            rabbitTemplate.convertAndSend(RabbitmqConfig.EXCHANGE_TOPICS_INFORM, "inform.sms.email", message);
            // 打印发送的消息
            System.out.println("Send Message is:'" + message + "'");
        }
    }
}

3.2.7 消费者

package cn.wangningbo.handler;

import cn.wangningbo.config.RabbitmqConfig;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * 消费者
 */
@Component
public class ReceiveHandler {
    //监听email队列
    @RabbitListener(queues = {RabbitmqConfig.QUEUE_INFORM_EMAIL})
    public void receive_email(String msg, Message message, Channel channel) {
        System.out.println(msg);
    }

    //监听sms队列
    @RabbitListener(queues = {RabbitmqConfig.QUEUE_INFORM_SMS})
    public void receive_sms(String msg, Message message, Channel channel) {
        System.out.println(msg);
    }
} 

3.2.8 测试

启动生产者即可测试!查看消息到了哪里的消费者!

你可能感兴趣的:(RabbitMQ)