RabbitMQ队列

RabbitMQ队列

1、死信的概念

​ 先从概念解释上搞清楚这个定义,死信,顾名思义就是无法被消费的消息,字面意思可以这样理解,一般来说,producer将消息投递到broker或者直接到queue里了,consumer 从 queue取出消息进行消费,但某些时候由于特定的原因导致queue中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信自然就有了死信队列。
​ 应用场景:为了保证订单业务的消息数据不丢失,需要使用到RabbitMQ的死信队列机制,当消息消费发生异常时,将消息投入死信队列中.还有比如说:用户在商城下单成功并点击去支付后在指定时间未支付时自动失效

1.1、死信的来源

  • 消息TTL过期;
  • 队列达到最大长度(队列满了,无法再添加数据到mq.中);
  • 消息被拒绝(basic.reject 或 basic.nack)并且requeue=false;

1.2、死信实战

架构图

image-20240301203605503

1.3、消费者01

//死信队列
public class DeadMessageConsumer01 {//普通交换机public static final String NORMAL_EXCHANGE = "normal_exchange";//死信交换机public static final String DEAD_EXCHANGE = "dead_exchange";//普通队列public static final String NORMAL_QUEUE = "normal_queue";//死信队列public static final String DEAD_QUEUE = "dead_queue";public static void main(String[] args) throws IOException, TimeoutException {Channel channel = RabbitMQUtils.getChannel();//声明普通和死信交换机channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);//声明普通队列,这一次需要使用参数了Map<String, Object> arguments = new HashMap<>();//这里就要设置死信到死信交换机上了arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE);//设置死信routingKeyarguments.put("x-dead-letter-routing-key","deadKey");channel.queueDeclare(NORMAL_QUEUE,false,false,false,arguments);//声明死信队列channel.queueDeclare(DEAD_QUEUE,false,false,false,null);//开始分别将死信和普通交换机与队列绑定channel.queueBind(NORMAL_QUEUE,NORMAL_EXCHANGE,"normalKey");channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"deadKey");DeliverCallback deliverCallback = (consumerTag,message) ->{System.out.println("死信测试普通队列消费者01收到消息:"+new String(message.getBody()));};channel.basicConsume(NORMAL_QUEUE,true,deliverCallback,consumerTag -> {});}}

1.4、生产者

//死信队列的生产者
public class DeadMessageProducers {//普通交换机public static final String NORMAL_EXCHANGE = "normal_exchange";public static void main(String[] args) throws IOException, TimeoutException {Channel channel = RabbitMQUtils.getChannel();//死信消息设置TTL过期时间,设置10秒AMQP.BasicProperties basicProperties = new AMQP.BasicProperties().builder().expiration("10000").build();//发10条消息for (int i = 1; i < 11; i++) {String message = "dead"+i;channel.basicPublish(NORMAL_EXCHANGE,"normalKey",basicProperties,message.getBytes());System.out.println("发送消息成功:"+ message);}}
}

​ 先把消费者启动,将交换机和队列先声明出来,然后关掉消费者再启动生产者,生产者一直发这10个消息,但是消费者已经停止了,没人消费,所以TTL一到期就会被放进死信队列里,我们在web管理界面就能看到,普通队列的10条消息很快就跑到死信队列中了。

image-20240302110147069

image-20240302111240369

​ 然后现在死信队列里有那10条消息,再来一个专门消费死信消息的消费者2,非常简单,只需要消费死信队列即可。

//死信队列
public class DeadMessageConsumer02 {//死信队列public static final String DEAD_QUEUE = "dead_queue";public static void main(String[] args) throws IOException, TimeoutException {Channel channel = RabbitMQUtils.getChannel();DeliverCallback deliverCallback = (consumerTag,message) ->{System.out.println("死信测试普通队列消费者02收到消息:"+new String(message.getBody()));};channel.basicConsume(DEAD_QUEUE,true,deliverCallback,consumerTag -> {});}
}

启动它消费死信队列

image-20240302111857175

消费完成,看看管理界面,死信消息归0

image-20240302111933926

1.5、队列达到最大长度的死信

​ 在原来的消费者基础上增加一条

//设置队列最大长度
arguments.put("x-max-length",6);

​ 原来的生产者也不需要给消息设置过期时间了直接发送消息即可。

​ 测试之前先将原来的普通队列删除再启动消费者

image-20240302134737969

​ 仍然把消费者停掉,生产者启动,发送10条消息6条正常4条超过了限制会被放到死信队列

image-20240302134915165

1.6、消息被拒死信队列

我们先把原来的消息全部消费掉或者直接删除原来的队列,再演示新的。

消费者做一些修改

DeliverCallback deliverCallback = (consumerTag,message) ->{String msg = new String(message.getBody());if (msg.equals("dead5")){System.out.println("拒绝第5个消息:"+ msg);//basicReject第二个参数是是否还放回队列channel.basicReject(message.getEnvelope().getDeliveryTag(),false);} else {System.out.println("死信测试普通队列消费者01收到消息:" + msg);channel.basicAck(message.getEnvelope().getDeliveryTag(),false);}};
channel.basicConsume(NORMAL_QUEUE,false,deliverCallback,consumerTag -> {});

生产者发送10条消息,看到消费者01拒绝了第5个消息,其他的正常收到

image-20240302143440745

通过管理界面看到第5条消息放到了死信队列中

image-20240302143507611

启动专门消费死信队列的消费者02将第5个消息消费。

image-20240302143537357

2、延迟队列

2.1、延迟队列概念

​ 延时队列,队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望在指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列。

其实,TTL过期导致的死信队列就是延迟队列

应用场景:

  • 1.订单在十分钟之内未支付则自动取消
  • ⒉新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。
  • 3.用户注册成功后,如果三天内没有登陆则进行短信提醒。
  • 4.用户发起退款,如果三天内没有得到处理则通知相关运营人员。
  • 5.预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议

​ 这些场景都有一个特点,需要在某个事件发生之后或者之前的指定时间点完成某一项任务,如:发生订单生成事件,在十分钟之后检查该订单支付状态,然后将未支付的订单进行关闭;看起来似乎使用定时任务,一直轮询数据,每秒查一次,取出需要被处理的数据,然后处理不就完事了吗?

​ 如果数据量比较少,确实可以这样做,比如:对于“如果账单一周内未支付则进行自动结算”这样的需求,如果对于时间不是严格限制,而是宽松意义上的一周,那么每天晚上跑个定时任务检查一下所有未支付的账单,确实也是一个可行的方案。

​ 但对于数据量比较大,并且时效性较强的场景,如:“订单十分钟内未支付则关闭“,短期内未支付的订单数据可能会有很多,活动期间甚至会达到百万甚至千万级别,对这么庞大的数据量仍旧使用轮询的方式显然是不可取的,很可能在一秒内无法完成所有订单的检查,同时会给数据库带来很大压力,无法满足业务要求而且性能低下。

2.1.1、RabbitMQ 中的 TTL

​ TTL 是什么呢?TTL 是 RabbitMQ 中一个消息或者队列的属性,表明一条消息或者该队列中的所有 消息的最大存活时间, 单位是毫秒。

​ 换句话说,如果一条消息设置了 TTL 属性或者进入了设置 TTL 属性的队列,那么这 条消息如果在 TTL 设置的时间内没有被消费,则会成为"死信"。如果同时配置了队列的 TTL 和消息的 TTL,那么较小的那个值将会被使用,有两种方式设置 TTL。

  • 消息设置 TTL

    例如:

     rabbitTemplate.convertAndSend("X","XC",message,msg -> {//发送消息设置消息的TTLmsg.getMessageProperties().setExpiration(ttl+"000");return msg;});
    
  • **队列设置 TTL **

    例如:在创建队列的时候设置队列的“x-message-ttl”属性

     //声明普通队列 TTL 为40秒@Bean("queueB")public Queue queueB(){HashMap<String, Object> map = new HashMap<>(3);//设置死信交换机map.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);//设置死信RoutingKeymap.put("x-dead-letter-routing-key","YD");//设置过期时间,10秒map.put("x-message-ttl",40000);return QueueBuilder.durable(QUEUE_B).withArguments(map).build();}
    

2.2、整合SpringBoot

添加依赖项

<properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><!-- 添加 Spring Boot 版本 --><spring.boot.version>2.3.4.RELEASE</spring.boot.version><!-- 添加 Lombok 版本 --><lombok.version>1.18.20</lombok.version><!-- 添加 Spring AMQP 版本 --><spring.amqp.version>2.1.4.RELEASE</spring.amqp.version><!-- 添加 Springfox Swagger 版本 --><springfox.version>2.9.2</springfox.version></properties><dependencies><!-- RabbitMQ 依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId><version>${spring.boot.version}</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>${spring.boot.version}</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><version>${spring.boot.version}</version><scope>test</scope></dependency><dependency><groupId>com.taobao.arthas</groupId><artifactId>fastjson</artifactId><version>1.2.80-fix</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>${lombok.version}</version></dependency><!-- Swagger --><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>${springfox.version}</version></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger-ui</artifactId><version>${springfox.version}</version></dependency><!-- RabbitMQ 测试依赖 --><dependency><groupId>org.springframework.amqp</groupId><artifactId>spring-rabbit-test</artifactId><version>${spring.amqp.version}</version><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>

创建application.properties文件

spring.rabbitmq.host=192.168.111.28
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.redis.password=123

swagger2的配置类SwaggerConfig

@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("zm", "http://zmblog.vip","3339332352@qq.com")).build();}
}

2.3、队列TTL

​ 创建两个队列 QA 和 QB,两者队列 TTL 分别设置为 10S 和 40S,然后在创建一个交换机 X 和死信交 换机 Y,它们的类型都是 direct,创建一个死信队列 QD,它们的绑定关系如下:

image-20240302163555580

2.4、配置文件类代码

TtlQueueConfig

//TTL队列,配置类代码
@Configuration
public class TtlQueueConfig {//普通交换机名称public static final String X_EXCHANGE = "X";//死信交换机名称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 DEAD_LETTER_QUEUE = "QD";//声明普通交换机X_EXCHANGE,起一个别明xExchange注入bean@Bean("xExchange")public DirectExchange xExchange(){return new DirectExchange(X_EXCHANGE);}//声明死信交换机Y_EXCHANGE,起一个别明yExchange注入bean@Bean("yExchange")public DirectExchange yExchange(){return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);}//声明普通队列 TTL 为10秒@Bean("queueA")public Queue queueA(){HashMap<String, Object> map = new HashMap<>(3);//设置死信交换机map.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);//设置死信RoutingKeymap.put("x-dead-letter-routing-key","YD");//设置过期时间,10秒map.put("x-message-ttl",10000);return QueueBuilder.durable(QUEUE_A).withArguments(map).build();}//声明普通队列 TTL 为40秒@Bean("queueB")public Queue queueB(){HashMap<String, Object> map = new HashMap<>(3);//设置死信交换机map.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);//设置死信RoutingKeymap.put("x-dead-letter-routing-key","YD");//设置过期时间,10秒map.put("x-message-ttl",40000);return QueueBuilder.durable(QUEUE_B).withArguments(map).build();}//死信队列@Bean("queueD")public Queue queueD(){return QueueBuilder.durable(DEAD_LETTER_QUEUE).build();}//绑定@Beanpublic Binding QABindingX(@Qualifier("queueA") Queue queueA,@Qualifier("xExchange") DirectExchange xExchange){return BindingBuilder.bind(queueA).to(xExchange).with("XA");}@Beanpublic Binding QBBindingX(@Qualifier("queueB") Queue queueB,@Qualifier("xExchange") DirectExchange xExchange){return BindingBuilder.bind(queueB).to(xExchange).with("XB");}@Beanpublic Binding QDBindingY(@Qualifier("queueD") Queue queueD,@Qualifier("yExchange") DirectExchange yExchange){return BindingBuilder.bind(queueD).to(yExchange).with("YD");}
}

生产者controller,DeadLetterProducers

//生产者发送延迟消息
@Slf4j
@RestController
public class DeadLetterProducers {//自动注入@Autowiredprivate RabbitTemplate rabbitTemplate;//发消息@GetMapping("/sendMsg/{message}")public void sendMsg(@PathVariable String message){log.info("当前时间:{},发送一条消息给两个TTL队列:{}",new Date().toString(),message);rabbitTemplate.convertAndSend("X","XA","消息来自于TTL为10秒的队列:"+message);rabbitTemplate.convertAndSend("X","XB","消息来自于TTL为40秒的队列:"+message);}
}

消费者:

//消费者
@Slf4j
@Component
public class DeadLetterQueueConsumer {//接收消息,定义一个监听器来监听死信队列中的消息@RabbitListener(queues = "QD")public void receiveD(Message message, Channel channel){String s = new String(message.getBody());log.info("当前时间:{},收到死信队列的消息:{}",new Date().toString(),s);}}

成功启动

image-20240302203312306

​ 在浏览器上输入localhost:8080/sendMsg/哈哈哈哈哈

​ 发现后台开始输出信息,先是发送的提示信息,然后过了10秒消费者收到了一个信息,然后再过30秒收到了第二条信息。

image-20240302203706023

​ 结果是没有问题的,第一条消息在10S后变成了死信消息,然后被消费者消费掉,第二条消息在40S之后变成了死信消息,然后被消费掉,这样一个延时队列就打造完成了。

​ 不过,如果这样使用的话,岂不是每增加一个新的时间需求,就要新增一个队列,这里只有10S和40S两个时间选项,如果需要一个小时后处理,那么就需要增加TTL为一个小时的队列,如果是预定会议室然后提前通知这样的场景,岂不是要增加无数个队列才能满足需求?

2.5、延迟队列优化

​ 在这里新增了一个队列 QC,它是通用的延迟队列,生产者掌握时间(这是设定消息的过期时间,之前是设定队列的过期时间),绑定关系如下,该队列不设置 TTL 时间

image-20240303104749887

我们让生产者来决定延迟队列的时间,需要多少就指定多少这样更符合需求。

在TtlQueueConfig类中添加内容

public static final String QUEUE_C = "QC";
//新加一个通用TTL的普通队列@Bean("queueC")public Queue queueC(){HashMap<String, Object> map = new HashMap<>(2);//设置死信交换机map.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);//设置死信RoutingKeymap.put("x-dead-letter-routing-key","YD");//TTL就交给生产者指定了这里就不需要写了return QueueBuilder.durable(QUEUE_C).withArguments(map).build();}
//QC绑定普通交换机@Beanpublic Binding QCBindingX(@Qualifier("queueC") Queue queueC,@Qualifier("xExchange") DirectExchange xExchange){return BindingBuilder.bind(queueC).to(xExchange).with("XC");}

生产者的controller添加一个请求

//发指定TTL的消息
@GetMapping("/sendExpMsg/{message}/{ttl}")
public void sendExpMsg(@PathVariable String message,@PathVariable String ttl){log.info("当前时间:{},发送一条时长为:{}毫秒的队列消息给队列QC:{}",new Date(),ttl,message);rabbitTemplate.convertAndSend("X","XC",message,msg -> {//发送消息设置消息的TTLmsg.getMessageProperties().setExpiration(ttl+"000");return msg;});
}

​ 我们启动服务,浏览器上分别输入localhost:8080/sendExpMsg/这是第一条信息/20,localhost:8080/sendExpMsg/这是第二条信息/2

image-20240303121301599

​ 我们发现,结果并不是我们所设想的那样,延时20秒的和2秒的两条消息都是在20秒时接收到,同时接收了,而这个顺序也是按照先进先出的队列特性来的,显然这不是实际需求。

​ 在最开始的时候,我们就了解到如果在消息属性上设置TTL的方式,消息可能并不会按时“死亡”,因为RabbitMQ只会检查第一个消息是否过期,如果过期则丢到死新信队列,如果第一个消息的延时时间非常长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行。

​ 那么如何解决这个问题,就需要使用RabbitMQ的插件实现延迟队列

2.6、RabbitMQ的插件实现延迟队列

​ 如果不能实现在消息粒度上的TTL,并使其在设置的TL时间及时死亡,就无法设计成一个通用的延时队列。

2.6.1、安装RabbitMQ的延时队列插件

​ 在官网上下载https://www.rabbitmq.com/community-plugins.html,rabbitmq_delayed_message__exchange插件,然后解压放置到RabbitMQ的插件目录。

​ 下载3.8.0的连接rabbitmq_delayed_message_exchange-3.8.0.ez (github.com)

image-20240303124317709

​ 进入 RabbitMQ 的安装目录下的 plgins 目录,执行下面命令让该插件生效,然后重启 RabbitMQ

/usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins
rabbitmq-plugins enable rabbitmq_delayed_message_exchange

​ 把插件放到RabbitMQ的插件文件夹

image-20240303124759301

​ 然后进行安装

image-20240303124912826

​ 重启Rabbitmq之后就可以了,可以看到交换机的类型新加一个x-delayed-message类型的

image-20240303125247971

​ 这样延迟的任务就交给交换机来做了,生产者把消息发给交换机,然后消息就在交换机延迟了,然后延迟时间过了之后就可以发给普通的队列了。

2.6.2、基于插件的延迟消息实现

​ 我们新加一个队列delayed.queue,一个自定义交换机 delayed.exchange,绑定关系如下:

在这里插入图片描述

​ 在我们自定义的交换机中,这是一种新的交换类型,该类型消息支持延迟投递机制 消息传递后并 不会立即投递到目标队列中,而是存储在 mnesia(一个分布式数据系统)表中,当达到投递时间时,才 投递到目标队列中。

配置文件类

//使用插件实现延迟
public class DelayedQueueConfig {//声明延迟类型的交换机名称public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";//队列public static final String DELAYED_QUEUE_NAME = "delayed.queue";//routingKeypublic static final String DELAYED_ROUTING_KEY = "delayed.routingKey";//声明队列@Beanpublic Queue delayedQueue(){return QueueBuilder.durable(DELAYED_QUEUE_NAME).build();}//声明交换机CustomExchange是自定义交换机,由于使用插件提供的类型在原来的四个类型中没有所以只能自定义/* public CustomExchange(String name, String type, boolean durable, boolean autoDelete, Map<String, Object> arguments) {super(name, durable, autoDelete, arguments);this.type = type;}第一个参数就是交换机的名称第二个参数就是交换机的自定义类型第三个参数就是是否持久化第四个参数就是是否自动删除第五个是其他参数* */@Beanpublic CustomExchange delayedExchange(){HashMap<String, Object> arguments = new HashMap<>();//设置自定义交换机的类型//消息确实是延迟了,但是怎么传播到队列呢,是要多播呢还是直连呢,所以还得设置类型//这里设置直连,怎么发送是直连的发送,啥时候发送就是延迟。arguments.put("x-delayed-type","direct");return new CustomExchange(DELAYED_EXCHANGE_NAME,"x-delayed-message",true,false,arguments);}//绑定//返回中最后的构建,build有参数,noargs没有参数@Beanpublic Binding delayedBinding(@Qualifier("delayedQueue") Queue delayedQueue,@Qualifier("delayedExchange") CustomExchange delayedExchange){return BindingBuilder.bind(delayedQueue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();}
}

​ 生产者controller新加请求方法

//延迟插件发送消息
@GetMapping("/sendDelayMsg/{message}/{delayTime}")
public void sendDelayMsg(@PathVariable String message,@PathVariable int delayTime){log.info("当前时间:{},发送一条时长为:{}秒的队列消息延迟队列delay.queue:{}",new Date(),delayTime,message);rabbitTemplate.convertAndSend(DelayedQueueConfig.DELAYED_EXCHANGE_NAME,DelayedQueueConfig.DELAYED_ROUTING_KEY,message, msg -> {//发送消息设置延时时长msg.getMessageProperties().setDelay(delayTime*1000);return msg;});
}

​ 消费者

//专门消费由延迟交换机发出的消息
@Slf4j
@Component
public class DelayedLetterConsumer {@RabbitListener(queues = DelayedQueueConfig.DELAYED_QUEUE_NAME)public void receiveDelayed(Message message){String s = new String(message.getBody());log.info("当前时间:{},收到delayed.queue队列的消息:{}",new Date(),s);}
}

我们启动服务浏览器输入localhost:8080/sendDelayMsg/这是第一条信息/20,localhost:8080/sendDelayMsg/这是第二条信息/2

image-20240303142214558

​ 可以看到结果是很正确的,第二条消息在发送2秒后就收到了,第一条消息在它发送20秒后收到的

2.6.3、总结

​ 延时队列在需要延时处理的场景下非常有用,使用RabbitMQ来实现延时队列可以很好的利用RabbitMQ的特性,如:消息可靠发送、消息可靠投递、死信队列来保障消息至少被消费一次以及未被正确处理的消息不会被丢弃。另外,通过RabbitMQ集群的特性,可以很好的解决单点故障问题,不会因为单个节点挂掉导致延时队列不可用或者消息丢失。

​ 当然,延时队列还有很多其它选择,比如利用Java的DelayQueue,利用Redis的zset,利用Quartz或者利用kafka的时间轮,这些方式各有特点,看需要适用的场景。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于redis实现用户登陆

因为session有数据共享问题&#xff0c;不同tomcat服务器中的session不能共享&#xff0c;之后负载均衡就无法实现。所以我们用redis代替session。redis可以被多个tomcat服务器共享&#xff0c;redis基于内存。 之前的session可以看做登陆凭证&#xff0c;本次登陆凭证由sessi…

C语言指针总结(完结篇)

前言 这篇博客终于迎来了指针博客的大结局&#xff0c;本篇主要分析习题来回顾之前的指针总结的知识点&#xff0c;这篇博客的题有点绕&#xff0c;哈哈算是经典了 个人主页&#xff1a;小张同学zkf 若有问题 评论区见 感兴趣就关注一下吧 目录 1. sizeof和strlen的对比 1.1 …

TikTok矩阵获客软件的核心源代码是什么?

随着互联网的不断发展&#xff0c;社交媒体已成为企业获客的重要渠道之一&#xff0c;在众多的社交媒体平台中&#xff0c;TikTok凭借其庞大的用户群体和活跃的社交氛围&#xff0c;成为了众多企业竞相争夺的营销高地。 在这样的背景下&#xff0c;TikTok矩阵获客软件应运而生…

Linux Ubuntu部署SVN服务端结合内网穿透实现客户端公网访问

文章目录 前言1. Ubuntu安装SVN服务2. 修改配置文件2.1 修改svnserve.conf文件2.2 修改passwd文件2.3 修改authz文件 3. 启动svn服务4. 内网穿透4.1 安装cpolar内网穿透4.2 创建隧道映射本地端口 5. 测试公网访问6. 配置固定公网TCP端口地址6.1 保留一个固定的公网TCP端口地址6…

RISC-V架构学习资料整理

1、韦东山——D1S哪吒开发板的裸机代码仓库 https://github.com/bigmagic123/d1-nezha-baremeta 2、melis系统移植到D1S https://blog.51cto.com/u_13800193/6268813 3、韦东山的gitee仓库 https://gitee.com/weidongshan 4、D1S编译工具链下载 https://github.com/Tina-Linux/…

Tomcat安装步骤及详细配置教程(2022最新版)

网上的tomcat安装及配置教程一大堆&#xff0c;但是好多都过时了&#xff0c;根本不适用现在的版本&#xff0c;今天凯歌整理一篇Tomcat安装步骤及详细配置教程&#xff0c;2022年最新版~ Tomcat安装及配置教程主要分为四步&#xff1a; 步骤一&#xff1a;首先确认自己是否已…

学习Java的第一天

一、Java简介 Java 是由 Sun Microsystems 公司于 1995 年 5 月推出的 Java 面向对象程序设计语言和 Java 平台的总称。由 James Gosling和同事们共同研发&#xff0c;并在 1995 年正式推出。 后来 Sun 公司被 Oracle &#xff08;甲骨文&#xff09;公司收购&#xff0c;Jav…

nvm安装和使用保姆级教程(详细)

一、 nvm是什么 &#xff1a; nvm全英文也叫node.js version management&#xff0c;是一个nodejs的版本管理工具。nvm和npm都是node.js版本管理工具&#xff0c;为了解决node.js各种版本存在不兼容现象可以通过它可以安装和切换不同版本的node.js。 二、卸载之前安装的node: …

java核心面试题汇总

文章目录 1. Java1.1. TCP三次握手/四次挥手1.2 HashMap底层原理1.3 Java常见IO模型1.4 线程与线程池工作原理1.5 讲一讲ThreadLocal、Synchronized、volatile底层原理1.6 了解AQS底层原理吗 2. MySQL2.1 MySQL索引为何不采用红黑树&#xff0c;而选择B树2.2 MySQL索引为何不采…

python基础第二天

世界杯小组赛成绩 注意&#xff1a; 1.循环 1.1while 1.2for 1.3 range 1.4 while else while 循环正常执行完才能执行else语句

【学习】torch.nn.CrossEntropyLoss交叉熵损失函数

交叉熵损失函数torch.nn.CrossEntropyLoss 交叉熵主要是用来判定实际的输出与期望的输出的接近程度&#xff0c;为什么这么说呢&#xff0c;举个例子&#xff1a; 在做分类的训练的时候&#xff0c;如果一个样本属于第K类&#xff0c;那么这个类别所对应的输出节点的输出值应…

计讯物联环保数采仪TS910全力打造绿色宜居生态环境

植树造林自古以来就有调节气候、涵养水源、减轻大气污染的益处。如今&#xff0c;随着科技的迅速发展&#xff0c;我们除了能够以植树造林来改善生活环境&#xff0c;保持生态系统&#xff0c;还能通过物联网、大数据、云计算、边缘计算、人工智能等新一代信息技术集成应用于监…

使用 Docker 部署 Fiora 在线聊天室平台

一、Fiora 介绍 Fiora 简介 Fiora 是一款开源免费的在线聊天系统。 GitHub&#xff1a;https://github.com/yinxin630/fiora Fiora 功能 注册账号并登录&#xff0c;可以长久保存你的数据加入现有群组或者创建自己的群组&#xff0c;来和大家交流和任意人私聊&#xff0c;并添…

探索SRM软件:了解SRM在企业中的作用

作为供应商&#xff0c;对于SRM软件并不陌生&#xff0c;但对于刚刚进入采购行业的新手来说&#xff0c;就可能不太了解这个软件的作用了。现在让我们来科普一下&#xff1a;通常&#xff0c;我们将SRM称为供应商关系管理&#xff0c;其全称为Supplier Relationship Management…

让照片说话唱歌的软件,盘点这3款!

在数字时代&#xff0c;我们总是渴望找到新的方式来表达自我、分享生活。近年来&#xff0c;随着人工智能和图像处理技术的飞速发展&#xff0c;一种新型的软件应运而生&#xff0c;它们能够让照片“说话”甚至“唱歌”&#xff0c;给我们的生活带来了无限乐趣和创意空间。那么…

如何在Linux系统Docker本地部署SimpleMindMap并实现远程访问?

文章目录 1. Docker一键部署思维导图2. 本地访问测试3. Linux安装Cpolar4. 配置公网地址5. 远程访问思维导图6. 固定Cpolar公网地址7. 固定地址访问 SimpleMindMap 是一个可私有部署的web思维导图工具。它提供了丰富的功能和特性&#xff0c;包含插件化架构、多种结构类型&…

卷积神经网络的原理

前面介绍了卷积运算的基本原理和概念&#xff0c;从本质上来说&#xff0c;卷积神经网络就是将图像处理中的二维离散卷积运算和神经网络相结合。这种卷积运算可以用于自动提取特征&#xff0c;而卷积神经网络也主要应用于二维图像的识别。下面我们将采用图示的方法直观地介绍卷…

Windows安装MySQL详细教程

1.1 下载MySQL压缩包 官网下载链接[点击跳转] 按图中选择&#xff0c;然后点击【Download】 点击图中箭头所指方向直接下载 1.2 解压下载好的压缩包后找到【bin】文件夹&#xff0c;并记下文件路径&#xff08;下文将以路径 D:\mysql-8.0.36-winx64\bin 为例&#xff09; 1.…

自学新标日第七课(未完结)

第七课 单词 单词假名声调词义コーヒーコーひー&#xff13;咖啡コーラーコーラー&#xff11;可乐お茶おちゃ&#xff10;茶ワインわいん&#xff11;葡萄酒パンぱん&#xff11;面包ケーキけーき&#xff11;蛋糕お粥おかゆ&#xff10;粥昼ごはんひるごはん&#xff13;午…

JS 对象数组排序方法测试

输出 一.Array.prototype.sort() 1.默认排序 sort() sort() 方法就地对数组的元素进行排序&#xff0c;并返回对相同数组的引用。默认排序是将元素转换为字符串&#xff0c;然后按照它们的 UTF-16 码元值升序排序。 由于它取决于具体实现&#xff0c;因此无法保证排序的时…