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
TTL
即Time 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中指,消费多条相同的消息,得到与消费该消息一次相同的结果。
乐观锁机制
---End---
永远积极向上!!!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!