MQ

1MQ

1.1MQ概述

1.1.1MQ基本概念

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

1.1.2MQ的优势和劣势

在项目中,可将一些无需即时返回且耗时的操作提取出来,进行异步处理,而这种异步处理的方式大大的节省了服务器的请求响应时间,从而提高了系统的吞吐量。

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

优势:

  • 应用解耦

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

  • 异步提速:

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

  • 削峰限流:减少高峰时期对服务器压力

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

消息被MQ保存起来了,然后系统就可以按照自己的消费能力来消费,比如每秒1000个数据,这样慢慢写入数据库,这样就不会卡死数据库了。但是使用了MQ之后,限制消费消息的速度为1000,但是这样一来,高峰期产生的数据势必会被积压在MQ中,高峰就被“削”掉了。但是因为消息积压,在高峰期过后的一段时间内,消费消息的速度还是会维持在1000QPS,直到消费完积压的消息,这就叫做“填谷”。

劣势:

  • 系统可用性降低

系统引入的外部依赖越多,系统稳定性越差。一旦 MQ 宕机,就会对业务造成影响。如何保证MQ的高可用?

  • 系统复杂度提高

MQ 的加入大大增加了系统的复杂度,以前系统间是同步的远程调用,现在是通过 MQ 进行异步调用。如何保证消息没有被重复消费?怎么处理消息丢失情况?那么保证消息传递的顺序性?

  • 一致性问题

A 系统处理完业务,通过 MQ 给B、C、D三个系统发消息数据,如果 B 系统、C 系统处理成功,D 系统处理失败。如何保证消息数据处理的一致性?

1.1.3常见的MQ产品

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

1.1.4RabbitMQ简介

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

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

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,在消息量大的时候建立 TCP Connection的开销将是巨大的,效率也较低。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) and fanout (multicast)
  • Queue:消息最终被送到这里等待 consumer 取走
  • Binding:exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key。Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据
  • RabbitMQ 提供了 6 种工作模式:简单模式、work queues、Publish/Subscribe 发布与订阅模式、Routing 路由模式、Topics 主题模式、RPC 远程调用模式(远程调用,不太算 MQ;暂不作介绍)。

1.1.5JMS

  • JMS 即 Java 消息服务(JavaMessage Service)应用程序接口,是一个 Java 平台中关于面向消息中间件的API
  • JMS 是 JavaEE 规范中的一种,类比JDBC
  • 很多消息中间件都实现了JMS规范,例如:ActiveMQ。RabbitMQ 官方没有提供 JMS 的实现包,但是开源社区有。

1.2RabbitMQ的安装和配置

Windows版本安装:注意点:如果windows的C:\Users下的用户是中文名称,启动安装好的RabbitMQ会失败,解决方法:用户修改成英文名称而且不能有空格和特殊字符,要么安装linux版本的RabbitMQ

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

Rabbitmq是基于ERLANG语言写的,在安装rabbitmq之前,需要先安装erlang。

每个版本的RabbitMQ都对Erlang的版本有一定的要求,具体的版本支持的信息可以在以下页面查看。

Erlang安装包下载网址:https://www.erlang.org/downloads

查看rabbit与erlang的版本:https://www.rabbitmq.com/which-erlang.html

1.2.1安装Erlang

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

安装时注意,创建一个没有空格和中文的路径安装。

安装成功以后,配置环境变量:

将环境变量追加至path:

打开命令行窗口,键入erl:

代表安装成功。

1.2.2安装rabbitmq

Rabbitmq的安装可以采用exe形式,也可以采用解压缩形式。这里采用exe形式安装windows版本。

先下载:

点击下一步安装即可。

安装成功,配置环境变量:

将环境变量追加至path

1.2.3启动rabbitmq管理插件

RabbitMQ的安装包默认是没有启用任何插件的,所以我们需要运行以下命令来启用RabbmitMQ的管理插件

rabbitmq-plugins.bat enable rabbitmq_management

 

出现以上页面说明管理插件启用成功,需要重启RabbitMQ服务后生效。

1.2.4访问管理页面

浏览器中访问http://localhost:15672/

默认用户名:guest,密码:guest

1.2.5linux安装RabbitMQ

Centos7安装RabbitMQ

下载rpm包:一个erlang环境,一个rabbitmq-server

  • 上传安装包到Centos
  • 安装Erlang、RabbitMQ

rpm -ivh erlang-22.0.7-1.el7.x86_64.rpm

yum install -y rabbitmq-server-3.7.18-1.el7.noarch.rpm

     

  • 默认安装完成后配置文件模板在:

/usr/share/doc/rabbitmq-server-3.7.18/rabbitmq.config.example

配置文件模板拷贝到/etc/rabbitmq目录下并修改配置文件名称

cp /usr/share/doc/rabbitmq-server-3.7.18/rabbitmq.config.example /etc/rabbitmq/

 mv /etc/rabbitmq/rabbitmq.config.example /etc/rabbitmq/rabbitmq.config

       修改配置文件

vi /etc/rabbitmq/rabbitmq.config

 

保存并退出

  • 启动rabbitmq插件管理

rabbitmq-plugins enable rabbitmq_management

        
  • 启动RabbitMQ

systemctl start rabbitmq-server # 启动rabbitmq服务
systemctl restart rabbitmq-server # 重启服务
systemctl stop rabbitmq-server  # 停止服务

systemctl status rabbitmq-server #查看状态

6、访问web管理界面
输入你的 ip:15672
进入如下界面
用户名是:guest
密码也是:guest

1.3模式介绍

1.3.1简单模式

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

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

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

RabbitMQ添加vhost

创建生产者工程和消费者工程:

创建maven工程,导入依赖:

<dependencies>
    <dependency>
        <groupId>com.rabbitmq</groupId>
        <artifactId>amqp-client</artifactId>
        <version>5.12.0</version>
    </dependency>
</dependencies>

创建生产者:

package com.tjetc;

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

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class Producer {
    public static void main(String[] args) throws IOException, TimeoutException {
        /*
        1、设置主机ip:默认为localhost
        2、设置虚拟主机名称
        3、设置连接用户名和密码,默认为guest
        4、连接端口:默认为 5672*/
        //创建连接工厂(ConnectionFactory)对象
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        factory.setVirtualHost("carat");
        factory.setUsername("guest");
        factory.setPassword("guest");
        factory.setPort(5672);
        //创建连接
        Connection connection = factory.newConnection();
        //创建通道
        Channel channel = connection.createChannel();
        /*
        参数1:队列名称
        参数2:是否定义持久化队列,关闭以后重启是不是保存数据
        参数3:是否独占本次连接,是否只允许一个消费端连接
        参数4:是否在不使用的时候自动删除队列
        参数5:队列其它参数*/
        //声明(创建)队列
        channel.queueDeclare("simple_queue", true, false, false, null);
        //发布消息给队列
        String msg = "Hello Rabbit";
        /*
            参数1:交换机名称,如果没有指定(空字符串)则使用默认交换机 Default Exchange
            参数2:路由(键)key,简单模式(无路由key)可以传递队列名称
            参数3:消息其它属性
            参数4:消息体(内容)
        */
        channel.basicPublish("", "simple_queue", null, msg.getBytes());
        // 关闭资源
        channel.close();
        connection.close();
    }
}

封装重复代码,封装工具类:

package com.tjetc.producer.common;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/*
封装工具类
*/
public class AmqpClientUtils {
    private static ConnectionFactory factory;
    static {
        /*
        1、设置主机ip:默认为localhost
        2、设置虚拟主机名称
        3、设置连接用户名和密码,默认为guest
        4、设置连接端口:默认为 5672*/
        //创建连接工厂(ConnectionFactory)对象
        factory = new ConnectionFactory();
        factory.setHost("localhost");
        factory.setVirtualHost("carat");
        factory.setUsername("guest");
        factory.setPassword("guest");
        factory.setPort(5672);
    }
    //创建连接
    public static Connection getConnection() throws IOException, TimeoutException {
        return factory.newConnection();
    }
}

创建消费者:

package com.tjetc.consumer;
import com.rabbitmq.client.*;
import com.tjetc.consumer.common.AmqpClientUtils;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer {
    public static void main(String[] args) throws IOException, TimeoutException {
        //创建连接
        Connection connection = AmqpClientUtils.getConnection();
        //创建队列
        Channel channel = connection.createChannel();
        //定义队列
        channel.queueDeclare("simple_queue", true, false, false, null);
        //创建对象DefaultConsumer,处理消息
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            /*
             consumerTag 消息者标签,在channel.basicConsume时候可以指定
             envelope 消息包的内容
                      可从中获取消息id,routingKey,交换机,消息和重传标志(收到消息失败后是否需要重新发送)
             properties 属性信息
             body 消息
             */
            @Override
            public void handleDelivery(String consumerTag,
                                       Envelope envelope,
                                       AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                String exchange = envelope.getExchange();
                System.out.println(exchange);//交换机
                String routingKey = envelope.getRoutingKey();
                System.out.println(routingKey);//路由键(key)
                String msg = new String(body, "utf-8");
                System.out.println(msg);//处理消息
            }
        };
        /*
        参数1:队列名称
        参数2:是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,
                          设置为false则需要手动确认
        参数3:消息接收到后回调
        */
        //监听消息
        channel.basicConsume("simple_queue", true, consumer);
        //关闭资源
        channel.close();
        connection.close();
    }
}

分别启动生产者和消费者。消费端接收到消息。

1.3.2工作模式

  • Work Queues:多了一个或一些消费端,多个消费端共同消费同一个队列中的消息。
  • 应用场景:对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度。
  • Work Queues 与入门程序的简单模式的代码几乎是一样的。可以完全复制,并多复制一个消费者进行多个消费者同时对消费消息的测试。
  • 在一个队列中如果有多个消费者,那么消费者之间对于同一个消息的关系是竞争的关系。
  • Work Queues 对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度。例如:短信服务部署多个,只需要有一个节点成功发送即可。
//如果没有一个名字叫work_queue的队列,则会创建该队列,如果有则不会创建
channel.queueDeclare("work_queue", true, false, false, null);
  • 创建生产者,与上例相同:
public class Producer_WorkQueues {
    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = AmqpClientUtils.getConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare("work_queue", true, false, false, null);
        for (int i = 0; i < 17; i++) {
            String msg = "第" + i + "次发送:" + "Hello Rabbit";
            channel.basicPublish("", "work_queue", null, msg.getBytes());
        }
        channel.close();
        connection.close();
    }
}
  • 创建多个消费者

编辑配置,勾选允许并行运行

public class Consumer_WorkQueues {
    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = AmqpClientUtils.getConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare("work_queue", true, false, false, null);
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag,
                                       Envelope envelope,
                                       AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("message" + msg);
            }
        };
        channel.basicConsume("work_queue", true, consumer);
    }
}

启动三个消费者,再启动生产者。结果如下,17条消息,分别被消费。

   三个消费者

生产者

消费前

消费后

1.3.3发布/订阅模式

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

  • P:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给X(交换机)
  • C:消费者,消息的接收者,会一直等待消息到来
  • Queue:消息队列,接收消息、缓存消息
  • Exchange:交换机(X)。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。Exchange有常见以下3种类型:
    • Fanout:广播,将消息交给所有绑定到交换机的队列
    • Direct:定向,把消息交给符合指定routing key 的队列
    • Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列

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

  • 交换机需要与队列进行绑定,绑定之后;一个消息可以被多个消费者都收到。
  • 发布订阅模式与工作队列模式的区别:
    • 工作队列模式不用定义交换机,而发布/订阅模式需要定义交换机
    • 发布/订阅模式的生产方是面向交换机发送消息,工作队列模式的生产方是面向队列发送消息(底层使用默认交换机)
    • 发布/订阅模式需要设置队列和交换机的绑定,工作队列模式不需要设置,实际上工作队列模式会将队列绑定到默认的交换机

创建生产者:

public class Producer_PubSub {
    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = AmqpClientUtils.getConnection();
        Channel channel = connection.createChannel();
        /*参数:
        1. exchange:交换机名称
        2. type:交换机类型
            DIRECT("direct"),:定向
            FANOUT("fanout"),:扇形(广播),发送消息到每一个与之绑定队列。
            TOPIC("topic"),通配符的方式
            HEADERS("headers");参数匹配
        3. durable:是否持久化
        4. autoDelete:是否自动删除
        5. 其他参数
        */
        //定义交换机
        channel.exchangeDeclare("fanout_exchange", BuiltinExchangeType.FANOUT, true, false, null);
        //定义两个队列
        channel.queueDeclare("fanout_queue1", true, false, false, null);
        channel.queueDeclare("fanout_queue2", true, false, false, null);
        /*  参数:
            1. queue:队列名称
            2. exchange:交换机名称
            3. routingKey:路由键,绑定规则
               如果交换机的类型为fanout ,routingKey设置为""
        */
        //交换机与两个队列绑定
        channel.queueBind("fanout_queue1", "fanout_exchange", "");
        channel.queueBind("fanout_queue2", "fanout_exchange", "");
        //投递数据到交换机
        String msg = "广播模式交换机,发布订阅";
        channel.basicPublish("fanout_exchange", "", null, msg.getBytes());
        //关闭资源
        channel.close();
        connection.close();
    }
}

创建2个消费者

消费者1:

public class Consumer_PubSub1 {
    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = AmqpClientUtils.getConnection();
        Channel channel = connection.createChannel();
        //接收消息,创建DefaultConsumer匿名类对象
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag,
                                       Envelope envelope,
                                       AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("message:" + msg);
            }
        };
        //消费
        channel.basicConsume("fanout_queue1", true, consumer);
        //不关闭资源
    }
}

消费后:

消费者2:

public class Consumer_PubSub2 {
    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = AmqpClientUtils.getConnection();
        Channel channel = connection.createChannel();
        //接收消息,创建DefaultConsumer匿名类对象
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag,
                                       Envelope envelope,
                                       AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("message:" + msg);
            }
        };
        //消费
        channel.basicConsume("fanout_queue2", true, consumer);
        //不关闭资源
    }
}

消费后:

分别启动生产者,消费者1和消费者2,发现两个消费只会消费相同的消息,只是不同的消费者执行的操作不同。

1.3.4路由模式

  • 队列与交换机的绑定,不能是任意绑定了,而是要指定一个 RoutingKey(路由key)
  • 消息的发送方在向 Exchange 发送消息时,也必须指定消息的 RoutingKey
  • Exchange 不再把消息交给每一个绑定的队列,而是根据消息的 Routing Key 进行判断,只有队列的Routingkey 与消息的 Routing key 完全一致,才会接收到消息
  • P:生产者,向 Exchange 发送消息,发送消息时,会指定一个routing key
  • X:Exchange(交换机),接收生产者的消息,然后把消息递交给与 routing key 完全匹配的队列
  • C1:消费者,其所在队列指定了需要 routing key 为 error 的消息
  • C2:消费者,其所在队列指定了需要 routing key 为 info、error、warning 的消息

创建生产者:

public class Producer_Routing {
    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = AmqpClientUtils.getConnection();
        Channel channel = connection.createChannel();
        /*参数:
        1. exchange:交换机名称
        2. type:交换机类型
            DIRECT("direct"),:定向
            FANOUT("fanout"),:扇形(广播),发送消息到每一个与之绑定队列。
            TOPIC("topic"),通配符的方式
            HEADERS("headers");参数匹配
        3. durable:是否持久化
        4. autoDelete:自动删除
        5. internal:内部使用。 一般false
        6. arguments:参数*/
        //定义交换机
        channel.exchangeDeclare("direct_exchange", BuiltinExchangeType.DIRECT,
                true, false, false, null);
        //定义两个队列
        channel.queueDeclare("direct_queue1", true, false, false, null);
        channel.queueDeclare("direct_queue2", true, false, false, null);
        //绑定队列和交换机//队列1绑定 error
        channel.queueBind("direct_queue1", "direct_exchange", "error");
        //队列2绑定 info  error  warning
        channel.queueBind("direct_queue2", "direct_exchange", "info");
        channel.queueBind("direct_queue2", "direct_exchange", "error");
        channel.queueBind("direct_queue2", "direct_exchange", "warning");
        //发送消息
        String msg = "路由模式交换机";
        channel.basicPublish("direct_exchange", "info", null, msg.getBytes());
        //关闭资源
        channel.close();
        connection.close();
    }
}

创建消费者1:

public class Consumer_Routing1 {
    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = AmqpClientUtils.getConnection();
        Channel channel = connection.createChannel();
        //接收消息,创建DefaultConsumer匿名类对象
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag,
                                       Envelope envelope,
                                       AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("message:" + msg);
            }
        };
        //消费
        channel.basicConsume("direct_queue1", true, consumer);
        //不关闭资源
    }
}

创建消费者2:

public class Consumer_Routing2 {
    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = AmqpClientUtils.getConnection();
        Channel channel = connection.createChannel();
        //接收消息,创建DefaultConsumer匿名类对象
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag,
                                       Envelope envelope,
                                       AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("message:" + msg);
            }
        };
        //消费
        channel.basicConsume("direct_queue2", true, consumer);
        //不关闭资源
    }
}

 

1.3.5Topics通配符模式

  • Topic 类型与 Direct 相比,都是可以根据 RoutingKey 把消息路由到不同的队列。只不过 Topic 类型Exchange 可以让队列在绑定 Routing key 的时候使用通配符
  • Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如:insert
  • 通配符规则:# 匹配一个或多个词,* 匹配不多不少恰好1个词,例如:# 能够匹配 item.insert.abc 或者 item.insert,item.* 只能匹配 item.insert

  • 红色 Queue:绑定的是# ,因此凡是以 usa. 开头的 routing key 都会被匹配到
  • 黄色 Queue:绑定的是 #.news ,因此凡是以 .news 结尾的 routing key 都会被匹配

创建生产者:

public class Producer_Topics {
    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = AmqpClientUtils.getConnection();
        Channel channel = connection.createChannel();
        channel.exchangeDeclare("topic_exchange", BuiltinExchangeType.TOPIC,
                true, false, false, null);
        channel.queueDeclare("topic_queue1", true, false, false, null);
        channel.queueDeclare("topic_queue2", true, false, false, null);
        channel.queueBind("topic_queue1", "topic_exchange", "#.error");
        channel.queueBind("topic_queue1", "topic_exchange", "order.*");
        channel.queueBind("topic_queue2", "topic_exchange", "*.*");
        String msg = "通配符交换机";
        channel.basicPublish("topic_exchange", "goods.error", null, msg.getBytes());
        channel.close();
        connection.close();
    }
}

创建消费者1:

public class Consumer_Topic1 {
    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = AmqpClientUtils.getConnection();
        Channel channel = connection.createChannel();
        //接收消息,创建DefaultConsumer匿名类对象
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag,
                                       Envelope envelope,
                                       AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("message:" + msg);
            }
        };
        //消费
        channel.basicConsume("topic_queue1", true, consumer);
        //不关闭资源
    }
}

创建消费者2:

public class Consumer_Topic2 {
    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = AmqpClientUtils.getConnection();
        Channel channel = connection.createChannel();
        //接收消息,创建DefaultConsumer匿名类对象
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag,
                                       Envelope envelope,
                                       AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("message:" + msg);
            }
        };
        //消费
        channel.basicConsume("topic_queue2", true, consumer);
        //不关闭资源
    }
}

启动生产者,消费1和消费者2。

1.4SpringBoot整合RabbitMQ

1.4.1创建生产者工程

分别创建生产者和消费者工程

1.4.2导入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

1.4.3编写yml配置

producer

spring:
  rabbitmq:
    host: localhost
    port: 5672
    virtual-host: carat
    username: guest
    password: guest

consumer

spring:
  rabbitmq:
    host: localhost
    port: 5672
    virtual-host: carat
    username: guest
    password: guest

server:
  port: 8081

1.4.4编写rabbitmq配置类

进行交换机,队列,绑定关系的配置

package com.tjetc.producer.boot.configuration;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitMQConfig {
    //定义交换机
    @Bean("bootExchange")
    public Exchange bootExchange() {
        return ExchangeBuilder.topicExchange("boot_topic_exchange").durable(true).build();
    }
    //定义队列
    @Bean("bootQueue")
    public Queue bootQueue() {
        return QueueBuilder.durable("boot_queue").build();
    }
    //队列与交换机绑定
    @Bean
    public Binding binding(@Qualifier("bootExchange") Exchange exchange,
                           @Qualifier("bootQueue") Queue queue) {
        return BindingBuilder.bind(queue).to(exchange).with("boot.#").noargs();
    }
}

1.4.5注入RabbitTemplate

package com.tjetc.producer.boot;

import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class RabbitmqProducerBootApplicationTests {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    void contextLoads() {
        rabbitTemplate.convertAndSend("boot_topic_exchange",
                "boot.aaa", "springboot --- rabbitmq");
    }

}

运行Producer启动类

1.4.6编写消费端代码

消费端依赖,配置与生产端相同。只需创建一个类,使用@Componet注解注册到ioc容器。

在方法上加@RabbitListener注解,该方法即是获取消息的方法。在注解内指定队列的名称。

方法的参数上可以获取Message对象,即为消息对象。

@Component
public class RabbitMQListener {
    @RabbitListener(queues = {"boot_queue"})
    public void listener(Message message) {
        try {
            byte[] body = message.getBody();
            String msg = new String(body, "utf-8");
            System.out.println("消息体:" + msg);
            System.out.println(message);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }
}

启动消费端程序,即可拿到消息。

1.5RabbitMQ高级特性

1.5.1消息的可靠投递

在使用 RabbitMQ 的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景。RabbitMQ 为我们提供了两种方式用来控制消息的投递可靠性模式。

  • confirm 确认模式
  • return 退回模式

rabbitmq 整个消息投递的路径为:

producer--->rabbitmq broker--->exchange--->queue--->consumer

  • 消息从 producer 到 exchange 则会返回一个 confirmCallback 。
  • 消息从 exchange-->queue 投递失败则会返回一个 returnCallback 。

我们将利用这两个 callback 控制消息的可靠性投递

在生产者工程中设置。

启动生产者到交换机的确认模式:

  1. 开启确认模式:
spring:
  rabbitmq:
    host: localhost
    port: 5672
    virtual-host: carat
    username: guest
    password: guest
    #开启确认模式
    publisher-confirm-type: correlated

 

  1. 在配置类中编写回调

确认回调是rabbitTemplate对象设置的。首先获取rabbitTemplate对象,获取以后设置回调方法,在方法内提供参数boolean类型b,可以获取是否成功的信息。根据这个信息知道消息是否已经到达交换机。

@Configuration
public class RabbitMQConfig {
    @Autowired
    CachingConnectionFactory factory;
    @Bean
    public RabbitTemplate rabbitTemplate() {
        //创建RabbitTemplate对象
        RabbitTemplate template = new RabbitTemplate(factory);
        //设置确认回调
        template.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            @Override
            public void confirm(CorrelationData correlationData, boolean b, String s) {
                if (b) {
                    System.out.println("消息发送成功");
                } else {
                    System.out.println("消息发送失败");
                    System.out.println(s);
                }
            }
        });
        return template;
    }
    //定义交换机
    @Bean("bootExchange")
    public Exchange bootExchange() {
        return ExchangeBuilder.topicExchange("boot_topic_exchange").durable(true).build();
    }
    //定义队列
    @Bean("bootQueue")
    public Queue bootQueue() {
        return QueueBuilder.durable("boot_queue").build();
    }
    //队列与交换机绑定
    @Bean
    public Binding binding(@Qualifier("bootExchange") Exchange exchange,
                           @Qualifier("bootQueue") Queue queue) {
        return BindingBuilder.bind(queue).to(exchange).with("boot.#").noargs();
    }
}

在测试类中测试:

@SpringBootTest
class RabbitmqProducerBootApplicationTests {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Test
    void contextLoads() throws InterruptedException {
        rabbitTemplate.convertAndSend("boot_topic_exchange",
                "boot.aaa", "springboot --- rabbitmq");
        //等待2秒结束程序,暂时不关闭通道和连接,不然确认机制代码无法完成
        Thread.sleep(2000);
    }
}

如果成功,则返回消息发送成功:

消息发送成功

如果失败,则返回失败消息:

消息发送失败
channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'boot_topic_exchange1111' in vhost 'carat', class-id=60, method-id=40)

  1. 开启交换机到队列的退回模式:
spring:
  rabbitmq:
    host: localhost
    port: 5672
    virtual-host: carat
    username: guest
    password: guest
    #开启确认模式
    publisher-confirm-type: correlated
    #开启退回模式 
    publisher-returns: true
  1. 编写退回回调
@Configuration
public class RabbitMQConfig {
    @Autowired
    CachingConnectionFactory factory;
    @Bean
    public RabbitTemplate rabbitTemplate() {
        //创建RabbitTemplate对象
        RabbitTemplate template = new RabbitTemplate(factory);
        //设置确认回调
        template.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            @Override
            public void confirm(CorrelationData correlationData, boolean b, String s) {
                if (b) {
                    System.out.println("消息发送成功");
                } else {
                    System.out.println("消息发送失败");
                    System.out.println(s);
                }
            }
        });
        /*mandatory为true
            如果exchange根据自身类型和消息routingKey无法找到一个合适的queue存储消息
            那么broker会调用basic.return方法将消息返还给生产者;
          mandatory设置为false时
            出现上述情况broker会直接将消息丢弃
        */
        template.setMandatory(true);
        //设置退回的回调
        template.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
            @Override
            public void returnedMessage(ReturnedMessage returnedMessage) {
                System.out.println("交换机到队列失败,消息路由失败");
                System.out.println(returnedMessage.getReplyText());
            }
        });
        return template;
    }

测试:只有当交换机到队列失败时,才会执行回退回调:

@SpringBootTest
class RabbitmqProducerBootApplicationTests {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Test
    public void test1() throws InterruptedException {
        rabbitTemplate.convertAndSend("boot_topic_exchange",
                "bbb.aaa", "springboot --- rabbitmq");
        //等待2秒结束程序,暂时不关闭通道和连接,不然确认机制代码无法完成
        Thread.sleep(2000);
    }
}

交换机到队列失败,消息路由失败
NO_ROUTE

消息发送失败,原因是找不到路由。

1.5.2消费端Ack

ack指Acknowledge,确认。 表示消费端收到消息后的确认方式。

有三种确认方式:

  • 自动确认:acknowledge="none"
  • 手动确认:acknowledge="manual"
  • 根据异常情况确认:acknowledge="auto",(这种方式使用麻烦,不作讲解)

其中自动确认是指,当消息一旦被Consumer接收到,则自动确认收到,并将相应 message 从 RabbitMQ 的消息缓存中移除。但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失。如果设置了手动确认方式,则需要在业务处理成功后,调用channel.basicAck(),手动签收,如果出现异常,则调用channel.basicNack()方法,让其自动重新发送消息。

  1. 在消费端配置中开启手动确认:
spring:
  rabbitmq:
    host: localhost
    port: 5672
    virtual-host: carat
    username: guest
    password: guest
    #开启手动确认模式
    listener:
      simple:
        acknowledge-mode: manual

server:
  port: 8081
  1. 消费端编码:
package com.tjetc.consumer.boot.lister;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class RabbitMQListener {
    @RabbitListener(queues = {"boot_queue"})
    public void listener(Message message, Channel channel) throws IOException {
        try {
            int i = 1 / 0;//java.lang.ArithmeticException: / by zero
            byte[] body = message.getBody();
            String msg = new String(body, "utf-8");
            System.out.println("消息体:" + msg);
            System.out.println(message);
            /* void basicAck(long var1, boolean var3) throws IOException;
            1、消息的标签,mq发送的每一条消息,都有一个标签,可以认为是id
            2、在这之前发送的所有消息一并确认(id小于该条消息的,一起确认)
            */
            //消费(执行)成功,手动确认消息执行成功
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            e.printStackTrace();
            /*void basicNack(long var1, boolean var3, boolean var4) throws IOException;
            第三个参数表示是否将消息放回队列*/
            //有异常出现,代表消息处理失败,这是将消息重新返回队列
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
        }
    }
}

1.5.3TTL

TTL 全称 Time To Live(存活时间/过期时间)。

当消息到达存活时间后,还没有被消费,会被自动清除。

RabbitMQ可以对消息设置过期时间,也可以对整个队列(Queue)设置过期时间。

  1. 可以为队列中的消息统一设置过期时间。
//定义交换机
@Bean("bootTTLExchange")
public Exchange bootTTLExchange(){
    return ExchangeBuilder.topicExchange("boot_ttl_topic_exchange").durable(true).build();
}

@Bean("bootTTLQueue")
public Queue bootTTLQueue(){
    //设置队列中所有消息的过期时间
   
return QueueBuilder.durable(“boot_ttl_topic_queue”).ttl(10000).build();
}

//队列与交换机绑定
@Bean
public Binding bindingTTL(@Qualifier("bootTTLExchange") Exchange exchange, @Qualifier("bootTTLQueue") Queue queue){
    return BindingBuilder.bind(queue).to(exchange).with("ttl.#").noargs();
}

 

 

@Test
public void test2() throws InterruptedException {
    for (int i = 0; i < 5; i++) {
        rabbitTemplate.convertAndSend("boot_ttl_topic_exchange",
                "ttl.aaa", "springboot --- rabbitmq");
    }
    //等待2秒结束程序,暂时不关闭通道和连接,不然确认机制代码无法完成
    Thread.sleep(2000);
}

向队列中发送10条消息,10秒后过期

  1. 为某条消息单独设置过期时间。如果两者都进行了设置,以时间短的为准。设置消息过期时间使用参数:expiration。单位:ms(毫秒),当该消息在队列头部时(消费时),会单独判断这一消息是否过期。在发送消息时设置消息的过期时间。

在发送消息时,传入MessagePostProcessor对象,重写postProcessMessage方法,参数message设置过期时间,String类型。

@Test
public void test3() throws InterruptedException {
    for (int i = 0; i < 10; i++) {
        if (i == 0) {
            rabbitTemplate.convertAndSend("boot_ttl_topic_exchange",
                    "ttl.aaa", "springboot --- rabbitmq", new MessagePostProcessor() {
                        @Override
                        public Message postProcessMessage(Message message) throws AmqpException {
                            message.getMessageProperties().setExpiration("5000");
                            return message;
                        }
                    });
        }
        rabbitTemplate.convertAndSend("boot_ttl_topic_exchange",
                "ttl.aaa", "springboot --- rabbitmq");
    }
    //等待2秒结束程序,暂时不关闭通道和连接,不然确认机制代码无法完成
    Thread.sleep(2000);
}

思考:为什么对第一个发送的消息设置过期时间?

1.5.4死信队列

死信队列,英文缩写:DLX  。Dead Letter Exchange(死信交换机),当消息成为Dead message后,可以被重新发送到另一个交换机,这个交换机就是DLX。

消息成为死信的三种情况:

  1. 队列消息长度到达限制;
  2. 消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false;
  3. 原队列存在消息过期设置,消息到达超时时间未被消费;

队列绑定死信交换机:

  1. 死信交换机和死信队列和普通的没有区别
  2. 当消息成为死信后,如果该队列绑定了死信交换机,则消息会被死信交换机重新路由到死信队列。

死信队列的实现:

  1. 创建用于测试死信队列的普通交换机和队列
//定义用于测试死信队列的交换机
@Bean("testDLXExchange")
public Exchange testDLXExchange() {
    return ExchangeBuilder.topicExchange("test_dlx_exchange").durable(true).build();
}
//定义用于测试死信队列的队列
@Bean("testDLXQueue")
public Queue testDLXQueue() {
    /*
    设置队列的最大长度
    设置队列关联的死信交换机
    设置队列关联的死信队列的routingkey
        当队列中的消息成为死信后,投递给死信交换机
    */
    return QueueBuilder.durable("test_dlx_queue").maxLength(10)
            .deadLetterExchange("dlx_exchange")
            .deadLetterRoutingKey("dlx.#").build();
}
//测试死信队列与死信交换机绑定
@Bean
public Binding bindingTestDLX(@Qualifier("testDLXExchange") Exchange exchange,
                              @Qualifier("testDLXQueue") Queue queue) {
    return BindingBuilder.bind(queue).to(exchange).with("test.dlx.#").noargs();
}
  1. 创建死信交换机和队列
//定义死信队列的交换机
@Bean("dlxExchange")
public Exchange dlxExchange() {
    return ExchangeBuilder.topicExchange("dlx_exchange").durable(true).build();
}
//定义死信队列的队列
@Bean("dlxQueue")
public Queue dlxQueue() {
    return QueueBuilder.durable("dlx_queue").build();
}
//死信队列与死信交换机绑定
@Bean
public Binding bindingDLX(@Qualifier("dlxExchange") Exchange exchange,
                          @Qualifier("dlxQueue") Queue queue) {
    return BindingBuilder.bind(queue).to(exchange).with("dlx.#").noargs();
}
  1. 测试死信的第一个条件:

队列的长度达到最大限制。这里创建的普通队列的最大长度为10,即队列最多可存放10条消息。

如果队列中已经存在10条消息没有消费,生产者继续向队列发布消息,则会成为死信,自动加入死信队列。

测试代码:发送15条消息,此时,不开启消费端。则有5条消息会存入死信队列

@Test
void testDLX() {
    for (int i = 0; i < 15; i++) {
        rabbitTemplate.convertAndSend("test_dlx_exchange", "test.dlx.aaa", "springboot --- 消息");
    }
}

启动客户端,客户端既能处理普通队列中的消息,又能处理死信队列中的消息。

@Component
public class RabbitMQListener {
    @RabbitListener(queues = {"boot_queue","test_dlx_queue","dlx_queue"})
    public void listener(Message message, Channel channel) throws IOException {
        try {
            byte[] body = message.getBody();
            String msg = new String(body, "utf-8");
            System.out.println("消息体:" + msg);
            System.out.println(message);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            e.printStackTrace();
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
        }
    }
}
执行结果:其中有10条消息来自普通队列,5条消息来自死信队列
  1. 测试死信的第二个条件消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列

10条消息全部存储在普通队列中

启动消费端,消费失败以后,消息不重回队列 ,注意不能消费dlx_queue死信队列

@Component
public class RabbitMQListener {
    @RabbitListener(queues = {"test_dlx_queue"})
    public void listener(Message message, Channel channel) throws IOException {
        try {
            int i = 1 / 0;//java.lang.ArithmeticException: / by zero
            byte[] body = message.getBody();
            String msg = new String(body, "utf-8");
            System.out.println("消息体:" + msg);
            System.out.println(message);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            e.printStackTrace();
            /*void basicNack(long var1, boolean var3, boolean var4) throws IOException;
            第三个参数表示是否将消息放回队列*/
            //有异常出现,代表消息处理失败,false不放回队列
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
        }
    }
}

15条消息全部进入死信队列:

  1. 测试死信的第三个条件,原队列存在消息过期设置,消息到达超时时间未被消费

发送消息时设置消息的过期时间,或者为队列设置统一的过期时间。设置前3个消息的过期时间为5秒。

@Test
public void test4() throws InterruptedException {
    for (int i = 0; i < 10; i++) {
        if (i == 0 || i == 1 || i == 2) {
            rabbitTemplate.convertAndSend("test_dlx_exchange",
                    "test.dlx.aaa", "springboot --- rabbitmq", new MessagePostProcessor() {
                        @Override
                        public Message postProcessMessage(Message message) throws AmqpException {
                            message.getMessageProperties().setExpiration("5000");
                            return message;
                        }
                    });
        }else{
            rabbitTemplate.convertAndSend("test_dlx_exchange",
                    "test.dlx.aaa", "springboot --- rabbitmq");
        }
    }
    //等待2秒结束程序,暂时不关闭通道和连接,不然确认机制代码无法完成
    Thread.sleep(2000);
}

启动生产者,发布10条消息,5秒钟以后,第一条消息到期,到达死信队列,第2条现在是头消息,也到期,进入死信队列,第3条消息成为头消息,也到期,进入死信队列。剩下7条在普通队列。

1.5.5延迟队列

延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费。

需求:

  1. 下单后,30分钟未支付,取消订单,回滚库存。
  2. 新用户注册成功7天后,发送短信问候。

实现方式:

  1. 定时器
  2. 延迟队列

很可惜,在RabbitMQ中并未提供延迟队列功能。

但是可以使用:TTL+死信队列 组合实现延迟队列的效果。

 

posted @ 2022-07-12 09:03  carat9588  阅读(110)  评论(0编辑  收藏  举报