RabbitMQ基础

1.MQ概述

MQ全称 Message Queue(消息队列),是在消息的传输过程中保存消息的容器。多用于分布式系统之间进行通信

1.MQ的优势

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

系统的耦合性越高,容错性就越低,可维护性就越低 

库存系统与订单系统耦合,当库存系统出现问题挂了,则订单系统也可能出现问题

系统的耦合性越高,容错性就越低,可维护性就越低

使用 MQ 使得应用间解耦,提升容错性和可维护性

2.异步提速

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

一个下单操作耗时:20 + 300 + 300 + 300 = 920ms

用户点击完下单按钮后,需要等待920ms才能得到下单响应,太慢 

当使用MQ之后,当有用户下单时,只需要将订单放到MQ中,然后其它系统异步去获取,不需要等待

用户点击完下单按钮后,只需等待25ms就能得到下单响应 (20 + 5 = 25ms)

提升用户体验和系统吞吐量(单位时间内处理请求的数目)

其它工作都会异步执行,效率提高了很多

3.削峰填谷

如订单系统,在下单的时候就会往数据库写数据。但是数据库只能支撑每秒1000左右的并发写入,并发量再高就容易宕机。低峰期的时候并发也就100多个,但是在高峰期时候,并发量会突然激增到5000以上,这时A系统瞬间就被打死了

这时就可以用MQ把峰值消掉

如果用了MQ,A系统就不会直接接收到这5000个请求

消息被MQ保存起来了,然后系统就可以按照自己的消费能力来消费,比如每秒1000个消息,这样慢慢写入数据库 ,这就叫削峰

但是使用了MQ之后,限制消费消息的速度为1000,但是这样一来,高峰期产生的数据势必会被积压在MQ中,高峰就被“削”掉了。但是因为消息积压,在高峰期过后的一段时间内,消费消息的速度还是会维持在1000QPS,直到消费完积压的消息,这就叫填谷

应用解耦:提高系统容错性和可维护性

异步提速:提升用户体验和系统吞吐量

削峰填谷:提高系统稳定性

2.MQ的劣势

系统可用性降低

系统引入的外部依赖越多,系统稳定性越差。一旦 MQ 宕机,就会对业务造成影响

如何保证MQ的高可用?

系统复杂度提高

MQ 的加入大大增加了系统的复杂度,以前系统间是同步的远程调用,现在是通过 MQ 进行异步调用

如何保证消息没有被重复消费?怎么处理消息丢失情况?那么保证消息传递的顺序性?

一致性问题

A 系统处理完业务,通过 MQ 给B、C、D三个系统发消息,如果 B 系统、C 系统处理成功,D 系统处理失败

如何保证消息数据处理的一致性?

小结

既然MQ有优势也有劣势,那么使用MQ需要满足什么条件呢

1.生产者不需要从消费者处获得反馈。引入消息队列之前的直接调用,其接口的返回值应该为空,这才让明明下层的动作还没做,上层却当成动作做完了继续往后走,即所谓异步成为了可能

2.容许短暂的不一致性

3.确实是用了有效果。即解耦、提速、削峰这些方面的收益,超过加入MQ,管理MQ这些成本

3.常见的MQ产品

目前业界有很多的 MQ 产品,例如 RabbitMQ、RocketMQ、ActiveMQ、Kafka、ZeroMQ、MetaMq等,也有直接使用 Redis 充当消息队列的案例,而这些消息队列产品,各有侧重,在实际选型时,需要结合自身需求及 MQ 产品特征,综合考虑

2.RabbitMQ

实现MQ的大致有两种主流方式:AMQP、JMS

1.AMQP

AMQP,即 Advanced Message Queuing Protocol(高级消息队列协议),是一个网络协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,遵循此协议,不收客户端和中间件产品和开发语言限制。2006年,AMQP 规范发布。类比HTTP

2007年,Rabbit 技术公司基于 AMQP 标准开发的 RabbitMQ 1.0 发布,RabbitMQ 采用 Erlang 语言开发。Erlang 语言由 Ericson 设计,专门为开发高并发和分布式系统的一种语言,在电信领域使用广泛

RabbitMQ 基础架构

2.RabbitMQ概述

RabbitMQ 中的相关概念 

Broker:接收和分发消息的应用,RabbitMQ Server就是 Message Broker

Virtual host:出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出多个vhost,每个用户在自己的 vhost 创建 exchange/queue 等

Connection:publisher/consumer 和 broker 之间的 TCP 连接

Channel:如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCPConnection的开销将是巨大的,效率也较低。Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的 channel 进行通讯,AMQP method 包含了channel id 帮助客户端和message broker 识别 channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的 Connection 极大减少了操作系统建立 TCP connection 的开销

Exchange:message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到queue 中去。常用的类型有:direct (point-to-point), topic (publish-subscribe) andfanout (multicast)

Queue:消息最终被送到这里等待 consumer 取走

Binding:exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key。Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据

RabbitMQ提供了6种模式:简单模式,work模式,Publish/Subscribe发布与订阅模式,Routing路由模式,Topics主题模式,RPC远程调用模式(远程调用,不太算MQ;暂不作介绍)

官网对应模式介绍:RabbitMQ Tutorials | RabbitMQ

3.JMS

JMS 即 Java消息服务(JavaMessage Service)应用程序接口,是一个Java平台中关于面向消息中间件的API

JMS是JavaEE规范中的一种,类比JDBC

很多消息中间件都实现了JMS规范,例如:ActiveMQ;RabbitMQ官方没有提供JMS的实现包,但是开源社区有

4.安装RabbitMQ

官网:RabbitMQ: easy to use, flexible messaging and streaming | RabbitMQ

下载RabbitMQ 

下载Erlang

GitHub - rabbitmq/erlang-rpm: Latest Erlang/OTP releases packaged as a zero dependency RPM, just enough for running RabbitMQ

下载socat

Index of /CentOS/7/x86_64 (iotti.biz)

安装Erlang

首先将下载好的文件上传到服务器,创建一个文件夹用来存放文件

将下载的三个.rpm文件上传到服务器的刚创建的文件夹中

解压安装erlang

rpm -Uvh erlang-26.2.2-1.el7.x86_64.rpm 

安装完成后输入如下指令查看版本号

erl -v

安装RabbitMQ

在RabbitMQ安装过程中需要依赖socat插件,首先安装该插件 

rpm -ivh socat-1.7.3.2-5.el7.lux.x86_64.rpm

解压

rpm -ivh rabbitmq-server-3.13.0-1.el8.noarch.rpm

启动RabbitMQ服务 

# 启动rabbitmq

systemctl start rabbitmq-server

# 查看rabbitmq状态

systemctl status rabbitmq-server

#停止rabbitmq

systemctl stop rabbitmq-server

# 设置rabbitmq服务开机自启动

systemctl enable rabbitmq-server

# 重启rabbitmq服务

systemctl restart rabbitmq-server

RabbitMQWeb管理界面及授权操作

# 打开RabbitMQWeb管理界面插件

rabbitmq-plugins enable rabbitmq_management

修改默认配置信息

cd /lib/rabbitmq/lib/rabbitmq_server-3.13.0/plugins/rabbit-3.13.0/ebin/rabbit.app

vim rabbit.app

然后我们打开浏览器,访问(服务器ip)http://192.168.235.129:15672/

有一个默认的账号密码guest,但该情况仅限于本机localhost进行访问,所以需要添加一个远程登录的用户

3.快速入门

1.

P:生产者,也就是要发送消息的程序

C:消费者:消息的接收者,会一直等待消息到来

queue:消息队列,图中红色部分。类似一个邮箱,可以缓存消息;生产者向其中投递消息,消费者从其中取出消息

需求: 使用简单模式完成消息传递

步骤:

1.创建工程(生成者、消费者)

2.分别添加依赖

3.编写生产者发送消息

4.编写消费者接收消息

生产者 rabbitmq-producer

pom.xml 依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com</groupId>
    <artifactId>rabbitmq-producer</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <!--rabbitmq java 客户端-->
        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>5.6.0</version>
        </dependency>
    </dependencies>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

</project>

Producer_HelloWorld

package com;

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

    /*
        发送消息
    */
public class Producer_HelloWorld {
    public static void main(String[] args) throws Exception{

        //1.创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();

        //2.设置参数
        factory.setHost("192.168.235.129"); //ip 默认值localhost
        factory.setPort(5672); //端口 默认值5672
        factory.setVirtualHost("/ljb"); //虚拟机 默认值/
        factory.setUsername("ljb");   //用户名 默认guest
        factory.setPassword("ljb");   //密码 默认值guest

        //3.创建连接 Connection
        Connection connection = factory.newConnection();

        //4.创建Channel
        Channel channel = connection.createChannel();

        //5.创建队列Queue
        /*
          queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments)
          参数:
          1.String queue:队列名称。如果当前队列名称不存在,则会创建该队列,否则不会创建
          2.boolean durable:是否持久化。即队列在服务器重启后是否还存在
          3.boolean exclusive:
            *是否独占。只能有一个消费者监听这队列
            *当Connection关闭时,是否删除队列
          4.boolean autoDelete:是否自动删除。如果为true,至少有一个消费者连接到这个队列,之后所有与这个队列连接的消费者都断开时,队列会自动删除。
          5.Map<String, Object> arguments:附带参数。队列的其他属性,例如:x-message-ttl、x-expires、x-max-length、x-maxlength-bytes、x-dead-letter-exchange、x-dead-letter-routing-key、x-max-priority
         */
        //如果没有一个名字叫hello_world的队列,则会创建该队列,如果有则不会创建
        channel.queueDeclare("hello_world",true,false,false,null);

        //6.发送消息
        /*
           basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body)
           参数:
           1.String exchange:交换机名称。简单模式下交换机会使用默认的 ""
           2.String routingKey:路由名称
           3.BasicProperties props:配置信息
           4.byte[] body:发送消息数据
         */
        String body = "hello rabbitmq~~~";
        channel.basicPublish("","hello_world",null,body.getBytes());

        //7.释放资源
        channel.close();
        connection.close();
    }
}

 

消费者rabbitmq-consumer

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com</groupId>
    <artifactId>rabbitmq-consumer</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <!--rabbitmq java 客户端-->
        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>5.6.0</version>
        </dependency>
    </dependencies>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

</project>

Consumer_HelloWorld 

package com;

import com.rabbitmq.client.*;

public class Consumer_HelloWord {
    public static void main(String[] args) throws Exception {

        //1.创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();

        //2.设置参数
        factory.setHost("192.168.235.129"); //ip 默认值localhost
        factory.setPort(5672); //端口 默认值5672
        factory.setVirtualHost("/ljb"); //虚拟机 默认值/
        factory.setUsername("ljb");//用户名 默认guest
        factory.setPassword("ljb");//密码 默认值guest

        //3.创建连接 Connection
        Connection connection = factory.newConnection();

        //4.创建Channel
        Channel channel = connection.createChannel();

        //5.创建队列Queue
        /*
          queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments)
          参数:
          1.String queue:队列名称。如果当前队列名称不存在,则会创建该队列,否则不会创建
          2.boolean durable:是否持久化。即队列在服务器重启后是否还存在
          3.boolean exclusive:
            *是否独占。只能有一个消费者监听这队列
            *当Connection关闭时,是否删除队列
          4.boolean autoDelete:是否自动删除。如果为true,至少有一个消费者连接到这个队列,之后所有与这个队列连接的消费者都断开时,队列会自动删除。
          5.Map<String, Object> arguments:附带参数。队列的其他属性,例如:x-message-ttl、x-expires、x-max-length、x-maxlength-bytes、x-dead-letter-exchange、x-dead-letter-routing-key、x-max-priority
         */
        //如果没有一个名字叫hello_world的队列,则会创建该队列,如果有则不会创建
        channel.queueDeclare("hello_world",true,false,false,null);
        /*
           basicConsume(String queue, boolean autoAck, Consumer callback)
           参数:
           1.String queue:队列名称
           2.boolean autoAck:是否自动确认
           3.Consumer callback:回调对象
         */

        //6.接收信息
        Consumer consumer = new DefaultConsumer(channel){
            /*
               回调方法,当收到消息后,会自动执行该方法
               void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
                String consumerTag:标识
                Envelope envelope:获取一些信息,交换机,路由key...
                AMQP.BasicProperties properties:配置信息
                byte[] body:数据
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) {
                System.out.println("consumerTag:" + consumerTag);
                System.out.println("Exchange:" + envelope.getExchange());
                System.out.println("RoutingKey:" + envelope.getRoutingKey());
                System.out.println("properties:" + properties);
                System.out.println("body:" + new String(body));
            }
        };

        channel.basicConsume("hello_world", true, consumer);

        //7.关闭资源?不要 (消费者需要监听生产者消息,这里不关闭)
    }
}

启动消费者后 , 可以接收到生产者发送的数据

4.工作队列模式

Work Queues :与入门程序的简单模式相比,多了一个或一些消费端,多个消费端共同消费同一个队列中的消息。

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

1.生产者 

package com;

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

/*
发送消息
*/
public class Producer_WorkQueues {
public static void main(String[] args) throws Exception{

    //1.创建连接工厂
    ConnectionFactory factory = new ConnectionFactory();

    //2.设置参数
    factory.setHost("192.168.235.129"); //ip 默认值localhost
    factory.setPort(5672); //端口 默认值5672
    factory.setVirtualHost("/ljb"); //虚拟机 默认值/
    factory.setUsername("ljb");   //用户名 默认guest
    factory.setPassword("ljb");   //密码 默认值guest

    //3.创建连接 Connection
    Connection connection = factory.newConnection();

    //4.创建Channel
    Channel channel = connection.createChannel();

    //5.创建队列Queue
    /*
      queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments)
      参数:
      1.String queue:队列名称。如果当前队列名称不存在,则会创建该队列,否则不会创建
      2.boolean durable:是否持久化。即队列在服务器重启后是否还存在
      3.boolean exclusive:
        *是否独占。只能有一个消费者监听这队列
        *当Connection关闭时,是否删除队列
      4.boolean autoDelete:是否自动删除。如果为true,至少有一个消费者连接到这个队列,之后所有与这个队列连接的消费者都断开时,队列会自动删除。
      5.Map<String, Object> arguments:附带参数。队列的其他属性,例如:x-message-ttl、x-expires、x-max-length、x-maxlength-bytes、x-dead-letter-exchange、x-dead-letter-routing-key、x-max-priority
     */
    //如果没有一个名字叫hello_world的队列,则会创建该队列,如果有则不会创建
    channel.queueDeclare("work_queues",true,false,false,null);

    for (int i = 0; i < 10; i++) {
        String body = i+"hello rabbitmq~~~";
        //6.发送消息
        channel.basicPublish("","work_queues",null,body.getBytes());

    }
    /*
       basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body)
       参数:
       1.String exchange:交换机名称。简单模式下交换机会使用默认的 ""
       2.String routingKey:路由名称
       3.BasicProperties props:配置信息
       4.byte[] body:发送消息数据
     */

    //7.释放资源
    channel.close();
    connection.close();
}
}

2.消费者

第一个消费者 Consumer_WorkQueues1 

package com;

import com.rabbitmq.client.*;

public class Consumer_WorkQueues1 {
    public static void main(String[] args) throws Exception {

        //1.创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();

        //2.设置参数
        factory.setHost("192.168.235.129"); //ip 默认值localhost
        factory.setPort(5672); //端口 默认值5672
        factory.setVirtualHost("/ljb"); //虚拟机 默认值/
        factory.setUsername("ljb");//用户名 默认guest
        factory.setPassword("ljb");//密码 默认值guest

        //3.创建连接 Connection
        Connection connection = factory.newConnection();

        //4.创建Channel
        Channel channel = connection.createChannel();

        //5.创建队列Queue
        /*
          queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments)
          参数:
          1.String queue:队列名称。如果当前队列名称不存在,则会创建该队列,否则不会创建
          2.boolean durable:是否持久化。即队列在服务器重启后是否还存在
          3.boolean exclusive:
            *是否独占。只能有一个消费者监听这队列
            *当Connection关闭时,是否删除队列
          4.boolean autoDelete:是否自动删除。如果为true,至少有一个消费者连接到这个队列,之后所有与这个队列连接的消费者都断开时,队列会自动删除。
          5.Map<String, Object> arguments:附带参数。队列的其他属性,例如:x-message-ttl、x-expires、x-max-length、x-maxlength-bytes、x-dead-letter-exchange、x-dead-letter-routing-key、x-max-priority
         */
        //如果没有一个名字叫hello_world的队列,则会创建该队列,如果有则不会创建
        channel.queueDeclare("work_queues",true,false,false,null);
        /*
           basicConsume(String queue, boolean autoAck, Consumer callback)
           参数:
           1.String queue:队列名称
           2.boolean autoAck:是否自动确认
           3.Consumer callback:回调对象
         */

        //6.接收信息
        Consumer consumer = new DefaultConsumer(channel){
            /*
               回调方法,当收到消息后,会自动执行该方法
               void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
                String consumerTag:标识
                Envelope envelope:获取一些信息,交换机,路由key...
                AMQP.BasicProperties properties:配置信息
                byte[] body:数据
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) {
                /*System.out.println("consumerTag:" + consumerTag);
                System.out.println("Exchange:" + envelope.getExchange());
                System.out.println("RoutingKey:" + envelope.getRoutingKey());
                System.out.println("properties:" + properties);*/
                System.out.println("body:" + new String(body));
            }
        };

        channel.basicConsume("work_queues", true, consumer);

        //7.关闭资源?不要 (消费者需要监听生产者消息,这里不关闭)
    }
}

第二个消费者 Consumer_WorkQueues2

package com;

import com.rabbitmq.client.*;

public class Consumer_WorkQueues2 {
    public static void main(String[] args) throws Exception {

        //1.创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();

        //2.设置参数
        factory.setHost("192.168.235.129"); //ip 默认值localhost
        factory.setPort(5672); //端口 默认值5672
        factory.setVirtualHost("/ljb"); //虚拟机 默认值/
        factory.setUsername("ljb");//用户名 默认guest
        factory.setPassword("ljb");//密码 默认值guest

        //3.创建连接 Connection
        Connection connection = factory.newConnection();

        //4.创建Channel
        Channel channel = connection.createChannel();

        //5.创建队列Queue
        /*
          queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments)
          参数:
          1.String queue:队列名称。如果当前队列名称不存在,则会创建该队列,否则不会创建
          2.boolean durable:是否持久化。即队列在服务器重启后是否还存在
          3.boolean exclusive:
            *是否独占。只能有一个消费者监听这队列
            *当Connection关闭时,是否删除队列
          4.boolean autoDelete:是否自动删除。如果为true,至少有一个消费者连接到这个队列,之后所有与这个队列连接的消费者都断开时,队列会自动删除。
          5.Map<String, Object> arguments:附带参数。队列的其他属性,例如:x-message-ttl、x-expires、x-max-length、x-maxlength-bytes、x-dead-letter-exchange、x-dead-letter-routing-key、x-max-priority
         */
        //如果没有一个名字叫hello_world的队列,则会创建该队列,如果有则不会创建
        channel.queueDeclare("work_queues",true,false,false,null);
        /*
           basicConsume(String queue, boolean autoAck, Consumer callback)
           参数:
           1.String queue:队列名称
           2.boolean autoAck:是否自动确认
           3.Consumer callback:回调对象
         */

        //6.接收信息
        Consumer consumer = new DefaultConsumer(channel){
            /*
               回调方法,当收到消息后,会自动执行该方法
               void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
                String consumerTag:标识
                Envelope envelope:获取一些信息,交换机,路由key...
                AMQP.BasicProperties properties:配置信息
                byte[] body:数据
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) {
                /*System.out.println("consumerTag:" + consumerTag);
                System.out.println("Exchange:" + envelope.getExchange());
                System.out.println("RoutingKey:" + envelope.getRoutingKey());
                System.out.println("properties:" + properties);*/
                System.out.println("body:" + new String(body));
            }
        };

        channel.basicConsume("work_queues", true, consumer);

        //7.关闭资源?不要 (消费者需要监听生产者消息,这里不关闭)
    }
}

小结 :
1.在一个队列中如果有多个消费者,那么消费者之间对于同一个消息的关系是竞争的关系
2. Work Queues对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度。例如:短信服务部署多个,只需要有一个节点成功发送即可

5.Publish/Subscribe发布与订阅模式(Pub/Sub)

在订阅模型中,多了一个 Exchange 角色,而且过程略有变化:

P:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给X(交换机)

C:消费者,消息的接收者,会一直等待消息到来

Queue:消息队列,接收消息、缓存消息

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

Exchange有常见以下3种类型:

Fanout:广播,将消息交给所有绑定到交换机的队列

Direct:定向,把消息交给符合指定routing key 的队列

Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列

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

案例需求

通过生产者生产一条消息,被c1,c2同时接收,消息是一条日志消息,c1将把日志打印在控制台上,c2将日志存储在数据库中

交换机需要与队列进行绑定,绑定之后;一个消息可以被多个消费者都收到

发布订阅模式与工作队列模式的区别:

1..工作队列模式不用定义交换机,而发布/订阅模式需要定义交换机

2.发布/订阅模式的生产方是面向交换机发送消息,工作队列模式的生产方是面向队列发送消息(底层使用默认交换机)

3.发布/订阅模式需要设置队列和交换机的绑定,工作队列模式不需要设置,实际上工作队列模式会将队列绑定到默认的交换机

生产者

package com;

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

/*
发送消息
*/
public class Producer_PubSub {
public static void main(String[] args) throws Exception{

    //1.创建连接工厂
    ConnectionFactory factory = new ConnectionFactory();

    //2.设置参数
    factory.setHost("192.168.235.129"); //ip 默认值localhost
    factory.setPort(5672); //端口 默认值5672
    factory.setVirtualHost("/ljb"); //虚拟机 默认值/
    factory.setUsername("ljb");   //用户名 默认guest
    factory.setPassword("ljb");   //密码 默认值guest

    //3.创建连接 Connection
    Connection connection = factory.newConnection();

    //4.创建Channel
    Channel channel = connection.createChannel();

    //5.创建交换机
    /*
    * exchangeDeclare(String exchange, BuiltinExchangeType type, boolean durable, boolean autoDelete, boolean internal, Map<String, Object> arguments)
    * 参数:
        1.exchange:交换机名称
        2.type:交换机类型
            direct:定向
            fanout:扇形(广播),放消息到每一个与之绑定队列
            topic:通配符的方式
            * headers:阐述匹配
         3.durable:是否持久化
         4.autoDelete:自动删除
         5.internal:内部使用。一般false
         6.arguments:参数
    * */
    String exchange = "test_fanout";
    channel.exchangeDeclare(exchange, BuiltinExchangeType.FANOUT, true, false, false, null);

    //6.创建队列
    String queue1name = "fanout_queue_1";
    String queue2name = "fanout_queue_2";
    channel.queueDeclare(queue1name, true, false, false, null);
    channel.queueDeclare(queue2name, true, false, false, null);

    //7.绑定队列与交换机
    /*
    * queueBind(String queue, String exchange, String routingKey)
    * 参数:
    * 1.queue:队列名称
    * 2.exchange:交换机名称
    * 3.routingKey:路由键,绑定规则
    *   如果交换机的类型为fanout,routingKey设置为“”
    * */
    channel.queueBind(queue1name,exchange,"");
    channel.queueBind(queue2name,exchange,"");

    //8.发送消息
    String body="日志信息:张三调用了findAll方法...日志级别:info...";
    channel.basicPublish(exchange,"",null,body.getBytes());

    //9. 释放资源
    channel.close();
    connection.close();

}
}

消费者c1

package com;

import com.rabbitmq.client.*;

public class Consumer_PubSub1 {
    public static void main(String[] args) throws Exception {

        //1.创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();

        //2.设置参数
        factory.setHost("192.168.235.129"); //ip 默认值localhost
        factory.setPort(5672); //端口 默认值5672
        factory.setVirtualHost("/ljb"); //虚拟机 默认值/
        factory.setUsername("ljb");//用户名 默认guest
        factory.setPassword("ljb");//密码 默认值guest

        //3.创建连接 Connection
        Connection connection = factory.newConnection();

        //4.创建Channel
        Channel channel = connection.createChannel();

        String queue1name = "fanout_queue_1";
        String queue2name = "fanout_queue_2";

        //6.接收信息
        Consumer consumer = new DefaultConsumer(channel){

            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) {
                /*System.out.println("consumerTag:" + consumerTag);
                System.out.println("Exchange:" + envelope.getExchange());
                System.out.println("RoutingKey:" + envelope.getRoutingKey());
                System.out.println("properties:" + properties);*/
                System.out.println("body:" + new String(body));
                System.out.println("将日志信息打印到控制台...");
            }
        };
        //消费者接受交换机中哪个队列的消息在这里指定!!!
        channel.basicConsume(queue1name, true, consumer);

        //7.关闭资源?不要 (消费者需要监听生产者消息,这里不关闭)
    }
}

消费者c2 

package com;

import com.rabbitmq.client.*;

public class Consumer_PubSub2 {
    public static void main(String[] args) throws Exception {

        //1.创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();

        //2.设置参数
        factory.setHost("192.168.235.129"); //ip 默认值localhost
        factory.setPort(5672); //端口 默认值5672
        factory.setVirtualHost("/ljb"); //虚拟机 默认值/
        factory.setUsername("ljb");//用户名 默认guest
        factory.setPassword("ljb");//密码 默认值guest

        //3.创建连接 Connection
        Connection connection = factory.newConnection();

        //4.创建Channel
        Channel channel = connection.createChannel();

        String queue1name = "fanout_queue_1";
        String queue2name = "fanout_queue_2";
        /*
           basicConsume(String queue, boolean autoAck, Consumer callback)
           参数:
           1.String queue:队列名称
           2.boolean autoAck:是否自动确认
           3.Consumer callback:回调对象
         */

        //6.接收信息
        Consumer consumer = new DefaultConsumer(channel){
            /*
               回调方法,当收到消息后,会自动执行该方法
               void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
                String consumerTag:标识
                Envelope envelope:获取一些信息,交换机,路由key...
                AMQP.BasicProperties properties:配置信息
                byte[] body:数据
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) {
                /*System.out.println("consumerTag:" + consumerTag);
                System.out.println("Exchange:" + envelope.getExchange());
                System.out.println("RoutingKey:" + envelope.getRoutingKey());
                System.out.println("properties:" + properties);*/
                System.out.println("body:" + new String(body));
                System.out.println("将日志信息保存数据库...");
            }
        };

        channel.basicConsume(queue2name, true, consumer);

        //7.关闭资源?不要 (消费者需要监听生产者消息,这里不关闭)
    }
}

6.Routing 路由模式

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

消息的发送方在向 Exchange 发送消息时,也必须指定消息的 RoutingKey

Exchange 不再把消息交给每一个绑定的队列,而是根据消息的 Routing Key 进行判断,只有队列的Routingkey 与消息的 Routing key 完全一致,才会接收到消息

生产者

package com;

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

public class Producer_Routing {

    public static void main(String[] args) throws Exception {
        //1.创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();

        //2.设置参数
        factory.setHost("192.168.235.129"); //ip 默认值localhost
        factory.setPort(5672); //端口 默认值5672
        factory.setVirtualHost("/ljb"); //虚拟机 默认值/
        factory.setUsername("ljb");   //用户名 默认guest
        factory.setPassword("ljb");   //密码 默认值guest

        //3.创建连接 Connection
        Connection connection = factory.newConnection();

        //4.创建Channel
        Channel channel = connection.createChannel();
        //5.创建队列
        //创建交换机
        String exchange = "exchange_direct";
        channel.exchangeDeclare(exchange, BuiltinExchangeType.DIRECT, true, false, false, null);

        //创建队列
        String queue1name = "direct_queue_1";
        String queue2name = "direct_queue_2";
        channel.queueDeclare(queue1name, true, false, false, null);
        channel.queueDeclare(queue2name, true, false, false, null);

        //队列与交换机绑定
        //队列1绑定 error
        channel.queueBind(queue1name, exchange, "error");
        //队列2绑定 info error warning
        channel.queueBind(queue2name, exchange, "info");
        channel.queueBind(queue2name, exchange, "error");
        channel.queueBind(queue2name, exchange, "warning");
        //发送消息
        String body = "日志信息:张三调用了findAll方法...日志级别:info...";
        channel.basicPublish(exchange, "error", null, body.getBytes());
        //9. 释放资源
        channel.close();
        connection.close();

    }
}


 消费者1

package com;

import com.rabbitmq.client.*;

import java.io.IOException;

public class Consumer_Routing1 {
    public static void main(String[] args) throws Exception {

        //1.创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();

        //2.设置参数
        factory.setHost("192.168.235.129"); //ip 默认值localhost
        factory.setPort(5672); //端口 默认值5672
        factory.setVirtualHost("/ljb"); //虚拟机 默认值/
        factory.setUsername("ljb");//用户名 默认guest
        factory.setPassword("ljb");//密码 默认值guest

        //3.创建连接 Connection
        Connection connection = factory.newConnection();

        //4.创建Channel
        Channel channel = connection.createChannel();

        String queue1name = "direct_queue_1";
        String queue2name = "direct_queue_2";

        //5.接收消息
        Consumer consumer = new DefaultConsumer(channel){

            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("body:"+new String(body));
                System.out.println("将日志信息打印到控制台");
            }
        };
        channel.basicConsume(queue2name,true,consumer);

    }
}

消费者2

package com;

import com.rabbitmq.client.*;

import java.io.IOException;

public class Consumer_Routing2 {
    public static void main(String[] args) throws Exception {

        //1.创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();

        //2.设置参数
        factory.setHost("192.168.235.129"); //ip 默认值localhost
        factory.setPort(5672); //端口 默认值5672
        factory.setVirtualHost("/ljb"); //虚拟机 默认值/
        factory.setUsername("ljb");//用户名 默认guest
        factory.setPassword("ljb");//密码 默认值guest

        //3.创建连接 Connection
        Connection connection = factory.newConnection();

        //4.创建Channel
        Channel channel = connection.createChannel();

        String queue1name = "direct_queue_1";
        String queue2name = "direct_queue_2";

        //5.接收消息
        Consumer consumer = new DefaultConsumer(channel){

            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("body:"+new String(body));
                System.out.println("将日志信息存储到数据库");
            }
        };
        channel.basicConsume(queue1name,true,consumer);

    }
}

结果:生产者生产队列指定了不同的RoutingKey,消费者在接受消息时,指定的不同的队列名都和指定的RoutingKey有关,实现接受不同队列消息后的不同业务操作

小结Routing模式要求队列在绑定交换机时要指定routing key,消息会转发到符合routing key的队列 

7.Topics 通配符模式

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

Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert 通配符规则:

#匹配一个或多个词, * 匹配不多不少恰好1个词

例如:

ljb.#  能够匹配  ljb.insert.abc  或者  ljb.insert,
ljb.*  只能匹配  ljb.insert

生产者

package com;

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

/*
发送消息
*/
public class Producer_Topics {
public static void main(String[] args) throws Exception{

    //1.创建连接工厂
    ConnectionFactory factory = new ConnectionFactory();

    //2.设置参数
    factory.setHost("192.168.235.129"); //ip 默认值localhost
    factory.setPort(5672); //端口 默认值5672
    factory.setVirtualHost("/ljb"); //虚拟机 默认值/
    factory.setUsername("ljb");   //用户名 默认guest
    factory.setPassword("ljb");   //密码 默认值guest

    //3.创建连接 Connection
    Connection connection = factory.newConnection();

    //4.创建Channel
    Channel channel = connection.createChannel();

    //5.创建交换机
    String exchange = "exchange_topic";
    channel.exchangeDeclare(exchange, BuiltinExchangeType.TOPIC, true, false, false, null);

    //6.创建队列
    String queue1name = "topic_queue_1";
    String queue2name = "topic_queue_2";
    channel.queueDeclare(queue1name, true, false, false, null);
    channel.queueDeclare(queue2name, true, false, false, null);

    //7.队列与交换机绑定
    //需求:所有error级别的日志存入数据库,所有order系统的日志存入sjk
    channel.queueBind(queue1name, exchange, "#.error");
    channel.queueBind(queue1name, exchange, "order.*");
    channel.queueBind(queue2name, exchange, "*.*");
    //8.发送消息
    String body = "日志信息:张三调用了findAll方法...日志级别:info...";
    channel.basicPublish(exchange, "order.info", null, body.getBytes());
    //9. 释放资源
    channel.close();
    connection.close();

}
}

消费者1 

package com;

import com.rabbitmq.client.*;

import java.io.IOException;

public class Consumer_Topic1 {
    public static void main(String[] args) throws Exception {

        //1.创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();

        //2.设置参数
        factory.setHost("192.168.235.129"); //ip 默认值localhost
        factory.setPort(5672); //端口 默认值5672
        factory.setVirtualHost("/ljb"); //虚拟机 默认值/
        factory.setUsername("ljb");//用户名 默认guest
        factory.setPassword("ljb");//密码 默认值guest

        //3.创建连接 Connection
        Connection connection = factory.newConnection();

        //4.创建Channel
        Channel channel = connection.createChannel();

        String queue1name = "topic_queue_1";
        String queue2name = "topic_queue_2";

        //5.接收消息
        Consumer consumer = new DefaultConsumer(channel){

            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("body:"+new String(body));
                System.out.println("将日志信息存入数据库");
            }
        };
        channel.basicConsume(queue1name,true,consumer);

    }
}

消费者2

package com;

import com.rabbitmq.client.*;

import java.io.IOException;

public class Consumer_Topic2 {
    public static void main(String[] args) throws Exception {

        //1.创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();

        //2.设置参数
        factory.setHost("192.168.235.129"); //ip 默认值localhost
        factory.setPort(5672); //端口 默认值5672
        factory.setVirtualHost("/ljb"); //虚拟机 默认值/
        factory.setUsername("ljb");//用户名 默认guest
        factory.setPassword("ljb");//密码 默认值guest

        //3.创建连接 Connection
        Connection connection = factory.newConnection();

        //4.创建Channel
        Channel channel = connection.createChannel();

        String queue1name = "topic_queue_1";
        String queue2name = "topic_queue_2";

        //5.接收消息
        Consumer consumer = new DefaultConsumer(channel){

            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("body:"+new String(body));
                System.out.println("将日志信息打印控制台");
            }
        };
        channel.basicConsume(queue2name,true,consumer);

    }
}

小结:Topic 主题模式可以实现 Pub/Sub 发布与订阅模式和 Routing 路由模式的功能,只是 Topic 在配置routing key 的时候可以使用通配符,显得更加灵活 

8.五大模式总结

简单模式:利用频道直接声明队列和发送消息,直观上就是直接把消息丢到一个队列区

工作队列模式:和简单模式的区别就是在一个队列里创建了多个信息

供给多个消费者获取,但是消费者是竞争关系,有顺序的竞争

Pub/Sub 订阅模式:引入了交换机,绑定队列,相当于把队列交给交换机管理,交换机的类型是BuiltinExchangeType.FANOUT,发送消息的时候RoutingKey是" "(空字符串)

Routing 路由模式:和订阅模式的区别是交换机类型是BuiltinExchangeType.DIRECT,然后在绑定队列的时候还加入了多个Routing Key,把队列和Routingkey关联;在发送消息加入队列名和RoutingKey,也就指定了消息是发到了哪个队列的哪个RoutingKey上

topic 通配符模式:和路由模式类似,区别在于交换机的类型是BuiltinExchangeType.TOPIC,然后在绑定队列的时候的RoutingKey的写法引入通配符,也是将队列与RoutingKey进行关联

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值