谷粒商城篇章9 ---- P248-P261/P292-P294 ---- 消息队列【分布式高级篇六】
目录
1 消息队列(Message Queue)简介
1.1 概述
1.2 消息服务中两个重要概念
1.3 消息队列主要有两种形式的目的地
1.4 JMS和AMQP对比
1.5 应用场景
1.6 Spring支持
1.7 SpringBoot自动配置
1.7 市面上的MQ产品
2 RabbitMQ
2.1 RabbitMQ简介
2.1.1 RabbitMQ简介
2.1.2 核心概念
2.2 docker安装RabbitMQ
2.3 RabbitMQ管理操作页面介绍
2.3.1 Overview概述
2.3.2 Connections连接信息
2.3.3 Channels信道信息
2.3.4 Exchanges交换机信息
2.3.5 Queues队列信息
2.3.6 Admin用户信息
2.4 RabbitMQ运行机制
2.5 交换机(Exchange)类型
2.5.1 Direct Exchange(直连式)
2.5.2 Fanout Exchange(扇出/广播式)
2.5.3 Topic Exchange(主题/发布订阅式)
2.6 收发消息测试
2.6.1 exchange.direct发送消息
2.6.1.1 exchange.direct绑定队列
2.6.1.2 exchange.direct发送消息
2.6.2 exchange.fanout发送消息
2.6.2.1 exchange.fanout绑定队列
2.6.2.2 exchange.fanout发送消息
2.6.3 exchange.topic发送消息
2.6.3.1 exchange.topic绑定队列
2.6.3.2 exchange.topic发送消息
3 SpringBoot整合RabbitMQ
3.1 引入依赖
3.2 yml配置
3.3 开启RabbitMQ
3.4 整合测试
3.4.1 AmqpAdmin使用
3.4.1.1 创建交换机
3.4.1.2 创建队列
3.4.1.3 创建绑定关系
3.4.2 RabbitTemplate使用
3.4.2.1 发送消息
3.4.2.1.1 使用json序列化对象
3.4.3 RabbitListener & RabbitHandler接收消息
3.4.3.1 RabbitListener监听接收消息
3.4.3.1.1 简单接收消息
3.4.3.1.2 接收消息内容并反序列化
3.4.3.1.3 完整参数写法
3.4.3.1.4 验证多个消费者监听场景
3.4.3.2 RabbitListener和RabbitHandler监听接收消息
3.4.3.2.1 @RabbitListener和@RabbitHandler的区别与作用
3.4.3.2.2 测试重载处理不同类型的消息
3.5 消息可靠抵达
3.5.1 发送端确认
3.5.1.1 ConfirmCallback(确认模式)
3.5.1.2 ReturnCallback(回退模式)
3.5.1.3 消息唯一id
3.5.2 消费端确认(Ack确认机制)
3.5.2.1 消费端自动确认
3.5.2.2 消费端手动确认
3.5.2.3 消费端退货
4 RabbitMQ延时队列(实现定时任务)
4.1 为什么不使用定时任务?
4.2 使用场景
4.3 消息的存活时间TTL(Time To Live)
4.4 DXL和死信队列
4.5 延时队列实现
4.5.1 设置队列过期时间实现延时队列
4.5.2 设置消息过期时间实现延时队列
4.6 延时队列定时关单模拟
4.6.1 实现方式
4.6.1.1 基础版
4.6.1.2 升级版
4.6.2 实现
4.6.2.1 创建Queue、Exchange、Binding方式
4.6.2.2 发送消息相关代码
4.6.2.3 监听接收消息相关代码
4.6.3 测试
1 消息队列(Message Queue)简介
1.1 概述
大多应用,可以通过消息服务中间件来提升系统异步通信和扩展解耦能力。
1.2 消息服务中两个重要概念
消息代理(message broker)和目的地(destination)。
1.3 消息队列主要有两种形式的目的地
- 队列(queue):点对点消息通信(point-to-point);
- 主题(topic):发布(publish)/订阅(subscribe)消息通信。
点对点模式和发布订阅模式区别:
(1)点对点式:
- 消息发送者发送消息,消息代理将其放入一个队列中,消息接收者从队列中获取消息内容,消息读取后被移除队列。
- 消息只有唯一的发送者和接受者(只能有一个接收者读取信息) ,但不是说只有一个接收者。
(2)发布订阅式:
- 发送者(发布者)发送消息到主题,多个接收者(订阅者)监听(订阅)这个主题,那么就会在消息到达时同时收到消息。
1.4 JMS和AMQP对比
(1)JMS
- 是Java消息服务的规范
- 基于jvm消息代理的规范
- 用@JmsListener监听消息
- JMS是java提供的一套消息服务API标准,其目的是为所有的java应用程序提供统一的消息通信的标准,类似java的 jdbc,只要遵循jms标准的应用程序之间都可以进行消息通信。
- ActiveMQ、HornetMQ是JMS实现的。
(2)AMQP(Advacnce Message Queuing Protocol)
- 高级消息队列协议,是一个消息代理的规范,兼容JMS
- RabbitMQ 是AMQP的实现
- 用@RabbitListener(AMQP)监听消息
- 它和AMQP有什么 不同,jms是java语言专属的消息服务标准,它是在api层定义标准,并且只能用于java应用;而AMQP是在协议层定义的标准,是跨语言的 。
- RabbitMQ是AMQP的实现。
1.5 应用场景
在实际应用中常用的场景:异步处理、应用解耦、流量削峰和消息通讯四个场景。
1.6 Spring支持
- spring-jms提供了对JMS的支持
- spring-rabbit提供了对AMQP的支持
- 需要ConnectionFactory的实现来连接消息代理
- 提供JmsTemplate、RabbitTemplate来发送消息
- JmsListener(JMS).@RabbitListener(AMQP)注解在方法上监听消息代理发布的消息
- EnableJms、@EnableRabbit开启支持
1.7 SpringBoot自动配置
- JmsAutoConfiguration
- RabbitAutoConfiguration
1.7 市面上的MQ产品
ActiveMQ、RabbitMQ、RocketMQ、Kafka。
2 RabbitMQ
2.1 RabbitMQ简介
RabbitMQ官方文档:Networking and RabbitMQ — RabbitMQ
2.1.1 RabbitMQ简介
RabbitMQ是一个由erlang开发的AMQP(Advanced Message Queue Protocol)。
2.1.2 核心概念
- Message:消息,消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头是由一些可选属性组成,这些属性包含routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。
- Publisher:生产者,也是一个向交换机发布消息的客户端应用程序。
- Exchange:交换机,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。Exchange有4种类型:direct(默认)、 fanout、topic、headers,不同类型的Exchange转发消息的策略有所区别。
- Queue:消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直 在队列里面,等待消费者连接到这个队列将其取走。
- Binding:绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交 换器理解成一个由绑定构成的路由表。Exchange 和Queue的绑定可以是多对多的关系。
- Connection:网络连接,比如一个TCP连接。
- Channel:信道多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内的虚拟连接,AMQP 命令都是通过信道 发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁 TCP 都 是非常昂贵的开销,所以引入了信道的概念,以复用一条 TCP 连接。
- Consumer:信息的消费者,表示一个从消息队列中取得消息的客户端应用程序。
- Virtual Host:虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加 密环境的独立服务器域。每个 vhost 本质上就是一个 mini 版的 RabbitMQ 服务器,拥 有自己的队列、交换器、绑定和权限机制。vhost 是 AMQP 概念的基础,必须在连接时 指定,RabbitMQ 默认的 vhost 是 / 。
- Broker:表示消息队列服务器实体。
拓展: RabbitMQ为什么使用信道而不直接使用TCP连接通信?
原因:TCP连接的创建和销毁开销特别大。创建需要 3 次握手,销毁需要 4 次挥手。高峰时每秒成千上万条TCP连接的创建会照成资源巨大浪费。而且操作系统每秒处理TCP连接数也是有限制的,会造成性能瓶颈。而如果一条线程使用一条信道,一条TCP连接可以容纳无限的信道,即使每秒成千上万的请求也不会照成性能瓶颈。
2.2 docker安装RabbitMQ
# 下载影像
docker pull rabbitmq:3.9.11-management# 安装
docker run -d --name=rabbitmq --restart=always -p 5671:5671 -p 5672:5672 -p 4369:4369 -p 15671:15671 -p 15672:15672 -p 25672:25672 rabbitmq:3.9.11-management
-
4369、25672:Erlang发现和集群端口
-
5671、5672:AMQP端口
-
15672:web管理后台端口
-
61613、61614:STOMP协议端口
-
1883、8883:MQTT协议接口
rabbitmq的虚拟主机以路径来区分,例如:/、/a、/b。虚拟主机之间是相互隔离的。
2.3 RabbitMQ管理操作页面介绍
2.3.1 Overview概述
2.3.2 Connections连接信息
2.3.3 Channels信道信息
2.3.4 Exchanges交换机信息
2.3.5 Queues队列信息
2.3.6 Admin用户信息
2.4 RabbitMQ运行机制
AMQP中的消息路由
- AMQP中消息的路由过程和Java开发者熟悉的JMS存在一些差别,AMQP中增加了Exchange和Binding的角色。生产者把消息发布到Exchange上,消息最终到达队列并被消费者接收,Binding决定交换机将消息发送到哪个队列。
2.5 交换机(Exchange)类型
Exchange分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct、fanout、topic、headers 。headers 匹配 AMQP 消息的 header 而不是路由键,headers 交换器和 direct 交换器完全一致,但性能差很多,目前几乎用不到了,所以直接看另外三种类型:
2.5.1 Direct Exchange(直连式)
消息中的路由键(routing key)如果和Binding 中的 binding key 一致, 交换器就将消息发到对应的队列中。路由键与队列名完全匹配,如果一个队列绑定到交换机要求路由键为“dog”,则只转发 routingkey 标记为“dog”的消息,不会转发“dog.puppy”,也不会转发“dog.guard”等等。它是完全匹配、单播的模式。
2.5.2 Fanout Exchange(扇出/广播式)
每个发到 fanout 类型交换器的消息都会分到所有绑定的队列上去。fanout 交换器不处理路由键,只是简单的将队列绑定到交换器上,每个发送到交换器的消息都会被转发到与该交换器绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。fanout 类型转发消息是最快的。
2.5.3 Topic Exchange(主题/发布订阅式)
topic 交换器通过模式匹配分配消息的路由键属性,将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上。它将路由键和绑定键的字符串切分成单词,这些单词之间用点隔开。它同样也会识别两个通配符:符号“#”和符号“*”。#匹配0个或多个单词,*匹配一个单词。
2.6 收发消息测试
按照如下图,新建交换机、队列进行测试。
(1)新建交换机,如下:
(2)新建队列,如下:
2.6.1 exchange.direct发送消息
2.6.1.1 exchange.direct绑定队列
2.6.1.2 exchange.direct发送消息
只有atguigu队列拿到消息,直连型路由键需要完全匹配。
Ask Mode:
1. Nack message requeue true:接收消息但不做确认,消息会重新加入队列;
2. Automatic ack:获取消息,应答确认,消息不重新入队,将会从队列中删除;
3. Reject requeue true:拒绝消息,消息会重新加入队列;
4. Reject requeue false:拒绝消息,消息会被从队列中删除。
2.6.2 exchange.fanout发送消息
2.6.2.1 exchange.fanout绑定队列
2.6.2.2 exchange.fanout发送消息
所有与exchange.fanout交换机绑定的队列都会收到消息,这就是扇出型交换机。
2.6.3 exchange.topic发送消息
2.6.3.1 exchange.topic绑定队列
2.6.3.2 exchange.topic发送消息
(1)
(2)
atguigu、atguigu.emps、atguigu.news这三个队列收到了消息。“#”匹配0个或多个单词。
3 SpringBoot整合RabbitMQ
3.1 引入依赖
gulimall-order/pom.xml
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
3.2 yml配置
gulimall-order/src/main/resources/application.yml
spring:rabbitmq:host: 172.1.11.10port: 5672virtual-host: /
3.3 开启RabbitMQ
相关注解 @EnableRabbit,监听消息必须使用,收发消息可以不使用该注解。
gulimall-order/src/main/java/com/wen/gulimall/order/GulimallOrderApplication.java
@EnableRabbit
@SpringBootApplication
public class GulimallOrderApplication {public static void main(String[] args) {SpringApplication.run(GulimallOrderApplication.class, args);}}
3.4 整合测试
3.4.1 AmqpAdmin使用
3.4.1.1 创建交换机
(1)交换机类型
(2)创建交换机
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.core.AmqpAdmin;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.boot.test.context.SpringBootTest;import javax.annotation.Resource;@Slf4j
@SpringBootTest
class GulimallOrderApplicationTests {@Resourceprivate AmqpAdmin amqpAdmin;@Testvoid createExchange(){// 声明交换机/*** DirectExchange(String name, 【交换机名称】* boolean durable, 【是否持久化】* boolean autoDelete, 【是否自动删除】* Map<String, Object> arguments) 【自定义参数】*/DirectExchange directExchange = new DirectExchange("hello-java-exchange",true,false);amqpAdmin.declareExchange(directExchange);log.info("Exchange[{}]创建成功","hello-java-exchange");}
}
3.4.1.2 创建队列
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.core.AmqpAdmin;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.boot.test.context.SpringBootTest;import javax.annotation.Resource;@Slf4j
@SpringBootTest
class GulimallOrderApplicationTests {@Resourceprivate AmqpAdmin amqpAdmin;@Testvoid createQueue(){// 创建队列/*** Queue(String name, 【队列名称】* boolean durable, 【是否持久化】* boolean exclusive, 【是否排他(只能被一个consumer连接占用)】* boolean autoDelete, 【是否自动删除】* @Nullable Map<String, Object> arguments) 【自定义参数】*/Queue queue = new Queue("hello-java-queue",true,false,false);amqpAdmin.declareQueue(queue);log.info("Queue[{}]创建成功","hello-java-queue");}
}
3.4.1.3 创建绑定关系
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.core.AmqpAdmin;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.boot.test.context.SpringBootTest;import javax.annotation.Resource;@Slf4j
@SpringBootTest
class GulimallOrderApplicationTests {@Resourceprivate AmqpAdmin amqpAdmin;@Testpublic void createBinding(){// 创建绑定/*** Binding(String destination, 【目的地,队列name或交换机name】* Binding.DestinationType destinationType, 【目的地类型,queue还是exchange(路由)】* String exchange, 【交换机】* String routingKey, 【路由键】* @Nullable Map<String, Object> arguments) 【自定义参数】** 将exchange指定交换机和destination目的地进行绑定,使用routingKey作为路由键*/Binding binding = new Binding("hello-java-queue",Binding.DestinationType.QUEUE,"hello-java-exchange","hello-java",null);amqpAdmin.declareBinding(binding);log.info("Binding[{}]创建成功","hello-java");}
}
3.4.2 RabbitTemplate使用
3.4.2.1 发送消息
如果发送的消息是个对象,我们会使用序列化机制,将对象写出。对象必须实现Serializable接口,例如:
@Slf4j
@SpringBootTest
class GulimallOrderApplicationTests {@Resourceprivate RabbitTemplate rabbitTemplate;@Testvoid sendMessage(){/*** convertAndSend(String exchange, 【交换机】* String routingKey, 【路由键】* Object object) 【发送的信息】*/String msg = "hjhajkxdhjashj哈哈哈哈哈";OrderReturnReasonEntity entity = new OrderReturnReasonEntity();entity.setCreateTime(new Date());entity.setId(2L);entity.setName("多多");//1. 发送消息,如果发送的消息是个对象,我们会使用序列化机制,将对象写出。对象必须实现SerializablerabbitTemplate.convertAndSend("hello-java-exchange","hello-java",entity);log.info("发送消息【{}】成功",entity);}
}
队列收到的消息,如下:
3.4.2.1.1 使用json序列化对象
通过源码分析,如果容器中没有MessageConverter默认使用使用SimpleMessageConverter
这里自定义RabbitMQ配置使用Jackson2JsonMessageConverter 消息转换器
gulimall-order/src/main/java/com/wen/gulimall/order/config/MyRabbitConfig.java
@Configuration
public class MyRabbitConfig {@Beanpublic MessageConverter messageConverter(){return new Jackson2JsonMessageConverter();}
}
重新发送消息测试:
@Slf4j
@SpringBootTest
class GulimallOrderApplicationTests {@Resourceprivate RabbitTemplate rabbitTemplate;@Testvoid sendMessage(){/*** convertAndSend(String exchange, 【交换机】* String routingKey, 【路由键】* Object object) 【发送的信息】*/String msg = "hjhajkxdhjashj哈哈哈哈哈";OrderReturnReasonEntity entity = new OrderReturnReasonEntity();entity.setCreateTime(new Date());entity.setId(2L);entity.setName("多多");//1. 发送消息,如果发送的消息是个对象,我们会使用序列化机制,将对象写出。对象必须实现SerializablerabbitTemplate.convertAndSend("hello-java-exchange","hello-java",entity);log.info("发送消息【{}】成功",entity);}
}
测试结果如下:
3.4.3 RabbitListener & RabbitHandler接收消息
3.4.3.1 RabbitListener监听接收消息
监听消息使用注解@RabbitListener,该注解标注在方法上,使用该注解的前提需要主类上使用注解@EnableRabbit。
3.4.3.1.1 简单接收消息
使用注解@RabbitListener,并通过属性queues声明需要监听的所有队列。
gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java
@RabbitListener(queues = {"hello-java-queue"})
public void receiveMessage(Object msg){System.out.println("接收到消息...内容:"+msg+"==>类型:"+msg.getClass());
}
监听结果:
接收到消息...内容:(Body:'[B@17ee5a8a(byte[77])' MessageProperties [headers={__TypeId__=com.wen.gulimall.order.entity.OrderReturnReasonEntity}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=hello-java-exchange, receivedRoutingKey=hello-java, deliveryTag=1, consumerTag=amq.ctag-K2kLpChLRK15TemEJ4XRBQ, consumerQueue=hello-java-queue])==>类型:class org.springframework.amqp.core.Message
3.4.3.1.2 接收消息内容并反序列化
由上一步接收到的消息内容可知消息的类型是org.springframework.amqp.core.Message,将需要反序列化的类放在第二个参数的位置可以将消息内容自动封装成对象。(消息接收对象类型要和消息发送对象类型保持一致)
第一个参数:Message -- 原生消息详细信息,消息头+消息体。
第二个参数:T<发送消息的类型>
@RabbitListener(queues = {"hello-java-queue"})
public void receiveMessage2(Message msg, OrderReturnReasonEntity content){// 消息体byte[] body = msg.getBody();// 消息头信息MessageProperties messageProperties = msg.getMessageProperties();System.out.println("接收到消息...:"+msg+"==>内容:"+content);
}
监听结果:
接收到消息...:(Body:'[B@2ec3c5a5(byte[77])' MessageProperties [headers={__TypeId__=com.wen.gulimall.order.entity.OrderReturnReasonEntity}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=hello-java-exchange, receivedRoutingKey=hello-java, deliveryTag=1, consumerTag=amq.ctag-vuaHxsngkQHP8vWIUy6oGA, consumerQueue=hello-java-queue])==>内容:OrderReturnReasonEntity(id=2, name=多多, sort=null, status=null, createTime=Thu Jan 25 14:32:30 CST 2024)
3.4.3.1.3 完整参数写法
参数类型:
1) Message message:原生消息详细信息,消息头+消息体。
2)T<发送消息的类型> :OrderReturnReasonEntity content。
3) Channel channel:当前传输数据的通道。(一个连接可以容纳多个通道)
@RabbitListener(queues = {"hello-java-queue"})
public void receiveMessage3(Message msg,OrderReturnReasonEntity content,Channel channel){log.info("信道Channel:{}",channel);// 消息体byte[] body = msg.getBody();// 消息头信息MessageProperties messageProperties = msg.getMessageProperties();System.out.println("接收到消息...:"+msg+"==>内容:"+content);
}
监听结果:
3.4.3.1.4 验证多个消费者监听场景
Queue可以由多个消费者监听,只能有一个消费者接收到消息。只要收到消息,队列删除该消息。
场景:
1)启动多个订单服务,同一个消息,只有一个客户端接收到;
2)只有一个消息处理完,方法运行结束,才可以接收到下一个消息。
模拟发送10条消息。
gulimall-order/src/test/java/com/wen/gulimall/order/GulimallOrderApplicationTests.java
@Slf4j
@SpringBootTest
class GulimallOrderApplicationTests {@Resourceprivate AmqpAdmin amqpAdmin;@Resourceprivate RabbitTemplate rabbitTemplate;@Testvoid sendMessage(){for(int i=0;i<10;i++) {OrderReturnReasonEntity entity = new OrderReturnReasonEntity();entity.setCreateTime(new Date());entity.setId(2L);entity.setName("多多"+i);//1. 发送消息,如果发送的消息是个对象,我们会使用序列化机制,将对象写出。对象必须实现SerializablerabbitTemplate.convertAndSend("hello-java-exchange", "hello-java", entity);log.info("发送消息【{}】成功", entity);}}
}
复制一个订单模块,并启用。
监听结果:
每个客户端都是接收同一个队列,一条消息只能由一个客户端接收,没有重复消息。
在此期间有两个客户端有两个连接,一个客户端只能有一个连接,一个连接可以有多个信道(channel)。
3.4.3.2 RabbitListener和RabbitHandler监听接收消息
3.4.3.2.1 @RabbitListener和@RabbitHandler的区别与作用
- @RabbitListener:标注在类或方法上。【作用:监听从哪些队列接收消息】
- @RabbitHandler:标注在方法上。【作用:重载区分不同类型的消息】
3.4.3.2.2 测试重载处理不同类型的消息
发送消息接口,发送两种不同类型的消息
gulimall-order/src/main/java/com/wen/gulimall/order/controller/RabbitController.java
@RestController
public class RabbitController {@Resourceprivate RabbitTemplate rabbitTemplate;@GetMapping("/sendMq")public String sendMq(Integer num){for(int i=0;i<num;i++){if(i%2==0){OrderReturnReasonEntity entity = new OrderReturnReasonEntity();entity.setCreateTime(new Date());entity.setId(2L);entity.setName("多多"+i);//1. 发送消息,如果发送的消息是个对象,我们会使用序列化机制,将对象写出。对象必须实现SerializablerabbitTemplate.convertAndSend("hello-java-exchange", "hello-java", entity);}else {OrderEntity orderEntity = new OrderEntity();orderEntity.setOrderSn(UUID.randomUUID().toString());rabbitTemplate.convertAndSend("hello-java-exchange", "hello-java", orderEntity);}}return "ok";}
}
重载方法监听接收不同类型的消息
gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java
@RabbitListener(queues = {"hello-java-queue"})
@Slf4j
@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {//@RabbitListener(queues = {"hello-java-queue"})@RabbitHandlerpublic void receiveMessage3(Message msg,OrderReturnReasonEntity content,Channel channel) throws InterruptedException {//log.info("信道Channel:{}",channel);log.info("接收到消息..."+content);// 消息体//byte[] body = msg.getBody();消息头信息//MessageProperties messageProperties = msg.getMessageProperties();Thread.sleep(3000);//System.out.println("消息处理完成=>"+content.getName());}@RabbitHandlerpublic void receiveMessage3(Message msg,OrderEntity content,Channel channel) {log.info("接收到消息..."+content);}
}
重启订单服务,请求http://localhost:9000/sendMq?num=10发送消息测试,通过@RabbitListener(queues={"hello-java-queue"})声明队列,@RabbitHandler重载用于接收同一个队列不同类型的数据。
注意:接收同一个队列不同类型的数据并自动封装成相应的对象,单使用@RabbitListener是无法实现的,需要@RabbitListener和@RabbitHandler搭配使用。如上。
测试结果:
3.5 消息可靠抵达
官网参考文档:https://www.rabbitmq.com/=> Docs => Server Documentation => Reliable Delivery
1. 为什么不适用事务消息:保证消息不丢失,可靠抵达,因为使用事务消息会使性能下降250倍,为此引入确认机制。
2. 发送端确认
- publisher:confirmCallback 确认模式;
- publisher:returnCallback 未投递到 queue 退回模式。
3. 消费端确认
- consumer:ack机制。
3.5.1 发送端确认
https://www.rabbitmq.com/ => Docs => Server Documentation => Reliable Delivery =>
Acknowledgements and Confirms => Publisher confirms
3.5.1.1 ConfirmCallback(确认模式)
1. 开启 p->e 的发送确认配置,我这里使用的是SpringBoot2.7.8之前的配置已经废弃,这里使用spring.rabbitmq.publisher-confirm-type=correlated替换spring.rabbitmq.publisher-confirms=true
2. 发送端确认类型spring.rabbitmq.publisher-confirm-type有3种,如下
- none值:是禁用发布确认模式,是默认值。
- correlated值:是发布消息成功到交换器后会触发回调方法。
- simple值:经测试有两种效果,其一效果和CORRELATED值一样会触发回调方法,其二在发布消息成功后使用rabbitTemplate调用waitForConfirms或waitForConfirmsOrDie方法等待broker节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是waitForConfirmsOrDie方法如果返回false则会关闭channel,则接下来无法发送消息到broker。
开启配置
gulimall-order/src/main/resources/application.yml
spring:rabbitmq:host: 172.xx.xx.xxport: 5672virtual-host: /publisher-confirm-type: correlated
自定义RabbitTemplate,设置确认回调
gulimall-order/src/main/java/com/wen/gulimall/order/config/MyRabbitConfig.java
@Configuration
public class MyRabbitConfig {@Resourceprivate RabbitTemplate rabbitTemplate;@Beanpublic MessageConverter messageConverter(){return new Jackson2JsonMessageConverter();}/*** 定制RabbitTemplate* 1.服务收到消息就回调* 1)spring.rabbitmq.publisher-confirms=true(这里使用spring.rabbitmq.publisher-confirm-type=correlated)* 2)设置确认回调* 注意:springboot rabbitmq 中 spring.rabbitmq.publisher-confirms 已经失效。需要使用 spring.rabbitmq.publisher-confirm-type 替代* publisher-confirm-type有3种:* 1)NONE值是禁用发布确认模式,是默认值* 2)CORRELATED值是发布消息成功到交换器后会触发回调方法* 3)SIMPLE值经测试有两种效果,其一效果和CORRELATED值一样会触发回调方法,其二在发布消息成功后使用rabbitTemplate调用waitForConfirms或waitForConfirmsOrDie方法等待broker节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是waitForConfirmsOrDie方法如果返回false则会关闭channel,则接下来无法发送消息到broker;*/@PostConstruct //MyRabbitConfig对象创建完成之后,执行这个方法public void initRabbitTemplate(){// 设置确认回调rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {/*** 只要消息抵达Broker就ack=true* @param correlationData 当前消息的唯一关联数据(这个是消息的唯一id)* @param b (ack) 消息是否成功收到* @param s (cause) 失败的原因*/@Overridepublic void confirm(CorrelationData correlationData, boolean b, String s) {System.out.println("confirm...correlationData["+correlationData+"]==>ack["+b+"]==>cause["+s+"]");}});}
}
测试
请求http://localhost:9000/sendMq?num=10 ,看测试结果,只要消息抵达Broker就ack=true,如下:
3.5.1.2 ReturnCallback(回退模式)
开启 e->q 的确认配置,消息不能由交换机投递到目标队列将调用returnCallback。
gulimall-order/src/main/resources/application.yml
server:port: 9000
spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://172.xx.xx.xx:9906/gulimall_omsusername: rootpassword: rootrabbitmq:host: 172.xx.xx.xxport: 5672virtual-host: /# 开启发送端确认publisher-confirm-type: correlated# 开启发送端消息抵达队列的确认,默认是falsepublisher-returns: true# 只要消息抵达队列,以异步发送优先回调我们的returnConfirmtemplate:mandatory: trueredis:host: 172.xx.xx.xxcloud:nacos:discovery:server-addr: 172.xx.xx.xx:8848main:allow-circular-references: true
mybatis-plus:mapper-locations: classpath:/mapper/**/*.xmlglobal-config: # 全局配置db-config:id-type: auto # 主键自增
设置消息抵达队列的确认回调
@Configuration
public class MyRabbitConfig {@Resourceprivate RabbitTemplate rabbitTemplate;@Beanpublic MessageConverter messageConverter(){return new Jackson2JsonMessageConverter();}/*** 定制RabbitTemplate* 1.服务收到消息就回调* 1)spring.rabbitmq.publisher-confirms=true(这里使用spring.rabbitmq.publisher-confirm-type=correlated)* 2)设置确认回调 ConfirmCallback* 2. 消息抵达队列就回调* 1)spring.rabbitmq.publisher-returns=true* spring.rabbitmq.template.mandatory=true* 2)设置消息抵达队列的确认回调 ReturnCallback* 注意:springboot rabbitmq 中 spring.rabbitmq.publisher-confirms 已经失效。需要使用 spring.rabbitmq.publisher-confirm-type 替代* publisher-confirm-type有3种:* 1)NONE值是禁用发布确认模式,是默认值* 2)CORRELATED值是发布消息成功到交换器后会触发回调方法* 3)SIMPLE值经测试有两种效果,其一效果和CORRELATED值一样会触发回调方法,其二在发布消息成功后使用rabbitTemplate调用waitForConfirms或waitForConfirmsOrDie方法等待broker节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是waitForConfirmsOrDie方法如果返回false则会关闭channel,则接下来无法发送消息到broker;*/@PostConstruct //MyRabbitConfig对象创建完成之后,执行这个方法public void initRabbitTemplate(){// 设置确认回调rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {/*** 只要消息抵达Broker就ack=true* @param correlationData 当前消息的唯一关联数据(这个是消息的唯一id)* @param b (ack) 消息是否成功收到* @param s (cause) 失败的原因*/@Overridepublic void confirm(CorrelationData correlationData, boolean b, String s) {System.out.println("confirm...correlationData["+correlationData+"]==>ack["+b+"]==>cause["+s+"]");}});// 设置消息抵达队列的确认回调rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {/*** 只要消息没有投递给指定的队列,就触发这个失败回调* @param message the returned message. 投递失败的消息详细信息* @param replyCode the reply code. 回复的状态码* @param replyText the reply text. 回复的文本内容* @param exchange the exchange. 当时这个消息接收的交换机* @param routingKey the routing key. 当时这个消息用的哪个路由键*/@Overridepublic void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {System.out.println("Fail Message["+message+"]==>replyCode["+replyCode+"]==>replyText["+replyText+"]==>exchange["+exchange+"]==>routingKey["+routingKey+"]");}});}
}
测试:
该模式需要消息投递到指定队列失败才会触发,这里将路由键故意改错便于测试,测试结束后将错误改回来。
错误消息:
Fail Message[(Body:'[B@f04a8cf(byte[855])' MessageProperties [headers={__TypeId__=com.wen.gulimall.order.entity.OrderEntity}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, deliveryTag=0])]==>replyCode[312]==>replyText[NO_ROUTE]==>exchange[hello-java-exchange]==>routingKey[hello-java333]
3.5.1.3 消息唯一id
在发送消息时参数CorrelationData就是消息的唯一id。
这个id在发送端确认时是可以拿到的,可以用于排查哪些消息为成功抵达(与消费端接收到存放在数据库中的消息唯一id进行对比)。
3.5.2 消费端确认(Ack确认机制)
3.5.2.1 消费端自动确认
1)消费端确认,默认是自动确认的,只要消息收到,客户端会自动确认,服务端就会移除这个消息。
2)消费端自动确认存在问题:
在接收消息这里打上断点,在处理完第一个消息后,关掉服务器,模拟突发状况,服务器宕机,如下:
关掉服务器后,剩余未处理的4个消息也消失不见,未经过消费端处理,相当于消息丢失。
3.5.2.2 消费端手动确认
为了解决消息自动确认,遇到突发状况造成消息丢失问题,可以使用手动确认。 只有手动确认的消息才能被队列移除。
开启消费端手动确认配置
gulimall-order/src/main/resources/application.yml
spring:rabbitmq:host: 172.1.11.10port: 5672virtual-host: /# 开启发送端确认publisher-confirm-type: correlated# 开启发送端消息抵达队列的确认,默认是falsepublisher-returns: true# 只要消息抵达队列,以异步发送优先回调我们的returnConfirmtemplate:mandatory: true# 开启消费端手动确认listener:simple:acknowledge-mode: manual
1. 消息抵达客户端,不手动ack测试
发送五个消息进行测试,客户端就算拿到消息不做消息抵达确认,队列不能移除未确认的消息,只是消息的状态有ready变为unacked,关闭客户端服务器后消息的状态又由unacked变为ready,下次重启客户端服务器又可以接收消息。
关闭客户端服务器,观察消息的状态:
(由unacked变为ready)
2. 消息抵达客户端,模拟部分消息手动ack测试
客户端确认,debug模式下无法模拟真实情况下宕机,关闭了也会继续执行,这里断点放开,根据投递标签deliveryTag(channel内按顺序自增)取余模拟客户端突然宕机未接收到消息,如下:
@RabbitListener(queues = {"hello-java-queue"})
@Slf4j
@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {...//@RabbitListener(queues = {"hello-java-queue"})@RabbitHandlerpublic void receiveMessage3(Message msg,OrderReturnReasonEntity content,Channel channel) throws InterruptedException {//log.info("信道Channel:{}",channel);System.out.println("接收到消息..."+content);// 消息体byte[] body = msg.getBody();// 消息头信息MessageProperties messageProperties = msg.getMessageProperties();Thread.sleep(3000);System.out.println("消息处理完成=>"+content.getName());// channel内按顺序自增的long deliveryTag = msg.getMessageProperties().getDeliveryTag();System.out.println("deliveryTag=>"+deliveryTag);// basicAck(long deliveryTag, // channel内按顺序自增// boolean multiple) // 是否批量确认// debug下无法模拟真实情况下的宕机,关闭了也会继续执行,// 这里根据投递标签deliveryTag模拟客户端突然宕机未接收到消息try {if(deliveryTag%2==0) {channel.basicAck(deliveryTag,false);// 手动ack确认接收到消息System.out.println("签收了货物..."+deliveryTag);}} catch (IOException e) {throw new RuntimeException(e);}}}
此时只确认签收了货物-2和货物-4,还有3个未确认,如下:
关闭客户端服务器,消息状态由unacked变成ready,如下:
重启客户端服务器,剩余的消息可以继续签收,因为deliveryTag是根据信道channel里的消息顺序自增,客户端重启后消息又重新排序。之前发送的5个消息(1,2,3,4,5)只确认了偶数2和4,客户端重启后channel内的消息顺序变为(1,2,3),消费端可以再次确认接收一个消息。
3.5.2.3 消费端退货
@RabbitListener(queues = {"hello-java-queue"})
@Slf4j
@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {...//@RabbitListener(queues = {"hello-java-queue"})@RabbitHandlerpublic void receiveMessage3(Message msg,OrderReturnReasonEntity content,Channel channel) throws InterruptedException {//log.info("信道Channel:{}",channel);System.out.println("接收到消息..."+content);// 消息体byte[] body = msg.getBody();// 消息头信息MessageProperties messageProperties = msg.getMessageProperties();Thread.sleep(3000);System.out.println("消息处理完成=>"+content.getName());// channel内按顺序自增的long deliveryTag = msg.getMessageProperties().getDeliveryTag();System.out.println("deliveryTag=>"+deliveryTag);// basicAck(long deliveryTag, // channel内按顺序自增// boolean multiple) // 是否批量确认// debug下无法模拟真实情况下的宕机,关闭了也会继续执行,// 这里根据投递标签deliveryTag模拟客户端突然宕机未接收到消息try {// 签收货物非批量模式if(deliveryTag%2==0) {// 收货channel.basicAck(deliveryTag,false);// 手动ack确认接收到消息System.out.println("签收了货物..."+deliveryTag);}else {// 退货// requeue=false 丢弃 ;requeue=true 重回队列// basicNack(long deliveryTag, boolean multiple, boolean requeue)channel.basicNack(deliveryTag,false,false);System.out.println("没有签收货物..."+deliveryTag);}} catch (IOException e) {throw new RuntimeException(e);}}
}
channel.basicNack(deliveryTag,false,false);中requeue=false丢弃掉未被签收的货物,清空队列,如下:
当requeue=true时,deliveryTag为奇数消息被拒收重新入队deliveryTag变成偶数,被签收。
4 RabbitMQ延时队列(实现定时任务)
4.1 为什么不使用定时任务?
定时任务的时效性问题?
场景:订单30min中未支付,关闭订单
出现问题:定时任务在0min中扫描的时候没有订单未支付,1min中后下订单,定时任务第二次执行的时候(30min)这时下订单29min中未支付订单不会被关闭,到定时任务第三次执行的时候(60min)才能关闭未支付的订单,订单从创建到关闭花费了30+29=59min,不满足订单30min中未支付关闭订单。
4.2 使用场景
4.3 消息的存活时间TTL(Time To Live)
- 消息的TTL就是消息的存活时间。
- RabbitMQ可以对队列和消息分别设置TTL。
- 对队列设置就是队列没有消费者连着的保留时间,也可以对每一个单独的消息做单独的设置。超过了这个时间,我们认为这个消息就死了,称之为死信。
- 如果队列设置了,消息也设置了,那么会取小的。所以一个消息如果被路由到不同的队列中,这个消息死亡的时间有可能不一样(不同的队列设置)。这里单讲单个消息的TTL,因为它才是实现延时任务的关键。可以通过设置消息的expiration字段或者x-message-ttl属性来设置时间,两者是一样的效果。
4.4 DXL和死信队列
- DXL(Dead Letter Exchanges)即死信交换机,它其实就是一个正常的交换机,能够与任何队列绑定。
- 死信队列是指队列(正常)上的消息(过期)变成死信后,能够发送到另外一个交换机(DLX),然后被路由到一个队列上,这个队列就是死信队列。
- 成为死信一般有以下几种情况:
1)消息被拒收(basic.reject/basic.nack)requeue=false;
2)消息的TTL到了,消息过期;
3)队列长度限制被超越(队列满了)。
4.5 延时队列实现
1. 延时队列实现有两种方式:设置队列过期时间、设置消息过期时间实现延时队列。
2. 建议使用设置队列过期时间实现延时队列,因为设置消息过期时间,MQ是惰性检查。比如:第一个进来的消息过期时间是5min,第二个进来的消息过期时间是2min,MQ会先检查第一个消息,等第一个消息进入死信队列,再去检查第二个消息,这时第二个消息已经死3min了。
4.5.1 设置队列过期时间实现延时队列
4.5.2 设置消息过期时间实现延时队列
4.6 延时队列定时关单模拟
以下单为例,设置队列过期时间实现延时队列。
4.6.1 实现方式
4.6.1.1 基础版
交换机与队列一一对应,一台路由器路由一个队列。
给队列user.order.delay.queue(死信队列)设置了三个参数:
- x-dead-letter-exchange:user.order.exchange (死信交换机)
- x-dead-letter-rounting-key:order(死信路由键)
- x-message-ttl:60000(队列里面所有消息存活时间60000ms)
4.6.1.2 升级版
一个微服务模块配置一个交换机,一个交换机绑定多个队列。
4.6.2 实现
4.6.2.1 创建Queue、Exchange、Binding方式
容器中的 Binding、Queue、Exchange都会自动创建(RabbitMQ中没有的情况)
- 第一次发送消息【使用队列】的时候创建交换机、队列、绑定关系
- RabbitMQ中没有交换机、队列才会创建;RabbitMQ中只要有,属性发生变化也不会覆盖。
gulimall-order/src/main/java/com/wen/gulimall/order/config/MyMQConfig.java
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.Exchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.HashMap;
import java.util.Map;/*** @author W* @createDate 2024/02/21 14:39* @description: 创建 Exchange 、 Queue、 Binding*/
@Configuration
public class MyMQConfig {//@RabbitListener(queues = "order.release.order.queue")//public void listen(Message message, Channel channel, OrderEntity orderEntity) throws IOException {// System.out.println("收到过期订单消息,准备关闭订单:------->"+orderEntity);// // 确认收到消息// channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);//}/*** 容器中的 Binding、Queue、Exchange都会自动创建(RabbitMQ没有的情况)* 延时队列* RabbitMQ中只要有。属性发生变化也不会覆盖。* @return*/@Beanpublic Queue orderDelayQueue(){Map<String, Object> arguments = new HashMap<>();//x-dead-letter-exchange: order-event-exchange//x-dead-letter-routing-key: order.release.order//x-message-ttl: 60000arguments.put("x-dead-letter-exchange","order-event-exchange");arguments.put("x-dead-letter-routing-key","order.release.order");arguments.put("x-message-ttl",60000);// String name, 【队列名称】// boolean durable, 【是否持久化】// boolean exclusive, 【是否排它】// boolean autoDelete,【是否自动删除】// @Nullable Map<String, Object> arguments 【自定义参数,死信路由、死信路由键、消息存活时间】Queue queue = new Queue("order.delay.queue",true,false,false,arguments);return queue;}/*** 死信队列* @return*/@Beanpublic Queue orderReleaseOrderQueue(){Queue queue = new Queue("order.release.order.queue",true,false,false);return queue;}/*** 普通路由/死信路由* @return*/@Beanpublic Exchange orderEventExchange(){// String name, boolean durable, boolean autoDelete, Map<String, Object> argumentsTopicExchange topicExchange = new TopicExchange("order-event-exchange",true,false);return topicExchange;}/*** 交换机与延时队列的绑定* @return*/@Beanpublic Binding orderCreateOrder(){// String destination, 【目的地】// DestinationType destinationType, 【目的地类型,queue、exchange】// String exchange,// String routingKey,// @Nullable Map<String, Object> argumentsreturn new Binding("order.delay.queue",Binding.DestinationType.QUEUE,"order-event-exchange","order.create.order",null);}/*** 死信交换机与死信队列的绑定* @return*/@Beanpublic Binding orderReleaseOrder(){return new Binding("order.release.order.queue",Binding.DestinationType.QUEUE,"order-event-exchange","order.release.order",null);}
}
启动订单服务后,给延时队列发送消息后(相关代码如下),创建队列和交换机以及绑定关系,如下:
4.6.2.2 发送消息相关代码
gulimall-order/src/main/java/com/wen/gulimall/order/web/HelloController.java
@Controller
public class HelloController {@Resourceprivate RabbitTemplate rabbitTemplate;@ResponseBody@GetMapping("/test/createOrder")public String createOrderTest(){OrderEntity orderEntity = new OrderEntity();orderEntity.setOrderSn(UUID.randomUUID().toString());orderEntity.setCreateTime(new Date());rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",orderEntity);return "ok";}
}
4.6.2.3 监听接收消息相关代码
gulimall-order/src/main/java/com/wen/gulimall/order/config/MyMQConfig.java
@Configuration
public class MyMQConfig {@RabbitListener(queues = "order.release.order.queue")public void listen(Message message, Channel channel, OrderEntity orderEntity) throws IOException {System.out.println("当前时间"+new Date() +"收到过期订单消息,准备关闭订单:------->"+orderEntity);// 确认收到消息channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);}...// 创建交换机 队列 绑定
}
4.6.3 测试
- 启动订单服务,请求 http://order.gulimall.com/test/createOrder 给延时队列(order.delay.queue) 发送消息。
- 一分钟后,死信队列(order.release.order.queue) 接收到延时队列(order.delay.queue)中过期的消息(即死信),消费者监听队列(order.release.order.queue)消费信息,控制台打印如下:
当前时间Thu Feb 22 15:16:20 CST 2024收到过期订单消息,准备关闭订单:------->OrderEntity(id=null, memberId=null, orderSn=0d784fd2-6c82-48cc-aa2c-e573bf8de1d1, couponId=null, createTime=Thu Feb 22 15:15:20 CST 2024, memberUsername=null, totalAmount=null, payAmount=null, freightAmount=null, promotionAmount=null, integrationAmount=null, couponAmount=null, discountAmount=null, payType=null, sourceType=null, status=null, deliveryCompany=null, deliverySn=null, autoConfirmDay=null, integration=null, growth=null, billType=null, billHeader=null, billContent=null, billReceiverPhone=null, billReceiverEmail=null, receiverName=null, receiverPhone=null, receiverPostCode=null, receiverProvince=null, receiverCity=null, receiverRegion=null, receiverDetailAddress=null, note=null, confirmStatus=null, deleteStatus=null, useIntegration=null, paymentTime=null, deliveryTime=null, receiveTime=null, commentTime=null, modifyTime=null)
- 一分钟后,死信队列(order.release.order.queue) 接收到延时队列(order.delay.queue)中过期的消息(即死信),如果没有消费者监听接收消息,消息在死信队列(order.release.order.queue)中,如下:
相关文章:

谷粒商城篇章9 ---- P248-P261/P292-P294 ---- 消息队列【分布式高级篇六】
目录 1 消息队列(Message Queue)简介 1.1 概述 1.2 消息服务中两个重要概念 1.3 消息队列主要有两种形式的目的地 1.4 JMS和AMQP对比 1.5 应用场景 1.6 Spring支持 1.7 SpringBoot自动配置 1.7 市面上的MQ产品 2 RabbitMQ 2.1 RabbitMQ简介 2.1.1 RabbitMQ简介 2…...

【Spring连载】使用Spring Data访问 MongoDB(五)----生命周期事件
【Spring连载】使用Spring Data访问 MongoDB(五)----生命周期事件Lifecycle Events 一、实体回调Entity Callbacks1.1 实现实体回调1.2 注册实体回调 二、特定存储的实体回调 一、实体回调Entity Callbacks 1.1 实现实体回调 1.2 注册实体回调 二、特…...

JavaSec 之 SQL 注入简单了解
文章目录 JDBC 注入语句拼接(Statement)修复方案 语句拼接(PrepareStatement)修复方案 预编译 JdbcTemplate修复方案 MyBatisLike 注入Order By 注入In 注入 寒假学了一个月 pwn,真心感觉这玩意太底层学的我生理不适应了,接下来学一段时间 java 安全缓一…...

第十一章——期约与异步函数
ECMAScript 6及之后的几个版本逐步加大了对异步编程机制的支持,提供了令人眼前一亮的新特性。ECMAScript 6新增了正式的Promise(期约)引用类型,支持优雅地定义和组织异步逻辑。接下来几个版本增加了使用async和await关键字定义异步…...

工具方法合集-utils.js
通用 import get from lodash.get import cloneDeep from lodash.clonedeep // 深度clone export function deepClone(obj) {return obj ? cloneDeep(obj) : obj } export function lodashGet(obj, key, defaultValue = ) {//这个 defaultValue 不能给默认 值 会报错;retur…...

安卓11-设置HDMI分辨率流程
安卓11中从设置-显示设置hdmi分辨率流程:framework层通过jni控制底层驱动实现,标准驱动模型 packages\apps\Settings\src\com\android\settings\display\HdmiSettings.javaprivate void updateResolution(final ITEM_CONTROL control, final int index) {showWaitin…...

Vue3+vite搭建基础架构(11)--- 菜单栏功能和Tab页功能实现
Vue3vite搭建基础架构(11)--- 菜单栏功能和Tab页功能实现 说明删除项目中不需要的文件userStore全局属性代码菜单栏代码Tab页代码解决浏览器输入地址时不会打开tab页问题和切换tab页时参数丢失问题 说明 这里记录下自己在Vue3vite的项目实现菜单栏功能和…...

餐饮神秘顾客公司:关于餐饮行业神秘顾客调查注意事项
在餐饮业,顾客体验往往决定品牌的成败。为深入了解顾客需求和感受,许多餐饮企业引入“神秘顾客”调查。然而,此调查并非简单走过场,其中细节和注意事项颇多。餐饮行业神秘顾客调查需注意以下几点: 1. 专业培训&#x…...

概率密度函数(PDF)与神经网络中的激活函数
原创:项道德(daode3056,daode1212) 在量子力学中,许多现象都是统计的结果,基本上用的是正态分布,然而,从本质上思考,应该还存在低阶的分布,标准的正态分布是它的极限,这样一来,或许在…...

.netcore 6.0/7.0项目迁移至.netcore 8.0 注意事项
1、SqlSugarCore 相关 1.1 主项目添加数据,否则会报数据库连接错误: <InvariantGlobalization>false</InvariantGlobalization> <PropertyGroup><TargetFramework>net8.0</TargetFramework><Nullable>enable</…...

算法打卡day2|数组篇|Leetcode 977.有序数组的平方、 209.长度最小的子数组、59.螺旋矩阵II
算法题 Leetcode 977.有序数组的平方 题目链接: 977.有序数组的平方 大佬视频讲解:977.有序数组的平方 个人思路 第一时间就只想到暴力解法,双重循环一个循环比较一个循环赋值;但这样可能会超时,所以还能用双指针࿰…...

Hive【内部表、外部表、临时表、分区表、分桶表】【总结】
目录 Hive的物种表结构特性 一、内部表 建表 使用场景 二、外部表 建表:关键词【EXTERNAL】 场景: 外部表与内部表可互相转换 三、临时表 建表 临时表横向对比编辑 四、分区表 建表:关键字【PARTITIONED BY】 场景: 五、分桶表 …...

随手写的小程序2 一个nc能控制的程序
小程序源代码 下载: https://download.csdn.net/download/nn_84/88846445?spm1001.2014.3001.5501 请下载 Qt 5.12.12 server.pro : QT gui networkCONFIG c11 console CONFIG - app_bundle# You can make your code fail to compile if it uses deprecated APIs. # In o…...

Android中通过属性动画实现文字轮播效果
前些天发现了一个蛮有意思的人工智能学习网站,8个字形容一下"通俗易懂,风趣幽默",感觉非常有意思,忍不住分享一下给大家。 👉点击跳转到教程 一、创建一个自定义ProvinceView类,具体代码如下 /*** Author: ly* Date: 2024/2/22* D…...

最长的回文串
开始想的简单了,确实没想到奇数字母删去一个后也能用 解法: 桶排序 #include<iostream> #include<vector> #include<algorithm> using namespace std; #define endl \n #define int long long signed main() {int t;cin >> t…...

2023 H1 中国边缘公有云服务市场 Top2,百度智能云加速推动分布式云智能化升级
近期,IDC 发布了《中国边缘云市场跟踪研究 2023 H1》。报告显示,2023 上半年,中国边缘公有云服务市场规模 24.3 亿元,同比增速达到 41.8%。 其中,百度智能云以 15.7% 的市场份额位列中国边缘公有云服务市场第二&#…...

Emlog博客网站快速搭建并结合内网穿透实现远程访问本地站点
文章目录 前言1. 网站搭建1.1 Emolog网页下载和安装1.2 网页测试1.3 cpolar的安装和注册 2. 本地网页发布2.1 Cpolar临时数据隧道2.2.Cpolar稳定隧道(云端设置)2.3.Cpolar稳定隧道(本地设置) 3. 公网访问测试总结 前言 博客作为使…...

力扣经典题目解析--旋转图像(字节二面)
题目 原题地址: . - 力扣(LeetCode) 给定一个 n n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。 你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。 示例 1࿱…...

【ARMv8M Cortex-M33 系列 8.1 -- RT-Thread 堆内存 检查命令 free 实现及介绍】
文章目录 RT-Thread 堆内存 检查命令 free 实现及介绍rt_memory_info 函数验证 RT-Thread 堆内存 检查命令 free 实现及介绍 在RT-Thread系统中,通常可以通过rt_memory_info函数获取当前的堆内存使用信息,然后你可以包装这个函数来显示剩余的堆空间。rt…...

milvus Delete API流程源码分析
Delete API执行流程源码解析 milvus版本:v2.3.2 整体架构: Delete 的数据流向: 1.客户端sdk发出Delete API请求。 from pymilvus import (connections,Collection, )print("start connecting to Milvus") connections.connect("default", host"192…...

CentOS使用Docker搭建Halo网站并实现无公网ip远程访问
🔥博客主页: 小羊失眠啦. 🎥系列专栏:《C语言》 《数据结构》 《C》 《Linux》 《Cpolar》 ❤️感谢大家点赞👍收藏⭐评论✍️ 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默&…...

【JVM】垃圾回收算法
📝个人主页:五敷有你 🔥系列专栏:JVM ⛺️稳中求进,晒太阳 垃圾回收算法 Java是如何实现垃圾回收的呢?简单来说,垃圾回收就做两件事 找到内存中存活的对象释放不在存活对象的内存&…...

如何和将原始request的Header中的值传递给openfeign请求的Header? 以及又如何获取openfeign请求中Header中的值
如何和将原始request的Header中的值传递给openfeign请求的Header? 以及又如何获取openfeign请求中Header中的值 如何和将原始request的Header中的值传递给openfeign请求的Header参考 [https://www.jb51.net/article/282522.htm](https://www.jb51.net/article/28252…...

Flink 侧输出流(SideOutput)
🌸在平时大部分的 DataStream API 的算子的输出是单一输出,也就是某一种或者说某一类数据流,流向相同的地方。 🌸在处理不同的流中,除了 split 算子,可以将一条流分成多条流,这些流的数据类型也…...

C语言中关于#include的一些小知识
写代码的过程中,因为手误,重复包含了头文件 可以看到没有报错 如果是你自己编写的头文件,那么如果没加唯一包含标识的话,那么编译器会编译报错的。如果是系统自带的头文件,由于其每个头文件都加了特殊标识,…...

DSP芯片 机器码下载方法 【主要 “扯” 用Uniflash下载的方法】
还是以德州仪器的TMS320F28335芯片为 “栗子”,说说这事儿。 编制好的程序经过开发环境可以编译成扩展名为 .out 文件,这个文件就是DSP可以运行机器码,我们把这个文件下载到 DSP芯片中的程序区, 下载好了,这个芯片原…...

速盾网络:CDN用几天关了可以吗?安全吗?
在使用CDN(内容分发网络)时,有时候会考虑暂时关闭或暂停使用CDN服务的情况。但是,对于关闭CDN服务的时间长短以及安全性问题,很多人可能存在疑问。在本文中,我们将讨论CDN使用中关闭几天是否安全以及相关注…...

MR混合现实情景实训教学系统在高空作业课堂中的应用
高空作业是一项高风险的工作,对于从业者来说,不仅需要具备专业的技能,还需要有丰富的实践经验。然而,传统的课堂教学往往只能通过理论讲解和模拟训练来传授知识,无法提供真实的实践环境。而MR混合现实情景实训教学系统…...

Windows系统中定时执行python脚本
背景:本地Windows系统指定目录下会有文件的修改新增,这些变化的文件需要定时的被上传到git仓库中,这样不需要每次变更手动上传了。 首先编写一个检测文件夹下文件变化并且上传git仓库的python脚本(确保你已经在E:\edc_workspace\data_edc_et…...

HashMap 源码学习-jdk1.8
1、一些常量的定义 这里针对MIN_TREEIFY_CAPACITY 这个值进行解释一下。 java8里面,HashMap 的数据结构是数组 (链表或者红黑树),每个数组节点下可能会存在链表和红黑树之间的转换,当同一个索引下面的节点超过8个时…...