RabbitMQ确保消息可靠性

news2025/1/16 12:42:47

消息丢失的可能性

在这里插入图片描述
支付服务先扣减余额和更新支付状态(这俩是同步调用),然后通过RabbitMq异步调用支付服务更新订单状态。但是有些情况下,可能订单已经支付 ,但是更新订单状态却失败了,这就出现了消息丢失。

  1. 发送者在发送的过程中出现了网络故障
  2. RabbitMQ在发送消息的过程中出现了问题
  3. 消费者在更新订单状态的时候出现了问题

发送者的可靠性

发送者确认机制需要与MQ进行通信和确认,会影响消息发送的效率且一般出现的概率极低,所以一般不用这个。


方法1. 发送者重连

确保发送者与MQ之间连接的可靠性。有的时候由于网络波动,可能出现发送者连接MQ失败的情况,这个配置是关闭的,可以开启连接失败后的重连机制:

spring:
  rabbitmq:
    connection-timeout: 1s # 设置MQ的连接超时时间
    template:
      retry:
        enabled: true # 开启超时重试机制(默认是false)
        initial-interval: 1000ms # 失败后的初始等待时间
        multiplier: 1 # 失败后下次的等待时长倍数,下次等待时长 = initial-interval * multiplier
        max-attempts: 3 # 最大重试次数

【注】:当网络不稳定时,利用重试机制可以提高消息发送的成功率,但是SpringAMQP提供的重试机制是阻塞式的重试,如果需要多次重试等待,当前线程被阻塞,会影响性能。
如果对业务性能有要求,建议禁用重试机制,如果一定要使用,要合理的配置等待时常和重试次数,或使用异步线程来执行发送消息的代码。

方法2. 发送者确认

确保消息发送的可靠性。SpringAMQP提供了Publisher Confirm和Publisher Return两种机制,开启确认机制后,当发送者发送消息给MQ后,MQ会返回确认结果给发送者,返回的结果有以下几种情况:

  • 消息投递到MQ,但是路由失败,此时通过PublisherReturn返回路由异常信息,然后返回ACK,告知投递成功。例如:
    • 消息发送给图中的exchange1,但是RoutingKey写错了,没有匹配到正确的队列,也会导致路由失败。
    • 消息发送给图中的exchange2,但是它底下没有绑定新的队列,就会导致路由失败。
  • 临时消息【不需要往磁盘做持久化的消息】投递到MQ,并入队成功,返回ACK,告知投递成功。
  • 持久消息投递到MQ,并入队完成持久化,返回ACK,告知投递成功。
  • 其他情况都会返回NACK,告知投递失败。
    在这里插入图片描述
    步骤:
  1. 在发送方publisher所在的微服务的application.yml中配置:
spring:
  rabbitmq:
    publisher-confirm-type: correlated # 开启publisher confirm机制,并设置confirm类型
    publisher-returns: true # 开启publisher return机制

publisher-confirm-type有三种模式:

  • none:关闭confirm机制
  • simple:同步阻塞等待MQ回执消息
  • correlated:MQ异步回调方式返回回执消息(常用)
  1. 开启回调机制:每个RabbitTemplate只能配置一个ReturnCallback,在发送者publisher所在的项目启动时配置即可。
@Configuration
@RequiredArgsConstructor
@Slf4j
public class MqConfig {
    private final RabbitTemplate rabbitTemplate;
    @PostConstruct // 在Bean初始化完成后调用这个方法(只会调用一次)
    public void init() {
    	// 返回ACK,但是此时路由失败,就会走这个方法
        rabbitTemplate.setReturnsCallback(returnedMessage -> {
            log.error("监听到了消息return callback");
            log.debug("exchange: {}", returnedMessage.getExchange());
            log.debug("routingKey: {}", returnedMessage.getRoutingKey());
            log.debug("message: {}", returnedMessage.getMessage());
            log.debug("replyCode: {}", returnedMessage.getReplyCode());
            log.debug("replyText: {}", returnedMessage.getReplyText());
        });
    }
}
  1. 开启消息确认机制:发送消息、指定消息ID、每次发送消息都需要配置一个ConfirmCallback
public void testConfirmCallback() {
  CorrelationData cd = new CorrelationData(UUID.randomUUID().toString());
  cd.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {
      // 【几乎不可能发生】Future发生异常时的处理逻辑
      @Override
      public void onFailure(Throwable ex) {
          log.error("spring ampq处理确认结果异常", ex);
      }

      // 成功,拿到MQ结果,判断是ACK还是NACK
      @Override
      public void onSuccess(CorrelationData.Confirm result) {
          if(result.isAck()) {
              // ACK
              log.debug("收到ACK,消息发送成功");
          }else {
              // NACK
              log.debug("收到NACK,消息发送失败,失败原因:{}",result.getReason());
          }
      }
  });
  rabbitTemplate.convertAndSend("hmall.direct", "blue", "hello world", cd); // 发送消息
}

MQ的可靠性

RabbitMQ一般会将收到的信息保存到内存(速度快)中,降低消息收发的延迟,这样会导致:

  1. MQ宕机,内存中的消息会丢失。
  2. 内存空间有限,消费者故障或处理过慢,会导致消息积压,引发MQ阻塞。
    在这里插入图片描述

【案例】发送者往MQ发消息,MQ会把数据保存到内存中,如果内存满了,MQ就会把一部分数据迁移到磁盘中暂时进行持久化存储,移动到磁盘的这段时间发送者发送的消息就会产生丢失。

方法1. 数据持久化

数据持久化就是把数据持久化到磁盘,但是不是向上边那个案例,等满了再去持久化(被动),而是提前进行持久化。

  1. 交换机的持久化(默认开启的)
    在这里插入图片描述
  2. 队列的持久化(默认开启的)
    在这里插入图片描述
  3. 消息持久化(默认是非持久的)
    在发送消息的时候设定的
    在这里插入图片描述
    案例】:比较一下持久化和非持久化的性能。
    发100w条消息给MQ:
    这是非持久化的方式:使用纯内存的方式存储,每次内存满之后,MQ就会把消息写到磁盘中,此时就会出现阻塞状态,处理速度降低到0
    在这里插入图片描述
    问题】可能出现消息丢失和MQ阻塞
    解决办法】使用持久化的方式:
public void testSendPersistentMsg() {
    // 自定义构建消息
    Message msg = MessageBuilder.withBody("hello world".getBytes(StandardCharsets.UTF_8)) // 消息体
            .setDeliveryMode(MessageDeliveryMode.PERSISTENT) // 投递模式(持久化)
            .build();
    for (int i = 0; i < 100000; ++i) {
        rabbitTemplate.convertAndSend("simple.queue", msg);
    }
}

Mq并没有阻塞,每发一条消息就赶紧把它存到磁盘中,和纯内存方式相比,不会有个中断的过程。
在这里插入图片描述

方法2. Lazy Queue(推荐)

问题】:由于使用了消息持久化的方式,发到MQ的消息不仅要到内存,还要在磁盘中写一份,这会导致整体的并发能力下降
特征】:

  • 接收到消息后直接入磁盘,不再存储到内存
  • 在写磁盘的时候也对写入磁盘的操作进行一些优化,比传统的写操作高很多
  • 消费者要消费消息时,才会从磁盘中读取并加载到内存
    • 问题】:可能会影响消费者处理消息的速度
    • 解决】:可以提前缓存部分消息到内存,最多2048条

控制台声明Lazy Queue队列

在这里插入图片描述

Java代码添加

声明Bean
@Bean
public Queue lazyQueue(){
    return QueueBuilder
            .durable("lazy.queue")
            .lazy() // 开启Lazy模式
            .build();
}
@RabbitListener注解
@RabbitListener(queuesToDeclare = @Queue(
        name = "lazy.queue",
        durable = "true",
        arguments = @Argument(name = "x-queue-mode", value = "lazy") // 开启Lazy模式
))
public void listenLazyQueue(String msg){
    log.info("接收到 lazy.queue的消息:{}", msg);
}

消费者的可靠性

消费者确认机制

为了确认消费者是否成功处理消息,当消费者处理消息结束后,应该向MQ发送一个回执,告知MQ自己的消息处理状态。有如下几种消息处理状态:

  • ack:处理消息成功,RabbitMQ从队列中删除该消息
  • nack:消息处理失败,RabbitMQ需要再次投递消息
  • reject:消息处理失败并拒绝该消息,RabbitMQ从队列中删除该消息【在处理的过程中,发现消息的内容有问题,没有重试的必要,直接拒绝就行】

【注意】:不管是哪种情况,都应该等消息处理完后得到结果再返回,不要一拿到消息就返回
返回消息处理状态的过程,类似于处理事务,事务处理成功,返回ACK;处理失败,返回NACK

SpringAMQP允许通过在消费者的配置文件选择ACK的处理方式,有三种:

  • none:不处理,消息投递给消费者后立刻ack,消息会立刻从MQ中删除,别用
  • manual:手动模式,需要在业务代码中调用api,发送ack或reject,存在业务入侵,但是更灵活。
  • auto:自动模式,利用AOP对消息处理逻辑进行了环绕增强
    • 业务处理正常:自动返回ack
    • 业务处理异常:自动返回nack
    • 消息处理或校验异常【MessageConversionException】:自动返回reject
spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: auto# 不做处理

失败重试策略

在消费者出现异常时,利用本地重试,而不是无限的重新入队到mq,可以在消费者的yaml文件中添加配置来开启重试机制。

spring:
  rabbitmq:
    listener:
      simple:
        retry:
          enabled: true # 开启消费者失败重试
          initial-interval: 1000ms # 初识的失败等待时长为1秒
          multiplier: 1 # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval
          max-attempts: 3 # 最大重试次数(达到最大重试次数后,MQ会把消息丢弃)
          stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false

问题】:在开启重试模式后,重试次数耗尽,如果消息仍然失败,默认会把消息进行丢弃。
解决】:因此需要有MessageRecoverer接口来处理,包含三种不同的实现:

  • RejectAndDontRequeueRecoverer(默认):重试耗尽后,直接reject,丢弃消息。
  • ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队。
  • RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机 。
    在这里插入图片描述

修改失败重试策略为RepublishMessageRecoverer

  1. 定义接收失败的交换机、队列、
  2. 定义RepublishMessageRecoverer
@Configuration
public class ErrorMessageConfiguration {
    // 定义接收失败的交换机
    @Bean
    public DirectExchange errorExchange() {
        return new DirectExchange("error.direct");
    }

    // 定义接收失败的队列
    @Bean
    public Queue errorQueue() {
        return new Queue("error.queue");
    }

    // 定义绑定关系
    @Bean
    public Binding errorQueueBinding() {
        return BindingBuilder.bind(errorQueue())
                .to(errorExchange())
                .with("error");
    }
    
    // 定义失败处理策略
    @Bean
    public MessageRecoverer messageRecoverer(RabbitTemplate rabbitTemplate) {
        return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
    }
}

业务幂等性

f(x) = f(f(x)),指同一个业务,执行一次或多次对业务状态的影响是一致的。

  • 幂等业务:查询业务、删除业务
  • 非幂等业务:用户下单需要扣减库存、用户退款业务需要恢复余额

方案1. 唯一消息id

给每个消息设置一个唯一id,利用id区分是否是重复消息:

  • 每条消息都生成一个唯一id,与消息一起投递给消费者
  • 消费者接收到消息后处理自己的业务,业务处理成功后将消息id保存到数据库中
  • 如果下次又收到相同消息,去数据库查询判断是否存在,存在则视为重复消息放弃处理
  1. 在发送方配置Bean用来自动创建消息id
@Configuration
public class MqConfig {
    @Bean
    public MessageConverter messageConverter() {
        Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
        converter.setCreateMessageIds(true); // 配置自动创建消息id
        return converter;
    }
}
  1. 在接收方接收消息id
@RabbitListener(queues = "simple.queue")
    public void listenSimpleQueue(Message msg) { // 使用字符串发送,就用字符串接收
        log.info("监听到simple.queue的消息:{}", msg);
        log.info("消息id:{}", msg.getMessageProperties().getMessageId());
        // throw new RuntimeException("故意的");
    }

在这里插入图片描述

方案2. 业务判断(常用)

结合业务逻辑,基于业务本身做判断。
案例】:当用户下单成功后,通过MQ通知交易服务来修改订单状态为已支付(这里记作消息1),修改成功后交易服务返回ACK给MQ,此时出现了网络的故障,MQ没有收到交易服务发送的ACK,MQ认为交易服务宕机,消息又重新入队。
就在此刻,用户点击了申请退款,直接向交易服务修改订单状态为退款中(这个操作没有走MQ,此时订单状态是退款中,但是消息1还在消息队列中)。
此时网络恢复了,MQ又将消息1发送给交易服务,此时交易服务又把订单状态标记为已支付(订单申请退款中的状态又被覆盖了)。
在这里插入图片描述
解决】:通知来的时候,先判断订单的状态,再进行操作。

@Component
@RequiredArgsConstructor
public class PayStatusListener {
    private final IOrderService orderService;
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "trade.pay.success.queue", durable = "true"),
            exchange = @Exchange(name = "pay.direct"),
            key = "pay.success"
    ))
    public void listenPaySuccess(Long orderId) {
        // 1.查询订单
        Order order = orderService.getById(orderId);
        // 2.判断订单状态是否为未支付
        if(order == null || order.getStatus() != 1) {
            // 不做处理
            return;
        }
        // 3.标记订单状态为已支付
        orderService.markOrderPaySuccess(orderId);
    }
}

在这里插入图片描述

延迟消息

延迟消息:发送者发送消息时指定一个时间,消费者不会立刻收到消息,而是在指定时间之后才收到消息。
延迟任务:设置在一定时间后才执行的任务。
在这里插入图片描述

方案1. 死信交换机

当一个队列中的消息满足下列情况之一的,就会成为死信

  • 消费者使用basic.reject或basic.nack声明消费失败,并且消息的requeue参数设置为false。
  • 消息是一个过期消息(达到队列设置的过期时间 或 消息本身设置的过期时间),超时无人消费。
  • 要投递的队列消息堆积满了,最早的消息可能成为死信。

队列通过dead-letter-exchange属性指定了一个交换机,那么该队列中的死信就会投递到这个交换机中,这个交换机就叫做死信交换机(DLX)。
在这里插入图片描述

  1. 声明死信队列、死信交换机、它们之间的绑定关系:
@RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "dlx.queue", durable = "true"), // 死信队列
            exchange = @Exchange(name = "dlx.direct", type = ExchangeTypes.DIRECT), // 死信交换机
            key = {"hi"} 
    ))
    public void listenDlxQueue(String msg) {
        log.info("消费者监听到dlx.queue的消息: " + msg);
    }
  1. 声明普通队列、普通交换机、它们之间的绑定关系,并把队列绑定到死信交换机上(此时就不需要把它绑定消费者了):
@Configuration
public class NormalConfiguration {
    @Bean
    public DirectExchange normalExchange() { // 普通交换机
        return ExchangeBuilder.directExchange("normal.direct").build();
    }
    @Bean
    public Queue normalQueue() { // 普通队列
        return QueueBuilder
                .durable("normal.direct") // 队列名字
                .deadLetterExchange("dlx.direct") // 死信交换机名字
                .build();
    }
    @Bean
    public Binding normalQueueBinding(Queue normalQueue, DirectExchange normalExchange) { // 绑定关系
        // 把队列绑定到交换机
        return BindingBuilder
                .bind(normalQueue) // 队列
                .to(normalExchange) // 交换机
                .with("hi");// 这里绑定关系要和普通队列的绑定关系保持一致
    }
}

  1. 发送延迟消息:
@Test
public void testSendDelayMsg() {
    rabbitTemplate.convertAndSend("normal.direct", "hi", "hello world", message -> {
        // 当消息被转成Message对象后,还可以进一步做加工
        message.getMessageProperties().setExpiration("10000"); // 设置消息过期时间(10s)
        return message;
    });
}

【注】:normal.direct和normal.queue之间绑定的BindingKey 与 dlx.direct和dlx.queue之间绑定的BindingKey要一致

方案2. 延迟消息插件DelayExchange(推荐)

这个插件可以将普通交换机改造为支持延迟消息功能的交换机,当消息投递到交换机后,可以暂存一段时间,到后期再投递到队列。

一、安装插件

  1. 插件下载地址:DelayExchange
  2. 需要把插件放在RabbitMQ插件目录对应的数据卷下
docker volume inspect mq-plugins

在这里插入图片描述
在这里插入图片描述

  1. 执行命令,安装插件
docker exec -it rabbitmq rabbitmq-plugins enable rabbitmq_delayed_message_exchange

二、使用插件

  1. 声明延迟交换机:只要设置delay的属性为true即可
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(name = "delay.queue", durable = "true"),
        exchange = @Exchange(name = "delay.direct", delayed = "true", type = ExchangeTypes.DIRECT), // 只要设置一个delayed属性为true即可
        key = {"hi"}
))
public void listenDelayQueue(String msg) {
    log.info("消费者监听到delay.queue的消息: " + msg);
}
  1. 发送延迟消息:通过消息头x-delay来设置过期时间
@Test
public void testSendDelayMsgByPlugin() {
    rabbitTemplate.convertAndSend("delay.direct", "delay", "hello world", message -> {
        message.getMessageProperties().setDelay(10000);// 添加延迟消息属性
        return message;
    });
}

延迟消息的实现需要记录消息的过期时间,计时的时钟需要依赖cpu,是个cpu密集型任务。因此使用延迟消息时,需要避免同一时刻在mq里存在大量的延迟消息(尽可能地让延迟消息的延迟时间不要太长)。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

重生之我在21世纪学C++—string

一、string 概念 string 字符串是一种更加高级的封装&#xff0c;string 字符串中包含大量的方法&#xff0c;这些方法可以使得字符串的操作变得更加简单。如果 string 使用的好&#xff0c;慢慢你就不想使用字符数组来存放字符串了&#xff0c;因为更简单嘛。 C 将字符串直接…

day10_Structured Steaming

文章目录 Structured Steaming一、结构化流介绍&#xff08;了解&#xff09;1、有界和无界数据2、基本介绍3、使用三大步骤(掌握)4.回顾sparkSQL的词频统计案例 二、结构化流的编程模型&#xff08;掌握&#xff09;1、数据结构2、读取数据源2.1 File Source2.2 Socket Source…

x86_64搭建ARM交叉编译工具链

点击上方"蓝字"关注我们 01、下载 >>> GCC 64位交叉编译下载&#xff1a;https://releases.linaro.org/components/toolchain/binaries/latest-7/arm-linux-gnueabihf/ 喜欢那个版本自己找 02、简介 >>> 交叉编译器中“交叉”的意思就是在一个架构…

迅翼SwiftWing | ROS 固定翼开源仿真平台正式发布!

经过前期内测调试&#xff0c;ROS固定翼开源仿真平台今日正式上线&#xff01;现平台除适配PX4ROS环境外&#xff0c;也已实现APROS环境下的单机飞行控制仿真适配。欢迎大家通过文末链接查看项目地址以及具体使用手册。 1 平台简介 ROS固定翼仿真平台旨在实现固定翼无人机决策…

IOS界面传值-OC

1、页面跳转 由 ViewController 页面跳转至 NextViewController 页面 &#xff08;1&#xff09;ViewController ViewController.h #import <UIKit/UIKit.h>interface ViewController : UIViewControllerend ViewController.m #import "ViewController.h" …

用 Python 自动化处理日常任务

&#x1f496; 欢迎来到我的博客&#xff01; 非常高兴能在这里与您相遇。在这里&#xff0c;您不仅能获得有趣的技术分享&#xff0c;还能感受到轻松愉快的氛围。无论您是编程新手&#xff0c;还是资深开发者&#xff0c;都能在这里找到属于您的知识宝藏&#xff0c;学习和成长…

Linux:地址空间(续)与进程控制

hello&#xff0c;各位小伙伴&#xff0c;本篇文章跟大家一起学习《Linux&#xff1a;地址空间与进程控制》&#xff0c;感谢大家对我上一篇的支持&#xff0c;如有什么问题&#xff0c;还请多多指教 &#xff01; 如果本篇文章对你有帮助&#xff0c;还请各位点点赞&#xff0…

链家房价数据爬虫和机器学习数据可视化预测

完整源码项目包获取→点击文章末尾名片&#xff01;

亿道三防丨三防笔记本是什么意思?和普通笔记本的优势在哪里?

三防笔记本是什么意思&#xff1f;和普通笔记本的优势在哪里&#xff1f; 在现代社会中&#xff0c;笔记本电脑已经成为人们工作和生活中不可或缺的一部分。然而&#xff0c;在一些特殊行业或环境中&#xff0c;普通笔记本电脑由于其脆弱性和对环境条件的敏感性&#xff0c;往…

通过proto文件构建 完整的 gRPC 服务端和客户端案例

基础教程-简单案例&#xff08;快入入门java-grpc框架&#xff09; 参考官方入门案例教程&#xff1a;里面我看proto编译&#xff0c;其实直接用maven就能直接将.proto文件编译成java代码。快速入门 | Java | gRPC 框架https://grpc.org.cn/docs/languages/java/quickstart/ …

UML系列之Rational Rose笔记九:组件图

一、新建组件图 二、组件图成品展示 三、工作台介绍 最主要的还是这个component组件&#xff1b; 然后还有这几个&#xff0c;正常是用不到的&#xff1b;基本的使用第四部分介绍一下&#xff1a; 四、基本使用示例 这些&#xff0c;主要是运用package还有package specifica…

K8S 节点选择器

今天我们来实验 pod 调度的 nodeName 与 nodeSelector。官网描述如下&#xff1a; 假设有如下三个节点的 K8S 集群&#xff1a; k8s31master 是控制节点 k8s31node1、k8s31node2 是工作节点 容器运行时是 containerd 一、镜像准备 1.1、镜像拉取 docker pull tomcat:8.5-jre8…

c++领域展开第十二幕——类和对象(STL简介——简单了解STL)超详细!!!!

文章目录 前言STL简介什么是STLSTL的版本STL的六大组件STL的重要性如何学习STL 总结 前言 上篇博客我们了解了初阶的模版函数&#xff0c;以及有关的一些使用方法。 今天我们来了解了解STL库的有关知识 跟我一起上车吧 STL简介 什么是STL STL&#xff1a;是C标准库的重要组成…

Onedrive精神分裂怎么办(有变更却不同步)

Onedrive有时候会分裂&#xff0c;你在本地删除文件&#xff0c;并没有同步到云端&#xff0c;但是本地却显示同步成功。 比如删掉了一个目录&#xff0c;在本地看已经删掉&#xff0c;onedrive显示已同步&#xff0c;但是别的电脑并不会同步到这个删除操作&#xff0c;在网页版…

科研绘图系列:R语言绘制微生物物种系统发育树(phylogenetic tree)

禁止商业或二改转载,仅供自学使用,侵权必究,如需截取部分内容请后台联系作者! 文章目录 介绍构成要素有根树与无根树构建方法应用领域说明的问题教程加载R包数据下载导入数据数据预处理系统发育树可视化准备画图数据1. 构建基础系统发育树 `p1`2. 添加条形图 `p2`3. 添加热图…

1️⃣Java中的集合体系学习汇总(List/Map/Set 详解)

目录 01. Java中的集合体系 02. 单列集合体系​ 1. Collection系列集合的遍历方式 &#xff08;1&#xff09;迭代器遍历&#xff08;2&#xff09;增强for遍历​编辑&#xff08;3&#xff09;Lambda表达式遍历 03.List集合详解 04.Set集合详解 05.总结 Collection系列…

微信小程序:跨页面数据修改全攻略

一、引言 在微信小程序开发中&#xff0c;常常会遇到需要在不同页面之间修改数据的情况。比如在商品详情页添加商品到购物车后&#xff0c;购物车页面需要实时更新商品数量和总价&#xff1b;在用户设置页面修改了个人信息&#xff0c;首页的用户信息展示区域也需要同步更新。…

寒假第一次牛客周赛 Round 76回顾

AC数&#xff1a;2&#xff08;A、C&#xff09; B 思路&#xff1a; 等价于求&#xff1a; 数量最多的字符 #include<stdio.h> int main() {int n,num;int a[26]{0};//用于存储字母 a 到 z 的出现次数。scanf("%d",&n);char s[n];scanf("%s",s)…

【 PID 算法 】PID 算法基础

一、简介 PID即&#xff1a;Proportional&#xff08;比例&#xff09;、Integral&#xff08;积分&#xff09;、Differential&#xff08;微分&#xff09;的缩写。也就是说&#xff0c;PID算法是结合这三种环节在一起的。粘一下百度百科中的东西吧。 顾名思义&#xff0c;…

Ubuntu打开文件夹不显示文件

1.情况介绍 使用ubuntu打开文件夹不显示文件夹里面的内容&#xff0c;而是直接打开了资源查看器。 2.解决办法 命令行安装nautilus sudo apt-get install nautilus