7. RabbitMQ 消息队列——延时队列(Spring Boot + 安装message_exchange“延迟插件“ 的详细配置说明)的详细讲解
7. RabbitMQ 消息队列——延时队列(Spring Boot + 安装message_exchange"延迟插件" 的详细配置说明)的详细讲解
文章目录
- 7. RabbitMQ 消息队列——延时队列(Spring Boot + 安装message_exchange"延迟插件" 的详细配置说明)的详细讲解
- 1. RabbitMQ 延时队列概述
- 2. RabbitMQ 中的 TTL
- 3. RabbitMQ 整合 Spring Boot
- 3.1 Spring Boot 当中搭建配置 RabbitMQ 的 队列 TTL(延时队列)
- 3.2 延时队列优化
- 3.3 Rabbitmq 插件实现延迟队列
- 4. 补充:Docker 容器当中安装 message_exchange"延迟插件" 的详细步骤
- 5. 最后:
1. RabbitMQ 延时队列概述
延时队列,队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望 在指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的 元素的队列。
延时队列的应用场景:
场景:有一个订单,15 分钟内如果不支付,就把该订单设置为交易关闭,那么就不能再进行支付了,这类实现延迟任务的场景就可以采用延时队列的方式来实现,当然除了延时队列,还可以采用其它的方式——> 定时任务的方式。
- 订单在十分钟之内未支付则自动取消。
- 新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。
- 用户注册后,如果三天内没有登陆则进行短信提醒。
- 用户发起退款,如果三天内没有得到处理则通知相关运营人员。
- 预定会议后,需要在预定的时间点前 10 分钟,通知各个参会人员参加会议。
这些场景都有一个特点,需要在某个事件发生之后或者之前的指定时间点完成某一项任务,如: 发生订单生成事件,在十分钟之后检查该订单支付状态,然后将未支付的订单进行关闭;看起来似乎 使用定时任务,一直轮询数据,每秒查一次,取出需要被处理的数据,然后处理不就完事了吗?如果 数据量比较少,确实可以这样做,比如:对于“如果账单一周内未支付则进行自动结算”这样的需求, 如果对于时间不是严格限制,而是宽松意义上的一周,那么每天晚上跑个定时任务检查一下所有未支 付的账单,确实也是一个可行的方案。但对于数据量比较大,并且时效性较强的场景,如:“订单十 分钟内未支付则关闭“,短期内未支付的订单数据可能会有很多,活动期间甚至会达到百万甚至千万 级别,对这么庞大的数据量仍旧使用轮询的方式显然是不可取的,很可能在一秒内无法完成所有订单 的检查,同时会给数据库带来很大压力,无法满足业务要求而且性能低下。
2. RabbitMQ 中的 TTL
TTL 是什么呢?TTL 是 RabbitMQ 中一个消息或者队列的属性,表明一条消息或者队列中的所有消息的最大存活时间。
单位是毫秒。换句话说,如果一条消息设置了 TTL 属性或者进入了设置 TTL 属性的队列,那么这条消息如果在 TTL 设置的时间内没有被消费,则会成为 “死信 ” 。如果同时配置了队列的 TTL 和消息的 TTL ,那么较小的那个值将会被使用,有两种方式设置了 TTL 。
消息设置 TTL:
另一种方式便是针对每条消息设置 TTL
队列设置 TTL:
第一种是在创建队列的时候设置队列的 “x-message-ttl ”
属性。
延时队列 和 死信队列:
如果设置了队列的 TTL 属性,那么一旦消息过期,就会被队列丢弃(如果配置了死信队列被丢到死信队 列中),而第二种方式,消息即使过期,也不一定会被马上丢弃,因为消息是否过期是在即将投递到消费者 之前判定的,如果当前队列有严重的消息积压情况,则已过期的消息也许还能存活较长时间;另外,还需 要注意的一点是,如果不设置 TTL,表示消息永远不会过期,如果将 TTL 设置为 0,则表示除非此时可以 直接投递该消息到消费者,否则该消息将会被丢弃。
上一篇我们介绍了死信队列,刚刚又介绍了 TTL,至此利用 RabbitMQ 实现延时队列的两大要素已 经集齐,接下来只需要将它们进行融合,再加入一点点调味料,延时队列就可以新鲜出炉了。想想看,延 时队列,不就是想要消息延迟多久被处理吗,TTL 则刚好能让消息在延迟多久之后成为死信,另一方面, 成为死信的消息都会被投递到死信队列里,这样只需要消费者一直消费死信队列里的消息就完事了,因为 里面的消息都是希望被立即处理的消息。
3. RabbitMQ 整合 Spring Boot
1. 创建项目
添加相关依赖,在 pom.xml
文件当中
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!--RabbitMQ 依赖--><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>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.47</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><!--swagger--><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>2.9.2</version></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger-ui</artifactId><version>2.9.2</version></dependency><!--RabbitMQ 测试依赖--><dependency><groupId>org.springframework.amqp</groupId><artifactId>spring-rabbit-test</artifactId><scope>test</scope></dependency></dependencies>
<?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.5.3</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.rainbowsea</groupId><artifactId>springboot-rabbitmq</artifactId><version>0.0.1-SNAPSHOT</version><name>springboot-rabbitmq</name><description>Demo project for Spring Boot</description><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!--RabbitMQ 依赖--><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>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.47</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><!--swagger--><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>2.9.2</version></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger-ui</artifactId><version>2.9.2</version></dependency><!--RabbitMQ 测试依赖--><dependency><groupId>org.springframework.amqp</groupId><artifactId>spring-rabbit-test</artifactId><scope>test</scope></dependency></dependencies></project>
在 resources/创建一个 application.properties
文件当中(当然也可以使用 yaml
配置文件),配置相关的配置文件
spring.rabbitmq.host=192.168.76.156
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=123
添加 Swagger 配置类
package com.rainbowsea.rabbitmq.springbootrabbitmq.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;@Configuration
@EnableSwagger2
public class SwaggerConfig {@Beanpublic Docket webApiConfig() {return new Docket(DocumentationType.SWAGGER_2).groupName("webApi").apiInfo(webApiInfo()).select().build();}private ApiInfo webApiInfo() {return new ApiInfoBuilder().title("rabbitmq 接口文档").description("本文档描述了 rabbitmq 微服务接口定义").version("1.0").contact(new Contact("enjoy6288", "http://rainbowsea.com", "3339966@qq.com")).build();}
}
3.1 Spring Boot 当中搭建配置 RabbitMQ 的 队列 TTL(延时队列)
代码架构图:
创建两个队列 QA 和 QB,两者队列 TTL 分别设置为 10S 和 40S,然后在创建一个交换机 X 和死信交 换机 Y,它们的类型都是 direct,创建一个死信队列 QD,它们的绑定关系如下:
1. 创建文件类代码(该代码就是用于配置 RabbitMQ 当中的交换机和队列的信息配置的),作为一个配置类存在的 。
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.amqp.core.QueueBuilder;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.HashMap;
import java.util.Map;@Configuration // 标注是一个配置类,加载到 IOC 容器当中
public class TtlQueueConfig {// 普通交换机的名称public static final String X_EXCHANGE = "X";// 声明 XExchange 别名;会注入到 IOC 容器当中 普通交换机的@Bean(value = "xExchange")public DirectExchange xExchange() {return new DirectExchange(X_EXCHANGE);}}
package com.rainbowsea.rabbitmq.springbootrabbitmq.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.amqp.core.QueueBuilder;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.HashMap;
import java.util.Map;@Configuration // 标注是一个配置类,加载到 IOC 容器当中
public class TtlQueueConfig {// 普通交换机的名称public static final String X_EXCHANGE = "X";// 死信队列的名称public static final String DEAD_LETTER_QUEUE = "QD";// 死信交换机的名称public static final String Y_DEAD_LETTER_EXCHANGE = "Y";// 普通队列的名称public static final String QUEUE_A = "QA";public static final String QUEUE_B = "QB";// 声明普通队列 TTL 为 10s@Bean("queueA")public Queue queueA() {// 创建一个 Map 用于存放,设置队列信息Map<String, Object> arguments = new HashMap<>(3);// 设置死信交换机 注意这里的参数 x-dead-letter-exchange 是固定的不可arguments.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);// 设置死信的 RoutingKey 为 YD ,注意这里的参数 x-dead-letter-routing-key 是固定的不可随便写arguments.put("x-dead-letter-routing-key", "YD");// 设置 TTL ,单位是 ms 注意这里的参数 x-message-ttl 是固定的不可随便写arguments.put("x-message-ttl", 10000);return QueueBuilder.durable(QUEUE_A).withArguments(arguments).build();}// 声明普通队列 TTL 为 40s@Bean("queueB")public Queue queueB() {// 创建一个 Map 用于存放,设置队列信息Map<String, Object> arguments = new HashMap<>(3);// 设置死信交换机 注意这里的参数 x-dead-letter-exchange 是固定的不可arguments.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);// 设置死信的 RoutingKey 为 YD ,注意这里的参数 x-dead-letter-routing-key 是固定的不可随便写arguments.put("x-dead-letter-routing-key", "YD");// 设置 TTL ,单位是 ms 注意这里的参数 x-message-ttl 是固定的不可随便写arguments.put("x-message-ttl", 40000);return QueueBuilder.durable(QUEUE_B).withArguments(arguments).build();}}
// 声明死信队列@Bean("queueD")public Queue queueD() {// 不带参数,就不需要 .withArguments(arguments)//return QueueBuilder.durable(QUEUE_B).withArguments(arguments).build();return QueueBuilder.durable(DEAD_LETTER_QUEUE).build();}
// 绑定/*** 注意:这里的 public Binding queueABindingX(@Qualifier("queueA") Queue queueA,* @Qualifier("xExchange") DirectExchange xExchange)** @Qualifier("xExchange") DirectExchange xExchange)* @Qualifier("xExchange") DirectExchange xExchange)* @Qualifier 参数内的值是,我们上边: @Bean(value = "xExchange"),@Bean("queueA") 注入到 IOC 容器当中的值* ,必须要是注入到了 IOC容器当中,不然是无法被使用上的,同时注意名称,不要写错了,要保持一致* @Bean(value = "xExchange")* public DirectExchange xExchange() {* @Bean("queueA") public Queue queueA() {*/@Beanpublic Binding queueABindingX(@Qualifier("queueA") Queue queueA,@Qualifier("xExchange") DirectExchange xExchange) {// 第一个参数是 队列名(注入到了IOC容器当中了),第二个参数是交换机(注入到了IOC容器当中了)// 第三个蚕食是: 队列名对于绑定交换机之间的交互的 routingKey 信息"XA"// 表示将 队列 queueA 与 xExchange 交换机,通过 routingKey 为 "XA" 进行一个绑定return BindingBuilder.bind(queueA).to(xExchange).with("XA");}
完整配置代码如下:
package com.rainbowsea.rabbitmq.springbootrabbitmq.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.amqp.core.QueueBuilder;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.HashMap;
import java.util.Map;@Configuration // 标注是一个配置类,加载到 IOC 容器当中
public class TtlQueueConfig {// 普通交换机的名称public static final String X_EXCHANGE = "X";// 死信队列的名称public static final String DEAD_LETTER_QUEUE = "QD";// 死信交换机的名称public static final String Y_DEAD_LETTER_EXCHANGE = "Y";// 普通队列的名称public static final String QUEUE_A = "QA";public static final String QUEUE_B = "QB";// 声明 XExchange 别名;会注入到 IOC 容器当中 普通交换机的@Bean(value = "xExchange")public DirectExchange xExchange() {return new DirectExchange(X_EXCHANGE);}// 声明 yExchange 别名;会注入到 IOC 容器当中 普通交换机的@Bean(value = "yExchange")public DirectExchange yExchange() {return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);}// 声明普通队列 TTL 为 10s@Bean("queueA")public Queue queueA() {// 创建一个 Map 用于存放,设置队列信息Map<String, Object> arguments = new HashMap<>(3);// 设置死信交换机 注意这里的参数 x-dead-letter-exchange 是固定的不可arguments.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);// 设置死信的 RoutingKey 为 YD ,注意这里的参数 x-dead-letter-routing-key 是固定的不可随便写arguments.put("x-dead-letter-routing-key", "YD");// 设置 TTL ,单位是 ms 注意这里的参数 x-message-ttl 是固定的不可随便写arguments.put("x-message-ttl", 10000);return QueueBuilder.durable(QUEUE_A).withArguments(arguments).build();}// 声明普通队列 TTL 为 40s@Bean("queueB")public Queue queueB() {// 创建一个 Map 用于存放,设置队列信息Map<String, Object> arguments = new HashMap<>(3);// 设置死信交换机 注意这里的参数 x-dead-letter-exchange 是固定的不可arguments.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);// 设置死信的 RoutingKey 为 YD ,注意这里的参数 x-dead-letter-routing-key 是固定的不可随便写arguments.put("x-dead-letter-routing-key", "YD");// 设置 TTL ,单位是 ms 注意这里的参数 x-message-ttl 是固定的不可随便写arguments.put("x-message-ttl", 40000);return QueueBuilder.durable(QUEUE_B).withArguments(arguments).build();}// 声明死信队列@Bean("queueD")public Queue queueD() {// 不带参数,就不需要 .withArguments(arguments)//return QueueBuilder.durable(QUEUE_B).withArguments(arguments).build();return QueueBuilder.durable(DEAD_LETTER_QUEUE).build();}// 绑定/*** 注意:这里的 public Binding queueABindingX(@Qualifier("queueA") Queue queueA,* @Qualifier("xExchange") DirectExchange xExchange)** @Qualifier("xExchange") DirectExchange xExchange)* @Qualifier("xExchange") DirectExchange xExchange)* @Qualifier 参数内的值是,我们上边: @Bean(value = "xExchange"),@Bean("queueA") 注入到 IOC 容器当中的值* ,必须要是注入到了 IOC容器当中,不然是无法被使用上的,同时注意名称,不要写错了,要保持一致* @Bean(value = "xExchange")* public DirectExchange xExchange() {* @Bean("queueA") public Queue queueA() {*/@Bean // 绑定也是要注入到 IOC 容器当中的public Binding queueABindingX(@Qualifier("queueA") Queue queueA,@Qualifier("xExchange") DirectExchange xExchange) {// 第一个参数是 队列名(注入到了IOC容器当中了),第二个参数是交换机(注入到了IOC容器当中了)// 第三个蚕食是: 队列名对于绑定交换机之间的交互的 routingKey 信息"XA"// 表示将 队列 queueA 与 xExchange 交换机,通过 routingKey 为 "XA" 进行一个绑定return BindingBuilder.bind(queueA).to(xExchange).with("XA");}// 绑定/*** 注意:这里的 public Binding queueBBindingX(@Qualifier("queueB") Queue queueB,* @Qualifier("xExchange") DirectExchange xExchange)** @Qualifier("xExchange") DirectExchange xExchange)* @Qualifier("xExchange") DirectExchange xExchange)* @Qualifier 参数内的值是,我们上边: @Bean(value = "xExchange"),@Bean("queueB") 注入到 IOC 容器当中的值* ,必须要是注入到了 IOC容器当中,不然是无法被使用上的,同时注意名称,不要写错了,要保持一致* @Bean(value = "xExchange")* public DirectExchange xExchange() {* @Bean("queueB") public Queue queueB() {*/@Bean // 绑定也是要注入到 IOC 容器当中的public Binding queueBBindingX(@Qualifier("queueB") Queue queueB,@Qualifier("xExchange") DirectExchange xExchange) {// 第一个参数是 队列名(注入到了IOC容器当中了),第二个参数是交换机(注入到了IOC容器当中了)// 第三个蚕食是: 队列名对于绑定交换机之间的交互的 routingKey 信息"XB"// 表示将 队列 queueB 与 xExchange 交换机,通过 routingKey 为 "XB" 进行一个绑定return BindingBuilder.bind(queueB).to(xExchange).with("XB");}// 绑定/*** 注意:这里的 public Binding queueDBindingX(@Qualifier("queueD") Queue queueD,* @Qualifier("yExchange") DirectExchange yExchange)** @Qualifier("xExchange") DirectExchange xExchange)* @Qualifier("xExchange") DirectExchange xExchange)* @Qualifier 参数内的值是,我们上边: @Bean(value = "yExchange"),@Bean("queueD") 注入到 IOC 容器当中的值* ,必须要是注入到了 IOC容器当中,不然是无法被使用上的,同时注意名称,不要写错了,要保持一致* @Bean(value = "yExchange")* public DirectExchange yExchange() {* @Bean("queueD")* public Queue queueD() {*/@Bean // 绑定也是要注入到 IOC 容器当中的public Binding queueDBindingX(@Qualifier("queueD") Queue queueD,@Qualifier("yExchange") DirectExchange yExchange) {// 第一个参数是 队列名(注入到了IOC容器当中了),第二个参数是交换机(注入到了IOC容器当中了)// 第三个蚕食是: 队列名对于绑定交换机之间的交互的 routingKey 信息"YD"// 表示将 队列 queueD 与 yExchange 交换机,通过 routingKey 为 "YD" 进行一个绑定return BindingBuilder.bind(queueD).to(yExchange).with("YD");}}
生产者——》发送消息的代码:
package com.rainbowsea.rabbitmq.springbootrabbitmq.controller;import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;
import java.util.Date;/*** 生产者:发送延时消息* http://localhost:8080/ttl/sendMsg/嘻嘻嘻*/@RestController
@RequestMapping("/ttl")
@Slf4j
public class SendMsgController {@Resource // 注入到 IOC 容器当中private RabbitTemplate rabbitTemplate;@GetMapping("/sendMsg/{message}")public void sendMsg(@PathVariable String message) {log.info("当前时间:{},发送一条消息给两个 TTL 队列:{}", new Date().toString(), message);rabbitTemplate.convertAndSend("X","XA","消息来自ttl为 10s的队列" + message);rabbitTemplate.convertAndSend("X","XB","消息来自ttl为 40s的队列" + message);}}
消费者——》消费/读取消息的代码:
package com.rainbowsea.rabbitmq.springbootrabbitmq.consumer;import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;import java.io.IOException;
import java.util.Date;/*** 消费者(消费/读取消息)*/@Slf4j
@Component // 当 IOC 容器读取到
public class DeadLetterQueueConsumer {@RabbitListener(queues = "QD")public void receiveD(Message message, Channel channel) {String msg = new String(message.getBody());log.info("当前时间:{},收到死信队列的消息:{}", new Date().toString(), msg);}}
运行效果:
该延时队列存在的问题:
第一条消息在 10 S 后变成了死信消息,然后被消费者消费/读取掉了,第二条消息在 40S 之后变成了死信消息,然后被消费掉,这样一个延时队列就被打造完成了。
不过,该延时队列存在一个问题:
如果这样使用的话,岂不是每增加一个新的时间需求,就要新增一个队列。 这里只有 10S 和 40S 两个时间选项,如果需要一个小时后处理,那么就需要增加
TTL
为一个小时的队列,如果是预定会议室,然后提前通知这样的场景,岂不是要增加无数个队列才能满足需求。
3.2 延时队列优化
优化思路: 就是把延时时间,交给发送消息的生产者上决定,而不是写死在队列当中,通过一个无延时队列来处理,生产者上设置的一个延时发送的时间。
在这里新增了一个队列 QC,绑定关系如下,该队列不设置 TTL 时间
特别注意: 这里需要将上面创建了的 交换机,队列删除了,因为 RabbitMQ 消息队列不能将一个已经存在的交换机/队列,的基础上,进行修改,而是只能重新生成 。
在 TtlQueueConfig 配置类上添加上一个,RoutingKey 为 XC ,队列为 QC,对应 死信队列还是 QD 。
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.amqp.core.QueueBuilder;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.HashMap;
import java.util.Map;@Configuration // 标注是一个配置类,加载到 IOC 容器当中
public class TtlQueueConfig {// 普通交换机的名称public static final String X_EXCHANGE = "X";// 死信队列的名称public static final String DEAD_LETTER_QUEUE = "QD";// 死信交换机的名称public static final String Y_DEAD_LETTER_EXCHANGE = "Y";// 普通队列的名称public static final String QUEUE_A = "QA";public static final String QUEUE_B = "QB";//普通队列的名称public static final String QUEUE_C = "QC";// 声明队列 QC 同时绑定上 (以 RoutingKey 为 XC )QD 死信交换机@Bean("queueC")public Queue queueC(){// 创建一个 Map 用于存放,设置队列信息Map<String, Object> arguments = new HashMap<>(3);// 设置死信交换机 注意这里的参数 x-dead-letter-exchange 是固定的不可arguments.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);// 设置死信的 RoutingKey 为 YD ,注意这里的参数 x-dead-letter-routing-key 是固定的不可随便写arguments.put("x-dead-letter-routing-key", "YD");// 不设置 TTL 时间,也是通过生产者来设置return QueueBuilder.durable(QUEUE_C).withArguments(arguments).build();}// 将声明的队列 QC 绑定到 X 交换机当中@Bean // 绑定也是要注入到 IOC 容器当中的public Binding queueBindingX(@Qualifier("queueC") Queue queueC,@Qualifier("xExchange") DirectExchange xExchange) {// 第一个参数是 队列名(注入到了IOC容器当中了),第二个参数是交换机(注入到了IOC容器当中了)// 第三个蚕食是: 队列名对于绑定交换机之间的交互的 routingKey 信息"XC"// 表示将 队列 queueC 与 xExchange 交换机,通过 routingKey 为 "XC" 进行一个绑定return BindingBuilder.bind(queueC).to(xExchange).with("XC");}}
完整代码内容:
package com.rainbowsea.rabbitmq.springbootrabbitmq.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.amqp.core.QueueBuilder;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.HashMap;
import java.util.Map;@Configuration // 标注是一个配置类,加载到 IOC 容器当中
public class TtlQueueConfig {// 普通交换机的名称public static final String X_EXCHANGE = "X";// 死信队列的名称public static final String DEAD_LETTER_QUEUE = "QD";// 死信交换机的名称public static final String Y_DEAD_LETTER_EXCHANGE = "Y";// 普通队列的名称public static final String QUEUE_A = "QA";public static final String QUEUE_B = "QB";//普通队列的名称public static final String QUEUE_C = "QC";// 声明队列 QC 同时绑定上 (以 RoutingKey 为 XC )QD 死信交换机@Bean("queueC")public Queue queueC(){// 创建一个 Map 用于存放,设置队列信息Map<String, Object> arguments = new HashMap<>(3);// 设置死信交换机 注意这里的参数 x-dead-letter-exchange 是固定的不可arguments.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);// 设置死信的 RoutingKey 为 YD ,注意这里的参数 x-dead-letter-routing-key 是固定的不可随便写arguments.put("x-dead-letter-routing-key", "YD");// 不设置 TTL 时间,也是通过生产者来设置return QueueBuilder.durable(QUEUE_C).withArguments(arguments).build();}// 将声明的队列 QC 绑定到 X 交换机当中@Bean // 绑定也是要注入到 IOC 容器当中的public Binding queueBindingX(@Qualifier("queueC") Queue queueC,@Qualifier("xExchange") DirectExchange xExchange) {// 第一个参数是 队列名(注入到了IOC容器当中了),第二个参数是交换机(注入到了IOC容器当中了)// 第三个蚕食是: 队列名对于绑定交换机之间的交互的 routingKey 信息"XC"// 表示将 队列 queueC 与 xExchange 交换机,通过 routingKey 为 "XC" 进行一个绑定return BindingBuilder.bind(queueC).to(xExchange).with("XC");}// 声明 XExchange 别名;会注入到 IOC 容器当中 普通交换机的@Bean(value = "xExchange")public DirectExchange xExchange() {return new DirectExchange(X_EXCHANGE);}// 声明 yExchange 别名;会注入到 IOC 容器当中 普通交换机的@Bean(value = "yExchange")public DirectExchange yExchange() {return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);}// 声明普通队列 TTL 为 10s@Bean("queueA")public Queue queueA() {// 创建一个 Map 用于存放,设置队列信息Map<String, Object> arguments = new HashMap<>(3);// 设置死信交换机 注意这里的参数 x-dead-letter-exchange 是固定的不可arguments.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);// 设置死信的 RoutingKey 为 YD ,注意这里的参数 x-dead-letter-routing-key 是固定的不可随便写arguments.put("x-dead-letter-routing-key", "YD");// 设置 TTL ,单位是 ms 注意这里的参数 x-message-ttl 是固定的不可随便写arguments.put("x-message-ttl", 10000);return QueueBuilder.durable(QUEUE_A).withArguments(arguments).build();}// 声明普通队列 TTL 为 40s@Bean("queueB")public Queue queueB() {// 创建一个 Map 用于存放,设置队列信息Map<String, Object> arguments = new HashMap<>(3);// 设置死信交换机 注意这里的参数 x-dead-letter-exchange 是固定的不可arguments.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);// 设置死信的 RoutingKey 为 YD ,注意这里的参数 x-dead-letter-routing-key 是固定的不可随便写arguments.put("x-dead-letter-routing-key", "YD");// 设置 TTL ,单位是 ms 注意这里的参数 x-message-ttl 是固定的不可随便写arguments.put("x-message-ttl", 40000);return QueueBuilder.durable(QUEUE_B).withArguments(arguments).build();}// 声明死信队列@Bean("queueD")public Queue queueD() {// 不带参数,就不需要 .withArguments(arguments)//return QueueBuilder.durable(QUEUE_B).withArguments(arguments).build();return QueueBuilder.durable(DEAD_LETTER_QUEUE).build();}// 绑定/*** 注意:这里的 public Binding queueABindingX(@Qualifier("queueA") Queue queueA,* @Qualifier("xExchange") DirectExchange xExchange)** @Qualifier("xExchange") DirectExchange xExchange)* @Qualifier("xExchange") DirectExchange xExchange)* @Qualifier 参数内的值是,我们上边: @Bean(value = "xExchange"),@Bean("queueA") 注入到 IOC 容器当中的值* ,必须要是注入到了 IOC容器当中,不然是无法被使用上的,同时注意名称,不要写错了,要保持一致* @Bean(value = "xExchange")* public DirectExchange xExchange() {* @Bean("queueA") public Queue queueA() {*/@Bean // 绑定也是要注入到 IOC 容器当中的public Binding queueABindingX(@Qualifier("queueA") Queue queueA,@Qualifier("xExchange") DirectExchange xExchange) {// 第一个参数是 队列名(注入到了IOC容器当中了),第二个参数是交换机(注入到了IOC容器当中了)// 第三个蚕食是: 队列名对于绑定交换机之间的交互的 routingKey 信息"XA"// 表示将 队列 queueA 与 xExchange 交换机,通过 routingKey 为 "XA" 进行一个绑定return BindingBuilder.bind(queueA).to(xExchange).with("XA");}// 绑定/*** 注意:这里的 public Binding queueBBindingX(@Qualifier("queueB") Queue queueB,* @Qualifier("xExchange") DirectExchange xExchange)** @Qualifier("xExchange") DirectExchange xExchange)* @Qualifier("xExchange") DirectExchange xExchange)* @Qualifier 参数内的值是,我们上边: @Bean(value = "xExchange"),@Bean("queueB") 注入到 IOC 容器当中的值* ,必须要是注入到了 IOC容器当中,不然是无法被使用上的,同时注意名称,不要写错了,要保持一致* @Bean(value = "xExchange")* public DirectExchange xExchange() {* @Bean("queueB") public Queue queueB() {*/@Bean // 绑定也是要注入到 IOC 容器当中的public Binding queueBBindingX(@Qualifier("queueB") Queue queueB,@Qualifier("xExchange") DirectExchange xExchange) {// 第一个参数是 队列名(注入到了IOC容器当中了),第二个参数是交换机(注入到了IOC容器当中了)// 第三个蚕食是: 队列名对于绑定交换机之间的交互的 routingKey 信息"XB"// 表示将 队列 queueB 与 xExchange 交换机,通过 routingKey 为 "XB" 进行一个绑定return BindingBuilder.bind(queueB).to(xExchange).with("XB");}// 绑定/*** 注意:这里的 public Binding queueDBindingX(@Qualifier("queueD") Queue queueD,* @Qualifier("yExchange") DirectExchange yExchange)** @Qualifier("xExchange") DirectExchange xExchange)* @Qualifier("xExchange") DirectExchange xExchange)* @Qualifier 参数内的值是,我们上边: @Bean(value = "yExchange"),@Bean("queueD") 注入到 IOC 容器当中的值* ,必须要是注入到了 IOC容器当中,不然是无法被使用上的,同时注意名称,不要写错了,要保持一致* @Bean(value = "yExchange")* public DirectExchange yExchange() {* @Bean("queueD")* public Queue queueD() {*/@Bean // 绑定也是要注入到 IOC 容器当中的public Binding queueDBindingX(@Qualifier("queueD") Queue queueD,@Qualifier("yExchange") DirectExchange yExchange) {// 第一个参数是 队列名(注入到了IOC容器当中了),第二个参数是交换机(注入到了IOC容器当中了)// 第三个蚕食是: 队列名对于绑定交换机之间的交互的 routingKey 信息"YD"// 表示将 队列 queueD 与 yExchange 交换机,通过 routingKey 为 "YD" 进行一个绑定return BindingBuilder.bind(queueD).to(yExchange).with("YD");}}
生产者——> 发送消息,代码
这里,生产者,指定一个延时时间(TTL),交给 X 交换机——>以 routingKey = XC 发送给 QC 队列——> 发送给死信交换机——> 以 routingKey = YD 分配给 QD 死信队列——>最后给消费者读取/消费消息。
import lombok.extern.slf4j.Slf4j;
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.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;
import java.util.Date;/*** 生产者:发送延时消息* http://localhost:8080/ttl/sendMsg/嘻嘻嘻*/@RestController
@RequestMapping("ttl")
@Slf4j
public class SendMsgController {@Resource // 注入到 IOC 容器当中private RabbitTemplate rabbitTemplate;@GetMapping("/sendExpirationMsg/{message}/{ttlTime}")public void sendMsg(@PathVariable String message,@PathVariable String ttlTime) {log.info("当前时间:{},发送一条时长{}毫秒 TTL 消息给队列QC:{}", new Date().toString(), ttlTime, message);rabbitTemplate.convertAndSend("X", "XC", message, msg -> {// 发送消息的时候,延时时长msg.getMessageProperties().setExpiration(ttlTime);return msg;});log.info("当前时间:{},发送一条时长{}毫秒 TTL 信息给队列 C:{}", new Date(), ttlTime, message);}
}
测试效果:
浏览器发送请求:
- http://localhost:8080/ttl/sendExpirationMsg/%E4%BD%A0%E5%A5%BD1/20000
- http://localhost:8080/ttl/sendExpirationMsg/%E4%BD%A0%E5%A5%BD2/2000
分析测试结果:
看起来似乎没什么问题,但是在最开始的时候,就介绍过如果使用消息属性上设置 TTL 的方式,消息可能并不会按时 “死亡” ,因为RabbitMQ 只会检查第一个消息是否过期,如果过期则丢到死信队列,如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行 。
3.3 Rabbitmq 插件实现延迟队列
上文中提到的问题,确实是一个问题,如果不能实现在消息粒度上的 TTL,并使其在设置的 TTL 时间 及时死亡,就无法设计成一个通用的延时队列。那如何解决呢,接下来我们就去解决该问题。
我们可以通过安装一个:rabbitmq-delayed-message-exchange 插件
来解决这个问题。这个插件是如何解决的呢:既然延时队列,将消息放入到队列当中存在,一个消息过期时间优先级的问题,那么我们就不将延时的消息,放入到队列当中,而是通过交换机进行处理。
rabbitmq-delayed-message-exchange 插件
就是让我们延时的消息,可以不走队列,而是通过交换机本身进行一个处理,让消费者直接从交换机当中消费,读取消息。
消息发送后不会直接投递到队列,
而是存储到 Mnesia(嵌入式数据库),检查 x-delay 时间(消息头部);
延迟插件在 RabbitMQ 3.5.7 及以上的版本才支持,依赖 Erlang/OPT 18.0 及以上运行环境;
Mnesia 是一个小型数据库,不适合于大量延迟消息的实现
解决了消息过期时间不一致(优先级)出现的问题。
安装:安装延时队列插件 详细步骤:
- 在官网上下载:https://www.rabbitmq.com/community-plugins
下载 rabbitmq_delayed_message_exchange
插件
- 然后解压放置到 RabbitMQ 的插件目录,进入 RabbitMQ 的安装目录下的 plgins 目录,执行下面命令让该插件生效。
[root@RainbowSea plugins]# cd /usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins # 进入到 RabbitMQ 插件目录
- 将下载好的
rabbitmq_delayed_message_exchange
插件,上传到 RabbitMQ 的安装目录下的 plgins 目录 当中。
- 执行
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
命令,启用该插件
[root@RainbowSea plugins]# rabbitmq-plugins enable rabbitmq_delayed_message_exchange
rabbitmq-plugins list
查看所有插件
[root@RainbowSea plugins]# rabbitmq-plugins list
添加成功的效果:
具体代码实现:
- 在这里新增了一个队列
delayed.queue
,一个自定义交换机delayed.exchange
,绑定关系如下:
编写配置类代码:
在我们自定义的交换机中,这是一种新的交换类型,该类型消息支持延迟投递机制 消息传递后并 不会立即投递到目标队列中,而是存储在 mnesia (一个分布式数据系统)表中,当达到投递时间时,才投递到目标队列中。
package com.rainbowsea.rabbitmq.springbootrabbitmq.config;import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.CustomExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.HashMap;
import java.util.Map;@Configuration
public class DelayedQueueConfig {public static final String DELAYED_QUEUE_NAME = "delayed.queue";public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";@Bean("delayedQueue") // 声明队列,基于 rabbitmq_delayed_message_exchange 插件的public Queue delayedQueue() {return new Queue(DELAYED_QUEUE_NAME);}// 声明交换机(可以算是一种自定义交换机),基于 rabbitmq_delayed_message_exchange 插件的@Bean("delayedExchange")public CustomExchange delayedExchange() {// 创建一个 Map 用于存放,设置队列信息Map<String, Object> arguments = new HashMap<>(3);// 自定义交换机的类型, 这里定义为 direct 直接类型arguments.put("x-delayed-type", "direct");/*** CustomExchange 方法参数* 1.交换机的名称* 2.交换机的类型* 3.是否需要持久化* 4.是否需要自动删除* 5.其它的参数*/return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message", true, false, arguments);}// 基于 rabbitmq_delayed_message_exchange 插件的 将队列绑定到交换机当中@Beanpublic Binding delayedQueueBindingdelayedExchange(@Qualifier("delayedQueue") Queue delayedQueue,@Qualifier("delayedExchange") CustomExchange delayedExchange) {return BindingBuilder.bind(delayedQueue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();}
}
生产者-发送消息的: 这里我们使用的是,通过生产者,设置延时时间,而不是通过队列写死延时时间的方式。
import com.rainbowsea.rabbitmq.springbootrabbitmq.config.DelayedQueueConfig;
import lombok.extern.slf4j.Slf4j;
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.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;
import java.util.Date;/*** 生产者:发送延时消息* http://localhost:8080/ttl/sendMsg/嘻嘻嘻*/@RestController
@RequestMapping("ttl")
@Slf4j
public class SendMsgController {@Resource // 注入到 IOC 容器当中private RabbitTemplate rabbitTemplate;@GetMapping("sendDelayMsg/{message}/{delayTime}")public void sendMsg(@PathVariable String message, @PathVariable Integer delayTime) {log.info(" 当前时间: {}, 发送一条延迟{} 毫秒的信息给队列 delayed.queue:{}", new Date(), delayTime, message);rabbitTemplate.convertAndSend(DelayedQueueConfig.DELAYED_EXCHANGE_NAME, DelayedQueueConfig.DELAYED_ROUTING_KEY, message,correlationData -> {// 发送消息的时候,延迟时长,单位:mscorrelationData.getMessageProperties().setDelay(delayTime);return correlationData;});}}
消费者/读取/消费——使用插件交换机当中延时的消息:
package com.rainbowsea.rabbitmq.springbootrabbitmq.consumer;import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import com.rainbowsea.rabbitmq.springbootrabbitmq.config.DelayedQueueConfig;import java.util.Date;/*** 消费者,消费/读取消息,基于rabbitmq_delayed_message_exchange 插件*/
@Slf4j
@Component
public class DelayQueueConsumer {// 监听消息@RabbitListener(queues = DelayedQueueConfig.DELAYED_QUEUE_NAME)public void receiveDelayQueue(Message message) {String msg = new String(message.getBody());log.info("当前时间:{} ,收到延时队列的消息:{}", new Date().toString(), msg);}
}
运行结果: 浏览器当中发出请求:
- http://localhost:8080/ttl/sendDelayMsg/come on baby1/20000
- http://localhost:8080/ttl/sendDelayMsg/come on baby2/2000
总结:
延时队列在需要延时处理的场景下非常有用,使用 RabbitMQ 来实现延时队列可以很好的利用 RabbitMQ 的特性,如:消息可靠发送,消息可靠投递,死信队列 来保障消息至少被消费一次以及未被正确处理的消息不会被丢弃。另外,通过 RabbitMQ 集群的特性,可以很好的解决单击故障问题,不会因为单个节点挂掉导致延时队列不可用或者消息丢失。
当然,延时队列还有很多其它选择,比如利用 Java 的 DelayQueue,利用 Redis的 zset,利用 Quartz 或者利用 kafka 的时间轮,这些方式各有特点,看需要适用的场景。
4. 补充:Docker 容器当中安装 message_exchange"延迟插件" 的详细步骤
- 确定 Docker 容器对应映射的卷的目录位置:
docker inspect rabbitmq
对应的运行结果:
"Mounts": [{"Type": "volume","Name": "rabbitmq-plugin","Source": "/var/lib/docker/volumes/rabbitmq-plugin/_data","Destination": "/plugins","Driver": "local","Mode": "z","RW": true,"Propagation": ""},{"Type": "volume","Name": "cca7bc3012f5b76bd6c47a49ca6911184f9076f5f6263b41f4b9434a7f269b11","Source": "/var/lib/docker/volumes/cca7bc3012f5b76bd6c47a49ca6911184f9076f5f6263b41f4b9434a7f269b11/_data","Destination": "/var/lib/rabbitmq","Driver": "local","Mode": "","RW": true,"Propagation": ""}]
和容器内 /plugins
目录对应的宿主机目录是:/var/lib/docker/volumes/rabbitmq-plugin/_data
注意:这里必须必须必须,将下载到的
插件
拷贝到 RabbitMQ 服务器plugins
目录下,不然时无法安装成功的。
- 下载对应的 messaging 延时插件
messaging 延时插件的官网文档地址: https://www.rabbitmq.com/community-plugins
下载在 Docker 容器当中下载安装文件:
wget https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases/download/v3.13.0/rabbitmq_delayed_message_exchange-3.13.0.ez
mv rabbitmq_delayed_message_exchange-3.13.0.ez /var/lib/docker/volumes/rabbitmq-plugin/_data
- 启用该延时插件:
# 登录进入容器内部
docker exec -it rabbitmq /bin/bash# rabbitmq-plugins命令所在目录已经配置到$PATH环境变量中了,可以直接调用
rabbitmq-plugins enable rabbitmq_delayed_message_exchange# 退出Docker容器
exit# 重启Docker容器
docker restart rabbitmq
- 插件安装成功的,确定,验证:
- 确认点1:查看当前节点已启用插件的列表:
- 确认点2:如果创建新交换机时可以在type中看到x-delayed-message选项,那就说明插件安装好了
5. 最后:
“在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。”
相关文章:
7. RabbitMQ 消息队列——延时队列(Spring Boot + 安装message_exchange“延迟插件“ 的详细配置说明)的详细讲解
7. RabbitMQ 消息队列——延时队列(Spring Boot 安装message_exchange"延迟插件" 的详细配置说明)的详细讲解 文章目录 7. RabbitMQ 消息队列——延时队列(Spring Boot 安装message_exchange"延迟插件" 的详细配置说明)的详细讲解1. RabbitMQ 延时队列概…...
使用 MyBatis-Plus 实现高效的 Spring Boot 数据访问层
在开发 Spring Boot 应用时,数据访问是不可或缺的部分。为了提高开发效率并减少样板代码,MyBatis-Plus 提供了强大的功能,能够简化与数据库交互的操作。本文将详细介绍如何在 Spring Boot 中使用 MyBatis-Plus,并结合具体代码示例…...
Linux学习笔记——零基础详解:什么是Bootloader?U-Boot启动流程全解析!
零基础详解:什么是Bootloader?U-Boot启动流程全解析! 一、什么是Bootloader?📌 举个例子: 二、U-Boot 是什么?三、U-Boot启动过程:分为两个阶段🔹 第一阶段(汇…...
网络初识 - Java
网络发展史: 单机时代(独立模式) -> 局域网时代 -> 广域网时代 -> 移动互联网时代 网络互联:将多台计算机链接再一起,完成数据共享。 数据共享的本质是网络数据传输,即计算机之间通过网络来传输数…...
(51单片机)独立按键控制流水灯LED流向(独立按键教程)(LED使用教程)
源代码 如上图将7个文放在Keli5 中即可,然后烧录在单片机中就行了 烧录软件用的是STC-ISP,不知道怎么安装的可以去看江科大的视频: 【51单片机入门教程-2020版 程序全程纯手打 从零开始入门】https://www.bilibili.com/video/BV1Mb411e7re?…...
QML输入控件: TextArea的应用(带行号的编辑器)
目录 引言📚 相关阅读🔨BUG修复实现思路代码解析主窗口代码自定义TextAreaItem组件行号显示部分文本编辑区域滚动同步 关键功能解析1. 动态更新行号2. 属性映射3. 外观定制 运行效果总结工程下载 引言 在开发Qt/QML应用程序时,文本编辑功能是…...
kafka 的存储文件结构
Kafka 的存储文件结构是其高吞吐量和高效性能的关键部分。Kafka 的存储结构是围绕 日志(Log) 的设计展开的,而每个 Kafka 分区(Partition) 都会以日志文件的形式存储。Kafka 采用了顺序写入、分段存储和索引文件的机制…...
FAISS原理深度剖析与LLM检索分割难题创新解决方案
一、FAISS核心技术解构:突破传统检索的次元壁 1.1 高维空间的降维艺术 FAISS(Facebook AI Similarity Search)通过独创的Product Quantization(乘积量化)技术,将高维向量空间切割为多个正交子空间。每个子…...
Windows操作系统安全配置(一)
1.操作系统和数据库系统管理用户身份标识应具有不易被冒用的特点,口令应有复杂度要求并定期更换 配置方法:运行“gpedit.msc”计算机配置->Windows设置->安全设置>帐户策略->密码策略: 密码必须符合复杂性要求->启用 密码长度最小值->…...
JavaScript promise实例——通过XHR获取省份列表
文章目录 需求和步骤代码示例效果 需求和步骤 代码示例 <!DOCTYPE html> <html lang"zh-CN"><head><meta charset"UTF-8"><!-- 确保IE浏览器使用最新的渲染引擎 --><meta http-equiv"X-UA-Compatible" conten…...
【力扣hot100题】(065)搜索旋转排序数组
难点在于情况真的很多需要逐一判断,画个走向图再写判断条件就行了。 还有处理边界条件也比较麻烦。 class Solution { public:int search(vector<int>& nums, int target) {int left0;int rightnums.size()-1;while(left<right){int mid(leftright)/…...
毕设论文的分类号与UDC查询的网站
毕业论文分类号 中图分类号查询链接 找到自己的细分类,一个一个点就好,然后就找到了 毕业论文UDC UDC查询...
pyqt5实现多个窗口互相调用
使用以下代码可以实现多窗口之间的相互调用: import sys from PyQt5.QtWidgets import QApplication, QMainWindow import win0, win1, win2, win3 # 分别包含 Ui_win0 和 Ui_win1class Win0Window(QMainWindow, win0.Ui_win0):def __init__(self, x, parentNone)…...
Python基于Django的企业it资产管理系统(附源码,文档说明)
博主介绍:✌IT徐师兄、7年大厂程序员经历。全网粉丝15W、csdn博客专家、掘金/华为云//InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ 🍅文末获取源码联系🍅 👇🏻 精彩专栏推荐订阅👇dz…...
浅谈在HTTP中GET与POST的区别
从 HTTP 报文来看: GET请求方式将请求信息放在 URL 后面,请求信息和 URL 之间以 ?隔开,请求信息的格式为键值对,这种请求方式将请求信息直接暴露在 URL 中,安全性比较低。另外从报文结构上来看,…...
计算机视觉5——运动估计和光流估计
一、运动估计 (一)运动场(Motion Field) 定义与物理意义 运动场是三维场景中物体或相机运动在二维图像平面上的投影,表现为图像中每个像素点的运动速度矢量。其本质是场景点三维运动(平移、旋转、缩放等&a…...
8. RabbitMQ 消息队列 + 结合配合 Spring Boot 框架实现 “发布确认” 的功能
8. RabbitMQ 消息队列 结合配合 Spring Boot 框架实现 “发布确认” 的功能 文章目录 8. RabbitMQ 消息队列 结合配合 Spring Boot 框架实现 “发布确认” 的功能1. RabbitMQ 消息队列 结合配合 Spring Boot 框架实现 “发布确认” 的功能1.1 回退消息 2.备用交换机3. API说…...
铰链损失函数 Hinge Loss和Keras 实现
一、说明 在为了了解 Keras 深度学习框架的来龙去脉,本文介绍铰链损失函数,然后使用 Keras 实现它们以进行练习并了解它们的行为方式。在这篇博客中,您将首先找到两个损失函数的简要介绍,以确保您在我们继续实现它们之前直观地理解…...
【Tips】Cloudflare用户与网站之间的中介
Cloudflare是一家提供多种网络服务的公司,旨在帮助网站和应用程序提高性能、安全性和可靠性。它为全球网站、应用程序和网络提供一系列网络安全、性能优化和可靠性服务。Cloudflare 的核心使命是让互联网更加安全、快速和可靠。 以下是其主要服务介绍:…...
PDF 转图片,一行代码搞定!批量支持已上线!
大家好,我是程序员晚枫。今天我要给大家带来一个超实用的功能——popdf 现在支持 PDF 转图片了,而且还能批量操作!是不是很激动?别急,我来手把手教你玩转这个功能。 1. 一行代码搞定单文件转换 popdf 的核心就是简单暴…...
可以使用费曼学习法阅读重要的书籍
书本上画了很多线,回头看等于没画出任何重点。 不是所有的触动都是有效的。就像你曾经看过很多好文章,当时被触动得一塌糊涂,还把它们放进了收藏夹,但一段时间之后,你就再也记不起来了。如果让你在一本书上画出令自己…...
基于Flask的笔记本电脑数据可视化分析系统
【Flask】基于Flask的笔记本电脑数据可视化分析系统(完整系统源码开发笔记详细部署教程)✅ 目录 一、项目简介二、项目界面展示三、项目视频展示 一、项目简介 该系统采用Python语言进行编写,以Flask为后端框架,利用Echarts实现数…...
JavaScript基础--17-数组的常见方法
数组的方法清单 数组的类型相关 方法描述备注Array.isArray()判断是否为数组toString()将数组转换为字符串Array.from(arrayLike)将伪数组转化为真数组Array.of(value1, value2, value3)创建数组:将一系列值转换成数组 注意,获取数组的长度是用length…...
TDengine 从入门到精通(2万字长文)
第一章:走进 TDengine 的世界 TDengine 是个啥? 如果你首次接触 TDengine,心里可能满是疑惑:这究竟是个什么东西,能派上什么用场?简单来讲,TDengine 是一款开源、高性能且云原生的时序数据库&…...
ctfshow VIP题目限免(前10题)
目录 源码泄露 前台JS绕过 协议头信息泄露 robots后台泄露 phps源码泄露 源码压缩包泄露 版本控制泄露源码 版本控制泄露源码2 vim临时文件泄露 cookie泄露 源码泄露 根据题目提示是源代码泄露,右键查看页面源代码发现了 flag 前台JS绕过 发现右键无法使用了&…...
PyTorch中的各种损失函数的详细解析与通俗理解!
目录 1. 前言 2. 回归任务中的损失函数 2.1 L1损失(Mean Absolute Error, MAE) 2.2 平方损失(Mean Squared Error, MSE) 2.3 平滑L1损失(Smooth L1 Loss) 3. 分类任务中的损失函数 3.1 交叉熵损失&a…...
leetcode53-最大子数组和
leetcode 53 思路 本题使用贪心算法,核心在于对每一个元素进行遍历,在遍历过程里,持续更新当前的子数组和以及最大子数组和 假设 sum 是当前的子数组和,max 是到目前为止最大的子数组和遍历数组时,计算每个位置的当…...
解决Long类型前端精度丢失和正常传回后端问题
在 Java 后端开发中,可能会遇到前后端交互过程中 Long 类型精度丢失的问题。尤其是在 JavaScript 中,由于其 Number 类型是双精度浮点数,超过 16 位的 Long 类型值就会发生精度丢失。 问题背景 假设有如下实体类: public class…...
Green Coding规范:从循环语句到数据库查询的节能写法
本文系统化阐述绿色编码的工程实践方案,通过重构循环控制结构、优化集合操作范式、改进数据库访问模式三大技术路径,实现典型业务系统降低28.6%的CPU指令周期消耗。基于对JVM和SQL解释器的指令级分析,提出循环展开的黄金分割法则、位图索引加…...
Spring Boot整合Elasticsearch
摘要:本文手把手教你如何在Spring Boot项目中整合Elasticsearch(ES),并实现基本的CRUD与搜索操作。包含版本选择、配置、代码示例及常见问题解决。 一、环境准备 1.1 软件版本 Spring Boot: 3.xElasticsearch: 8.x(推…...
2025年渗透测试面试题总结- 某四字大厂面试复盘扩展 一面(题目+回答)
网络安全领域各种资源,学习文档,以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具,欢迎关注。 目录 某四字大厂面试复盘扩展 一面 一、Java内存马原理与查杀 二、冰蝎与哥斯拉原理对比(技术演…...
java基础自用笔记:文件、递归、常见的字符集、IO流
文件 递归 递归文件夹寻找某个文件,要先判断是否有权限进入文件夹(若无权限返回null)然后再判断文件数量是否不为0 常见的字符集 IO流 字节流 文件字节输入流 当读取到最后一个字节的时候,装在buffer[0]中,如果后面没…...
关于计算机网络的一些疑问
目录 1. HTTP/1.x 中的“队头阻塞”(Head-of-Line Blocking)问题解释什么是队头阻塞?其他相关概念: 2. HPACK 算法是什么?HPACK 的作用:HPACK 的特点:HPACK 提升性能的原因: 3. 如何…...
超大规模数据场景(思路)——面试高频算法题目
目录 用4KB内存寻找重复元素 从40个亿中产生不存在的整数【位】 如果只让用10MB空间存储? 初次遍历 二次遍历 用2GB内存在20亿个整数中查找出现次数最多的数【分块】 从亿万个URL中查找问题【分块 堆】 40亿个非负整数中找出现两次的数【位 不过多个位哈】 …...
高级:性能优化面试题深度剖析
一、引言 在Java应用开发中,性能优化是确保系统高效运行的关键。面试官通过相关问题,考察候选人对性能优化的理解和实践经验。本文将深入探讨Java应用性能优化的方法,包括JVM调优、数据库优化等,结合实际开发场景,帮助…...
【软件】在 macOS 上安装和配置 Apache HTTP 服务器
在 macOS 上安装 Apache HTTP 服务器的步骤: 1.安装 Apache HTTP 服务器 macOS 系统可能已经预装了 Apache HTTP 服务器。你可以通过终端检查它是否已经安装: httpd -v如果系统提示command not found,说明 Apache 未安装。你可以通过 Home…...
数据结构之链表
定义:在计算机科学中,链表是数据元素的线性集合,其每个元素都指向下一个元素,元素存储上并不连续。 链表分类:单向链表(每个元素知道下一个元素是谁)、双向链表(每个元素知道其上一…...
基于Python的懂车帝汽车数据爬虫分析与可视化系统
【Python】基于Python的懂车帝汽车数据爬虫分析与可视化系统 (完整系统源码开发笔记详细部署教程)✅ 目录 一、项目简介二、项目界面展示三、项目视频展示 一、项目简介 🚗🔥【视频简介】🔥🚗 大家好&…...
setInterval问题以及前端如何实现精确的倒计时
一、为什么setInterval不能实现 原因有两:1、js是单线程,基于事件循环执行其他任务(这里建议读者可以多去了解一下浏览器线程与事件循环相关知识) 2、setinterval是每隔delay时间,把逻辑放到任务队列中,而…...
Python爬虫教程010:使用scrapy爬取当当网数据并保存
文章目录 3.6 爬取当当网数据3.6.1 创建项目3.6.2 查找要爬取的数据对象3.6.3 保存数据3.6 爬取当当网数据 3.6.1 创建项目 【1、创建项目】: scrapy startproject scrapy_dangdang_095【2、创建爬虫文件】 cd scrapy_dangdang_095\scrapy_dangdang_095\spiders scrapy ge…...
达芬奇预设:复古16mm胶片质感老式电影放映机转场过渡+音效
达芬奇预设:复古16mm胶片质感老式电影放映机转场过渡音效 特征: DaVinci Resolve 宏 8 过渡 幻灯片投影仪效果 可在任何帧速率和分辨率下工作 教程包括 系统要求: 达芬奇 Resolve 18.0...
Spring MVC 的请求处理流程是怎样的?
Spring MVC 请求处理流程的大致可分为以下几个步骤: 1. 请求到达 DispatcherServlet: 所有请求首先到达 DispatcherServlet(前端控制器)。DispatcherServlet 是 Spring MVC 的核心,它负责接收请求,并将请求委派给其他…...
PyTorch 实现图像版多头注意力(Multi-Head Attention)和自注意力(Self-Attention)
本文提供一个适用于图像输入的多头注意力机制(Multi-Head Attention)PyTorch 实现,适用于 ViT、MAE 等视觉 Transformer 中的注意力计算。 模块说明 输入支持图像格式 (B, C, H, W)内部转换为序列 (B, N, C),其中 N H * W多头注…...
STM32单片机入门学习——第17节: [6-5] TIM输入捕获
写这个文章是用来学习的,记录一下我的学习过程。希望我能一直坚持下去,我只是一个小白,只是想好好学习,我知道这会很难,但我还是想去做! 本文写于:2025.04.06 STM32开发板学习——第17节: [6-5] TIM输入捕获 前言开发板说明引用解答和科普一…...
P8819 [CSP-S 2022] 星战 Solution
Preface 不可以,总司令 的来源. Description 给定一张 n n n 点 m m m 边的有向图 G G G,有 q q q 次操作分四种: 1 u v:使边 u → v u\to v u→v 失活.2 u:使点 u u u 的所有入边失活.3 u v:使边…...
【spring02】Spring 管理 Bean-IOC,基于 XML 配置 bean
文章目录 🌍一. bean 创建顺序🌍二. bean 对象的单例和多例❄️1. 机制❄️2. 使用细节 🌍三. bean 的生命周期🌍四. 配置 bean 的后置处理器 【这个比较难】🌍五. 通过属性文件给 bean 注入值🌍六. 基于 X…...
Llama 4架构解析与本地部署指南:MoE模型在170亿参数下的效率突破
Meta最新发布的Llama 4系列标志着开源大语言模型(LLM)的重大演进,其采用的混合专家(MoE)架构尤为引人注目。 两大核心模型——Llama 4 Scout(170亿参数含16专家)和Llama 4 Maverick(170亿参数含128专家)——展现了Meta向高效能AI模型的战略转型,这些模型在挑战传统扩…...
`docker run --restart no,always,on-failure,unless-stopped`笔记250406
docker run --restart no,always,on-failure,unless-stopped 笔记250406 docker run --restart 用于配置容器的自动重启策略,当容器意外退出时,Docker 会根据策略自动重新启动容器。这是确保服务高可用的重要参数。 语法 docker run --restart <策略…...
stl的VS的string的内部实现,引用计数的写实拷贝,编码
本章目标 1.stl的vs的string的内部实现 2.引用计数的写实拷贝 3.编码 1.stl的string的内部实现 我们先来看一个例子 string s1; cout<<sizeof(s1)<<endl;我们知道类的内存管理也是遵循内存对齐的规则的. 我们假设当前机器的环境是32位的.string类的内部有三个成…...
Docker 从入门到进阶 (Win 环境) + Docker 常用命令
目录 引言 一、准备工作 1.1 系统要求 1.2 启用虚拟化 二、安装Docker 2.1 安装WSL 2 2.2 安装Docker Desktop 2.3检查是否安装成功 三、配置Docker 3.1 打开Docker配置中心 四、下载和管理Docker镜像 4.1 拉取镜像 4.2 查看已下载的镜像 4.3 运行容器 4.4 查看正…...