MQ全称 Message Queue(消息队列),是在消息的传输过程中保存消息的容器。它是应用程序和应用程序之间的通信方法。
为什么使用MQ?
如此图,mq总结为三个好处
应用解耦
以电商应用为例,应用中有订单系统、库存系统、物流系统、支付系统。用户创建订单后,如果耦合调用库存系统、物流系统、支付系统,若任何一个子系统出了故障,都会造成下单操作异常。当转变成基于消息队列的方式后,系统间调用的问题会减少很多,比如物流系统因为发生故障,需要几分钟来修复。在这几分钟的时间里,物流系统要处理的内容被缓存在消息队列中,用户的下单操作可以正常完成。当物流系统恢复后,继续处理订单信息即可,中间用户感受不到物流系统的故障,提升系统的可用性。
异步提速
使用了mq,用户点击完下单按钮后,只需等待25ms就能得到下单响应 (20 + 5 = 25ms)。
否则就是920ms(300+300+300+20=920ms),mq显然提升了用户体验和系统吞吐量。
削峰填谷
举个例子,如果订单系统最多能处理一千次订单,这个处理能力应付正常时段的下单时绰绰有余,正常时段我们下单一秒后就能返回结果。但是在高峰期,如果有两千次下单操作系统是处理不了的,只能限制订单超过一千后不允许用户下单。使用消息队列做缓冲,我们可以取消这个限制,把一秒内下的订单分散成一段时间来处理,这时有些用户可能在下单十几秒后才能收到下单成功的操作,但是比不能下单的体验要好。 简单来说: 就是在访问量剧增的情况下,但是应用仍然不能停,比如“双十一”下单的人多,但是淘宝这个应用仍然要运行,所以就可以使用消息中间件采用队列的形式减少突然访问的压力,使用了 MQ 之后,限制消费消息的速度为1000,这样一来,高峰期产生的数据势必会被积压在 MQ 中,高峰就被“削”掉了,但是因为消息积压,在高峰期过后的一段时间内,消费消息的速度还是会维持在1000,直到消费完积压的消息,这就叫做“填谷”。提高系统的稳定性。
使用MQ的劣势
系统可用性降低
系统引入的外部依赖越多,系统稳定性越差。一旦 MQ 宕机,就会对业务造成影响。
系统复杂度提高
MQ 的加入大大增加了系统的复杂度,以前系统间是同步的远程调用,现在是通过 MQ 进行异步调用。
一致性问题
A 系统处理完业务,通过 MQ 给B、C、D三个系统发消息数据,如果 B 系统、C 系统处理成功,D 系统处理失败。使用mq后要保证数据处理的一致性。
常见的MQ组件
目前业界有很多的 MQ 产品,例如 RabbitMQ、RocketMQ、ActiveMQ、Kafka、ZeroMQ、MetaMQ等,也有直接使用 Redis 充当消息队列的案例,而这些消息队列产品,各有侧重,在实际选型时,需要结合自身需求及 MQ 产品特征
RabbitMQ的概述
RabbitMQ的概念
RabbitMQ是一个由erlang开发的AMQP(Advanced Message Queue 高级消息队列协议 )的开源实现,由于erlang 语言的高并发特性,性能较好,本质是个队列,FIFO 先入先出,里面存放的内容是message,可复用的企业消息系统,是当前最主流的消息中间件之一。
RabbitMQ的原理
名词解释:
-
Broker:接收和分发消息的应用,RabbitMQ Server就是 Message Broker
-
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 的分发依据
-
Virtual host:出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出多个vhost,每个用户在自己的 vhost 创建 exchange/queue 等
Java使用RabbitMQ
RabbitMQ 提供了 6 种工作模式:简单模式、work queues、Publish/Subscribe 发布与订阅模式、Routing 路由模式、Topics 主题模式、RPC 远程调用模式(远程调用,不太算 MQ;暂不作介绍)。 官网对应模式介绍:RabbitMQ Tutorials — RabbitMQ
安装完RabbitMQ后,启动服务,访问地址ip:15672即可进入登录页面
默认账号密码为guest
提供了一个图形化管理页面。
这里我们使用Java来操作RabbitMQ
首先创建一个maven工程,然后在此工程下再创建两个maven工程,一个名为product,一个名为consumer,整体结构如下,这里介绍五种模式。product生产消息,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.aaa</groupId>
<artifactId>qy168-rabbitmq</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>qy168-product</module>
<module>qy168-consumer</module>
</modules>
<dependencies>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.14.2</version>
</dependency>
</dependencies>
</project>
simple (简单模式)
消息生产者
public class HelloProduct {
public static void main(String[] args) throws Exception {
//1.设置连接对象的信息
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("192.168.184.130");//设rabbitMq主机的地址:默认127.0.0.1 这里为安装rabbitMq服务的ip
factory.setPort(5672);//设置rabbitmq主机的端口号。默认为5672
factory.setUsername("szy"); //设置rabbitmq的账号 默认guest 如果使用默认账号 不用写此行代码
factory.setPassword("123456");//设置rabbitmq的密码 默认guest 如果使用默认账号 不用写此行代码
factory.setVirtualHost("/aaa");//设置虚拟主机 默认 如果使用默认主机账号 不用写此行代码
//2. 获取连接对象
Connection connection=factory.newConnection();
//3. 获取Channel信道
Channel channel = connection.createChannel();
//4. 创建队列--如果队列不存在则创建---如果存在则不创建.
/*
String queue,队列的名称--命名规则就可以
boolean durable, 是否持久化。--
boolean exclusive, 是否独占--当前channel是否独占该队列
boolean autoDelete, 是否自动删除该队列。
Map<String, Object> arguments:该队列的属性参数--null
*/
channel.queueDeclare("hello_queue",true,false,false,null);
//5. 发送消息
/*
String exchange, 交换机的名称--像简单模式没有交换机 ""
String routingKey, 路由key. 像简单模式 默认给定为队列的名称
BasicProperties props,消息的属性--现在给定null.
byte[] body: 消息的内容
*/
String msg="hello rabbitmq lam qy168~~~~~~~~~~~~";
channel.basicPublish("","hello_queue",null,msg.getBytes());
//6.关闭资源。
channel.close();
connection.close();
}
}
消费者监听方
public class HelloConsumer {
public static void main(String[] args) throws IOException, TimeoutException {
//1.设置连接对象的信息
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("192.168.184.130");//设rabbitMq主机的地址:默认127.0.0.1 这里为安装rabbitMq服务的ip
factory.setPort(5672);//设置rabbitmq主机的端口号。默认为5672
factory.setUsername("szy"); //设置rabbitmq的账号 默认guest 如果使用默认账号 不用写此行代码
factory.setPassword("123456");//设置rabbitmq的密码 默认guest 如果使用默认账号 不用写此行代码
factory.setVirtualHost("/aaa");//设置虚拟主机 默认 如果使用默认主机账号 不用写此行代码
//2. 获取连接对象
Connection connection=factory.newConnection();
//3. 获取Channel信道
Channel channel = connection.createChannel();
//4. 创建队列--如果队列不存在则创建---如果存在则不创建.
channel.queueDeclare("hello_queue",true,false,false,null);
//5. 监听消息
/*
String queue, 监听的队列名
boolean autoAck, 是否自动确认。
Consumer callback: 回调函数---当队列中有消息时就会自动触发该对象
*/
DefaultConsumer callback = new DefaultConsumer(channel) {
//一定重写该方法。
/*
String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body: 接受的消息---byte数组
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body);
System.out.println("消息的内容:" + msg);
}
};
channel.basicConsume("hello_queue", true, callback);
//不能关闭connection和channel 关了就监听不了了
}
}
先运行生产者方代码,可以在图形化界面查看到代码生成的交换机exchange和队列queue还有队列所带的message,再运行消费者,就能够收到生产者的消息了。以下几种模式不再赘述。
Work queues(工作模式)
public class WorkProduct {
public static void main(String[] args) throws Exception {
//1.设置连接对象的信息
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("192.168.184.130");//设rabbitMq主机的地址:默认127.0.0.1
factory.setPort(5672);//设置rabbitmq主机的端口号。默认为5672
factory.setUsername("szy"); //设置rabbitmq的账号 默认guest
factory.setPassword("123456");//设置rabbitmq的密码 默认guest
factory.setVirtualHost("/aaa");//设置虚拟主机 默认 /
//2. 获取连接对象
Connection connection=factory.newConnection();
//3. 获取Channel信道
Channel channel = connection.createChannel();
//4. 创建队列--如果队列不存在则创建---如果存在则不创建.
/*
String queue,队列的名称--命名规则就可以
boolean durable, 是否持久化。--
boolean exclusive, 是否独占--当前channel是否独占该队列
boolean autoDelete, 是否自动删除该队列。
Map<String, Object> arguments:该队列的属性参数--null
*/
channel.queueDeclare("work_queue",true,false,false,null);
//5. 发送消息
/*
String exchange, 交换机的名称--像简单模式没有交换机 ""
String routingKey, 路由key. 像简单模式 默认给定为队列的名称
BasicProperties props,消息的属性--现在给定null.
byte[] body: 消息的内容
*/
for (int i = 0; i < 10; i++) {
String msg="work rabbitmq lam qy168~~~~~~~~~~~~"+i;
channel.basicPublish("","work_queue",null,msg.getBytes());
}
//6.关闭资源。
channel.close();
connection.close();
}
}
public class WorkConsumer01 {
public static void main(String[] args) throws IOException, TimeoutException {
//1.设置连接对象的信息
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("192.168.184.130");//设rabbitMq主机的地址:默认127.0.0.1
factory.setPort(5672);//设置rabbitmq主机的端口号。默认为5672
factory.setUsername("szy"); //设置rabbitmq的账号 默认guest
factory.setPassword("123456");//设置rabbitmq的密码 默认guest
factory.setVirtualHost("/aaa");//设置虚拟主机 默认 /
//2. 获取连接对象
Connection connection=factory.newConnection();
//3. 获取Channel信道
Channel channel = connection.createChannel();
//4. 创建队列--如果队列不存在则创建---如果存在则不创建.
channel.queueDeclare("work_queue",true,false,false,null);
//5. 监听消息
/*
String queue, 监听的队列名
boolean autoAck, 是否自动确认。
Consumer callback: 回调函数---当队列中有消息时就会自动触发该对象
*/
DefaultConsumer callback = new DefaultConsumer(channel) {
//一定重写该方法。
/*
String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body: 接受的消息---byte数组
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body);
System.out.println("消息的内容:" + msg);
}
};
channel.basicConsume("work_queue", true, callback);
//不能关闭connection和channel 关了就监听不了了
}
}
public class WorkConsumer02 {
public static void main(String[] args) throws IOException, TimeoutException {
//1.设置连接对象的信息
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("192.168.184.130");//设rabbitMq主机的地址:默认127.0.0.1
factory.setPort(5672);//设置rabbitmq主机的端口号。默认为5672
factory.setUsername("szy"); //设置rabbitmq的账号 默认guest
factory.setPassword("123456");//设置rabbitmq的密码 默认guest
factory.setVirtualHost("/aaa");//设置虚拟主机 默认 /
//2. 获取连接对象
Connection connection=factory.newConnection();
//3. 获取Channel信道
Channel channel = connection.createChannel();
//4. 创建队列--如果队列不存在则创建---如果存在则不创建.
channel.queueDeclare("work_queue",true,false,false,null);
//5. 监听消息
/*
String queue, 监听的队列名
boolean autoAck, 是否自动确认。
Consumer callback: 回调函数---当队列中有消息时就会自动触发该对象
*/
DefaultConsumer callback = new DefaultConsumer(channel) {
//一定重写该方法。
/*
String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body: 接受的消息---byte数组
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body);
System.out.println("消息的内容:" + msg);
}
};
channel.basicConsume("work_queue", true, callback);
//不能关闭connection和channel 关了就监听不了了
}
}
执行代码后会发现,两个消费者轮询接受生产者的消息。
Publish/Subscribe(发布与订阅模式)
在订阅模型中,多了一个 Exchange 角色,而且过程略有变化:
-
P:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给X(交换机)
-
C:消费者,消息的接收者,会一直等待消息到来
-
Queue:消息队列,接收消息、缓存消息
-
Exchange:交换机(X)。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。Exchange有常见以下3种类型:
-
Fanout:广播,将消息交给所有绑定到交换机的队列
-
Direct:定向,把消息交给符合指定routing key 的队列
-
Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列
-
Exchange(交换机):只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange 绑定,或者没有符合路由规则的队列,那么消息会丢失!
public class PublishSubscribeProduct {
public static void main(String[] args) throws IOException, TimeoutException {
//1.设置连接对象的信息
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("192.168.184.130");//设rabbitMq主机的地址:默认127.0.0.1
factory.setPort(5672);//设置rabbitmq主机的端口号。默认为5672
factory.setUsername("szy"); //设置rabbitmq的账号 默认guest
factory.setPassword("123456");//设置rabbitmq的密码 默认guest
factory.setVirtualHost("/aaa");//设置虚拟主机 默认 /
//2. 获取连接对象
Connection connection=factory.newConnection();
//3. 获取Channel信道
Channel channel = connection.createChannel();
//4. 创建交换机
/*
String exchange,交换机的名称
BuiltinExchangeType type, 交换机的类型
boolean durable: 是否持久化
*/
channel.exchangeDeclare("fanout_exchange", BuiltinExchangeType.FANOUT,true);
//5. 创建队列
channel.queueDeclare("fanout_queue01",true,false,false,null);
channel.queueDeclare("fanout_queue02",true,false,false,null);
//6.交换机和队列绑定
/*
String queue,队列名
String exchange,交换机名
String routingKey: 路由key 因为广播模式没有路由key ”“
*/
channel.queueBind("fanout_queue01","fanout_exchange","");
channel.queueBind("fanout_queue02","fanout_exchange","");
//7. 发送消息
String msg="这时一个发布订阅工作模式";
channel.basicPublish("fanout_exchange", "", null, msg.getBytes());
channel.close();
connection.close();
}
}
public class PublishSubscribeConsumer01 {
public static void main(String[] args) throws IOException, TimeoutException {
//1.设置连接对象的信息
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("192.168.184.130");//设rabbitMq主机的地址:默认127.0.0.1
factory.setPort(5672);//设置rabbitmq主机的端口号。默认为5672
factory.setUsername("szy"); //设置rabbitmq的账号 默认guest
factory.setPassword("123456");//设置rabbitmq的密码 默认guest
factory.setVirtualHost("/aaa");//设置虚拟主机 默认 /
//2. 获取连接对象
Connection connection=factory.newConnection();
//3. 获取Channel信道
Channel channel = connection.createChannel();
//4. 创建队列--如果队列不存在则创建---如果存在则不创建.
//channel.queueDeclare("work_queue",true,false,false,null);
//5. 监听消息
/*
String queue, 监听的队列名
boolean autoAck, 是否自动确认。
Consumer callback: 回调函数---当队列中有消息时就会自动触发该对象
*/
DefaultConsumer callback = new DefaultConsumer(channel) {
//一定重写该方法。
/*
String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body: 接受的消息---byte数组
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body);
System.out.println("消息的内容:" + msg);
}
};
channel.basicConsume("fanout_queue01", true, callback);
//不能关闭connection和channel 关了就监听不了了
}
}
public class PublishSubscribeConsumer02 {
public static void main(String[] args) throws IOException, TimeoutException {
//1.设置连接对象的信息
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("192.168.184.130");//设rabbitMq主机的地址:默认127.0.0.1
factory.setPort(5672);//设置rabbitmq主机的端口号。默认为5672
factory.setUsername("szy"); //设置rabbitmq的账号 默认guest
factory.setPassword("123456");//设置rabbitmq的密码 默认guest
factory.setVirtualHost("/aaa");//设置虚拟主机 默认 /
//2. 获取连接对象
Connection connection=factory.newConnection();
//3. 获取Channel信道
Channel channel = connection.createChannel();
//4. 创建队列--如果队列不存在则创建---如果存在则不创建.
//channel.queueDeclare("work_queue",true,false,false,null);
//5. 监听消息
/*
String queue, 监听的队列名
boolean autoAck, 是否自动确认。
Consumer callback: 回调函数---当队列中有消息时就会自动触发该对象
*/
DefaultConsumer callback = new DefaultConsumer(channel) {
//一定重写该方法。
/*
String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body: 接受的消息---byte数组
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body);
System.out.println("消息的内容:" + msg);
}
};
channel.basicConsume("fanout_queue02", true, callback);
//不能关闭connection和channel 关了就监听不了了
}
}
Routing(路由模式)
-
队列与交换机的绑定,不能是任意绑定了,而是要指定一个 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 DirectProduct {
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("192.168.184.130");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("direct_exchange", BuiltinExchangeType.DIRECT,true);
channel.queueDeclare("direct_queue01",true,false,false,null);
channel.queueDeclare("direct_queue02",true,false,false,null);
channel.queueBind("direct_queue01","direct_exchange","error");
channel.queueBind("direct_queue02","direct_exchange","error");
channel.queueBind("direct_queue02","direct_exchange","info");
channel.queueBind("direct_queue02","direct_exchange","warning");
String msg="路由模式发送消息";
channel.basicPublish("direct_exchange","error",null,msg.getBytes());
}
}
public class DirectConsumer01 {
public static void main(String[] args) throws IOException, TimeoutException {
//1.设置连接对象的信息
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("192.168.184.130");//设rabbitMq主机的地址:默认127.0.0.1
//2. 获取连接对象
Connection connection=factory.newConnection();
//3. 获取Channel信道
Channel channel = connection.createChannel();
//4. 创建队列--如果队列不存在则创建---如果存在则不创建.
//channel.queueDeclare("work_queue",true,false,false,null);
//5. 监听消息
/*
String queue, 监听的队列名
boolean autoAck, 是否自动确认。
Consumer callback: 回调函数---当队列中有消息时就会自动触发该对象
*/
DefaultConsumer callback = new DefaultConsumer(channel) {
//一定重写该方法。
/*
String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body: 接受的消息---byte数组
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body);
System.out.println("消息的内容:" + msg);
}
};
channel.basicConsume("direct_queue01", true, callback);
//不能关闭connection和channel 关了就监听不了了
}
}
public class DirectConsumer02 {
public static void main(String[] args) throws IOException, TimeoutException {
//1.设置连接对象的信息
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("192.168.184.130");//设rabbitMq主机的地址:默认127.0.0.1
//2. 获取连接对象
Connection connection=factory.newConnection();
//3. 获取Channel信道
Channel channel = connection.createChannel();
//4. 创建队列--如果队列不存在则创建---如果存在则不创建.
//channel.queueDeclare("work_queue",true,false,false,null);
//5. 监听消息
/*
String queue, 监听的队列名
boolean autoAck, 是否自动确认。
Consumer callback: 回调函数---当队列中有消息时就会自动触发该对象
*/
DefaultConsumer callback = new DefaultConsumer(channel) {
//一定重写该方法。
/*
String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body: 接受的消息---byte数组
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body);
System.out.println("消息的内容:" + msg);
}
};
channel.basicConsume("direct_queue02", true, callback);
//不能关闭connection和channel 关了就监听不了了
}
}
Topics(主题模式)
Topic 类型与 Direct 相比,都是可以根据 RoutingKey 把消息路由到不同的队列。只不过 Topic 类型Exchange 可以让队列在绑定 Routing key 的时候使用通配符。
Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
通配符规则:# 匹配一个或多个词,* 只匹配一个词,例如:item.# 能够匹配 item.insert.abc 或者 item.insert,item.* 只能匹配 item.insert
public class TopicProduct {
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("192.168.184.130");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("topic_exchange", BuiltinExchangeType.TOPIC,true);
channel.queueDeclare("topic_queue01",true,false,false,null);
channel.queueDeclare("topic_queue02",true,false,false,null);
channel.queueBind("topic_queue01","topic_exchange","*.orange.*");
channel.queueBind("topic_queue02","topic_exchange","*.*.rabbit");
channel.queueBind("topic_queue02","topic_exchange","lazy.#");
String msg="主题模式发送消息";
channel.basicPublish("topic_exchange","lazy",null,msg.getBytes());
}
}
public class TopicConsumer01 {
public static void main(String[] args) throws IOException, TimeoutException {
//1.设置连接对象的信息
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("192.168.184.130");//设rabbitMq主机的地址:默认127.0.0.1
//2. 获取连接对象
Connection connection=factory.newConnection();
//3. 获取Channel信道
Channel channel = connection.createChannel();
//4. 创建队列--如果队列不存在则创建---如果存在则不创建.
//channel.queueDeclare("work_queue",true,false,false,null);
//5. 监听消息
/*
String queue, 监听的队列名
boolean autoAck, 是否自动确认。
Consumer callback: 回调函数---当队列中有消息时就会自动触发该对象
*/
DefaultConsumer callback = new DefaultConsumer(channel) {
//一定重写该方法。
/*
String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body: 接受的消息---byte数组
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body);
System.out.println("消息的内容:" + msg);
}
};
channel.basicConsume("topic_queue01", true, callback);
//不能关闭connection和channel 关了就监听不了了
}
}
public class TopicConsumer02 {
public static void main(String[] args) throws IOException, TimeoutException {
//1.设置连接对象的信息
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("192.168.184.130");//设rabbitMq主机的地址:默认127.0.0.1
//2. 获取连接对象
Connection connection=factory.newConnection();
//3. 获取Channel信道
Channel channel = connection.createChannel();
//4. 创建队列--如果队列不存在则创建---如果存在则不创建.
//channel.queueDeclare("work_queue",true,false,false,null);
//5. 监听消息
/*
String queue, 监听的队列名
boolean autoAck, 是否自动确认。
Consumer callback: 回调函数---当队列中有消息时就会自动触发该对象
*/
DefaultConsumer callback = new DefaultConsumer(channel) {
//一定重写该方法。
/*
String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body: 接受的消息---byte数组
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body);
System.out.println("消息的内容:" + msg);
}
};
channel.basicConsume("topic_queue02", true, callback);
//不能关闭connection和channel 关了就监听不了了
}
}
Topic 在配置routing key 的时候可以使用通配符,显得更加灵活。
springboot整合rabbitmq
1、先创建好Springboot项目
这里父模块和子模块都为sprngboot项目,子模块共享父模块的依赖
2、在父模块导入依赖
<?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.aaa</groupId>
<artifactId>qt168-springboot-rabbitmq</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
</dependencies>
</project>
3、生产者端配置yml文件
server.port=8088
spring.rabbitmq.virtual-host=/
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.host=192.168.184.130
spring.rabbitmq.publisher-confirm-type=correlated
spring.rabbitmq.publisher-returns=true
confirm returns 先写上,后面会讲
4、定义交换机,队列以及绑定关系的配置类,因为这里有现成的交换机和队列(刚刚在java工程创建的交换机和队列可以拿来用)
5、注入RabbitTemplate,调用方法,完成消息发送
@SpringBootTest
class SpringbootRabbitProductApplicationTests {
//springboot封装了一个工具类。该类提供了相应的方法。
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void contextLoadsPt() {
//String exchange, String routingKey, Object message
rabbitTemplate.convertAndSend("topic_exchange", "lazy.orange.aaa", "哈哈哈");
}
6、消费者端编写yml配置,基本信息配置 定义监听类,使用@RabbitListener注解完成队列监听。
server.port=7777
spring.rabbitmq.virtual-host=/
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.host=192.168.184.130
spring.rabbitmq.listener.simple.acknowledge-mode=manual
acknowledge 先写上 后面会讲
@Component
public class MyRabbitListener {
//把收到的消息封装到一个Message对象中
@RabbitListener(queues = {"topic_queue01"})
public void myListener01(Message message){
byte[] body = message.getBody();
String msg=new String(body);
System.out.println("收到的消息内容:"+msg);
System.out.println("根据消息写自己的业务代码");
}
}
- SpringBoot提供了快速整合RabbitMQ的方式
- 基本信息再yml中配置,队列交互机以及绑定关系在配置类中使用Bean的方式配置,这里使用了现有的交换机和队列(java使用RabbitMQ所创建的),之后会再演示
- 生产端直接注入RabbitTemplate完成消息发送
- 消费端直接使用@RabbitListener完成消息接收
保证消息的稳定性
消息的可靠投递
在生产环境中由于一些不明原因,导致 rabbitmq 重启,在 RabbitMQ 重启期间生产者消息投递失败,导致消息丢失,需要手动处理和恢复。于是,我们开始思考,如何才能进行 RabbitMQ 的消息可靠投递呢?特别是在这样比较极端的情况,RabbitMQ 集群不可用的时候,无法投递的消息该如何处理呢?
在使用 RabbitMQ 的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景。RabbitMQ 为我们提供了两种方式用来控制消息的投递可靠性模式。
- confirm 确认模式
- return 退回模式
- 消息从 producer 到 exchange 则会返回一个 confirmCallback 。
- 消息从 exchange 到 queue 投递失败则会返回一个 returnCallback 。
我们将利用这两个 callback 控制消息的可靠性投递
confirm和return的实现
1. 设置ConnectionFactory的publisher-confirm-type: correlated开启 确认模式。(上面已配置)
spring.rabbitmq.publisher-confirm-type=correlated (生产者yml文件)
2. 使用rabbitTemplate.setConfirmCallback设置回调函数。当消息发送到exchange后回调confirm方法。在方法中判断ack,如果为true,则发送成功,如果为false,则发送失败,需要处理。
spring.rabbitmq.listener.simple.acknowledge-mode=manual (消费者yml文件 先加上后面讲)
3. 设置ConnectionFactory的publisher-returns="true" 开启 退回模式。
spring.rabbitmq.publisher-returns=true (生产者yml文件)
4. 使用rabbitTemplate.setReturnCallback设置退回函数,当消息从exchange路由到queue失败后执行回调函数returnedMessage。
上代码:
生产者创建交换机和队列和绑定关系
@Configuration
public class RabbitMqConfig {
public static String exchange_name01 = "aaa_exchange01";
public static String queue_name01 = "aaa_queue01";
public static String queue_name02 = "aaa_queue02";
//创建交换机
@Bean
public Exchange exchange01() {
Exchange exchang = ExchangeBuilder.topicExchange(exchange_name01).durable(true).build();
return exchang;
}
//创建队列
@Bean
public Queue queue01() {
Queue queue = QueueBuilder.durable(queue_name01).build();
return queue;
}
@Bean
public Queue queue02() {
//为队列设置过期时间
// Queue queue = QueueBuilder.durable(queue_name02).withArgument("x-message-ttl",20000).build();
Queue queue = QueueBuilder.durable(queue_name02).build();
return queue;
}
//创建队列和交换机的绑定关系
@Bean
public Binding binding01() {
Binding binding = BindingBuilder.bind(queue01()).to(exchange01()).with("*.orange.*").noargs();
return binding;
}
@Bean
public Binding binding02() {
Binding binding = BindingBuilder.bind(queue02()).to(exchange01()).with("*.*.rabbit").noargs();
return binding;
}
@Bean
public Binding binding03() {
Binding binding = BindingBuilder.bind(queue02()).to(exchange01()).with("lazy.#").noargs();
return binding;
}
}
测试类中使用机制发送消息
@SpringBootTest
class SpringbootRabbitProductApplicationTests {
//springboot封装了一个工具类。该类提供了相应的方法。
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void contextLoadsPt() {
//String exchange, String routingKey, Object message
rabbitTemplate.convertAndSend("topic_exchange", "lazy.orange.aaa", "哈哈哈");
}
@Test
void contextLoads() {
Map<String, Object> map = new HashMap<>();
map.put("orderId", UUID.randomUUID());
map.put("productId", 1);
map.put("num", 15);
rabbitTemplate.convertAndSend(RabbitMqConfig.exchange_name01,"lazy.orange.rabbit", JSON.toJSONString(map));
}
//生产者到交换机 confirm机制
@Test
public void testConfirm(){
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if(ack){
System.out.println("发送消息成功");
}else{
System.out.println("发送消息到交换机失败");
}
}
});
Map<String, Object> map = new HashMap<>();
map.put("orderId", UUID.randomUUID());
map.put("productId", 2);
map.put("num", 15);
rabbitTemplate.convertAndSend(RabbitMqConfig.exchange_name01,"aaa.bbb.rabbit", JSON.toJSONString(map));
}
//交换机到队列 return 机制
@Test
void returenTest() {
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("返还消息:"+message);
System.out.println("失败原因"+replyText);
}
});
Map<String, Object> map = new HashMap<>();
map.put("orderId", UUID.randomUUID());
map.put("productId", 3);
map.put("num", 15);
//给消息设置过期时间 如果队列也设置了过期时间 那么谁的时间短根据谁过期
MessagePostProcessor messagePostProcessor = new MessagePostProcessor(){
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setExpiration("30000");
//给消息设置Id
//message.getMessageProperties().setMessageId();
return message;
}
};
rabbitTemplate.convertAndSend(RabbitMqConfig.exchange_name01,"aaa.bbb.rabbit", JSON.toJSONString(map),messagePostProcessor);
}
//队列到消费者的消息可靠性机制要在 消费者端进行
@Test
public void testDead(){
for (int i = 0; i < 10; i++) {
rabbitTemplate.convertAndSend("pt_exchange","","消息内容"+i);
}
}
}
消费者监听消息
@Component
public class MyRabbitListener {
//把收到的消息封装到一个Message对象中
@RabbitListener(queues = {"topic_queue01"})
public void myListener01(Message message){
byte[] body = message.getBody();
String msg=new String(body);
System.out.println("收到的消息内容:"+msg);
System.out.println("根据消息写自己的业务代码");
}
@RabbitListener(queues = {"aaa_queue02"})
public void myListener02(Message message, Channel channel) throws IOException {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try{
byte[] body = message.getBody();
String msg=new String(body);
Map map = JSON.parseObject(msg, Map.class);
System.out.println("收到的消息内容:"+map);
// int c = 1/0;
System.out.println("根据消息写自己的业务代码");
//deliveryTag 消息标识 boolean multiple 是否把该消息之前的消息也确认掉
channel.basicAck(deliveryTag,true);
}catch (Exception e){
System.out.println("程序出现意外情况");
//requeue:true 要求继续发送消息
channel.basicNack(deliveryTag,true,true);
}
}
}
ACK确认机制
此机制在上面的生产者测试类代码中顺便加入了,配置文件内容也有添加,在此解释:
多个消费者同时收取消息,收取消息到一半,突然某个消费者挂掉,要保证此条消息不丢失,就需要acknowledgement机制,就是消费者消费完要通知服务端,服务端才将数据删除,这样就解决了,即使一个消费者出了问题,没有同步消息给服务端,还有其他的消费端去消费,保证了消息不丢的case。
ack指Acknowledge,确认,表示消费端收到消息后的确认方式。
有三种确认方式:
自动确认:acknowledge=none
手动确认:acknowledge=manual
根据异常情况确认:acknowledge="auto",(这种方式使用麻烦,并且不常用,不作讲解)
其中自动确认是指,当消息一旦被Consumer接收到,则自动确认收到,并将相应 message 从 RabbitMQ 的消息队列中移除。但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失。如果设置了手动确认方式,则需要在业务处理成功后,调用channel.basicAck(),手动签收,如果出现异常,则调用channel.basicNack()方法,让其自动重新发送消息。