最全学习笔记,各种知识点及代码案例
一、RabbitMQ概念及作用
概念:是一个接收,存储,转发的消息中间件
作用:
(1)流量削峰:
比如:订单系统,最大能处理一万次请求,但是在高峰期来了两万次,那么只能进行限制。如果使用消息队列,那么就可以取消掉这个限制,使用消息队列作缓冲。在用户端就是有的下单后就成功,有的过十几秒后收到成功下单信息,总比下单失败的体验好
(2)应用解耦
可以提高可用性,使用户感受不到故障
3.异步处理
比如A调B,B需要很长时间才能响应,那么就要采用异步的方式去处理
基础概念可以参考此博客: link.
RabbitMQ的六大模式
(1)简单模式:简单的发送与接收,没有特别的处理
(2)工作模式:一个生产者端,多个消费者端
(3)发布订阅模式:一个生产者端,多个消费者端同时接收所有的消息
(4)路由模式:生产者按routing key发送消息,不同的消费者端按不同的routing key接收消息。
(5)主题模式(通配符):通过routing key根据一些规则进行匹配
(6)发布确认模式:手动ack确认,并可以进行相应处理
交换机的四种类型
Direct Exchange(直连交换机)
Fanout Exchange(扇型交换机)
Topic Exchange(主题交换机)
Headers Exchange(头交换机)
二、linux下载安装
(1)下载
Rabbitmq的下载地址:
https://rabbitmq.com/download.html
注意版本是有对应的
(2)安装
cd /opt
1.依次执行下面命令:
rpm -ivh erlang-21.3-1.el7.x86_64.rpm
yum install socat -y
rpm -ivh rabbitmq-server-3.8.8-1.el7.noarch.rpm
2.添加开机启动:
chkconfig rabbitmq-server on
3.启动:
/sbin/service rabbitmq-server start
(停止:)
( /sbin/service rabbitmq-server stop)
4.查看启动状态:
/sbin/service rabbitmq-server status
5.安装web管理界面,注意要先停止rabbitmq服务
rabbitmq-plugins enable rabbitmq_management
6.然后再启动
/sbin/service rabbitmq-server start
7.在浏览器访问(写你本机IP,可以通过 ip -addr查看) 192.168.188.112:15672
有的访问不到是因为没有关闭防火墙,可以执行下面命令
systemctl stop firewalld
systemctl enable firewalld
默认用户名和密码都是 guest
登录后发现不能登,是需要创建一个用户的
8.创建用户
添加用户
rabbitmqctl add_user admin 123
设置用户角色(这里设置的超级管理员)
rabbitmqctl set_user_tags admin administrator
设置用户权限(可读可写可配置)
rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"
查看用户列表
rabbitmqctl list_users
9.用创建的用户登录
下面就开始进入写代码阶段啦
三、代码案例准备工作
1.创建工程
2.引入依赖
<dependencies>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.8.0</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
</dependencies>
四、案例讲解
案例1:
简单队列模式
一个生产者,一个消费者
代码结构:
生产者代码:
//生产者:发消息
public class Producer {
//队列名称
public static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.188.112");
//用户名和密码
factory.setUsername("admin");
factory.setPassword("123");
//创建连接
Connection connection = factory.newConnection();
//获取信道
Channel channel = connection.createChannel();
//创建一个队列
/*参数
1.队列名称
2.队列消息是否持久化 默认不持久化
3.该队列是否只供一个消费者进行消费,是否进行消息共享,true可以多个消费者消费,默认false
4.是否自动删除,消费完后该队列是否删除
5.其他参数
*/
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//定义消息
/*参数
1.发送到哪个交换机
2.路由的key值是哪个,本次是队列的名称
3.其他参数信息
4.发送的消息消息体
*/
String message = "hello world";
//发送一个消息
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
System.out.println("消息发送完毕");
}
}
运行后可去web端查看
消费者代码:
//消费者
public class Consumer {
//队列名称
public static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws IOException, TimeoutException {
//创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.188.112");
factory.setUsername("admin");
factory.setPassword("123");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//声明 接收消息
DeliverCallback deliverCallback = (consumerTag,message)->{
System.out.println(new String(message.getBody()));
};
CancelCallback cancelCallback = consumerTag->{
System.out.println("消息消费被中断");
};
//取消消息
/*
消费消息
参数:
1.消费哪个队列
2.消费成功后是否自动应答
3.消费者未成功消费的回调
4.消费者取消消费者的回调
*/
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
执行后会发现,消息被成功消费
案例二
工作模式
N个生产者,多个消费者
1.抽取信道获取的工具类,避免重复写代码
public class RabbitMqUtils {
public static Channel geyChannel() throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.188.112");
factory.setUsername("admin");
factory.setPassword("123");
Connection connection = factory.newConnection();
return connection.createChannel();
}
}
2.创建消费者1和消费者2
//消费者1
public class Consumer01 {
//队列名称
public static final String QUEUE_NAME = "work";
public static void main(String[] args) throws IOException, TimeoutException {
//获取信道
Channel channel = RabbitMqUtils.geyChannel();
//这里就不讲解了,案例一已经讲解过了
//消息接收
DeliverCallback deliverCallback = (consumerTag, message)->{
System.out.println(new String(message.getBody()));
};
CancelCallback cancelCallback = consumerTag->{
System.out.println("消息消费被中断,也就是回调接口");
};
System.out.println("T1:等待消息中----");
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
创建消费者2
与上面代码一模一样复制一份
//消费者2
public class Consumer01 {
//队列名称
public static final String QUEUE_NAME = "work";
public static void main(String[] args) throws IOException, TimeoutException {
//获取信道
Channel channel = RabbitMqUtils.geyChannel();
//这里就不讲解了,案例一已经讲解过了
//消息接收
DeliverCallback deliverCallback = (consumerTag, message)->{
System.out.println(new String(message.getBody()));
};
CancelCallback cancelCallback = consumerTag->{
System.out.println("消息消费被中断,也就是回调接口");
};
System.out.println("T2:等待消息中----");
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
3.创建生产者
//生产者:发消息
public class Producer {
//队列名称
public static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.geyChannel();
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
for (int i = 0; i < 10; i++) {
String message = "hello world"+i;
//发送一个消息
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
System.out.println("消息发送完毕");
}
}
}
4.启动消费者1,消费者2,生产者
可以发现:默认是采用轮询去进行消费的
案例三
应答模式
1.首先先了解一些概念
消息应答机制
概念:为了保证消息不丢失,引入了消息应答机制,消息应答就是:消费者在接受到消息并且处理该消息后,告诉rabbitmq已经处理了,rabbitmq可以把消息删除了
1.1 自动应答
接收到消息后就会自动应答,不管后面程序有没有问题。这样就不是那么的靠谱
1.2 手动应答
(1) Channel.basicAck 用于肯定确认
(2) Channel.basicNack 用于否定确认(比第三种多一个参数multiple,批量处理参数)
(3) Channel.basicReject 用于拒绝确认
好处:可以批量应答并且减少网络拥堵
批量应答解释:
true: 该信道上,当第一个消息成功消费后,那么该信道上的所有消息都会应答
false:
消息自动重新入队机制
如果消费者由于某些原因失去了连接,导致消息未发生AKC曲儿,那么RabbitMQ会将该消息重新入队,并很快的将其重新分发给另一个消费者进行消费,这样即使某个消费者偶尔死亡,也可以确保不会丢失任何信息
2.编写测试用例
2.1创建生成者
//应答模式
public class Producer {
//队列名称
public static final String TASK_QUEUE_NAME = "ack_queue";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.geyChannel();
channel.queueDeclare(TASK_QUEUE_NAME,false,false,false,null);
Scanner sc = new Scanner(System.in);
while (sc.hasNext()){
String message = sc.next();
//发送一个消息
channel.basicPublish("",TASK_QUEUE_NAME,null,message.getBytes());
System.out.println("消息发送完毕");
}
}
}
2.2创建一个睡眠工具类,为了代码美观
public class SleepUtils {
public static void sleep(int second){
try {
Thread.sleep(second*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2.3创建消费者1
//应答模式
//消费者1
public class Consumer01 {
//队列名称
public static final String QUEUE_NAME = "ack_queue";
public static void main(String[] args) throws IOException, TimeoutException {
//获取信道
Channel channel = RabbitMqUtils.geyChannel();
//消息接收
DeliverCallback deliverCallback = (consumerTag, message)->{
//这里模拟消费的快的消费者
SleepUtils.sleep(1);
System.out.println(new String(message.getBody(),"UTF-8"));
//手动应答
/**
* 参数1:消息的标记 tag
* 参数2:是否批量应答
*/
channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
};
CancelCallback cancelCallback = consumerTag->{
System.out.println("消息消费被中断,也就是回调接口");
};
System.out.println("C1:等待消息中----");
//这里设置为false,不自动应答
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME,autoAck,deliverCallback,cancelCallback);
}
}
2.4创建消费者2
将消费者1的代码赋值过来,sleep改为30s
//应答模式
//消费者1
public class Consumer01 {
//队列名称
public static final String QUEUE_NAME = "ack_queue";
public static void main(String[] args) throws IOException, TimeoutException {
//获取信道
Channel channel = RabbitMqUtils.geyChannel();
//消息接收
DeliverCallback deliverCallback = (consumerTag, message)->{
//这里模拟消费的慢的消费者
SleepUtils.sleep(30);
System.out.println(new String(message.getBody(),"UTF-8"));
//手动应答
/**
* 参数1:消息的标记 tag
* 参数2:是否批量应答
*/
channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
};
CancelCallback cancelCallback = consumerTag->{
System.out.println("消息消费被中断,也就是回调接口");
};
System.out.println("C2:等待消息中----");
//这里设置为false,不自动应答
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME,autoAck,deliverCallback,cancelCallback);
}
}
启动生产者,消费者1,消费者2
生产者发送两条消息 aa,bb,可以观察到aa被C1消费了,而bb因为时间过长还未被C2消费掉,这时候把C2程序停了,会发现bb消息被C1成功消费了!
持久化相关知识点:
队列如何实现持久化
生产者方设置
只需要将参数改为true
启动后可能会报错,原因是该队列原本是不持久化的,不能强制变为持久化,需要去客户端将该队列删除后再重启就好了
持久的会显示一个D
消息如何实现持久化
生产者方设置
在发送消息的时候添加一个属性即可
MessageProperties.PERSISTENT_TEXT_PLAIN
注意:将消息标记为持久化并不能完全保证不会丢失消息,比如在准备存储到磁盘的时候,还没有存储完,消息还在缓存的一个间隔点,此时并没有真正的写入磁盘。持久性保证并不强,但是对于简单任务队列而言,这已经(绰绰有余)了
不公平分发
消费者方设置
可以看到上面的事例,消费端是轮询进行消费的,就是你一条,我一条,那么假如消费者1消费消息的能力强,而消费者2消费消息的能力弱,还才用轮询的方式的话,那么效率就十分低,这时候可以设置成不公平分发消息
在消费端设置
可以拿案例三进行测试
预期值
作用:限制缓冲区的大小,以避免缓冲区里面无限制的未确认消息问题
(相当于一次就取设置的数量的消息,理解成权重也行)
设置的basicQos不为1的时候就成了预期值
channel.basicQos(3);
发布确认
前提:
1.设置要求队列必须持久化
2.设置要求队列中的消息必须持久化
3.开启发布确认
channel.confirmSelect();
分类:
1.单个确认发布
这是一种简单的确认方式,它是一种**同步确认发布**的方式
缺点:发布速度特别慢,因为如果没有确认发布的消息就会阻塞所有后续消息的发布
相关方法: waitForConfirmsOrDie(long)
2.批量确认发布
批量的进行确认
3.异步确认发布
客户端噌噌噌的发就完事了,消费端 每当消费成功后就通过函数回调来进行确认,失败的也会进行回调并通知给客户端,消息会被标记上序号,代码也相当复杂。
//确认模式
/**
* 使用的时间来比较哪种确认方式是最好的
* 1.单个确认
* 2.批量确认
* 3.异步批量确认
*/
public class ConfirmMessage {
//队列名称
public static final String TASK_QUEUE_NAME = "ack_queue";
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
// 1.单个确认
// ConfirmMessage.publishMessageIndivdually();
// 2.批量确认
// ConfirmMessage.publishMessageBatch();
// 3.异步确认
ConfirmMessage.publishMessageAsync();
}
//单个确认 900ms
public static void publishMessageIndivdually() throws IOException, TimeoutException, InterruptedException {
Channel channel = RabbitMqUtils.geyChannel();
//队列声明
String queueName = "ack_queue_01";
channel.queueDeclare(queueName,true,false,false,null);
//开启发布确认
channel.confirmSelect();
//开始时间
long start = System.currentTimeMillis();
//--------------------------------------------------------------------
for (int i = 0; i < 1000; i++) {
String message = i + "message";
channel.basicPublish("",queueName,null,message.getBytes());
//单个消息马上进行发布确认(true表示确认成功)
boolean flag = channel.waitForConfirms();
}
//--------------------------------------------------------------------
//结束时间
long end = System.currentTimeMillis();
System.out.println(end-start+"ms");
}
//批量发布确认 135ms
public static void publishMessageBatch() throws IOException, TimeoutException, InterruptedException {
Channel channel = RabbitMqUtils.geyChannel();
//队列声明
String queueName = "ack_queue_02";
channel.queueDeclare(queueName,true,false,false,null);
//开启发布确认
channel.confirmSelect();
//开始时间
long start = System.currentTimeMillis();
//--------------------------------------------------------------------
//批量确认消息大小
int batchSize = 1000;
//批量发布消息 批量发布确认
for (int i = 0; i < 1000; i++) {
String message = i + "message";
channel.basicPublish("",queueName,null,message.getBytes());
//判断达到100条的时候,批量确认一次
if(i%batchSize == 0){
//发布确认
channel.waitForConfirms();
}
}
//--------------------------------------------------------------------
//结束时间
long end = System.currentTimeMillis();
System.out.println(end-start+"ms");
}
//异步发布确认 67ms
public static void publishMessageAsync() throws IOException, TimeoutException, InterruptedException {
Channel channel = RabbitMqUtils.geyChannel();
//队列声明
String queueName = "ack_queue_03";
channel.queueDeclare(queueName,true,false,false,null);
//开启发布确认
channel.confirmSelect();
//--------------------------------------------------------------------
/**
* 参数1:消息的标记,相当于每个消息的ID
* 参数2:是否为批量确认
*/
//消息确认成功,回调函数
ConfirmCallback ackCallback = (deliveryTag,multiple) ->{
System.out.println("确认的消息:"+deliveryTag);
//2.删除掉已经确认的消息,剩下的就是未确认的消息
};
//消息确认失败,回调函数
ConfirmCallback nackCallback = (deliveryTag,multiple) ->{
System.out.println("未确认的消息:"+deliveryTag);
//3.打印一下未确认的消息都有哪些
};
/** 准备消息的监听器,监听哪些消息成功了,哪些消息失败了
* 参数1:监听哪些消息成功了
* 参数2:监听哪些消息失败了
*/
channel.addConfirmListener(ackCallback,nackCallback); //异步通知
//开始时间
long start = System.currentTimeMillis();
//批量发布消息
for (int i = 0; i < 1000; i++) {
String message = i + "message";
channel.basicPublish("",queueName,null,message.getBytes());
}
//--------------------------------------------------------------------
//结束时间
long end = System.currentTimeMillis();
System.out.println(end-start+"ms");
}
}
五、交换机相关案例
1.概念:
绑定:是交换机与队列之间的桥梁,交换机中可以绑定多个队列,通过路由Key进行关联
1.类型
直接(路由类型)
主题
标题(头类型)
扇出(发布订阅)
案例四
发布订阅模式(FANOUT)
1.创建生产者代码
这里使用默认队列,即不去声明队列名
public class Producer {
//交换机的名称
public static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.geyChannel();
Scanner sc = new Scanner(System.in);
while (sc.hasNext()){
String message = sc.next();
channel.basicPublish(EXCHANGE_NAME,"",null,message.getBytes("UTF-8"));
System.out.println("生产者发出消息成功:"+message);
}
}
}
2.创建消费者1
//交换机
public class ReceiveLogs01 {
//交换机的名称
public static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.geyChannel();
//1.声明一个交换机
/**
* 参数1:交换机名称
* 参数2:交换机类型
*/
channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
//声明一个临时队列
/**
* 生成一个临时的队列,队列名称是随机的
* 当消费者断开与队列的连接时,队列就会自动删除
*/
String queueName = channel.queueDeclare().getQueue();
/**
* 绑定交换机与队列
* 参数1:队列名称
* 参数2:交换机名称
* 参数3,路由key
*/
channel.queueBind(queueName,EXCHANGE_NAME,"");
//接收消息
DeliverCallback deliverCallback = (consumerTag,message)->{
System.out.println("01收到的消息"+new String(message.getBody(),"UTF-8"));
};
channel.basicConsume(queueName,true,deliverCallback,consumerTag->{});
}
}
3.创建消费者2
//交换机
public class ReceiveLogs01 {
//交换机的名称
public static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.geyChannel();
//1.声明一个交换机
/**
* 参数1:交换机名称
* 参数2:交换机类型
*/
channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
//声明一个临时队列
/**
* 生成一个临时的队列,队列名称是随机的
* 当消费者断开与队列的连接时,队列就会自动删除
*/
String queueName = channel.queueDeclare().getQueue();
/**
* 绑定交换机与队列
* 参数1:队列名称
* 参数2:交换机名称
* 参数3,路由key
*/
channel.queueBind(queueName,EXCHANGE_NAME,"");
//接收消息
DeliverCallback deliverCallback = (consumerTag,message)->{
System.out.println("02收到的消息"+new String(message.getBody(),"UTF-8"));
};
channel.basicConsume(queueName,true,deliverCallback,consumerTag->{});
}
}
案例五
直接模式(DIRECT)
通过路由key,到指定的队列去进行消费
一个队列可以绑多个key
1.创建消费者1
public class ReceiveLogsDirect01 {
public static final String EXCHANGE_NAME ="direct_logs";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.geyChannel();
//声明交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
//声明一个队列
channel.queueDeclare("console",false,false,false,null);
/*
参数1:队列名
参数2:交换机名
参数3:路由key
*/
//一个队列可以绑定多个路由key
channel.queueBind("console",EXCHANGE_NAME,"info");
channel.queueBind("console",EXCHANGE_NAME,"warning");
//接收消息
DeliverCallback deliverCallback = (consumerTag, message)->{
System.out.println("ReceiveLogsDirect01收到的消息"+new String(message.getBody(),"UTF-8"));
};
//消费者消费
channel.basicConsume("console",true,deliverCallback,consumerTag->{});
}
}
2.创建消费者2
public class ReceiveLogsDirect02 {
public static final String EXCHANGE_NAME ="direct_logs";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.geyChannel();
//声明交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
//声明一个队列
channel.queueDeclare("disk",false,false,false,null);
/*
参数1:队列名
参数2:交换机名
参数3:路由key
*/
//一个队列可以绑定多个路由key
channel.queueBind("disk",EXCHANGE_NAME,"error");
//接收消息
DeliverCallback deliverCallback = (consumerTag, message)->{
System.out.println("ReceiveLogsDirect02收到的消息"+new String(message.getBody(),"UTF-8"));
};
//消费者消费
channel.basicConsume("disk",true,deliverCallback,consumerTag->{});
}
}
3.创建生产者
public class DorectLogs {
public static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.geyChannel();
Scanner sc = new Scanner(System.in);
while (sc.hasNext()){
String message = sc.next();
//发送一个消息
//这个是发给交换机,然后通过路由key去路由到相应的消费者
//这里的路由key可以换成别的再试试
//参数1:交换机名称 参数2:路由key 参数3:消息
channel.basicPublish(EXCHANGE_NAME,"error", null,message.getBytes("UTF-8"));
System.out.println("消息发送完毕");
}
}
}
案例六
主题模式(TOPIC)
规定:topic类型的路由key不能随意写,必须要满足一定的条件,它必须是一个单词列表,已点号分隔开。
比如 quick.orange.rabbit
在这个规则列表里,其中有两个替换符大家注意
*(星号):可以代替一个单词
#(井号):可以代替零个或者多个单词
例1:
Q1:路由key是 ( * . orange . *)
Q2:路由key是 ( * . * . rabbit ) 和 (lazy.#)
发送 quick.orange.rabbit时 --------------- 被队列Q1,Q2接收到
发送 lazy.orange.elephant时 --------------- 被队列Q1,Q2接收到
发送 quick.orange.fox时 --------------- 被队列Q1接收到
发送 lazy.pink.elephant时 --------------- 被队列Q2接收到
发送 quick.brown.fox时 -------------------不匹配任何绑定,不会被任何队列接收到,会被丢弃
当一个队列绑定键是#,那么这个队列将接收所有数据,就有点像fanout了
如果队列绑定键当中没有#合*出现,那么该队列绑定类型就是direct了
所以说主题模式包含了上面两个模式
1创建消费者1
public class ReceiveLogsTopic01 {
public static final String EXCHANGE_NAME ="topic_logs";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.geyChannel();
//声明交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
//声明一个队列 名称为Q1
channel.queueDeclare("Q1",false,false,false,null);
/*
参数1:队列名
参数2:交换机名
参数3:路由key
*/
//一个队列可以绑定多个路由key
channel.queueBind("Q1",EXCHANGE_NAME,"*.orange.*");
//接收消息
DeliverCallback deliverCallback = (consumerTag, message)->{
System.out.println("ReceiveLogsDirect01收到的消息"+new String(message.getBody(),"UTF-8"));
System.out.println("路由key:"+message.getEnvelope().getRoutingKey());
};
//消费者消费
channel.basicConsume("Q1",true,deliverCallback,consumerTag->{});
}
}
2创建消费者2
public class ReceiveLogsTopic02 {
public static final String EXCHANGE_NAME ="topic_logs";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.geyChannel();
//声明交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
//声明一个队列 名称为Q1
channel.queueDeclare("Q2",false,false,false,null);
/*
参数1:队列名
参数2:交换机名
参数3:路由key
*/
//一个队列可以绑定多个路由key
channel.queueBind("Q2",EXCHANGE_NAME,"lazy.#");
//接收消息
DeliverCallback deliverCallback = (consumerTag, message)->{
System.out.println("ReceiveLogsDirect01收到的消息"+new String(message.getBody(),"UTF-8"));
System.out.println("路由key:"+message.getEnvelope().getRoutingKey());
};
//消费者消费
channel.basicConsume("Q2",true,deliverCallback,consumerTag->{});
}
}
3创建生产者
//主题模式
public class TopicLogs {
public static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.geyChannel();
Scanner sc = new Scanner(System.in);
while (sc.hasNext()){
String message = sc.next();
//发送一个消息
//参数1:交换机名称 参数2:路由key 参数三:消息
//这里可以尝试这些key
//quick.orange.rabbit
//lazy.orange.fox
channel.basicPublish(EXCHANGE_NAME,"lazy.orange.topic", null,message.getBytes("UTF-8"));
System.out.println("消息发送完毕");
}
}
}
死信队列
概念:死信,顾名思义就是无法被消费的消息,这样的消息如果没有后续处理,就变成了死信,有死信自然就有了死信队列
应用场景:
1.为了保证订单业务的消息数据不丢失,需要使用RabbitMQ的死信队列机制,当消息消费发生异常时,将消息投入死信队列中。
2.还有比如:用户在商城下单成功并点击支付后,在指定时间未支付时自动失效
死信的来源
1.消息TTL过期
2.队列达到了最大长度
3.消息被拒绝
下面将由三个案例进行模拟三种死信产生
案例七(消息TTL过期产生死信)
说明:这里定义两个交换机,两个队列
交换机1:普通交换机 队列1 : 普通队列
交换机2:死信交换机 队列2:死信队列
队列1:路由key :zhangsan
队列2:路由key:lisi
给队列1绑定死信队列,即队列2,然后进行测试
1.普通队列消费者代码
//死信队列
public class Consumer01 {
//普通交换机
public static final String NORMAL_EXCHANGE = "normal_exchange";
//死信交换机
public static final String DEAD_EXCHANGE = "dead_exchange";
//普通队列
public static final String NORMAL_QUEUE = "normal_queue";
//死信队列
public static final String DEAD_QUEUE = "dead_queue";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.geyChannel();
//声明死信和普通交换机
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
Map<String,Object> arguments = new HashMap<>();
//过期时间 两处设置,普通队列这设置,或在生产方设置,一般在生产方设置
// arguments.put("x-message-ttl",10000);
//正常队列设置死信交换机
arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE);
//设置死信路由key
arguments.put("x-dead-letter-routing-key","lisi");
//声明死信和普通队列(第五个参数很重要)
channel.queueDeclare(NORMAL_QUEUE,false,false,false,arguments);
//----------------------
channel.queueDeclare(DEAD_QUEUE,false,false,false,null);
//绑定普通的交换机与队列
channel.queueBind(NORMAL_QUEUE,NORMAL_EXCHANGE,"zhangsan");
//绑定死信的交换机与队列
channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"lisi");
// -----------------------上面都是配置代码------------------------------
System.out.println("等待接收消息。。。");
//接收消息
DeliverCallback deliverCallback = (consumerTag, message)->{
System.out.println("普通队列收到的消息"+new String(message.getBody(),"UTF-8"));
System.out.println("路由key:"+message.getEnvelope().getRoutingKey());
};
channel.basicConsume(NORMAL_QUEUE,true,deliverCallback,consumerTag->{});
}
}
去控制台可观察到
2.生产者代码
public class Producer01 {
public static final String NORMAL_EXCHANGE = "normal_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.geyChannel();
//死信消息 设置TTL时间 5s,5s过后没有被消费则会自动进入死信队列
//AMQP参数
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("5000").build();
for (int i = 0; i < 11; i++) {
String message = "message"+i;
channel.basicPublish(NORMAL_EXCHANGE,"zhangsan",properties,message.getBytes("UTF-8"));
}
}
}
3死信队列消费者代码
//死信队列
public class Consumer02 {
//死信队列
public static final String DEAD_QUEUE = "dead_queue";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.geyChannel();
// -----------------------上面都是配置代码------------------------------
System.out.println("等待接收消息。。。");
//接收消息
DeliverCallback deliverCallback = (consumerTag, message)->{
System.out.println("死信队列收到的消息"+new String(message.getBody(),"UTF-8"));
System.out.println("路由key:"+message.getEnvelope().getRoutingKey());
};
channel.basicConsume(DEAD_QUEUE,true,deliverCallback,consumerTag->{});
}
}
4.测试
3.1 先启动生产者1的代码,创建出队列,交换机及绑定
3.2 停止掉生成者1的代码,模拟消费失败的情况,看消息能否进入到死信队列
3.3 启动普通队列消费者代码,然后去Rabbitmq控制台观察会发现,五秒后,消息全部堆积到了死信队列上
3.4 启动死信队列消费者代码,会发现刚才消费失败的消息在死信队列进行消费了
案例七(队列达到最大长度)
更改上面代码即可
1.设置队列最大长度,修改Consumer01 中部分代码
//设置正常队列长度限制
arguments.put("x-max-length",5);
2.在控制台中删除普通队列然后再重启Consumer01 ,可以观察到控制台的变化
3.消息生产者代码
//进入死信的方式1:设置最大长度
public class Producer02 {
public static final String NORMAL_EXCHANGE = "normal_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtils.geyChannel();
//发送消息
for (int i = 0; i < 10; i++) {
String message = "message"+i;
System.out.println("发送消息····"+message);
channel.basicPublish(NORMAL_EXCHANGE,"zhangsan",null,message.getBytes("UTF-8"));
}
}
}
4.测试
- 停掉消费者Consumer02 和 Consumer01
- 启动生产者代码
- 观察控制台,会发现 死信队列中堆积了五条,普通队列中堆积了五条
案例八(消息被拒绝)
1.修改Consumer01 中部分代码
2.修改Consumer01 中部分代码,修改为手动应答,并拒绝掉某一条消息
截图部分替换为以下部分,做的改动主要是,设置成手动应答,然后拒绝掉其中一条消息
DeliverCallback deliverCallback = (consumerTag, message)->{
String msg = new String(message.getBody());
if(msg.equals("message2")){
System.out.println("此消息被拒绝:"+msg);
//参数1:标号 参数2:表示是否批量应答
channel.basicReject(message.getEnvelope().getDeliveryTag(),false);
}else {
System.out.println("普通队列收到的消息"+new String(message.getBody(),"UTF-8"));
channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
}
};
channel.basicConsume(NORMAL_QUEUE,false,deliverCallback,consumerTag->{});
3.测试
3.1 去控制台删除普通队列
3.2 启动普通消费者Consumer01,停止死信消费者Consumer02
3.3 启动生产者Producer02
3.4 观察控制台会发现,其中有一条消息被拒绝了,并且在死信队列中
3.5 启动死信消费者Consumer02后,该 消息被消费
六、高级部分及案例
以下案例整合springboot进行实现
案例九 延迟队列
通过设置ttl过期时间+死信队列 来实现
1.新建子项目springboot-rabbitmq
继续NEXT,不选任何起步依赖,后面自己引入
2.引入依赖,编写yml文件
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</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>
<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>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.58</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
yml
spring:
rabbitmq:
host: 192.168.188.112
port: 5672
username: admin
password: 123
配置类:在上面的代码中,队列,交换机及绑定关系的声明都是在消费者或者生产者方去声明出来的,在springboot整合中,这些关系要写在配置类中,配置类只关注于这些配置信息
下面根据这个图去写配置类
X生成者,Y消费者
XA,XB 交换机
QA,QB,QC 队列
QD 死信队列
3.编写配置类
/**
* TTL队列 配置文件类
*/
@Configuration
public class TtlQueueConfig {
//普通交换机名称
public static final String X_EXCHANGE = "X";
//死信交换机名称
public static final String Y_DEAD_LETTER_EXCHANGE = "Y";
//普通队列名称
public static final String QUEUE_A = "QA";
public static final String QUEUE_B = "QB";
//死信队列名称
public static final String DEAD_LETTER_D = "QD";
//通用延迟队列
public static final String QUEUE_C = "QC";
//-------------声明交换机---------------
//声明普通交换机
@Bean("xExchange")
public DirectExchange xExchange(){
return new DirectExchange(X_EXCHANGE);
}
//声明死信交换机
@Bean("yExchange")
public DirectExchange YExchange(){
return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);
}
//-------------声明队列-----------------
//声明普通队列A,B
@Bean("queueA")
public Queue queueA(){
Map<String,Object> arguments = new HashMap<>();
//设置死信交换机,设置死信Key,设置过期时间(ms)
arguments.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);
arguments.put("x-dead-letter-routing-key","YD");
arguments.put("x-message-ttl",10000);
return QueueBuilder.durable(QUEUE_A).withArguments(arguments).build();
}
@Bean("queueB")
public Queue queueB(){
Map<String,Object> arguments = new HashMap<>();
//设置死信交换机,设置死信Key,设置过期时间(ms)
arguments.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);
arguments.put("x-dead-letter-routing-key","YD");
arguments.put("x-message-ttl",40000);
return QueueBuilder.durable(QUEUE_B).withArguments(arguments).build();
}
//声明死信队列
@Bean("queueD")
public Queue queueD(){
return QueueBuilder.durable(DEAD_LETTER_D).build();
}
//声明通用延迟队列
@Bean("queueC")
public Queue queueC(){
Map<String,Object> arguments = new HashMap<>();
//设置死信交换机,设置死信Key,设置过期时间(ms)
arguments.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);
arguments.put("x-dead-letter-routing-key","YD");
return QueueBuilder.durable(QUEUE_C).withArguments(arguments).build();
}
//--------------绑定关系----------------------
@Bean
public Binding queueABindingX(@Qualifier("queueA") Queue queueaA,
@Qualifier("xExchange") DirectExchange xExchange){
//队列,交换机,routingKey
return BindingBuilder.bind(queueaA).to(xExchange).with("XA");
}
@Bean
public Binding queueBBindingX(@Qualifier("queueB") Queue queueaB,
@Qualifier("xExchange") DirectExchange xExchange){
//队列,交换机,routingKey
return BindingBuilder.bind(queueaB).to(xExchange).with("XB");
}
@Bean
public Binding queueDBindingY(@Qualifier("queueD") Queue queueaD,
@Qualifier("yExchange") DirectExchange yExchange){
//队列,交换机,routingKey
return BindingBuilder.bind(queueaD).to(yExchange).with("YD");
}
@Bean
public Binding queueCBindingX(@Qualifier("queueC") Queue queueaC,
@Qualifier("xExchange") DirectExchange xExchange){
//队列,交换机,routingKey
return BindingBuilder.bind(queueaC).to(xExchange).with("XC");
}
}
swagger是个自动生成接口文档的工具,这里之后用于测试
@Configuration
@EnableSwagger2
public class SwaggerConfig {
public Docket webApiConfig(){
return new Docket(DocumentationType.SWAGGER_2)
.groupName("webApi")
.apiInfo(webApiInfo())
.select()
.build();
}
public ApiInfo webApiInfo(){
return new ApiInfoBuilder()
.title("rabbitmq接口文档")
.description("本文档描述了rabbitmq微服务接口定义")
.version("1.0")
.contact(new Contact("gzx","www.baidu.com","7852@qq.com"))
.build();
}
}
4.编写生产者
@Controller
@RequestMapping("/ttl")
public class SendMsgController {
@Autowired
private RabbitTemplate rabbitTemplate;
//开始发消息
@GetMapping("/sendMsg/{message}")
public void sendMsg(@PathVariable String message){
//参数1 交换机名 参数2 routingKey 参数3 消息
rabbitTemplate.convertAndSend("X","XA","消息来自ttl为10s的队列:"+message);
rabbitTemplate.convertAndSend("X","XB","消息来自ttl为40s的队列:"+message);
}
//开始发消息 消息 TTL 可以自己设置过期时间
@GetMapping("/sendMsg/{message}/{ttlTime}")
public void sendTimeMsg(@PathVariable("message") String message,@PathVariable("ttlTime") String ttlTime){
log.info("当前时间:{},发送死信队列的消息:{},延迟时间为: {}",new Date().toString(),message,ttlTime);
//参数1 交换机名 参数2 routingKey 参数3 消息
rabbitTemplate.convertAndSend("X","XC",message,msg->{
//发送消息的时候,设置延迟时长
msg.getMessageProperties().setExpiration(ttlTime);
return msg;
});
}
}
5.编写消费者
@Slf4j
@Controller
@RequestMapping("/ttl")
public class SendMsgController {
@Autowired
private RabbitTemplate rabbitTemplate;
//开始发消息
@GetMapping("/sendMsg/{message}")
public void sendMsg(@PathVariable String message){
log.info("当前时间:{},发送死信队列的消息:{}",new Date().toString(),message);
//参数1 交换机名 参数2 routingKey 参数3 消息
rabbitTemplate.convertAndSend("X","XA","消息来自ttl为10s的队列:"+message);
rabbitTemplate.convertAndSend("X","XB","消息来自ttl为40s的队列:"+message);
}
}
6.测试
启动项目
网址输入:localhost:8080/ttl/sendMsg/哈哈哈发个消息
观察控制台
10s后 收到
40s后 收到
上面就是延迟队列的测试了,但是实际生产中,延迟时间肯定是不能像上面这样写死的,下面进行改进,使得延迟时间可以随意填写
1.在配置类中添加队列C的配置
//通用延迟队列
public static final String QUEUE_C = "QC";
//声明通用延迟队列
@Bean("queueC")
public Queue queueC(){
Map<String,Object> arguments = new HashMap<>();
//设置死信交换机,设置死信Key,设置过期时间(ms)
arguments.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);
arguments.put("x-dead-letter-routing-key","YD");
return QueueBuilder.durable(QUEUE_C).withArguments(arguments).build();
}
@Bean
public Binding queueCBindingX(@Qualifier("queueC") Queue queueaC,
@Qualifier("xExchange") DirectExchange xExchange){
//队列,交换机,routingKey
return BindingBuilder.bind(queueaC).to(xExchange).with("XC");
}
2.编写测试接口
//开始发消息 消息 TTL
@GetMapping("/sendMsg/{message}/{ttlTime}")
public void sendTimeMsg(@PathVariable("message") String message,@PathVariable("ttlTime") String ttlTime){
log.info("当前时间:{},发送死信队列的消息:{},延迟时间为:{}",new Date().toString(),message,ttlTime);
//参数1 交换机名 参数2 routingKey 参数3 消息
rabbitTemplate.convertAndSend("X","XC",message,msg->{
//发送消息的时候,设置延迟时长
msg.getMessageProperties().setExpiration(ttlTime);
return msg;
});
}
3.进行测试
固定好过期时间的接口测试:
3.1 网址输入
http://localhost:8080/ttl/sendMsg/哈哈哈哈/4000
http://localhost:8080/ttl/sendMsg/哈哈哈哈/20000
3.2 然后观察控制台
4s后消费一条
20s后消费一条
自定义过期时间的接口测试:
3.3 网址输入
http://localhost:8080/ttl/sendMsg/哈哈哈哈/2000
http://localhost:8080/ttl/sendMsg/哈哈哈哈/20000
3.4 然后观察控制台
2s后消费一条
20s后消费一条
注意:如果先发送20s的,再发送2的,会发现2s后消费的消息也会在第20s的时候才会被消费
3.5
网址输入
http://localhost:8080/ttl/sendMsg/哈哈哈哈/2000
http://localhost:8080/ttl/sendMsg/哈哈哈哈/20000
3.4 然后观察控制台
20s后消费了两条
原因:因为队列是FIFO,先进先出的特性,因此第二条消息也会等到第一条消息被消费后才能被消费
因此:这种延迟队列的实现是有弊端的
解决办法:下载一个延迟队列的插件
插件下载链接: 点击跳转.
进入到RabbitMQ的安装目录下的plgins目录,执行下面命令让该插件生效,然后重启RabbitMQ
1.将插件拷贝到下面这个路径下
/usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins
cp rabbitmq_delayed_message_exchange-3.8.0.ez /usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins
cd /usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins
2.安装插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
3.重启rabbitmq
systemctl restart rabbitmq-server
4.检查是否安装成功
在控制台新增交换机选择类型时会多出来一项
案例十 ------ 延迟队列(基于插件的实现)正确用法!
概念:基于死信队列的实现,消息的延迟是在队列中去实现的,而基于插件的延迟是在交换机中去实现的,因此更为方便,并且解决了上面的问题
案例实现
1.编写配置类
@Configuration
public class DelayedQueueConfig {
//队列名
public static final String DELAYED_QUEUE_NAME = "delayed.queue";
//交换机名
public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
//路由key
public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";
//声明队列
@Bean
public Queue delayedQueue(){
return new Queue(DELAYED_QUEUE_NAME);
}
//声明交换机
@Bean
public CustomExchange delayedExchage(){
/**
* 参数1:交换机名称
* 参数2:交换机的类型
* 参数3:是否需要持久化
* 参数4:是否需要自动删除
* 参数5:自定义参数
*/
Map<String,Object> arguments = new HashMap<>();
arguments.put("x-delayed-type","direct");
return new CustomExchange(DELAYED_EXCHANGE_NAME,"x-delayed-message",true,false,arguments);
}
//绑定
public Binding delayedQueueBindingDelayedExchange(
@Qualifier("delayedQueue") Queue delayedQueue,
@Qualifier("delayedExchage") CustomExchange delayedExchage){
return BindingBuilder.bind(delayedQueue).to(delayedExchage).with(DELAYED_ROUTING_KEY).noargs();
}
}
2.编写发送者
在SendMsgController里增加下面代码
//基于插件的 发消息 消息及延迟时间
@GetMapping("/sendDelayedTimeMsg/{message}/{delayTime}")
public void sendDelayedTimeMsg(@PathVariable("message") String message,@PathVariable("delayTime") Integer delayTime){
log.info("当前时间:{},发送死信队列的消息:{},延迟时间为: {}",new Date().toString(),message,delayTime);
//参数1 交换机名 参数2 routingKey 参数3 消息
rabbitTemplate.convertAndSend("delayed.exchange","delayed.routingkey",message,msg->{
//发送消息的时候,设置延迟时长
msg.getMessageProperties().setDelay(delayTime);
return msg;
});
}
3.编写消费者
//基于插件-延迟队列消费者
@Component
@Slf4j
public class DelayQueueConsumer {
@RabbitListener(queues = "delayed.queue")
public void receiveDealyQueue(Message message) throws Exception{
String msg = new String(message.getBody());
log.info("基于插件---当前时间:{},收到延迟队列的消息:{}",new Date().toString(),msg);
}
}
4.测试
启动项目
在网址输入:
http://localhost:8080/ttl/sendDelayedTimeMsg/哈哈哈哈/20000
http://localhost:8080/ttl/sendDelayedTimeMsg/哈哈哈哈/2000
在控制台可以看到:
可以发现,先发送延迟20s的消息后消费了,2s延迟的正常先消费了
发布确认高级
引论:消息从生产者到交换机的过程失败,以及消息从交换机路由到消费者的时候失败,这两种情况下的处理
配置文件需要添加spring.rabbitmq.publisher-confirm-type=correlated
- NONE
禁用发布确认模式,是默认值 - CORRELATED
发布消息成功到交换机后会触发回调方法 - SIMPLE
相当于之前的单个确认
1.编写配置类
//发布确认(高级)
@Configuration
public class ConfirmConfig {
//队列名
public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
//交换机名
public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
//路由key
public static final String CONFIRM_ROUTING_KEY = "confirm.routingkey";
//声明队列
@Bean("confirmQueue")
public Queue confirmQueue(){
return new Queue(CONFIRM_QUEUE_NAME);
}
//声明交换机
@Bean("confirmExchage")
public DirectExchange confirmExchage(){
return new DirectExchange(CONFIRM_EXCHANGE_NAME);
}
//绑定
@Bean
public Binding confirmQueueBindingDelayedExchange(
@Qualifier("confirmQueue") Queue confirmQueue,
@Qualifier("confirmExchage") DirectExchange confirmExchage){
return BindingBuilder.bind(confirmQueue).to(confirmExchage).with(CONFIRM_ROUTING_KEY);
}
}
2.编写发送者
添加到controller里
//发布确认 发消息
@GetMapping("/sendConfirmMsg/{message}")
public void sendConfirmTimeMsg(@PathVariable("message") String message){
//这个对象用于给回调接口传送参数信息
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
log.info("当前时间:{},发送发布确认的消息:{}",new Date().toString(),message);
//参数1 交换机名 参数2 routingKey 参数3 消息 参数4 用于回调的消息
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,ConfirmConfig.CONFIRM_ROUTING_KEY,message,correlationData);
}
3.编写消费者
@Component
@Slf4j
public class ConfirmConsumer {
@RabbitListener(queues = ConfirmConfig.CONFIRM_QUEUE_NAME)
public void receiveDealyQueue(Message message) throws Exception{
String msg = new String(message.getBody());
log.info("确认发布高级:{},收到的消息:{}",new Date().toString(),msg);
}
}
4.编写交换机接收消息成功与失败回调接口
@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void init(){
rabbitTemplate.setConfirmCallback(this);
}
/**
*
* @param correlationData 保存回调消息的ID及相关信息
* @param ack 交换机是否收到消息
* @param s null(交换机收到消息) 失败原因(交换机未收到消息)
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String s) {
if(ack){
log.info("交换机收到ID为:{}的消息",correlationData.getId());
}else {
log.info("交换机未收到ID为:{}的消息,原因{}",correlationData.getId(),s);
}
}
}
5.测试
1.在浏览器输入
http://localhost:8080/ttl/sendConfirmMsg/哈哈哈
2.观察到控制台,消息消费成功
3.修改发送消息方,将交换机名称换成一个不存在的交换机名称,比如加个123
4.然后再在浏览器输入http://localhost:8080/ttl/sendConfirmMsg/哈哈哈
会发现走了回调接口中的未成功发送的方法
注意:当交换机名称正确,而路由key不存在时,这时候发送出的消息会丢失掉,并不会走回调方法,要想走回调需要下面的步骤
6.实现队列的回调接口
添加如下内容到上面的 MyCallBack 类
public class MyCallBack implements RabbitTemplate.ConfirmCallback , RabbitTemplate.ReturnsCallback
@PostConstruct
public void init(){
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnsCallback(this);
}
//只有消息不可达目的地时候 将 消息返还给生产者
@Override
public void returnedMessage(ReturnedMessage message) {
log.error("消息{},被交换机{}退回,退回原因:{},路由key:{}",
message.getMessage().getBody(),message.getExchange(),message.getReplyText(),message.getRoutingKey());
}
7.测试交换机未到队列的消息的回退
1.修改接口,发送到一个不存在的路由key
2.在浏览器输入
http://localhost:8080/ttl/sendConfirmMsg/%E5%93%88%E5%93%88%E5%93%88%E5%93%88
3.观察控制台
总结
1.ConfirmCallback 接口的实现是判断并回调的 消息从生产者到交换机这个过程的成功与失败
2.ReturnsCallback 接口的实现是判断并回调的 消息从交换机到目的地队列这个过程的成功与失败
备份交换机
引论:当生产者发送消息到交换机A失败,会再将消息转发到交换机B上,这样的好处是可以防止消息丢失以及对消息的告警,一般备份交换机使用广播类型
这里新增一个备份交换机,两个队列,然后作为上面确认交换机的备份交换机
在上面的确认案例进行更改
1.在配置类ConfirmConfig中增加如下内容
//------备份交换机
//队列名
public static final String BACKUP_QUEUE_NAME = "backup.queue";
public static final String WARNING_QUEUE_NAME = "warning.queue";
//交换机名
public static final String BACKUP_EXCHANGE_NAME = "backup.exchange";
//-------------
//-----备份交换机的配置
//声明队列
@Bean("backupQueue")
public Queue backupQueue(){
return new Queue(BACKUP_QUEUE_NAME);
}
@Bean("warningQueue")
public Queue warningQueue(){
return new Queue(WARNING_QUEUE_NAME);
}
//声明交换机
@Bean("backupExchage")
public FanoutExchange backupExchage(){
return new FanoutExchange(BACKUP_EXCHANGE_NAME);
}
//绑定
@Bean
public Binding backupQueueBindingbackupExchage(
@Qualifier("backupQueue") Queue backupQueue,
@Qualifier("backupExchage") FanoutExchange backupExchage) {
return BindingBuilder.bind(backupQueue).to(backupExchage);
}
@Bean
public Binding warningQueueBindingbackupExchage(
@Qualifier("warningQueue") Queue warningQueue,
@Qualifier("backupExchage") FanoutExchange backupExchage){
return BindingBuilder.bind(warningQueue).to(backupExchage);
}
2.修改要备份的交换机的配置代码
增加配置备份交换机的属性
//声明交换机
@Bean("confirmExchage")
public DirectExchange confirmExchage(){
return ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME).durable(true).withArgument("alternate-exchange",BACKUP_EXCHANGE_NAME).build();
}
3.编写备份交换机绑定队列的消息接收
@Component
@Slf4j
public class WarningConsumer {
//接收报警消息
@RabbitListener(queues = ConfirmConfig.WARNING_QUEUE_NAME)
public void receiveDealyQueue1(Message message) throws Exception{
String msg = new String(message.getBody());
log.info("备份交换机转发---接收到报警消息:{},收到的消息:{}",new Date().toString(),msg);
}
//接收报警消息
@RabbitListener(queues = ConfirmConfig.BACKUP_QUEUE_NAME)
public void receiveDealyQueue2(Message message) throws Exception{
String msg = new String(message.getBody());
log.info("备份交换机转发---接收到消息:{},收到的消息:{}",new Date().toString(),msg);
}
}
4.修改生产者,使得原交换机不能路由到指定交换机,从而走备份交换机
5.测试
网址输入:http://localhost:8080/ttl/sendConfirmMsg/123
观察控制台:
原交换机没有将消息成功路由,从而走了备份交换机
并且,备份交换机的优先级高于发布确认中配置的消息不可达时所调用的方法。
综上所述:备份交换机的意义就在于,当原交换机收到消息后,未能成功路由消息,从而走备份交换机进行消息的传递
消息重复消费
MQ在返回ack确认时发生网络中断,导致消息重复消费
解决思路:
1.保证幂等性,一般是使用全局ID,每条消息都有自己唯一的ID,每次消费消息时先判断一下该ID消息是否被消费过,这个ID可以有两种方式,一种是通过唯一ID+ 指纹码机制(指纹码就是指基于业务拼接出来的),另一种就是使用Redis的setnx命令,它天然具有幂等性
setnx():
将 key 的值设为 value,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不做任何动作。
优先级队列
设置优先级,根据优先级进行消息的消费
惰性队列
消息保存在内存中还是保存在磁盘中
正常队列:消息保存在内存中
惰性队列:消息保存在磁盘中
使用场景:大量消息不能消费而堆积的时候,很占内存
Rabbitmq集群的搭建
1.准备两台虚拟机
修改两个主机名称为node1 , node2
vi /etc/hostname
2.配置各个节点的hosts文件,让各个节点都能互相认识对方
vi /etc/hostsx
3.确保各个文件使用的cookie是一个值
分别执行命令
node1执行:
scp /var/lib/rabbitmq/.erlang.cookie root@node2:/var/lib/rabbitmq/.erlang.cookie
node2执行
scp /var/lib/rabbitmq/.erlang.cookie root@node1:/var/lib/rabbitmq/.erlang.cookie
4.启动RabbitMQfuwu,顺带启动Erlang虚拟机和RabbitMQ应用服务(两台节点上分别执行下面命令)
5.在node2上执行下面命令(相当于把node2加入到node1中,主要步骤)
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster rabbit@node1
rabbitmqctl start_app
6.查看集群状态
rabbitmqctl cluster_status
7.需要重新设置用户
添加用户
rabbitmqctl add_user admin 123
设置用户角色(这里设置的超级管理员)
rabbitmqctl set_user_tags admin administrator
设置用户权限(可读可写可配置)
rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"
查看用户列表
rabbitmqctl list_users
8.登录到控制台
192.168.188.110.15762
192.168.188.112.15762
任意一台都可以
补充:
脱离集群命令
在 node1上执行
注意:
假设在node1上创建一个队列hello,发送消息后,不进行消费,然后把node1宕机了,消息会被node2消费吗?
答案是不能,在node1上创建那只能是在node1上
解决办法:配置镜像队列
配置镜像队列(才能使得集群起到集群的作用)
1.登录进入控制台
2.创建策略
说明:
ha-mode( 备机模式):exactly( 指定模式)
ha-params(备份几份,备份几个节点):2
ha-sync-mode(备份模式):automatic(自动)
测试:创建一个mirrior_hello队列,不进行消费,去控制台看
测试:
这时候再进行上面的操作,在node1上创建一个队列hello,发送消息后,不进行消费,然后把node1宕机了,然后启动消费者,会发现消息会被成功消费,因为该消息成功备份了
集群下实现负载均衡
可以使用nginx或使用keeplived