尚品汇-MQ模块搭建测试、消息不丢失(重)(四十三)

embedded/2024/11/8 18:24:39/

目录:

(1)消息不丢失

(2)消息确认

(3)消息确认业务封装

 (4)封装发送端消息确认

(5)封装消息发送

(6)发送确认消息测试

(7)消息发送失败,设置重发机制

(1)消息不丢失

消息的不丢失,在MQ角度考虑,一般有三种途径:

1,生产者不丢数据

2,MQ服务器不丢数据

3,消费者不丢数据

保证消息不丢失有两种实现方式:

  1. 开启事务模式
  2. 消息确认模式

说明:开启事务会大幅降低消息发送及接收效率,使用的相对较少,因此我们生产环境一般都采取消息确认模式,以下我们只是讲解消息确认模式

(2)消息确认

消息持久化

如果希望RabbitMQ重启之后消息不丢失,那么需要对以下3种实体均配置持久化

Exchange

声明exchange时设置持久化(durable = true)并且不自动删除(autoDelete = false)

Queue

声明queue时设置持久化(durable = true)并且不自动删除(autoDelete = false)

message

发送消息时通过设置deliveryMode=2持久化消息

处理消息队列丢数据的情况,一般是开启持久化磁盘的配置。这个持久化配置可以和confirm机制配合使用,你可以在消息持久化磁盘后,再给生产者发送一个Ack信号。这样,如果消息持久化磁盘之前,rabbitMQ阵亡了,那么生产者收不到Ack信号,生产者会自动重发。那么如何持久化呢,其实也很容易,就下面两步:

1、将queue的持久化标识durable设置为true,则代表是一个持久的队列

2、发送消息的时候将deliveryMode=2

这样设置以后,rabbitMQ就算挂了,重启后也能恢复数据

发送确认

有时,业务处理成功,消息也发了,但是我们并不知道消息是否成功到达了rabbitmq,如果由于网络等原因导致业务成功而消息发送失败,那么发送方将出现不一致的问题,此时可以使用rabbitmq的发送确认功能,即要求rabbitmq显式告知我们消息是否已成功发送。

手动消费确认

有时,消息被正确投递到消费方,但是消费方处理失败,那么便会出现消费方的不一致问题。比如:订单已创建的消息发送到用户积分子系统中用于增加用户积分,但是积分消费方处理却都失败了,用户就会问:我购买了东西为什么积分并没有增加呢?

要解决这个问题,需要引入消费方确认,即只有消息被成功处理之后才告知rabbitmq以ack,否则告知rabbitmq以nack

(3)消息确认业务封装

搭建rabbit-util模块

由于消息队列是公共模块,我们把mq的相关业务封装到该模块,其他service微服务模块都可能使用,因此我们把他封装到一个单独的模块,需要使用mq的模块直接引用该模块即可

搭建方式如:common-util,导入常量类 MqConst

package com.atguigu.gmall.constant;public class MqConst {/*** 消息补偿*/public static final String MQ_KEY_PREFIX = "mq:list";public static final int RETRY_COUNT = 3;/*** 商品上下架*/public static final String EXCHANGE_DIRECT_GOODS = "exchange.direct.goods";public static final String ROUTING_GOODS_UPPER = "goods.upper";public static final String ROUTING_GOODS_LOWER = "goods.lower";//队列public static final String QUEUE_GOODS_UPPER  = "queue.goods.upper";public static final String QUEUE_GOODS_LOWER  = "queue.goods.lower";/*** 取消订单,发送延迟队列*/public static final String EXCHANGE_DIRECT_ORDER_CANCEL = "exchange.direct.order.cancel";//"exchange.direct.order.create" test_exchange;public static final String ROUTING_ORDER_CANCEL = "order.create";//延迟取消订单队列public static final String QUEUE_ORDER_CANCEL  = "queue.order.cancel";//取消订单 延迟时间 单位:秒 真实业务public static final int DELAY_TIME  = 24*60*60;//  测试取消订单// public static final int DELAY_TIME  = 3;/*** 订单支付*/public static final String EXCHANGE_DIRECT_PAYMENT_PAY = "exchange.direct.payment.pay";public static final String ROUTING_PAYMENT_PAY = "payment.pay";//队列public static final String QUEUE_PAYMENT_PAY  = "queue.payment.pay";/*** 减库存*/public static final String EXCHANGE_DIRECT_WARE_STOCK = "exchange.direct.ware.stock";public static final String ROUTING_WARE_STOCK = "ware.stock";//队列public static final String QUEUE_WARE_STOCK  = "queue.ware.stock";/*** 减库存成功,更新订单状态*/public static final String EXCHANGE_DIRECT_WARE_ORDER = "exchange.direct.ware.order";public static final String ROUTING_WARE_ORDER = "ware.order";//队列public static final String QUEUE_WARE_ORDER  = "queue.ware.order";/*** 关闭交易*/public static final String EXCHANGE_DIRECT_PAYMENT_CLOSE = "exchange.direct.payment.close";public static final String ROUTING_PAYMENT_CLOSE = "payment.close";//队列public static final String QUEUE_PAYMENT_CLOSE  = "queue.payment.close";/*** 定时任务*/public static final String EXCHANGE_DIRECT_TASK = "exchange.direct.task";public static final String ROUTING_TASK_1 = "seckill.task.1";//队列public static final String QUEUE_TASK_1  = "queue.task.1";/*** 秒杀*/public static final String EXCHANGE_DIRECT_SECKILL_USER = "exchange.direct.seckill.user";public static final String ROUTING_SECKILL_USER = "seckill.user";//队列public static final String QUEUE_SECKILL_USER  = "queue.seckill.user";/*** 定时任务*/public static final String ROUTING_TASK_18 = "seckill.task.18";//队列public static final String QUEUE_TASK_18  = "queue.task.18";}

pom.xml

 

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>common</artifactId><groupId>com.atguigu.gmall</groupId><version>1.0</version></parent><modelVersion>4.0.0</modelVersion><artifactId>rabbit-util</artifactId><dependencies><!--rabbitmq消息队列--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency><!--rabbitmq 协议--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-bus-amqp</artifactId></dependency></dependencies></project>

service-mq引入rabbit-util模块依赖

<!--rabbitmq消息--><dependency><groupId>com.atguigu.gmall</groupId><artifactId>rabbit-util</artifactId><version>1.0</version></dependency>

 (4)封装发送端消息确认

在rabbit-util 中添加类

package com.atguigu.gmall.common.config;/*** @Description 消息发送确认* <p>* ConfirmCallback  只确认消息是否正确到达 Exchange 中* ReturnCallback   消息没有正确到达队列时触发回调,如果正确到达队列不执行* <p>* 1. 如果消息没有到exchange,则confirm回调,ack=false* 2. 如果消息到达exchange,则confirm回调,ack=true* 3. exchange到queue成功,则不回调return* 4. exchange到queue失败,则回调return* */
@Component
@Slf4j
public class MQProducerAckConfig implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {@Autowiredprivate RabbitTemplate rabbitTemplate;// 修饰一个非静态的void()方法,在服务器加载Servlet的时候运行,并且只会被服务器执行一次在构造函数之后执行,init()方法之前执行。@PostConstructpublic void init() {rabbitTemplate.setConfirmCallback(this);            //指定 ConfirmCallbackrabbitTemplate.setReturnCallback(this);             //指定 ReturnCallback}@Overridepublic void confirm(CorrelationData correlationData, boolean ack, String cause) {if (ack) {log.info("消息发送成功:" + JSON.toJSONString(correlationData));} else {log.info("消息发送失败:" + cause + " 数据:" + JSON.toJSONString(correlationData));}}@Overridepublic void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {// 反序列化对象输出System.out.println("消息主体: " + new String(message.getBody()));System.out.println("应答码: " + replyCode);System.out.println("描述:" + replyText);System.out.println("消息使用的交换器 exchange : " + exchange);System.out.println("消息使用的路由键 routing : " + routingKey);}}

(5)封装消息发送

在rabbit-util 中添加类RabbitService

package com.atguigu.gmall.common.service;@Service
public class RabbitService {@Autowiredprivate RabbitTemplate rabbitTemplate;/***  发送消息* @param exchange 交换机* @param routingKey 路由键* @param message 消息*/public boolean sendMessage(String exchange, String routingKey, Object message) {rabbitTemplate.convertAndSend(exchange, routingKey, message);return true;}}

(6)发送确认消息测试

在service-mq编写测试代码

消息发送端

package com.atguigu.gmall.mq.controller;@RestController
@RequestMapping("/mq")
public class MqController {@Autowiredprivate RabbitService rabbitService;/*** 消息发送*///http://localhost:8282/mq/sendConfirm@GetMapping("sendConfirm")public Result sendConfirm() {rabbitService.sendMessage("exchange.confirm", "routing.confirm", "来人了,开始接客吧!");return Result.ok();}
}

消息接收端

在service-mq 中编写

这里交换机队列绑定:有两种方式一种是配置,一种是注解

我们这里使用注解进行创建绑定

@SneakyThrows 是Lombk的异常 处理注解

package com.atguigu.gmall.mq.receiver;@Component
public class ConfirmReceiver {@SneakyThrows
@RabbitListener(bindings=@QueueBinding(value = @Queue(value = "queue.confirm",autoDelete = "false"),exchange = @Exchange(value = "exchange.confirm",autoDelete = "false"),key = {"routing.confirm"}))
public void process(Message message, Channel channel){System.out.println("RabbitListener:"+new String(message.getBody()));// 参数一:消息的唯一标识,参数二:是否批量确认 false 确认一个消息,true 批量确认
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);}
}

测试:http://localhost:8282/mq/sendConfirm

 加入了确认机制后,成功后:会有提示

把交换机写错,发送到交换机会失败

 

 

把路由key写错,交换机到队列消息失败:


 

(7)消息发送失败,设置重发机制

实现思路:借助redis来实现重发机制

在rabbit-util 模块中添加依赖

<!-- redis -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency><!-- spring2.X集成redis所需common-pool2-->
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId>
</dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId>
</dependency>

自定义一个实体类来接收消息

@Data
public class GmallCorrelationData extends CorrelationData {//  消息主体private Object message;//  交换机private String exchange;//  路由键private String routingKey;//  重试次数private int retryCount = 0;//  消息类型  是否是延迟消息private boolean isDelay = false;//  延迟时间private int delayTime = 10;
}

 

修改发送方法

//  封装一个发送消息的方法
public Boolean sendMsg(String exchange,String routingKey, Object msg){//  将发送的消息 赋值到 自定义的实体类GmallCorrelationData gmallCorrelationData = new GmallCorrelationData();//  声明一个correlationId的变量String correlationId = UUID.randomUUID().toString().replaceAll("-","");gmallCorrelationData.setId(correlationId);gmallCorrelationData.setExchange(exchange);gmallCorrelationData.setRoutingKey(routingKey);gmallCorrelationData.setMessage(msg);//  发送消息的时候,将这个gmallCorrelationData 对象放入缓存。redisTemplate.opsForValue().set(correlationId, JSON.toJSONString(gmallCorrelationData),10, TimeUnit.MINUTES);//  调用发送消息方法
//this.rabbitTemplate.convertAndSend(exchange,routingKey,msg);this.rabbitTemplate.convertAndSend(exchange,routingKey,msg,gmallCorrelationData);//  默认返回truereturn true;
}

发送失败调用重发方法  MQProducerAckConfig 配置类中修改 

@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {//  ack = true 说明消息正确发送到了交换机if (ack){System.out.println("哥们你来了.");log.info("消息发送到了交换机");}else {//  消息没有到交换机log.info("消息没发送到交换机");//  调用重试发送方法this.retrySendMsg(correlationData);}
}@Override
public void returnedMessage(Message message, int code, String codeText, String exchange, String routingKey) {System.out.println("消息主体: " + new String(message.getBody()));System.out.println("应答码: " + code);System.out.println("描述:" + codeText);System.out.println("消息使用的交换器 exchange : " + exchange);System.out.println("消息使用的路由键 routing : " + routingKey);//  获取这个CorrelationData对象的Id  spring_returned_message_correlationString correlationDataId = (String) message.getMessageProperties().getHeaders().get("spring_returned_message_correlation");//  因为在发送消息的时候,已经将数据存储到缓存,通过 correlationDataId 来获取缓存的数据String strJson = (String) this.redisTemplate.opsForValue().get(correlationDataId);//  消息没有到队列的时候,则会调用重试发送方法GmallCorrelationData gmallCorrelationData = JSON.parseObject(strJson,GmallCorrelationData.class);//  调用方法  gmallCorrelationData 这对象中,至少的有,交换机,路由键,消息等内容.this.retrySendMsg(gmallCorrelationData);
}/*** 重试发送方法* @param correlationData   父类对象  它下面还有个子类对象 GmallCorrelationData*/
private void retrySendMsg(CorrelationData correlationData) {//  数据类型转换  统一转换为子类处理GmallCorrelationData gmallCorrelationData = (GmallCorrelationData) correlationData;//  获取到重试次数 初始值 0int retryCount = gmallCorrelationData.getRetryCount();//  判断if (retryCount>=3){//  不需要重试了log.error("重试次数已到,发送消息失败:"+JSON.toJSONString(gmallCorrelationData));} else {//  变量更新retryCount+=1;//  重新赋值重试次数 第一次重试 0->1 1->2 2->3gmallCorrelationData.setRetryCount(retryCount);System.out.println("重试次数:\t"+retryCount);//  更新缓存中的数据this.redisTemplate.opsForValue().set(gmallCorrelationData.getId(),JSON.toJSONString(gmallCorrelationData),10, TimeUnit.MINUTES);//  调用发送消息方法 表示发送普通消息  发送消息的时候,不能调用 new RabbitService().sendMsg() 这个方法this.rabbitTemplate.convertAndSend(gmallCorrelationData.getExchange(),gmallCorrelationData.getRoutingKey(),gmallCorrelationData.getMessage(),gmallCorrelationData);}
}

把路由写错,交换机到队列消息失败:

  把交换机写错:发送到交换机失败

 

总结

为了防止消息不丢失 ,从以下三方面考虑:

1,生产者不丢数据

2,MQ服务器不丢数据

3,消费者不丢数据

   

生产者:

MQ服务器:

RabbitMQ重启之后消息不丢失

消息队列丢数据的情况,一般是开启持久化磁盘的配置。这个持久化配置可以和confirm机制配合使用,你可以在消息持久化磁盘后,再给生产者发送一个Ack信号

开启确认回调(成功到达交换机发送ack标识,没有达到重发)、失败回调(设置重发机制)

为了防止消费者处理消息丢失,我们开启消费者消息消费确认回调,消息被成功处理之后才告知rabbitmq以ack,否则告知rabbitmq以nack


http://www.ppmy.cn/embedded/104348.html

相关文章

游戏引擎详解——图片

图片 图片的格式 图片文件格式pngjpg 纹理压缩格式ETC1/2PVRTCASTC 图片的属性 图片属性解释分辨率宽高像素值&#xff08;pt&#xff09;&#xff0c;如&#xff1a;1024*1024位深度用来存储像素颜色的值&#xff0c;如RGBA8888&#xff0c;红黄蓝透明度4个维度每个8bit&…

2024HarmonyOS应用开发者高级认证最新整理题库和答案(已收录182道 )

更新截止2024-08-27,完整题库一共182道题,足够覆盖90%考题,如有新题和遗漏我会持续补充 所有题目的选项都是打乱顺序的,记答案不要记序号 完整题库请在我的网盘下载或查看在线文档 完整题库在线文档预览 单选(已收录102道) 1 . 以下哪个装饰器用来表示并发共享对象。(B) A. @…

2024年6月 青少年等级考试机器人实操真题二级

202406 青少年等级考试机器人实操真题二级 第 1 题 请查看第二大题“实际操作”再进行本题作答 下列能增加模型稳定性的是&#xff1f;&#xff08; &#xff09; A&#xff1a;增大与地面的接触面积 B&#xff1a;减小与地面的接触面积 C&#xff1a;增高模型重心 D&a…

瑞芯微RK3588 camera驱动总结三调试

一、指令 先介绍一些很有用的指令 1.media-ctl -p -d /dev/media0 用于打印 /dev/media0 设备的媒体管道拓扑结构。媒体管道拓扑结构描述了多媒体设备内部各组件之间的连接关系&#xff0c;包括视频输入、视频输出、编码器、解码器、ISP&#xff08;图像信号处理器&#xff…

检查内参 外参

目录 检查内参 外参 像素点投影到世界坐标系&#xff0c;再投回到2d坐标系&#xff1a; 检查内参 外参 import cv2 import numpy as np# 假设我们有以下相机内参 K np.array([[418.96369417, 0.0, 489.16315478],[0.0, 419.04813353, 267.88796254],[0.0, 0.0, 1.0]], dtyp…

Selenium自动化测试(IDE环境搭建)

一、Selenium概念 1.1 Selenium介绍 Selenium是一款开源免费的Web应用程序功能自动化测试工具Selenium还支持基于Web的管理任务自动化 1.2 Selenium的主要用途 1、Web应用程序的功能自动化测试 对Web系统进行功能测试&#xff0c;版本迭代时避免重复劳动 2、兼容性测试 测试…

docker实战基础二(Docker基础命令)

一、Docker 镜像的标签 更改 Docker 镜像的标签(tag)可以通过 docker tag 命令来实现。这个命令允许你为现有的镜像创建一个新的标签。这对于组织和管理镜像非常有用,特别是在开发和部署工作流中。 命令语法 docker tag SOURCE_IMAGE[:TAG] TARGET_IMAGE[:TAG]SOURCE_IMA…

ParallelsDesktop19可在任何Mac上运行Windows软件

ParallelsDesktop19是一款Mac虚拟机软件&#xff0c;可在任何Mac上运行Windows&#xff0c;体验不同操作系统之间的无缝集成&#xff0c;并具有创新设计和增强功能&#xff0c;如无密码登录与TouchID、支持macOSSonoma14和增强打印选项。此外&#xff0c;它还支持运行更多Windo…