1.MQ的相关概念
1.1 什么是MQ消息中间件
MQ全称 Message Queue(消息队列),是在消息的传输过程中保存消息的容器。多用于分布式系统之间进行通信。
- MQ,消息队列,存储消息的中间件
- 分布式系统通信两种方式:直接远程调用 和借助第三方 完成间接通信
- 发送方称为生产者,接收方称为消费者
1.2 为什么使用MQ
在项目中,可将一些无需即时返回且耗时的操作提取出来,进行异步处理,而这种异步处理的方式大大的节省了服务器的请求响应时间,从而提高了系统的吞吐量。
1.3 MQ的优势
1. 应用解耦
以电商应用为例,应用中有订单系统、库存系统、物流系统、支付系统。用户创建订单后,如果耦合调用库存系统、物流系统、支付系统,任何一个子系统出了故障,都会造成下单操作异常。当转变成基于消息队列的方式后,系统间调用的问题会减少很多,比如物流系统因为发生故障,需要几分钟来修复。在这几分钟的时间里,物流系统要处理的内容被缓存在消息队列中,用户的下单操作可以正常完成。当物流系统恢复后,继续处理订单信息即可,中间用户感受不到物流系统的故障,提升系统的可用性。
2. 异步提速
上面要完成下单需要花费的时间: 20 + 300 + 300 + 300 = 920ms 用户点击完下单按钮后,需要等待920ms才能得到下单响应,太慢!
使用MQ可以解决上述问题
用户点击完下单按钮后,只需等待25ms就能得到下单响应 (20 + 5 = 25ms)。
提升用户体验和系统吞吐量(一秒内处理请求的数量)。
因为用户只是调用了你的订单系统,而订单系统只需要保存订单信息,响应给用户即可
3. 削峰填谷
举个例子,如果订单系统最多能处理一千次订单,这个处理能力应付正常时段的下单时绰绰有余,正常时段我们下单一秒后就能返回结果。但是在高峰期,如果有五千次下单操作系统是处理不了的,只能限制订单超过一千后不允许用户下单。使用消息队列做缓冲,我们可以取消这个限制,把一秒内下的订单分散成一段时间来处理,这时有些用户可能在下单十几秒后才能收到下单成功的操作,但是比不能下单的体验要好。 简单来说: 就是在访问量剧增的情况下,但是应用仍然不能停,比如“双十一”下单的人多,但是淘宝这个应用仍然要运行,所以就可以使用消息中间件采用队列的形式减少突然访问的压力
使用了 MQ 之后,限制消费消息的速度为1000,这样一来,高峰期产生的数据势必会被积压在 MQ 中,高峰就被“削”掉了,但是因为消息积压,在高峰期过后的一段时间内,消费消息的速度还是会维持在1000,直到消费完积压的消息,这就叫做“填谷”。
使用MQ后,可以提高系统稳定性。
1.4 MQ的劣势
1.系统可用性降低
系统引入的外部依赖越多,系统稳定性越差。一旦 MQ 宕机,就会对业务造成影响。如何保证MQ的高可用?
2.系统复杂度提高
MQ 的加入大大增加了系统的复杂度,以前系统间是同步的远程调用,现在是通过 MQ 进行异步调用。如何保证消息没有被重复消费?怎么处理消息丢失情况?那么保证消息传递的顺序性?
3.一致性问题
A 系统处理完业务,通过 MQ 给B、C、D三个系统发消息数据,如果 B 系统、C 系统处理成功,D 系统处理失败。如何保证消息数据处理的一致性?
1.5 常见的 MQ 产品
目前业界有很多的 MQ 产品,例如 RabbitMQ、RocketMQ、ActiveMQ、Kafka、ZeroMQ、MetaMq等,也有直接使用 Redis 充当消息队列的案例,而这些消息队列产品,各有侧重,在实际选型时,需要结合自身需求及 MQ 产品特征
2. RabbitMQ的概念
2.1 RabbitMQ的概念
2007 年发布,是一个在 AMQP(高级消息队列协议)基础上完成的,可复用的企业消息系统,是当前最主流的消息中间件之一。
RabbitMQ是一个由erlang开发的AMQP(Advanced Message Queue 高级消息队列协议 )的开源实现,由于erlang 语言的高并发特性,性能较好,本质是个队列,FIFO 先入先出,里面存放的内容是message .
RabbitMQ是一个消息中间件:它接受并转发消息。你可以把它当做一个快递站点,当你要发送一个包裹时,你把你的包裹放到快递站,快递员最终会把你的快递送到收件人那里,按照这种逻辑RabbitMQ是一个快递站,一个快递员帮你传递快件。RabbitMQ与快递站的主要区别在于,它不处理快件而是接收,存储和转发消息数据。
2.2 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 的分发依据
2.3 图形化介绍
添加用户信息和界面
添加虚拟主机
给用户分配虚拟主机或删除
获取队列消息
连接和信道
添加查看消费者
创建交换机
绑定交换机
3. RabbitMQ 的工作模式
RabbitMQ 提供了 6 种工作模式:简单模式、work queues、Publish/Subscribe 发布与订阅模式、Routing 路由模式、Topics 主题模式、RPC 远程调用模式(远程调用,不太算 MQ;暂不作介绍)。 官网对应模式介绍:RabbitMQ Tutorials | RabbitMQ
spring方式的依赖
<!--表示包含子类的模块--> <modules> <module>consumer</module> <module>product</module> </modules> <!---如果该工程是一个父工程。那么打包方式为pom。而且这种父工程不写代码。只子工程的管理--> <packaging>pom</packaging><!--引入rabbitmq的协议依赖--> <dependencies> <dependency> <groupId>com.rabbitmq</groupId> <artifactId>amqp-client</artifactId> <version>5.14.2</version> </dependency> </dependencies>
3.1 Simple(简单模式)
在上图的模型中,有以下概念:
P:生产者,也就是要发送消息的程序
queue:消息队列,图中红色部分。类似一个邮箱,可以缓存消息;生产者向其中投递消息,消费者从其中取出消息
C:消费者:消息的接收者,会一直等待消息到来
生产者:
package com.xqw.demo1;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 1. 设置连接对象的信息ConnectionFactory
* 2. 获取连接对象 Connection
* 3. 获取Channel信道对象
* 4. 创建队列 queueDeclare
* 5. 发送消息。basicPublish()
*
*/
public class SimpleProduct {
public static void main(String[] args) throws IOException, TimeoutException {
//1.设置连接对象的信息
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.198.200");//设置rabbitMq主机的地址:默认127.0.0.1
factory.setPort(5672);//设rabbitMq主机的端口号。默认为5672
factory.setUsername("shuai");//设置rabb置itmq的账号 默认guest
factory.setPassword("qingfengzilai.");//设置rabbitmq的密码 默认guest
factory.setVirtualHost("/a");//设置虚拟主机 默认 /
//2.获取连接对象
Connection connection = factory.newConnection();
//3.获取channel信道
Channel channel = connection.createChannel();
//4.创建队列--如果队列不存在则创建---如果存在则不创建.
/*参数说明:
String queue,队列的名称--命名规则就可以
boolean durable, 是否持久化。-- 不设置持久化,重启虚拟器mq后,队列会消失
boolean exclusive, 是否独占--当前channel是否独占该队列
boolean autoDelete, 是否自动删除该队列。 如果没有消费者连接该队列 则会自动删除该对象
Map<String, Object> arguments:该队列的属性参数--null
*/
channel.queueDeclare("simple_queue", true, false, false, null);
//5.发送消息
/*参数说明:
String exchange, 交换机的名称--工作模式没有交换机 "" ---往哪个交换机发
String routingKey, 路由key. 像简单模式没有交换机绑定路由key,所以默认是队列的名称
BasicProperties props,消息的属性--现在给定null.
byte[] body: 消息的内容
*/
String msg="hello rabbitmq lam ~~~~~~";//消息内容
channel.basicPublish("","simple_queue",null,msg.getBytes());
//6.关闭连接和信道资源,不关闭会产生浪费。--不关闭资源会看到信道和连接
channel.close();
connection.close();
}
}
消费者:
package com.demo1;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class SimpleConsumer {
public static void main(String[] args) throws IOException, TimeoutException {
//1.设置连接对象的信息
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.198.200");//设置rabbitMq主机的地址:默认127.0.0.1
factory.setPort(5672);//设置rabbitMq主机的端口号。默认为5672
factory.setUsername("shuai");//设置rabbitmq的账号 默认guest
factory.setPassword("qingfengzilai.");//设置rabbitmq的密码 默认guest
factory.setVirtualHost("/a");//设置虚拟主机 默认 /
//2.获取连接对象
Connection connection = factory.newConnection();
//3.获取channel信道
Channel channel = connection.createChannel();
//4.监听消息
//回调的函数---当有消息会触发这个回调函数
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);
}
};
/*参数说明:
String queue, 监听的队列名---要和你发送的队列名一致
boolean autoAck, 是否自动确认。----接收到消息会被队列移除
Consumer callback: 回调函数---当队列中有消息时就会自动触发该对象
*/
channel.basicConsume("simple_queue", true, callback);
//消费端不能关闭连接和信道--因为要一直监听消息
}
}
消费了就没有了
3.2 Work Queues(工作模式)
Work Queues:工作模式对比简单模式相比,只是多了一个或一些消费端,多个消费端共同消费同一个队列中的消息。
应用场景:对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度。
生产者:
package com.demo2;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 1. 设置连接对象的信息ConnectionFactory
* 2. 获取连接对象 Connection
* 3. 获取Channel信道对象
* 4. 创建队列 queueDeclare
* 5. 发送消息。basicPublish()
*
*/
public class WorkProduct {
public static void main(String[] args) throws IOException, TimeoutException {
//1.设置连接对象的信息
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.198.200");//设置rabbitMq主机的地址:默认127.0.0.1
factory.setPort(5672);//设置rabbitMq主机的端口号。默认为5672
factory.setUsername("shuai");//设置rabbitmq的账号 默认guest
factory.setPassword("qingfengzilai.");//设置rabbitmq的密码 默认guest
factory.setVirtualHost("/a");//设置虚拟主机 默认 /
//2.获取连接对象
Connection connection = factory.newConnection();
//3.获取channel信道
Channel channel = connection.createChannel();
//4.创建队列--如果队列不存在则创建---如果存在则不创建.
/*参数说明:
String queue,队列的名称--命名规则就可以
boolean durable, 是否持久化。--不设置持久化,重启虚拟器mq后,队列会消失
boolean exclusive, 是否独占--当前channel是否独占该队列
boolean autoDelete, 是否自动删除该队列。如果没有消费者连接该队列 则会自动删除该对象
Map<String, Object> arguments:该队列的属性参数--null
*/
channel.queueDeclare("work_queue", true, false, false, null);
//5.发送消息
/*参数说明:
String exchange, 交换机的名称--工作模式没有交换机,设置为"" ---往哪个交换机发
String routingKey, 路由key. 工作模式没有交换机绑定路由key,所以默认是队列的名称
BasicProperties props,消息的属性--现在给定null.
byte[] body: 消息的内容
*/
for (int i = 0; i < 10; i++) {
String msg="work rabbitmq lam "+i;
channel.basicPublish("","work_queue",null,msg.getBytes());
}
//6.关闭连接和信道资源,不关闭会产生浪费。--不关闭资源会看到信道和连接
channel.close();
connection.close();
}
}
消费者:
package com.demo2;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class WorkConsumer01 {
public static void main(String[] args) throws IOException, TimeoutException {
//1.设置连接对象的信息
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.198.200");//设置rabbitMq主机的地址:默认127.0.0.1
factory.setPort(5672);//设置rabbitMq主机的端口号。默认为5672
factory.setUsername("shuai");//设置rabbitmq的账号 默认guest
factory.setPassword("qingfengzilai.");//设置rabbitmq的密码 默认guest
factory.setVirtualHost("/a");//设置虚拟主机 默认 /
//2.获取连接对象
Connection connection = factory.newConnection();
//3.获取channel信道
Channel channel = connection.createChannel();
/*消费端可写可不写*/
//4.创建队列--如果队列不存在则创建---如果存在则不创建.
/*参数说明:
String queue,队列的名称--命名规则就可以
boolean durable, 是否持久化。-- 不设置持久化,重启虚拟器mq后,队列会消失
boolean exclusive, 是否独占--当前channel是否独占该队列
boolean autoDelete, 是否自动删除该队列。 如果没有消费者连接该队列 则会自动删除该对象
Map<String, Object> arguments:该队列的属性参数--null
*/
channel.queueDeclare("work_queue", true, false, false, null);
//5.监听消息
//回调的函数---当有消息会触发这个回调函数
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);
}
};
/*参数说明:
String queue, 监听的队列名---要和你发送的队列名一致
boolean autoAck, 是否自动确认。----接收到消息会被队列移除
Consumer callback: 回调函数---当队列中有消息时就会自动触发该对象
*/
channel.basicConsume("work_queue", true, callback);
//消费端不能关闭连接和信道--因为要一直监听消息
}
}
package com.demo2;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class WorkConsumer02 {
public static void main(String[] args) throws IOException, TimeoutException {
//1.设置连接对象的信息
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.198.200");//设置rabbitMq主机的地址:默认127.0.0.1
factory.setPort(5672);//设置rabbitMq主机的端口号。默认为5672
factory.setUsername("shuai");//设置rabbitmq的账号 默认guest
factory.setPassword("qingfengzilai.");//设置rabbitmq的密码 默认guest
factory.setVirtualHost("/a");//设置虚拟主机 默认 /
//2.获取连接对象
Connection connection = factory.newConnection();
//3.获取channel信道
Channel channel = connection.createChannel();
/*消费端可写可不写*/
//4.创建队列--如果队列不存在则创建---如果存在则不创建.
/*参数说明:
String queue,队列的名称--命名规则就可以
boolean durable, 是否持久化。-- 不设置持久化,重启虚拟器mq后,队列会消失
boolean exclusive, 是否独占--当前channel是否独占该队列
boolean autoDelete, 是否自动删除该队列。 如果没有消费者连接该队列 则会自动删除该对象
Map<String, Object> arguments:该队列的属性参数--null
*/
channel.queueDeclare("work_queue", true, false, false, null);
//5.监听消息
//回调的函数---当有消息会触发这个回调函数
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);
}
};
/*参数说明:
String queue, 监听的队列名---要和你发送的队列名一致
boolean autoAck, 是否自动确认。----接收到消息会被队列移除
Consumer callback: 回调函数---当队列中有消息时就会自动触发该对象
*/
channel.basicConsume("work_queue", true, callback);
//消费端不能关闭连接和信道--因为要一直监听消息
}
}
多条消息会被不同的消费者消费掉,但是要注意一条消息不能被不同的消费者消费
如果让其中一个消费者等1秒 看看会不会有影响
package com.xqw.demo2;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class WorkConsumer02 {
public static void main(String[] args) throws IOException, TimeoutException {
//1.设置连接对象的信息
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.198.200");//设置rabbitMq主机的地址:默认127.0.0.1
factory.setPort(5672);//设置rabbitMq主机的端口号。默认为5672
factory.setUsername("shuai");//设置rabbitmq的账号 默认guest
factory.setPassword("qingfengzilai.");//设置rabbitmq的密码 默认guest
factory.setVirtualHost("/a");//设置虚拟主机 默认 /
//2.获取连接对象
Connection connection = factory.newConnection();
//3.获取channel信道
Channel channel = connection.createChannel();
/*消费端可写可不写*/
//4.创建队列--如果队列不存在则创建---如果存在则不创建.
/*参数说明:
String queue,队列的名称--命名规则就可以
boolean durable, 是否持久化。-- 不设置持久化,重启虚拟器mq后,队列会消失
boolean exclusive, 是否独占--当前channel是否独占该队列
boolean autoDelete, 是否自动删除该队列。 如果没有消费者连接该队列 则会自动删除该对象
Map<String, Object> arguments:该队列的属性参数--null
*/
channel.queueDeclare("work_queue", true, false, false, null);
//5.监听消息
//回调的函数---当有消息会触发这个回调函数
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 {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {//就算慢也会平均了消息
throw new RuntimeException(e);
}
String msg=new String(body);//转换为字符就行了
System.out.println("消息的内容"+msg);
}
};
/*参数说明:
String queue, 监听的队列名---要和你发送的队列名一致
boolean autoAck, 是否自动确认。----接收到消息会被队列移除
Consumer callback: 回调函数---当队列中有消息时就会自动触发该对象
*/
channel.basicConsume("work_queue", true, callback);
//消费端不能关闭连接和信道--因为要一直监听消息
}
}
并无影响
在一个队列中如果有多个消费者,那么消费者之间对于同一个消息的关系是==竞争==的关系。
Work Queues 对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度。例如:短信服务部署多个,只需要有一个节点成功发送即可。
3.3 Publish/Subscribe(广播模式)
RabbitMQ tutorial - Publish/Subscribe | RabbitMQ
在订阅模型中,多了一个 Exchange 角色,而且过程略有变化:
- P:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给X(交换机)
Exchange:交换机(X)。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。Exchange有常见以下3种类型:
- Fanout:广播,将消息交给所有绑定到交换机的队列
- Direct:定向,把消息交给符合指定routing key 的队列
- Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列
- Queue:消息队列,接收消息、缓存消息
- C:消费者,消息的接收者,会一直等待消息到来
Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange 绑定,或者没有符合路由规则的队列,那么消息会丢失!
生产者:
package com.demo3;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class PublishSubscribeProduct {
public static void main(String[] args) throws IOException, TimeoutException {
//1.设置连接对象的信息
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.198.200");//设置rabbitMq主机的地址:默认127.0.0.1
factory.setPort(5672);//设置rabbitMq主机的端口号。默认为5672
factory.setUsername("shuai");//设置rabbitmq的账号 默认guest
factory.setPassword("qingfengzilai.");//设置rabbitmq的密码 默认guest
factory.setVirtualHost("/a");//设置虚拟主机 默认 /
//2.获取连接对象
Connection connection = factory.newConnection();
//3.通过连接对象创建channel信道
Channel channel = connection.createChannel();
//4.创建交换机
/*参数说明:
String exchange,交换机的名称--命名规则就可以
BuiltinExchangeType type, 交换机的类型
boolean durable: 是否持久化 --不设置持久化,重启虚拟器mq后,交换机会消失
*/
//fanout广播模式
channel.exchangeDeclare("fanout_exchange", BuiltinExchangeType.FANOUT,true);
//5.创建队列
/*参数说明:
String queue,队列的名称--命名规则就可以
boolean durable, 是否持久化。--不设置持久化,重启虚拟器mq后,队列会消失
boolean exclusive, 是否独占--当前channel是否独占该队列
boolean autoDelete, 是否自动删除该队列。如果没有消费者连接该队列 则会自动删除该对象
Map<String, Object> arguments:该队列的属性参数--null
*/
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 exchange, 交换机的名称
String routingKey, 路由key. 广播模式没有路由key
BasicProperties props,消息的属性--现在给定null.
byte[] body: 消息的内容
*/
String msg="这是一个发布订阅工作模式~~~~";
channel.basicPublish("fanout_exchange","",null,msg.getBytes());
//8.关闭资源。
channel.close();
connection.close();
}
}
消费者:
package com.demo3;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class PublishSubscribeConsumer01 {
public static void main(String[] args) throws IOException, TimeoutException {
//1.设置连接对象的信息
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.198.200");//设置rabbitMq主机的地址:默认127.0.0.1
factory.setPort(5672);//设置rabbitMq主机的端口号。默认为5672
factory.setUsername("shuai");//设置rabbitmq的账号 默认guest
factory.setPassword("qingfengzilai.");//设置rabbitmq的密码 默认guest
factory.setVirtualHost("/");//设置虚拟主机 默认 /
//2.获取连接对象
Connection connection = factory.newConnection();
//3.获取channel信道
Channel channel = connection.createChannel();
//回调的函数---当有消息会触发这个回调函数
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);
}
};
/*参数说明:
String queue, 监听的队列名
boolean autoAck, 是否自动确认。
Consumer callback: 回调函数---当队列中有消息时就会自动触发该对象
*/
//4.监听消息
channel.basicConsume("fanout_queue01", true, callback);
//消费端不能关闭连接和信道--因为要一直监听消息
}
}
package com.xqw.demo3;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class PublishSubscribeConsumer02 {
public static void main(String[] args) throws IOException, TimeoutException {
//1.设置连接对象的信息
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.198.200");//设置rabbitMq主机的地址:默认127.0.0.1
factory.setPort(5672);//设置rabbitMq主机的端口号。默认为5672
factory.setUsername("xqw");//设置rabbitmq的账号 默认guest
factory.setPassword("qingfengzilai.");//设置rabbitmq的密码 默认guest
factory.setVirtualHost("/aaa");//设置虚拟主机 默认 /
//2.获取连接对象
Connection connection = factory.newConnection();
//3.获取channel信道
Channel channel = connection.createChannel();
//回调的函数---当有消息会触发这个回调函数
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);
}
};
/*参数说明:
String queue, 监听的队列名
boolean autoAck, 是否自动确认。
Consumer callback: 回调函数---当队列中有消息时就会自动触发该对象
*/
//4.监听消息
channel.basicConsume("fanout_queue02", true, callback);
//消费端不能关闭连接和信道--因为要一直监听消息
}
}
3.4 Routing(路由模式)
Routinghttps://rabbitmq.com/tutorials/tutorial-four-python.html
队列与交换机的绑定,不能是任意绑定了,而是要指定一个 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.xqw.demo4;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class DirectProduct {
public static void main(String[] args) throws IOException, TimeoutException {
//1.设置连接对象的信息 如果不给设置其他的信息 默认用的 guest的信息
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.198.200");
//2.获取连接对象
Connection connection = factory.newConnection();
//3.通过连接对象创建channel信道
Channel channel = connection.createChannel();
//4.创建交换机
/*参数说明:
String exchange,交换机的名称
BuiltinExchangeType type, 交换机的类型
boolean durable: 是否持久化
*/
channel.exchangeDeclare("direct_exchange", BuiltinExchangeType.DIRECT,true);
//5.创建队列
/*参数说明:
String queue,队列的名称--命名规则就可以
boolean durable, 是否持久化。--不设置持久化,重启虚拟器mq后,队列会消失
boolean exclusive, 是否独占--当前channel是否独占该队列
boolean autoDelete, 是否自动删除该队列。如果没有消费者连接该队列 则会自动删除该对象
Map<String, Object> arguments:该队列的属性参数--null
*/
channel.queueDeclare("direct_queue01",true,false,false,null);
channel.queueDeclare("direct_queue02",true,false,false,null);
//6.交换机和队列绑定
/*参数说明:
String queue,队列名
String exchange,交换机名
String routingKey: 路由key
*/
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");
//7.发送消息
/*参数说明:
String exchange, 交换机的名称
String routingKey, 路由key.
BasicProperties props,消息的属性--现在给定null.
byte[] body: 消息的内容
*/
String msg="这是一个路由模式!!!!!!!!!!!!!!!!";
channel.basicPublish("direct_exchange","error",null,msg.getBytes());
//8.关闭资源。
channel.close();
connection.close();
}
}
如果路由是info只会匹配到绑定路由info的direct_queue02
如果路由是hhh没有与之匹配的路由
消费者:
package com.xqw.demo4;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class DirectConsumer01 {
public static void main(String[] args) throws IOException, TimeoutException {
//1.设置连接对象的信息
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.198.200");//设置rabbitMq主机的地址:默认127.0.0.1
factory.setPort(5672);//设置rabbitMq主机的端口号。默认为5672
//2.获取连接对象
Connection connection = factory.newConnection();
//3.获取channel信道
Channel channel = connection.createChannel();
//回调的函数---当有消息会触发这个回调函数
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);
}
};
/*参数说明:
String queue, 监听的队列名
boolean autoAck, 是否自动确认。
Consumer callback: 回调函数---当队列中有消息时就会自动触发该对象
*/
//4.监听消息
channel.basicConsume("direct_queue01", true, callback);
}
}
package com.xqw.demo4;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class DirectConsumer02 {
public static void main(String[] args) throws IOException, TimeoutException {
//1.设置连接对象的信息
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.198.200");//设置rabbitMq主机的地址:默认127.0.0.1
factory.setPort(5672);//设置rabbitMq主机的端口号。默认为5672
//2.获取连接对象
Connection connection = factory.newConnection();
//3.获取channel信道
Channel channel = connection.createChannel();
//回调的函数---当有消息会触发这个回调函数
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);
}
};
/*参数说明:
String queue, 监听的队列名
boolean autoAck, 是否自动确认。
Consumer callback: 回调函数---当队列中有消息时就会自动触发该对象
*/
//4.监听消息
channel.basicConsume("direct_queue02", true, callback);
}
}
Routing 模式要求队列在绑定交换机时要指定 routing key,消息会转发到符合 routing key 的队列
3.5 Topics 主题模式
Topic 类型与 Direct 相比,都是可以根据 RoutingKey 把消息路由到不同的队列。只不过 Topic 类型Exchange 可以让队列在绑定 Routing key 的时候使用通配符!
Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
通配符规则:# 匹配一个或多个词,* 匹配不多不少恰好1个词,
例如:item.# 能够匹配 item.insert.abc 或者 item.insert,
item.* 只能匹配 item.insert
生产者:
package com.xqw.demo5;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class TopicPublishProduct {
public static void main(String[] args) throws IOException, TimeoutException {
//1.设置连接对象的信息 如果不给设置其他的信息 默认用的 guest的信息
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.198.200");
//2.获取连接对象
Connection connection = factory.newConnection();
//3.获取channel信道
Channel channel = connection.createChannel();
//4.创建交换机
/*参数说明:
String exchange,交换机的名称--命名规则就可以
BuiltinExchangeType type, 交换机的类型
boolean durable: 是否持久化 --不设置持久化,重启虚拟器mq后,交换机会消失
*/
channel.exchangeDeclare("topic_exchange", BuiltinExchangeType.TOPIC,true);
//5.创建队列
/*参数说明:
String queue,队列的名称--命名规则就可以
boolean durable, 是否持久化。--不设置持久化,重启虚拟器mq后,队列会消失
boolean exclusive, 是否独占--当前channel是否独占该队列
boolean autoDelete, 是否自动删除该队列。如果没有消费者连接该队列 则会自动删除该对象
Map<String, Object> arguments:该队列的属性参数--null
*/
channel.queueDeclare("topic_queue01",true,false,false,null);
channel.queueDeclare("topic_queue02",true,false,false,null);
//6.交换机和队列绑定
/*参数说明:
String queue,队列名
String exchange,交换机名
String routingKey: 路由key # 匹配一个或多个词,* 匹配不多不少恰好1个词,
*/
channel.queueBind("topic_queue01","topic_exchange","*.orange.*");
channel.queueBind("topic_queue02","topic_exchange","*.*.rabbit");
channel.queueBind("topic_queue02","topic_exchange","lazy.#");
//7.发送消息
/*参数说明:
String exchange, 交换机的名称
String routingKey, 路由key.
BasicProperties props,消息的属性--现在给定null.
byte[] body: 消息的内容
*/
String msg="这是一个主题模式~~~~~~";
//路由key:只要找到一个匹配就行就算有多个匹配也只是发一条消息 可以自定义key
channel.basicPublish("topic_exchange","lazy.orange",null,msg.getBytes());
//8.关闭资源。
channel.close();
connection.close();
}
}
路由是lazy.orange.hh都能匹配到
路由是lazy.orange.rabbit能匹配到topic_queue02队列两次,但是只要匹配到一次就行,就算有多个也只发送一次消息
路由是lazy.hhh能匹配到lazy.#
路由是lazy也能匹配到lazy.#,因为#是匹配N个即使是0也可以
路由是lazy.orange只能匹配到lazy.#,因为orange需要满足匹配不多不少恰好1个词
消费者:
package com.xqw.demo5;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class TopicPublishConsumer01 {
public static void main(String[] args) throws IOException, TimeoutException {
//1.设置连接对象的信息
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.198.200");//设置rabbitMq主机的地址:默认127.0.0.1
factory.setPort(5672);//设置rabbitMq主机的端口号。默认为5672
//2.获取连接对象
Connection connection = factory.newConnection();
//3.获取channel信道
Channel channel = connection.createChannel();
//回调的函数---当有消息会触发这个回调函数
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);
}
};
/*参数说明:
String queue, 监听的队列名
boolean autoAck, 是否自动确认。
Consumer callback: 回调函数---当队列中有消息时就会自动触发该对象
*/
//4.监听消息
channel.basicConsume("topic_queue01", true, callback);
}
}
package com.xqw.demo5;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class TopicPublishConsumer02 {
public static void main(String[] args) throws IOException, TimeoutException {
//1.设置连接对象的信息
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.198.200");//设置rabbitMq主机的地址:默认127.0.0.1
factory.setPort(5672);//设置rabbitMq主机的端口号。默认为5672
//2.获取连接对象
Connection connection = factory.newConnection();
//3.获取channel信道
Channel channel = connection.createChannel();
//回调的函数---当有消息会触发这个回调函数
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);
}
};
/*参数说明:
String queue, 监听的队列名
boolean autoAck, 是否自动确认。
Consumer callback: 回调函数---当队列中有消息时就会自动触发该对象
*/
//4.监听消息
channel.basicConsume("topic_queue02", true, callback);
}
}
Topic 主题模式可以实现 Pub/Sub 发布与订阅模式和 Routing 路由模式的功能,只是 Topic 在配置routing key 的时候可以使用通配符,显得更加灵活。
4. springboot和rabbitmq整合
(1)生产者端
1.创建生产者SpringBoot工程
2.引入start,依赖坐标
<!--启动amqp依赖:自动装备功能,无需手动创建连接对象 创建一个工具类 rabbitmq--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency><!-- json的依赖 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>2.0.23</version> </dependency>
3.编写yml配置,基本信息配置
#端口号 server.port=8787 #rabbit的信息 spring.rabbitmq.virtual-host=/ spring.rabbitmq.port=5672 spring.rabbitmq.username=guest spring.rabbitmq.password=guest spring.rabbitmq.host=192.168.198.200
4.定义交换机,队列以及绑定关系的配置类.
也可以用之前spring创建的主题模式
package com.config;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/*
* springBoot如果没有存在队列和交换机以及绑定关系如何处理:
* [1]在rabbitMQ图形化界面来创建队列交换机以及绑定关系[管理员]
* [2]在springboot中通过代码也可以。
* */
@Configuration
public class RabbitMqConfig {
public static String exchange_name01="topic_config_exchange";
public static String queueName01="topic_config_queue01";
public static String queueName02="topic_config_queue02";
public static String routingKey01="*.orange.*";
public static String routingKey02="*.*.rabbit";
public static String routingKey03="lazy.#";
//创建交换机
@Bean
public Exchange exchange(){
/*交换机名 是否持久化:不设置持久化,重启虚拟器mq后,交换机会消失*/
Exchange exchange = ExchangeBuilder.topicExchange(exchange_name01).durable(true).build();
return exchange;
}
//创建队列--构造函数方法
@Bean
public Queue queue1(){
//参数可以点QueueBuilder进去看,对应之前的spring创建队列的参数,调用就是执行,不调用就是不执行
/*durable表示使用持久化 队列名 */
Queue queue01 = QueueBuilder.durable(queueName01).build();
return queue01;
}
@Bean
public Queue queue2(){
/*队列名 */
Queue queue02 = QueueBuilder.durable(queueName02).build();
return queue02;
}
//创建队列和交换机的绑定关系
@Bean
public Binding binding(){
/*参数说明:
String queue,队列名
String exchange,交换机名
String routingKey: 路由key # 匹配一个或多个词,* 匹配不多不少恰好1个词,
*/
/*队列对象 交换机对象 with绑定 路由key 绑定的参数,没有就是noargs() */
Binding binding = BindingBuilder.bind(queue1()).to(exchange()).with(routingKey01).noargs();
return binding;
}
@Bean
public Binding binding2(){
/*队列对象 交换机对象 with绑定 路由key 绑定的参数,没有就是noargs() */
Binding binding = BindingBuilder.bind(queue2()).to(exchange()).with(routingKey02).noargs();
return binding;
}
@Bean
public Binding binding3(){
/*队列对象 交换机对象 with绑定 路由key 绑定的参数,没有就是noargs() */
Binding binding = BindingBuilder.bind(queue2()).to(exchange()).with(routingKey03).noargs();
return binding;
}
}
5. 注入RabbitTemplate,调用方法,完成消息发送
使用之前主题模式的发送
@SpringBootTest
class SpringbootRabbitProductApplicationTests {
//springboot封装了一个工具类,该类提供了相应的方法
@Autowired
private RabbitTemplate rabbitTemplate;
/*这个是用了之前spring里主题模式*/
@Test
void contextLoads() {
//参数:String exchange, String routingKey, Object message
/*参数说明:
String exchange, 交换机的名称
String routingKey, 路由key. 根据你创建交换机的类型匹配
byte[] body: 消息的内容
*/
//转换和发送
rabbitTemplate.convertAndSend("topic_exchange","lazy.orange.hhh","今天是个好日子");
}
}
现在boot里的主题模式的发送
@SpringBootTest
class SpringbootRabbitProductApplicationTests {
//springboot封装了一个工具类,该类提供了相应的方法
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void contextLoads2(){
/*参数说明:
String exchange, 交换机的名称
String routingKey, 路由key. 根据你创建交换机的类型匹配
byte[] body: 消息的内容
*/
Map<String, Object> map = new HashMap<>();
map.put("orderId", UUID.randomUUID());
map.put("productId",1);
map.put("num",15);
String s = JSON.toJSONString(map);//在真正开发的时候建议转换成String类型
rabbitTemplate.convertAndSend(RabbitMqConfig.exchange_name,"lazy.orange.hhh", s);
}
}
(2) 消费者端:
1.创建生产者SpringBoot工程
2.引入start,依赖坐标
<!--启动amqp依赖:自动装备功能,无需手动创建连接对象 创建一个工具类 rabbitmq--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
3.编写yml配置,基本信息配置
#端口号 server.port=8787 #rabbit的信息 spring.rabbitmq.virtual-host=/ spring.rabbitmq.port=5672 spring.rabbitmq.username=guest spring.rabbitmq.password=guest spring.rabbitmq.host=192.168.198.200
4. 定义监听类,使用@RabbitListener注解完成队列监听。
之前spring里主题模式的监听方式
@Component
public class MyRabbitListener {
@RabbitListener(queues = {"topic_queue01","topic_queue02"})//队列名 多个,以”,“分割
//把收到的消息封装到一个Message对像中
public void myListener01(Message message){
byte[] body = message.getBody();//获取消息内容
String msg=new String(body);//转换为字符,因为byte数组当不了消息
System.out.println("收到的消息内容"+msg);
System.out.println("这里可以根据消息写自己的业务代码");
}
}
现在boot里主题模式的监听方式
@Component
public class MyRabbitListener {
@RabbitListener(queues = {"topic_config_queue01","topic_config_queue02"})//队列名 多个,以”,“分割
//把收到的消息封装到一个Message对像中
public void myListener02(Message message){
byte[] body = message.getBody();//获取消息内容
String msg=new String(body);//转换为字符,因为byte数组当不了消息
//sting不好拿订单号之类的 需要转换为对象
Map map = JSON.parseObject(msg, Map.class);//再解析成对象
System.out.println("收到的消息内容"+map);
System.out.println("这里可以根据消息写自己的业务代码");//用对象取值方便业务操作方便
}
}
SpringBoot提供了快速整合RabbitMQ的方式
基本信息再yml中配置,队列交互机以及绑定关系在配置类中使用Bean的方式配置
生产端直接注入RabbitTemplate完成消息发送
消费端直接使用@RabbitListener完成消息接收
5. 保证消息的稳定性
5.1 消息的可靠投递
在生产环境中由于一些不明原因,导致 rabbitmq 重启,在 RabbitMQ 重启期间生产者消息投递失败,导致消息丢失,需要手动处理和恢复。于是,我们开始思考,如何才能进行 RabbitMQ 的消息可靠投递呢?特别是在这样比较极端的情况,RabbitMQ 集群不可用的时候,无法投递的消息该如何处理呢?
在使用 RabbitMQ 的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景。RabbitMQ 为我们提供了两种方式用来控制消息的投递可靠性模式。
- - confirm 确认模式
- - return 退回模式
- - 消息从 producer 到 exchange 则会返回一个 confirmCallback 。
- - 消息从 exchange-->queue 投递失败则会返回一个 returnCallback 。
我们将利用这两个 callback 控制消息的可靠性投递
confirm和return的实现
1.设置ConnectionFactory的publisher-confirm-type: correlated开启 确认模式。
#开启confirm机制 生产者到---交换机
spring.rabbitmq.publisher-confirm-type=correlated
2.使用rabbitTemplate.setConfirmCallback设置回调函数。当消息发送到exchange后回调confirm方法。在方法中判断ack,如果为true,则发送成功,如果为false,则发送失败,需要处理。
//在使用 RabbitMQ 的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景。
//RabbitMQ 为我们提供了两种方式用来控制消息的投递可靠性模式。
/*
* 如果要使用confirm机制必须遵循如下两步:
* 第一步:配置文件中开启confirm机制 spring.rabbitmq.publisher-confirm-type=correlated
* 第二步:为rabbitTemplate设置setConfirmCallback回调函数 发送消息交换机的名不存在就会报错
* */
@Test
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("生产者发送消息到交换机失败");
//1.可以失败后做相关的业务 再次发送消息 :定时器 :线程的延时器
// ----取消相应的业务
}
}
});
/*发送的消息*/
Map<String, Object> map = new HashMap<>();
map.put("orderId", UUID.randomUUID());
map.put("productId",2);
map.put("num",25);
rabbitTemplate.convertAndSend(RabbitMqConfig.exchange_name,"aaa.hhh.rabbit",JSON.toJSONString(map));
}
成功结果
2024-05-30 15:11:23.499 INFO 18704 --- [ main] o.s.a.r.c.CachingConnectionFactory : Attempting to connect to: [192.168.198.200:5672]
2024-05-30 15:11:23.563 INFO 18704 --- [ main] o.s.a.r.c.CachingConnectionFactory : Created new connection: rabbitConnectionFactory#548d5ed3:0/SimpleConnection@63a28987 [delegate=amqp://guest@192.168.198.200:5672/, localPort= 61158]
生产者发送消息到交换机成功
如果访问不存在的交换机就会失败
结果:
2024-05-30 15:09:22.719 INFO 11112 --- [ main] o.s.a.r.c.CachingConnectionFactory : Attempting to connect to: [192.168.198.200:5672]
2024-05-30 15:09:22.777 INFO 11112 --- [ main] o.s.a.r.c.CachingConnectionFactory : Created new connection: rabbitConnectionFactory#38b5f25:0/SimpleConnection@6dab01d9 [delegate=amqp://guest@192.168.198.200:5672/, localPort= 61086]
2024-05-30 15:09:22.825 ERROR 11112 --- [68.198.200:5672] o.s.a.r.c.CachingConnectionFactory : Channel shutdown: channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'hhh_exchange' in vhost '/', class-id=60, method-id=40)
生产者发送消息到交换机失败
3.设置ConnectionFactory的publisher-returns="true" 开启 退回模式。
#开启return机制 交换机---队列
spring.rabbitmq.publisher-returns=true
4.使用rabbitTemplate.setReturnCallback设置退回函数,当消息从exchange路由到queue失败后执行回调函数returnedMessage。
/*
* 如果要使用return机制必须遵守如下两步:
*第一步:配置文件中开启return机制 spring.rabbitmq.publisher-returns=true
*第二步:设置rabbitTemplate的setReturnCallback回调函数
* */
@Test
public void testRetuning(){
//路由器到队列没找对对应的路由名或者路由不存在就会报错触发回调函数
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("失败的状态码:"+replyCode);
System.out.println("失败的原因:"+replyText);
System.out.println("交换机名称:"+exchange);
System.out.println("路由key名称:"+routingKey);
}
});
/*发送的消息*/
Map<String, Object> map = new HashMap<>();
map.put("orderId", UUID.randomUUID());
map.put("productId",2);
map.put("num",25);
rabbitTemplate.convertAndSend(RabbitMqConfig.exchange_name,"lzay.orange.rabbit",JSON.toJSONString(map));
}
成功的结果
如果路由匹配不上就会失败
2024-05-30 15:37:23.413 INFO 11676 --- [ main] o.s.a.r.c.CachingConnectionFactory : Attempting to connect to: [192.168.198.200:5672]
2024-05-30 15:37:23.477 INFO 11676 --- [ main] o.s.a.r.c.CachingConnectionFactory : Created new connection: rabbitConnectionFactory#38b5f25:0/SimpleConnection@6dab01d9 [delegate=amqp://guest@192.168.198.200:5672/, localPort= 61907]
失败的消息:(Body:'{"productId":2,"orderId":"cd9d93e5-6a1d-4ab1-870a-966ee4b7a730","num":25}' MessageProperties [headers={}, contentType=text/plain, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, deliveryTag=0])
失败的状态码:312
失败的原因:NO_ROUTE
交换机名称:topic_config_exchange
路由key名称:aaa.bbb.ccc
5.合并
//在使用 RabbitMQ 的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景。
//RabbitMQ 为我们提供了两种方式用来控制消息的投递可靠性模式。
/*发送消息前的处理 如果虚拟机名不存在或者找不到就会触发回调函数
路由器到队列没找对对应的路由名或者路由不存在就会报错触发回调函数
* 如果要使用confirm机制和return机制必须遵守如下四步:
* 第一步:配置文件中开启confirm机制 spring.rabbitmq.publisher-confirm-type=correlated
* 第二步:为rabbitTemplate设置setConfirmCallback回调函数 发送消息交换机的名不存在就会报错
* 第三步 配置文件中开启return机制 spring.rabbitmq.publisher-returns=true
* 第四步:设置rabbitTemplate的setReturnCallback回调函数
* */
@Test
void testConfirmAndReturnCallback(){
// 发送消息前的处理 如果交换机名不存在或者找不到就会触发回调函数
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback(){
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack){
System.out.println("生产者发送消息到交换机成功");
}else {
System.out.println("生产者发送消息到交换机失败");
//1.可以失败后做相关的业务 再次发送消息 :定时器 :线程的延时器
// ----取消相应的业务
}
}
});
//路由器到队列没找对对应的路由名或者路由不存在就会报错触发回调函数
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("失败的状态码:"+replyCode);
System.out.println("失败的原因:"+replyText);
System.out.println("交换机名称:"+exchange);
System.out.println("路由key名称:"+routingKey);
}
});
/*发送的消息*/
Map<String, Object> map = new HashMap<>();
map.put("orderId", UUID.randomUUID());
map.put("productId",2);
map.put("num",25);
rabbitTemplate.convertAndSend(RabbitMqConfig.exchange_name,"aaa.hhh.rabbit",JSON.toJSONString(map));
}
5.2 ACK确认机制
多个消费者同时收取消息,收取消息到一半,突然某个消费者挂掉,要保证此条消息不丢失,就需要acknowledgement机制,就是消费者消费完要通知服务端,服务端才将数据删除
这样就解决了,及时一个消费者出了问题,没有同步消息给服务端,还有其他的消费端去消费,保证了消息不丢的case。
ACK的实现
ack指Acknowledge,确认。 表示消费端收到消息后的确认方式。
有三种确认方式:
自动确认:acknowledge="none"
手动确认:acknowledge="manual"
根据异常情况确认:acknowledge="auto",(这种方式使用麻烦,并且不常用,不作讲解)
其中自动确认是指,当消息一旦被Consumer接收到,则自动确认收到,并将相应 message 从 RabbitMQ 的消息队列中移除。但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失。
如果设置了手动确认方式,则需要在业务处理成功后,调用channel.basicAck(),手动签收,如果出现异常,则调用channel.basicNack()方法,让其自动重新发送消息。
在消费端的properties里加上配置
#改为ack模式的步骤:
#[1]配置文件中改为手动确认机制
#[2]当业务代码处理完毕后需要调用channel.basicAck(deliveryTag, multiple) 手动签收
#如果代码出现意外情况调用basicNack(deliveryTag, multiple, requeue)
spring.rabbitmq.listener.simple.acknowledge-mode=manual
/*
* 多个消费者同时收取消息,收取消息到一半,突然某个消费者挂掉,要保证此条消息不丢失,
* 就需要acknowledgement机制,就是消费者消费完要通知服务端,服务端才将数据删除
* 这样就解决了,及时一个消费者出了问题,没有同步消息给服务端,还有其他的消费端去消费,保证了消息不丢的case。
* */
@RabbitListener(queues = {"topic_config_queue02"})//队列名
//把收到的消息封装到一个Message对像中
public void myListener03(Message message, Channel channel) throws IOException {
//拿到消息的标识,用于手动签收删除队列消息
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
byte[] body = message.getBody();//获取消息内容
String msg=new String(body);//转换为字符,因为byte数组当不了消息
Map map = JSON.parseObject(msg, Map.class);//再把转为json的Map解析,因为存的是jison格式的map
System.out.println("收到的消息内容"+map);
//int c=10/0;
System.out.println("这里可以根据消息写自己的业务代码");
//参数:
// long deliveryTag,消息的标识
// boolean multiple 是否把该消息之前的消息也确认掉,确认了就会把它之前的消息删除了
channel.basicAck(deliveryTag,true); //手动签收就会删除消息(手动告诉rabbitmq队列删除该消息)
}catch (Exception e){
System.out.println("程序出现意外情况");
//boolean requeue 出现异常时是否要求队列继续发送该消息,一般会设置它继续发送 解开上面的异常 int c=10/0 就会一直发
channel.basicNack(deliveryTag,true,true);
}
}
消费掉就没有了
如果出现了异常就会一直发送
总结
保证消息的可靠性方式:
消息持久化: RabbitMQ的消息默认存在内存中的,一旦服务器意外挂掉,消息就会丢失 .
消息持久化需做到三点
Exchange设置持久化 Queue设置持久化 Message持久化发送:发送消息设置发送模式deliveryMode=2,代表持久化消息
-
保证消息的可靠性投递----生产方确认Confirm和Return机制
-
保证消息在队列中的可靠性.----持久化和rabbitmg集群模式
-
保证消息可靠的被消费----消费端采用ACK确认机制
6. 延迟队列
6.1 TTL
TTL 全称 Time To Live(存活时间/过期时间)。
当消息到达存活时间后,还没有被消费,会被自动清除。
RabbitMQ可以对消息设置过期时间,也可以对整个队列(Queue)设置过期时间。
图形化创建有过期时间的队列
1.为指定的队列设置过期时间
/*为整个队列设置过期时间*/
@Bean
public Queue queue2(){
// durable表示使用持久化( 队列名) withArgument表示设置过期时间,单位是毫秒
Queue queue02 = QueueBuilder.durable(queueName02).withArgument("x-message-ttl",10000).build();
return queue02;
}
注意:使用前要如果有之前的队列,需要把之前的队列删掉不然会报错
报错信息
Caused by: com.rabbitmq.client.ShutdownSignalException: channel error; protocol method: #method<channel.close>(reply-code=406, reply-text=PRECONDITION_FAILED - inequivalent arg 'x-message-ttl' for queue 'topic_config_queue02' in vhost '/': received the value '10000' of type 'signedint' but current is none, class-id=50, method-id=10)
at com.rabbitmq.client.impl.ChannelN.asyncShutdown(ChannelN.java:517)
at com.rabbitmq.client.impl.ChannelN.processAsync(ChannelN.java:341)
at com.rabbitmq.client.impl.AMQChannel.handleCompleteInboundCommand(AMQChannel.java:182)
at com.rabbitmq.client.impl.AMQChannel.handleFrame(AMQChannel.java:114)
at com.rabbitmq.client.impl.AMQConnection.readFrame(AMQConnection.java:739)
at com.rabbitmq.client.impl.AMQConnection.access$300(AMQConnection.java:47)
at com.rabbitmq.client.impl.AMQConnection$MainLoop.run(AMQConnection.java:666)
at java.lang.Thread.run(Thread.java:745)
2.为指定的消息设置过期时间
//如果队列设置了过期时问而消息也设置了过期时间。---谁的时问短采用谁的。
@Test
void testTtl(){
/*发送的消息*/
Map<String, Object> map = new HashMap<>();
map.put("orderId", UUID.randomUUID());
map.put("productId",6);
map.put("num",26);
/*设置消息过期时间*/
MessagePostProcessor messagePostProcessor=new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setExpiration("20000");//单位是毫秒
//message.getMessageProperties().setMessageId(
//
//
// ); message的id
return message;
}
};
rabbitTemplate.convertAndSend(RabbitMqConfig.exchange_name,"aaa.orange.rabbit",JSON.toJSONString(map),messagePostProcessor);
}
总结:
设置队列过期时间使用参数:x-message-ttl,单位:ms(毫秒),会对整个队列消息统一过期。
设置消息过期时间使用参数:expiration。单位:ms(毫秒),当该消息在队列头部时(消费时),会单独判断这一消息是否过期。
如果队列和消息都设置了过期时间,谁的时间短就用谁的
注意:如果队列里有未消费的消息,那么再添加过期时间的消息不会被删除
6.2 死信队列
死信队列,英文缩写:DLX 。Dead Letter Exchange(死信交换机),当消息成为Dead message后,可以被重新发送到另一个交换机,这个交换机就是DLX。而于该交换机绑定的队列就是死信队列
什么样的消息会成为死信消息
队列消息长度到达限制;
消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false;
原队列存在消息过期设置,消息到达超时时间未被消费;
队列绑定死信交换机:
图形化创建:
创建死信交换机和普通交换机
创建死信队列和普通队列 注意:普通队列需要绑定死信交换机
死信队列
普通交换机绑定普通队列
死信交换机绑定死信队列
代码发送
/*发给死信交换机*/
@Test
public void testDeadExchange(){
Map<String, Object> map = new HashMap<>();
map.put("orderId", UUID.randomUUID());
map.put("productId",6);
map.put("num",26);
map.put("money",200);
for (int i = 0; i < 10; i++) {
//广播模式没有路由key
rabbitTemplate.convertAndSend("pt_exchange","",JSON.toJSONString(map));
}
}
过了20秒就会进死信队列
过程是发送给普通交换机--普通交换机发给普通队列--普通队列过期时间到了--发给死信交换机--死信交换机发给死信队列
package com.xqw.config;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class MyRabbitListen {
// 死信队列==比如超过30分钟未支付,就到死信队列里了,可以从死信队列获取订单消息修改状态
@RabbitListener(queues = {"dead_queue"})
public void siXin(Message message, Channel channel){
byte[] body = message.getBody();
String msg=new String(body);
System.out.println("消息:==="+msg);
System.out.println("假装这里是三十分钟未支付取消订单的业务");
}
}
TTL+死信队列====>应用场景---延迟队列
6.3延迟队列
延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费。
需求:
下单后,30分钟未支付,取消订单,回滚库存。
新用户注册成功7天后,发送短信问候。
实现方式:
定时器:---springboot定时器。每个一秒查询数据库订单--当前时间比对--大于30--取消订单
延迟队列:
通过消息队列完成延迟队列的功能
很可惜,在RabbitMQ中并未提供延迟队列功能。
但是可以使用:TTL+死信队列 组合实现延迟队列的效果。
总结:
延迟队列 指消息进入队列后,可以被延迟一定时间,再进行消费。
RabbitMQ没有提供延迟队列功能,但是可以使用 : TTL + DLX 来实现延迟队列效果。
代码形式:
package com.xqw.config;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/*
* springBoot如果没有存在队列和交换机以及绑定关系如何处理:
* [1]在rabbitMQ图形化界面来创建队列交换机以及绑定关系[管理员]
* [2]在springboot中通过代码也可以。
* */
@Configuration
public class RabbitMqConfig {
//==========================================关于死信队列和死信交换机==================================================
private String pt_exchange="pt_exchange";
private String dead_exchange="dead_exchange";
private String pt_queue="pt_queue";
private String dead_queue="dead_queue";
//创建交换机
@Bean
public Exchange pt_exchange(){
Exchange build = ExchangeBuilder.topicExchange(pt_exchange).build();
return build;
}
//死信交换机
@Bean
public Exchange dead_exchange(){
Exchange build = ExchangeBuilder.topicExchange(dead_exchange).build();
return build;
}
//创建普通队列并绑定死信交换机
@Bean
public Queue pt_queue(){
Queue build = QueueBuilder.durable(pt_queue).
//过时超过10秒就会去死信交换机
withArgument("x-message-ttl", 20000).
//消息超过5条也会去
withArgument("x-max-length", 5).
//绑定的死信交换机
withArgument("x-dead-letter-exchange", dead_exchange).
//绑定死信路由key
withArgument("x-dead-letter-routing-key","*.rabbit.*")
.build();
return build;
}
//创建死信队列
@Bean
public Queue dead_queue(){
Queue build = QueueBuilder.durable(dead_queue).build();
return build;
}
//普通交换机绑定普通队列
@Bean
public Binding pt_exchange_queue(){
Binding binding= BindingBuilder.bind(pt_queue()).to(pt_exchange()).with("*.rabbit.*").noargs();
return binding;
}
//死信交换机绑定死信队列
@Bean
public Binding dead_exchange_queue(){
Binding binding= BindingBuilder.bind(dead_queue()).to(dead_exchange()).with("*.rabbit.*").noargs();
return binding;
}
}
生产者
/*发给死信交换机*/
@Test
public void testDeadExchange(){
Map<String, Object> map = new HashMap<>();
map.put("orderId", UUID.randomUUID());
map.put("productId",6);
map.put("num",26);
map.put("money",200);
rabbitTemplate.convertAndSend("pt_exchange","aaa.rabbit.bbb",JSON.toJSONString(map));
}
消费者:
package com.xqw.config;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class MyRabbitListen {
// 死信队列==比如超过30分钟未支付,就到死信队列里了,可以从死信队列获取订单消息修改状态
@RabbitListener(queues = {"dead_queue"})
public void siXin(Message message, Channel channel){
byte[] body = message.getBody();
String msg=new String(body);
System.out.println("消息:==="+msg);
System.out.println("假装这里是三十分钟未支付取消订单的业务");
}
}
7. rabbitmq如何保证幂等性
在编程中一个幂等操作的特点是其任意多次执行所产生的结果与一次执行的产生的结果相同,在mq中由于网络故障或客户端延迟消费mq自动重试过程中可能会导致消息的重复消费,那我们如何保证消息的幂等问题呢?也可以理解为如何保证消息不被重复消费呢,不重复消费也就解决了幂等问题。
解决方式:
1、生成全局id,存入redis或者数据库,在消费者消费消息之前,查询一下该消息是否有消费过。
2、如果该消息已经消费过,则告诉mq消息已经消费,将该消息丢弃(手动ack)。
3、如果没有消费过,将该消息进行消费并将消费记录写进redis或者数据库中。
简单描述一下需求,如果订单完成之后,需要为用户累加积分,又需要保证积分不会重复累加。那么再mq消费消息之前,先去数据库查询该消息是否已经消费,如果已经消费那么直接丢弃消息。
生产者
package com.score.producer;
import com.alibaba.fastjson.JSONObject;
import com.xiaojie.score.entity.Score;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.util.UUID;
/**
* @author
* @version 1.0
* @description:发送积分消息的生产者
* @date
*/
@Component
@Slf4j
public class ScoreProducer implements RabbitTemplate.ConfirmCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
//定义交换机
private static final String SCORE_EXCHANGE = "score_exchaneg";
//定义路由键
private static final String SCORE_ROUTINNGKEY = "score.add";
/**
* @description: 订单完成
* @param:
* @return: java.lang.String
* @author xiaojie
* @date:
*/
public String completeOrder() {
String orderId = UUID.randomUUID().toString();
System.out.println("订单已完成");
//发送积分通知
Score score = new Score();
score.setScore(100);
score.setOrderId(orderId);
String jsonMSg = JSONObject.toJSONString(score);
sendScoreMsg(jsonMSg, orderId);
return orderId;
}
/**
* @description: 发送积分消息
* @param:
* @param: message
* @param: orderId
* @return: void
* @author
* @date:
*/
@Async
public void sendScoreMsg(String jsonMSg, String orderId) {
this.rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.convertAndSend(SCORE_EXCHANGE, SCORE_ROUTINNGKEY, jsonMSg, message -> {
//设置消息的id为唯一
message.getMessageProperties().setMessageId(orderId);
return message;
});
}
@Override
public void confirm(CorrelationData correlationData, boolean ack, String s) {
if (ack) {
log.info(">>>>>>>>消息发送成功:correlationData:{},ack:{},s:{}", correlationData, ack, s);
} else {
log.info(">>>>>>>消息发送失败{}", ack);
}
}
}
消费者
package com.score.consumer;
import com.alibaba.fastjson.JSONObject;
import com.rabbitmq.client.Channel;
import com.xiaojie.score.entity.Score;
import com.xiaojie.score.mapper.ScoreMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Map;
/**
* @author
* @version 1.0
* @description: 积分的消费者
* @date
*/
@Component
@Slf4j
public class ScoreConsumer {
@Autowired
private ScoreMapper scoreMapper;
@RabbitListener(queues = {"score_queue"})
public void onMessage(Message message, @Headers Map<String, Object> headers, Channel channel) throws IOException {
String orderId = message.getMessageProperties().getMessageId();
if (StringUtils.isBlank(orderId)) {
return;
}
log.info(">>>>>>>>消息id是:{}", orderId);
String msg = new String(message.getBody());
Score score = JSONObject.parseObject(msg, Score.class);
if (score == null) {
return;
}
//执行前去数据库查询,是否存在该数据,存在说明已经消费成功,不存在就去添加数据,添加成功丢弃消息
Score dbScore = scoreMapper.selectByOrderId(orderId);
if (dbScore != null) {
//证明已经消费消息,告诉mq已经消费,丢弃消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
return;
}
Integer result = scoreMapper.save(score);
if (result > 0) {
//积分已经累加,删除消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
return;
} else {
log.info("消费失败,采取相应的人工补偿");
}
}
}