目录
1、MQ
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
根据下面的教程安装
访问web管理界面
输入你的 ip:15672(例如:192.168.140.195: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 {
//创建连接工厂(ConnectionFactory)对象
ConnectionFactory factory = new ConnectionFactory();
//设置主机ip
factory.setHost("192.168.140.195");
//设置端口
factory.setPort(5672);
//设置虚拟主机
factory.setVirtualHost("bjyx");
//设置账号
factory.setUsername("guest");
//设置密码
factory.setPassword("guest");
//创建新连接
Connection connection = factory.newConnection();
//创建通道
Channel channel = connection.createChannel();
//定义队列
//第一个参数:队列名称
//第二个参数:是否持久化队列
//第三个参数:是否独占连接
//第四个参数:是否在不使用的时候删除队列
//第五个参数:其他参数
channel.queueDeclare("simple_queue", true, false, false, null);
//发布消息给队列
String msg = "hello rabbitmq!";
//第一个参数 交换机名称空字符串表示没有指定交换机,使用默认交换机
//第二个参数 路由键因为没有路由键使用队列名称
//第三个参数 其他参数
//第四个参数 消息体
channel.basicPublish("", "simple_queue", null, msg.getBytes());
//关闭资源
channel.close();
connection.close();
}
}
封装代码
common/AmqpClientUtils
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 {
//创建连接工厂(ConnectionFactory)对象
factory = new ConnectionFactory();
//设置主机ip
factory.setHost("192.168.140.195");
//设置端口
factory.setPort(5672);
//设置虚拟主机
factory.setVirtualHost("bjyx");
//设置账号
factory.setUsername("guest");
//设置密码
factory.setPassword("guest");
}
public static Connection getConnection() throws IOException, TimeoutException {
return factory.newConnection();
}
}
Producer修改
创建消费者:
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) {
/**
*
* @param consumerTag 消费标记
* @param envelope 包装对象,包含交换机、消息id、队列……信息
* @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();
System.out.println("exchange=" + exchange);
//路由键
String routingKey = envelope.getRoutingKey();
System.out.println("routingKey" + routingKey);
//处理消息
String msg = new String(body, "utf-8");
System.out.println("message=" + msg);
}
};
//消费消息
/**
* "simple_queue" 队列名称
* true 是否自动确认
* consumer 处理消息的对象(回调)
*/
channel.basicConsume("simple_queue", true, consumer);
//关闭资源
channel.close();
connection.close();
}
}
分别启动生产者和消费者。消费端接收到消息。
1.3.2工作模式
- Work Queues:多了一个或一些消费端,多个消费端共同消费同一个队列中的消息。
- 应用场景:对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度。
- Work Queues 与入门程序的简单模式的代码几乎是一样的。可以完全复制,并多复制一个消费者进行多个消费者同时对消费消息的测试。
- 1. 在一个队列中如果有多个消费者,那么消费者之间对于同一个消息的关系是竞争的关系。
- 2. Work Queues 对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度。例如:短信服务部署多个,只需要有一个节点成功发送即可。
创建生产者,与上例相同:
package com.tjetc.producer;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.tjetc.producer.common.AmqpClientUtils;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ProducerWork {
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);
//循环投递数据(循环10次)
for (int i = 0; i < 10; i++) {
String msg = i + ",hello rabbitmq";
channel.basicPublish("", "work_queue", null, msg.getBytes());
}
//关闭资源
channel.close();
connection.close();
}
}
创建多个消费者
package com.tjetc.consumer;
import com.rabbitmq.client.*;
import com.tjetc.producer.common.AmqpClientUtils;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ConsumerWork {
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匿名类对象,消费数据的处理逻辑
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);
//不关闭资源,不关闭资源程序不停止,一直监听队列中的数据
}
}
启动多个消费者,再启动生产者。结果如下,消息,分别被多个消费消费。
1.3.3发布/订阅模式
在订阅模型中,多了一个 Exchange 角色,而且过程略有变化:
- P:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给X(交换机)
- C:消费者,消息的接收者,会一直等待消息到来
- Queue:消息队列,接收消息、缓存消息
- Exchange:交换机(X)。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。Exchange有常见以下3种类型:
- Fanout:广播,将消息交给所有绑定到交换机的队列
- Direct:定向,把消息交给符合指定routing key 的队列
- Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列
Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与 Exchange 绑定,或者没有符合路由规则的队列,那么消息会丢失!
1. 交换机需要与队列进行绑定,绑定之后;一个消息可以被多个消费者都收到。
2. 发布订阅模式与工作队列模式的区别:
- 工作队列模式不用定义交换机,而发布/订阅模式需要定义交换机
- 发布/订阅模式的生产方是面向交换机发送消息,工作队列模式的生产方是面向队列发送消息(底层使用默认交换机)
- 发布/订阅模式需要设置队列和交换机的绑定,工作队列模式不需要设置,实际上工作队列模式会将队列绑定到默认的交换机
创建生产者:
package com.tjetc.producer;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.tjetc.producer.common.AmqpClientUtils;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ProducerPubSub {
public static void main(String[] args) throws IOException, TimeoutException {
//创建连接
Connection connection = AmqpClientUtils.getConnection();
//创建通道
Channel channel = connection.createChannel();
//定义交换机
/**
* "fanout_exchange" 交换机的名称
* BuiltinExchangeType.FANOUT 交换机的类型
* true 交换机是否持久化
* false 是否自动删除
* map:null 其他参数
*/
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);
//交换机与队列绑定
/**
* 第一个参数:队列名称
* 第二个参数: 交换机名称
* 第三个参数:绑定路由键(广播模式不需要路由键,所以设置成空字符串)
*/
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:
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 ConsumerPubSub1 {
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);
//不关闭资源
}
}
消费掉队列1
消费者2:
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 ConsumerPubSub2 {
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);
//不关闭资源
}
}
消费掉队列2
分别启动生产者,消费者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 的消息
创建生产者:
package com.tjetc.producer.common;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ProducerRouting {
public static void main(String[] args) throws IOException, TimeoutException {
//创建连接
Connection connection = AmqpClientUtils.getConnection();
//创建通道
Channel channel = connection.createChannel();
//定义交换机
channel.exchangeDeclare("direct_exchange", BuiltinExchangeType.DIRECT, true, false, null);
//定义2个队列
channel.queueDeclare("direct_queue1", true, false, false, null);
channel.queueDeclare("direct_queue2", true, false, false, null);
//队列与交换机的绑定
channel.queueBind("direct_queue1", "direct_exchange", "error");
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:
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 ConsumerRouting1 {
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:
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 ConsumerRouting2 {
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 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
- 通配符规则:# 匹配一个或多个词,* 匹配不多不少恰好1个词,例如:item.# 能够匹配 item.insert.abc 或者 item.insert,item.* 只能匹配 item.insert
- 红色 Queue:绑定的是 usa.# ,因此凡是以 usa. 开头的 routing key 都会被匹配到
- 黄色 Queue:绑定的是 #.news ,因此凡是以 .news 结尾的 routing key 都会被匹配
创建生产者:
package com.tjetc.producer;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.tjetc.producer.common.AmqpClientUtils;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ProducerTopic {
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, null);
//定义2个队列
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 = "topic通配符交换机";
channel.basicPublish("topic_exchange", "goods.error", null, msg.getBytes());
//关闭资源
channel.close();
connection.close();
}
}
创建消费者1:
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 ConsumerTopic1 {
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:
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 ConsumerTopic2 {
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.4SpringBoot整合RabbitMQ
1.4.1创建生产者工程
分别创建生产者和消费者工程
1.4.2导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency><dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
20220709-rabbitmq的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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.tjetc</groupId>
<artifactId>20220709-rabbitmq</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>rabbitmq-consumer</module>
<module>rabbitmq-producer</module>
<module>rabbitmq-producer-boot</module>
<module>rabbitmq-consumer-boot</module>
</modules>
<dependencies>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.12.0</version>
</dependency>
</dependencies>
</project>
consumer-boot的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">
<parent>
<artifactId>20220709-rabbitmq</artifactId>
<groupId>com.tjetc</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>rabbitmq-consumer-boot</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
producer-boot的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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.tjetc</groupId>
<artifactId>20220709-rabbitmq</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>rabbitmq-producer-boot</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
1.4.3编写yml配置
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 exchange() {
return ExchangeBuilder.topicExchange("boot_exchange").durable(true).build();
}
//定义队列
@Bean("bootQueue")
public Queue queue() {
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
public void test1() {
rabbitTemplate.convertAndSend("boot_exchange", "boot.aaa", "springboot整合rabbitmq");
}
}
1.4.6编写消费端代码
消费端依赖,配置与生产端相同。只需创建一个类,使用@Componet注解注册到ioc容器。
在方法上加@RabbitListener注解,该方法即是获取消息的方法。在注解内指定队列的名称。
方法的参数上可以获取Message对象,即为消息对象。
启动消费端程序,即可拿到消息。
package com.tjetc.consumer.boot.listener;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.UnsupportedEncodingException;
@Component
public class RabbitMqListener {
@RabbitListener(queues = {"boot_queue"})
public void messageListener(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();
}
}
}
启动类
package com.tjetc.consumer.boot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class RabbitmqConsumerBootApplication {
public static void main(String[] args) {
SpringApplication.run(RabbitmqConsumerBootApplication.class, args);
}
}
1.5RabbitMQ高级特性
1.5.1消息的可靠投递
在使用 RabbitMQ 的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景。RabbitMQ 为我们提供了两种方式用来控制消息的投递可靠性模式。
- confirm 确认模式
- return 退回模式
rabbitmq 整个消息投递的路径为:
producer--->rabbitmq broker--->exchange--->queue--->consumer
- 消息从 producer 到 exchange 则会返回一个 confirmCallback 。
- 消息从 exchange-->queue 投递失败则会返回一个 returnCallback 。
我们将利用这两个 callback 控制消息的可靠性投递
在生产者工程中设置。
启动生产者到交换机的确认模式:
1、开启确认模式:
在配置类中编写回调
确认回调是rabbitTemplate对象设置的。首先获取rabbitTemplate对象,获取以后设置回调方法,在方法内提供参数boolean类型b,可以获取是否成功的信息。根据这个信息知道消息是否已经到达交换机。
package com.tjetc.producer.boot.configuration;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitMQConfig {
@Autowired
private CachingConnectionFactory cachingConnectionFactory;
@Bean
public RabbitTemplate rabbitTemplate() {
//创建RabbitTemplate模板对象
RabbitTemplate rabbitTemplate = new RabbitTemplate(cachingConnectionFactory);
//设置确认回调
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
if (b) {
System.out.println("correlationData:" + correlationData);
System.out.println("消息发送成功");
} else {
System.out.println("消息发送失败");
System.out.println(s);
}
}
});
return rabbitTemplate;
}
//定义交换机
@Bean("bootExchange")
public Exchange exchange() {
return ExchangeBuilder.topicExchange("boot_exchange").durable(true).build();
}
//定义队列
@Bean("bootQueue")
public Queue queue() {
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.5.2消费端Ack
ack指Acknowledge,确认。 表示消费端收到消息后的确认方式。
有三种确认方式:
- 自动确认:acknowledge="none"
- 手动确认:acknowledge="manual"
- 根据异常情况确认:acknowledge="auto",(这种方式使用麻烦,不作讲解)
其中自动确认是指,当消息一旦被Consumer接收到,则自动确认收到,并将相应 message 从 RabbitMQ 的消息缓存中移除。但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失。如果设置了手动确认方式,则需要在业务处理成功后,调用channel.basicAck(),手动签收,如果出现异常,则调用channel.basicNack()方法,让其自动重新发送消息。
在消费端配置中开启手动确认:
消费端编码:
1.5.3TTL
TTL 全称 Time To Live(存活时间/过期时间)。
当消息到达存活时间后,还没有被消费,会被自动清除。
RabbitMQ可以对消息设置过期时间,也可以对整个队列(Queue)设置过期时间。
1、可以为队列中的消息统一设置过期时间。
向队列中发送10条消息:
2、为某条消息单独设置过期时间。如果两者都进行了设置,以时间短的为准。设置消息过期时间使用参数:expiration。单位:ms(毫秒),当该消息在队列头部时(消费时),会单独判断这一消息是否过期。在发送消息时设置消息的过期时间。
在发送消息时,传入MessagePostProcessor对象,重写postProcessMessage方法,参数message设置过期时间,String类型。
思考:为什么对第一个发送的消息设置过期时间?
只判断队头是否过期,过期就删掉,第二个元素跑到队头再次判断是否过期,依次往下判断。
5秒在第二个位置,5秒后先判断第一个位置是否过期,10秒很明显没有过期,不会删除,一直等待10秒后再删除,第二个数据也不会到队头进行判断,一直等待,10秒后依次到队头进行判断然后删除。
1.5.4死信队列
死信队列,英文缩写:DLX 。Dead Letter Exchange(死信交换机),当消息成为Dead message后,可以被重新发送到另一个交换机,这个交换机就是DLX。
消息成为死信的三种情况:
- 队列消息长度到达限制;
- 消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false;
- 原队列存在消息过期设置,消息到达超时时间未被消费;
队列绑定死信交换机:
- 死信交换机和死信队列和普通的没有区别
- 当消息成为死信后,如果该队列绑定了死信交换机,则消息会被死信交换机重新路由到死信队列。
死信队列的实现:
1、创建用于测试死信队列的普通交换机和队列
2、创建死信交换机和队列
3、测试死信的第一个条件:
队列的长度达到最大限制。这里创建的普通队列的最大长度为10,即队列最多可存放10条消息。如果队列中已经存在10条消息没有消费,生产者继续向队列发布消息,则会成为死信,自动加入死信队列。
测试代码:发送15条消息,此时,不开启消费端。则有5条消息会存入死信队列。
启动客户端,客户端既能处理普通队列中的消息,又能处理死信队列中的消息。
执行结果:其中有10条消息来自普通队列,5条消息来自死信队列
4、测试死信的第二个条件消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列
10条消息全部存储在普通队列中
启动消费端,消费失败以后,消息不重回队列 ,注意不能消费dlx_queue死信队列
15条消息全部进入死信队列:
运行生产者的test后:
运行消费端后:
5、测试死信的第三个条件,原队列存在消息过期设置,消息到达超时时间未被消费,发送消息时设置消息的过期时间,或者为队列设置统一的过期时间。设置前3个消息的过期时间为15秒。
启动生产者,发布10条消息,刚开始10条消息,全部在普通队列。
15秒钟以后,第一条消息到期,到达死信队列,第2条现在是头消息,也到期,进入死信队列,第3条消息成为头消息,也到期,进入死信队列。
1.5.5延迟队列
延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费。
需求:
- 下单后,30分钟未支付,取消订单,回滚库存。
- 新用户注册成功7天后,发送短信问候。
实现方式:
- 定时器
- 延迟队列
很可惜,在RabbitMQ中并未提供延迟队列功能。
但是可以使用:TTL+死信队列 组合实现延迟队列的效果。