SpringBoot教程(十五) | SpringBoot集成RabbitMq(消息丢失、消息重复、消息顺序、消息顺序)

news/2024/9/17 7:18:51/ 标签: spring boot, java-rabbitmq, rabbitmq

SpringBoot教程(十五) | SpringBoot集成RabbitMq(消息丢失、消息重复、消息顺序、消息顺序)

  • RabbitMQ常见问题解决方案
    • 问题一:消息丢失的解决方案
      • (1)生成者丢失消息
        • 丢失的情景
        • 解决方案1:发送方确认机制(推荐,最常用)
        • 解决方案2:事务(不推荐,因为性能差)
      • (2)MQ丢失消息
        • 丢失的情景
        • 解决方案:开启RabbitMQ的持久化+开启镜像队列
      • (3)消费者丢失消息
        • 丢失的情景 1
        • 解决方案:无需解决
        • 丢失的情景 2
        • 扩展:重试机制
        • 解决方案:消费者方确认机制(推荐,最常用)
    • 问题二:消息重复的解决方案
      • 什么时候会重复消费
      • 如何解决
    • 问题三:保证消息顺序的解决方案
      • 单一队列和单一消费者模式(RabbitMQ)
    • 问题四:消息堆积的解决方案
      • 消息堆积原因
      • 预防措施
      • 已出事故的解决措施

RabbitMQ常见问题解决方案

问题一:消息丢失的解决方案

首先明确一条消息的传送流程:生产者->MQ->消费者
所以这三个节点都可能丢失数据

(1)生成者丢失消息

丢失的情景

发送消息过程中出现网络问题:生产者以为发送成功,但RabbitMQ server没有收到

解决方案1:发送方确认机制(推荐,最常用)

发送方确认机制最大的好处在于它是异步的,等信道返回ark确认的同时继续发送下一条消息(不会堵塞其他消息的发送)

(一)修改application.properties配置

# 确认消息已发送到交换机(Exchange)
spring.rabbitmq.publisher-confirms=true #旧版本 
spring.rabbitmq.publisher-confirm-type=correlated #新版本 
# 确认消息已发送到队列(Queue)
spring.rabbitmq.publisher-returns=true

springBoot 2.2.0.RELEASE版本之前 是使用 spring.rabbitmq.publisher-confirms=true
在2.2.0及之后 使用spring.rabbitmq.publisher-confirm-type=correlated 属性配置代替

(二)新建配置文件RabbitTemplate

对于 发送确认 写法有多种方式,以下的是其中的一种方式

import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class RabbitTemplateConfig {@Beanpublic RabbitTemplate createRabbitTemplate(ConnectionFactory connectionFactory){RabbitTemplate rabbitTemplate = new RabbitTemplate();rabbitTemplate.setConnectionFactory(connectionFactory);//setMandatory设置表示:消息在没有被队列接收时是否应该被退回给生产者(true:退回;false:丢弃)。//通常与yml配置文件中的publisher-returns配合一起使用,若不配置该项,setReutrnCallback将不会有消息返回rabbitTemplate.setMandatory(true);//帮助生产者判断 确认消息是否成功发送到RabbitMQ//ack 为true表示已发送成功 false表示发送失败rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {System.out.println("ConfirmCallback:     "+"相关数据:"+correlationData);System.out.println("ConfirmCallback:     "+"确认情况:"+ack);System.out.println("ConfirmCallback:     "+"原因:"+cause);});//当消息无法 放到队列里面时 返回的提醒rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {System.out.println("ReturnCallback:     "+"消息:"+message);System.out.println("ReturnCallback:     "+"回应码:"+replyCode);System.out.println("ReturnCallback:     "+"回应信息:"+replyText);System.out.println("ReturnCallback:     "+"交换机:"+exchange);System.out.println("ReturnCallback:     "+"路由键:"+routingKey);});return rabbitTemplate;}
}
解决方案2:事务(不推荐,因为性能差)

RabbitMQ提供的事务功能,在生产者发送数据之前开启RabbitMQ事务

(2)MQ丢失消息

丢失的情景

RabbitMQ服务端接收到消息后由于服务器宕机或重启等原因(消息默认存在内存中)导致消息丢失;

解决方案:开启RabbitMQ的持久化+开启镜像队列

RabbitMQ的持久化分为三个部分:交换器的持久化、队列的持久化、消息的持久化
三者 都 持久化 才能保证 RabbitMQ服务重启之后,消息才能存在且能发出去

交换机持久化
交换机持久化描述的是当这个交换机上没有注册队列时,这个交换机是否删除。
如果要打开持久化的话也很简单 (上面列子都是有体现的)

//定义直接交换机
@Bean
public DirectExchange directExchange() {//第一个参数:定义交换机的名称,第二个参数:是否持久化,第三个参数:是否自动删除return new DirectExchange("directExchange", true, false);
}

队列持久化
队列持久化描述的是当这个队列没有消费者在监听时,是否进行删除。
持久化做法:

//定义队列
@Bean
public Queue directQueue() {//第一个参数:队列的名称,第二个参数:是否持久化return new Queue("directQueue", true);
}

消息持久化

关键配置 持久化(MessageDeliveryMode.PERSISTENT)

@Test
public void testDurableMessage() {// 1.准备消息Message message = MessageBuilder.withBody("hello, rabbitmq".getBytes(StandardCharsets.UTF_8)).setDeliveryMode(MessageDeliveryMode.PERSISTENT).build();// 2.消息ID,封装到CorrelationData中CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());// 3.发送消息rabbitTemplate.convertAndSend("simple.queue", message, correlationData);log.info("发送消息成功");
}

(3)消费者丢失消息

丢失的情景 1

RabbitMQ服务端向消费者发送完消息之后,网络断了,消息并没有到达消费者

解决方案:无需解决

无需解决。因为此情景下服务端收不到确认消息,会再次发送的。

丢失的情景 2

启用了重试机制,重试指定次数之后,还没成功,但消息被确认。

扩展:重试机制

重试机制的三大前提

  1. 重试模式已启用:通过配置 spring.rabbitmq.listener.simple.retry.enabled=true 来启用重试模式。
  2. 抛出了异常:在 @RabbitListener 标注的方法中抛出了异常,通常是 RuntimeException 或 Error。
    Spring AMQP 会捕获这些异常并根据配置的重试策略来重试消息。
  3. 未达到最大重试次数:消息的重试次数尚未达到配置的最大值(spring.rabbitmq.listener.simple.retry.maxAttempts)。

配置以下即可实现重试操作

# 是否支持重试
spring.rabbitmq.listener.simple.retry.enabled=true
# 重试次数(默认3次)
spring.rabbitmq.listener.simple.retry.max-attempts=5
解决方案:消费者方确认机制(推荐,最常用)

改成手动后就 可以实现 “先操作业务逻辑(数据库操作)后,再手动从队列上删除这个消息” 的动作
其中“从队列上删除这个消息“这个动作体现就是 使用 channel.basicAck 去完成的。
切记改成手动后,这个channel.basicAck方法一定要写。

(一)修改application.properties配置

# 设置消费端手动 ack
spring.rabbitmq.listener.simple.acknowledge-mode=manual

(二)修改Service接收信息项

当消息在进入 emailProcess、smsProcess(被@RabbitListener注解) 方法时就已经被视为“接收到了”,但是需要 你 执行 channel.basicAck(手动确认)才能让这个消息从队列上删除。

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;import org.springframework.stereotype.Service;import java.io.IOException;@Service
public class DirectReceiver {@RabbitHandler@RabbitListener(queues = "emailQueue")  //监听的队列名称public void emailProcess(Channel channel, Message message) throws IOException {try{System.out.println(new String(message.getBody(),"UTF-8"));//TODO 具体业务.......//你使用手动消息确认模式时,basicAck 一定要执行,不然会导致会保留在队列中,无法被消费//第1个参数表示消息投递序号//第2个参数false只确认当前一个消息收到(大多数情况下都设置为false),true确认所有consumer获得的消息channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);} catch (Exception e) {//若是消息没有成功接收,第二个参数设置为true的话,代表重新放回队列中,false则为丢弃,在此也可以做成放置死信队列的操作channel.basicReject(message.getMessageProperties().getDeliveryTag(),false);}}}

确认和拒绝消息:

  • basicAck: 这个方法用于确认消息已被成功处理。
    第一个参数是消息的delivery tag(用于标识消息),
    第二个参数指定是否批量确认(false表示只确认当前消息)。
  • basicReject: 这个方法用于拒绝消息。
    第一个参数同样是delivery tag,
    第二个参数指定是否将消息重新放回队列(false表示不重新放回,即丢弃消息)。

方法解释:

  • emailProcess: 这个方法监听emailQueue队列。
    当队列中有消息时,它会打印出消息的内容,并尝试确认消息。
    如果处理过程中发生异常,它会拒绝消息,但不会重新放回队列(第二个参数为false)。

问题二:消息重复的解决方案

什么时候会重复消费

1.自动提交模式时

消费者收到消息后,要自动提交,但提交后,网络出故障,RabbitMQ服务器没收到提交消息,那么此消息会被重新放入队列,会再次发给消费者。

2.手动提交模式时

情景1:网络故障问题,同上。
情景2:接收到消息并处理结束了,此时消费者挂了,没有手动提交消息。

总体来说就是:网络不可达消费端宕机

如何解决

消费端处理消息的业务逻辑保持幂等性

比如你拿个数据要写库,先根据主键查一下,如果这数据有了,就别插入了,update 一下。
比如你是写 Redis,那没问题了,反正每次都是 set,天然幂等性。

问题三:保证消息顺序的解决方案

单一队列和单一消费者模式(RabbitMQ)

在这里插入图片描述

在RabbitMQ中,可以确保一个队列只被一个消费者消费,这样可以保证消息按照发送的顺序被处理。
因为队列本身就是一个先进先出的结构。

适用场景:RabbitMQ用户且对消息顺序有严格要求的场景。
优点:实现简单,易于管理。
缺点:可能成为性能瓶颈,在处理大量消息时需要考虑扩展性。

问题四:消息堆积的解决方案

消息堆积原因

消息堆积即消息没及时被消费,是生产者生产消息速度快于消费者消费的速度导致的。
消费者消费慢可能是因为:本身逻辑耗费时间较长、阻塞了。

预防措施

生产者
1.减少发布频率
3.考虑使用队列最大长度限制
消费者
1.优化代码

已出事故的解决措施

情况1:堆积的消息还需要使用

方案1:简单修复

修复 消费者(consumer)的问题,让他恢复消费速度,然后等待几个小时消费完毕

方案2:复杂修复

单队列消费转变为多队列并行消费

也是需要先 修复 消费者(consumer)的问题,再进行下面的步骤

步骤 1: 队列和路由设置
1.创建新队列:在RabbitMQ中创建10个新队列,每个队列分配一个独特的名称。
2. 设置交换机:定义一个直连型(Direct)交换机。
3. 绑定路由键:将每个新队列通过唯一的路由键绑定到直连型交换机上。

伪代码例子:

// 假设这是配置类的一部分  
@Bean  
Queue queue1() {  return new Queue("queue1", false);  
}  
@Bean  
Queue queue2() { return new Queue("queue2", false);  
}
// 以此类推,为其他9个队列创建Bean  
.........
@Bean  
DirectExchange exchange() {  return new DirectExchange("myExchange");  
}  
@Bean  
Binding binding1(Queue queue1, DirectExchange exchange) {  return BindingBuilder.bind(queue1).to(exchange).with("routingKey1");  
}  
@Bean  
Binding binding2(Queue queue2, DirectExchange exchange) { return BindingBuilder.bind(queue2).to(exchange).with("routingKey2");  
}
// 以此类推,为其他队列和路由键创建绑定  
......

步骤 2: 消息分发
1.接收堆积数据:现有消费者(或分发者)接收从发送者处堆积的数据。
2.分发到新队列:实现分发逻辑,将接收到的消息根据路由键分发到相应的10个新队列中。

伪代码例子:

@RabbitListener(queues = "oldQueue")  
public void emailProcess(Message message, Channel channel) throws IOException {  try {  // 生成1-10之间的顺序数  SequentialRandom sequentialRandom = new SequentialRandom()String key = sequentialRandom.getNextSequentialRandom();// 重新发送消息到交换机,交换机将根据routingKey将消息路由到正确的队列  rabbitTemplate.convertAndSend("myExchange", "routingKey"+key, new String(message.getBody(),"UTF-8")); // 确认原始队列中的消息(如果您想要的话)  channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);  } catch (Exception e) {  // 处理异常,可能包括记录日志、发送警报等  channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);  }  
}  
public class SequentialRandom {  private int currentIndex = 1; // 初始索引为1  /**  * 获取下一个顺序数* @return 下一个数字,从1到10循环  */  public int getNextSequentialRandom() {  int next = currentIndex;  currentIndex = (currentIndex % 10) + 1; // 使用模运算实现循环,并更新索引  return next;  }  
}

步骤 3: 并行消费
1.开发新消费端:编写新的消费端程序,该程序能够监并处理来自10个新队列的消息。
2. 部署并启动:将新消费端程序部署到服务器,并启动它以开始并行消费。

伪代码例子:

@Component  
public class ParallelConsumer {  @RabbitListener(queues = {"queue1"})  public void receiveMessage1(Message message) {  // 处理消息  }  @RabbitListener(queues = {"queue2"})  public void receiveMessage2(Message message) {  // 处理消息  }  // ... @RabbitListener(queues = {"queue10"})  public void receiveMessage3(Message message) {  // 处理消息  }  
}

情况2:堆积的消息不需要使用

删除消息即可。(可以在RabbitMQ控制台删除,或者使用命令)。


http://www.ppmy.cn/news/1522692.html

相关文章

点赞收藏功能该如何设计?

这周给一个小伙伴做模拟面试,因为他在公司的项目是一个短视频电商的项目,模仿的是微博。看到他简历里写了做了短视频的收藏功能,于是让他讲讲具体的做法是什么样子的。 结果回答的并不理想,答案里有不少硬伤,今天松哥…

【Python】CSV文件的简单使用

1.读取CSV文件 import csvpath "123.csv"with open(path) as f: # 打开csv文件csvReader csv.reader(f) # 读文件建立Reader对象listReader list(csvReader) # 将数据转换成列表print(listReader)2.写入CSV文件 import csvpath "123.csv"with ope…

uniapp小程序下载缓存服务器上的图片

1. 使用uni.downloadFile,但是注意下载图片的地址里的域名,需要在微信公众平台里面的downloadFile合法域名进行配置。 export default function downloadAndCacheImage(imageUrl, name) {return new Promise((resolve, reject) > {console.log("imageUrl&q…

【消息中间件】Kafka从入门到精通

1 Kafka入门 概念 架构 1.1 概述 1.1.1 初始Kafka Kafka是一个由Scala和Java语言开发的,经典高吞吐量的分布式消息发布和订阅系统,也是大数据技术领域中用作数据交换的核心组件之一。以高吞吐,低延迟,高伸缩,高可靠…

DDD的实现流程

领域驱动设计(DDD)是一种面向领域的软件设计方法。它强调将业务知识和业务逻辑集成到软件设计中,从而提高软件系统的可维护性、可扩展性和可测试性。DDD 的实现流程主要包括以下几个步骤: 确定业务领域 首先,需要明确…

有temp表包含A,B两列,使用SQL,对B列进行处理,形成C列,按A列顺序,B列值不变,则C列累计技术,B列值变化,则C列重新开始计数

有temp表,使用SQL,对B列进行处理,形成C列,按A列顺序,B列值不变,则C列累计技术,B列值变化,则C列重新开始计数 建表语句如下 CREATE TABLE temp(A STRING ,B STRING );INSERT INTO …

C语言 - 预处理详解(一)#预定义符号 ##define #undef

文章目录 前言 一、预定义符号 二、#define (一)、#define 定义的标识符 (二)、#define 定义的宏 (三)、#define 替换规则 (四)、# 和 ## 1、 # 的作用 2、## 的作用 (五)、带副作用的宏参数 (六)、宏和函数的对比 (七)、命名约定 三、#undef 总结 前言 路漫漫其修远兮&#…

【双指针】N数之和

N数之和 两数之和题目题目解析暴力思路双指针优化 三数之和题目题目解析暴力思路双指针优化 四数之和题目题目解析暴力思路双指针优化 两数之和 题目 题目链接: 查找总价格为目标值的两个商品 虽然题目名字不是两数之和, 但是由于和后面的三数之和, 四数之和是连起来的, 于是…

【持续更新】Adobe Audition 2024 (v24.4.1.003)最新免费修改版

Adobe Audition是一款专为录音、编辑和掌握音频素材设计的专业解决方案。此编辑器支持从MP3、AAC到AIFF等多种重要格式,并能从CD中导入音轨。 其多轨编辑功能使您可以在任意数量的轨道上混合音乐、语音和声音片段,运用丰富的工作室动态效果,如…

nginx配置中的服务器名称

通常,在nginx的配置节中: server {listen 80;server_name example.org www.example.org;... } server_name(服务器名称) 指令定义确定哪个服务器块用于给定请求。可以使用确切名称、通配符名称、ip地址或正则表达式来定义它们: se…

如何在 AWS S3 中设置跨区域复制

如何在 AWS S3 中设置跨区域复制 概述 欢迎来到雲闪世界。 Amazon Simple Storage Service (S3) 是一种可扩展的对象存储服务,广泛用于存储和检索数据。其主要功能之一是跨区域复制 (CRR),允许跨不同的 AWS 区域自动异步复制对象。此功能对于灾难恢复、…

二手手机回收小程序搭建,小程序功能特点

随着社会生活水平的提高,对手机的更新换代的速度也在逐渐加快,出现了大量的闲置手机,而这也给手机回收市场带来了巨大的发展空间! 目前,手机回收市场进入到了发展快速期,吸引了越来越多的企业加入大市场中…

4.7 Sensors -- useScroll

4.7 Sensors – useScroll https://vueuse.org/core/useScroll/ 作用 响应式的监听滚动位置和状态。 官方示例 <script setup lang"ts"> import { useScroll } from vueuse/coreconst el ref<HTMLElement | null>(null) const { x, y, isScrolling…

Spring常用中间件

1. 数据库中间件 &#xff08;1&#xff09;MySQL: 常用的关系型数据库&#xff0c;支持JDBC和JPA。 &#xff08;2&#xff09;PostgreSQL: 功能强大的开源关系型数据库&#xff0c;支持复杂查询。 &#xff08;3&#xff09;MongoDB: NoSQL数据库&#xff0c;适合存储非结构化…

【Rust练习】13.数组

练习题来自&#xff1a;https://practice-zh.course.rs/compound-types/array.html 1 fn main() {// 使用合适的类型填空let arr: __ [1, 2, 3, 4, 5];// 修改以下代码&#xff0c;让它顺利运行assert!(arr.len() 4); }显然这个数组的长度是5. fn main() {// 使用合适的类…

ELK学习笔记(三)——使用Filebeat8.15.0收集日志

使用Filebeat收集日志 前面教程已经把ElasticSearch和Kibana部署完毕&#xff0c;接着我们就要使用filebeat去收集我们的java服务日志&#xff0c;这里首先介绍一下ELK和EFK的区别。 一、ELK和EFK的区别 在收集和处理日志时&#xff0c;使用 ELK&#xff08;Elasticsearch, …

8. GIS数据分析师岗位职责、技术要求和常见面试题

本系列文章目录&#xff1a; 1. GIS开发工程师岗位职责、技术要求和常见面试题 2. GIS数据工程师岗位职责、技术要求和常见面试题 3. GIS后端工程师岗位职责、技术要求和常见面试题 4. GIS前端工程师岗位职责、技术要求和常见面试题 5. GIS工程师岗位职责、技术要求和常见面试…

tabBar设置底部菜单选项以及iconfont图标

tabBartabBar属性:设置底部 tab 的表现 ​ ​ ​ ​ 首先在pages.json页面写一个tabBar对象,里面放入list对象数组,里面至少要有2个、最多5个 tab, 如果只有一个tab的话,H5(浏览器)依然可以显示底部有一个导航栏,如果没有,需要重启后才有,小程序则报错,只有2个以上才可以…

C# 窗口页面布局

1.Groupbox 单机鼠标右键&#xff0c;置于底层 2.Label 在右方属性中修改名称 3.ComboBox 点击属性中的集合&#xff0c;可以添加选择项 4.CheckBox 在属性中修改名称 5.RichTextBox 富文本 在属性中修改名称与区域 6.StatusStrip 状态栏 将AutoSize改成false就可以修改…

基于Java的宿舍报修管理系统的设计与实现(论文+源码)_kaic

基于Java的宿舍报修管理系统的设计与实现(论文源码)_kaic 摘  要 随着教育改革‎‏的不断‎‏深入&#xff0c;‎‏学校宿‎‏舍的管‎‏理体系‎‏也在不‎‏断地完‎‏善&#xff0c;校园后勤服务是学校管理的重要工作&#xff0c;学校提供优秀的后勤服务&#xff0c;能提…