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

RabbitMQ 消息可靠保障

RabbitMQ 消息可靠保障

    • 消息的可靠性保证
      • 生产者重连
      • 生产者确认
        • 解决思路A-确认机制
        • 解决思路B-备份交换机
      • MQ 服务器宕机导致消息丢失
      • 消费端消息的可靠性保障
    • 消费端限流
    • 给消息生成唯一id

消息的可靠性保证

实际项目中 MQ 的流程一般是:生产端把消息路由到交换机,然后由交换机把消息发送到队列,接着就是消费端拿到消息进行消费,这三个过程都有可能造成消息的不稳定,导致不可靠
在这里插入图片描述

生产者重连

Spring Boot 提供了一种重试机制,只需要通过配置即可实现消息重试从而确保消息的可靠性,这里简单介绍一下:

spring:rabbitmq:listener:simple:acknowledge-mode: auto  # 开启自动确认消费机制retry:enabled: true # 开启消费者失败重试initial-interval: 5000ms # 初始失败等待时长为5秒multiplier: 1  # 失败的等待时长倍数(下次等待时长 = multiplier * 上次等待时间)max-attempts: 3 # 最大重试次数stateless: true # true无状态;false有状态(如果业务中包含事务,这里改为false)

通过配置在消费者的方法上如果执行失败或执行异常只需要抛出异常(一定要出现异常才会触发重试,注意:不要捕获异常) 即可实现消息重试,这样也可以保证消息的可靠性。
在这里插入图片描述
失败重试机制:https://www.bilibili.com/video/BV1mN4y1Z7t9?p=24&spm_id_from=pageDriver&vd_source=c3fc4867486b99c0b85501121f575279

生产者确认

生产者确认机制分为两部分,生产者到交换机和交换机到队列的可靠性保障

解决思路A-确认机制

在生产端进行确认,具体操作中会分别针对交换机和队列来确认,如果没有成功发送的队列服务器上,那就可以尝试重新发送

首先需要增加下列配置

spring.rabbitmq.host=192.168.133.128
spring.rabbitmq.port=5672
spring.rabbitmq.password=admin
spring.rabbitmq.username=admin
spring.rabbitmq.virtual-host=/
spring.rabbitmq.listener.type=simple
spring.rabbitmq.publisher-confirm-type=CORRELATED #交换机的确认
spring.rabbitmq.publisher-returns=true #队列的确认

这里 publisher-confirm-type 有三种模式可选:

  • none:表示禁用发送方确认机制
  • correlated:表示开启发送方确认机制
  • simple:表示开启发送方确认机制,并支持 waitForConfirms() 和 waitForConfirmsOrDie() 的调用。

接下来增加一个Rabbit 配置

@Configuration
@Slf4j
public class RabbitConfig implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback {@Resourceprivate RabbitTemplate rabbitTemplate;@PostConstructpublic void init() {rabbitTemplate.setConfirmCallback(this);rabbitTemplate.setReturnsCallback(this);}@Overridepublic void confirm(CorrelationData correlationData, boolean ack, String cause) {// 消息发送到交换机成功或失败时调用此方法log.info("confirm函数打印correlationData:" + correlationData);log.info("confirm函数打印ack:" + ack);log.info("confirm函数打印cause:" + cause);}@Overridepublic void returnedMessage(ReturnedMessage returnedMessage) {// 发送队列失败时才调用此方法log.info("消息主体:" + new String(returnedMessage.getMessage().getBody()));log.info("应答码:" + returnedMessage.getReplyCode());log.info("描述:" + returnedMessage.getReplyText());log.info("交换机:" + returnedMessage.getExchange());log.info("路由key:" + returnedMessage.getRoutingKey());}
}

在这里插入图片描述
为了方便测试,定义一个交换机和队列吧并声明它们的绑定关系

public class RabbitMQConfig {//定义一个交换机已及 routingkey,用来测试消息的可靠传递测试public static final String NORMAL_EXCHANGE = "normal.demo.exchange";public static final String NORMAL_ROUTING_KEY = "normal.demo.routingkey";public static final String NORMAL_QUEUE = "normal.demo.queue";@Beanpublic DirectExchange normalExchange() {return new DirectExchange(NORMAL_EXCHANGE);}@Beanpublic Queue normalQueue() {return new Queue(NORMAL_QUEUE);}@Beanpublic Binding normalBinding(@Qualifier("normalQueue") Queue queue,@Qualifier("normalExchange") DirectExchange exchange) {return BindingBuilder.bind(queue).to(exchange).with(NORMAL_ROUTING_KEY);}
}

在 controller 层测试

@RestController
@RequestMapping("/rabbit")
public class RabbitController {@Resourceprivate RabbitTemplate rabbitTemplate;@GetMapping("/test")public String test(){rabbitTemplate.convertAndSend(RabbitMQConfig.NORMAL_EXCHANGE, RabbitMQConfig.NORMAL_ROUTING_KEY, "Message Test Confirm~~");return "success.";}
}

消息成功路由到交换机,然后交换机发送到队列,正常情况下是下面的输出

2024-08-17T17:08:35.105+08:00  INFO 2484 --- [nio-8080-exec-1] o.s.a.r.c.CachingConnectionFactory       : Attempting to connect to: [192.168.133.128:5672]
2024-08-17T17:08:35.149+08:00  INFO 2484 --- [nio-8080-exec-1] o.s.a.r.c.CachingConnectionFactory       : Created new connection: rabbitConnectionFactory#59f45950:0/SimpleConnection@3542faa [delegate=amqp://admin@192.168.133.128:5672/, localPort=63927]
2024-08-17T17:08:35.204+08:00  INFO 2484 --- [nectionFactory2] com.example.demo.config.RabbitConfig     : confirm函数打印correlationData:null
2024-08-17T17:08:35.204+08:00  INFO 2484 --- [nectionFactory2] com.example.demo.config.RabbitConfig     : confirm函数打印ack:true
2024-08-17T17:08:35.205+08:00  INFO 2484 --- [nectionFactory2] com.example.demo.config.RabbitConfig     : confirm函数打印cause:null

现在改一下,模拟路由交换机失败的场景,注意没有 RabbitMQConfig.NORMAL_EXCHANGE+"~",这个交换机

rabbitTemplate.convertAndSend(RabbitMQConfig.NORMAL_EXCHANGE+"~", RabbitMQConfig.NORMAL_ROUTING_KEY, "Message Test Confirm~~");

重新测试,此时输出如下,错误提示的还是很明显的

2024-08-17T17:11:39.981+08:00  INFO 15864 --- [nio-8080-exec-1] o.s.a.r.c.CachingConnectionFactory       : Attempting to connect to: [192.168.133.128:5672]
2024-08-17T17:11:40.037+08:00  INFO 15864 --- [nio-8080-exec-1] o.s.a.r.c.CachingConnectionFactory       : Created new connection: rabbitConnectionFactory#59f45950:0/SimpleConnection@568384f7 [delegate=amqp://admin@192.168.133.128:5672/, localPort=64266]
2024-08-17T17:11:40.074+08:00 ERROR 15864 --- [68.133.128:5672] o.s.a.r.c.CachingConnectionFactory       : Shutdown Signal: channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'normal.demo.exchange~' in vhost '/', class-id=60, method-id=40)
2024-08-17T17:11:40.076+08:00  INFO 15864 --- [nectionFactory3] com.example.demo.config.RabbitConfig     : confirm函数打印correlationData:null
2024-08-17T17:11:40.076+08:00  INFO 15864 --- [nectionFactory3] com.example.demo.config.RabbitConfig     : confirm函数打印ack:false
2024-08-17T17:11:40.077+08:00  INFO 15864 --- [nectionFactory3] com.example.demo.config.RabbitConfig     : confirm函数打印cause:channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'normal.demo.exchange~' in vhost '/', class-id=60, method-id=40)

现在再模拟交换机发送消息到队列失败的场景,改一下 routingKey

rabbitTemplate.convertAndSend(RabbitMQConfig.NORMAL_EXCHANGE, RabbitMQConfig.NORMAL_ROUTING_KEY+"~", "Message Test Confirm~~");

重新测试,此时输出如下,returnedMessage 方法执行了

2024-08-17T17:13:49.835+08:00  INFO 12892 --- [nio-8080-exec-1] o.s.a.r.c.CachingConnectionFactory       : Attempting to connect to: [192.168.133.128:5672]
2024-08-17T17:13:49.889+08:00  INFO 12892 --- [nio-8080-exec-1] o.s.a.r.c.CachingConnectionFactory       : Created new connection: rabbitConnectionFactory#59f45950:0/SimpleConnection@572e41be [delegate=amqp://admin@192.168.133.128:5672/, localPort=64519]
2024-08-17T17:13:49.933+08:00  INFO 12892 --- [nectionFactory1] com.example.demo.config.RabbitConfig     : 消息主体:Message Test Confirm~~
2024-08-17T17:13:49.934+08:00  INFO 12892 --- [nectionFactory1] com.example.demo.config.RabbitConfig     : 应答码:312
2024-08-17T17:13:49.934+08:00  INFO 12892 --- [nectionFactory1] com.example.demo.config.RabbitConfig     : 描述:NO_ROUTE
2024-08-17T17:13:49.934+08:00  INFO 12892 --- [nectionFactory1] com.example.demo.config.RabbitConfig     : 交换机:normal.demo.exchange
2024-08-17T17:13:49.934+08:00  INFO 12892 --- [nectionFactory1] com.example.demo.config.RabbitConfig     : 路由key:normal.demo.routingkey~
2024-08-17T17:13:49.935+08:00  INFO 12892 --- [nectionFactory1] com.example.demo.config.RabbitConfig     : confirm函数打印correlationData:null
2024-08-17T17:13:49.936+08:00  INFO 12892 --- [nectionFactory1] com.example.demo.config.RabbitConfig     : confirm函数打印ack:true
2024-08-17T17:13:49.936+08:00  INFO 12892 --- [nectionFactory1] com.example.demo.config.RabbitConfig     : confirm函数打印cause:null

顺便了解一下spring.rabbitmq.template.mandatory 配置
当使用 RabbitMQ 作为消息队列时,spring.rabbitmq.template.mandatory 配置项用于控制 MQ 消息模板(RabbitTemplate)的发送行为,特别是在消息路由时找不到队列(queue)的情况下的处理方式。
2、spring.rabbitmq.template.mandatory属性可能会返回三种值null、false、true,
3、spring.rabbitmq.template.mandatory 结果为 false 时会忽略掉spring.rabbitmq.publisher-returns 属性的值,也就是 returnedMessage 方法不会执行;结果为 true,returnedMessage 可以得到执行
4、spring.rabbitmq.template.mandatory 结果为null(即不配置)时结果由spring.rabbitmq.publisher-returns 确定
在这里插入图片描述

解决思路B-备份交换机

上面思路A对于消息是否成功发送到队列是通过重写 RabbitTemplate.ReturnsCallback 接口来操作的,备份交换机是对这一操作的替换,二选一,如果即配置了 ReturnsCallback 回调 又配置了备份交换机,实际运行发现,备份交换机优先级更高

为目标交换机指定备份交换机,当目标交换机投递失败时,把消息投递至备份交换机(实际项目中很少用,一般用思路A)
在这里插入图片描述

在上面代码的基础上改一下,RabbitMQConfig 就单纯的声明队列和交换机

public class RabbitMQConfig {//定义一个正常交换机以及 routingkey,用来测试消息的可靠传递测试public static final String NORMAL_EXCHANGE = "normal.demo.exchange";public static final String NORMAL_ROUTING_KEY = "normal.demo.routingkey";public static final String NORMAL_QUEUE = "normal.demo.queue";//定义一个备份交换机以及 队列,用来测试消息的可靠传递测试public static final String NORMAL_EXCHANGE_BACKUP = "normal.demo.exchange.backup";public static final String NORMAL_QUEUE_BACKUP = "normal.demo.queue.backup";
}

定义一个消费者,监听队列,实际项目中都会有的,有如下注意点:

  • 备份交换机必须定义成 FANOUT 类型
  • 声明正常交换机的时候指定一个备份交换机,通过 alternate-exchange 参数,代码有体现
@Slf4j
@Component
public class RabbitConsumer {@RabbitListener(bindings = @QueueBinding(value = @Queue(value = RabbitMQConfig.NORMAL_QUEUE),exchange = @Exchange(value = RabbitMQConfig.NORMAL_EXCHANGE, type = ExchangeTypes.DIRECT, arguments = {@Argument(name = "alternate-exchange", value = RabbitMQConfig.NORMAL_EXCHANGE_BACKUP)}),key = RabbitMQConfig.NORMAL_ROUTING_KEY))public void receiveA(Message message, Channel channel) throws IOException {String msg = new String(message.getBody());log.info("正常队列{}收到消息:{}",  RabbitMQConfig.NORMAL_QUEUE, msg);}@RabbitListener(bindings = @QueueBinding(value = @Queue(value = RabbitMQConfig.NORMAL_QUEUE_BACKUP),exchange = @Exchange(value = RabbitMQConfig.NORMAL_EXCHANGE_BACKUP, type = ExchangeTypes.FANOUT)))public void receiveB(Message message, Channel channel) throws IOException {String msg = new String(message.getBody());log.info("备份队列{}收到消息:{}",  RabbitMQConfig.NORMAL_QUEUE_BACKUP, msg);}
}

注意:@RabbitListener 必须先创建队列,不然报错, 有三个 属性 queues()、bindings()、queuesToDeclare(),它们之间是互斥的。设定了queues(),就不能再设定 bindings() 和 queuesToDeclare()了,具体用法可以看这篇文章

接下来模拟无法到达队列(如果无法到达交换机直接报错,没有后面什么事了…),测试能不能正常进入备份交换机

rabbitTemplate.convertAndSend(RabbitMQConfig.NORMAL_EXCHANGE, RabbitMQConfig.NORMAL_ROUTING_KEY+"~", "Message Test Confirm~~");

运行结果如下:

2024-08-17T18:25:32.253+08:00  INFO 22476 --- [nectionFactory2] com.example.demo.config.RabbitConfig     : confirm函数打印correlationData:null
2024-08-17T18:25:32.254+08:00  INFO 22476 --- [nectionFactory2] com.example.demo.config.RabbitConfig     : confirm函数打印ack:true
2024-08-17T18:25:32.254+08:00  INFO 22476 --- [nectionFactory2] com.example.demo.config.RabbitConfig     : confirm函数打印cause:null
2024-08-17T18:25:32.257+08:00  INFO 22476 --- [ntContainer#0-1] c.example.demo.rabbitmq.RabbitConsumer   : 备份队列normal.demo.queue.backup收到消息:Message Test Confirm~~

MQ 服务器宕机导致消息丢失

这一点官方早就考虑到了,现在无论是交换机还是队列,消息都是默认持久化到硬盘上,哪怕服务器重启也不会导致消息丢失
消息持久化

@Override
public void sendMessage() {// 构造消息(将消息持久化)Message message = MessageBuilder.withBody("单程车票".getBytes(StandardCharsets.UTF_8)).setDeliveryMode(MessageDeliveryMode.PERSISTENT).build();// 向MQ发送消息(消息内容都为消息表记录的id)rabbitTemplate.convertAndSend(RabbitMQConfig.Direct_Exchange, routingKey, message);
}

队列和交换机的持久化,默认就是 true,无需显示指定

@Bean
public Queue queue() {// 四个参数:name(队列名)、durable(持久化)、 exclusive(独占)、autoDelete(自动删除)return new Queue(MESSAGE_QUEUE, true);
}
@Bean
public DirectExchange exchange() {// 四个参数:name(交换机名)、durable(持久化)、autoDelete(自动删除)、arguments(额外参数)return new DirectExchange(Direct_Exchange, true, false);
}

消费端消息的可靠性保障

配置文件增加 spring.rabbitmq.listener.simple.acknowledge-mode=manual 配置,也就是下面这样

spring.rabbitmq.host=192.168.133.128
spring.rabbitmq.port=5672
spring.rabbitmq.password=admin
spring.rabbitmq.username=admin
spring.rabbitmq.virtual-host=/
spring.rabbitmq.listener.type=simple
spring.rabbitmq.publisher-confirm-type=CORRELATED
spring.rabbitmq.publisher-returns=true
spring.rabbitmq.listener.simple.acknowledge-mode=manual #把消息确认模式改成手动确认

因为 RabbitMQ 客户端默认是 自动返回 ACK 确认的,也就是不管是处理成功还是失败,默认都按成功来处理,这样就不太好,所以这个配置要改成手动确认

需要提前知道个东西,啥是 deliveryTag?
在这里插入图片描述
队列的定义,这里把备份交换机队列都删了哈

public class RabbitMQConfig {//定义一个正常交换机以及 routingkey,用来测试消息的可靠传递测试public static final String NORMAL_EXCHANGE = "normal.demo.exchange";public static final String NORMAL_ROUTING_KEY = "normal.demo.routingkey";public static final String NORMAL_QUEUE = "normal.demo.queue";
}

消费者逻辑如下

@Slf4j
@Component
public class RabbitConsumer {@RabbitListener(bindings = @QueueBinding(value = @Queue(value = RabbitMQConfig.NORMAL_QUEUE),exchange = @Exchange(value = RabbitMQConfig.NORMAL_EXCHANGE, type = ExchangeTypes.DIRECT, arguments = {@Argument(name = "alternate-exchange", value = RabbitMQConfig.NORMAL_EXCHANGE_BACKUP)}),key = RabbitMQConfig.NORMAL_ROUTING_KEY))public void receiveA(Message message, Channel channel) throws IOException {// 消息内容String msg = new String(message.getBody());// 消息的 deliveryTaglong deliveryTag = message.getMessageProperties().getDeliveryTag();try {// 操作成功,返回 ack 信息int i = 1/0; // 模拟消息处理异常channel.basicAck(deliveryTag, false);log.info("正常队列{}收到消息:{}",  RabbitMQConfig.NORMAL_QUEUE, msg);} catch (Exception ex) {// 获取当前消息是否是重复投递的// true-说明消息已经重复投递过一次了;false-说明当前消息是第一次投递Boolean redelivered = message.getMessageProperties().getRedelivered();// 操作失败,返回 Nack 信息if (redelivered) {// requeue 参数:控制消息是否重新放入队列 true-重放队列,broker会重新投递消息; false-不重放,broker会丢弃消息channel.basicNack(deliveryTag, false, false); // basicNack(long deliveryTag, boolean multiple, boolean requeue)} else {channel.basicNack(deliveryTag, false, true);}log.info("正常队列{}收到消息,,redelivered: {},但是处理异常:{}",  RabbitMQConfig.NORMAL_QUEUE, redelivered, msg);}// basicReject 和 basicNack 区别-> 唯一的区别就是 basicNack 有批量操作控制,就是 multiple 参数:是否批量处理(true 表示将一次性 ack 所有小于 deliveryTag 的消息)// channel.basicReject(deliveryTag, false); //basicReject(long deliveryTag, boolean requeue)}
}
2024-08-17T19:48:38.131+08:00  INFO 18192 --- [nectionFactory2] com.example.demo.config.RabbitConfig     : confirm函数打印correlationData:null
2024-08-17T19:48:38.132+08:00  INFO 18192 --- [nectionFactory2] com.example.demo.config.RabbitConfig     : confirm函数打印ack:true
2024-08-17T19:48:38.132+08:00  INFO 18192 --- [nectionFactory2] com.example.demo.config.RabbitConfig     : confirm函数打印cause:null
2024-08-17T19:48:55.205+08:00  INFO 18192 --- [ntContainer#0-1] c.example.demo.rabbitmq.RabbitConsumer   : 正常队列normal.demo.queue收到消息,,redelivered: false,但是处理异常:Message Test Confirm~~
2024-08-17T19:49:12.007+08:00  INFO 18192 --- [ntContainer#0-1] c.example.demo.rabbitmq.RabbitConsumer   : 正常队列normal.demo.queue收到消息,,redelivered: true,但是处理异常:Message Test Confirm~~

具体效果可以本地 debug 下哈,消费完并且都 ack 后这里都是0了
在这里插入图片描述

消费端限流

我们都知道 MQ 有削峰填谷的效果,假设有下面的场景,消息队列里面有10000条消息,但是消费端的并发能力只有 1000,为了避免一次性把这1万条消息全部取出,导致消费端压力太大,我们可以做个设置,每次最多取1000条消息,对消费端也是一种保护。

从操作层面来说也比较简单,配置spring.rabbitmq.listener.simple.prefetch 属性即可
在这里插入图片描述
做个小实验:
当不设置 prefetch 时,生产者一次性向交换机中投递100条消息,消费者确认消息的方式设置为手动确认,在确认消息之前线程休眠 5 秒(因为 rabbitmq 管理控制台数据更新是 5 秒更新一次,这里设置为 5 秒比较方便观察),可以观察到队列是一次性将100条数据全部发送给了消费者,然后消费者再进行处理:

生产者一次发送10条消息到交换机:

 @GetMapping("/test")public String test(){for (int i = 0; i < 100; i++) {rabbitTemplate.convertAndSend(RabbitMQConfig.NORMAL_EXCHANGE, RabbitMQConfig.NORMAL_ROUTING_KEY, "Message Test Confirm"+i);}return "success.";}

消费者在确认之前休眠5秒:

@RabbitListener(bindings = @QueueBinding(value = @Queue(value = RabbitMQConfig.NORMAL_QUEUE_BACKUP),exchange = @Exchange(value = RabbitMQConfig.NORMAL_EXCHANGE_BACKUP, type = ExchangeTypes.FANOUT)))
public void receiveB(Message message, Channel channel) throws IOException {String msg = new String(message.getBody());log.info("备份队列{}收到消息:{}",  RabbitMQConfig.NORMAL_QUEUE_BACKUP, msg);
}

在这里插入图片描述
现在增加spring.rabbitmq.listener.simple.prefetch=2配置,就是一次从队列中取两条数据,再观察结果:
会发现,消费端不再是一次性全部把消息取出,而是每次只取 2 个,这就是 prefetch 配置作用
在这里插入图片描述

给消息生成唯一id

可以在消费端打印消息 id,此时消息Id是 null

2024-08-18T20:43:46.295+08:00  INFO 24028 --- [ntContainer#0-1] c.example.demo.rabbitmq.RabbitConsumer   : 消息id:null,消息内容:Message Test Confirm

重写 MessageConverter

@Bean
public MessageConverter messageConverter() {// 定义消息转换器Jackson2JsonMessageConverter jjmc = new Jackson2JsonMessageConverter();// 配置自动创建消息 id, 用于识别不同消息,也可以在业务中基于ID判断是否重复消息jjmc.setCreateMessageIds(true);return jjmc;
}

再次打印

2024-08-18T21:01:20.752+08:00  INFO 24268 --- [ntContainer#0-1] c.example.demo.rabbitmq.RabbitConsumer   : 消息id:ae1a208d-8045-4cb1-a4d9-6d8aabc6b8c8,消息内容:"Message Test Confirm"

相关文章:

RabbitMQ 消息可靠保障

RabbitMQ 消息可靠保障 消息的可靠性保证生产者重连生产者确认解决思路A-确认机制解决思路B-备份交换机 MQ 服务器宕机导致消息丢失消费端消息的可靠性保障 消费端限流给消息生成唯一id 消息的可靠性保证 实际项目中 MQ 的流程一般是&#xff1a;生产端把消息路由到交换机&…...

Redis 作为 PHP 的会话存储

使用 Redis 作为 PHP 的会话存储&#xff0c;可以实现多个服务器之间的会话共享&#xff0c;提高会话管理的效率&#xff0c;特别是在分布式系统中。这种方法将会话数据存储在 Redis 中&#xff0c;而不是使用默认的文件系统&#xff0c;从而使多个服务器可以访问相同的会话数据…...

基于伏图的数字心脏模拟仿真APP应用介绍

一、背景介绍 心脏是保证人体正常运转最重要的动力&#xff0c;人体内的血液循环通过心血管运输到各个部位&#xff0c;因此&#xff0c;心血管系统的稳定是人体健康的关键。心血管内科领域极具专业性&#xff0c;其理论研究与技术发展日新月异&#xff0c;心血管疾病患者往往…...

智云-一个抓取web流量的轻量级蜜罐docker一键启动

智云-一个抓取web流量的轻量级蜜罐docker安装教程 github地址 https://github.com/xiaoxiaoranxxx/POT-ZHIYUN docker快速启动(v1.4) git clone https://github.com/xiaoxiaoranxxx/POT-ZHIYUN.git cd POT-ZHIYUN docker-compose up -d默认映射到80和8080端口 mysql不对外开放…...

原生HTML5、CSS、JavaScript实现简易网易云音乐播放

1.效果图 2.源码 1.index.html <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>网易云音乐</title><link rel"stylesheet" href"../CSS/index.css"> </head>…...

网上商城小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;用户管理&#xff0c;商品信息管理&#xff0c;商品类型管理&#xff0c;活动专区管理&#xff0c;新品上架管理&#xff0c;用户评价管理&#xff0c;订单管理&#xff0c;系统管理 微信端账号功能包…...

微分方程(Blanchard Differential Equations 4th)中文版Section2.2

动力系统的几何分析 捕食者-猎物系统的向量场 在第2.1节中&#xff0c;我们展示了两个不同捕食者-猎物系统的 R ( t ) R(t) R(t) 和 F ( t ) F(t) F(t) 图形&#xff0c;但没有描述我们是如何生成这些图形的。我们将在第2.5节中解决这个问题&#xff0c;采用欧拉方法推广到…...

Swift 环境搭建

Swift 环境搭建 Swift 是由苹果公司开发的一种强类型编程语言&#xff0c;用于iOS、macOS、watchOS和tvOS应用程序的开发。搭建Swift开发环境是开始使用Swift进行编程的第一步。本文将详细介绍如何在不同的操作系统上搭建Swift开发环境。 在macOS上搭建Swift环境 系统要求 …...

科技与出版

科技与出版 ISSN: 1005-0590 CN: 11-3209/G3 常设栏目&#xff1a;特别策划、产业观察、融媒之光、编辑实务、营销方略、学术探索、创作空间等。 稿件要求 (1)来稿应有创新性&#xff1b;立论科学&#xff0c;主题明确&#xff0c;推理严谨&#xff1b;词语准确&#xff0c;…...

5年前端面试之路

作者&#xff1a;星空海绵 顺便吆喝一声&#xff0c;技术大厂&#xff0c;内推捞人&#xff0c;前/后端or测试←感兴趣 --加班偶尔较多&#xff0c;但周末加班两倍工资。 --15-35K&#xff0c;工资一线城市属于一般&#xff0c;但二线城市很可以。 前言 由于公司要进行&#xf…...

产品运营(一)--产品运营是什么?

1.运营是什么&#xff1f; 通过一系列穿针引线式的行为和资源投入&#xff0c;让一件事能持续良性运转。 运营面向的主体不同&#xff0c;使用的运营手段也是不同的。作用&#xff1a;赋予产品闪耀的光芒。距离用户最近的人&#xff08;体验用户&#xff0c;成为用户?demo:k…...

学习大数据DAY41 Hive 分区表创建

目录 分区表 分区表应用场景 oracle 分区表种类 oracle 分区-范围分区 oracle 分区-列表分区 oracle 分区-散列分区 oracle 分区-组合分区 oracle 分区-分区表操作 hive 分区-创建分区表 hive 分区-分区表操作 hive 分区-动态分区表配置 上机练习 分区表 分区是将一…...

【三维目标检测模型】ImVoteNet

【版权声明】本文为博主原创文章&#xff0c;未经博主允许严禁转载&#xff0c;我们会定期进行侵权检索。 参考书籍&#xff1a;《人工智能点云处理及深度学习算法》 本文为专栏《Python三维点云实战宝典》系列文章&#xff0c;专栏介绍地址“https://blog.csdn.net/suiyin…...

力扣 | 背包dp | 279. 完全平方数、518. 零钱兑换 II、474. 一和零、377. 组合总和 Ⅳ

文章目录 一、279. 完全平方数二、518. 零钱兑换 II三、474. 一和零四、377. 组合总和 Ⅳ 一、279. 完全平方数 LeetCode&#xff1a;279. 完全平方数 朴素想法&#xff1a; 这个题目最简单的想法是&#xff0c;可以用 O ( n n ) O(n\sqrt{}n) O(n ​n)的动态规划解决&#x…...

【ECMAScript性能优化的技巧与陷阱】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…...

Swift实时监听判断是否连接有网络WIFI和蜂窝数据

本章节讲解如何使用swift连接网络&#xff0c;实时的监听到网络的状态&#xff0c;在主界面中进行调用&#xff0c;网络包含Wi-Fi 和 蜂窝。 1.封装一个判断是否有网络的类 2.在封装类注册通知 3.主界面接收注册通知&#xff0c;并且调用封装的网络类 4.成功测试&#xff0c;有…...

(三)Flink Source 数据源

Flink 数据源主要分为内置数据源和第三方数据源。其中内置数据源包含文件、Socket 连接、集合类型数据等,不需要引入其它依赖库。第三方数据源定义了 Flink 和外部系统数据交互的逻辑,Flink 提供了非常丰富的数据源连接器,例如 Kafka、Elasticsearch、RabbitMQ、JDBC 等。 …...

第四届机电一体化、自动化与智能控制国际学术会议(MAIC 2024)

目录 大会官网 会议简介 组织机构 大会主席 程序委员会主席 主讲嘉宾 征稿主题 参会说明 大会官网 http://www.icmaic.org 会议简介 第四届机电一体化、自动化与智能控制国际学术会议&#xff08;MAIC 2024&#xff09;将于2024年9月27-29日在中国成都召开。MAIC 20…...

leetcode 089 打家劫舍

leetcode 089 打家劫舍 题目 一个专业的小偷&#xff0c;计划偷窃沿街的房屋。每间房内都藏有一定的现金&#xff0c;影响小偷偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统&#xff0c;如果两间相邻的房屋在同一晚上被小偷闯入&#xff0c;系统会自动报警。 给定…...

等保测评基础知识(六)

《计算机病毒防治管理办法》51号令 第十四条 从事计算机设备或者媒体生产、销售、出租、维修行业的单位和个人,应当对计算机设备或者媒体进行计算机病毒检测、清除工作,并备有检测、清除的记录。 第十九条 计算机信息系统的使用单位有下列行为之一的,由公安机关处以警告…...

作业帮 TiDB 7.5.x 使用经验

作者&#xff1a; 是我的海 原文来源&#xff1a; https://tidb.net/blog/5f9784d3 近期在使用 TiDB 时遇到的一些小问题的梳理总结&#xff0c;大部分版本都在6.5.6和7.5.2 1、limit 导致的扫描量过大的优化 研发定时任务每天需要扫描大量数据&#xff0c;到时机器网卡被…...

c语言练习题1

1.输出Helloword /*输出Helloword*/ #include<stdio.h> int main() {printf("Hello word!");return 0; }2.整型变量的定义与使用 /*整型变量的定义与使用*/ #include <stdio.h> int main() {int a;int b;a 10;b 20;int c a b;int d a - b;printf(…...

嵌入式开发就业方向有哪些?前景未来可期!

在科技日新月异的今天&#xff0c;嵌入式系统几乎渗透到了我们生活的各个角落。从简单的家用电器到复杂的工业自动化设备&#xff0c;再到我们手中的智能手机&#xff0c;无一不体现出嵌入式技术的魅力。因此&#xff0c;嵌入式领域的就业前景广阔&#xff0c;为众多求职者提供…...

系列:水果甜度个人手持设备检测-github等开源库和方案

系列:水果甜度个人手持设备检测 -- github等开源库和方案 概述 通常来说&#xff0c;年纪轻轻的我们一般都喜欢走捷径&#xff0c;对于智能设备和算法软件领域来说&#xff0c;GitHub应该算为数不多的的捷径之一。就算因为效果不好/知识产权/方向不同等原因不用&#xff0c;…...

Visual Studio中 生成版本号

Visual Stuodio WPF项目 自动生成版本号 生成递增版本号 软件版本号主要标识了软件的版本&#xff0c;通过其可以了解软件、类库文件的当前版本&#xff0c;使得软件版本控制有所依据。 我们也可以在项目属性上可以看到相关设置的界面&#xff0c;对应的英文名称分别为&#…...

AI入门指南(四):分类问题、回归问题、监督、半监督、无监督学习是什么?

文章目录 一、前言二、分类问题、回归问题是什么&#xff1f;分类问题概念常见算法分类问题的实际应用&#xff1a;银行贷款审批案例 回归问题概念常见算法回归问题实际应用&#xff1a;线性回归模型预测房价 小结 三、监督、半监督、非监督学习是什么&#xff1f;监督学习非监…...

Linux下本地端口转发

在Linux下进行本地端口转发处理&#xff0c;可以进行如下操作&#xff1a; 1.确认NetFilter相关驱动编译到内核&#xff0c;并且CONFIG_IP_NF_TARGET_REDIRECTy&#xff1b; 2.开启转发功能&#xff1a;echo 1 > /proc/sys/net/ipv4/ip_forward&#xff1b; 3.设置转发规…...

RPC 和 HTTP 理解

网上充斥着各类类似于这样的文章&#xff1a;rpc 比 http 快了多少倍&#xff1f;既然有了 http&#xff0c;为什么还要用 rpc 调用等等。遇到这类文章&#xff0c;说明对 http 和 rpc 是由理解误区的。 这里再次重复强调一遍&#xff0c;通信协议不是 rpc 最重要的部分&#x…...

Visual Studio 2022 v17.11 发布

Visual Studio 2022 版本 17.11 正式发布 (GA)&#xff0c;此版本主要是基于用户反馈的各项改进。 “每项增强、每项修复和每项新功能均根据你的反馈而制定。无论你是在构建 Web、桌面、云还是游戏应用程序&#xff0c;Visual Studio 2022 v17.11 都旨在让你的开发体验更流畅、…...

通讯专题-RS232

1 概述 RS-232是一种点对点通信协议&#xff0c;这意味着每个数据信号沿一根导线传输&#xff08;差分信号使用两根导线传输一个数据信号&#xff09;&#xff0c;RS-232为全双工方式运行&#xff08;总线可同时发送和接收数据&#xff09;。 根据新修订的标准为容性负载为2500…...