当前位置: 首页 > news >正文

【RabbitMQ实战】Springboot 整合RabbitMQ组件,多种编码示例,带你实践 看完这一篇就够了

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 前言
  • 一、对RabbitMQ管理界面深入了解
    • 1、在这个界面里面我们可以做些什么?
  • 二、编码练习
    • (1)使用direct exchange(直连型交换机)
    • (2)使用Topic Exchange 主题交换机。
    • (3)使用Fanout Exchang 扇型交换机。
  • 三、消息确认种类
    • A:消息发送确认
    • B: 消费接收确认
      • 方式一:通过配置类的方式实现
      • 方式二:通过yml配置来完成消费者确认


前言

该篇文章内容较多,包括有RabbitMQ一些理论介绍,provider消息推送实例,consumer消息消费实例,Direct、Topic、Fanout多种交换机的使用,同时简单介绍对消息回调、手动确认等。


这里面的每一种使用都包含实际编码示例,供大家理解,共同进步,如有不足。还请指教。

一、对RabbitMQ管理界面深入了解

装完rabbitMq,启动MQ后,本地浏览器输入http://ip:15672/ ,看到一个简单后台管理界面;
在这里插入图片描述
对于其中的一些具体指标的解释:

  • Ready: 待消费的消息总数。
  • Unacked: 待应答的消息总数。
  • Total:总数 Ready+Unacked。
  • Publish: producter pub消息的速率。
  • Publisher confirm: broker确认pub消息的速率。
  • Deliver(manual ack): customer手动确认的速率。
  • Deliver( auto ack): customer自动确认的速率。
  • Consumer ack: customer正在确认的速率。
  • Redelivered: 正在传递’redelivered’标志集的消息的速率。
  • Get (manual ack): 响应basic.get而要求确认的消息的传输速率。
  • Get (auto ack): 响应于basic.get而发送不需要确认的消息的速率。
  • Return: 将basic.return发送给producter的速率。
  • Disk read: queue从磁盘读取消息的速率。
  • Disk write: queue从磁盘写入消息的速率。

Connections:client的tcp连接的总数。
Channels:通道的总数。
Exchange:交换器的总数。
Queues:队列的总数。
Consumers:消费者的总数。

更详细的可见:
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_19343089/article/details/135724659

1、在这个界面里面我们可以做些什么?

可以手动创建虚拟host,创建用户,分配权限,创建交换机,创建队列等等,还有查看队列消息,消费效率,推送效率等等。

以上这些管理界面的操作在这篇暂时不做扩展描述,我想着重介绍后面实例里会使用到的。

首先先介绍一个简单的一个消息推送到接收的流程,提供一个简单的图:
在这里插入图片描述
黄色的圈圈就是我们的消息推送服务,将消息推送到 中间方框里面也就是 rabbitMq的服务器,然后经过服务器里面的交换机、队列等各种关系(后面会详细讲)将数据处理入列后,最终右边的蓝色圈圈消费者获取对应监听的消息。

常用的交换机有以下三种,因为消费者是从队列获取信息的,队列是绑定交换机的(一般),所以对应的消息推送/接收模式也会有以下几种:

- Direct Exchange

直连型交换机,根据消息携带的路由键将消息投递给对应队列。

大致流程,有一个队列绑定到一个直连交换机上,同时赋予一个路由键 routing key 。
然后当一个消息携带着路由值为X,这个消息通过生产者发送给交换机时,交换机就会根据这个路由值X去寻找绑定值也是X的队列。

- Fanout Exchange

扇型交换机,这个交换机没有路由键概念,就算你绑了路由键也是无视的。 这个交换机在接收到消息后,会直接转发到绑定到它上面的所有队列。

- Topic Exchange

主题交换机,这个交换机其实跟直连交换机流程差不多,但是它的特点就是在它的路由键和绑定键之间是有规则的。
简单地介绍下规则:

(星号) * 用来表示一个单词 (必须出现的)
(井号) # 用来表示任意数量(零个或多个)单词
通配的绑定键是跟队列进行绑定的,举个小例子
队列Q1 绑定键为 .TT. 队列Q2绑定键为 TT.#
如果一条消息携带的路由键为 A.TT.B,那么队列Q1将会收到;
如果一条消息携带的路由键为TT.AA.BB,那么队列Q2将会收到;

主题交换机是非常强大的,为啥这么膨胀?
当一个队列的绑定键为 “#”(井号) 的时候,这个队列将会无视消息的路由键,接收所有的消息。
当 * (星号) 和 # (井号) 这两个特殊字符都未在绑定键中出现的时候,此时主题交换机就拥有的直连交换机的行为。
所以主题交换机也就实现了扇形交换机的功能,和直连交换机的功能。

另外还有 Header Exchange 头交换机 ,Default Exchange 默认交换机,Dead Letter Exchange 死信交换机,这几个该篇暂不做讲述。

好了,一些简单的介绍到这里为止, 接下来我们来一起编码。

二、编码练习

本次实例教程需要创建2个springboot项目,一个 rabbitmq-provider (生产者),一个rabbitmq-consumer(消费者)。【补充说明:我这里模块名称创建错了,其中生产者我创建成了rabbitmq-consumer,消费者我这里叫做 rabbitmq-consumer-true】

首先创建 rabbitmq-provider,

pom.xml里用到的jar依赖:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.6.3</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.atguigu.gulimall</groupId><artifactId>rabbitmq-consumer</artifactId><version>0.0.1-SNAPSHOT</version><name>rabbitmq-consumer</name><description>RabbitMQ生产者模块</description><url/><licenses><license/></licenses><developers><developer/></developers><scm><connection/><developerConnection/><tag/><url/></scm><properties><java.version>1.8</java.version><!--        <spring-cloud.version>2021.0.4</spring-cloud.version>--><spring-cloud.version>2021.0.1</spring-cloud.version></properties><dependencies><dependency><groupId>com.atguigu.gulimall</groupId><artifactId>gulimall-common</artifactId><version>0.0.1-SNAPSHOT</version><exclusions><exclusion><artifactId>servlet-api</artifactId><groupId>javax.servlet</groupId></exclusion></exclusions></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.amqp</groupId><artifactId>spring-rabbit-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

然后application.yml:

server:port: 8021#数据源配置
spring:datasource:username: rootpassword: rooturl: jdbc:mysql://192.168.56.10:3306/gulimall_umsdriver-class-name: com.mysql.cj.jdbc.Driver#注册到注册中心cloud:nacos:discovery:server-addr: 127.0.0.1:8848application:name: rabbitmq-consumer#配置rabbitMq 服务器rabbitmq:host: 127.0.0.1port: 5672username: guestpassword: guest#虚拟host 可以不设置,使用server默认hostvirtual-host: /
#    publisher-returns: true  #确认消息已发送到队列(Queue)  这个在生产者模块配置 这个后期再配置,这会还用不到
#    publisher-confirm-type: correlated   #确认消息已发送到交换机(Exchange) 这个在生产者模块配置 这个后期再配置,这会还用不到logging:level:com.atguigu.gulimall: debug   #调整product模块日志的输出模式是debug级别,这样就能在控制台看到dao包下的输出日志了。

一定要注意 要注意 要注意!!!!!
里面的virtual-host 是指RabbitMQ控制台中的下面的位置(我理解是指你的队列和交换机在哪个分组下面,可以为每一个项目创建单独的分组,但是在此我没有单独创建,直接放到了 / 下面)
在这里插入图片描述
那么怎么建一个单独的host呢? 假如我就是想给某个项目接入,使用一个单独host,顺便使用一个单独的账号,就好像我文中配置的 root 这样。

其实也很简便:

virtual-host的创建:
在这里插入图片描述

账号user的创建:
在这里插入图片描述

然后记得给账号分配权限,指定使用某个virtual host:
指定给自己刚刚为某个项目单独创建的virtual host。
在这里插入图片描述

其实还可以特定指定交换机使用权等等:
在这里插入图片描述

(1)使用direct exchange(直连型交换机)

创建DirectRabbitConfig.java(对于队列和交换机持久化以及连接使用设置,在注释里有说明,后面的不同交换机的配置就不做同样说明了):

package com.atguigu.gulimall.rabbitmqconsumer.config;import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.amqp.core.Queue;/*** 这里使用的是direct exchange(直连型交换机),  也就是交换机和队列是一对一关系* 模拟 rabbitmq-provider (生产者),这里模块名字写错了。这个是消息生产者** @author: jd* @create: 2024-06-24*/
@Configuration
public class DirectRabbitConfig {// 声明需要使用的交换机/路由Key/队列的名称public static final String DEFAULT_EXCHANGE = "TestDirectExchange";public static final String DEFAULT_ROUTE = "TestDirectRouting";public static final String DEFAULT_QUEUE = "TestDirectQueue";// 声明交换机,需要几个声明几个,这里就一个@Beanpublic DirectExchange directExchange(){return new DirectExchange(DEFAULT_EXCHANGE);}//创建队列//队列 起名:TestDirectQueue@Beanpublic Queue TestDirectQueue(){// durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效// exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable// autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。//   return new Queue("TestDirectQueue",true,true,false);//一般设置一下队列的持久化就好,其余两个就是默认falsereturn new Queue(DEFAULT_QUEUE,true);}//绑定交换机和队列,并指定路由键//绑定  将队列和交换机绑定, 并设置用于匹配键:TestDirectRoutingBinding bindingDirect(){return BindingBuilder.bind(TestDirectQueue()).to(directExchange()).with(DEFAULT_ROUTE);}/*** 这个是做什么用的 ,为了后面 生产者确认那,找到交换机,找不到队列用的,* @return*/@BeanDirectExchange lonelyDirectExchange() {return new DirectExchange("lonelyDirectExchange");}}

然后写个简单的接口进行消息推送(根据需求也可以改为定时任务等等,具体看需求),SendMessageController.java:

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;/***  模拟 rabbitmq-provider (生产者) 这里模块名字写错了。这个是消息生产者,一般消息的生产者会直接在业务层调用,*  不会单独的搞一个消息生产者,这里因为没有业务调用,去调用这个MQ的生产者,所以这里直接创建一个模块模拟消息生产者** 发送消息控制器(MQ入消息的入口)* //原文链接:https://blog.csdn.net/qq_35387940/article/details/100514134* @author: jd* @create: 2024-06-24*/
@RestController
public class SendMessageController {@AutowiredRabbitTemplate rabbitTemplate;  //使用RabbitTemplate,这提供了接收/发送等等方法/*** 通过postman发送消息给消息队列-直流交换机* @return*/@GetMapping("/sendDirectMessage")String sendDirectMessage(){String messageId = String.valueOf(UUID.randomUUID());String messageData = "test message, hello!";String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));Map<String,Object> map=new HashMap<>();map.put("messageId",messageId);map.put("messageData",messageData);
//        map.put("messageData","666666");map.put("createTime",createTime);//将消息携带绑定键值:TestDirectRouting 发送到交换机TestDirectExchangerabbitTemplate.convertAndSend("TestDirectExchange", "TestDirectRouting", map);//        //生产者发送字符串类型消息,则后面的消息消费者,也需要接受字符串类型的入参进行消费
//        rabbitTemplate.convertAndSend("TestDirectExchange", "TestDirectRouting", "77777");System.out.println("调用完毕");return "ok";}}

把rabbitmq-provider项目运行,调用下接口:

在这里插入图片描述

在这里插入图片描述
因为我们目前还没弄消费者 rabbitmq-consumer,消息没有被消费的,我们去rabbitMq管理页面看看,是否推送成功:(我这里发送了三次,所以有三个消息积压了)
在这里插入图片描述

再看看队列(界面上的各个英文项代表什么意思,可以自己查查哈,对理解还是有帮助的):
在这里插入图片描述

很好,消息已经推送到rabbitMq服务器上面了。

接下来,创建rabbitmq-consumer项目:

pom.xml里的jar依赖:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.6.3</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.atguigu.gulimall</groupId><artifactId>rabbitmq-consumer-true</artifactId><version>0.0.1-SNAPSHOT</version><name>rabbitmq-consumer-true</name><description>RabbitMQ消费者模块</description><url/><licenses><license/></licenses><developers><developer/></developers><scm><connection/><developerConnection/><tag/><url/></scm><properties><java.version>1.8</java.version><!--        <spring-cloud.version>2021.0.4</spring-cloud.version>--><spring-cloud.version>2021.0.1</spring-cloud.version></properties><dependencies><dependency><groupId>com.atguigu.gulimall</groupId><artifactId>gulimall-common</artifactId><version>0.0.1-SNAPSHOT</version><exclusions><exclusion><artifactId>servlet-api</artifactId><groupId>javax.servlet</groupId></exclusion></exclusions></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.amqp</groupId><artifactId>spring-rabbit-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

然后是 application.yml:

server:port: 8022#数据源配置
spring:datasource:url: jdbc:mysql://192.168.56.10:3306/gulimall_umsusername: rootpassword: rootdriver-class-name:  com.mysql.cj.jdbc.Driver#配置nacoscloud:nacos:discovery:server-addr: 127.0.0.1#配置服务名称application:name: rabbitmq-consumer-true# 配置rabbitMq 服务器#spring.application.name=rabbitmq-consumer-truerabbitmq:host: 127.0.0.1port: 5672username: guestpassword: guest#虚拟host 可以不设置,使用server默认hostvirtual-host: /
#    listener:  #这个在测试消费多个消息的时候,不能有下面这些配置,否则只能消费一个消息后就不继续消费了
#      simple:
#        acknowledge-mode: manual  #指定MQ消费者的确认模式是手动确认模式  这个在消费者者模块配置
#        prefetch: 1 #一次只能消费一条消息   这个在消费者者模块配置#配置日志输出级别
logging:level:com.atguigu.gulimall: debug#配置日志级别

然后一样,创建DirectRabbitConfig.java(消费者单纯的使用,其实可以不用添加这个配置,直接建后面的监听就好,使用注解来让监听器监听对应的队列即可。配置上了的话,其实消费者也是生成者的身份,也能推送该消息。):

package com.atguigu.gulimall.consumertrue.config;import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/***   消费者配置类**  原文链接:https://blog.csdn.net/qq_35387940/article/details/100514134* 创建DirectRabbitConfig.java  关于队列的配置只是消息的生产者中配置即可。这个消费者不用配置,配置了的话,就也可以当成生产者了* (消费者单纯的使用,其实可以不用添加这个配置,直接建后面的监听就好,* 使用注解来让监听器监听对应的队列即可。配置上了的话,其实消费者也是生成者的身份,也能推送该消息。):** @author: jd* @create: 2024-06-25*/
@Configuration
public class DirectRabbitConfig {// 声明需要使用的交换机/路由Key/队列的名称public static final String DEFAULT_EXCHANGE = "TestDirectExchange";public static final String DEFAULT_ROUTE = "TestDirectRouting";public static final String DEFAULT_QUEUE = "TestDirectQueue";//队列 起名:TestDirectQueue@Beanpublic Queue TestDirectQueue() {return new Queue(DEFAULT_QUEUE,true);}//Direct交换机 起名:TestDirectExchange@BeanDirectExchange TestDirectExchange() {return new DirectExchange(DEFAULT_EXCHANGE);}//绑定  将队列和交换机绑定, 并设置用于匹配键:TestDirectRouting@BeanBinding bindingDirect() {return BindingBuilder.bind(TestDirectQueue()).to(TestDirectExchange()).with(DEFAULT_ROUTE);}}

然后是创建消息接收监听类,RabbitMQListener.java:

package com.atguigu.gulimall.consumertrue.listener;import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;import java.util.Map;/*** 消息消费监听类* @author: jd* @create: 2024-06-25*/
@Component
@Slf4j
@RabbitListener(queues = "TestDirectQueue")//监听的队列名称 TestDirectQueue
public class RabbitMQListener {/*** 当消息发送者发送的是Map的时候,通过这个消息处理器进行处理* @param testMessage*/@RabbitHandler(isDefault = true)public void process(Map testMessage) {System.out.println("RabbitMQListener消费者收到消息  : "+testMessage.toString());}/*** 当消息发送者发送的是String类型的时候,用这个监听处理器去接受消息并处理* @param testMessage*//* @RabbitHandler(isDefault = true)public void process(String testMessage) {System.out.println("DirectReceiver消费者收到消息  : "+testMessage);//正常开发中,会在消费到消息之后,开始做一些业务处理//模拟业务处理//业务开始String str = testMessage + "--消费成功";System.out.println("业务处理完毕"+str);//业务结束}*/}

然后将rabbitmq-consumer-true项目运行起来,可以看到把之前推送的那条消息消费下来了:
在这里插入图片描述
然后可以再继续调用rabbitmq-consumer项目的推送消息接口,可以看到消费者即时消费消息:
在这里插入图片描述
消费下来了
在这里插入图片描述
那么直连交换机既然是一对一,那如果咱们配置多台监听绑定到同一个直连交互的同一个队列,会怎么样?
在这里插入图片描述
消费的结果如下:
在这里插入图片描述

可以看到是实现了轮询的方式对消息进行消费,而且不存在重复消费。

(2)使用Topic Exchange 主题交换机。

在rabbitmq-consume项目里面创建TopicRabbitConfig.java:

package com.atguigu.gulimall.rabbitmqconsumer.config;import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** 使用Topic Exchange 主题交换机。** @author: jd* @create: 2024-06-25*/
@Configuration
public class TopicRabbitConfig {//设置绑定键public static final String man = "topic.man";public static final String woman = "topic.woman";public static final String TOPIC_EXCHANGE = "topicExchange";//创建队列/*** 第一个主题队列** @return*/@Beanpublic Queue firstQueue() {return new Queue(man);}/*** 第二个主题队列** @return*/@Beanpublic Queue secondQueue() {return new Queue(woman);}/*** 创建一个主题交换机** @return TopicExchange*/@BeanTopicExchange exchange() {return new TopicExchange(TOPIC_EXCHANGE);}/*** //将firstQueue和topicExchange绑定,而且绑定的键值为topic.man* //这样只要是消息携带的路由键是topic.man,才会分发到该队列** @return*/@BeanBinding bindingExchangeMessageForFirstQueue() {return BindingBuilder.bind(firstQueue()).to(exchange()).with(man);}/*** //将secondQueue和topicExchange绑定,而且绑定的键值为用上通配路由键规则topic.#* // 这样只要是消息携带的路由键是以topic.开头,都会分发到该队列** @return*/@BeanBinding bindingExchangeMessageForSecondQueue() {return BindingBuilder.bind(secondQueue()).to(exchange()).with("topic.#");}}

然后添加多2个接口,用于推送消息到主题交换机:


//    然后添加多2个接口,用于推送消息到主题交换机找那个,再主题交换机中通过设置的路由键来推送到主题为topic.man的队列中以供消费
//    https://blog.csdn.net/qq_35387940/article/details/100514134/*** 用于向MQ发送携带topic.man路由键的消息* @return*/@GetMapping("/sendTopicMessageToMan")public String sendTopicMessageToMan(){String messageId = String.valueOf(UUID.randomUUID());String messageData ="send topic message to man";String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));Map<String,Object> map=new HashMap<>();map.put(QueueConstant.MESSAGE_ID,messageId);map.put(QueueConstant.MESSAGE_DATA,messageData);map.put(QueueConstant.MESSAGE_TIME,createTime);rabbitTemplate.convertAndSend(TopicRabbitConfig.TOPIC_EXCHANGE,TopicRabbitConfig.man,map);System.out.println("sendTopicMessageToMan() 执行成功");return "sendTopicMessageToMan is ok";}/*** 用于向MQ发送携带topic.woman路由键的消息。 这样会在exchange中去找绑定中这个路由键绑定的队列,并向其中进行转发* topic.# 这个是通用的绑定规则,只要是携带着topic.开头的就会转发到绑定的这个队列中* https://blog.csdn.net/qq_35387940/article/details/100514134* @return*/@GetMapping("/sendTopicMessageToTotal")public String sendTopicMessageToTotal(){String messageId = String.valueOf(UUID.randomUUID());String messageData ="send topic message to woman";String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));Map<String,Object> map=new HashMap<>();map.put(QueueConstant.MESSAGE_ID,messageId);map.put(QueueConstant.MESSAGE_DATA,messageData);map.put(QueueConstant.MESSAGE_TIME,createTime);
//        rabbitTemplate.convertAndSend(TopicRabbitConfig.TOPIC_EXCHANGE,TopicRabbitConfig.woman,map);rabbitTemplate.convertAndSend(TopicRabbitConfig.TOPIC_EXCHANGE,"topic.woman1",map); //测试携带路由键符合topic.#的是否能转发到topic.woman的队列System.out.println("sendTopicMessageToTotal() 执行成功");return "sendTopicMessageToTotal is ok";}

生产者这边已经完事,先不急着运行,在rabbitmq-consumer-true项目上,创建TopicManListener.java:

package com.atguigu.gulimall.consumertrue.listener;import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;import java.util.Map;/** 
主题交换机 监听topic.man队列* @author: jd* @create: 2024-06-25*/
@Component
@Slf4j
@RabbitListener(queues = "topic.man")//监听的队列名称 TestDirectQueue
public class TopicManListener {@RabbitHandlerpublic void process(Map testMessage) {System.out.println("TopicManListener主题消费者收到消息  : "+testMessage.toString());}}

再创建一个TopicTotalListener.java:

package com.atguigu.gulimall.consumertrue.listener;import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;import java.util.Map;/*** @author: jd* @create: 2024-06-25*/@Component
@Slf4j
@RabbitListener(queues = "topic.woman")
public class TopicTotalListener {@RabbitHandlerpublic void process(Map testMessage){System.out.println("TopicTotalListener主题消费者收到消息  : "+testMessage.toString());}
}

同样,加主题交换机的相关配置,TopicRabbitConfig.java(消费者一定要加这个配置吗? 不需要的其实,理由在前面已经说过了。):

package com.atguigu.gulimall.rabbitmqconsumer.config;import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** 使用Topic Exchange 主题交换机。** @author: jd* @create: 2024-06-25*/
@Configuration
public class TopicRabbitConfig {//设置绑定键public static final String man = "topic.man";public static final String woman = "topic.woman";public static final String TOPIC_EXCHANGE = "topicExchange";//创建队列/*** 第一个主题队列** @return*/@Beanpublic Queue firstQueue() {return new Queue(man);}/*** 第二个主题队列** @return*/@Beanpublic Queue secondQueue() {return new Queue(woman);}/*** 创建一个主题交换机** @return TopicExchange*/@BeanTopicExchange exchange() {return new TopicExchange(TOPIC_EXCHANGE);}/*** //将firstQueue和topicExchange绑定,而且绑定的键值为topic.man* //这样只要是消息携带的路由键是topic.man,才会分发到该队列** @return*/@BeanBinding bindingExchangeMessageForFirstQueue() {return BindingBuilder.bind(firstQueue()).to(exchange()).with(man);}/*** //将secondQueue和topicExchange绑定,而且绑定的键值为用上通配路由键规则topic.#* // 这样只要是消息携带的路由键是以topic.开头,都会分发到该队列** @return*/@BeanBinding bindingExchangeMessageForSecondQueue() {return BindingBuilder.bind(secondQueue()).to(exchange()).with("topic.#");}}

然后把rabbitmq-consumer,rabbitmq-consumer-true两个项目都跑起来,先调用/sendTopicMessage1 接口:
在这里插入图片描述

然后看消费者rabbitmq-consumer的控制台输出情况:
TopicManReceiver监听队列1,绑定键为:topic.man
TopicTotalReceiver监听队列2,绑定键为:topic.#
而当前推送的消息,携带的路由键为:topic.man

所以可以看到两个监听消费者receiver都成功消费到了消息,因为这两个recevier监听的队列的绑定键都能与这条消息携带的路由键匹配上。
在这里插入图片描述
接下来调用接口/sendTopicMessage2:
在这里插入图片描述
然后看消费者rabbitmq-consumer的控制台输出情况:
TopicManReceiver监听队列1,绑定键为:topic.man
TopicTotalReceiver监听队列2,绑定键为:topic.#
而当前推送的消息,携带的路由键为:topic.woman

所以可以看到两个监听消费者只有TopicTotalReceiver成功消费到了消息。
在这里插入图片描述

(3)使用Fanout Exchang 扇型交换机。

同样地,先在rabbitmq-provider项目上创建FanoutRabbitConfig.java:

package com.atguigu.gulimall.rabbitmqconsumer.config;import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** 使用Fanout Exchang 扇型交换机* @author: jd* @create: 2024-06-25*/
@Configuration
public class FanoutRabbitConfig {//队列名称public static final String  FANOUT_QUEUE_A ="fanout.a";public static final String  FANOUT_QUEUE_B ="fanout.b";public static final String  FANOUT_QUEUE_C ="fanout.c";public static final String  FANOUT_EXCHANGE = "fanout.exchange";//创建队列 FANOUT_QUEUE_A@Beanpublic Queue queueA(){return new Queue(FANOUT_QUEUE_A,true);}//创建队列 FANOUT_QUEUE_B@Beanpublic Queue queueB(){return new Queue(FANOUT_QUEUE_B);}//创建队列 FANOUT_QUEUE_C@Beanpublic Queue queueC(){return new Queue(FANOUT_QUEUE_C);}//创建交换机@Beanpublic FanoutExchange  fanoutExchange(){return new FanoutExchange(FANOUT_EXCHANGE);}//绑定将多有的队列都绑定到这个交换机@BeanBinding bindingExchangeA() {return BindingBuilder.bind(queueA()).to(fanoutExchange());}@BeanBinding bindingExchangeB() {return BindingBuilder.bind(queueB()).to(fanoutExchange());}@BeanBinding bindingExchangeC() {return BindingBuilder.bind(queueC()).to(fanoutExchange());}}

然后是写一个接口用于推送消息,

 /*** 发送消息给扇形交换机 扇型交换机* @return*/@GetMapping("/sendFanoutMessage")public String sendFanoutMessage(){String messageId = String.valueOf(UUID.randomUUID());String messageData = "message: testFanoutMessage ";String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));Map<String, Object> map = new HashMap<>();map.put(QueueConstant.MESSAGE_ID,messageId);map.put(QueueConstant.MESSAGE_DATA,messageData);map.put(QueueConstant.MESSAGE_TIME,createTime);rabbitTemplate.convertAndSend(FanoutRabbitConfig.FANOUT_EXCHANGE,null,map);System.out.println("sendFanoutMessage() 执行成功");return "sendFanoutMessage is ok";}

接着在rabbitmq-consumer-true项目里加上消息消费类,
在这里插入图片描述

FanoutReceiverA.java:
FanoutReceiverB.java:
FanoutReceiverC.java:

package com.atguigu.gulimall.consumertrue.listener;import com.atguigu.gulimall.consumertrue.config.FanoutRabbitConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;import java.util.Map;/*** 扇形交换机-队列A的监听器,及监听到消息后的处理器* @author: jd* @create: 2024-06-25*/
@Component
@Slf4j
@RabbitListener(queues = FanoutRabbitConfig.FANOUT_QUEUE_A)
public class FanoutReceiverA {@RabbitHandlerpublic void process(Map message){System.out.println("FanoutReceiverA消费者收到消息  : "+message.toString());}}
package com.atguigu.gulimall.consumertrue.listener;import com.atguigu.gulimall.consumertrue.config.FanoutRabbitConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;import java.util.Map;/*** 扇形交换机-队列B的监听器,及监听到消息后的处理器* @author: jd* @create: 2024-06-25*/
@Component
@Slf4j
@RabbitListener(queues = FanoutRabbitConfig.FANOUT_QUEUE_B)
public class FanoutReceiverB {@RabbitHandlerpublic void process(Map message){System.out.println("FanoutReceiverB消费者收到消息  : "+message.toString());}
}
package com.atguigu.gulimall.consumertrue.listener;/*** @author: jd* @create: 2024-06-25*/import com.atguigu.gulimall.consumertrue.config.FanoutRabbitConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;import java.util.Map;/*** 扇形交换机-队列B的监听器,及监听到消息后的处理器* @author: jd* @create: 2024-06-25*/
@Component
@Slf4j
@RabbitListener(queues = FanoutRabbitConfig.FANOUT_QUEUE_C)
public class FanoutReceiverC {@RabbitHandlerpublic void process(Map message){System.out.println("FanoutReceiverC消费者收到消息  : "+message.toString());}
}

然后加上扇型交换机的配置类,FanoutRabbitConfig.java(消费者真的要加这个配置吗? 不需要的其实,理由在前面已经说过了)

package com.atguigu.gulimall.consumertrue.config;import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** 使用Fanout Exchang 扇型交换机* @author: jd* @create: 2024-06-25*/
@Configuration
public class FanoutRabbitConfig {//队列名称public static final String  FANOUT_QUEUE_A ="fanout.a";public static final String  FANOUT_QUEUE_B ="fanout.b";public static final String  FANOUT_QUEUE_C ="fanout.c";public static final String  FANOUT_EXCHANGE = "fanout.exchange";//创建队列 FANOUT_QUEUE_A@Beanpublic Queue queueA(){return new Queue(FANOUT_QUEUE_A,true);}//创建队列 FANOUT_QUEUE_B@Beanpublic Queue queueB(){return new Queue(FANOUT_QUEUE_B);}//创建队列 FANOUT_QUEUE_C@Beanpublic Queue queueC(){return new Queue(FANOUT_QUEUE_C);}//创建交换机@Beanpublic FanoutExchange  fanoutExchange(){return new FanoutExchange(FANOUT_EXCHANGE);}//绑定将多有的队列都绑定到这个交换机@BeanBinding bindingExchangeA() {return BindingBuilder.bind(queueA()).to(fanoutExchange());}@BeanBinding bindingExchangeB() {return BindingBuilder.bind(queueB()).to(fanoutExchange());}@BeanBinding bindingExchangeC() {return BindingBuilder.bind(queueC()).to(fanoutExchange());}}

最后将rabbitmq-provider和rabbitmq-consumer项目都跑起来,调用下接口/sendFanoutMessage :
在这里插入图片描述
在这里插入图片描述
可以看到只要发送到 fanoutExchange 这个扇型交换机的消息, 三个队列都绑定这个交换机,所以三个消息接收类都监听到了这条消息。

在这里插入图片描述

到了这里其实三个常用的交换机的使用我们已经完毕了,那么接下来我们继续讲讲消息的回调,其实就是消息确认(生产者推送消息成功,消费者接收消息成功)。

三、消息确认种类

RabbitMQ的消息确认有两种。

一种是消息发送确认。这种是用来确认生产者将消息发送给交换器,交换器传递给队列的过程中,消息是否成功投递。发送确认分为两步,一是确认是否到达交换器,二是确认是否到达队列。

第二种是消费接收确认。这种是确认消费者是否成功消费了队列中的消息。

消息确认的作用是什么?

为了防止消息丢失。消息丢失分为发送丢失和消费者处理丢失,相应的也有两种确认机制。

先来一起学习一下:

A:消息发送确认

在rabbitmq-consumer项目的application.yml文件上,加上消息确认的配置项后:

server:port: 8021#数据源配置
spring:datasource:username: rootpassword: rooturl: jdbc:mysql://192.168.56.10:3306/gulimall_umsdriver-class-name: com.mysql.cj.jdbc.Driver#注册到注册中心cloud:nacos:discovery:server-addr: 127.0.0.1:8848application:name: rabbitmq-consumer#配置rabbitMq 服务器rabbitmq:host: 127.0.0.1port: 5672username: guestpassword: guest#虚拟host 可以不设置,使用server默认hostvirtual-host: /publisher-returns: true  #确认消息已发送到队列(Queue)  这个在生产者模块配置 这个后期再配置,这会还用不到publisher-confirm-type: correlated   #确认消息已发送到交换机(Exchange) 这个在生产者模块配置 这个后期再配置,这会还用不到logging:level:com.atguigu.gulimall: debug   #调整product模块日志的输出模式是debug级别,这样就能在控制台看到dao包下的输出日志了。

然后是配置相关的消息确认回调函数,RabbitConfig.java:

package com.atguigu.gulimall.rabbitmqconsumer.config;import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** 配置相关的消息确认回调函数,RabbitConfig.java:* https://blog.csdn.net/qq_35387940/article/details/100514134** 先从总体的情况分析,推送消息存在四种情况:** ①消息推送到server,但是在server里找不到交换机* ②消息推送到server,找到交换机了,但是没找到队列* ③消息推送到sever,交换机和队列啥都没找到* ④消息推送成功* 具体哪些会触发回调,分别又会触发哪个函数,看下面的测试** @author: jd* @create: 2024-06-25*/
@Configuration
public class RabbitConfig {@Beanpublic RabbitTemplate createRabbitTemplate(ConnectionFactory connectionFactory){RabbitTemplate rabbitTemplate =new RabbitTemplate();rabbitTemplate.setConnectionFactory(connectionFactory);//设置开启Mandatory,才能触发回调函数,无论消息推送结果怎么样都强制调用回调函数rabbitTemplate.setMandatory(true);rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback(){@Overridepublic void confirm(CorrelationData correlationData, boolean ack, String cause) {System.out.println("ConfirmCallback:     "+"相关数据:"+correlationData);System.out.println("ConfirmCallback:     "+"确认情况:"+ack);System.out.println("ConfirmCallback:     "+"原因:"+cause);}});rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {@Overridepublic void returnedMessage(ReturnedMessage returnedMessage) {System.out.println("ReturnCallback:     "+"消息:"+returnedMessage.getMessage());System.out.println("ReturnCallback:     "+"回应码:"+returnedMessage.getReplyCode());System.out.println("ReturnCallback:     "+"回应信息:"+returnedMessage.getReplyText());System.out.println("ReturnCallback:     "+"交换机:"+returnedMessage.getExchange());System.out.println("ReturnCallback:     "+"路由键:"+returnedMessage.getRoutingKey());}});return rabbitTemplate;}
}

到这里,生产者推送消息的消息确认调用回调函数已经完毕。
可以看到上面写了两个回调函数,一个叫 ConfirmCallback ,一个叫 RetrunCallback;
那么以上这两种回调函数都是在什么情况会触发呢?

先从总体的情况分析,推送消息存在四种情况:

①消息推送到server,但是在server里找不到交换机
②消息推送到server,找到交换机了,但是没找到队列
③消息推送到sever,交换机和队列啥都没找到
④消息推送成功

那么我先写几个接口来分别测试和认证下以上4种情况,消息确认触发回调函数的情况:

①消息推送到server,但是在server里找不到交换机 (是否到达交换机)
写个测试接口,把消息推送到名为‘non-existent-exchange’的交换机上(这个交换机是没有创建没有配置的):

/*** ①消息推送到server,但是在server里找不到交换机** 写个测试接口,把消息推送到名为‘non-existent-exchange’的交换机上(这个交换机是没有创建没有配置的)* 调用接口,查看rabbitmq-provuder项目的控制台输出情况(原因里面有说,没有找到交换机'non-existent-exchange'):*在控制台中* 调用后返回:http://localhost:8021/TestMessageAck*ConfirmCallback:     相关数据:null* ConfirmCallback:     确认情况:false* ConfirmCallback:     原因:channel error; protocol method: #method<channel.close>(reply-code=404,* reply-text=NOT_FOUND - no exchange 'non-existent-exchange' in vhost '/', class-id=60, method-id=40)**  结论: ①这种情况触发的是 ConfirmCallback 回调函数* @return*/@GetMapping("/TestMessageAck")public String TestMessageAck() {String messageId = String.valueOf(UUID.randomUUID());String messageData = "message: non-existent-exchange test message ";String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));Map<String, Object> map = new HashMap<>();map.put("messageId", messageId);map.put("messageData", messageData);map.put("createTime", createTime);rabbitTemplate.convertAndSend("non-existent-exchange", "TestDirectRouting", map);return "ok";}

调用接口,查看rabbitmq-provuder项目的控制台输出情况(原因里面有说,没有找到交换机’non-existent-exchange’):
在这里插入图片描述
结论: ①这种情况触发的是 ConfirmCallback 回调函数。

②消息推送到server,找到交换机了,但是没找到队列 (是否到达队列)
这种情况就是需要新增一个交换机,但是不给这个交换机绑定队列,我来简单地在DirectRabitConfig里面新增一个直连交换机,名叫‘lonelyDirectExchange’,但没给它做任何绑定配置操作:

    @BeanDirectExchange lonelyDirectExchange() {return new DirectExchange("lonelyDirectExchange");}

然后写个测试接口,把消息推送到名为‘lonelyDirectExchange’的交换机上(这个交换机是没有任何队列配置的):

/*** ②消息推送到server,找到交换机了,但是没找到队列* 这种情况就是需要新增一个交换机,但是不给这个交换机绑定队列,* 我来简单地在DirectRabitConfig里面新增一个直连交换机,名叫‘lonelyDirectExchange’,但没给它做任何绑定配置操作:** 然后写个测试接口,把消息推送到名为‘lonelyDirectExchange’的交换机上(这个交换机是没有任何队列配置的):**可以看到这种情况,在控制台中 两个函数都被调用了;* 这种情况下,消息是推送成功到服务器了的,所以ConfirmCallback对消息确认情况是true;* 而在RetrunCallback回调函数的打印参数里面可以看到,消息是推送到了交换机成功了,但是在路由分发给队列的时候,找不到队列,所以报了错误 NO_ROUTE 。** 调用后返回:http://localhost:8021/TestMessageAck2* ReturnCallback:     回应码:312* ReturnCallback:     回应信息:NO_ROUTE* ReturnCallback:     交换机:lonelyDirectExchange* ReturnCallback:     路由键:TestDirectRouting* ConfirmCallback:     相关数据:null* ConfirmCallback:     确认情况:true* ConfirmCallback:     原因:null**   结论:②这种情况触发的是 ConfirmCallback和RetrunCallback两个回调函数。* @return*/@GetMapping("/TestMessageAck2")public String TestMessageAck2() {String messageId = String.valueOf(UUID.randomUUID());String messageData = "message: lonelyDirectExchange test message ";String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));Map<String, Object> map = new HashMap<>();map.put("messageId", messageId);map.put("messageData", messageData);map.put("createTime", createTime);rabbitTemplate.convertAndSend("lonelyDirectExchange", "TestDirectRouting", map);  //lonelyDirectExchange这个交换机没有和任何队列做绑定,return "ok";}

调用接口,查看rabbitmq-provuder项目的控制台输出情况:
在这里插入图片描述
在这里插入图片描述

ConfirmCallback:     相关数据:null
ConfirmCallback:     确认情况:true
ConfirmCallback:     原因:null
ReturnCallback:     消息:(Body:'[serialized object]' MessageProperties [headers={}, contentType=application/x-java-serialized-object, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, deliveryTag=0])
ReturnCallback:     回应码:312
ReturnCallback:     回应信息:NO_ROUTE
ReturnCallback:     交换机:lonelyDirectExchange
ReturnCallback:     路由键:TestDirectRouting

可以看到这种情况,两个函数都被调用了;
这种情况下,消息是推送成功到服务器了的,所以ConfirmCallback对消息确认情况是true;
而在RetrunCallback回调函数的打印参数里面可以看到,消息是推送到了交换机成功了,但是在路由分发给队列的时候,找不到队列,所以报了错误 NO_ROUTE 。
结论:②这种情况触发的是 ConfirmCallback和RetrunCallback两个回调函数。

③消息推送到sever,交换机和队列啥都没找到
这种情况其实一看就觉得跟①很像,没错 ,③和①情况回调是一致的,所以不做结果说明了。
结论: ③这种情况触发的是 ConfirmCallback 回调函数。

④消息推送成功
那么测试下,按照正常调用之前消息推送的接口就行,就调用下 /sendFanoutMessage接口,可以看到控制台输出:

ConfirmCallback:     相关数据:null
ConfirmCallback:     确认情况:true
ConfirmCallback:     原因:null

结论: ④这种情况触发的是 ConfirmCallback 回调函数。

总结:
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback(){}通过设置这个参数,其中使用内部类进行实现,来记录消息发送到交换器Exchange后触发回调。
使用该功能需要开启确认, publisher-confirm-type: correlated #确认消息已发送到交换机(Exchange) 这个在生产者模块配置

rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback(){})通过设置这个参数,如果消息从交换器发送到对应队列失败时触发(比如根据发送消息时指定的routingKey找不到队列时会触发)
publisher-returns: true #确认消息已发送到队列(Queue) 这个在生产者模块配置

以上是生产者推送消息的消息确认 回调函数的使用介绍(可以在回调函数根据需求做对应的扩展或者业务数据处理)。

B: 消费接收确认

接下来我们继续, 消费者接收到消息的消息确认机制。

(1)确认模式

AcknowledgeMode.NONE:不确认
AcknowledgeMode.AUTO:自动确认
AcknowledgeMode.MANUAL:手动确认
spring-boot中配置方法:

spring.rabbitmq.listener.simple.acknowledge-mode = manual

(2)手动确认
在这里插入图片描述
未确认的消息数

上图为channel中未被消费者确认的消息数。

通过RabbitMQ的host地址加上默认端口号15672访问管理界面。

(2.1)成功确认

void basicAck(long deliveryTag, boolean multiple) throws IOException;

deliveryTag:该消息的index

multiple:是否批量. true:将一次性ack所有小于deliveryTag的消息。

消费者成功处理后,调用channel.basicAck(message.getMessageProperties().getDeliveryTag(), false)方法对消息进行确认。

(2.2)失败确认

void basicNack(long deliveryTag, boolean multiple, boolean requeue)

throws IOException;

deliveryTag:该消息的index。

multiple:是否批量. true:将一次性拒绝所有小于deliveryTag的消息。

requeue:被拒绝的是否重新入队列。

void basicReject(long deliveryTag, boolean requeue) throws IOException;

deliveryTag:该消息的index。

requeue:被拒绝的是否重新入队列。

channel.basicNack 与 channel.basicReject 的区别在于basicNack可以批量拒绝多条消息,而basicReject一次只能拒绝一条消息。

①自动确认, 这也是默认的消息确认情况。  AcknowledgeMode.NONE
RabbitMQ成功将消息发出(即将消息成功写入TCP Socket)中立即认为本次投递已经被正确处理,不管消费者端是否成功处理本次投递。
所以这种情况如果消费端消费逻辑抛出异常,也就是消费端没有处理成功这条消息,那么就相当于丢失了消息。
一般这种情况我们都是使用try catch捕捉异常后,打印日志用于追踪数据,这样找出对应数据再做后续处理。② 根据情况确认, 这个不做介绍
③ 手动确认 , 这个比较关键,也是我们配置接收消息确认机制时,多数选择的模式。
消费者收到消息后,手动调用basic.ack/basic.nack/basic.reject后,RabbitMQ收到这些消息后,才认为本次投递成功。
basic.ack用于肯定确认 
basic.nack用于否定确认(注意:这是AMQP 0-9-1RabbitMQ扩展) 
basic.reject用于否定确认,但与basic.nack相比有一个限制:一次只能拒绝单条消息 消费者端以上的3个方法都表示消息已经被正确投递,但是basic.ack表示消息已经被正确处理。
而basic.nack,basic.reject表示没有被正确处理:着重讲下reject,因为有时候一些场景是需要重新入列的。channel.basicReject(deliveryTag, true);  拒绝消费当前消息,如果第二参数传入true,就是将数据重新丢回队列里,那么下次还会消费这消息。设置false,就是告诉服务器,我已经知道这条消息数据了,因为一些原因拒绝它,而且服务器也把这个消息丢掉就行。 下次不想再消费这条消息了。使用拒绝后重新入列这个确认模式要谨慎,因为一般都是出现异常的时候,catch异常再拒绝入列,选择是否重入列。但是如果使用不当会导致一些每次都被你重入列的消息一直消费-入列-消费-入列这样循环,会导致消息积压。顺便也简单讲讲 nack,这个也是相当于设置不消费某条消息。channel.basicNack(deliveryTag, false, true);
第一个参数依然是当前消息到的数据的唯一id;
第二个参数是指是否针对多条消息;如果是true,也就是说一次性针对当前通道的消息的tagID小于当前这条消息的,都拒绝确认。
第三个参数是指是否重新入列,也就是指不确认的消息是否重新丢回到队列里面去。同样使用不确认后重新入列这个确认模式要谨慎,因为这里也可能因为考虑不周出现消息一直被重新丢回去的情况,导致积压。

看了上面这么多介绍,接下来我们一起配置下,看看一般的消息接收 手动确认是怎么样的。

方式一:通过配置类的方式实现

此时还不需要加下面的配置,因为这种方式是通过 配置类注解来配置的手动消费者确认,再下面的方式二则是通过yml的配置来设置的消费者手动确认,我们先来看方式一是怎么实现的
在这里插入图片描述

​​​​​​
在消费者项目里,
新建MessageListenerConfig.java上添加代码相关的配置代码:

package com.atguigu.gulimall.consumertrue.config;import com.atguigu.gulimall.consumertrue.listener.MyAckReceiver;
import org.springframework.amqp.core.AcknowledgeMode;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** 一般的消息接收 手动确认是怎么样的,消费者的手动消息确认,配置类* https://blog.csdn.net/qq_35387940/article/details/100514134* @author: jd* @create: 2024-06-25*/
//@Configuration     //注释掉这个注解,这样第一种MQ消费者的确认模式就失效了,以为你这个里面配置着对某个队列的监控呢。 第二种MQ的配置方式的话和这个的区别,不用这种配置类,而是在yml中配置东西
public class MessageListenerConfig {@Autowiredprivate CachingConnectionFactory connectionFactory;@Autowiredprivate MyAckReceiver myAckReceiver;//消息接收处理类@Beanpublic SimpleMessageListenerContainer simpleMessageListenerContainer(){SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);container.setConcurrentConsumers(1);container.setMaxConcurrentConsumers(1);container.setAcknowledgeMode(AcknowledgeMode.MANUAL); // RabbitMQ默认是自动确认,这里改为手动确认消息//设置一个队列,在这里设置了队列,container.setQueueNames("TestDirectQueue");//如果同时设置多个如下: 前提是队列都是必须已经创建存在的//  container.setQueueNames("TestDirectQueue","TestDirectQueue2","TestDirectQueue3");//另一种设置队列的方法,如果使用这种情况,那么要设置多个,就使用addQueues//container.setQueues(new Queue("TestDirectQueue",true));//container.addQueues(new Queue("TestDirectQueue2",true));//container.addQueues(new Queue("TestDirectQueue3",true));//这里设置了监听器,因为上面设置了队列,所以在监听器中就不需要用监听器的注解了 。container.setMessageListener(myAckReceiver);return container;}
}

对应的手动确认消息监听类,MyAckReceiver.java(手动确认模式需要实现 ChannelAwareMessageListener):
//之前的相关监听器可以先注释掉,以免造成多个同类型监听器都监听同一个队列。【比如我之前用的RabbitMQListener 、RabbitMQListener2 为了让其失效,直接注释掉其中的//@RabbitListener(queues = “TestDirectQueue”)//监听的队列名称 TestDirectQueue】 这个注解即可,这样这个监听器就无法监听相关队列了。

在这里插入图片描述
MyAckReceiver.java

package com.atguigu.gulimall.consumertrue.listener;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.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Map;/*** 对应的手动确认消息监听类,MyAckReceiver.java(手动确认模式需要实现 ChannelAwareMessageListener):* //之前的相关监听器可以先注释掉,以免造成多个同类型监听器都监听同一个队列。** 注意:因为这里是在MessageListenerConfig 类中指定了是要监听哪个队列,以及消息的确认机制,所以这里不需要使用* @RabbitListener(queues = "TestDirectQueue")  和 @RabbitHandler(isDefault = true)注解了* @author: jd* @create: 2024-06-25*/@Component
public class MyAckReceiver implements ChannelAwareMessageListener {@Overridepublic void onMessage(Message message, Channel channel) throws Exception {long deliveryTag = message.getMessageProperties().getDeliveryTag();try {byte[] body = message.getBody();ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(body));Map<String,String> msgMap  = (Map<String,String>)objectInputStream.readObject();String messageId = msgMap.get("messageId");String messageData = msgMap.get("messageData");String createTime = msgMap.get("createTime");objectInputStream.close();System.out.println("  MyAckReceiver  messageId:"+messageId+"  messageData:"+messageData+"  createTime:"+createTime);System.out.println("消费的主题队列来自:"+message.getMessageProperties().getConsumerQueue());
//        消费者成功处理后,调用channel.basicAck(message.getMessageProperties().getDeliveryTag(), false)方法对消息进行确认。channel.basicAck(deliveryTag, true); // deliveryTag:该消息的index   multiple:是否批量. true:将一次性ack所有小于deliveryTag的消息。    第二个参数,手动确认可以被批处理, 当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息
//		channel.basicReject(deliveryTag, true);//第二个参数,true会重新放回队列,所以需要自己根据业务逻辑判断什么时候使用拒绝} catch (Exception e) {channel.basicReject(deliveryTag, false);e.printStackTrace();}}
}

这时,先调用接口/sendDirectMessage, 给直连交换机TestDirectExchange 的队列TestDirectQueue 推送一条消息,可以看到监听器正常消费了下来:
在这里插入图片描述
在这里插入图片描述
第一次验证我们发现,消费者没有消费掉直流交换机中的消息,而且也在直流队列中积压了起来,
在这里插入图片描述
这是由于我们的配置类忘记加了 @Configuration 注解了,所以此时这个不是配置类,也就是这里对MQ的配置不会生效,所以加上之后 ,我们再去试试:
在这里插入图片描述
可看到下图 消费成功
在这里插入图片描述
配置类中 container.setAcknowledgeMode(AcknowledgeMode.MANUAL); // RabbitMQ默认是自动确认,这里改为手动确认消息 是发挥作用的关键;

方式二:通过yml配置来完成消费者确认

特别注意:因为这里我们要使用yml配置来实现,所以我们需要关闭配置类的作用,使之失效,我这里直接把@Configuration 给注释掉 了,这样配置类不会起作用了!!_
在这里插入图片描述
第二种方式正式开始啦 (#.#)
首先我们来在yml中开启手动确认的配置

server:port: 8022#数据源配置
spring:datasource:url: jdbc:mysql://192.168.56.10:3306/gulimall_umsusername: rootpassword: rootdriver-class-name:  com.mysql.cj.jdbc.Driver#配置nacoscloud:nacos:discovery:server-addr: 127.0.0.1#配置服务名称application:name: rabbitmq-consumer-true# 配置rabbitMq 服务器#spring.application.name=rabbitmq-consumer-truerabbitmq:host: 127.0.0.1port: 5672username: guestpassword: guest#虚拟host 可以不设置,使用server默认hostvirtual-host: /listener:  #这个在测试消费多个消息的时候,不能有下面这些配置,否则只能消费一个消息后就不继续消费了simple:acknowledge-mode: manual  #指定MQ消费者的确认模式是手动确认模式  这个在消费者者模块配置prefetch: 1 #一次只能消费一条消息   这个在消费者者模块配置#配置日志输出级别
logging:level:com.atguigu.gulimall: debug#配置日志级别

其中的 几行是开启的关键
listener: #这个在测试消费多个消息的时候,不能有下面这些配置,否则只能消费一个消息后就不继续消费了
simple:
acknowledge-mode: manual #指定MQ消费者的确认模式是手动确认模式 这个在消费者者模块配置
prefetch: 1 #一次只能消费一条消息 这个在消费者者模块配置

此处直接用接口来当生产者了;
然后我们在生产者模块用于放消息的controller中增加一个放消息的请求方法,用于往队列里面连续放入5个放消息
SendMessageController.java

    /*** 原文链接:https://blog.csdn.net/weixin_45724872/article/details/119655638* 将信号放入MQ* @param message* @return*/@PostMapping("/msg/muscle")public String receiveMuscleSign(@RequestBody String message) {//处理业务for (int i = 1; i <= 5; i++) {rabbitTemplate.convertAndSend("muscle_fanout_exchange","",message+i);}return " receiveMuscleSign ok";}

开发消费者
此处用一个类下的两个方法来模拟2个消费者

package com.atguigu.gulimall.consumertrue.listener;import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;/****此处用一个类下的两个方法来模拟2个消费者*原文链接:https://blog.csdn.net/weixin_45724872/article/details/119655638原文链接:https://blog.csdn.net/weixin_45724872/article/details/119655638* @author: jd* @create: 2024-06-25*/
@Component
public class MyConsumerListener {@RabbitListener(bindings = {@QueueBinding(value = @Queue("consumer_queue_1"),//绑定交换机exchange = @Exchange(value = "muscle_fanout_exchange", type = "fanout"))})public void consumer1(String msg, Message message, Channel channel) throws Exception {long deliveryTag = message.getMessageProperties().getDeliveryTag();try {System.out.println("消费者1 => " + msg);//channel.basicAck(deliveryTag, false); // 因为 yml中 prefetch 设置为 1(或未设置,因为默认可能是 0,表示无限制,但这不是推荐的做法),RabbitMQ 将只发送一个消息给消费者,并等待该消息的确认。在这种情况下,// 如果你注释掉了 channel.basicAck,消费者将只能消费一个消息,并且不会收到下一个消息,直到你发送确认或关闭连接。 所以对于消息队列中的五个消息只能销费一个,除非你手动确认,否则不会再消费其他的消息} catch (Exception e) {channel.basicReject(deliveryTag, false);e.printStackTrace();}}@RabbitListener(bindings = {@QueueBinding(value = @Queue("consumer_queue_2"),//绑定交换机exchange = @Exchange(value = "muscle_fanout_exchange", type = "fanout"))})public void consumer2(String msg,Message message, Channel channel) throws Exception {long deliveryTag = message.getMessageProperties().getDeliveryTag();try {System.out.println("消费者2 => " + msg);channel.basicAck(deliveryTag, false);} catch (Exception e) {channel.basicReject(deliveryTag, false);e.printStackTrace();}}}

注意一点,消费者1的手动ACK我们是注释掉了

而消费者2的手动ACK我们是开着的

原因是为了对照试验

我们期望的情况是:一共5条消息,消费者1和2都一一处理;

处理完毕后再取下一条,否则不让取;

那么按我们代码这样写;

消费者1只能取一条 (只是处理一条的原因,)
而消费者2则能取满5条(因为消费者1的手动ACK被我们注释了,此处又不是自动ACK)

消费者1只是处理一条的原因:下图中的perfetchCount有问题,我们实际上配置的是prefetch: 1 ,我们直接按照这个配置来理解就行
在这里插入图片描述
消费者一,就是注释了对消息消费之后的确认回馈给RabbitMQ的设置,所以消费者对五条消息中消费到第一个之后,因为我们在yml中又配置了每次消费一条,而且也是手动确认的,所以MQ消费到这一条之后,就在那等着手动调用ack方法来完成的确认ack的反馈,结果我们这里注释了,所以就一直等不到第一条消息的回馈,所以就会一直等待,下面的4条消息也就无法继续消费了,

相反,消费者二就不一样了,他有消费完每一条消息之后,都调用了手动ack的回馈,所以可以消费5条消息,都消息完。

以下是实验截图
MQ 的初始状态:
在这里插入图片描述

首先用postman发送请求
在这里插入图片描述

看下图,生产者发送了5条消息,并得到了成功推送到了交换机和队列的回馈
在这里插入图片描述

接下来我们步入正题:看消费者里面,消费者1只是消费了一条,消费者2消费了全部的5条消息;
在这里插入图片描述
结果和我们预想的是一致的;

我们在看看MQ的管理页面来确认
在这里插入图片描述
可以看到,消费者2已经搞完了,而消费者1那边卡住了(消费者一消费了一条,但是在等待回馈,还剩余4条都没被消费,在等待消费)

我在实验的过程中,因为消费者1中的消息堆积了,如果再次发送5条消息到扇形交换机中,那队列1中会积累到9条待消费的,1条等待反馈的,10条总共的,我们可以实验一下子:
在这里插入图片描述
结果和我们预想的一样,那我们如何将这些积压的消息给去掉呢 ?
我自己试出了两种方式,最初试的直接重启服务,这样是无效的,因为进入队列的不被消费会一直在队列里面 。
下面是2种处理方法:
第一种是最直接的方法,直接把确认那行的代码给放开,这样这个消费者1 就会把队列1中积压的那些给消费掉了
第二种 我们将yml中的手动确认配置注释掉,这样就默认是自动确认了,这样我每次从postman中发送5条消息到扇形交换机,分发到两个队列之后,两个消费者都会一直可以消费,因为没消费一个都会自动确认回馈,不用等待了,这样也是可以的

我们实验如下:
实验1:
我们先把消费者1中注释的手动回馈给放开
在这里插入图片描述
可见console中 ,对于积压的消息直接给消费掉了。

实验2:
我们将消费者1中的手动反馈,给继续注释掉,发送2次 postman;

在这里插入图片描述
造成积压
在这里插入图片描述
我把yml中的手动消费者确认,改成自动的,也就是注释掉,可以看到,重启消费者模块后,积压的也被消费了
注释配置:
在这里插入图片描述
重启后,看控制台: 很明显启动后,积压的消息也被消费了,
在这里插入图片描述
在MQ控制台中也可以看到,积压消息被消费啦
在这里插入图片描述

关于手动确认的一些方法
细心的小伙伴可能发现了我们在消费者的catch处写了这样一行代码

channel.basicReject(deliveryTag, false);

以下是解释

一般是有3种确认的,其中1种是正确确认,另外2种是错误确认;

reject:只能否定一条消息
nack:可以否定一条或者多条消息

而错误确认的这两个,都有一个属性
boolean requeue

当它是true的时候,表示重新入队;
当它是false的时候,则表示抛弃掉;

使用拒绝后重新入列这个确认模式要谨慎,因为触发错误确认一般都是出现异常的时候,那么就可能导致死循环,即不断的入队-消费-报错-重新入队…;这将导致消息积压,万一就炸了…

实验错误确认
我们将上述的消费者代码加一行代码;

此处只改动了消费者1,消费者2不变

新增一条抛异常的语句
int num = 1/0;
package com.tubai;import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.stereotype.Component;@Component
public class MyConsumer {@RabbitListener(bindings = {@QueueBinding(value = @Queue("consumer_queue_1"),//绑定交换机exchange = @Exchange(value = "muscle_fanout_exchange", type = "fanout"))})public void consumer1(String msg,Message message, Channel channel) throws Exception {long deliveryTag = message.getMessageProperties().getDeliveryTag();try {System.out.println("消费者1 => " + msg);int num = 1/0;channel.basicAck(deliveryTag, false); //第二个参数,手动确认可以被批处理,当该参数为 true 时} catch (Exception e) {channel.basicReject(deliveryTag, false);e.printStackTrace();}}@RabbitListener(bindings = {@QueueBinding(value = @Queue("consumer_queue_2"),//绑定交换机exchange = @Exchange(value = "muscle_fanout_exchange", type = "fanout"))})public void consumer2(String msg,Message message, Channel channel) throws Exception {long deliveryTag = message.getMessageProperties().getDeliveryTag();try {System.out.println("消费者2 => " + msg);channel.basicAck(deliveryTag, false);} catch (Exception e) {channel.basicReject(deliveryTag, false);e.printStackTrace();}}
}

运行结果
在这里插入图片描述
可以看到我们的消费者1也正常了,因为我们是先打印后确认,因此1~5也会被打印出来;

如果重复入队…那么我们的程序就会死循环了,疯狂打印,各位可以自己试试;但是容易把内存占满O。。

本篇文章书写不易,自己打了好久,大家认可的话,或者开启了新认知,请给个点赞。收藏哦 (#.#) 谢谢大家!
参考文章也写的超级好,大家也可都学习学习,一起进步
Springboot 整合RabbitMq ,用心看完这一篇就够了
RabbitMQ的消息确认机制
SpringBoot集成RabbitMq 手动ACK
RabbitMQ控制界面详解

相关文章:

【RabbitMQ实战】Springboot 整合RabbitMQ组件,多种编码示例,带你实践 看完这一篇就够了

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、对RabbitMQ管理界面深入了解1、在这个界面里面我们可以做些什么&#xff1f; 二、编码练习&#xff08;1&#xff09;使用direct exchange(直连型交换机)&a…...

【你也能从零基础学会网站开发】理解DBMS数据库管理系统架构,从用户到数据到底经历了什么

&#x1f680; 个人主页 极客小俊 ✍&#x1f3fb; 作者简介&#xff1a;程序猿、设计师、技术分享 &#x1f40b; 希望大家多多支持, 我们一起学习和进步&#xff01; &#x1f3c5; 欢迎评论 ❤️点赞&#x1f4ac;评论 &#x1f4c2;收藏 &#x1f4c2;加关注 其实前面我们也…...

Vue.js 中的API接口封装实战与详解

在开发Web应用的过程中&#xff0c;我们常常需要和服务器进行数据交互&#xff0c;这就涉及到了API接口的调用。在Vue.js项目中&#xff0c;为了提高代码复用性、可维护性和降低错误率&#xff0c;我们将API接口进行合理的封装显得尤为重要。本文将详细介绍如何在Vue.js项目中实…...

职场内卷、不稳定、没前景……怎么破?

经济下行期&#xff0c;大家普遍反映混职场艰难。 再深究下&#xff0c;发现造成职场艰难的原因主要有三个&#xff1a; 1.内卷&#xff1a;狼多肉少 2.不稳定&#xff1a;裁员总是不期而遇 3.没前景&#xff1a;明知过几年会被优化&#xff0c;但无法改变&#xff0c;死气沉沉…...

LeetCode 算法:将有序数组转换为二叉搜索树 c++

原题链接&#x1f517;&#xff1a;将有序数组转换为二叉搜索树 难度&#xff1a;简单⭐️ 题目 给你一个整数数组 nums &#xff0c;其中元素已经按 升序 排列&#xff0c;请你将其转换为一棵 平衡 二叉搜索树。 示例 1&#xff1a; 输入&#xff1a;nums [-10,-3,0,5,9]…...

智慧公厕系统改变了人们对服务区公厕的看法

在过去&#xff0c;服务区公厕常常给人留下脏乱差的印象&#xff0c;成为人们在长途旅行途中不愿停留的地方。然而&#xff0c;随着智慧科技的不断发展和应用&#xff0c;智慧公厕系统的出现改变了人们对服务区公厕的看法&#xff0c;为公共卫生设施的提升注入了新的活力。 一、…...

终极指南:RNNS、Transformers 和 Diffusion 模型

一、说明 作为广泛使用这些工具和模型的人&#xff0c;我的目标是解开 RNN、Transformer 和 Diffusion 模型的复杂性和细微差别&#xff0c;为您提供详细的比较&#xff0c;为您的特定需求提供正确的选择。 无论您是在构建语言翻译系统、生成高保真图像&#xff0c;还是处理时间…...

WPF UI 3D 基本概念 点线三角面 相机对象 材质对象与贴图 3D地球 光源 变形处理 动作交互 辅助交互插件 系列三

WPF UI交互专题 平面图形 Path Drawing 绘图 渐变 Brush 矩阵 Transform 变形 阴影效果 模糊效果 自定义灰度去色效果 系列二-CSDN博客 1软件中的3D基本概念 WPF 中 3D 功能的设计初衷并非提供功能齐全的游戏开发平台。 WPF 中的 3D 图形内容封装在 Viewport3D 元素中&#x…...

分子AI预测赛Task2笔记

下面所述比较官方的内容都来自官方文档 ‍‌⁠‌‍​​​‌​​⁠​​​​​&#xfeff;​​​&#xfeff;‍‬​​‍⁠‍‍​​‬​&#xfeff;‌​​​‌‍‬​​​​​​‍‌Task2&#xff1a;赛题深入解析 - 飞书云文档 (feishu.cn) 赛题背景 强调了人工智能在科研领域&…...

剖析DeFi交易产品之UniswapV4:创建池子

本文首发于公众号&#xff1a;Keegan小钢 创建池子的底层函数是 PoolManager 合约的 initialize 函数&#xff0c;其代码实现并不复杂&#xff0c;如下所示&#xff1a; function initialize(PoolKey memory key, uint160 sqrtPriceX96, bytes calldata hookData)externalover…...

速盾:cdn内容分发服务有哪些优势?

CDN&#xff08;Content Delivery Network&#xff09;是指内容分发网络&#xff0c;是一种将网络内容分发到全球各个地点的技术和架构。在现代互联网架构中&#xff0c;CDN已经变得非常重要。CDN通过将内容分发到靠近用户的服务器上&#xff0c;提供高速、高效的服务。下面是C…...

如何利用React和Python构建强大的网络爬虫应用

如何利用React和Python构建强大的网络爬虫应用 引言&#xff1a; 网络爬虫是一种自动化程序&#xff0c;用于通过互联网抓取网页数据。随着互联网的不断发展和数据的爆炸式增长&#xff0c;网络爬虫越来越受欢迎。本文将介绍如何利用React和Python这两种流行的技术&#xff0c…...

炎黄数智人:招商局集团推出AI数字员工“招小影”

引言 在全球数字化浪潮的推动下&#xff0c;招商局集团开启了一项具有里程碑意义的项目。招商局集团将引入AI数字员工“招小影”&#xff0c;这一举措不仅彰显了招商局集团在智能化转型方面的坚定决心&#xff0c;也为企业管理模式的创新注入了新的活力。 “招小影”是一款集成…...

【开发篇】明明配置跨域声明,为什么却仍可以发送HTTP请求

一、问题 在SpringBoot项目中&#xff0c;明确指定仅允许指定网站跨域访问&#xff1a; 为什么开发人员却仍旧可以通过HTTP工具调用接口&#xff1f; 二、为什么 在回答这个问题之前&#xff0c;我们首先要了解一下什么是CORS&#xff01; 1、什么是CORS CORS的全称为跨域资源…...

单片机中有FLASH为啥还需要EEROM?

在开始前刚好我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「单片机的资料从专业入门到高级教程」&#xff0c; 点个关注在评论区回复“888”之后私信回复“888”&#xff0c;全部无偿共享给大家&#xff01;&#xff01;&#xff01; 一是EEPROM操作简单&…...

Qt的源码目录集合(V5.12.12版本)

目录 1.QObject实现源码 2.qml中的ListModel实现源码 3.qml中的JS运行时的环境和数据类型源码 1.QObject实现源码 .\Qt\Qt5.12.12\5.12.12\Src\qtbase\src\corelib\kernel\qobject.h .\Qt\Qt5.12.12\5.12.12\Src\qtbase\src\corelib\kernel\qobject.cpp .\Qt\Qt5.12.12\5…...

记因hive配置文件参数运用不当导致 sqoop MySQL导入数据到hive 失败的案例

sqoop MySQL导入数据到hive报错 ERROR tool.ImportTool: Encountered IOException running import job: java.io.IOException: Hive exited with status 64 报错解释&#xff1a; 这个错误表明Sqoop在尝试导入数据到Hive时遇到了问题&#xff0c;导致Hive进程异常退出。状态码…...

自动化邮件通知:批处理脚本的通讯增强

自动化邮件通知&#xff1a;批处理脚本的通讯增强 引言 批处理脚本在自动化任务中扮演着重要角色&#xff0c;无论是在系统管理、数据处理还是日常任务调度中。然而&#xff0c;批处理脚本的自动化能力可以通过集成邮件通知功能得到显著增强。当脚本执行完毕或在执行过程中遇…...

236、二叉树的最近公共祖先

前提&#xff1a; 所有 Node.val 互不相同 。p ! qp 和 q 均存在于给定的二叉树中。 代码如下&#xff1a; class Solution { public:TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {if (root q || root p || root NULL) return root;TreeN…...

WebStorm 2024 for Mac JavaScript前端开发工具

Mac分享吧 文章目录 效果一、下载软件二、开始安装1、双击运行软件&#xff08;适合自己的M芯片版或Intel芯片版&#xff09;&#xff0c;将其从左侧拖入右侧文件夹中&#xff0c;等待安装完毕2、应用程序显示软件图标&#xff0c;表示安装成功3、打开访达&#xff0c;点击【文…...

【Redis7】零基础篇

1 课程概述 2 Redis入门概述 2.1 是什么 Redis是基于内存的KV键值对内存数据库 Redis&#xff1a;Remote Dictionary Server(远程字典服务)是完全开源的&#xff0c;使用ANSIC语言编写遵守BSD协议&#xff0c;是一个高性能的Key-Value数据库提供了丰富的数据结构&#xff0c…...

[ROS 系列学习教程] 建模与仿真 - 使用 ros_control 控制差速轮式机器人

ROS 系列学习教程(总目录) 本文目录 一、差速轮式机器人二、差速驱动机器人运动学模型三、对外接口3.1 输入接口3.2 输出接口 四、控制器参数五、配置控制器参数六、编写硬件抽象接口七、控制机器人移动八、源码 ros_control 提供了多种控制器&#xff0c;其中 diff_drive_cont…...

Ubuntu22.04使用Systemd设置ROS 2开机自启动遇到的问题

在查找网上的各种开机自启动资料配置好开机自启动后&#xff0c;使用ros2 topic list不能显示话题。 1、问题解决&#xff1a;用户问题与domenID问题2、ROS2开机自启动服务教程3、多个ROS2开机自启动服务教程 1、问题解决&#xff1a;用户问题与domenID问题 在root用户下能看到…...

AI安全研究滞后?清华专家团来支招

在21世纪的科技浪潮中&#xff0c;人工智能&#xff08;AI&#xff09;无疑是最为耀眼的一抹亮色。随着技术的不断突破&#xff0c;AI正以前所未有的速度融入我们的日常生活&#xff0c;重塑着社会、经济乃至人类文明的面貌。然而&#xff0c;在这股汹涌澎湃的发展洪流中&#…...

12寸FAB 信息部内外工作职责的一些划分构思

FAB的信息部&#xff0c;也常被称为IT部门或信息化部门&#xff0c;承担着确保整个制造工厂的信息技术系统高效、安全运行的职责。以下是 一、FAB信息部的一些关键部门职责&#xff1a; 1. 战略规划&#xff1a;制定和实施信息技术战略&#xff0c;以支持FAB的长期业务目标和增…...

css做旋转星球可举一反三

<!DOCTYPE html> <html lang"en"><head> <meta charset"UTF-8" /> <title>旋转的星球</title> <style type"text/css">.box {/*position: relative;*/position: absolute;width: 139px;height: 139p…...

AcWing 1256:扩展二叉树

【题目来源】https://www.acwing.com/problem/content/1258/【题目描述】 由于先序、中序和后序序列中的任一个都不能唯一确定一棵二叉树&#xff0c;所以对二叉树做如下处理&#xff0c;将二叉树的空结点用 补齐&#xff0c;如图所示。 我们把这样处理后的二叉树称为原二叉树…...

三维家:SaaS的IT规模化降本之道|OceanBase 《DB大咖说》(十一)

OceanBase《DB大咖说》第 11 期&#xff0c;我们邀请到了三维家的技术总监庄建超&#xff0c;来分享他对数据库技术的理解&#xff0c;以及典型 SaaS 场景在数据库如何实现规模化降本的经验与体会。 庄建超&#xff0c;身为三维家的技术总监&#xff0c;独挑大梁&#xff0c;负…...

ai智能语音机器人是如何影响客户体验的?电销机器人部署

随着人工智能技术的进步&#xff0c;越来越多的企业在寻求如何将人工智能技术融合到现有的商业模式上&#xff0c;进而实现自动化、智能化。在通信行业大量使用智能语音机器人、聊天机器人、客服机器人时&#xff0c;它能和“客户体验”并驾齐驱吗&#xff0c;还是可以让客户体…...

vue3使用v-html实现文本关键词变色

首先看应用场景 这有一段文本内容&#xff0c;是项目的简介&#xff0c;想要实现将文本中的关键词进行变色处理 有如下关键词 实现思路 遍历文本内容&#xff0c;找到关键词&#xff0c;并使用某种方法更改其字体样式。经过搜寻资料决定采用v-html实现&#xff0c;但是v-h…...

C#面:举列 a=10,b=15,在不用第三方变量的前提下,把a,b的值互换

要在不使用第三方变量的前提下交换a和b的值&#xff0c;可以使用异或运算。异或运算的特性是&#xff0c;对于两个相同的数进行异或运算&#xff0c;结果为0&#xff1b;对于任意数与0进行异或运算&#xff0c;结果为该数本身。因此&#xff0c;可以通过多次异或运算来实现变量…...

编写动态库

1.创建库.c .h文件 2.编写Makefile文件 3.make之后形成.so文件 4.make output,形成mylib 5.把mylib拷贝到test里面 mv mylib /test 6.编译 gcc main.c -I mylib/include -L mylib/lib -lmymethod形成a.out 但是直接执行会出现以下问题 很显然没有找到动态库 7.解决加载找不…...

记一次阿里云服务器java应用无法响应且无法远程连接的问题排查

问题表现 java服务无响应&#xff0c;无法远程链接到服务器。 今天中午12点多&#xff0c;应用直接崩溃。后续进入到服务器&#xff0c;发现java进程都不在了&#xff0c; 排查过程 先安装atop工具 安装、配置并使用atop监控工具 等下次再出现时看相关时间点日志&#xff…...

雷池WAF+Modsecurity安装防护及系统加固

君衍. 一、雷池WAF1、什么是雷池2、什么是WAF3、雷池的功能4、WAF部署架构5、整体检测流程 二、雷池WAF环境依赖1、查看本地CPU架构2、Docker安装2.1 卸载旧版本2.2 安装yum-utils工具包2.3 设置镜像仓库2.4 安装docker2.5 启动docker并查看版本 3、Docker Compose安装3.1 卸载…...

【Python】已解决:SyntaxError: positional argument follows keyword argument

文章目录 一、分析问题背景二、可能出错的原因三、错误代码示例四、正确代码示例五、注意事项 已解决&#xff1a;SyntaxError: positional argument follows keyword argument 一、分析问题背景 在Python编程中&#xff0c;当我们在调用函数时混合使用位置参数&#xff08;p…...

leetcode-20-回溯-切割、子集

一、[131]分割回文串 给定一个字符串 s&#xff0c;将 s 分割成一些子串&#xff0c;使每个子串都是回文串。 返回 s 所有可能的分割方案。 示例: 输入: "aab" 输出: [ ["aa","b"], ["a","a","b"] ] 分析&…...

利用深度学习模型进行语音障碍自动评估

语音的产生涉及器官的复杂协调&#xff0c;因此&#xff0c;语音包含了有关身体各个方面的信息&#xff0c;从认知状态和心理状态到呼吸条件。近十年来&#xff0c;研究者致力于发现和利用语音生物标志物——即与特定疾病相关的语音特征&#xff0c;用于诊断。随着人工智能&…...

TP8 JS(html2canvas) 把DIV内容生成二维码并与背景图、文字组合生成分享海报

方法一&#xff1a;前端JS生成(推荐) 注意&#xff1a; 1.这个网页只能截图图片效果代码&#xff0c;其它任何html效果都不能有&#xff0c;不然截图就不准确 2.如果要生成的图片DIV内容中引用了第三个方的图片&#xff0c;就是不使用同一个域名下的图片&#xff0c;需要把后…...

计算机科学中的接口(Interface)介绍

计算机科学中的接口&#xff08;Interface&#xff09;介绍 计算机科学中&#xff0c;接口是一个广泛的概念&#xff0c;在不同上下文中有不同含义&#xff1a; 1.任何两电路或设备间的连接电路&#xff0c;用于连接CPU与内存、CPU与外设之间。这是一个重要的硬件层面的接口概…...

大创项目推荐 题目:基于深度学习卷积神经网络的花卉识别 - 深度学习 机器视觉

文章目录 0 前言1 项目背景2 花卉识别的基本原理3 算法实现3.1 预处理3.2 特征提取和选择3.3 分类器设计和决策3.4 卷积神经网络基本原理 4 算法实现4.1 花卉图像数据4.2 模块组成 5 项目执行结果6 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 基…...

黑芝麻科技A1000简介

文章目录 1. A1000 简介2. 感知能力评估3. 竞品对比4. 系统软件1. A1000 简介...

详解C语言分支与循环语句

分支语句 if elseswitch 循环语句 whilefordo while goto语句 文章目录 1.什么是语句2.分支语句&#xff08;选择结构&#xff09;2.1 if语句2.1.1 悬空else2.1.3 练习 2.2 switch语句2.2.1 在switch语句中的break2.2.2 default子句 3.循环语句3.1 while循环3.1.1 while语句中…...

Python商务数据分析知识专栏(五)——Python数据分析的应用③使用Pandas进行数据预处理

Python商务数据分析知识专栏&#xff08;五&#xff09;——Python数据分析的应用③使用Pandas进行数据预处理 使用Pandas进行数据预处理1.合并数据2.清洗数据3.标准化数据4.转换数据 使用Pandas进行数据预处理 1.合并数据 2.清洗数据 3.标准化数据 4.转换数据...

Nosql期末复习

mongodb基本常用命令&#xff08;只要掌握所有实验内容就没问题&#xff09; 上机必考&#xff0c;笔试试卷可能考&#xff1a; 1.1 数据库的操作 1.1.1 选择和创建数据库 &#xff08;1&#xff09;use dbname 如果数据库不存在则自动创建&#xff0c;例如&#xff0c;以下…...

Pytest+Allure+Yaml+PyMsql+Jenkins+Gitlab接口自动化(四)Jenkins配置

一、背景 Jenkins&#xff08;本地宿主机搭建&#xff09; 拉取GitLab(服务器)代码到在Jenkins工作空间本地运行并生成Allure测试报告 二、框架改动点 框架主运行程序需要先注释掉运行代码&#xff08;可不改&#xff0c;如果运行报allure找不到就直接注释掉&#xff09; …...

SQL面试题练习 —— 查询前2大和前2小用户并有序拼接

目录 1 题目2 建表语句3 题解 1 题目 有用户账户表&#xff0c;包含年份&#xff0c;用户id和值,请按照年份分组&#xff0c;取出值前两小和前两大对应的用户id&#xff0c;需要保持值最小和最大的用户id排首位。 样例数据 ------------------------- | year | user_id | v…...

Arthas常见使用姿势

文章目录 Arthas常见使用姿势官网基本命令通用参数解释表达式核心变量说明常用命令一些常用特殊案例举例其他技巧关于OGNLOGNL的常见使用OGNL的一些特殊用法与说明OGNL内置的虚拟属性OGNL的个人思考OGNL的杂碎&#xff0c;收集未做验证 Arthas常见使用姿势 官网 https://arth…...

Apache Kylin的入门学习

Apache Kylin的入门学习可以从以下几个方面进行&#xff1a; 1. 了解Kylin的基本概念 定义&#xff1a;Apache Kylin是一个开源的分布式分析引擎&#xff0c;它基于Hadoop和HBase构建&#xff0c;提供Hadoop/Spark之上的SQL查询接口及多维分析&#xff08;OLAP&#xff09;能…...

React@16.x(46)路由v5.x(11)源码(3)- 实现 Router

目录 1&#xff0c;Router 的结构2&#xff0c;实现2.1&#xff0c;react-router1&#xff0c;matchPath.js2&#xff0c;Router.js3&#xff0c;RouterContext.jsx4&#xff0c;index.jsx 2.2&#xff0c;react-router-domBrowserRouter.jsxindex.jsx 1&#xff0c;Router 的结…...

openGauss真的比PostgreSQL差了10年?

前不久写了MogDB针对PostgreSQL的兼容性文章&#xff0c;我在文中提到针对PostgreSQL而言&#xff0c;MogDB兼容性还是不错的&#xff0c;其中也给出了其中一个能源客户之前POC的迁移报告数据。 But很快我发现总有人回留言喷我&#xff0c;而且我发现每次喷的这帮人是根本不看文…...

Android12 MultiMedia框架之MediaExtractorService

上节学到setDataSource()时会创建各种Source&#xff0c;source用来读取音视频源文件&#xff0c;读取到之后需要demux出音、视频、字幕数据流&#xff0c;然后再送去解码。那么负责进行demux功能的media extractor模块是在什么时候阶段创建的&#xff1f;这里暂时不考虑APP创建…...

【Unity 3D角色移动】

【Unity 3D角色移动】 在Unity 3D中实现角色移动通常涉及到几个关键步骤&#xff0c;包括设置角色的物理属性、处理输入、更新角色的位置以及动画同步。下面是实现基本3D角色移动的步骤和示例代码&#xff1a; 步骤1&#xff1a;设置角色的物理属性 角色通常使用Character Co…...

搜维尔科技:使用Manus VR手套和ART光学追踪功能实现虚拟场景工业仿真操作

&#xff1a;使用Manus VR手套和ART光学追踪功能实现虚拟场景工业仿真操作 搜维尔科技&#xff1a;使用Manus VR手套和ART光学追踪功能实现虚拟场景工业仿真操作...

python学习2-数据结构与算法-链表

单链表是一种链式存取的数据结构&#xff0c;用一组地址任意的存储单元存放线性表中的数据元素。链表中的数据是以结点来表示的&#xff0c;每个结点的构成&#xff1a;元素(数据元素的映象) 指针(指示后继元素存储位置)&#xff0c;元素就是存储数据的存储单元&#xff0c;指…...

Android TV跨平台开发心得

这半年来陆陆续续做了一堆poc&#xff0c;刚开始是flutter&#xff0c;结果领导叫停了&#xff0c;说有其他部门做一样的事&#xff0c;真不巧&#xff1b;后来是react native&#xff0c;开发了个demo&#xff0c;上报上去了已经&#xff1b;现在又要做android nativewebview …...

震惊!张宇25版高数18讲发布,656页惹争议!

这个张宇老师在微博已经解释过了&#xff01; 我觉得张宇老师本意是好的&#xff0c;在考研数学教学创新这方面&#xff0c;他真的有自己的思考。 他为什么要这么做&#xff1f; 其实作为一个考研高数老师&#xff0c;他完全可以像其他老师一样&#xff0c;什么都不做&#x…...

24款捷豹XFL来袭,优惠超12万,车长5102mm,配三种动力可选

相比德系BBA对手,捷豹虽然拥有纯正的英伦运动血统,但捷豹车型在国内仍属偏小众的群体,为了进一步提升市占表现,捷豹也只能通过大幅优惠的方式吸引消费者。最近,2024款捷豹XFL正式上市,新车虽然拥有39.99—48.79万的指导价,但新车亮相后便进行了破10万的优惠政策,目前入…...

采用Java+ SpringBoot+ IntelliJ+idea开发的ADR药物不良反应监测系统源码

采用Java SpringBoot IntelliJidea开发的ADR药物不良反应监测系统源码 ADR药物不良反应监测系统有哪些应用场景&#xff1f; ADR药物不良反应监测系统有哪些应用场景&#xff1f; ADR药物不良反应监测系统具有广泛的应用场景&#xff0c;以下是一些主要的应用场景&#xff1a…...

LPDDR6带宽预计将翻倍增长:应对低功耗挑战与AI时代能源需求激增

在当前科技发展的背景下&#xff0c;低能耗问题成为了业界关注的焦点。国际能源署(IEA)近期报告显示&#xff0c;日常的数字活动对电力消耗产生显著影响——每次Google搜索平均消耗0.3瓦时&#xff08;Wh&#xff09;&#xff0c;而向OpenAI的ChatGPT提出的每一次请求则消耗2.9…...

【网络安全】新的恶意软件:无文件恶意软件GhostHook正在广泛传播

文章目录 推荐阅读 一种新的恶意软件 GhostHook v1.0 正在一个网络犯罪论坛上迅速传播。这种创新的无文件浏览器恶意软件由 Native-One 黑客组织开发&#xff0c;具有独特的分发方式和多功能性&#xff0c;对各种平台和浏览器构成重大威胁。 GhostHook v1.0 支持 Windows、Andr…...

Java中的死锁及其避免策略

一、技术难点&#xff1a; 在Java中&#xff0c;死锁是一个常见的并发问题&#xff0c;它指的是两个或更多的线程无限期地等待一个资源&#xff0c;而这些资源又被其他等待线程所持有。死锁通常发生在多个线程互相等待对方释放资源时&#xff0c;形成一个循环等待的条件。技术…...

时间|基于SprinBoot+vue的时间管理系统(源码+数据库+文档)

时间管理系统 目录 基于SprinBootvue的时间管理系统 一、前言 二、系统设计 三、系统功能设计 1管理员功能模块 2用户功能模块 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 博主介绍&#xff1a;✌️大厂码农…...