• RabbitMQ


    一、介绍

    1、RabbitMQ

    MQ全称为Message Queue,即消息队列,消息队列是在消息的传输过程中保存消息的容器,用于接收消息并以文件的方式存储。

    MQ全称为Message Queue, 消息队列(MQ)是一种应用程序对应用程序的通信方法应用程序通过读写出入队列的消息(针对应用程序的数据)来通信,而无需专用连接来链接它们。消息传递指的是程序之间通过在消息中发送数据进行通信,而不是通过直接调用彼此来通信,直接调用通常是用于诸如远程过程调用的技术。排队指的是应用程序通过 队列来通信。队列的使用除去了接收和发送应用程序同时执行的要求。

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

    RabbitMQ官方地址:http://www.rabbitmq.com/

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

    (1)、任务异步处理。

    将不需要同步处理的并且耗时长的操作由消息队列通知消息接收方进行异步处理。提高了应用程序的响应时间。消息队列本身是异步的,它允许接收者在消息发送很长时间后再取回消息。

    场景说明:用户注册后,需要发注册邮件和注册短信。传统的做法有两种1.串行的方式;2.并行方式

    1)串行方式:将注册信息写入数据库成功后,发送注册邮件,再发送注册短信。以上三个任务全部完成后,返回给客户端

     2)并行方式:将注册信息写入数据库成功后,发送注册邮件的同时,发送注册短信。以上三个任务完成后,返回给客户端。与串行的差别是,并行的方式可以提高处理的时间

    引入消息队列,将不是必须的业务逻辑,异步处理。改造后的架构如下:

    (2)、应用程序解耦合

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

    场景说明:用户下单后,订单系统需要通知库存系统。传统的做法是,订单系统调用库存系统的接口。

    传统模式的缺点:

    (a)、假如库存系统无法访问,则订单减库存将失败,从而导致订单失败

    (b)、订单系统与库存系统耦合

    引入消息队列:

    (a)、订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功。

    (b)、库存系统:订阅下单的消息,采用拉/推的方式,获取下单信息,库存系统根据下单信息,进行库存操作。

    (c)、假如:在下单时库存系统不能正常使用。也不影响正常下单,因为下单后,订单系统写入消息队列就不再关心其他的后续操作了。实现订单系统与库存系统的应用解耦

    (d)、为了保证库存肯定有,可以将队列大小设置成库存数量,或者采用其他方式解决。

    基于消息的模型,关心的是“通知”,而非“处理”。

    消息队列和RPC的区别与比较:

    • RPC: 异步调用,及时获得调用结果,具有强一致性结果,关心业务调用处理结果。

    • 消息队列:两次异步RPC调用,将调用内容在队列中进行转储,并选择合适的时机进行投递(错峰流控)

    (3)、流量削峰

    当上下游系统处理能力存在差距的时候,利用消息队列做一个通用的”载体”,在下游有能力处理的时候,再进行分发与处理。

    流量削峰也是消息队列中的常用场景,一般在秒杀或团抢活动中使用广泛

    应用场景:系统其他时间A系统每秒请求量就100个,系统可以稳定运行。系统每天晚间八点有秒杀活动,每秒并发请求量增至1万条,但是系统最大的处理能力只能每秒处理1000个请求,于是系统崩溃,服务器宕机。

    之前架构:大量用户(100万用户)通过浏览器在晚上八点高峰期同时参与秒杀活动。大量的请求涌入我们的系统中,高峰期达到每秒钟5000个请求,大量的请求打到MySQL上,每秒钟预计执行3000条SQL。但是一般的MySQL每秒钟扛住2000个请求就不错了,如果达到3000个请求的话可能MySQL直接就瘫痪了,从而系统无法被使用。但是高峰期过了之后,就成了低峰期,可能也就1万用户访问系统,每秒的请求数量也就50个左右,整个系统几乎没有任何压力。

    引入MQ:100万用户在高峰期的时候,每秒请求有5000个请求左右,将这5000请求写入MQ里面,系统A每秒最多只能处理2000请求,因为MySQL每秒只能处理2000个请求。系统A从MQ中慢慢拉取请求,每秒就拉取2000个请求,不要超过自己每秒能处理的请求数量即可。MQ,每秒5000个请求进来,结果只有2000个请求出去,所以在秒杀期间(将近一小时)可能会有几十万或者几百万的请求积压在MQ中。

    关于流量削峰:秒杀系统流量削峰这事儿应该怎么做?

    这个短暂的高峰期积压是没问题的,因为高峰期过了之后,每秒就只有50个请求进入MQ了,但是系统还是按照每秒2000个请求的速度在处理,所以说,只要高峰期一过,系统就会快速将积压的消息消费掉。我们在此计算一下,每秒在MQ积压3000条消息,1分钟会积压18万,1小时积压1000万条消息,高峰期过后,1个多小时就可以将积压的1000万消息消费掉。

    (4)、日志处理

    日志处理是指将消息队列用在日志处理中,比如 Kafka 的应用,解决大量日志传输的问题。

    (5)、消息通讯

    消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯,比如实现点对点消息队列,或者聊天室等。

    (6)、消息广播

    如果没有消息队列,每当一个新的业务方接入,我们都要接入一次新接口。有了消息队列,我们只需要关心消息是否送达了队列,至于谁希望订阅,是下游的事情,无疑极大地减少了开发和联调的工作量。

    市场上还有哪些消息队列?

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

    2、为什么使用RabbitMQ呢?

    (1)、使得简单,功能强大。

    (2)、基于AMQP协议。

    (3)、社区活跃,文档完善。

    (4)、高并发性能好,这主要得益于Erlang语言。

    (5)、Spring Boot默认已集成RabbitMQ

    3、引入消息队列的优缺点

    优点:优点就是以上的那些场景应用,就是在特殊场景下有其对应的好处,解耦、异步、削峰。

    缺点:

    1)、系统的可用性降低

    系统引入的外部依赖越多,系统越容易挂掉,本来只是A系统调用BCD三个系统接口就好,ABCD四个系统不报错整个系统会正常运行。引入了MQ之后,虽然ABCD系统没出错,但MQ挂了以后,整个系统也会崩溃。

    2)、系统的复杂性提高

    引入了MQ之后,需要考虑的问题也变得多了,如何保证消息没有重复消费?如何保证消息不丢失?怎么保证消息传递的顺序

    3)、一致性问题

    A系统发送完消息直接返回成功,但是BCD系统之中若有系统写库失败,则会产生数据不一致的问题。

    总结:所以总结来说,消息队列是一种十分复杂的架构,引入它有很多好处,但是也得针对它带来的坏处做各种额外的技术方案和架构来规避。引入MQ系统复杂度提升了一个数量级,但是在有些场景下,就是复杂十倍百倍,还是需要使用MQ。

    4、其它相关知识

    AMQP是什么 ?

    总结: AMQP是一套公开的消息队列协议,最早在2003年被提出,它旨在从协议层定义消息通信数据的标准格式,为的就是解决MQ市场上协议不统一的问题。RabbitMQ就是遵循AMQP标准协议开发的MQ服务。

    JMS是什么 ?

     总结:

    JMS是java提供的一套消息服务API标准,其目的是为所有的java应用程序提供统一的消息通信的标准,类似java的jdbc,只要遵循jms标准的应用程序之间都可以进行消息通信。它和AMQP有什么 不同,jms是java语言专属的消息服务标准,它是在api层定义标准,并且只能用于java应用;而AMQP是在协议层定义的标准,是跨语言的 。

    二、快速入门

    1、RabbitMQ 的工作原理

    下图是RabbitMQ的基本结构:

    broker:中间人。

    组成部分说明如下:

    Broker :消息队列服务进程,此进程包括两个部分:Exchange和Queue。

    Exchange :消息队列交换机,按一定的规则将消息路由转发到某个队列,对消息进行过虑。

    Queue :消息队列,存储消息的队列,消息到达队列并转发给指定的消费方。

    Producer :消息生产者,即生产方客户端,生产方客户端将消息发送到MQ。

    Consumer :消息消费者,即消费方客户端,接收MQ转发的消息。

    消息发布接收流程:

    -----发送消息-----

    (1)、生产者和Broker建立TCP连接。

    (2)、生产者和Broker建立通道。

    (3)、生产者通过通道将消息发送给Broker,由Exchange将消息进行转发

    (4)、Exchange将消息转发到指定的Queue(队列)

    ----接收消息-----

    (1)、消费者和Broker建立TCP连接

    (2)、消费者和Broker建立通道

    (3)、消费者监听指定的Queue(队列)

    (4)、当有消息到达Queue时Broker默认将消息推送给消费者。

    (5)、消费者接收到消息。

    2、启动rabbitmq

    从开始菜单安装并启动RabbitMQ。由于已经安装管理插件,浏览器直接访问:http://localhost:15672

    注意:15672为web管理端的端口。而生产者通过5672与消费者进行通信。

    3、入门程序HelloWorld

    生产者和消费者都属于客户端,我们先用 rabbitMQ官方提供的java client测试,目的是对RabbitMQ的交互过程有个清晰的认识。

    RabbitMQ给我们提供了客户端,我们只需要引入依赖就可以了。创建生产者工程和消费者工程,分别加入RabbitMQ java client的依赖。

    <dependency>
        <groupId>com.rabbitmq</groupId>
        <artifactId>amqp‐client</artifactId>
        <version>4.0.3</version><!‐‐此版本与spring boot 1.5.9版本匹配‐‐>
    </dependency>

    1)搭建父工程、rabbitmq-producer生产者子模块、rabbitmq-consumer消费者子模块,

    参考https://www.cnblogs.com/zwh0910/p/15484263.html

    如下所示:

    生产者工程和消费者模块,分别加入RabbitMQ java client的依赖。

    <dependencies>
            <dependency>
                <groupId>com.rabbitmq</groupId>
                <artifactId>amqp-client</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-logging</artifactId>
            </dependency>
        </dependencies>

    2)、消息生产者

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

    import com.rabbitmq.client.Channel;
    import com.rabbitmq.client.Connection;
    import com.rabbitmq.client.ConnectionFactory;
    
    import java.io.IOException;
    import java.util.concurrent.TimeoutException;
    
    /**
     * rabbitmq的入门程序
     *
     * @author Administrator
     * @version 1.0
     * @create 2018-06-17 9:05
     **/
    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();
                //声明队列,如果队列在mq 中没有则要创建
                //参数:String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
                /**
                 * 参数明细
                 * 1、queue 队列名称
                 * 2、durable 是否持久化,如果持久化,mq重启后队列还在
                 * 3、exclusive 是否独占连接,队列只允许在该连接中访问,其他连接不允许访问。如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
                 * 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
                 * 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 "+message);
            } 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();
                }
            }
    
    
        }
    }

    rabbitmq默认虚拟机名称为“/”,虚拟机相当于一个独立的mq服务。一个mq服务可以设置多个虚拟机,安装一个mq就可以实现多个虚拟的mq,即不用安装多个mq了,设置多个虚拟机就可以模拟多个mq。

    一个链接可以创建多个会话通道。生产者和mq服务所有通信都在channel通道中完成。

    运行main方法,会生成一个helloWorld队列

    点击helloworld,进入如下页面

     ready为1表示待发的消息为1条。

    3)、消息消费者

    在消费者工程下的test中创建测试类如下:

    import com.rabbitmq.client.*;
    
    import java.io.IOException;
    import java.util.concurrent.TimeoutException;
    
    /**
     * 入门程序消费者
     * @author Administrator
     * @version 1.0
     * @create 2018-06-17 9:25
     **/
    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();
    
            //监听队列
            //声明队列,如果队列在mq 中没有则要创建
            //参数:String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
            /**
             * 参数明细
             * 1、queue 队列名称
             * 2、durable 是否持久化,如果持久化,mq重启后队列还在
             * 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
             * 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
             * 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消息已接收,如果将此参数设置为tru表示会自动回复mq,如果设置为false要通过编程实现回复,如果不回复,消息一直存在
             * 3、callback,消费方法,当消费者接收到消息要执行的方法
             */
            channel.basicConsume(QUEUE,true,defaultConsumer);
    
        }
    }

    消息生产者和消息消费者都要声明队列,队列的名称要相同。如果消费方先启动,如果不声明队列就会报错,所以消费方要声明队列。

    消费者要保持连接,监听队列,所以不能关闭连接。

    启动main方法,控制台打印:

    receive message:hello world

    此时消费者没有关闭,

    如果生产者再发一条消息,消费者就又接收一条消息

    效果如下:

    总结

    1、发送端操作流程

    1)创建连接。2)创建通道。3)声明队列。4)发送消息

    2、接收端

    1)创建连接。2)创建通道。3)声明队列。4)监听队列。5)接收消息

    三、工作模式

     RabbitMQ有以下几种工作模式 :

    (1)、Work queues工作模式。(2)、Publish/Subscribe发布订阅(3)、Routing(4)、Topics通配符模式(5)、Header(6)、RPC

    1、Work queues工作队列模式

    work queues与入门程序相比,多了一个消费端,两个消费端共同消费同一个队列中的消息。多个消费者共同监听队列,消息不能被重复消费。

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

    测试:

    (1)、使用入门程序,启动多个消费者。

    (2)、生产者发送多个消息。

    启动多个消费者,将Consumer01复制两份得到Consumer02、Consumer03。分别启动Consumer01、Consumer02、Consumer03,就会有三个消费者。

    当消息生产者发送一个消息,Consumer01接收并消费这个消息。当消费者再次发送一个消息,Consumer02接收并消费这个消息。当消费者第三次发送消息,Consumer03接收并消费这个消息。即采用轮询的方式将消息发送给消费者。

    结果:

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

    2、Publish/subscribe发布订阅模式

    发布订阅模式:

    (1)、一个生产者将消息发送给交换机。

    (1)、与交换机绑定的有多个队列,每个消费者监听自己的队列。

    (2)、生产者将消息发给broker,由交换机将消息转发到绑定此交换机的每个队列,每个绑定交换机的队列都将接收到消息。

    如果发布订阅模式只有一个队列与交换机绑定,就是工作队列模式,如下:

    工作队列模式有一个默认的交换机。

    案例:用户通知,当用户充值成功或转账完成,系统通知用户,通知方式有短信、邮件多种方法 。通过发布订阅模式,可以实现短信和邮件都要通知。

    1)、消息生产者

    声明Exchange_fanout_inform交换机。声明两个队列并且绑定到此交换机,交换机和队列绑定时不需要指定routingkey。发送消息时不需要指定routingkey

    import com.rabbitmq.client.BuiltinExchangeType;
    import com.rabbitmq.client.Channel;
    import com.rabbitmq.client.Connection;
    import com.rabbitmq.client.ConnectionFactory;
    
    import java.io.IOException;
    import java.util.concurrent.TimeoutException;
    
    /**
     * @author Administrator
     * @version 1.0
     * @create 2018-06-17 18:10
     **/
    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();
                //声明队列,如果队列在mq 中没有则要创建
                //参数:String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
                /**
                 * 参数明细
                 * 1、queue 队列名称
                 * 2、durable 是否持久化,如果持久化,mq重启后队列还在
                 * 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
                 * 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
                 * 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、交换机的名称
                 * 2、交换机的类型
                 * fanout:对应的rabbitmq的工作模式是 publish/subscribe
                 * direct:对应的Routing    工作模式
                 * topic:对应的Topics工作模式
                 * headers: 对应的headers工作模式
                 */
                channel.exchangeDeclare(EXCHANGE_FANOUT_INFORM, BuiltinExchangeType.FANOUT);
                //进行交换机和队列绑定,不需要指定routingKey
                //参数:String queue, String exchange, String routingKey
                /**
                 * 参数明细:
                 * 1、queue 队列名称
                 * 2、exchange 交换机名称
                 * 3、routingKey 路由key,作用是交换机根据路由key的值将消息转发到指定的队列中,在发布订阅模式中调协为空字符串
                 */
                channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_FANOUT_INFORM,"");
                channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_FANOUT_INFORM,"");
                //发送消息,不需要指定routingKey
                //参数: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++){ // 一次发送5条消息
                    //消息内容
                    String message = "send inform message to user";
                    channel.basicPublish(EXCHANGE_FANOUT_INFORM,"",null,message.getBytes());
                    System.out.println("send to mq "+message);
                }
    
            } 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)、fanout:对应的rabbitmq的工作模式是 publish/subscribe

    (2)、direct:对应的Routing 工作模式

    (3)、topic:对应的Topics工作模式

    (4)、对应的headers工作模式

    2)、消息消费者-邮件发送

    import com.rabbitmq.client.*;
    
    import java.io.IOException;
    import java.util.concurrent.TimeoutException;
    
    /**
     * @author Administrator
     * @version 1.0
     * @create 2018-06-17 18:22
     **/
    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();
    
            /**
             * 参数明细
             * 1、queue 队列名称
             * 2、durable 是否持久化,如果持久化,mq重启后队列还在
             * 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
             * 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
             * 5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
             */
            channel.queueDeclare(QUEUE_INFORM_EMAIL,true,false,false,null);
            //声明一个交换机
            //参数:String exchange, String type
            /**
             * 参数明细:
             * 1、交换机的名称
             * 2、交换机的类型
             * fanout:对应的rabbitmq的工作模式是 publish/subscribe
             * direct:对应的Routing    工作模式
             * topic:对应的Topics工作模式
             * 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_EMAIL, EXCHANGE_FANOUT_INFORM, "");
    
            //实现消费方法
            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消息已接收,如果将此参数设置为tru表示会自动回复mq,如果设置为false要通过编程实现回复
             * 3、callback,消费方法,当消费者接收到消息要执行的方法
             */
            channel.basicConsume(QUEUE_INFORM_EMAIL,true,defaultConsumer);
    
        }
    }

    3)、消息消费者-短信发送

    import com.rabbitmq.client.*;
    
    import java.io.IOException;
    import java.util.concurrent.TimeoutException;
    
    /**
     * @author Administrator
     * @version 1.0
     * @create 2018-06-17 18:22
     **/
    public class Consumer02_subscribe_sms {
        //队列名称
        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) 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();
    
            /**
             * 参数明细
             * 1、queue 队列名称
             * 2、durable 是否持久化,如果持久化,mq重启后队列还在
             * 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
             * 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
             * 5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
             */
            channel.queueDeclare(QUEUE_INFORM_SMS,true,false,false,null);
            //声明一个交换机
            //参数:String exchange, String type
            /**
             * 参数明细:
             * 1、交换机的名称
             * 2、交换机的类型
             * fanout:对应的rabbitmq的工作模式是 publish/subscribe
             * direct:对应的Routing    工作模式
             * topic:对应的Topics工作模式
             * 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, "");
    
            //实现消费方法
            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消息已接收,如果将此参数设置为tru表示会自动回复mq,如果设置为false要通过编程实现回复
             * 3、callback,消费方法,当消费者接收到消息要执行的方法
             */
            channel.basicConsume(QUEUE_INFORM_SMS,true,defaultConsumer);
    
        }
    }

    4)、测试

    先启动生产者,发送了5条消息

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

    单击交换机,此时显示交换机绑定了两个队列,此时routingKey为空。

    此时每个队列都有5条消息

     先启动邮件发送消费者,

    此时,邮件队列中的消息个数为0

    再启动发送短息的消费者。此时连接和通道如下:

    使用生产者发送若干条消息,每条消息都转发到各各队列,每消费者都接收到了消息。

    5)、publish/subscribe与work queues有什么区别:

    不同点:

    (1)、work queues不用定义交换机,而publish/subscribe需要定义交换机。

    (2)、publish/subscribe的生产方是面向交换机发送消息,work queues的生产方是面向队列发送消息(底层使用默认交换机)。

    (3)、publish/subscribe需要设置队列和交换机的绑定,work queues不需要设置,实质上work queues会将队列绑定到默认的交换机 。

    (4)、工作队列模式中,消息只会被一个消费者接收到,而发布订阅模式,一个消息可以被多个消费者接收到。

    相同点:

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

    此时我们给发送短信的队列增加一个消费者,复制一份代码并启动。此时生产者发送5条消息,此时邮件的消费者接收到5条消息,发送短信的两个消费者分别接收到3条和2条消息。

    实际工作用什么 publish/subscribe还是work queues?

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

    3、Routing路由工作模式

    路由模式:

    (1)、一个交换机可以绑定多个队列,每个队列设置routingKey,并且一个队列可以设置多个routingkey。

    (1)、每个消费者监听自己的队列。

    (2)、生产者将消息发给交换机,发送消息时需要指定routingKey的值,由交换机根据routingkey来转发消息到指定的队列。

    如果生产者发送消息时指定的routingKey为error,则消息分别发送到两个队列。

    1)、消息生产者

    声明exchange_routing_inform交换机。声明两个队列并且绑定到此交换机,绑定时需要指定routingkey。发送消息时需要指定routingkey

    import com.rabbitmq.client.BuiltinExchangeType;
    import com.rabbitmq.client.Channel;
    import com.rabbitmq.client.Connection;
    import com.rabbitmq.client.ConnectionFactory;
    
    import java.io.IOException;
    import java.util.concurrent.TimeoutException;
    
    /**
     * @author Administrator
     * @version 1.0
     * @create 2018-06-17 19:23
     **/
    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();
                //声明队列,如果队列在mq 中没有则要创建
                //参数:String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
                /**
                 * 参数明细
                 * 1、queue 队列名称
                 * 2、durable 是否持久化,如果持久化,mq重启后队列还在
                 * 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
                 * 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
                 * 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、交换机的名称
                 * 2、交换机的类型
                 * fanout:对应的rabbitmq的工作模式是 publish/subscribe
                 * direct:对应的Routing    工作模式
                 * topic:对应的Topics工作模式
                 * headers: 对应的headers工作模式
                 */
                channel.exchangeDeclare(EXCHANGE_ROUTING_INFORM, BuiltinExchangeType.DIRECT);
                //进行交换机和队列绑定,一个队列设置多个routingKey
                //参数:String queue, String exchange, String routingKey
                /**
                 * 参数明细:
                 * 1、queue 队列名称
                 * 2、exchange 交换机名称
                 * 3、routingKey 路由key,作用是交换机根据路由key的值将消息转发到指定的队列中,在发布订阅模式中调协为空字符串
                 */
                channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_ROUTING_INFORM,ROUTINGKEY_EMAIL);
                channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_ROUTING_INFORM,"inform");
                channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_ROUTING_INFORM,ROUTINGKEY_SMS);
                channel.queueBind(QUEUE_INFORM_SMS,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++){
                    //发送消息的时候指定routingKey
                    String message = "send email inform message to user";
                    channel.basicPublish(EXCHANGE_ROUTING_INFORM,ROUTINGKEY_EMAIL,null,message.getBytes());
                    System.out.println("send to mq "+message);
                }
                /*for(int i=0;i<5;i++){
                    //发送消息的时候指定routingKey
                    String message = "send sms inform message to user";
                    channel.basicPublish(EXCHANGE_ROUTING_INFORM,ROUTINGKEY_SMS,null,message.getBytes());
                    System.out.println("send to mq "+message);
                }
                for(int i=0;i<5;i++){
                    //发送消息的时候指定routingKey
                    String message = "send inform message to user";
                    channel.basicPublish(EXCHANGE_ROUTING_INFORM,"inform",null,message.getBytes());
                    System.out.println("send to mq "+message);
                } */
    
            } 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();
                }
            }
    
    
        }
    }

    2)、邮件发送消费者

    import com.rabbitmq.client.*;
    
    import java.io.IOException;
    import java.util.concurrent.TimeoutException;
    
    /**
     * @author Administrator
     * @version 1.0
     * @create 2018-06-17 18:22
     **/
    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();
    
            /**
             * 参数明细
             * 1、queue 队列名称
             * 2、durable 是否持久化,如果持久化,mq重启后队列还在
             * 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
             * 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
             * 5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
             */
            channel.queueDeclare(QUEUE_INFORM_EMAIL,true,false,false,null);
            //声明一个交换机
            //参数:String exchange, String type
            /**
             * 参数明细:
             * 1、交换机的名称
             * 2、交换机的类型
             * fanout:对应的rabbitmq的工作模式是 publish/subscribe
             * direct:对应的Routing    工作模式
             * topic:对应的Topics工作模式
             * 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_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消息已接收,如果将此参数设置为tru表示会自动回复mq,如果设置为false要通过编程实现回复
             * 3、callback,消费方法,当消费者接收到消息要执行的方法
             */
            channel.basicConsume(QUEUE_INFORM_EMAIL,true,defaultConsumer);
    
        }
    }

    3)、短信发送消费者

    import com.rabbitmq.client.*;
    
    import java.io.IOException;
    import java.util.concurrent.TimeoutException;
    
    /**
     * @author Administrator
     * @version 1.0
     * @create 2018-06-17 18:22
     **/
    public class Consumer03_routing_sms {
        //队列名称
        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_SMS="inform_sms";
    
        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();
    
            /**
             * 参数明细
             * 1、queue 队列名称
             * 2、durable 是否持久化,如果持久化,mq重启后队列还在
             * 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
             * 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
             * 5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
             */
            channel.queueDeclare(QUEUE_INFORM_SMS,true,false,false,null);
            //声明一个交换机
            //参数:String exchange, String type
            /**
             * 参数明细:
             * 1、交换机的名称
             * 2、交换机的类型
             * fanout:对应的rabbitmq的工作模式是 publish/subscribe
             * direct:对应的Routing    工作模式
             * topic:对应的Topics工作模式
             * 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);
    
            //实现消费方法
            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消息已接收,如果将此参数设置为tru表示会自动回复mq,如果设置为false要通过编程实现回复
             * 3、callback,消费方法,当消费者接收到消息要执行的方法
             */
            channel.basicConsume(QUEUE_INFORM_SMS,true,defaultConsumer);
    
        }
    }

    4)、测试

    先启动生产者,打开RabbitMQ的管理界面,观察交换机绑定情况:

    点击交换机,发现交换机绑定了4个队列,并且队列绑定了routingKey

    队列queue_info_email绑定了2个routingKey:inform和inform_email。如果你不想绑定inform这个routingKey,可以点击Unbind进行解绑

    此时email队列有5条消息

    启动邮件的消费者,发现消费了5条消息。启动短信的消费者,发现没有消费消息。

    使用生产者发送若干条消息,交换机根据routingkey转发消息到指定的队列。

    5)、Routing模式和Publish/subscibe有啥区别?

    不同点:

    (1)、交换机的类型不同,Routing模式交换机的类型为direct。

    (2)、Routing模式要求队列在绑定交换机时要指定routingkey,消息会转发到符合routingkey的队列。发布订阅模式再绑定交换机时不需要指定routingKey,即不判断routingKey,消息会发送到每个绑定交换机的队列。

    Routing模式可以实现发布订阅模式的功能。但是Routing模式可以根据routingKey的判断,只发送到某一个或某几个队列,即符合规则的队列才会发,而不是像发布订阅模式一样给每个队列都发。

    4、Topics通配符模式

     路由模式:

    (1)、一个交换机可以绑定多个队列,每个队列可以设置一个或多个带通配符的routingKey,每个消费者监听自己的队列,并且设置带统配符的routingkey。

    (2)、生产者将消息发给broker,由交换机根据routingkey来转发消息到指定的队列。

    案例:

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

    1)、生产者

    声明交换机,指定topic类型:

    import com.rabbitmq.client.BuiltinExchangeType;
    import com.rabbitmq.client.Channel;
    import com.rabbitmq.client.Connection;
    import com.rabbitmq.client.ConnectionFactory;
    
    import java.io.IOException;
    import java.util.concurrent.TimeoutException;
    
    /**
     * @author Administrator
     * @version 1.0
     * @create 2018-06-17 19:23
     **/
    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();
                //声明队列,如果队列在mq 中没有则要创建
                //参数:String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
                /**
                 * 参数明细
                 * 1、queue 队列名称
                 * 2、durable 是否持久化,如果持久化,mq重启后队列还在
                 * 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
                 * 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
                 * 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、交换机的名称
                 * 2、交换机的类型
                 * fanout:对应的rabbitmq的工作模式是 publish/subscribe
                 * direct:对应的Routing    工作模式
                 * topic:对应的Topics工作模式
                 * 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_EMAIL,EXCHANGE_TOPICS_INFORM,ROUTINGKEY_EMAIL);
                channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_TOPICS_INFORM,ROUTINGKEY_SMS);
                //发送消息
                //参数: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++){
                    //发送消息的时候指定routingKey
                    String message = "send email inform message to user";
                    channel.basicPublish(EXCHANGE_TOPICS_INFORM,"inform.email",null,message.getBytes());
                    System.out.println("send to mq "+message);
                }
                for(int i=0;i<5;i++){
                    //发送消息的时候指定routingKey
                    String message = "send sms inform message to user";
                    channel.basicPublish(EXCHANGE_TOPICS_INFORM,"inform.sms",null,message.getBytes());
                    System.out.println("send to mq "+message);
                }
                for(int i=0;i<5;i++){
                    //发送消息的时候指定routingKey
                    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 "+message);
                }
    
    
            } 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();
                }
            }
        }
    }

    2)、发送短信消费端

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

    通配符规则:中间以“.”分隔。

    符号#可以匹配一个或多个词,比如inform.#,会接收以inform开头的routingKey,可以匹配inform.email,inform.sms,inform.sms.email

    符号*可以匹配一个词语。比如inform.*可以匹配inform.email,inform.sms

    当通配符为inform.#.email.#时,可以匹配inform.email(#可以代表空),inform.sms.email,inform.email.sms

    当通配符为inform.#.sms.#时,可以匹配inform.sms(#可以代表空),inform.email.sms、inform.sms.email

    import com.rabbitmq.client.*;
    
    import java.io.IOException;
    import java.util.concurrent.TimeoutException;
    
    /**
     * @author Administrator
     * @version 1.0
     * @create 2018-06-17 18:22
     **/
    public class Consumer04_topics_sms {
        //队列名称
        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_SMS="inform.#.sms.#";
    
        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();
    
            /**
             * 参数明细
             * 1、queue 队列名称
             * 2、durable 是否持久化,如果持久化,mq重启后队列还在
             * 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
             * 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
             * 5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
             */
            channel.queueDeclare(QUEUE_INFORM_SMS,true,false,false,null);
            //声明一个交换机
            //参数:String exchange, String type
            /**
             * 参数明细:
             * 1、交换机的名称
             * 2、交换机的类型
             * fanout:对应的rabbitmq的工作模式是 publish/subscribe
             * direct:对应的Routing    工作模式
             * topic:对应的Topics工作模式
             * 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);
    
            //实现消费方法
            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消息已接收,如果将此参数设置为tru表示会自动回复mq,如果设置为false要通过编程实现回复
             * 3、callback,消费方法,当消费者接收到消息要执行的方法
             */
            channel.basicConsume(QUEUE_INFORM_SMS,true,defaultConsumer);
    
        }
    }

    3)、发送邮件消费端

    import com.rabbitmq.client.*;
    
    import java.io.IOException;
    import java.util.concurrent.TimeoutException;
    
    /**
     * @author Administrator
     * @version 1.0
     * @create 2018-06-17 18:22
     **/
    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();
    
            /**
             * 参数明细
             * 1、queue 队列名称
             * 2、durable 是否持久化,如果持久化,mq重启后队列还在
             * 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
             * 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
             * 5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
             */
            channel.queueDeclare(QUEUE_INFORM_EMAIL,true,false,false,null);
            //声明一个交换机
            //参数:String exchange, String type
            /**
             * 参数明细:
             * 1、交换机的名称
             * 2、交换机的类型
             * fanout:对应的rabbitmq的工作模式是 publish/subscribe
             * direct:对应的Routing    工作模式
             * topic:对应的Topics工作模式
             * 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_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消息已接收,如果将此参数设置为tru表示会自动回复mq,如果设置为false要通过编程实现回复
             * 3、callback,消费方法,当消费者接收到消息要执行的方法
             */
            channel.basicConsume(QUEUE_INFORM_EMAIL,true,defaultConsumer);
    
        }
    }

    4)、测试

    启动生产者模式

    点击交换机

    此时email队列有5条消息

    启动两个消费者,发现只有邮件消费者消费了消息。

    使用生产者发送若干条消息,交换机根据routingkey统配符匹配并转发消息到指定的队列。

    本案例的需求使用Routing工作模式能否实现?

    使用Routing模式也可以实现本案例,共设置三个 routingkey,分别是email、sms、all,email队列绑定email和all,sms队列绑定sms和all,这样就可以实现上边案例的功能,实现过程比topics复杂。

    Topic模式更加强大,它可以实现Routing、publish/subscirbe模式的功能。

    5)、通配符模式和Routing模式的区别:

    (1)、通配符模式交换机的类型设置为topic。

    (2)、队列与交换机绑定的时候可以设置带通配符的routingKey。

    (3)、routingKey的匹配方式:Routin模式是相等匹配,Topics模式是通配符匹配。

    5、Header 模式

    header模式与routing不同的地方在于,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());

    2)、发送邮件消费者

    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);

    6、RPC模式

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

    (1)、客户端即是生产者就是消费者,向RPC请求队列发送RPC调用消息,同时监听RPC响应队列

    (2)、服务端监听RPC请求队列的消息,收到消息后执行服务端的方法,得到方法返回的结果。

    (3)、服务端将RPC方法 的结果发送到RPC响应队列。

    (4)、客户端(RPC调用方)监听RPC响应队列,接收到RPC调用结果。

    四、消息确认机制

    详见https://www.cnblogs.com/zwh0910/p/16060244.html#autoid-3-4-0

    五、消息持久化机制

    如果我们希望即使在RabbitMQ服务重启的情况下,也不会丢失消息,我们可以将Queue与Message都设置为可持久化的(durable),这样可以保证绝大部分情况下我们的RabbitMQ消息不会丢失。但依然解决不了小概率丢失事件的发生(比如RabbitMQ服务器已经接收到生产者的消息,但还没来得及持久化该消息时RabbitMQ服务器就断电了),如果我们需要对这种小概率事件也要管理起来,那么我们要用到事务。

    六、事务

    对事务的支持是AMQP协议的一个重要特性。假设当生产者将一个持久化消息发送给服务器时,因为consume命令本身没有任何Response返回,所以即使服务器崩溃,没有持久化该消息,生产者也无法获知该消息已经丢失。如果此时使用事务,即通过txSelect()开启一个事务,然后发送消息给服务器,然后通过txCommit()提交该事务,即可以保证,如果txCommit()提交了,则该消息一定会持久化,如果txCommit()还未提交即服务器崩溃,则该消息不会服务器接收。当然Rabbit MQ也提供了txRollback()命令用于回滚某一个事务。

  • 相关阅读:
    臭氧总量下载网址
    WRF遇到的问题
    linux 查询硬盘、内存、cpu命令
    降维中的特征选择
    偏最小二乘回归分析建模步骤的R实现(康复俱乐部20名成员测试数据)+补充pls回归系数矩阵的算法实现
    R语言机器学习之caret包运用
    用R语言做数据清理(详细教程)
    RColorBrewer的使用
    VOD, TVOD, SVOD FVOD的区别(转)
    Include promo/activity effect into the prediction (extended ARIMA model with R)
  • 原文地址:https://www.cnblogs.com/zwh0910/p/16056076.html
Copyright © 2020-2023  润新知