【RabbitMQ】延迟队列之死信交换机

🎉🎉欢迎来到我的CSDN主页!🎉🎉

🏅我是Java方文山,一个在CSDN分享笔记的博主。📚📚

🌟推荐给大家我的专栏《RabbitMQ实战》。🎯🎯

👉点击这里,就可以查看我的主页啦!👇👇

Java方文山的个人主页

🎁如果感觉还不错的话请给我点赞吧!🎁🎁

💖期待你的加入,一起学习,一起进步!💖💖

请添加图片描述

✨前言

了解延迟队列之前我们先了解两个概念TTL和 DXL两个概念:

TTL概念

TTL 顾名思义:指的是消息的存活时间,RabbitMQ可以通过x-message-tt参数来设置指定Queue(队列)和 Message(消息)上消息的存活时间,它的值是一个非负整数,单位为微秒。

RabbitMQ 可以从两种维度设置消息过期时间,分别是队列和消息本身

设置队列过期时间,那么队列中所有消息都具有相同的过期时间。
设置消息过期时间,对队列中的某一条消息设置过期时间,每条消息TTL都可以不同。
如果同时设置队列和队列中消息的TTL,则TTL值以两者中较小的值为准。而队列中的消息存在队列中的时间,一旦超过TTL过期时间则成为Dead Letter(死信)。

 DLX概念

DLX,全称为 Dead-Letter-Exchange,可以称之为死信交换器,也有人称之为死信邮箱。当
消息在一个队列中变成死信(dead message)之后,它能被重新被发送到另一个交换器中,这个
交换器就是 DLX,绑定 DLX 的队列就称之为死信队列或者可以称之为延迟队列。
消息变成死信一般是由于以下几种情况:
🎈 消息被拒绝( Basic.Reject/Basic.Nack ),并且设置 requeue 参数为 false
🎈 消息过期(TTL过期);
🎈队列达到最大长度。
延迟队列存储的对象是对应的延迟消息,所谓“延迟消息”是指当消息被发送以后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。
延迟队列的使用场景有很多,比如:
  • 在订单系统中,一个用户下单之后通常有 30 分钟的时间进行支付,如果 30 分钟之内没有支付成功,那么这个订单将进行异常处理,这时就可以使用延迟队列来处理这些订单了。
  • 用户希望通过手机远程遥控家里的智能设备在指定的时间进行工作。这时候就可以将用户指令发送到延迟队列,当指令设定的时间到了再将指令推送到智能设备。

AMQP 协议中,或者 RabbitMQ 本身没有直接支持延迟队列的功能,但是可以通过前面
所介绍的 DLX TTL 模拟出延迟队列的功能。

🎉死信交换机的使用

1.案例分析

下面我会讲解两种使用死信的方式:

第一种是我们不为正常的交换机设置消费者,为该队列中的消息设置TTL如果消息过期了就会变为死信就会被发送到死信交换机中处理对应的事务

假设我们有一个订单系统,订单有一个待支付状态,如果在30分钟内未支付,则自动变为已取消状态。我们可以通过 RabbitMQ 的 TTL 机制和死信队列来实现这个功能。

具体步骤如下:

  1. 创建一个普通的交换机(例如 orderExchange)和一个普通的队列(例如 orderQueue)。将队列绑定到交换机上。

  2. 创建一个死信交换机(例如 deadLetterExchange)和一个死信队列(例如 deadLetterQueue)。将死信队列绑定到死信交换机上。

  3. 将普通队列设置为有 TTL 的队列,并指定 TTL 的时间为30分钟。这样,如果消息在队列中存活时间超过30分钟,就会变为死信。

  4. 设置普通队列的死信交换机和死信路由键。当消息变为死信时,会被发送到死信交换机,并根据死信路由键路由到对应的死信队列。

  5. 创建一个死信消费者,监听死信队列中的消息。当收到订单消息时,判断订单是否已经支付,如果未支付则将其修改为已取消状态。

这种方式可以灵活地处理订单超时自动取消的需求,并且不需要为每个订单单独创建消费者,降低了系统的复杂性。同时,通过使用 RabbitMQ 的 TTL 机制和死信队列,还可以实现其他类似的延迟任务处理场景。

第二种则是为正常队列创建一个消费者但是开启手动确认,什么意思呢,我们的RabbitMQ中的消费者都是自动消费的,所以我们可以设置为手动确认消费,我接收到你这个消息了,但我还未处理,而是由消费者主动发送确认信号(ACK)给 RabbitMQ,告知消息已经成功处理,这条消息才算是被消费了。

以下情况会使用这种方式:

  1. 并发处理:当多个消费者同时消费同一个队列中的消息时,为了保证消息不被重复消费或丢失,可以使用手动签收。消费者在处理完消息后,手动发送 ACK 确认消息处理完成,这样 RabbitMQ 就知道该消息已经被正确处理,可以将其标记为已消费。

  2. 消息处理失败:如果消费者在处理消息时发生了异常或错误,可以选择不发送 ACK,即不确认消息的消费完成。这样 RabbitMQ 就会重新将该消息发送给其他消费者进行处理,确保消息不会丢失。

  3. 消息处理耗时较长:某些消息的处理可能需要较长的时间,为了防止 RabbitMQ 认为该消息处理超时而重新发送给其他消费者,可以使用手动签收。消费者在开始处理消息时,发送 ACK 告知 RabbitMQ 接收到消息并开始处理,然后在处理完成后再发送 ACK 确认消息处理完成。

2.案例1实践

创建交换机、队列以及它们的绑定关系
package org.example.produce.config;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

@Configuration
@SuppressWarnings("all")
public class RabbitConfig {
   

    /**
     * 定义正常队列
     * @return
     */
    @Bean
    public Queue QueueA(){
        Map<String, Object> config = new HashMap<>();
        config.put("x-message-ttl", 10000);//message在该队列queue的存活时间最大为10秒
        config.put("x-dead-letter-exchange", "deadExchange"); //x-dead-letter-exchange参数是设置该队列的死信交换器(DLX)
        config.put("x-dead-letter-routing-key", "bb");//x-dead-letter-routing-key参数是给这个DLX指定路由键
        return new Queue("QueueA", true, true, false, config);
    }


    /**
     * 定义死信队列
     * @return
     */
    @Bean
    public Queue QueueB(){
        return new Queue("QueueB");
    }

    /**
     * 自定义直连交换机
     * @return
     */
    @Bean
    public DirectExchange directExchangeA(){
        return new DirectExchange("direct-exchangeA",true,false);
    }

    /**
     * 自定义死信交换机
     * @return
     */
    @Bean
    public DirectExchange directExchangeB(){
        return new DirectExchange("direct-exchangeB",true,false);
    }

    /**
     * 将正常队列与直连交换机进行绑定,并设置路由键与死信交换机以及队列
     * @return
     */
    @Bean
    public Binding bindingExchangeA(){
        return BindingBuilder.bind(QueueA())
                .to(directExchangeA())
                .with("aa");
    }

    /**
     * 将死信队列与死信交换机进行绑定,并设置路由键
     * @return
     */
    @Bean
    public Binding bindingExchangeB(){
        return BindingBuilder.bind(QueueB())
                .to(directExchangeB())
                .with("bb");
    }

}
  1. x-message-ttl 参数是设置消息在队列中的存活时间,单位为毫秒。在这个例子中,设置了 10000 毫秒,即 10 秒钟。如果一个消息在该队列中未被消费者接收并确认处理,那么该消息就会被自动移除出队列,避免消息的过期占用队列资源。

  2. x-dead-letter-exchange 参数是设置该队列的死信交换器(DLX),表示当一个消息变成死信时,会被发送到指定的 DLX 中。在这个例子中,设置的死信交换器为 "direct-exchangeB",即将死信消息发送到名为 "direct-exchangeB" 的交换器。

  3. x-dead-letter-routing-key 参数是给该 DLX 指定一个路由键。当消息变成死信时,会根据该路由键将它发送到绑定该路由键的队列中。在这个例子中,设置的路由键为 "bb",即将死信消息发送到名为 "QueueB" 的队列中。

 创建消息的生产者
package org.example.produce.controller;

import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
public class Sender {
    @Autowired
    private AmqpTemplate rabbitTemplate;

    @RequestMapping("/send")
    public String send() {
        Map<String,Object> data=new HashMap<>();
        data.put("msg","订单ID:121452623345");
        rabbitTemplate.convertAndSend("direct-exchangeA","aa", data);
        return "😎";
    }
}
创建死信队列的消费者 
package org.example.produce.controller;

import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.util.Map;

@Component
@RabbitListener(queues = {"QueueB"})
public class BReceiver {
    @RabbitHandler
    public void handler(Map<String,Object> json){
        System.out.println(json);
    }
}

效果展示: 

 访问一下http://localhost:8081/send

访问一下http://118.178.124.148:15672

可以看到已经有我们的队列了,现在我们开启消费者服务查看一下

也是可以拿到原先队列中的消息的,说明我们的死信交换机和死信队列生效了

RabbitMQ死信队列优化

如果我们想要第一条消息在6s后变成了死信消息,然后被消费者消费掉,第二条消息在60s之后变成了死信消息,然后被消费掉,这样,岂不是每增加一个新的时间需求,就要新增一个队列,这里只有6s和60s两个时间选项,如果需要一个小时后处理,那么就需要增加TTL为一个小时的队列,如果是预定会议室然后提前通知这样的场景,岂不是要增加无数个队列才能满足需求??

其实我们可以增加一个延时队列,用于接收设置为任意延时时长的消息,增加一个相应的死信队列和routingkey

创建交换机、队列以及它们的绑定关系

package org.example.produce.config;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

@Configuration
@SuppressWarnings("all")
public class RabbitConfig {


    /**
     * 定义正常队列
     * @return
     */
    @Bean
    public Queue QueueA(){
        Map<String, Object> config = new HashMap<>();
        config.put("x-message-ttl", 10000);//message在该队列queue的存活时间最大为10秒
        config.put("x-dead-letter-exchange", "direct-exchangeB"); //x-dead-letter-exchange参数是设置该队列的死信交换器(DLX)
        config.put("x-dead-letter-routing-key", "bb");//x-dead-letter-routing-key参数是给这个DLX指定路由键
        return new Queue("QueueA", true, true, false, config);
    }


    /**
     * 定义死信队列
     * @return
     */
    @Bean
    public Queue QueueB(){
        return new Queue("QueueB");
    }


    // 声明延时队列C 不设置TTL
    @Bean
    public Queue QueueC(){
        Map<String, Object> config = new HashMap<>();
        // x-dead-letter-exchange    这里声明当前队列绑定的正常交换机
        config.put("x-dead-letter-exchange","direct-exchangeA");
        // x-dead-letter-routing-key  这里声明当前队列的死信路由key
        config.put("x-dead-letter-routing-key", "aa");
        return new Queue("QueueC", true, true, false, config);
    }

    /**
     * 自定义直连交换机
     * @return
     */
    @Bean
    public DirectExchange directExchangeA(){
        return new DirectExchange("direct-exchangeA",true,false);
    }

    /**
     * 自定义死信交换机
     * @return
     */
    @Bean
    public DirectExchange directExchangeB(){
        return new DirectExchange("direct-exchangeB",true,false);
    }

    /**
     * 自定义延迟交换机
     * @return
     */
    @Bean
    public DirectExchange directExchangeC(){
        return new DirectExchange("direct-exchangeC",true,false);
    }
    /**
     * 将正常队列与直连交换机进行绑定,并设置路由键与死信交换机以及队列
     * @return
     */
    @Bean
    public Binding bindingExchangeA(){
        return BindingBuilder.bind(QueueA())
                .to(directExchangeA())
                .with("aa");
    }

    /**
     * 将死信队列与死信交换机进行绑定,并设置路由键
     * @return
     */
    @Bean
    public Binding bindingExchangeB(){
        return BindingBuilder.bind(QueueB())
                .to(directExchangeB())
                .with("bb");
    }
    /**
     * 将正常队列与直连交换机进行绑定,并设置路由键与死信交换机以及队列
     * @return
     */
    @Bean
    public Binding bindingExchangeC(){
        return BindingBuilder.bind(QueueC())
                .to(directExchangeC())
                .with("cc");
    }


/



}

 创建消息的生产者

package org.example.produce.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@RestController
@Slf4j
public class Sender {
    @Autowired
    private AmqpTemplate rabbitTemplate;


    @RequestMapping("send02")
    public void sendMsg( Integer delay) {
        Map<String,Object> data=new HashMap<>();
        data.put("msg","延迟队列");
        rabbitTemplate.convertAndSend("direct-exchangeC", "cc",data , message -> {
            // 设置延迟毫秒值
            message.getMessageProperties().setExpiration(String.valueOf(delay * 1000));
            return message;
        });
    }
}


 

3.案例2实践

  • 消息通过 ACK 确认是否被正确接收,每个 Message 都要被确认(acknowledged),可以手动去 ACK 或自动 ACK

  • 自动确认会在消息发送给消费者后立即确认,但存在丢失消息的可能,如果消费端消费逻辑抛出异常,也就是消费端没有处理成功这条消息,那么就相当于丢失了消息

  • 如果消息已经被处理,但后续代码抛出异常,使用 Spring 进行管理的话消费端业务逻辑会进行回滚,这也同样造成了实际意义的消息丢失

  • 如果手动确认则当消费者调用 ack、nack、reject 几种方法进行确认,手动确认可以在业务失败后进行一些操作,如果消息未被 ACK 则会发送到下一个消费者

  • 如果某个服务忘记 ACK 了,则 RabbitMQ 不会再发送数据给它,因为 RabbitMQ 认为该服务的处理能力有限

  • ACK 机制还可以起到限流作用,比如在接收到某条消息时休眠几秒钟

  • 消息确认模式有:

    • AcknowledgeMode.NONE:自动确认

    • AcknowledgeMode.AUTO:根据情况确认

    • AcknowledgeMode.MANUAL:手动确认

配置yml文件关闭自动确认
server:
    port: 9999
spring:
    application:
        name: consume
    rabbitmq:
        host: localhost
        username: weiwei
        password: 123456
        port: 5672
        virtual-host: my_vhost
        listener:
            simple:
                acknowledge-mode: manual
为QueueA创建一个消费者并且手动确认

在刚刚上一个案例中我们不是没有为正常队列创建消费者吗,现在我们创建一个

package org.example.produce.controller;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.Map;

@Component
@RabbitListener(queues = {"QueueA"})
public class AReceiver {
    @RabbitHandler
    public void handler(Map<String,Object> json, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws Exception {
        System.out.println("QA接到消息"+json); // 打印接收到的消息
        channel.basicAck(tag,true); // 确认消息已被消费
    }
}

需要注意的 basicAck 方法需要传递两个参数

  • deliveryTag(唯一标识 ID):当一个消费者向 RabbitMQ 注册后,会建立起一个 Channel ,RabbitMQ 会用 basic.deliver 方法向消费者推送消息,这个方法携带了一个 delivery tag, 它代表了 RabbitMQ 向该 Channel 投递的这条消息的唯一标识 ID,是一个单调递增的正整数,delivery tag 的范围仅限于 Channel

  • multiple:为了减少网络流量,手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息

 现在我们看一下我们的消息是否会是怎么样的

2024-01-25 19:55:27.744  INFO 13668 --- [nio-8081-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2024-01-25 19:55:27.744  INFO 13668 --- [nio-8081-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2024-01-25 19:55:27.745  INFO 13668 --- [nio-8081-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
QA接到消息{msg=订单ID:121452623345}
QA接到消息{msg=订单ID:121452623345}

直接被QA接收消费,那么如果我拒绝呢?

消费者拒绝消息
package org.example.produce.controller;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;

import java.util.Map;

@Component
@RabbitListener(queues = {"QueueA"})
public class AReceiver {
    @RabbitHandler
    public void handler(Map<String,Object> json, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws Exception {
        System.out.println("拒绝"+json);
        channel.basicReject(tag,false);  // 拒绝消息
        Thread.sleep(1000);
    }
}

basicReject(tag,false)是用于拒绝消息并可选择是否将消息重新放回队列的方法。

具体来说,basicReject() 方法用于告知 RabbitMQ 服务器拒绝处理特定的消息,并可以选择将消息重新放回队列中等待重新投递。这样可以用于处理无法处理的消息或者避免消息丢失。

  • 参数 tag 表示消息的标签(delivery tag),它是一个唯一的标识符,用于表示消息在通道中的位置。每个消息都会被分配一个单独的标签。通过将正确的标签传递给 basicReject() 方法,可以告诉 RabbitMQ 服务器要拒绝哪个具体的消息。
  • 第二个参数 false 表示不将消息重新放回队列,即将消息丢弃。如果将其设置为 true,则表示将消息重新放回队列中等待重新投递。

 

被拒绝就会变成死信消息转到我们的死信交换机然后发送给死信队列

但是我们的死信也没有进行消费 ,只是消息保存在了队列中,那是因为我们开启了全局的手动消息确认,也就是上面所编写的配置,我们只需要像刚刚那样手动确认即可
 

package org.example.produce.controller;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;

import java.util.Map;

@Component
@RabbitListener(queues = {"QueueB"})
public class BReceiver {
    @RabbitHandler
    public void handler(Map<String,Object> json, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws Exception {
        System.out.println("QB接到消息"+json); // 打印接收到的消息
        channel.basicAck(tag,true); // 确认消息已被消费

    }
}

 可以看到在消息被拒后消息就会跑到死信队列中做处理

2024-01-25 20:00:53.759  INFO 13444 --- [  restartedMain] org.example.produce.ProduceApplication   : Started ProduceApplication in 3.916 seconds (JVM running for 4.403)
2024-01-25 20:01:16.094  INFO 13444 --- [nio-8081-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2024-01-25 20:01:16.095  INFO 13444 --- [nio-8081-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2024-01-25 20:01:16.095  INFO 13444 --- [nio-8081-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 0 ms
拒绝{msg=订单ID:121452623345}
QB接到消息{msg=订单ID:121452623345}

请添加图片描述

到这里我的分享就结束了,欢迎到评论区探讨交流!!

💖如果觉得有用的话还请点个赞吧 💖

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/348601.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

10.多柱状图(MuliBarChart)

愿你出走半生,归来仍是少年&#xff01; 环境&#xff1a;.NET 7、MAUI 话接上回&#xff08;9.单柱状图&#xff08;SingleBarChart&#xff09;&#xff09;&#xff0c;从单柱拓展到多柱状图。 1.数据设置 private void InitValue(List<BasicSerieDto> dtos){Serie…

[ACM学习] 进制转换

进制的本质 本质是每一位的数位上的数字乘上这一位的权重 将任意进制转换为十进制 原来还很疑惑为什么从高位开始&#xff0c;原来从高位开始的&#xff0c;可以被滚动地乘很多遍。 将十进制转换为任意进制

泽众云真机-机型集中化运维方案升级全面完成

2024年元月份&#xff0c;泽众云真机运维团队&#xff0c;经过几个月软硬件多轮安装调试&#xff0c;机型集中化运维方案升级全面完成。解决了云真机的机型集中化运维难题&#xff0c;方便了运营人员手机管理。 具体如下&#xff1a; 1、集中化运维&#xff0c;如服务器、PC、…

数学建模学习笔记||一文了解美赛论文如何写作

目录 ​编辑 Title/标题 要求 形式 Summary Sheet/摘要 要求 三要素 书写特点 内容 开头段 中间段 格式 内容 结尾段 关键词 Contents/目录 Introduction/引言 Problem Background/问题背景 Restatement of the Problem/问题重述 Literature Review/文献综…

RabbitMQ 笔记一

概览&#xff1a; MQ基本概念 RabbitMQ入门 基本工作模 1.MQ是什么&#xff1f; MQ:Message Queue, 存储消息的中间件&#xff0c;是消息发送过程中的暂存容器&#xff0c;主要用于解决分布式系统进程间的通信。 分布式系统通信的两种方式&#xff1a;直接远程调用、借助第三…

vue---打印本地当前时间Demo

<template><view class"content" tap"getCurrentTime()">打印时间</view> </template><script>export default {data() {return {title: Hello}},onLoad() {},methods: {getCurrentTime() {//获取当前时间并打印var _this …

E7数据库备份和恢复

E7数据库备份和恢复 一、实验目的 在Mysql上&#xff0c;学习如何备份数据库和恢复的各种方法。 二、实验要求: 1、基本硬件配置:英特尔Pentium III 以上,大于4G内存&#xff1b; 2、软件要求:Mysql&#xff1b; 3、时间:1小时&#xff1b; 4、撰写实验报告并按时提交。 三、…

windows下git pull超时,ping不通github

报错 ssh: connect to host github.com port 22: Connection timed out fatal: Could not read from remote repository. Please make sure you have the correct access rights and the repository exists. 解决办法 修改hosts 最后加一行&#xff0c;文件位置&#xff1a;…

利用 “diart“ 和 OpenAI 的 Whisper 简化实时转录

利用 "diart" 和 OpenAI 的 Whisper 简化实时转录 工作原理 Diart 是一个基于人工智能的 Python 库&#xff0c;用于实时记录说话者语言&#xff08;即 "谁在什么时候说话"&#xff09;&#xff0c;它建立在 pyannote.audio 模型之上&#xff0c;专为实时…

Ubuntu 22.04安装Nginx负载均衡

君衍. 一、编译安装Nginx二、轮询算法实现负载均衡三、加权轮询算法实现负载均衡四、ip_hash实现负载均衡 一、编译安装Nginx 这里我们先将环境准备好&#xff0c;我使用的是Ubuntu22.04操作系统&#xff1a; 这个是我刚安装好的&#xff0c;所以首先我们进行保存快照防止安装…

金融OCR领域实习日志(二)——四种OCR模型效果测试(附图)

文章目录 四种模型ocr效果简单测试模型场景1.paddle框架下PP-OCRv31.1.效果如下&#xff1a;1.2.总结 2.paddle框架下ppocr_server_v22.1.效果如下2.2.总结 3.CnOCR3.1.效果如下3.2.总结 4.TesseractOCR4.1.效果如下4.2.总结 5.后续想法 四种模型ocr效果简单测试 模型 PP-OCR…

文件操作---C++

文件操作目录 1.文本文件1.1写文件1.2读文件1.2.1第一种方式&#xff1a;流输入方式1.2.2第二种方式&#xff1a;getline成员函数1.2.3第三种方式&#xff1a;getline全局函数1.2.4第四种方式&#xff1a;按一个一个字符读取 2.二进制文件2.1写文件2.2读文件 程序运行时产生的数…

【C++进阶】STL容器--list使用迭代器问题分析

目录 前言 1. list的基本使用 1.1 list构造函数 1.2 list迭代器 1.3 list capacity 1.4 list元素访问 1.5 list 修改操作 insert erase swap resize clear 2. list失效迭代器问题 3. list使用算法库函数问题 总结 前言 list&#xff08;链表&#xff09;在C中非常重要…

分享7种SQL的进阶用法

分享7种SQL的进阶用法 前言 还只会使用SQL进行简单的insert、update、detele吗&#xff1f;本文给大家带来7种SQL的进阶用法&#xff0c;让大家在平常工作中使用SQL简化复杂的代码逻辑。 1.自定义排序&#xff08;ORDER BY FIELD&#xff09; 在MySQL中ORDER BY排序除了可以…

vue模拟聊天页面列表:滚动到底部,滚动到顶部触发加载更多

先看下效果&#xff1a; 代码&#xff1a; <template><div><div style"text-align: center"><button click"scrollTop">滚动到顶部</button><button click"scrollBottom">滚动到底部</button></d…

GitHub Copilot 与 ChatGPT:哪种工具更适合软件开发?

GitHub Copilot 与 ChatGPT&#xff1a;哪种工具更适合软件开发&#xff1f; 比较 ChatGPT 与 GitHub Copilot编程语言功能性定制化训练数据上下文准确性 ChatGPT 与 GitHub Copilot&#xff1a;哪个更适合软件开发&#xff1f;常见问题解答&#xff1a; 不断发展的编程世界正在…

基数排序算法

1. 排序算法分类 十种常见排序算法可以分为两大类&#xff1a; 比较类排序&#xff1a; 通过比较来决定元素间的相对次序&#xff0c;由于其时间复杂度不能突破O(nlogn)&#xff0c;因此也称为非线性时间比较类排序。比较类排序算法包括&#xff1a;插入排序、希尔排序、选择…

matlab绘图杂谈-stem函数和plot函数

出发点 今天在论文中看到一副这样的图&#xff0c;它既有曲线&#xff0c;又有点&#xff0c;并且对两者都添加了图例。三条曲线应该是用plot函数绘制的&#xff0c;而target哪个绿色的圆圈&#xff0c;我的理解是用stem函数绘制的。它只是1个点&#xff0c;并且没有竖线&…

Ps:可选颜色

可选颜色 Selective Color命令可以按指定的颜色&#xff08;范围&#xff09;进行单独的调整&#xff0c;且不会影响图像中的其他颜色。 Ps菜单&#xff1a;图像/调整/可选颜色 Adjustments/Selective Color Ps菜单&#xff1a;图层/新建调整图层/可选颜色 New Adjustment Laye…

Qt 基于海康相机 的视频标绘

需求&#xff1a; 基于 视频 进行 标注&#xff0c;从而进行测量。 曾经搞在线教育时&#xff0c;尝试在视频上进行文字或者图形的绘制&#xff0c;但是发现利用Qt widget 传sdk 句柄的方式&#xff0c;只能使用窗口叠加的方式&#xff08;Qt 基于海康相机的视频绘图_海康相…