上一遍我们学会了怎么使用RabbitMQ发送消息和消费消息,在文章末尾给大家了留下了许多问题,这一章咱们就去一一解决那些问题,还没有去看的同学可以去看看哦!此篇也是在上一遍基础进行改造!RabbitMQ之入门篇(二)_想成为大佬的小卒!的博客-CSDN博客
一.发送方确认机制
1.1单条确认机制
首先发送方调用channel.confirmSelect()方法, 调用channel.waitForConfirms()方法,等待确认,代码如下
package rabbitmq.demo.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 相当于生产者
* @Author sj
* @Date: 2022/04/18/ 10:58
* @Description
*/
@RestController
@Slf4j
public class ProducerController {
/**
* 序列化类
*/
private ObjectMapper objectMapper = new ObjectMapper();
/**
* 模拟MQ发送消息
* @param userPo
*/
@PostMapping("/send")
public void sendNews(@RequestBody UserPo userPo) throws JsonProcessingException {
// 获取连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
// 设置端口
connectionFactory.setHost("localhost");
// 获取于Broker的TCP连接connection
Connection connection;
// 序列化对象
String body = objectMapper.writeValueAsString(userPo);
// 获取字节码,因为MQ底层是使用字节码传输,方便下面使用
byte[] bytes = body.getBytes();
try {
connection = connectionFactory.newConnection();
// 创建信道
Channel channel = connection.createChannel();
// 启用发送方确认机制
channel.confirmSelect();
// 向交换机发送消息
channel.basicPublish(
// 发送到那个交换机
"consumer.exchange",
// routingKye是那个
"key.send",
// 是否有特殊参数
null,
// 消息体
bytes
);
// 发送方发送消息成功
log.info("消息发送成功");
// 发送方确认机制
if (channel.waitForConfirms()){
// 此处我们就可以进行一些相应的业务处理
log.info("RabbitMQ confirm success");
}else {
log.info("RabbitMQ confirm smessage");
}
// 关闭连接
channel.close();
connection.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
启动项目访问接口,由图可以看到消息发送成功了,确认机制也的确返回了是否发送成功的信息,我们就可以根据自己的业务进行相应的处理
1.2多条确认机制(不太建议使用)
首先发送方调用channel.confirmSelect()方法, 调用channel.waitForConfirms()方法,等待确认,代码如下
我们修改代码,以达到多条消息发送的情况
package rabbitmq.demo.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 相当于生产者
* @Author sj
* @Date: 2022/04/18/ 10:58
* @Description
*/
@RestController
@Slf4j
public class ProducerController {
/**
* 序列化类
*/
private ObjectMapper objectMapper = new ObjectMapper();
/**
* 模拟MQ发送消息
* @param userPo
*/
@PostMapping("/send")
public void sendNews(@RequestBody UserPo userPo) throws JsonProcessingException {
// 获取连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
// 设置端口
connectionFactory.setHost("localhost");
// 获取于Broker的TCP连接connection
Connection connection;
// 序列化对象
String body = objectMapper.writeValueAsString(userPo);
// 获取字节码,因为MQ底层是使用字节码传输,方便下面使用
byte[] bytes = body.getBytes();
try {
connection = connectionFactory.newConnection();
// 创建信道
Channel channel = connection.createChannel();
// 启用发送方确认机制
channel.confirmSelect();
for (int i=0 ; i<10;i++) {
// 向交换机发送消息
channel.basicPublish(
// 发送到那个交换机
"consumer.exchange",
// routingKye是那个
"key.send",
// 是否有特殊参数
null,
// 消息体
bytes
);
// 发送方发送消息成功
log.info("消息发送成功");
}
// 发送方确认机制
if (channel.waitForConfirms()){
log.info("RabbitMQ confirm success");
}else {
log.info("RabbitMQ confirm smessage");
}
// 关闭连接
channel.close();
connection.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
由图可见多条消息发送成功,但是我想大家也感觉到问题的所在,如果消息都发送成功了还好,但是如果其中有一条消息失败了,返回false,就出问题了,我们怎么知道是那一条消息失败了,怎么去排查勒,怎么去处理,不可能我们全部重发吧,所以不太建议大家使用
1.3异步确认消息机制
首先配置channel,channel.confirmSelect()方法,然后new ConfirmLisnener,覆写方法,成功调用addConfirmListnener,发送消息后,会回调此方法通知是否发送成功,异步确有可能单条,也可能是多条,取绝于MQ
修改代码如下:
package rabbitmq.demo.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConfirmListener;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 相当于生产者
* @Author sj
* @Date: 2022/04/18/ 10:58
* @Description
*/
@RestController
@Slf4j
public class ProducerController {
/**
* 序列化类
*/
private ObjectMapper objectMapper = new ObjectMapper();
/**
* 模拟MQ发送消息
* @param userPo
*/
@PostMapping("/send")
public void sendNews(@RequestBody UserPo userPo) throws JsonProcessingException {
// 获取连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
// 设置端口
connectionFactory.setHost("localhost");
// 获取于Broker的TCP连接connection
Connection connection;
// 序列化对象
String body = objectMapper.writeValueAsString(userPo);
// 获取字节码,因为MQ底层是使用字节码传输,方便下面使用
byte[] bytes = body.getBytes();
try {
connection = connectionFactory.newConnection();
// 创建信道
Channel channel = connection.createChannel();
ConfirmListener confirmListener = new ConfirmListener() {
// 成功回调此方法
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
log.info("ACK, deliveryTag:{} multiple:{}",deliveryTag,multiple);
}
// 失败回调此方法
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
log.info("NACK, deliveryTag:{} multiple:{}",deliveryTag,multiple);
}
};
// 启用发送方确认机制
channel.confirmSelect();
channel.addConfirmListener(confirmListener);
for (int i=0 ; i<10;i++) {
// 向交换机发送消息
channel.basicPublish(
// 发送到那个交换机
"consumer.exchange",
// routingKye是那个
"key.send",
// 是否有特殊参数
null,
// 消息体
bytes
);
// 发送方发送消息成功
log.info("消息发送成功");
}
Thread.sleep(1000000);
// 发送方确认机制
if (channel.waitForConfirms()){
log.info("RabbitMQ confirm success");
}else {
log.info("RabbitMQ confirm message");
}
// 关闭连接
channel.close();
connection.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
图中的deliveryTag代表消息序号,multiple代表消息是否发送成功,怎么看出是异步的了,看前面的线程编号是不是不一样?
二.消息返回机制(消息是否正确被路由)
1.消息返回机制的原理:消息发送后,中间件会对消息进行路由,如果没有发现目标队列,中间件会通知方式方,此时会回调ReturenLisnener方法,
2.如何开启?
在RabbitMQ基础配置中有一个关键配置想;mandtory,弱为false,RabbitMQ将直接丢弃无法路由的消息,若为ture,则不会
修改代码
package rabbitmq.demo.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.rabbitmq.client.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 相当于生产者
* @Author sj
* @Date: 2022/04/18/ 10:58
* @Description
*/
@RestController
@Slf4j
public class ProducerController {
/**
* 序列化类
*/
private ObjectMapper objectMapper = new ObjectMapper();
/**
* 模拟MQ发送消息
* @param userPo
*/
@PostMapping("/send")
public void sendNews(@RequestBody UserPo userPo) throws JsonProcessingException {
// 获取连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
// 设置端口
connectionFactory.setHost("localhost");
// 获取于Broker的TCP连接connection
Connection connection;
// 序列化对象
String body = objectMapper.writeValueAsString(userPo);
// 获取字节码,因为MQ底层是使用字节码传输,方便下面使用
byte[] bytes = body.getBytes();
try {
connection = connectionFactory.newConnection();
// 创建信道
Channel channel = connection.createChannel();
ConfirmListener confirmListener = new ConfirmListener() {
// 成功回调此方法
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
log.info("ACK, deliveryTag:{} multiple:{}",deliveryTag,multiple);
}
// 失败回调此方法
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
log.info("NACK, deliveryTag:{} multiple:{}",deliveryTag,multiple);
}
};
// 启用发送方确认机制
channel.confirmSelect();
channel.addConfirmListener(confirmListener);
channel.addReturnListener(new ReturnListener() {
// 消息路由失败会调用此方法
@Override
public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
log.info("Message Return:replyCode:{},replyText:{},exchange:{},routingKey:{},properties:{},body:{body}"
,replyCode,replyText,exchange,properties,body);
}
});
for (int i=0 ; i<10;i++) {
// 向交换机发送消息
channel.basicPublish(
// 发送到那个交换机
"consumer.exchange",
// routingKye是那个
"key.send",
true,
// 是否有特殊参数
null,
// 消息体
bytes
);
// 发送方发送消息成功
log.info("消息发送成功");
}
Thread.sleep(1000);
// 发送方确认机制
if (channel.waitForConfirms()){
log.info("RabbitMQ confirm success");
}else {
log.info("RabbitMQ confirm message");
}
// 关闭连接
channel.close();
connection.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
由图可以看出,我们的消息返回机制回调方法了,我们就可以在消息没有被正确路由的时候,在回调方法里面进行相应的业务处理
当然也就new ReturnCallback覆写handle方法,小编就不写了,他们的区别就是一个返回的对像,一个返回的参数!
三.消费端确认机制
消息一般默认是为自动签收ack,然后我们把ack改为false,就是手动签收,去消费者方修改代码
package rabbitmq.demo.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.rabbitmq.client.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.User;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 消费者 监听消息
* @Author sj
* @Date: 2022/04/18/ 11:13
* @Description
*/
@Service
@Slf4j
public class Consumer {
/**
*序列化类
*/
private ObjectMapper objectMapper = new ObjectMapper();
Channel channel;
/**
* 异步线程
* 声明消息对列、交换机、绑定消息的处理
* 监听对列
*/
@Async
public void handleMessage() throws IOException, TimeoutException, InterruptedException {
// 创建工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
// 设置端口
connectionFactory.setHost("localhost");
// 创建于broker的Tcp连接
try(Connection connection = connectionFactory.newConnection()){
// 创建信道
this.channel = connection.createChannel();
// 创建交换机
channel.exchangeDeclare(
// 交换机名字
"consumer.exchange",
// 交换机类型
BuiltinExchangeType.DIRECT,
// 数据是否持久化
true,
// 不用了是否删除
false,
// 是否需要特殊参数
null
);
channel.queueDeclare(
// 对列名
"key.consumer",
// 数据持久化
true,
// 是否独占吃列
false,
// 不用了是否删除
false,
// 是否需要特殊参数
null
);
// 对列和交换机进行绑定
channel.queueBind(
"key.consumer",
"consumer.exchange",
"key.send"
);
// 监听对列
channel.basicConsume(
// 监听那个对列
"key.consumer",
// 手动签收
false,
// 消息处理
deliverCallback,
consumerTag -> {}
);
while (true){
Thread.sleep(1000000);
}
}
}
/**
* 接收消息的处理
*/
DeliverCallback deliverCallback = (consumerTag, message) -> {
//multiple为false表示接收单条消息
channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
System.out.println("n1");
log.info("consumerTag是:{}",consumerTag);
// 转成String
String messageBody=new String(message.getBody());
// 序列化成对象
UserPo user = objectMapper.readValue(messageBody, UserPo.class);
// 然后接下来就对对象进行操作,我这里就打印一下就好了
log.info("接收到的对象是:{}",user);
};
}
可以看到代码中我把ack改为false了,然后在处理消息的时候添加了如下代码,这里也叫表示为手动签收了,如果想签收多条就把multiple改为ture,然后大家可以把字段代码注释叼,然后在访问,然后去看MQ可视化界面,就会看到消息未被消费,
//multiple为false表示接收单条消息 channel.basicNack(message.getEnvelope().getDeliveryTag(),false);
四.重回对列(不建议)
在手动签收时,添加requeue为true代表重回对列
//multiple为false表示接收单条消息,requeue表示重回对列 channel.basicNack(message.getEnvelope().getDeliveryTag(),false,true);
五.消费端限流机制
实际场景;业务高峰期,有个微服务崩溃了,崩溃期间堆压了大量消息,微服务上线后,突然收到大量并发消息,所以RabbitMQ开发了Qos功能,前提是手动签收
消费端限流机制参数:
如何开启:在channel监听对列前开启,代码如下: 好处就是避免,服务崩溃,重起时,消息堆积在一个消费端处等待消费;这样其他的消费者就没办法分担压力
// 同时只能处理2条消息 channel.basicQos(2);
六:消息过期机制
对列爆满怎么办:
默认情况下,消息进入对列,会永远存在,直到被消费,如果大量消息堆积就会给RabbitMQ产生很大的压力,所以就需要使用到RabbitMQ(TTL)的消息过期时间,防止消息大量积压。
RabbitMQ的过期时间称为TTL,生存时间,过期又分为消息TTL和对列TTL,TTL设置应该长于服务的重启时间
单条消息过期时间:修改代码如下:先设置过期时间,发送对列把特殊参数改为过期时间
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10S").build(); for (int i=0 ; i<10;i++) { // 向交换机发送消息 channel.basicPublish( // 发送到那个交换机 "consumer.exchange", // routingKye是那个 "key.send", true, // 是否有特殊参数 properties, // 消息体 bytes );
设置队列里消息过期时间:声明队列时添加如下代码
// 设置统一队列消息过期时间
HashMap<String, Object> args= new HashMap<>(16);
args.put("x-message-ttl",15000);
channel.queueDeclare(
// 对列名
"key.consumer",
// 数据持久化
true,
// 是否独占吃列
false,
// 不用了是否删除
false,
// 是否需要特殊参数
args
);
也可以设置设置队列的过期时间,一般不用当队列在这段时间没有消息来时M队列会被删除,很严重的
args.put("x-expire",1500)
七.死信队列
消息设置了过期时间,到时间我们就要移入死信队列
什么是死信队列:
对列被配置了DLX属性,当一个消息变成死信后,可以重新被发布到liwai一个Exchange,这个也是一个普通的交换机,然后会进入一个固定的队列
怎么样成为死信:
消息被拒绝,且requeue=false
消息过期
队列达到最大的长度
准备一个新的对列 和交换机并且进行绑定,,对以前的设置了TTL对列,设置死信属性,当过期时间到了,就会重新路由到我们准备的固定处理过期的死信
package rabbitmq.demo.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.rabbitmq.client.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.User;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.HashMap;
import java.util.concurrent.TimeoutException;
/**
* 消费者 监听消息
* @Author sj
* @Date: 2022/04/18/ 11:13
* @Description
*/
@Service
@Slf4j
public class Consumer {
/**
*序列化类
*/
private ObjectMapper objectMapper = new ObjectMapper();
Channel channel;
/**
* 异步线程
* 声明消息对列、交换机、绑定消息的处理
* 监听对列
*/
@Async
public void handleMessage() throws IOException, TimeoutException, InterruptedException {
// 创建工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
// 设置端口
connectionFactory.setHost("localhost");
// 创建于broker的Tcp连接
try(Connection connection = connectionFactory.newConnection()){
// 创建信道
this.channel = connection.createChannel();
// 创建交换机
channel.exchangeDeclare(
// 交换机名字
"exchange.dlx",
// 交换机类型
BuiltinExchangeType.TOPIC,
// 数据是否持久化
true,
// 不用了是否删除
false,
// 是否需要特殊参数
null
);
channel.queueDeclare(
// 对列名
"queue.dlx",
// 数据持久化
true,
// 是否独占吃列
false,
// 不用了是否删除
false,
// 是否需要特殊参数
null
);
// 对列和交换机进行绑定
channel.queueBind(
"exchange.dlx",
"queue.dlx",
"#"
);
// 创建交换机
channel.exchangeDeclare(
// 交换机名字
"consumer.exchange",
// 交换机类型
BuiltinExchangeType.DIRECT,
// 数据是否持久化
true,
// 不用了是否删除
false,
// 是否需要特殊参数
null
);
// 设置统一队列消息过期时间
HashMap<String, Object> args= new HashMap<>(16);
args.put("x-message-ttl",15000);
// 设置死信属性
args.put("x-dead-letter-exchange","exchange.dlx");
channel.queueDeclare(
// 对列名
"key.consumer",
// 数据持久化
true,
// 是否独占吃列
false,
// 不用了是否删除
false,
// 是否需要特殊参数
args
);
// 对列和交换机进行绑定
channel.queueBind(
"key.consumer",
"consumer.exchange",
"key.send"
);
// 同时只能处理2条消息
channel.basicQos(2);
// 监听对列
channel.basicConsume(
// 监听那个对列
"key.consumer",
// 手动签收
false,
// 消息处理
deliverCallback,
consumerTag -> {}
);
while (true){
Thread.sleep(1000000);
}
}
}
/**
* 接收消息的处理
*/
DeliverCallback deliverCallback = (consumerTag, message) -> {
//multiple为false表示接收单条消息,requeue表示重回对列
channel.basicNack(message.getEnvelope().getDeliveryTag(),false,true);
System.out.println("n1");
log.info("consumerTag是:{}",consumerTag);
// 转成String
String messageBody=new String(message.getBody());
// 序列化成对象
UserPo user = objectMapper.readValue(messageBody, UserPo.class);
// 然后接下来就对对象进行操作,我这里就打印一下就好了
log.info("接收到的对象是:{}",user);
};
}
如果有同学启动报错,记得去可视化界面删除对列,然后在启动,其他情况小编就不测试了,有兴趣的朋友可以试试哦!后面会持续更新更多RabbitMQ的使用;