1、概念
1.1、什么是mq(message queue)
本质上是一个队列,FIFO先进先出,不过在队列中存放的内容是message
1.2、mq能做什么
1、流量消峰
如果订单系统最多能处理一万次订单,这个处理能力应付正常时段的下单时绰绰有余,正常时段我们下单一秒后就能返回结果。但是在高峰期,如果有两万次下单操作系统是处理不了的,只能限制订单超过一万后不允许用户下单。使用消息队列做缓冲,我们可以取消这个限制,把一秒内下的订单分散成一段时间来处理,这时有些用户可能在下单十几秒后才能收到下单成功的操作,但是比不能下单的体验要好。
简单来说: 就是在访问量剧增的情况下,但是应用仍然不能停,比如“双十一”下单的人多,但是淘宝这个应用仍然要运行,所以就可以使用消息中间件采用队列的形式减少突然访问的压力
2、应用解耦
以电商应用为例,应用中有订单系统、库存系统、物流系统、支付系统。用户创建订单后,如果耦合调用库存系统、物流系统、支付系统,任何一个子系统出了故障,都会造成下单操作异常。当转变成基于消息队列的方式后,系统间调用的问题会减少很多,比如物流系统因为发生故障,需要几分钟来修复。在这几分钟的时间里,物流系统要处理的内存被缓存在消息队列中,用户的下单操作可以正常完成。当物流系统恢复后,继续处理订单信息即可,中间用户感受不到物流系统的故障,提升系统的可用性。
3、异步处理
有些服务间调用是异步的,例如 A 调用 B,B 需要花费很长时间执行,但是 A 需要知道 B 什么时候可以执行完,以前一般有两种方式,A 过一段时间去调用 B 的查询 api 查询。或者 A 提供一个 callback api, B 执行完之后调用 api 通知 A 服务。
这两种方式都不是很优雅,使用消息总线,可以很方便解决这个问题,A 调用 B 服务后,只需要监听 B 处理完成的消息,当 B 处理完成后,会发送一条消息给 MQ,MQ 会将此消息转发给 A 服务。这样 A 服务既不用循环调用 B 的查询 api,也不用提供 callback api。同样 B 服务也不用做这些操作。A 服务还能及时的得到异步处理成功的消息。
1.3、rabbitMQ的特点
1、性能好时效性微秒级,社区活跃度高,erlang语言编写,并发好,有管理界面,管理方便,适合数据量不是特别大时选择
1.4、工作原理
Broker:接收和分发消息的应用,一个broker就是一个rabbitMQ
channel:轻量级的连接工具
exchange:message到达broker的第一站,根据分发规则,匹配查询表中的routing key,分发到queue中
queue:消息被推送到这里,等待customer取走
2、rabbitMQ安装
2.1、下载erlang、rabbitMQ,二者版本要和linux版本保持一致(centOS为例)
erlang:rabbitmq/erlang - Packages · packagecloud
rabbitMQ:https://github.com/rabbitmq/rabbitmq-server/releases/tag/v3.8.8
下载完成后执行:
rpm -ivh erlang-22.3-1.el7.x86_64.rpm 安装erlang
yum install socat -y 安装socat环境
rpm -ivh rabbitmq-server-3.8.8-1.el7.noarch.rpm 安装rabbitMQ
erl -v 检查erlang安装是否成功
systemctl start rabbitmq-server 启动mq
systemctl status rabbitmq-server 查看mq状态
systemctl stop rabbitmq-server 停止mq
systemctl restart rabbitmq-server 重启
2.2、访问rabbitmq后台管理界面
1、停止rabbitMQ服务
systemctl stop rabbitmq-server
2、开启 web 管理插件
rabbitmq-plugins enable rabbitmq_management
3、添加新账号、设置用户角色、设置用户权限
rabbitmqctl add_user admin 123 新账号
rabbitmqctl set_user_tags admin administrator 用户角色
rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*" 权限(表示拥有所有资源的配置、读写权限)
4、查看当前用户和角色
rabbitmqctl list_users
5、访问web管理界面
linux的ip:15672
http://192.168.150.131:15672/http://192.168.150.131:15672/
3、生产者和消费者案例
3.1、单个生产者,单个消费者
生产者
package com.agw.test1;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author angw
* @version 1.0
* @Title:
* @date 2022/12/27 10:49
*/
public class Producer {
public static final String QUEUE_NAME = "hello2";
public static void main(String[] args) throws IOException, TimeoutException {
//创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.150.131");
factory.setUsername("admin");
factory.setPassword("123");
Connection connection = factory.newConnection();
//获取连接channel
Channel channel = connection.createChannel();
/*
生成一个队列
1、队列名称
2、队列消息是否持久化
3、是否只让一个消费者独自消费该队列消息
4、消费者断开连接之后是否删除该队列
5、其他参数
*/
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
/*
发送消息
1、交换机名称,默认空字符串标识
2、路由的key
默认采用队列名称作为路由key,消息会路由到该队列中
3、其他参数
4、发送消息的内容
*/
String message = "hello world";
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
System.out.println("发送了一条消息");
channel.close();
connection.close();
}
}
消费者
package com.agw.test1;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author angw
* @version 1.0
* @Title:消费者
* @date 2022/12/27 11:49
*/
public class Customer {
//队列名称
public static final String QUEUE_NAME = "hello2";
public static void main(String[] args) throws IOException, TimeoutException {
//创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.150.131");
factory.setUsername("admin");
factory.setPassword("123");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
System.out.println("准备消费");
DeliverCallback deliverCallback = (consumerTag,delivery) ->{
System.out.println(consumerTag); //消费者标记
String message = new String(delivery.getBody());//消息内容
System.out.println("接收到的消息:" + message);
System.out.println(delivery.getEnvelope().getDeliveryTag());//消息数量(第几个)
};
//4、消费者取消消费消息的回调接口
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println(consumerTag + "消费者取消消费消息");
};
/*
消费者消费消息
1、哪个队列
2、消费成功之后,是否要自动应答
3、队列推送消息给消费者的时候,如何消费消息
4、消费者取消消费消息的回调接口
*/
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
3.2、单个生产者,多个消费者(轮巡消费,平均分配)
将以上获取连接封装为工具类
package com.agw.utils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
/**
* @author angw
* @version 1.0
* @Title:
* @date 2022/12/27 15:51
*/
public class RabbitUtils {
public static Channel getChannel() throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.150.131");
factory.setUsername("admin");
factory.setPassword("123");
Connection connection = factory.newConnection();
//获取连接channel
Channel channel = connection.createChannel();
return channel;
}
}
生产者
package com.agw.test2;
import com.agw.utils.RabbitUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.TimeoutException;
/**
* @author angw
* @version 1.0
* @Title:生产者
* @date 2022/12/27 10:49
*/
public class Producer02 {
public static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
//创建连接工厂
Channel channel = RabbitUtils.getChannel();
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String message = scanner.next();
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
System.out.println("发送了一条消息" + message);
}
}
}
消费者
package com.agw.test2;
import com.agw.utils.RabbitUtils;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author angw
* @version 1.0
* @Title:消费者
* @date 2022/12/27 11:49
*/
public class Customer02 {
//队列名称
public static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
//创建连接工厂
Channel channel = RabbitUtils.getChannel();
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
System.out.println("准备消费");
DeliverCallback deliverCallback = (consumerTag,delivery) ->{
System.out.println(consumerTag); //消费者标记
String message = new String(delivery.getBody());//消息内容
System.out.println("接收到的消息:" + message);
};
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println(consumerTag + "消费者取消消费消息");
};
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
package com.agw.test2;
import com.agw.utils.RabbitUtils;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
/**
* @author angw
* @version 1.0
* @Title:消费者
* @date 2022/12/27 11:49
*/
public class Customer03 {
//队列名称
public static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
//创建连接工厂
Channel channel = RabbitUtils.getChannel();
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
System.out.println("准备消费");
DeliverCallback deliverCallback = (consumerTag,delivery) ->{
System.out.println(consumerTag); //消费者标记
String message = new String(delivery.getBody());//消息内容
System.out.println("接收到的消息:" + message);
};
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println(consumerTag + "消费者取消消费消息");
};
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
消费者手动应答或者拒绝策略
package com.agw.test2.task02;
import com.agw.utils.RabbitUtils;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
/**
* @author angw
* @version 1.0
* @Title:消费者
* @date 2022/12/27 11:49
*/
public class Customer02 {
//队列名称
public static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
//创建连接工厂
Channel channel = RabbitUtils.getChannel();
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
System.out.println("准备消费");
DeliverCallback deliverCallback = (consumerTag,delivery) ->{
System.out.println(consumerTag); //消费者标记
String message = new String(delivery.getBody());//消息内容
System.out.println("接收到的消息:" + message);
// /*
// 手动确认应答
// 参数: 1、那个消息 2、一次应答多个还是当前消息,false表示当前消息,true表示当前和之前的一起确认
// */
// channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);\
/*
拒绝确认应答:
1、那个消息
2、拒绝多个消息还是一个
3、是否重新入队
true 重新入队
false 丢弃消息,或者进入死信队列
*/
// channel.basicNack(delivery.getEnvelope().getDeliveryTag(),false,false);
//第一个参数:哪个消息,第二个参数:是否重新入队
channel.basicReject(delivery.getEnvelope().getDeliveryTag(),false);
};
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println(consumerTag + "消费者取消消费消息");
};
//false表示手动确认应答
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME,autoAck,deliverCallback,cancelCallback);
}
}
3.2.1、未应答的消息会重新入队,其他消费者会消费
生产者同上
消费者
package com.agw.task03;
import com.agw.utils.RabbitUtils;
import com.agw.utils.ThreadSleep;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
/**
* @author angw
* @version 1.0
* @Title:消费者
* @date 2022/12/27 11:49
*/
public class Customer03 {
//队列名称
public static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
//创建连接工厂
Channel channel = RabbitUtils.getChannel();
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
System.out.println("准备消费");
DeliverCallback deliverCallback = (consumerTag,delivery) ->{
System.out.println(consumerTag); //消费者标记
String message = new String(delivery.getBody());//消息内容
System.out.println("C03接收到的消息:" + message + ",时间较长");
ThreadSleep.sleep(30);
/*
手动确认应答
参数: 1、那个消息 2、一次应答多个还是当前消息,false表示当前消息,true表示当前和之前的一起确认
*/
channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
};
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println(consumerTag + "消费者取消消费消息");
};
//false表示手动确认应答
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME,autoAck,deliverCallback,cancelCallback);
}
}
package com.agw.task03;
import com.agw.utils.RabbitUtils;
import com.agw.utils.ThreadSleep;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
/**
* @author angw
* @version 1.0
* @Title:消费者
* @date 2022/12/27 11:49
*/
public class Customer003 {
//队列名称
public static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
//创建连接工厂
Channel channel = RabbitUtils.getChannel();
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
System.out.println("准备消费");
DeliverCallback deliverCallback = (consumerTag,delivery) ->{
System.out.println(consumerTag); //消费者标记
String message = new String(delivery.getBody());//消息内容
System.out.println("C03接收到的消息:" + message + ",时间较短");
ThreadSleep.sleep(3);
/*
手动确认应答
参数: 1、那个消息 2、一次应答多个还是当前消息,false表示当前消息,true表示当前和之前的一起确认
*/
channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
};
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println(consumerTag + "消费者取消消费消息");
};
//false表示手动确认应答
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME,autoAck,deliverCallback,cancelCallback);
}
}
时间较长的消费者消费消息,还未应答之前出现异常了,服务关闭了,该消息会重新入队供其他消费者消费
3.3、队列和消息的持久化
保证mq中断重启后,消息和队列依然存在
package com.agw.task04;
import com.agw.utils.RabbitUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.MessageProperties;
import java.util.Scanner;
/**
* @author angw
* @version 1.0
* @Title:队列持久化
* @date 2022/12/27 10:49
*/
public class Producer04 {
public static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
//创建连接工厂
Channel channel = RabbitUtils.getChannel();
//队列是否持久化
boolean persistence = true;
channel.queueDeclare(QUEUE_NAME,persistence,false,false,null);
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String message = scanner.next();
//第三个参数表示消息持久化
channel.basicPublish("",QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());
System.out.println("发送了一条消息" + message);
}
}
}
3.4、预取值
也就是不公平分发,不是轮训分发,根据消费者处理速度进行分配,需要手动设置每个消费者的混充区的大小(也就是最大处理值)
package com.agw.task05;
import com.agw.utils.RabbitUtils;
import com.agw.utils.ThreadSleep;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
/**
* @author angw
* @version 1.0
* @Title:消费者
* @date 2022/12/27 11:49
*/
public class Customer005 {
//队列名称
public static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
//创建连接工厂
Channel channel = RabbitUtils.getChannel();
channel.queueDeclare(QUEUE_NAME,true,false,false,null);
//设置预取值,表示通道上未确认消息最大个数
int prefetchCount = 4;
channel.basicQos(prefetchCount);
System.out.println("准备消费");
DeliverCallback deliverCallback = (consumerTag,delivery) ->{
System.out.println(consumerTag); //消费者标记
String message = new String(delivery.getBody());//消息内容
ThreadSleep.sleep(1);
/*
手动确认应答
参数: 1、那个消息 2、一次应答多个还是当前消息,false表示当前消息,true表示当前和之前的一起确认
*/
channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
System.out.println("C03接收到的消息:" + message + ",时间较短");
};
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println(consumerTag + "消费者取消消费消息");
};
//false表示手动确认应答
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME,autoAck,deliverCallback,cancelCallback);
}
}
注意:预取值不是越大越好,太大了容易导致硬盘堆积,太小了吞吐量小,应反复实验找到合适的值,一般在100-300之间是最佳的
4、发布确认
4.1、单个发布确认、批量发布确认
让生产者知道消息已经到达队列了,保证安全
发布确认分为单个发布确认和批量发布确认
单个发布确认:发布消息后只有被确认后,后面的消息才能继续发布,发布速度慢
批量发布确认:预先设置好一些批量设置的值,相比单个速度较快,但当发布出现问题时,不宜排查
package com.agw.task06;
import com.agw.utils.RabbitUtils;
import com.rabbitmq.client.Channel;
import java.util.UUID;
/**
* @author angw
* @version 1.0
* @Title:
* @date 2022/12/27 21:10
*/
public class PublishiConfirmsTest {
public static int COUNT = 230;
public static void main(String[] args) throws Exception {
// publishMessage();
publishMessageBatch();
}
//单个消息发布确认
public static void publishMessage() throws Exception {
Channel channel = RabbitUtils.getChannel();
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName,false,false,false,null);
//开启发布确认,去发布消息更加安全
channel.confirmSelect();
long begin = System.currentTimeMillis();
for (int i = 1; i < COUNT + 1; i++) {
String message = "消息" + i;
channel.basicPublish("",queueName,null,message.getBytes());
//等待broker确认收到消息的方法
boolean flag = channel.waitForConfirms();
if (flag){
System.out.println("收到消息" + i);
}
}
long end = System.currentTimeMillis();
System.out.println("共耗时:" + (end - begin));
}
//批量发布消息确认
public static void publishMessageBatch() throws Exception {
Channel channel = RabbitUtils.getChannel();
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName,false,false,false,null);
//开启发布确认,去发布消息更加安全
channel.confirmSelect();
//批量的值
int batchSize = 50;
int outstandingMessageCount = 0;
long begin = System.currentTimeMillis();
for (int i = 1; i < COUNT + 1; i++) {
String message = "消息" + i;
channel.basicPublish("",queueName,null,message.getBytes());
outstandingMessageCount ++;
//等待broker确认收到消息的方法
if (outstandingMessageCount == batchSize){
boolean falg = channel.waitForConfirms();
if (falg){
System.out.println("收到消息" + i);
outstandingMessageCount = 0;
}
}
}
//没有确认消息的
if (outstandingMessageCount > 0){
boolean flag = channel.waitForConfirms();
if (flag){
System.out.println("收到消息" );
outstandingMessageCount = 0;
}
}
long end = System.currentTimeMillis();
System.out.println("批量发布确认消息,共耗时:" + (end - begin));
}
}
4.2、异步发布确认
//异步消息发布确认
public static void publishMessageAsync() throws Exception {
Channel channel = RabbitUtils.getChannel();
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName,false,false,false,null);
//开启发布确认,去发布消息更加安全
channel.confirmSelect();
/*
一个线程安全的map,key有序,可通过key获取key之前的所有数据
*/
ConcurrentSkipListMap<Long, String> outstandingComfirms = new ConcurrentSkipListMap<>();
//确认收到消息的回调
ConfirmCallback ackCallback = (sequenceNumber,multple) -> {
if (multple){
//多个,此时签收小于等于sequenceNmber消息,签收也就是从map中移除
ConcurrentNavigableMap<Long, String> comfirmedPartMap = outstandingComfirms.headMap(sequenceNumber);
comfirmedPartMap.clear();
}else {
//单个,只签收当前sequenceNumber
outstandingComfirms.remove(sequenceNumber);
}
};
//未确认收到消息的回调
ConfirmCallback nackCallback = (sequenceNumber,multple) -> {
String message = outstandingComfirms.get(sequenceNumber);
System.out.println("序号为" + sequenceNumber + "需要重新发送");
};
//添加监听器,收到消息或者未收到消息的回调
channel.addConfirmListener(ackCallback,nackCallback);
long begin = System.currentTimeMillis();
for (int i = 1; i < COUNT + 1; i++) {
String message = "消息" + i;
outstandingComfirms.put(channel.getNextPublishSeqNo(),message);
channel.basicPublish("",queueName,null,message.getBytes());
}
long end = System.currentTimeMillis();
System.out.println("共耗时:" + (end - begin));
}
异步的效率要更高,更安全
4.3、以上三种发布确认速度对比
单个发布确认:每个消息都要等待确认,效率低,吞吐量有限
批量发布确认:一批一批确认,效率较高,吞吐量合理,但出现问题难以排查,一批中有一个出现错误,整批都会重新发送,会出问题
异步发布确认:最佳的性能,更高效和安全,出错了容易控制,但实现较为复杂
5、交换机(exchanges)
mq中,生产者生产的消息不会直接发送到队列上,会先到达交换机中,由交换机做一系列操作后推送到交换机上
5.1、交换机的类型
直接(direct)、主题(topic)、扇出(fanout)、标题(headers)
5.1.1、扇出(fanout)
package com.agw.task07;
import com.agw.utils.RabbitUtils;
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.Scanner;
import java.util.concurrent.TimeoutException;
/**
* @author angw
* @version 1.0
* @Title:
* @date 2022/12/27 10:49
*/
public class Producer07 {
//交换机名称
public static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws Exception {
//获取连接channel
Channel channel = RabbitUtils.getChannel();
//声明交换机,参数一:交换机名称,参数二:交换机类型
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
System.out.println("等待输入消息");
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String message = scanner.next();
channel.basicPublish(EXCHANGE_NAME,"",null,message.getBytes());
System.out.println("发送了一条消息" + message);
}
}
}
两个消费者,一个将消息打印在控制台,一个持久化到磁盘
public class Customer07 {
//交换机名称
public static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitUtils.getChannel();
//临时队列
String queueName = channel.queueDeclare().getQueue();
//绑定交换机和队列
channel.queueBind(queueName,EXCHANGE_NAME,"");
System.out.println("消费者1准备消费,把消息打印");
DeliverCallback deliverCallback = (consumerTag,delivery) ->{
String message = new String(delivery.getBody());//消息内容
System.out.println("消费者1接收到的消息:" + message);
};
//4、消费者取消消费消息的回调接口
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println(consumerTag + "消费者取消消费消息");
};
channel.basicConsume(queueName,true,deliverCallback,cancelCallback);
}
}
public class Customer007 {
//交换机名称
public static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitUtils.getChannel();
//临时队列
String queueName = channel.queueDeclare().getQueue();
//绑定交换机和队列
channel.queueBind(queueName,EXCHANGE_NAME,"");
System.out.println("消费者2准备消费,把消息写入磁盘");
DeliverCallback deliverCallback = (consumerTag,delivery) ->{
String message = new String(delivery.getBody());//消息内容
File file = new File("D:\\rabbitmq_info.txt");
FileUtils.writeStringToFile(file,message,"utf-8",true);
System.out.println("文件成功写入");
};
//4、消费者取消消费消息的回调接口
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println(consumerTag + "消费者取消消费消息");
};
channel.basicConsume(queueName,true,deliverCallback,cancelCallback);
}
}
5.1.2、直接(direct)
生产者
public class Producer08 {
//交换机名称
public static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] args) throws Exception {
//获取连接channel
Channel channel = RabbitUtils.getChannel();
//声明交换机,参数一:交换机名称,参数二:交换机类型
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
HashMap<String, String> bindingkeyMap = new HashMap<>();
bindingkeyMap.put("info","普通info信息");
bindingkeyMap.put("warning","警告warning信息");
bindingkeyMap.put("error","错误日志");
bindingkeyMap.put("debug","debug信息");
Set<Map.Entry<String, String>> entries = bindingkeyMap.entrySet();
for (Map.Entry<String, String> bindingKeyEntry : bindingkeyMap.entrySet()){
String key = bindingKeyEntry.getKey();
String value = bindingKeyEntry.getValue();
channel.basicPublish(EXCHANGE_NAME,key,null,value.getBytes());
System.out.println("发送了消息:" + value);
}
}
}
消费者
public class Customer08 {
//交换机名称
public static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitUtils.getChannel();
//绑定交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
String queueName = "console";
channel.queueDeclare(queueName,false,false,false,null);
//绑定交换机和队列
channel.queueBind(queueName,EXCHANGE_NAME,"info");
channel.queueBind(queueName,EXCHANGE_NAME,"warning");
System.out.println("消费者1准备消费,把消息打印");
DeliverCallback deliverCallback = (consumerTag,delivery) ->{
String message = new String(delivery.getBody());//消息内容
System.out.println("消费者1接受的路由" + delivery.getEnvelope().getRoutingKey() + ",接收到的消息:" + message);
};
//4、消费者取消消费消息的回调接口
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println(consumerTag + "消费者取消消费消息");
};
channel.basicConsume(queueName,true,deliverCallback,cancelCallback);
}
}
public class Customer008 {
//交换机名称
public static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitUtils.getChannel();
String queueName = "disk";
//绑定交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
channel.queueDeclare(queueName,false,false,false,null);
//绑定交换机和队列
channel.queueBind(queueName,EXCHANGE_NAME,"error");
System.out.println("消费者2准备消费,把消息写入磁盘");
DeliverCallback deliverCallback = (consumerTag,delivery) ->{
String message = new String(delivery.getBody());//消息内容
File file = new File("D:\\direct_error.txt");
FileUtils.writeStringToFile(file,message,"utf-8",true);
System.out.println("文件成功写入,key为:" + delivery.getEnvelope().getRoutingKey() + ",消息:" + message);
};
//4、消费者取消消费消息的回调接口
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println(consumerTag + "消费者取消消费消息");
};
channel.basicConsume(queueName,true,deliverCallback,cancelCallback);
}
}
5.1.3、主题(topic)
可以分模块,更加细粒度化的接收消息,使用topic,这里路由key不可以随意写,需满足一定的规则,如:"stock.usd.nyse", "nyse.vmw",单词列表最多不可超过255个字节,其中有两个替换符:
*(星号)可以代替一个单词
#(井号)可以替代零个或多个单词
案例:
生产者
public class Producer09 {
//交换机名称
public static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] args) throws Exception {
//获取连接channel
Channel channel = RabbitUtils.getChannel();
//声明交换机,参数一:交换机名称,参数二:交换机类型
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
Map<String, String> bindingKeyMap = new HashMap<>();
bindingKeyMap.put("quick.orange.rabbit","被队列 Q1Q2 接收到");
bindingKeyMap.put("lazy.orange.elephant","被队列 Q1Q2 接收到");
bindingKeyMap.put("quick.orange.fox","被队列 Q1 接收到");
bindingKeyMap.put("lazy.brown.fox","被队列 Q2 接收到");
bindingKeyMap.put("lazy.pink.rabbit","虽然满足两个绑定但只被队列 Q2 接收一次");
bindingKeyMap.put("quick.brown.fox","不匹配任何绑定不会被任何队列接收到会被丢弃");
bindingKeyMap.put("quick.orange.male.rabbit","是四个单词不匹配任何绑定会被丢弃");
bindingKeyMap.put("lazy.orange.male.rabbit","是四个单词但匹配 Q2");
for (Map.Entry<String, String> bindingKeyEntry : bindingKeyMap.entrySet()){
String key = bindingKeyEntry.getKey();
String value = bindingKeyEntry.getValue();
channel.basicPublish(EXCHANGE_NAME,key,null,value.getBytes());
System.out.println("发送了消息:" + value);
}
}
}
消费者
public class Customer09 {
//交换机名称
public static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitUtils.getChannel();
String queueName = "Q1";
channel.queueDeclare(queueName,false,false,false,null);
//绑定交换机
// channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
//绑定交换机和队列
channel.queueBind(queueName,EXCHANGE_NAME,"*.orange.*");
System.out.println("消费者1准备消费,把消息打印");
DeliverCallback deliverCallback = (consumerTag,delivery) ->{
String message = new String(delivery.getBody());//消息内容
System.out.println("消费者1接受的路由" + delivery.getEnvelope().getRoutingKey() + ",接收到的消息:" + message);
};
//4、消费者取消消费消息的回调接口
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println(consumerTag + "消费者取消消费消息");
};
channel.basicConsume(queueName,true,deliverCallback,cancelCallback);
}
}
public class Customer009 {
//交换机名称
public static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitUtils.getChannel();
String queueName = "Q2";
channel.queueDeclare(queueName,false,false,false,null);
//绑定交换机
// channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
//绑定交换机和队列
channel.queueBind(queueName,EXCHANGE_NAME,"*.*.rabbit");
channel.queueBind(queueName,EXCHANGE_NAME,"lazy.#");
System.out.println("消费者2准备消费,把消息写入磁盘");
DeliverCallback deliverCallback = (consumerTag,delivery) ->{
String message = new String(delivery.getBody());//消息内容
File file = new File("D:\\topic_logs.txt");
FileUtils.writeStringToFile(file,message,"utf-8",true);
System.out.println("文件成功写入,key为:" + delivery.getEnvelope().getRoutingKey() + ",消息:" + message);
};
//4、消费者取消消费消息的回调接口
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println(consumerTag + "消费者取消消费消息");
};
channel.basicConsume(queueName,true,deliverCallback,cancelCallback);
}
}
6、死信队列
消息由于某些原因,无法被消费会进入死信队列
死信的来源
1、消息 TTL 过期(消息存活时间过期)
2、队列达到最大长度(队列满了,无法再添加数据到 mq 中)
3、消息被拒绝(basic.reject 或 basic.nack)并且 requeue=false
6.1、死信演示
6.1.1、消息TTL过期
生产者
public class Producer10 {
public static final String NORMAL_EXCHANGE_NAME = "normal_exchange";
public static void main(String[] args) throws Exception {
//创建连接工厂
Channel channel = RabbitUtils.getChannel();
channel.exchangeDeclare(NORMAL_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
String message = "info";
//消息的TTL过期时间,如果在这个时间内这个消息没有被消费会进入死信队列
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("5000").build();
channel.basicPublish(NORMAL_EXCHANGE_NAME,"zhangsan", properties,message.getBytes());
System.out.println("发送了一条消息" + message);
}
}
消费者1
public class Customer10 {
//交换机名称
public static final String NORAML_EXCHANGE_NAME = "normal_exchange";
public static final String NORAML_QUEUE_NAME = "normal-queue";
public static final String DEAD_EXCHANGE_NAME = "dead_exchange";
public static final String DEAD_QUEUE_NAME = "dead-queue";
public static void main(String[] args) throws Exception {
Channel channel = RabbitUtils.getChannel();
System.out.println("消费者1准备消费,把消息打印");
//声明一个死信队列
channel.queueDeclare(DEAD_QUEUE_NAME,false,false,false,null);
//一个死信交换机
channel.exchangeDeclare(DEAD_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
//交换机和队列的绑定关系
channel.queueBind(DEAD_QUEUE_NAME,DEAD_EXCHANGE_NAME,"lisi");
//正常队列和私信交换机的绑定关系
HashMap<String, Object> deadLetterParams = new HashMap<>();
deadLetterParams.put("x-dead-letter-exchange",DEAD_EXCHANGE_NAME);
deadLetterParams.put("x-dead-letter-routing-key","lisi");
//声明一个正常队列
channel.queueDeclare(NORAML_QUEUE_NAME,false,false,false,deadLetterParams);
//一个正常的交换机
channel.exchangeDeclare(NORAML_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
//交换机和队列的绑定关系
channel.queueBind(NORAML_QUEUE_NAME,NORAML_EXCHANGE_NAME,"zhangsan");
DeliverCallback deliverCallback = (consumerTag,delivery) ->{
String message = new String(delivery.getBody());//消息内容
System.out.println("消费者1接收到的消息:" + message);
};
//4、消费者取消消费消息的回调接口
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println(consumerTag + "消费者取消消费消息");
};
channel.basicConsume(NORAML_QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
消费者2(死信消费者)
public class Customer010 {
public static final String DEAD_QUEUE_NAME = "dead-queue";
public static void main(String[] args) throws Exception {
Channel channel = RabbitUtils.getChannel();
System.out.println("死信消费者准备消费");
DeliverCallback deliverCallback = (consumerTag,delivery) ->{
String message = new String(delivery.getBody());//消息内容
System.out.println("消费者收到死信" + message);
};
//4、消费者取消消费消息的回调接口
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println(consumerTag + "消费者取消消费消息");
};
channel.basicConsume(DEAD_QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
队列参数
6.1.2、队列达到最大长度
生产者
public class Producer11 {
public static final String NORMAL_EXCHANGE_NAME = "normal_exchange";
public static void main(String[] args) throws Exception {
//创建连接工厂
Channel channel = RabbitUtils.getChannel();
channel.exchangeDeclare(NORMAL_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
for (int i = 1; i < 11; i++) {
String message = "info" + i;
channel.basicPublish(NORMAL_EXCHANGE_NAME,"zhangsan", null,message.getBytes());
}
System.out.println("发送了10条消息");
}
}
消费者
//正常队列和私信交换机的绑定关系
HashMap<String, Object> deadLetterParams = new HashMap<>();
deadLetterParams.put("x-dead-letter-exchange",DEAD_EXCHANGE_NAME);
deadLetterParams.put("x-dead-letter-routing-key","lisi");
//队列最大6条消息
deadLetterParams.put("x-max-length",6);
6.1.3、消息被拒绝
消费者
public class Customer3 {
//交换机名称
public static final String NORAML_EXCHANGE_NAME = "normal_exchange";
public static final String NORAML_QUEUE_NAME = "normal-queue";
public static final String DEAD_EXCHANGE_NAME = "dead_exchange";
public static final String DEAD_QUEUE_NAME = "dead-queue";
public static void main(String[] args) throws Exception {
Channel channel = RabbitUtils.getChannel();
System.out.println("消费者1准备消费,把消息打印");
//声明一个死信队列
channel.queueDeclare(DEAD_QUEUE_NAME,false,false,false,null);
//一个死信交换机
channel.exchangeDeclare(DEAD_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
//交换机和队列的绑定关系
channel.queueBind(DEAD_QUEUE_NAME,DEAD_EXCHANGE_NAME,"lisi");
//正常队列和私信交换机的绑定关系
HashMap<String, Object> deadLetterParams = new HashMap<>();
deadLetterParams.put("x-dead-letter-exchange",DEAD_EXCHANGE_NAME);
deadLetterParams.put("x-dead-letter-routing-key","lisi");
//声明一个正常队列
channel.queueDeclare(NORAML_QUEUE_NAME,false,false,false,deadLetterParams);
//一个正常的交换机
channel.exchangeDeclare(NORAML_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
//交换机和队列的绑定关系
channel.queueBind(NORAML_QUEUE_NAME,NORAML_EXCHANGE_NAME,"zhangsan");
DeliverCallback deliverCallback = (consumerTag,delivery) ->{
String message = new String(delivery.getBody());//消息内容
if (message.equals("info5")){
System.out.println("消息" + message + "被拒绝签收了");
channel.basicReject(delivery.getEnvelope().getDeliveryTag(),false);
}else {
System.out.println("消费者1接收到的消息:" + message);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
}
};
//4、消费者取消消费消息的回调接口
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println(consumerTag + "消费者取消消费消息");
};
channel.basicConsume(NORAML_QUEUE_NAME,false,deliverCallback,cancelCallback);
}
}
7、延迟队列
延迟队列内部是有序的,用来存储需要在指定时间执行某些操作的元素的队列。
一般用于大批量订单取消支付、高并发对数据库操作的时机
7.1、整合springboot项目演示
1、创建springboot项目,添加一下依赖,springboot项目需要是2.5.x或者2.6.x版本
<dependencies>
<!--RabbitMQ 依赖-->
<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>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--swagger-->
<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>
<!--RabbitMQ 测试依赖-->
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
2、连接rabbitmq
spring.rabbitmq.host=192.168.150.131
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=123
3、编写配置文件
package com.agw.config;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* @author angw
* @version 1.0
* @Title:声明交换机、队列、路由key之间的关系
* @date 2022/12/29 16:20
*/
@Configuration
public class TtlQueueConfig {
public static final String X_EXCHANGE = "X";
public static final String QUEUE_A = "QA";
public static final String QUEUE_B = "QB";
public static final String Y_DEAD_LETTER_EXCHANGE = "Y";
public static final String DEAD_LETTER_QUEUE = "QD";
// 声明 xExchange
@Bean("xExchange")
public DirectExchange xExchange(){
return new DirectExchange(X_EXCHANGE);
}
// 声明 yExchange
@Bean("yExchange")
public DirectExchange yExchange(){
return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);
}
//声明队列 A ttl 为 10s 并绑定到对应的死信交换机
@Bean("queueA")
public Queue queueA(){
Map<String, Object> args = new HashMap<>(3);
//声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
//声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key", "YD");
//声明队列的 TTL
args.put("x-message-ttl", 10000);
return QueueBuilder.durable(QUEUE_A).withArguments(args).build();
}
// 声明队列 A 绑定 X 交换机
@Bean
public Binding queueaBindingX(@Qualifier("queueA") Queue queueA,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queueA).to(xExchange).with("XA");
}
//声明队列 B ttl 为 40s 并绑定到对应的死信交换机
@Bean("queueB")
public Queue queueB(){
Map<String, Object> args = new HashMap<>(3);
//声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
//声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key", "YD");
//声明队列的 TTL
args.put("x-message-ttl", 40000);
return QueueBuilder.durable(QUEUE_B).withArguments(args).build();
}
//声明队列 B 绑定 X 交换机
@Bean
public Binding queuebBindingX(@Qualifier("queueB") Queue queue1B,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queue1B).to(xExchange).with("XB");
}
//声明死信队列 QD
@Bean("queueD")
public Queue queueD(){
return new Queue(DEAD_LETTER_QUEUE);
}
//声明死信队列 QD 绑定关系
@Bean
public Binding deadLetterBindingQAD(@Qualifier("queueD") Queue queueD,
@Qualifier("yExchange") DirectExchange yExchange){
return BindingBuilder.bind(queueD).to(yExchange).with("YD");
}
}
package com.agw.config;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Component
public class MsgTtlQueueConfig {
public static final String Y_DEAD_LETTER_EXCHANGE = "Y";
public static final String QUEUE_C = "QC";
//声明队列 C 死信交换机
@Bean("queueC")
public Queue queueB(){
Map<String, Object> args = new HashMap<>(3);
//声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
//声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key", "YD");
//没有声明 TTL 属性
return QueueBuilder.durable(QUEUE_C).withArguments(args).build();
}
//声明队列 B 绑定 X 交换机
@Bean
public Binding queuecBindingX(@Qualifier("queueC") Queue queueC,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queueC).to(xExchange).with("XC");
}
}
4、添加swagger配置类,配合接口测试使用的,接口测试网址http://127.0.0.1:8080/swagger-ui.html
package com.agw.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
/**
* @author angw
* @version 1.0
* @Title:
* @date 2022/12/29 16:30
*/
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket webApiConfig(){
return new Docket(DocumentationType.SWAGGER_2)
.groupName("webApi")
.apiInfo(webApiInfo())
.select()
.build();
}
private ApiInfo webApiInfo(){
return new ApiInfoBuilder()
.title("rabbitmq 接口文档")
.description("本文档描述了 rabbitmq 微服务接口定义")
.version("1.0")
.contact(new Contact("enjoy6288", "http://atguigu.com",
"1551388580@qq.com"))
.build();
}
}
5、生产者
package com.agw.producer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.Date;
/**
* @author angw
* @version 1.0
* @Title:
* @date 2022/12/29 16:51
*/
@RestController
@RequestMapping("ttl")
@Slf4j
public class SendMsgController {
@Resource
private RabbitTemplate rabbitTemplate;
@GetMapping("/sendMsg/{message}")
public void sendMsg(@PathVariable String message){
log.info("当前时间:{},发送了两条消息给TTL队列:{}",new Date(), message);
rabbitTemplate.convertAndSend("X","XA","消息来自ttl为10s的队列 :" + message);
rabbitTemplate.convertAndSend("X","XB","消息来自ttl为40s的队列 :" + message);
}
@GetMapping("/sendExpirationMsg/{message}/{ttlTime}")
public void sendExpirationMsg(@PathVariable String message, @PathVariable String ttlTime){
log.info("当前时间:{},发送了一条时长:{}的消息给QC队列:{}",new Date(),ttlTime, message);
//设置消息ttl的过期时间
MessagePostProcessor messagePostProcessor = correlationData -> {
correlationData.getMessageProperties().setExpiration(ttlTime);
return correlationData;
};
rabbitTemplate.convertAndSend("X","XC","消息来自QC队列 :" + message,messagePostProcessor);
}
}
消费者
package com.agw.customer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* @author angw
* @version 1.0
* @Title:
* @date 2022/12/29 17:16
*/
@Component
@Slf4j
public class DeadLetterCustomer {
@RabbitListener(queues = "QD")
public void receiveD(Message message){
String msg = new String(message.getBody());
log.info("当前时间{},收到死信队列的消息:{}",new Date(),msg);
}
}
测试后发现:给队列设置过期时间,消息到达过期时间后可以及时放入死信消息,而给消息设置过期时间后,如果第二个延迟时间短,第一个延迟时间长,第二个并不能及时的放入到死信队列,mq会首先检查第一个是否过期,过期就丢入,再检查下一个
7.2、消息延迟问题解决
7.2.1、延时队列插件安装
1、安装延时队列插件Community Plugins — RabbitMQ,下载rabbitmq_delayed_message_exchange 插件
2、放入/usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins目录下
3、执行rabbitmq-plugins enable rabbitmq_delayed_message_exchange安装
安装完成后刷新mq,多出如下交换机即为安装成功
7.2.2、代码演示延迟消息推送
1、自定义交换机,延迟推送消息
package com.agw.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.CustomExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* @author angw
* @version 1.0
* @Title:
* @date 2022/12/29 20:49
*/
@Configuration
public class DelayedQueueConfig {
public static final String DELAYED_QUEUE_NAME = "delayed.queue";
public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";
@Bean
public Queue delayedQueue() {
return new Queue(DELAYED_QUEUE_NAME);
}
/*
自定义交换机 我们在这里定义的是一个延迟交换机
不需要死信队列和死信交换机,支持消息延迟投递,消息生产出来后,没有达到投递时间,不会投递给队列,
会先存储到一个分布式系统表中,到达投递时间才会投递
*/
@Bean
public CustomExchange delayedExchange() {
Map<String, Object> args = new HashMap<>();
/*
自定义交换机的类型
1、交换机名称
2、新交换机类型
3、是否持久化
4、是否自动删除
5、参数(设置交换机类型)
*/
args.put("x-delayed-type", "direct");
return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message", false, false, args);
}
//绑定交换机和队列之间的关系
@Bean
public Binding bindingDelayedQueue(@Qualifier("delayedQueue") Queue queue,
@Qualifier("delayedExchange") CustomExchange delayedExchange) {
return BindingBuilder.bind(queue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
}
}
2、生产者发送延迟消息
//发送延迟消息
public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";
@GetMapping("sendDelayMsg/{message}/{delayTime}")
public void sendMsg(@PathVariable String message,@PathVariable Integer delayTime) {
log.info("当前时间:{},发送了一条时长:{}的消息给delayed.queue队列:{}",new Date(),delayTime, message);
//设置消息ttl的过期时间
MessagePostProcessor messagePostProcessor = correlationData -> {
correlationData.getMessageProperties().setDelay(delayTime);
return correlationData;
};
rabbitTemplate.convertAndSend(DELAYED_EXCHANGE_NAME, DELAYED_ROUTING_KEY,"消息来自delayed.queue队列 :" + message,messagePostProcessor);
}
3、消费者实时接收
//接收延迟队列的消息
public static final String DELAYED_QUEUE_NAME = "delayed.queue";
@RabbitListener(queues = DELAYED_QUEUE_NAME)
public void receiveDelay(Message message){
String msg = new String(message.getBody());
log.info("当前时间{},收到延迟队列的消息:{}",new Date(),msg);
}
8、发布确认高级
整合springboot以异步确认为例
1、创建springboot项目,添加配置文件
spring.rabbitmq.host=192.168.150.131
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=123
spring.rabbitmq.publisher-confirm-type=correlated
2、配置类
package com.agw.config;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ConfirmConfig {
public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
//声明业务 Exchange
@Bean("confirmExchange")
public DirectExchange confirmExchange(){
return new DirectExchange(CONFIRM_EXCHANGE_NAME);
}
// 声明确认队列
@Bean("confirmQueue")
public Queue confirmQueue(){
return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
}
// 声明确认队列绑定关系
@Bean
public Binding queueBinding(@Qualifier("confirmQueue") Queue queue,
@Qualifier("confirmExchange") DirectExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("key1");
}
}
3、生产者
package com.agw.producer;
import com.agw.config.MyCallBack;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.Date;
/**
* @author angw
* @version 1.0
* @Title:
* @date 2022/12/29 16:51
*/
@RestController
@RequestMapping("/confirm")
@Slf4j
public class Producer {
@Resource
private RabbitTemplate rabbitTemplate;
@Resource
private MyCallBack myCallBack;
//设置他的回调对象
@PostConstruct
public void init(){
//回调哪个接口
rabbitTemplate.setConfirmCallback(myCallBack);
}
public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
@GetMapping("/sendMessage/{message}")
public void sendMessage(@PathVariable String message) {
//相关性数据
CorrelationData correlationData1 = new CorrelationData("1");
String routeKey1 = "key1";
rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME, routeKey1, routeKey1+message,correlationData1);
log.info("当前时间:{},发送了消息:{}", new Date(), message);
CorrelationData correlationData2 = new CorrelationData("2");
String routeKey2 = "key2";
rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME, routeKey2, routeKey2 + message,correlationData2);
log.info("当前时间:{},发送了消息:{}", new Date(), message);
}
}
消费者
package com.agw.customer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class ConfirmConsumer {
public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
@RabbitListener(queues = CONFIRM_QUEUE_NAME)
public void receiveMsg(Message message) {
String msg = new String(message.getBody());
log.info("接受到队列{}的消息:{}",CONFIRM_QUEUE_NAME, msg);
}
}
回调接口
package com.agw.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
/**
* @author angw
* @version 1.0
* @Title:
* @date 2022/12/30 10:57
*/
@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback {
/**
*
* @param correlationData 消息相关数据
* @param ack 交换机是否收到消息
* @param cause 未收到的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
String id = correlationData != null? correlationData.getId() : "";
if (ack){
log.info("交换机收到消息,标记为:{}" + id);
}else {
log.info("交换机未收到消息,原因:{}",cause);
}
}
}
注意:这里如果发送的消息路由key如果不存在,消息到达交换机后会丢失
8.2、回退消息
当某个消息交换机接收后,但是消息的路由key不存在,消息被退回实现
生产者
//设置他的回调对象
@PostConstruct
public void init(){
//回调哪个接口
rabbitTemplate.setConfirmCallback(myCallBack);
/*
设置交换机如果无法将消息进行路由时,将消息返回给生产者,true表示退回,false表示丢弃
*/
rabbitTemplate.setMandatory(true);
//设置回退消息交给谁处理
rabbitTemplate.setReturnsCallback(myCallBack);
}
回调函数
package com.agw.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
/**
* @author angw
* @version 1.0
* @Title:
* @date 2022/12/30 10:57
*/
@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback {
/**
* 消息发布成功之后的响应
* @param correlationData 消息相关数据
* @param ack 交换机是否收到消息
* @param cause 未收到的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
String id = correlationData != null? correlationData.getId() : "";
if (ack){
log.info("交换机收到消息,标记为:{}" + id);
}else {
log.info("交换机未收到消息,原因:{}",cause);
}
}
//回退的方法,消息无法路由调用该方法
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
log.info("消息:{},被交换机{}退回,退回原因:{},路由key:{}",new String(returnedMessage.getMessage().getBody()),
returnedMessage.getExchange(),returnedMessage.getReplyText(),returnedMessage.getRoutingKey());
}
}
8.3、备份交换机解决路由key不存在问题
一个正常交换机、一台备份交换机,三个队列
配置类
package com.agw.config;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author angw
* @version 1.0
* @Title:
* @date 2022/12/30 15:53
*/
@Configuration
public class ConfirmConfig {
public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
public static final String BACKUP_EXCHANGE_NAME = "backup.exchange";
public static final String BACKUP_QUEUE_NAME = "backup.queue";
public static final String WARNING_QUEUE_NAME = "warning.queue";
// 声明确认队列
@Bean("confirmQueue")
public Queue confirmQueue() {
return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
}
//声明确认队列与交换机的绑定关系
@Bean
public Binding queueBinding(@Qualifier("confirmQueue") Queue queue,
@Qualifier("confirmExchange") DirectExchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with("key1");
}
//声明备份 Exchange
@Bean("backupExchange")
public FanoutExchange backupExchange() {
return new FanoutExchange(BACKUP_EXCHANGE_NAME);
}
//声明确认 Exchange 交换机的备份交换机
@Bean("confirmExchange")
public DirectExchange confirmExchange() {
ExchangeBuilder exchangeBuilder =
ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME)
.durable(true)
//设置该交换机的备份交换机
.withArgument("alternate-exchange", BACKUP_EXCHANGE_NAME);
return (DirectExchange) exchangeBuilder.build();
}
// 声明警告队列
@Bean("warningQueue")
public Queue warningQueue() {
return QueueBuilder.durable(WARNING_QUEUE_NAME).build();
}
// 声明报警队列绑定关系
@Bean
public Binding warningBinding(@Qualifier("warningQueue") Queue queue,
@Qualifier("backupExchange") FanoutExchange
backupExchange) {
return BindingBuilder.bind(queue).to(backupExchange);
}
// 声明备份队列
@Bean("backQueue")
public Queue backQueue() {
return QueueBuilder.durable(BACKUP_QUEUE_NAME).build();
}
// 声明备份队列绑定关系
@Bean
public Binding backupBinding(@Qualifier("backQueue") Queue queue,
@Qualifier("backupExchange") FanoutExchange backupExchange) {
return BindingBuilder.bind(queue).to(backupExchange);
}
}
报警消费者
package com.agw.customer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* @author angw
* @version 1.0
* @Title:报警消费者
* @date 2022/12/30 15:56
*/
@Slf4j
@Component
public class WarningCustomer {
public static final String WARNING_QUEUE_NAME = "warning.queue";
@RabbitListener(queues = WARNING_QUEUE_NAME)
public void receiveWarningMsg(Message message){
String msg = new String(message.getBody());
log.error("报警了,发现了不可路由的消息:{}",msg);
}
}
9、其他知识
9.1.1、幂等性
对于同一个操作发起的一次或者多次请求,结果是一致的,比如支付商品,第一次支付成功,但返回结果时候出现了异常,钱已经扣过了,用户再次点击会进行二次扣款,这就不是幂等性
9.1.2、消息重复消费
消费者在消费MQ消息时,MQ已经把消息发送给了消费者,消费者在给MQ返回ack的时候网络中断,MQ未收到确认消息,这条消息会发送给其他消费者,重复消费
解决:给消息设置一个全局id,根据全局id判断是否已经消费过了,可以使用数据库,但不推荐,推荐使用redis进行缓存,利用setnx命令,天然具有幂等性,效率高
9.2、优先级队列
普通队列是遵循消息先进先出的规则
应用场景:某些消息需要优先推出
1、生产者设置消息优先级
package com.agw.task11;
import com.agw.utils.RabbitUtils;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import java.util.Scanner;
/**
* @author angw
* @version 1.0
* @Title:优先级消费
* @date 2022/12/27 10:49
*/
public class Producer11 {
public static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
//创建连接工厂
Channel channel = RabbitUtils.getChannel();
//设置消息的优先级
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().priority(5).build();
for (int i = 1; i < 11; i++) {
String message = "info" + i;
if (i == 5){
channel.basicPublish("", QUEUE_NAME, properties, message.getBytes());
}else {
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
}
}
System.out.println("发送完成");
}
}
2、消费者设置队列最大优先级(最大255,但官方推荐1-10)
package com.agw.task11;
import com.agw.utils.RabbitUtils;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import java.util.HashMap;
/**
* @author angw
* @version 1.0
* @Title:优先级消费
* @date 2022/12/27 11:49
*/
public class Customer11 {
//队列名称
public static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
//创建连接工厂
Channel channel = RabbitUtils.getChannel();
HashMap<String, Object> params = new HashMap<>();
//设置优先级队列最大优先级
params.put("x-max-priority",10);
channel.queueDeclare(QUEUE_NAME,false,false,false,params);
System.out.println("开始消费消息");
DeliverCallback deliverCallback = (consumerTag,delivery) ->{
String message = new String(delivery.getBody());//消息内容
System.out.println("接收到的消息:" + message);
};
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println(consumerTag + "消费者取消消费消息");
};
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
9.3、惰性队列
使用场景:默认情况下,队列中的消息大多存储在内存中,当内存中消息较多无法接受新消息时,会置换到磁盘中,但这个效率较慢,惰性队列是直接将消息存储在磁盘中,不用关心消费者是否消费,占用的空间也更小
使用x-queue-mode设置
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-queue-mode", "lazy");
channel.queueDeclare("myqueue", false, false, false, args)
10、rabbitmq集群搭
搭建三台mq集群
1、修改三台主机名称为node1、node2、node3
vim /etc/hostname
hostname 查看主机名称
修改玩主机名称后重启服务器:reboot
2、配置各个节点的 hosts 文件,让各个节点都能互相识别对方,给三个节点都加上:
192.168.150.132 node1
192.168.150.133 node2
192.168.150.134 node3
vim /etc/hosts 修改配置文件
3、启动三台mq服务
rabbitmq-server -detached
4、将节点2连接到节点1
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster rabbit@node1
rabbitmqctl start_app(只启动应用服务)
5、将节点3连接到节点2或节点1
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster rabbit@node2
rabbitmqctl start_app
6、查看集群状态
rabbitmqctl cluster_status
8、需要重新设置用户
创建账号 rabbitmqctl add_user admin 123
设置用户角色 rabbitmqctl set_user_tags admin administrator
设置用户权限 rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"
到这里就完成了!
注意:这里某个队列属于节点1,一旦节点1宕机,会导致其他节点不能使用这个队列,还可能会导致消息丢失
9、解除集群
rabbitmqctl stop_app 停止
rabbitmqctl reset 重置(mq账号会消失)
rabbitmqctl start_app 启动
rabbitmqctl cluster_status 查看集群状态
rabbitmqctl forget_cluster_node 查看所有集群状态
rabbit@node2(node1 机器上执行)(如果没有解除掉,强制解除)
10.2.1、镜像队列
解决某节点挂掉,队列不可用消息丢失问题
三台节点中,给任意一节点添加policy
ha-mode表示某节点宕机后,镜像队列自动选择一个节点
ha-params表示几个镜像队列,2表示2-1个镜像队列
这样设置后,如果队列存在的节点宕机,镜像队列就会起作用,让然能获取到消息
10.3. Haproxy+Keepalive 高可用负载均衡
1、下载haproxy(node1和node2两个节点上)
yum -y install haproxy
2、修改 node1 和 node2 的 haproxy.cfg
vim /etc/haproxy/haproxy.cfg
#---------------------------------------------------------------------
# Example configuration for a possible web application. See the
# full configuration options online.
#
# http://haproxy.1wt.eu/download/1.4/doc/configuration.txt
#
#---------------------------------------------------------------------
#---------------------------------------------------------------------
# Global settings
#---------------------------------------------------------------------
global
# to have these messages end up in /var/log/haproxy.log you will
# need to:
#
# 1) configure syslog to accept network log events. This is done
# by adding the '-r' option to the SYSLOGD_OPTIONS in
# /etc/sysconfig/syslog
#
# 2) configure local2 events to go to the /var/log/haproxy.log
# file. A line like the following can be added to
# /etc/sysconfig/syslog
#
# local2.* /var/log/haproxy.log
#
log 127.0.0.1 local2
chroot /var/lib/haproxy
pidfile /var/run/haproxy.pid
maxconn 4000
user haproxy
group haproxy
daemon
# turn on stats unix socket
stats socket /var/lib/haproxy/stats
#---------------------------------------------------------------------
# common defaults that all the 'listen' and 'backend' sections will
# use if not designated in their block
#---------------------------------------------------------------------
defaults
mode http
log global
option httplog
option dontlognull
option redispatch
retries 3
timeout connect 5s
timeout client 120s
timeout server 120s
maxconn 3000
#---------------------------------------------------------------------
# round robin balancing between the various backends
#---------------------------------------------------------------------
listen my_rabbitmq_cluster
bind 0.0.0.0:6868
mode http
balance roundrobin
server rabbitmq_node1 192.168.150.135:5672 check inter 5000 rise 2 fall 3 weight 1
server rabbitmq_node2 192.168.150.133:5672 check inter 5000 rise 2 fall 3 weight 1
server rabbitmq_node3 192.168.150.134:5672 check inter 5000 rise 2 fall 3 weight 1
listen monitor
bind 0.0.0.0:8888
mode http
option httplog
stats enable
stats uri /stats
stats refresh 5s
3、两台节点启动haproxy
haproxy -f /etc/haproxy/haproxy.cfg
ps -ef | grep haproxy
4、访问地址
http://10.211.55.71:8888/stats
5、下载 keepalived
yum -y install keepalived
6、节点 node1 配置文件
vim /etc/keepalived/keepalived.conf
! Configuration File for keepalived
global_defs {
router_id nodeA #路由ID, 主备的ID不能相同
}
vrrp_script check_haproxy {
script "/etc/keepalived/haproxy_chk.sh"
interval 5 #检测脚本执行的间隔
}
vrrp_instance VI_1 {
state MASTER #是否是主服务器,master为主,backup为从
interface ens33
virtual_router_id 51 #主、备机的virtual_router_id必须相同
priority 100 #主、备机不同的优先级,,主机值比较大,备级较少
advert_int 1
authentication {
auth_type PASS
auth_pass 1111
}
virtual_ipaddress {
192.168.150.50 #虚拟ip地址
}
}
7、节点 node2 配置文件
需要修改 global_defs 的 router_id,如:nodeB
其次要修改 vrrp_instance_VI 中 state 为"BACKUP";
最后要将 priority 设置为小于 100 的值
8、添加 haproxy_chk.sh,监控脚本监控keepalived的状态,如果挂掉,另一个立刻上位
vim /etc/keepalived/haproxy_chk.sh
#!/bin/bash
if [ $(ps -C haproxy --no-header | wc -l) -eq 0 ];then
haproxy -f /etc/haproxy/haproxy.cfg
fi
sleep 2
if [ $(ps -C haproxy --no-header | wc -l) -eq 0 ];then
service keepalived stop
fi
9、修改权限
chmod 777 /etc/keepalived/haproxy_chk.sh
10、启动 keepalive 命令(node1 和 node2 启动)
systemctl start keepalived
11、观察 Keepalived 的日志,可查看虚拟ip在那台主机生效
tail -f /var/log/messages -n 200
12、观察最新添加的 Vip(虚拟ip)
ip add show
13、使用 vip 地址来访问 rabbitmq 集群
14、node1 模拟 keepalived 关闭状态
systemctl stop keepalived
10.4、联邦交换机
为了解决某种业务访问其他地区的交换机,网络延迟问题,两个交换机组成一个联邦交换机,本地的交换机收到消息后转发给其他地区的交换机
1、每台节点单独运行
2、每台机器上开启 federation 相关插件
rabbitmq-plugins enable rabbitmq_federation
rabbitmq-plugins enable rabbitmq_federation_management
3、在下游配置上游
4、添加policy
5、 出现running就成功了
不是running的解决方式如下,用户改为如下权限:
10.5、联邦队列
建立联邦队列,当上游队列消息较多,来不及消费是,会自动给下游队列消费,这两个队列名称相同
原理图:
10.6、 shovel
相当于一个铲子,将消息从一方铲到另一方,可以是不同的集群不同的交换机和队列
1、开启插件(需要的机器都开启)
rabbitmq-plugins enable rabbitmq_shovel
rabbitmq-plugins enable rabbitmq_shovel_management
2、添加shovel源和目的地