一、什么是MQ
- 消息队列(Message Queue)是一种用于在应用程序之间传递消息的通信方式,消息队列允许应用程序异步地发送和接收消息,并且不需要 直接连接到对方。
- 消息(Message)是指在应用间传送的数据。消息可以非常简单,比如只包含文本字符串,也可以更复杂,可能包含嵌入对象
- 队列(Queue)可以说是一个数据结构,可以存储数据。先进先出
二、主流产品

作用:
- 解耦:一个模块出现故障,不会影响其他模块
- 异步:当生产者消费者处理速度不匹配的时候,可以通过消息队列存储消息,提高响应速度
- 削峰:通过消息队列减轻消费者的压力,可以根据自己的速度进行响应
三、RabbitMQ的安装
1.环境
使用的是百度云的centos8
RabbitMQ下载地址:https://packagecloud.io/rabbitmq/erlang/packages/el/7/erlang-23.3.4.11-1.el7.x86_64.rpm/
erlang下载地址:
要根据自己的操作系统版本去下载对应的版本,我下载的是rabbitmq-server-3.12.14-1.el8.noarch.rpm和erlang-25.3.2.13-1.el8.x86_64.rpm
2.安装
rpm -ivh rabbitmq-server-3.12.14-1.el8.noarch.rpm
rpm -ivh erlang-25.3.2.13-1.el8.x86_64.rpm
3.启动
service rabbitmq-server start
//查看状态
service rabbitmq-server status
rabbitmqctl start_app
rabbitmqctl status
//打开插件
rabbit-plugins enable rabbitmq_management
//打开防火墙
firewall-cmd --add-port=5672/tcp --permanent
firewall-cmd --add-port=15672/tcp --permanent
firewall-cmd --add-port=25672/tcp --permanent
firewall-cmd --add-port=4369/tcp --permanent
//重启防火墙
firewall-cmd --reload
4.打开rabbitmq控制台
添加用户,分配权限
1.创建账号
rabbitmqctl add_user admin 123456
2.设置用户角色
rabbitmqctl set_user_tags admin administrator
3.设置用户权限
用户admin具有/vhost1这个virtual host中所有的配置,写、读权限
rabbitmqctl set_permissions [-p <vhostpath>]<user> <conf> <write> <read>
rabbitmqctl set_permissions -p "/" admin ".*" ".*"
4.
查询当前用户和角色
rabbitmqctl list_users

打开控制台地址:
云服务器ip:15672
用户名:admin 密码:admin
四、rabbitmq模型

在rabbitmq的服务器上有多个虚拟机,每个虚拟机 之间的资源是不互通的,虚拟机里面有exchange交换机和queue队列,queue用来存储和转发消息,exchange是用来绑定多个消息队列。queue中消息先存在内存,然后批量刷盘(比如1s刷盘一次)
生产者和消费者都需要与rabbitmq建立连接,并开启通道用来传送消息。
生产者可以通过交换机发送消息,message 到达 broker 的第一站,根据分发规则,匹配査询表中的 routing key,分发 消息到 queue 中去,也可以直接发送到队列当中。
消费者监听具体的队列,接收消息。
五、rabbitmq编程
1.依赖
java
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
</dependency>
2.生产者
java
package com.example.rabbitmq;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.MessageProperties;
public class FirstProducer {
private static final String HOST_NAME = "云服务器ip";
private static final int HOST_PORT = 5672;
private static final String QUEUE_NAME = "test";
private static final String VIRTUAL_HOST = "/mirror";
private static final String USER_NAME = "bing";
private static final String PASSWORD = "bing";
private static final String EXCHANGE_NAME = "callback";
public static void main(String[] args) throws Exception{
ConnectionFactory factory = new ConnectionFactory();
factory.setHost(HOST_NAME);
factory.setPort(HOST_PORT);
factory.setVirtualHost(VIRTUAL_HOST);
factory.setUsername(USER_NAME);
factory.setPassword(PASSWORD);
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
// 创建多个channel,如果number重复,返回null
// Channel channel1 = connection.createChannel(1);
// Channel channel2 = connection.createChannel(2);
//生成交换机,如果服务端没有该交换机,新建一个,有的话,如果参数不一致会报错
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "key1");
String message1 = "hello world1";
channel.basicPublish(EXCHANGE_NAME, "key1", MessageProperties.PERSISTENT_TEXT_PLAIN, message1.getBytes());
//没有交换机,直接发到队列.第一个参数是null
String message2 = "hello world2";
channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message2.getBytes());
}
}
3.消费者
3.1push模式
channel.basicConsume(QUEUE_NAME, false, consumer),使用false,手写应答:
basicAck 什么时候用?
消息处理成功 → 告诉 MQ:可以删掉这条消息了
作用
-
MQ 收到 ack,永久删除消息
-
不会再投递给任何消费者
basicReject 什么时候用?
消息处理失败 → 告诉 MQ:我处理不了
它有一个关键参数:requeue
① basicReject(..., requeue = true)
使用时机
-
临时故障:数据库挂了、Redis 超时、网络波动
-
你希望重新消费这条消息
行为
-
消息重新回到队列头部
-
马上会再次投递给消费者(可能造成循环)
② basicReject(..., requeue = false)
使用时机
-
永久失败:消息格式错、参数非法、数据已过期
-
再重试 100 次也没用
行为
-
消息直接丢弃
-
如果配置了死信队列(DLQ),会进入死信
basicNack 什么时候用?
channel.basicNack(deliveryTag, true, false);
第一个true代表批量处理,在此之前所有没有确认的消息,都进行nack,false代表处理单条消息
第二个true代表重新入队,false代表进入消息队列
java
try {
处理业务逻辑...
channel.basicAck(..., false); // 成功确认
} catch (可重试异常 e) {
channel.basicReject(..., true); // 重新入队
} catch (不可重试异常 e) {
channel.basicReject(..., false); // 丢弃/死信
}
java
package com.example.rabbitmq;
import com.rabbitmq.client.*;
import java.io.IOException;
public class FirstConsumerPush {
private static final String HOST_NAME = "ip";
private static final int HOST_PORT = 5672;
private static final String QUEUE_NAME = "test";
private static final String VIRTUAL_HOST = "/mirror";
private static final String USER_NAME = "bing";
private static final String PASSWORD = "bing";
private static final String EXCHANGE_NAME = "callback";
public static void main(String[] args) throws Exception{
ConnectionFactory factory = new ConnectionFactory();
factory.setHost(HOST_NAME);
factory.setPort(HOST_PORT);
factory.setVirtualHost(VIRTUAL_HOST);
factory.setUsername(USER_NAME);
factory.setPassword(PASSWORD);
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
channel.basicQos(1);
//处理接收到的消息,监听消息队列,消息传进来之后会触发这个方法的执行
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body)
throws IOException {
System.out.println("receive message:" + new String(body));
//手动编写应答代码
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
//false:不应答,需要在方法里面写应答的逻辑
channel.basicConsume(QUEUE_NAME, false, consumer);
//true:应答,在方法里面不需要写应答的逻辑
// channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
3.2pull模式
java
package com.example.rabbitmq;
import com.rabbitmq.client.*;
import java.io.IOException;
public class FirstConsumerPull {
private static final String HOST_NAME = "ip";
private static final int HOST_PORT = 5672;
private static final String QUEUE_NAME = "test";
private static final String VIRTUAL_HOST = "/mirror";
private static final String USER_NAME = "bing";
private static final String PASSWORD = "bing";
private static final String EXCHANGE_NAME = "callback";
public static void main(String[] args) throws Exception{
ConnectionFactory factory = new ConnectionFactory();
factory.setHost(HOST_NAME);
factory.setPort(HOST_PORT);
factory.setVirtualHost(VIRTUAL_HOST);
factory.setUsername(USER_NAME);
factory.setPassword(PASSWORD);
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
//每次拉取一条
channel.basicQos(1);
GetResponse response = channel.basicGet(QUEUE_NAME, false);
while (response != null) {
byte[] body = response.getBody();
System.out.println("receive:" + new String(body));
channel.basicAck(response.getEnvelope().getDeliveryTag(), false);
response = channel.basicGet(QUEUE_NAME, false);
}
//使用拉模式,主动获取消息,需要进行资源的关闭
channel.close();
connection.close();
}
}
4.创建监听
4.1生产者监听
| 监听器类型 | 监听事件 | 使用场景 |
|---|---|---|
| 确认监听器(Confirm) | 消息是否成功投递到 RabbitMQ 服务器(交换机) | 确保消息不丢失:若确认失败,可重试 / 记录日志 / 入库补偿 |
| 返回监听器(Return) | 消息到达交换机,但路由到队列失败(无匹配队列) | 处理 "路由失败" 的消息:如消息发错交换机、路由键错误,可重路由 / 告警 |
| 发送异常监听器(Exception) | 消息发送时的网络异常、连接中断等 | 捕获发送过程中的运行时异常,避免程序崩溃,实现失败重试 |
java
package com.example.rabbitmq;
import com.rabbitmq.client.*;
public class CallbackProducer {
private static final String HOST_NAME = "";
private static final int HOST_PORT = 5672;
private static final String QUEUE_NAME = "test";
private static final String VIRTUAL_HOST = "/mirror";
private static final String USER_NAME = "bing";
private static final String PASSWORD = "bing";
private static final String EXCHANGE_NAME = "callback";
public static void main(String[] args) throws Exception{
ConnectionFactory factory = new ConnectionFactory();
factory.setHost(HOST_NAME);
factory.setPort(HOST_PORT);
factory.setVirtualHost(VIRTUAL_HOST);
factory.setUsername(USER_NAME);
factory.setPassword(PASSWORD);
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//生成交换机,如果服务端没有该交换机,新建一个,有的话,如果参数不一致会报错
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "key1");
// Confirm 监听器:监听消息是否成功投递到 RabbitMQ Broker
channel.addConfirmListener(new ConfirmListener() {
// 消息成功发送到broker
@Override
public void handleAck(long deliveryTag, boolean multiple) {
System.out.println(" [Confirm] 消息确认成功: deliveryTag=" + deliveryTag + ", multiple=" + multiple);
}
// 消息没有成功投递到broker
@Override
public void handleNack(long deliveryTag, boolean multiple) {
System.out.println(" [Confirm] 消息确认失败: deliveryTag=" + deliveryTag + ", multiple=" + multiple);
}
});
// Return 监听器:监听无法路由的消息
channel.addReturnListener(new ReturnListener() {
@Override
public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) {
System.out.println(" [Return] 消息返回:");
System.out.println(" Reply Code: " + replyCode);
System.out.println(" Reply Text: " + replyText);
System.out.println(" Exchange: " + exchange);
System.out.println(" Routing Key: " + routingKey);
System.out.println(" Properties: " + properties);
System.out.println(" Body: " + new String(body));
}
});
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder()
.deliveryMode(2)
.priority(1)
.contentEncoding("UTF-8")
.correlationId("123")
.build();
channel.basicPublish(EXCHANGE_NAME, "key1", properties, "hello world1".getBytes());
channel.basicPublish(EXCHANGE_NAME, "key2", properties, "hello world2".getBytes());
//确保能收到服务端回调
Thread.sleep(1000);
channel.close();
connection.close();
}
}
4.2消费者监听器
| 监听器类型 | 监听事件 | 使用场景 |
|---|---|---|
| 消息监听处理器(MessageListener) | 收到消息并触发业务处理 | 核心监听器:消费消息的核心逻辑,对应你之前问的 basic.ack/reject 操作 |
| 消费异常监听器(ErrorHandler) | 消息处理过程中抛出异常 | 统一捕获消费异常,避免单个消息失败导致消费者停止,可实现重试 / 死信转发 |
| 消费者生命周期监听器 | 消费者启动、停止、连接中断 / 恢复 | 监控消费者状态,如消费者宕机告警、重连逻辑、资源释放 |
java
package com.example.rabbitmq;
import com.rabbitmq.client.*;
import java.io.IOException;
public class CallbackConsumer {
private static final String HOST_NAME = "";
private static final int HOST_PORT = 5672;
private static final String QUEUE_NAME = "test";
private static final String VIRTUAL_HOST = "/mirror";
private static final String USER_NAME = "bing";
private static final String PASSWORD = "bing";
private static final String EXCHANGE_NAME = "callback";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost(HOST_NAME);
factory.setPort(HOST_PORT);
factory.setVirtualHost(VIRTUAL_HOST);
factory.setUsername(USER_NAME);
factory.setPassword(PASSWORD);
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
channel.basicQos(1);
channel.basicConsume(QUEUE_NAME, false, // autoAck 设置为 false,启用手动确认
//成功收到消息
new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery delivery) throws IOException {
String message = new String(delivery.getBody());
System.out.println("receive message: " + message + " correlationId: " + delivery.getProperties().getCorrelationId());
// 手动确认消息
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
},
//消息在队列中被取消
new CancelCallback() {
@Override
public void handle(String consumerTag) throws IOException {
System.out.println("cancel: " + consumerTag);
}
},
//消费者断开连接
new ConsumerShutdownSignalCallback() {
@Override
public void handleShutdownSignal(String consumerTag, ShutdownSignalException signal) {
System.out.println("signal: " + signal);
}
});
// 防止程序立即退出,保持消费者运行
System.out.println("Waiting for messages. To exit press CTRL+C");
Thread.sleep(Long.MAX_VALUE); // 或者使用其他阻塞方式
// 资源关闭(实际应用中应在适当位置关闭)
channel.close();
connection.close();
}
}
六、应用场景
1.hello world
发送,接收消息
2.work queue
工作模式,一个生产者,多个消费者
注意问题:
建议手动应答,避免自动应答之后处理信息失败。

3.发布订阅

加入交换机,进一步解耦,使用fanout类型的交换机,交换机自动往绑定的队列去发消息
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
//队列的名称可以为null
channel.basicPublish(EXCHANGE_NAME, "", properties, "hello world1".getBytes());
4.基于内容的路由

在上一个交换机往所有的绑定队列上进行发送消息基础上,增加一个路由配置routing key,将消息发送到对应的队列上。
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "key1");
String message1 = "hello world1";
channel.basicPublish(EXCHANGE_NAME, "key1", MessageProperties.PERSISTENT_TEXT_PLAIN, message1.getBytes());
5.基于话题的路由

//生产者
//交换机是topic类型
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
String message1 = "hello world1";
//绑定指定的topic
channel.basicPublish(EXCHANGE_NAME, "a.info", MessageProperties.PERSISTENT_TEXT_PLAIN, message1.getBytes());
//消费者,将队列绑定一个模糊匹配topic
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "*.info");
6.headers头部路由机制
不再根据routing key,而是根据头部信息进行匹配
java
channel.exchangeDeclare(EXCHANGE_NAME, "headers");
//生产者队列一开始就绑定headers
Map<String, Object> headers=new HashMap<>();
headers.put("key1", "value1");
headers.put("key2", "value2");
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "key1",headers);
//将headers加入properties
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder()
.deliveryMode(2)
.priority(1)
.contentEncoding("UTF-8")
.correlationId("123")
.headers(headers)
.build();
//发送的消息会根据headers进行匹配
channel.basicPublish(EXCHANGE_NAME, "key1", properties, "hello world".getBytes());
//消费者队列绑定headers
Map<String, Object> headers=new HashMap<>();
//x-match特定参数,any代表有一个匹配就可以,all代表所有匹配才行
headers.put("x-match", "any");
headers.put("key1", "value11");
headers.put("key2", "value22");
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "key1",headers);
结果:因为两条头部都不匹配,所以消费者收不到生产者的消息
7.Publisher Confirms
核心:
- channel.confirmSelect();开启Publisher Confirms模式
- channel.waitForConfirms();同步确认等待,true代表消息确认成功,false代表确认失败
java
package com.example.rabbitmq;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConfirmCallback;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.util.UUID;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.TimeUnit;
/**
* RabbitMQ Publisher Confirm 机制:单条确认、批量确认、异步确认
*/
public class PublisherConfirmDemo {
// MQ 连接配置
private static final String HOST_NAME = "180.76.56.131";
private static final int HOST_PORT = 5672;
private static final String QUEUE_NAME = "test";
private static final String VIRTUAL_HOST = "/mirror";
private static final String USER_NAME = "bing";
private static final String PASSWORD = "bing";
private static final String EXCHANGE_NAME = "callback";
private static final String ROUTING_KEY = "key1";
// 测试消息数量
private static final int MESSAGE_COUNT = 1000;
public static void main(String[] args) throws Exception {
// 1. 单条同步确认(性能最差,适合少量消息)
System.out.println("===== 单条同步确认 =====");
publishMessagesWithSingleConfirm(MESSAGE_COUNT);
// 2. 批量同步确认(性能中等,适合中小量消息)
System.out.println("\n===== 批量同步确认 =====");
publishMessagesWithBatchConfirm(MESSAGE_COUNT, 100); // 每100条确认一次
// 3. 异步确认(性能最优,适合高并发、大量消息)
System.out.println("\n===== 异步确认 =====");
publishMessagesWithAsyncConfirm(MESSAGE_COUNT);
}
/**
* 1. 单条同步确认:发送一条,确认一条
* 特点:简单但性能差(每发一条都阻塞等待确认),适合消息量极少的场景
*/
public static void publishMessagesWithSingleConfirm(int messageCount) throws Exception {
// 创建连接和通道
Connection connection = getConnection();
Channel channel = connection.createChannel();
// 开启 Publisher Confirm 模式(核心)
channel.confirmSelect();
long startTime = System.currentTimeMillis();
for (int i = 0; i < 10; i++) {
String message = "Single Confirm Message - " + i + " - " + UUID.randomUUID();
// 发送消息
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, null, message.getBytes());
// 同步等待确认(阻塞)
// waitForConfirms():返回boolean,true=确认成功,false=确认失败
if (channel.waitForConfirms()) {
System.out.println("消息 " + i + " 发送并确认成功");
} else {
System.err.println("消息 " + i + " 发送失败,需重试");
// 此处可添加重试逻辑
}
}
long endTime = System.currentTimeMillis();
System.out.printf("单条确认发送 %d 条消息耗时:%d ms%n", messageCount, (endTime - startTime));
// 关闭资源
channel.close();
connection.close();
}
/**
* 2. 批量同步确认:发送一批,确认一批
* 特点:性能比单条好,减少确认次数,但如果批量中一条失败,无法定位具体失败消息
* @param messageCount 总消息数
* @param batchSize 每批确认的数量
*/
public static void publishMessagesWithBatchConfirm(int messageCount, int batchSize) throws Exception {
Connection connection = getConnection();
Channel channel = connection.createChannel();
//开启 Publisher Confirm 模式
channel.confirmSelect();
long startTime = System.currentTimeMillis();
for (int i = 0; i < messageCount; i++) {
String message = "Batch Confirm Message - " + i + " - " + UUID.randomUUID();
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, null, message.getBytes());
// 每积累 batchSize 条消息,批量确认一次
if ((i + 1) % batchSize == 0) {
// waitForConfirmsOrDie():确认失败直接抛异常,无需手动判断
boolean isConfirmed = channel.waitForConfirms(5000); // 超时5秒
if (!isConfirmed) {
throw new RuntimeException("批量确认超时,部分消息可能未被确认");
}
System.out.println("批量确认:第 " + (i + 1) + " 条消息确认成功");
}
}
// 处理剩余未批量的消息
if (messageCount % batchSize != 0) {
boolean isConfirmed = channel.waitForConfirms(5000); // 超时5秒
if (!isConfirmed) {
throw new RuntimeException("批量确认超时,部分消息可能未被确认");
}
System.out.println("批量确认:剩余 " + (messageCount % batchSize) + " 条消息确认成功");
}
long endTime = System.currentTimeMillis();
System.out.printf("批量确认发送 %d 条消息耗时:%d ms%n", messageCount, (endTime - startTime));
channel.close();
connection.close();
}
/**
* 3. 异步确认:发送和确认异步执行,性能最优,需要开启ackCallback和nackCallback
* 特点:非阻塞,高吞吐,可精准定位失败消息,适合高并发场景
*/
public static void publishMessagesWithAsyncConfirm(int messageCount) throws Exception {
Connection connection = getConnection();
Channel channel = connection.createChannel();
//开启 Publisher Confirm 模式
channel.confirmSelect();
// 存储未确认的消息(线程安全的有序Map,key=消息序号,value=消息内容)
ConcurrentNavigableMap<Long, String> unconfirmedMessages = new ConcurrentSkipListMap<>();
// 回调1:确认成功的回调
ConfirmCallback ackCallback = (deliveryTag, multiple) -> {
if (multiple) {
// multiple=true:批量处理,确认的是小于等于当前deliveryTag的所有消息
//headMap用于返回一个视图,该视图包含所有键小于或等于 deliveryTag 的键值对
ConcurrentNavigableMap<Long, String> confirmed = unconfirmedMessages.headMap(deliveryTag, true);
confirmed.clear(); // 移除已确认的消息
System.out.println("异步批量确认:deliveryTag=" + deliveryTag + " 及之前的消息确认成功");
} else {
// multiple=false:单条处理,仅确认当前deliveryTag的消息
unconfirmedMessages.remove(deliveryTag);
System.out.println("异步单条确认:deliveryTag=" + deliveryTag + " 消息确认成功");
}
};
// 回调2:确认失败的回调
ConfirmCallback nackCallback = (deliveryTag, multiple) -> {
String failedMessage = unconfirmedMessages.get(deliveryTag);
System.err.println("消息确认失败:deliveryTag=" + deliveryTag + ",消息内容=" + failedMessage);
// 此处可添加重试/告警/入库补偿逻辑
if (multiple) {
ConcurrentNavigableMap<Long, String> failed = unconfirmedMessages.headMap(deliveryTag, true);
failed.clear();
} else {
unconfirmedMessages.remove(deliveryTag);
}
};
// 注册异步确认回调(核心)
channel.addConfirmListener(ackCallback, nackCallback);
long startTime = System.currentTimeMillis();
for (int i = 0; i < messageCount; i++) {
String message = "Async Confirm Message - " + i + " - " + UUID.randomUUID();
// 获取当前消息的deliveryTag(从1开始递增)
long deliveryTag = channel.getNextPublishSeqNo();
//先存储后发送
// 存储未确认的消息
unconfirmedMessages.put(deliveryTag, message);
// 发送消息(非阻塞)
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, null, message.getBytes());
}
// 等待所有消息确认完成(实际生产中可通过监控unconfirmedMessages是否为空判断)
while (!unconfirmedMessages.isEmpty()) {
Thread.sleep(10);
}
long endTime = System.currentTimeMillis();
System.out.printf("异步确认发送 %d 条消息耗时:%d ms%n", messageCount, (endTime - startTime));
channel.close();
connection.close();
}
/**
* 创建 RabbitMQ 连接(抽取公共方法)
*/
private static Connection getConnection() throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost(HOST_NAME);
factory.setPort(HOST_PORT);
factory.setVirtualHost(VIRTUAL_HOST);
factory.setUsername(USER_NAME);
factory.setPassword(PASSWORD);
// 开启自动重连(生产环境建议开启)
factory.setAutomaticRecoveryEnabled(true);
factory.setNetworkRecoveryInterval(5000);
return factory.newConnection();
}
}
代码包含三种模式:
7.1单条确认
- 发送一条,确认一条
- 使用**channel.confirmSelect()**开启 Publisher Confirm 模式
- 使用**channel.waitForConfirms()同步等待确认,**true代表确认成功,false代表确认失败
- 特点:简单但性能差(每发一条都阻塞等待确认),适合消息量极少的场景
7.2批量同步确认
- 发送一批,确认一批
- 使用**channel.confirmSelect()**开启 Publisher Confirm 模式
- 使用**channel.waitForConfirms()同步等待确认,发一批消息后调用一次,**true代表确认成功,false代表确认失败
- 特点:性能比单条好,减少确认次数,但如果批量中一条失败,无法定位具体失败消息
7.3异步确认
- 发送和确认异步执行,性能最优
- 在发送消息之前,使用线程安全的map,ConcurrentNavigableMap存储未确认的消息
- 需要开启ackCallback和nackCallback,清除消息
- 当map为空之后,处理完成
- 特点:非阻塞,高吞吐,可精准定位失败消息,适合高并发场景
七、SpringBoot集成RabbitMQ
1.依赖
java
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2.配置
java
spring:
# RabbitMQ 基础配置
rabbitmq:
host: 127.0.0.1 # MQ 服务器地址(替换为你的实际地址)
port: 5672 # 端口(默认5672)
username: guest # 用户名(默认guest)
password: guest # 密码(默认guest)
virtual-host: / # 虚拟主机(默认/)
# 连接池配置(生产环境建议开启)
connection-timeout: 15000
cache:
channel:
size: 50 # 通道缓存大小
connection:
size: 10 # 连接池大小
# 生产者确认机制(Publisher Confirm)
publisher-confirm-type: correlated # 开启异步确认(NONE/ CORRELATED/ SIMPLE)
publisher-returns: true # 开启消息返回(路由失败时回调)
# 消费者配置
listener:
simple:
acknowledge-mode: manual # 手动确认(NONE/ AUTO/ MANUAL)
concurrency: 1 # 最小消费线程数
max-concurrency: 5 # 最大消费线程数
prefetch: 10 # 每次预取10条消息(限流)
retry:
enabled: true # 开启消费重试
max-attempts: 3 # 最大重试次数
initial-interval: 1000ms # 首次重试间隔
3.配置类RabbitMqConfig
主要是定义队列,交换机,队列交换机的绑定关系
java
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* RabbitMQ 队列/交换机/绑定 配置类
*/
@Configuration
public class RabbitMqConfig {
// 1. 定义常量(避免硬编码)
public static final String EXCHANGE_NAME = "callback";
public static final String QUEUE_NAME = "boot_test_queue";
public static final String ROUTING_KEY = "boot.test.key";
// 2. 声明直连交换机(Direct Exchange)
@Bean
public DirectExchange bootDirectExchange() {
// 参数:名称、是否持久化、是否自动删除
return new DirectExchange(EXCHANGE_NAME, true, false);
}
// 3. 声明队列(Queue)
@Bean
public Queue bootTestQueue() {
// 参数:名称、是否持久化、是否排他、是否自动删除
return QueueBuilder.durable(QUEUE_NAME).build();
}
// 4. 绑定交换机和队列(Binding)
//绑定队列到交换机,使用路由键
@Bean
public Binding bindQueueToExchange(Queue bootTestQueue, DirectExchange bootDirectExchange) {
return BindingBuilder.bind(bootTestQueue).to(bootDirectExchange).with(ROUTING_KEY);
}
}
4.生产者
使用init定义Confirm 和 Return 回调,定义sendMessage方法,发送消息:交换机、路由键、消息内容、消息追踪ID
java
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.UUID;
/**
* RabbitMQ 生产者
*/
@Component
public class RabbitMqProducer {
// 注入Spring封装的RabbitTemplate(核心发送工具)
@Autowired
private RabbitTemplate rabbitTemplate;
// 初始化:配置生产者确认和返回回调
@PostConstruct
public void init() {
// 1. 消息确认回调(是否成功投递到交换机)
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
String msgId = correlationData != null ? correlationData.getId() : "未知ID";
if (ack) {
System.out.println("消息[" + msgId + "]成功投递到交换机");
} else {
System.err.println("消息[" + msgId + "]投递失败:" + cause);
// 失败重试/入库补偿逻辑可在此实现
}
});
// 2. 消息返回回调(投递到交换机,但路由到队列失败)
rabbitTemplate.setReturnsCallback(returnedMessage -> {
String msg = new String(returnedMessage.getMessage().getBody());
System.err.println("消息路由失败:" +
"路由键=" + returnedMessage.getRoutingKey() +
",消息=" + msg +
",原因=" + returnedMessage.getReplyText());
});
}
/**
* 发送消息
* @param content 消息内容
*/
public void sendMessage(String content) {
// 生成唯一ID(用于追踪消息)
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
// 发送消息:交换机、路由键、消息内容、消息追踪ID
rabbitTemplate.convertAndSend(
RabbitMqConfig.EXCHANGE_NAME,
RabbitMqConfig.ROUTING_KEY,
content,
correlationData
);
System.out.println("消息发送成功,内容:" + content + ",ID:" + correlationData.getId());
}
}
5.消费者
@RabbitListener(queues = RabbitMqConfig.QUEUE_NAME)监听指定队列
java
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* RabbitMQ 消费者
*/
@Component
public class RabbitMqConsumer {
/**
* 监听指定队列(自动关联配置类声明的队列)
* @param message 消息体(Spring封装的Message)
* @param channel 原生Channel(用于手动确认)
* @param deliveryTag 消息投递标签(唯一标识)
*/
@RabbitListener(queues = RabbitMqConfig.QUEUE_NAME)
public void consumeMessage(Message message, Channel channel,
@org.springframework.amqp.core.AmqpHeaders.DeliveryTag long deliveryTag) throws IOException {
try {
// 1. 解析消息内容
String msgContent = new String(message.getBody());
System.out.println("收到消息:" + msgContent + ",deliveryTag:" + deliveryTag);
// 2. 模拟业务处理(可替换为实际业务逻辑)
if (msgContent.contains("error")) {
// 模拟:消息格式错误 → 永久失败,拒绝并进入死信队列(requeue=false)
throw new IllegalArgumentException("消息格式非法,无法处理");
}
// 3. 处理成功 → 手动确认(basic.ack)
// multiple=false:仅确认当前消息;true:确认所有小于等于当前deliveryTag的消息
channel.basicAck(deliveryTag, false);
System.out.println("消息[" + deliveryTag + "]确认成功");
} catch (IllegalArgumentException e) {
// 场景1:永久失败 → basic.nack(拒绝,不重新入队)
channel.basicNack(deliveryTag, false, false);
System.err.println("消息[" + deliveryTag + "]永久失败,已拒绝:" + e.getMessage());
} catch (Exception e) {
// 场景2:临时失败 → basic.nack(拒绝,重新入队)
channel.basicNack(deliveryTag, false, true);
System.err.println("消息[" + deliveryTag + "]临时失败,重新入队:" + e.getMessage());
}
}
}
6.测试类
java
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class RabbitMqTest {
@Autowired
private RabbitMqProducer rabbitMqProducer;
// 测试正常消息
@Test
public void testSendNormalMessage() {
rabbitMqProducer.sendMessage("SpringBoot集成RabbitMQ - 正常消息");
// 休眠1秒,确保消费者处理完成
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
}
// 测试永久失败消息
@Test
public void testSendErrorMessage() {
rabbitMqProducer.sendMessage("SpringBoot集成RabbitMQ - error - 永久失败消息");
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
}
// 测试临时失败消息(可自定义业务异常模拟)
@Test
public void testSendTempFailMessage() {
rabbitMqProducer.sendMessage("SpringBoot集成RabbitMQ - 临时失败消息");
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
}
}
八、扩展功能
1.队列形式

1.1classic:经典队列(默认)
classic:经典队列,先进先出队列,如果消息失败,就需要重新入队,放在头部
- 问题:如果消息积压,内存压力过大,性能可能下降,集群场景不适用
- version1:消息来了,持久化到磁盘,内存加载,也是全部加载
- version2:每次内存只加载需要的消息
- 适用于消息量比较小的场景
1.2Quorum仲裁队列
- 数据一定要持久化,而且要与其他节点进行消息同步,使用Raft 一致性算法进行消息的入队,可以处理毒消息。
- 毒消息:设置发送的次数,当失败重发次数达到你设置的次数之后,不会再入队
- 使用:在queueDeclare的时候,往params里面,设定x-queue-type为quorum
- Quorum是集群队列,当集群扩容的时候,需要你手动调整队列的扩容
- 适合长期存在的数据,对低延迟要求不高,要求数据安全与容错更高,比如电商订单,可以处理慢一点,但是不能丢失。
java
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class QuorumQueueConfig {
// Quorum 队列名称
public static final String QUORUM_ORDER_QUEUE = "quorum_order_queue";
/**
* 声明 Quorum 队列(核心:x-queue-type=quorum)
* 副本数默认 3,生产环境建议设为奇数(3/5)
*/
@Bean
public Queue quorumOrderQueue() {
Map<String, Object> args = new HashMap<>();
// 指定队列类型为 Quorum
args.put("x-queue-type", "quorum");
// 可选:设置初始副本数(默认 3)
args.put("x-quorum-initial-group-size", 3);
// 可选:消息过期时间(TTL),按需配置
args.put("x-message-ttl", 86400000); // 24小时过期
// 持久化 + Quorum 类型
return QueueBuilder.durable(QUORUM_ORDER_QUEUE)
.withArguments(args)
.build();
}
}
1.3流式队列Stream
适合消费者多,读消息比较频繁的场景
以append-only只添加的日志记录消息,持久化到磁盘,然后通过offset调整消费者的消费进度
java
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class StreamQueueConfig {
// Stream 队列名称
public static final String STREAM_LOG_QUEUE = "stream_log_queue";
/**
* 声明 Stream 队列(核心:x-queue-type=stream)
* 关键参数:x-max-length-bytes(最大存储字节)、x-stream-max-segment-size-bytes(分段大小)
*/
@Bean
public Queue streamLogQueue() {
Map<String, Object> args = new HashMap<>();
// 指定队列类型为 Stream
args.put("x-queue-type", "stream");
// 最大存储大小(10GB,按需配置)
args.put("x-max-length-bytes", 10 * 1024 * 1024 * 1024L);
// 消息保留时间(7天,过期自动删除)
args.put("x-max-age", "7d");
// 分段大小(默认 500MB,避免单个文件过大)
args.put("x-stream-max-segment-size-bytes", 500 * 1024 * 1024L);
// Stream 队列必须持久化
return QueueBuilder.durable(STREAM_LOG_QUEUE)
.withArguments(args)
.build();
}
}
java
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class StreamQueueConsumer {
/**
* 消费 Stream 队列(默认从最新偏移量开始消费)
*/
@RabbitListener(queues = StreamQueueConfig.STREAM_LOG_QUEUE)
public void consumeSystemLog(Message message, Channel channel) throws IOException {
String content = new String(message.getBody());
System.out.println("Stream 队列消费日志:" + content);
// Stream 队列无需手动 ack!偏移量自动提交(默认)
// 若需手动提交偏移量,需配置 x-stream-offset 并手动调用 channel.basicAck
}
/**
* 重放历史日志(从队列头部开始消费)
* 需在监听时指定偏移量参数
*/
@RabbitListener(
queues = StreamQueueConfig.STREAM_LOG_QUEUE,
arguments = {
@org.springframework.amqp.rabbit.annotation.Argument(
name = "x-stream-offset",
value = "first" // first=从头消费,last=从尾消费,数字=指定偏移量
)
}
)
public void replaySystemLog(Message message) {
String content = new String(message.getBody());
System.out.println("Stream 队列重放日志:" + content);
}
}
2.死信队列
2.1什么是死信队列

消息转移到死信队列过程中,是没有确认机制的,所以是不安全的。
消息在转移到死信队列之后会在headers里面增加一些信息,来表明这是一条死信消息。
2.2参数
只有在classic和quorum队列才能设置

2.3示例
- 首先在控制台设置一个队列加上上面的参数,定义一个死信交换机fanout类型,绑定一个死信队列
- 然后定义一个普通的生产者-消费者,消费者使用reject拒绝消息。
- 拒绝的消息会进入死信队列,定义一个消费者监听死信队列,会收到这条消息。
2.4基于死信队列实现延迟队列
一个正常队列,没有对应的消费者,设置一个消息的TTL,达到这个TTL消息转移到死信队列。死信队列再绑定消费者,进行正常的消费,实现一个延迟队列。
3.消息分片存储插件:rabbitmq_sharding
会对消息的 routing key 做哈希运算,然后用 hash % N(N 是绑定到交换机的队列数)来决定消息路由到哪个队列。
bash
rabbitmq-plugins enable rabbitmq_sharding

在新建交换机的时候,会出现一个新的类型:x-modulus-hash
之后消息会均匀分到三个分片上,然后消费者消费就需要消费三次。
java
//生产者
channel.exchangeDeclare(EXCHANGE_NAME, "x-modulus-hash");
channel.basicPublish(EXCHANGE_NAME, String.valueOf(i), properties, message.getBytes());
//消费者,只声明一个队列,队列名是交换机的名字
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
//三个分片,消费三次
channel.basicConsume(QUEUE_NAME, false, consumer);
channel.basicConsume(QUEUE_NAME, false, consumer);
channel.basicConsume(QUEUE_NAME, false, consumer);
存在的问题:
- 分片的过程中是不考虑顺序的
- 消费者绑定队列的时候,哪个队列消费者少,就绑定到哪个队列。但是消费者数量不等于消息数量,可能会导致一部分闲死,一部分忙死,根本达不到 "负载均衡"。所以实际开发中,尽量别单独用这个插件,改用 Stream 队列 / Quorum 队列 + 业务层分片(比如按订单 ID 手动分队列),才能真正均衡。