返回顶部

RabbitMQ(二)高级特性

开始前要将第一篇中的准备工作都完成
RabbitMQ(一)安装与入门


前言

通过上图可知消息投递失败将会发生在三个地方,生产者到交换机,交换机到队列,队列到消费者。所以为了保证消息的可靠性,需要开启消息确认机制(confirmCallback、returnCallback)以及消费端手动确认模式(手动ack)或者消费者重试机制。

  • confirm 确认模式
  • return 退回模式

RabbitMQ 整个消息投递的路径为:

producer—>RabbitMQ broker—>exchange—>queue—>consumer

消息从 producer 到 exchange 则会返回一个 confirmCallback 。

消息从 exchange–>queue 投递失败则会返回一个 returnCallback 。

将利用这两个 callback 控制消息的可靠性投递

注:因SpringBoot 整合RabbitMQ 当队列或交换机不存在时,自动创建,所以可靠性检测的一般是服务是否宕机。与消费者是否接收/确认消息无无关


一、消息可靠性投递(生产者端)

1.配置yml文件

spring:
rabbitmq:
host: 127.0.0.1 #ip地址
port: 5672 #端口
virtual-host: / #虚拟主机
username: guest #账号
password: guest #密码
# 开启publisher-confirm(确认模式) 有以下可选值
# simple:同步等待confirm结果,直到超时
# correlated:异步回调,定义ConfirmCallback。mq返回结果时会回调这个ConfirmCallback
# NONE:默认不开启
publisher-confirm-type: correlated
# 开启publish-return(回调模式)功能。可以定义ReturnCallback
# true:调用ReturnCallback
# false:直接丢弃消息
publisher-returns: true

2.编写自定义Callback类

@Component
public class ConfirmAndReturnConfig implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnsCallback {
@Resource
RabbitTemplate rabbitTemplate;
@PostConstruct //@PostConstruct注解:实现Bean初始化之前的操作
public void initMethod() {
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnsCallback(this);
}
/**
* @param correlationData 相关配置消息
* @param ack 表示exchange交换机是否收到了消息,true成功,false失败
* @param cause 失败原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (null != correlationData) {
correlationData.getReturned().getMessage().getMessageProperties().getReceivedDelay();
}
if (ack) {
//接收成功
System.out.println("confirm方法被执行了,消息已经送达Exchange,ack已发");
} else {
//接收失败
System.out.println("confirm方法被执行了,消息送达失败Exchange,原因:" + cause);
}
}
/**
* 回退模式:当消息发送给Exchange后,Exchange路由到Queue失败后才执行returnedMessage
* @param returnedMessage
*/
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
System.out.println("returnedMessage方法被执行了,因为消息送到队列失败");
}
}

3.运行测试

@SpringBootTest(classes = ProducerApplication.class)
@RunWith(SpringRunner.class)
public class ProducerTest {
//1.注入RabbitTemplate
@Resource
private RabbitTemplate rabbitTemplate;
//2.发送消息
@Test
public void testSend(){
/*convertAndSend参数:
交换机名
routingKey可以理解为组名为queue,成员hello
消息
*/
rabbitTemplate.convertAndSend(EXCHANGE_NAME,"queue.hello","这是一个消息。。。。。。");
}
}

3.1 测试结果


二、手动ACK确认机制(消费者者端)

ack指Acknowledge,确认。 表示消费端收到消息后的确认方式。

有三种确认方式:

  • 自动确认acknowledge="none"

    消费者接收消息后立即ack,然后慢慢处理,当消费者重启或出现异常时会丢失消息。

  • 手动确认acknowledge="manual"

    消费者接收消息后,不会立刻告诉RabbitMQ已经收到消息了,而是等待业务处理成功后,通过调用代码的方式手动向MQ确认消息已经收到。当业务处理失败,就可以做一些重试机制,甚至让MQ重新向消费者发送消息都是可以的。

  • 根据异常情况确认acknowledge="auto"

    该方式是通过抛出异常的类型,来做响应的处理(如重发、确认等)。这种方式比较麻烦

1.配置yml文件

spring:
rabbitmq:
host: 127.0.0.1 #ip地址
port: 5672 #端口号
virtual-host: /
username: guest #账号
password: guest #密码
listener:
# 容器类型simple或direct, simple理解为一对一;direct理解为一对多个消费者
simple:
# ACK模式(none自动,auto抛异常,manual手动,默认为auto)
acknowledge-mode: manual
# 开启重试
retry:
# 是否开启重试机制
enabled: true

2.编写消费者监听类

@Slf4j
@Component
public class RabbitMQListener {
private static final int MAX_RETRIES = 3;//消息最大重试次数
private static final long RETRY_INTERVAL = 3;//重试间隔(秒)
/**
* 手动进入死信队列
* RabbitListener中的参数用于表示监听的是哪一个队列
* ACK机制:
* 如果消息消费成功,则调用channel的basicACK()签收
* 如果消息消费失败,则调用channel的basicNack()拒绝签收
*/
@RabbitListener(queues = "topic_queue")//监听的队列名
public void ListenerQueue(Message msg, Channel channel) throws Exception {
//消息的index
long deliveryTag = msg.getMessageProperties().getDeliveryTag();
// 重试次数
int retryCount = 0;
boolean success = false;
// 消费失败并且重试次数<=重试上限次数
while (!success && retryCount < MAX_RETRIES) {
retryCount++;
// 具体业务逻辑
/**
* 模拟业务
* 模拟业务
* 模拟业务
* success = true or false
*/
System.out.println("正在处理业务逻辑");
// 如果失败则重试
if (!success) {
String errorTip = "第" + retryCount + "次消费失败" +
((retryCount < 3) ? "," + RETRY_INTERVAL + "s后重试" : ",进入死信队列");
log.error(errorTip);
Thread.sleep(RETRY_INTERVAL * 1000);
}
}
if (success) {
// 消费成功,确认
channel.basicAck(deliveryTag, false);//第二参数: 是否批量处理.true:将一次性ack所有小于等于deliveryTag的消息
log.info("消息消费成功");
} else {
// requeue:false 手动拒绝,进入抛弃或进入死信队列
channel.basicNack(deliveryTag, false, false);//第二参数:是否批量处理. 第三参数:拒绝后是否重新入队,如果设置为true ,则会添加在队列的末端
log.info("消息消费失败");
}
}
}

3.运行消费者主程序测试结果


三、消费端限流QOS

1.配置yml文件

spring:
rabbitmq:
host: 127.0.0.1 #ip地址
port: 5672 #端口号
virtual-host: /
username: guest #账号
password: guest #密码
listener:
# 容器类型simple或direct simple理解为一对一;direct理解为一对多个消费者
simple:
# ACK模式(none自动,auto抛异常,manual手动,默认为auto)
acknowledge-mode: manual
#每次从队列获取消息数量为1
prefetch: 1
# 开启重试
retry:
# 是否开启重试机制
enabled: true

2.编写测试类

@Slf4j
@Component
public class RabbitMQListener {
@RabbitListener(queues = "topic_queue")//监听的队列名
public void ListenerQueue2(Message msg, Channel channel) throws Exception {
//消息的index
long deliveryTag = msg.getMessageProperties().getDeliveryTag();
System.out.println("正在处理业务");
//为了能看出效果,休眠2秒
Thread.sleep(2000);
//确认
channel.basicAck(deliveryTag,true);
System.out.println(new String(msg.getBody()));
}

2.1 测试结果

基于上面代码,第二次输出时“正在处理业务”和getBody将会同时出现,或进入MQ管理界面http://localhost:15672/,点击导航栏Queues观察Messages列下的Total总消息数,会发现以1为单位递减


四、TTL

TTLTime To Live的缩写,含义为存活时间或者过期时间。即:

  • 当消息到达存活时间后,还没有被消费,会被自动清除。
  • RabbitMQ可以对消息设置过期时间,也可以对整个队列(Queue)设置过期时间。
  • 消息过期后,只有消息在队列顶端,才会判断其是否过期(否则过期消息不会被移除)。
  • 设置队列过期时间使用参数:x-message-ttl,单位:ms(毫秒),会对整个队列消息统一过期。
  • 设置消息过期时间使用参数:expiration。单位:ms(毫秒),当该消息在队列头部时(消费时),会单独判断 这一消息是否过期。
  • 如果两者都进行了设置,以时间短的为准。

1.队列过期时间

在配置队列时使用.ttl(10000)来设定TTL过期时间,单位为毫秒(ms)

@Bean("queue")
public Queue queue(){
return QueueBuilder
.durable(QUEUE_NAME)//durable持久化
.ttl(100000) //ttl过期时间100秒,单位ms
.build();
}

2.消息过期时间

在发送消息时实现new postProcessMessage()方法来设置消息过期时间

@Test
public void testSend3() throws Exception {
/*convertAndSend参数:
交换机名
routingKey可以理解为组名为queue,成员hello
消息
*/
rabbitTemplate.convertAndSend(EXCHANGE_NAME, "queue.hello", "这是一个会过期的消息。。。",
new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setExpiration(String.valueOf(5000));//5秒
return message;
}
});
//ConfirmCallback是异步的,执行之后我们实际上已经关闭了rabbitmq资源 ,所以需要休眠方便测试
Thread.sleep(2000);//2秒
}

五、死信队列

死信队列,英文缩写:DLX 。Dead Letter Exchange(死信交换机),当消息成为Dead message后,可以 被重新发送到另一个交换机,这个交换机就是DLX。

消息成为死信的三种情况(消息无法被消费):

  • 队列消息长度到达限制
  • 消费者拒接消费消息,basicNack/basicReject, 并且不把消息重新放入原目标队列,requeue=false
  • 原队列存在消息过期设置,消息到达超时时间未被消费

1.编写代码

队列绑定死信交换机: 给队列设置:

.deadLetterExchange(死信交换机名称)

.deadLetterRoutingKey(死信交换机routingKey)

  • 死信交换机和死信队列和普通的没有区别

  • 当消息成为死信后,如果该队列绑定了死信交换机,则消息会被死信交换机重新路由到死信队列

@Configuration
public class RabbitMQConfig {
public static final String EXCHANGE_NAME = "topic_exchange";//普通交换机
public static final String QUEUE_NAME = "topic_queue";//普通队列
public static final String QUEUE_DLX = "dlx_queue";//死信队列
public static final String EXCHANGE_DLX = "dlx_exchange";//死信交换机
public static final String DLX_ROUTINGKEY = "dlx.routing";//死信路由key
//1.交换机
@Bean("exchange")
public Exchange exchange(){
return ExchangeBuilder
.topicExchange(EXCHANGE_NAME)
.durable(true) //durable持久化
.build();
}
//死信交换机
@Bean("dlxExchange")
public Exchange dlxExchange(){
return ExchangeBuilder
.topicExchange(EXCHANGE_DLX)
.durable(true) //durable持久化
.build();
}
//2.队列
@Bean("queue")
public Queue queue(){
return QueueBuilder
.durable(QUEUE_NAME)//durable持久化
.ttl(10000) //ttl过期时间,单位ms
.deadLetterExchange(EXCHANGE_DLX)//绑定死信交换机
.deadLetterRoutingKey(DLX_ROUTINGKEY)//绑定死信路由key,因为是队列向死信路由发消息
.maxLength(10)//队列最大消息数量
.build();
}
//死信队列
@Bean("dlxQueue")
public Queue dlxQueue(){
return QueueBuilder
.durable(QUEUE_DLX)//durable持久化
.build();
}
//3.队列和交换机绑定
@Bean
public Binding bindQueueExchange(
@Qualifier("queue") Queue queue,
@Qualifier("exchange") Exchange exchange){
return BindingBuilder
.bind(queue)//绑定队列
.to(exchange)//绑定交换机
.with("queue.*")//routingKey可以理解为组名为queue
.noargs();//不要参数
}
//死信队列和交换机绑定
@Bean
public Binding bindDlxQueueExchange(
@Qualifier("dlxQueue") Queue dlxQueue,
@Qualifier("dlxExchange") Exchange dlxExchange){
return BindingBuilder
.bind(dlxQueue)//绑定队列
.to(dlxExchange)//绑定交换机
.with("dlx.*")//routingKey可以理解为组名为queue
.noargs();//不要参数
}
}

2.测试

这里以超过队列最大消息数来测试

@Test
public void testSend4() throws InterruptedException {
/*convertAndSend参数:
交换机名
routingKey可以理解为组名为queue,成员hello
消息
*/
//20条消息超过了队列设置的最大数量10
for (int i = 0; i < 20; i++) {
rabbitTemplate.convertAndSend(EXCHANGE_NAME, "queue.hello", "这是测试死信的消息");
}
//ConfirmCallback是异步的,执行之后我们实际上已经关闭了rabbitmq资源 ,所以需要休眠方便测试
Thread.sleep(2000);
}

2.1 测试结果


六、延迟队列

延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费。因为RabbitMQ中为提供延迟队列功能,所以我们可以使用TTL + 死信队列的方式来实现延迟队列。

  • 需求

    下单后,X分钟未支付,取消订单,回滚库存。

  • 实现方式:

    TTL + 死信队列

1.编写代码

路由和队列绑定与上面的五、死信交换机相同,只修改监听方法

@RabbitListener(queues = "dlx_queue")//监听的队列名
public void ListenerQueue2(Message msg, Channel channel) throws Exception {
System.out.println("当前时间:" + LocalTime.now());
//消息的index
long deliveryTag = msg.getMessageProperties().getDeliveryTag();
//接收消息内容
System.out.println(new String(msg.getBody()));
//处理业务
System.out.println("正在处理业务...");
System.out.println("判断状态...");
System.out.println("是否取消...");
//为了能看出效果,休眠2秒
//Thread.sleep(2000);
//确认
channel.basicAck(deliveryTag, true);
}

2.测试

@Test
public void testDelaySend4() throws InterruptedException {
rabbitTemplate.convertAndSend(EXCHANGE_NAME, "queue.hello", "这是测试延时队列的消息:" + LocalTime.now());
for (int i = 10; i > 0; i--) {
System.out.println(i+"...");
Thread.sleep(1000);
}
}

2.1 测试结果


七、消息轨迹追踪

使用消息踪迹追钟需要开启Tracing插件

1.开启插件

RabbitMQ默认安装了Tracing插件只要启用即可。

  • 进入MQ安装路径下的sbin目录
  • 打开命令行界面执行:rabbitmq-plugins enable rabbitmq_tracing

2.通过MQ管理页配置消息追踪

打开管理界面:http://localhost:15672/ ,如果没出现Tracing的可以重启一下MQ服务

  • Virtual host:需要追踪的虚拟路径
  • Format:日志文件格式(TEXT/JSON)
  • Max payload bytes:要记录的最大负载大小,以字节为单位。
  • Pattern:#匹配所有的消息,无论是发布还是消费的信息,publish.# 匹配所有发布的消息,deliver.# 匹配所有被消费的消息,#.test 如果test是队列,则匹配已经被消费了的test队列的消息。如果test是exchange,则匹配所有经过该exchange的消息。

配置完成后,点击Add Trace即可完成创建

3.测试

随便发送几条消息后,后回到Tracing界面点击.log文件,这时会要求输入账号密码,只要输入登录时的账号密码即可,如默认: guest/guest


八、应用问题

  • 消息补偿

    Producer:发送消息Q1和发送延迟消息Q3

    Consumer:接收消息Q1并发送确认消息Q2

    定时检查服务:

    第9步中:比对producer的db和消息确认的mdb,调用producer重发db中多的那些数据(即未发送成功或未被消费者成功确认的消息)

    回调检查服务:

    第6步中:监听到确认消息Q2,将消息写入数据库MDB

    第8步中:监听到延迟消息Q3,比对MDB中的消息,出现重复即代表该消息已被消费

  • 消息幂等性保障

    幂等性指一次和多次请求某一个资源,对于资源本身应该具有同样的结果。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。

    在MQ中指,消费多条相同的消息,得到与消费该消息一次相同的结果。

    乐观锁机制


posted @   r1se  阅读(161)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示