RabbitMQ如何避免丢失消息

news2024/11/27 17:52:34

目录标题

  • 消息丢失
    • 1. 生产者生产消息到RabbitMQ Server 消息丢失场景
      • 1. 网络问题
      • 2. 代码层面,配置层面,考虑不全导致消息丢失
      • 解决方案:开启confirm模式
    • 2. 队列本身可能丢失消息
      • 1. 消息未完全持久化,当机器重启后,消息会全部丢失,甚至Queue也不见了
      • 解决方案:
        • 交换机持久化:在声明交换器时将 durable 设为 true。
        • 队列持久化:在声明队列的时候把 durable 参数设置为true。
        • 消息持久化:
      • 2. 单节点模式问题,节点挂了,消息只存在当前节点。硬盘坏了,那消息真的就无法恢复了
      • 3. 默认的集群模式,消息只会存在与当前节点中,并不会同步到其他节点,其他节点也仅只会同步该节点的队列结构
        • 工作原理
      • 解决方案:镜像部署,消息会同步到其他节点上,可以设置同步的节点个数,但吞吐量会下降。
        • 工作原理
        • GM
        • 总结
    • 3. 消费端可能丢失消息
      • 消费端采用自动ack机制,还没有处理完毕,消费端宕机。
      • 解决方案:改为手动ack,当消息正确处理完成后,再通知mq。消费端处理消息异常后,回传nack,这样mq会把这条消息投递到另外一个消费端上。
        • 消息应答的方法
        • demo

消息丢失

消息从生产到消费,要经历三个阶段,分别是生产、队列转发与消费,每个环节都可能丢失消息。
在这里插入图片描述
以下以RabbitMQ为例,来说明各个阶段会产生的问题以及解决方式。在说明之前,先回顾一下RabbitMQ的一个基本架构图
在这里插入图片描述

1. 生产者生产消息到RabbitMQ Server 消息丢失场景

1. 网络问题

外界环境问题导致:发生网络丢包、网络故障等造成RabbitMQ Server端收不到消息,因为生产环境的网络是很复杂的,网络抖动,丢包现象很常见,下面会讲到针对这个问题是如何解决的。

2. 代码层面,配置层面,考虑不全导致消息丢失

一般情况下,生产者使用Confirm模式投递消息,如果方案不够严谨,比如RabbitMQ Server 接收消息失败后会发送nack消息通知生产者,生产者监听消息失败或者没做任何事情,消息存在丢失风险;
生产者发送消息到exchange后,发送的路由和queue没有绑定,消息会存在丢失情况,下面会讲到具体的例子,保证意外情况的发生,即使发生,也在可控范围内。

解决方案:开启confirm模式

首先生产者通过调用channel.confirmSelect方法将信道设置为confirm模式,一旦信道进入confirm模式,所有在该信道上面发布的消息都会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,RabbitMQ就会发送一个确认(Basic.Ack)给生产者(包含消息的唯一deliveryTag和multiple参数)。

其实Confirm模式有三种方式实现:
串行confirm模式:producer每发送一条消息后,调用waitForConfirms()方法,等待broker端confirm,如果服务器端返回false或者在超时时间内未返回,客户端进行消息重传。

for(int i = 0;i<50;i++){
    channel.basicPublish(
            exchange, routingKey,
            mandatory, immediate,
            messageProperties,
            message.getContent()
    );
    if (channel.waitForConfirms()) {
        System.out.println("发送成功");
    } else {
        //发送失败这里可进行消息重新投递的逻辑
        System.out.println("发送失败");
    }
}

批量confirm模式:producer每发送一批消息后,调用waitForConfirms()方法,等待broker端confirm。
问题:一旦出现confirm返回false或者超时的情况时,客户端需要将这一批次的消息全部重发,这会带来明显的重复消息数量,并且当消息经常丢失时,批量confirm性能应该是不升反降的。

for(int i = 0;i<50;i++){
    channel.basicPublish(
            exchange, routingKey,
            mandatory, immediate,
            messageProperties,
            message.getContent()
    );
}
if (channel.waitForConfirms()) {
    System.out.println("发送成功");
} else {
    System.out.println("发送失败");
}

异步confirm模式:提供一个回调方法,broker confirm了一条或者多条消息后producer端会回调这个方法。 我们分别来看看这三种confirm模式。

    public void sendQueue(String appId, String handleUserId, List<String> deviceIds) {
        List<Object> list = new ArrayList<>();
        JSONObject jsonObject = new JSONObject();
        jsonObject.put(DeviceConstant.COMMAND, DELETE);
        jsonObject.put(DeviceConstant.BODY, list );
        String topicExchange = RabbitMqConstant.EXCHANGE_TOPIC_DATA;
        String routingKey = RabbitMqConstant.ROUTING_KEY_LOCAL_DATA;
        //rabbitTemplate.convertAndSend(topicExchange, routingKey, jsonObject.toJSONString());
        try {
            Channel channel = rabbitTemplate.getConnectionFactory().createConnection().createChannel(false);
            channel.confirmSelect();
            channel.basicPublish(topicExchange, routingKey, null, jsonObject.toJSONString().getBytes());
            channel.addConfirmListener(new ConfirmListener() {
                //消息失败处理
                @Override
                public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                    log.info("sendQueue-ack-confirm-fail==>exchange:{}--routingkey:{}--deliveryTag:{}--multiple:{}--message:{}", topicExchange, routingKey, deliveryTag, multiple, jsonObject);
                    try {
                        Thread.sleep(3000l);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    //重发
                    channel.basicPublish(topicExchange, routingKey, null, jsonObject.toJSONString().getBytes());
                }
                //消息成功处理
                @Override
                public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                    log.info("sendQueue-ack-confirm-successs==>exchange:{}--routingkey:{}--deliveryTag:{}--multiple:{}", topicExchange, routingKey, deliveryTag, multiple);
                }
            });
        } catch (Exception e) {
            log.error("sendQueue-ack-发送消息失败:{}", ExceptionUtils.getStackTrace(e));
        }
    }

2. 队列本身可能丢失消息

1. 消息未完全持久化,当机器重启后,消息会全部丢失,甚至Queue也不见了

仅仅持久化了Message,而Exchange,Queue没有持久化,这个持久化是无效的。

解决方案:

交换机持久化:在声明交换器时将 durable 设为 true。

// 参数1:交换机的名字
// 参数2:交换机的类型,topic/direct/fanout/headers
// 参数3:是否持久化
channel.exchangeDeclare(exchangeName,exchangeType,true);

队列持久化:在声明队列的时候把 durable 参数设置为true。

在这里插入图片描述

消息持久化:

想让消息实现持久化需要在消息生产者推送消息的方法中修改参数,MessageProperties.PERSISTENT_TEXT_PLAIN 添加这个属性。
在这里插入图片描述

2. 单节点模式问题,节点挂了,消息只存在当前节点。硬盘坏了,那消息真的就无法恢复了

如果做了消息持久化方案,消息会持久化硬盘,机器重启后消息不会丢失;但是还有一个极端情况,这台服务器磁盘突然坏了(公司遇到过磁盘问题还是很多的),消息持久化不了,非高可用状态,这个模式生产环境慎重考虑。

3. 默认的集群模式,消息只会存在与当前节点中,并不会同步到其他节点,其他节点也仅只会同步该节点的队列结构

在这里插入图片描述
上图中的三个节点组成了一个RabbitMQ的集群。其中exchange是交换器,它的元数据信息(交换器名称、交换器属性、绑定键等)在所有节点上都是一致的,而队列中的实际消息数据则只会存在于所创建的那个节点上,其它节点只知道这个队列的元数据信息和一个指向拥有这个消息的队列的节点指针
RabbitMQ集群会同步四种类型的内部元数据:队列元数据(队列名和属性)、交换器元数据(交换器名和属性)、绑定键和虚拟机。在用户访问其中任何一个rabbitmq节点时查询到的queue、user、exchange和vhost等信息都是一致的。

那为什么普通集群只保持元数据同步,消息内容却没同步呢?这里涉及到存储空间和性能的问题,如果保持每个节点都有一份消息,那会导致每个节点的空间都非常大,消息的积压量会增加且无法通过扩容节点解决积压问题。另外如果要使每个节点存储一份消息,对于持久化的消息而言,内存和磁盘同步复制机制会导致性能受到很大影响。

工作原理

在这里插入图片描述
上图中的三个节点,其中节点1是数据节点(即实际存储消息内容的节点)。
如果客户端(生产者或消费者)与节点1建立了连接,那么关于消息的收发就只在节点1上进行(可以理解为简单的单机模式);

如果客户端(消费者)是与节点2或者节点3建立的连接,此时由于数据在节点1上,那么节点2或节点3只会起到一个消息转发的作用,例如此客户端是消费者,那么消息将由节点2或节点3从节点1中拉取,再经自身节点路由给消费者端;
如果客户端(生产者),那么消息先发给节点2或3,再路由到节点1的队列中存储。

一个节点可以是磁盘节点和内存节点,磁盘节点将元数据存储在磁盘,内存节点将元数据存储在内存。
这里需要注意的是,内存节点只是将元数据(比如队列名和属性、交换器名和属性和虚拟机等)存储在内存,因此在对资源管理(创建和删除队列、交换器和虚拟机等)时的性能有所提升,但是对发布和订阅的消息速率并没有提升。
RabbitMQ要求集群中至少有一个磁盘节点,当节点加入和离开集群时,必须通知磁盘节点(如果集群中唯一的磁盘节点崩溃了,则不能进行创建队列、创建交换器、创建绑定、添加用户、更改权限、添加和删除集群节点)。如果唯一磁盘的磁盘节点崩溃,集群是可以保持运行的,但不能更改任何东西。因此建议在集群中设置两个磁盘节点,只要一个即可正常操作。总之在无法得知它们如何使用才能保证最佳时建议最好都用磁盘节点。

总结:普通集群模式并不能保证服务的高可用,因为其它节点只复制了队列和交换器等元数据信息,并没有将真实的消息内容复制到自身节点。该部署模式只解决了单节点的压力问题,但是当数据节点宕机之后便无法提供服务了,消息的路由线路受到了阻隔,客户端则无法继续与服务交互。为了解决这个问题,就需要此消息数据也能被复制到集群的其它节点中,因此rabbitmq引入了镜像部署模式。

解决方案:镜像部署,消息会同步到其他节点上,可以设置同步的节点个数,但吞吐量会下降。

在这里插入图片描述
Rabbitmq的镜像集群实际上是在普通集群的基础上增加了策略,它需要先按照普通集群的方式进行部署,部署完成之后再通过创建镜像队列的策略实现主备节点消息同步。也就是说,每个备用节点都有和主节点一样的队列,这个队列是由主节点通过创建镜像队列所产生的,且这些备用节点能及时的同步主节点中队列的入队消息。当消息设置了持久化时,每个节点都有属于自己的本地消息持久化存储机制。当消息入队和出队时,所有关于对主节点的操作都会同步给备用节点用来更新。此集群模式在主节点宕机之后备用节点所保留的消息与主节点完全一致,即可实现高可用。

工作原理

在这里插入图片描述
上图就是镜像集群模式的实现流程,其中有三个节点(主节点、备节点1、备节点2)和三个镜像队列queue(其中备节点上的queue是由主节点镜像生成的)。要注意的是,这里的主节点和备节点是针对某个队列而言的,并不能认为一个节点作为了所有队列的主节点,因为在整个镜像集群模式下,会存在多个节点和多个队列,这时候任何一个节点都能作为某一个队列的镜像主节点,其它节点则成了镜像备节点(例如:有A、B、C三个节点和Q1、Q2、Q3三个队列,如果A作为Q1的镜像主节点,那么B和C就作为了Q1的镜像备节点,在此基础上,如果B作为了Q2的镜像主节点,那么A和C就是Q2的镜像备节点)。

每一个队列都是由两部分组成的,一个是queue,用来接收消息和发布消息,另外还有一个BackingQueue,它是用来做本地消息持久化处理。客户端发送给主节点队列的消息和ack应答都将会同步到其它备节点上。

所有关于镜像主队列(mirror_queue_master)的操作,都会通过组播GM的方式同步到其它备用节点上,这里的GM负责消息的广播,mirror_queue_slave则负责回调处理(更新本次同步内容),因此当消息发送给备用节点时,则由mirror_queue_slave来做实际处理,将消息存储在queue中,如果是持久化消息则同时存储在BackingQueue中。master上的回调则由coordinator来处理(发布本次同步内容)。在主节点中,BackingQueue的存储则是由Queue进行调用。对于生产者而言,消息发送给queue之后,接着调用mirror_queue_master进行持久化处理,之后再通过GM广播发送本次同步消息给备用节点,备用节点通过回调mirror_queue_slave同步本次消息到queue和BackingQueue;对于消费者而言,从queue中获取消息之后,消息队列会等待消费者的ack应答,ack应答收到之后删除queue和BackingQueue中的该条消息,并将本次ack内容通过GM广播发送给备用节点同步本次操作。如果slave宕机了,那对于客户端的服务提供将不会有任何影响。如果master宕机了,则其它备用节点就提升为master继续服务消息不会丢失。那这其中多个备用节点是如何选择其中一个来作为master的呢?这里通过选取出“最年长的”节点作为master,因为这个备用节点相对于其它节点而言是同步时间最长、同步状态最好的一个节点,但如果存在没有任何一个slave与master完全同步的情况,那么master中未同步的消息将会丢失。

GM

GM模块实现的一种可靠的组播通讯协议,该协议能够保证组播消息的原子性,即保证组中活着的节点要么都收到消息要么都收不到。
它的实现大致为:将所有的节点形成一个循环链表,每个节点都会监控位于自己左右两边的节点,当有节点新增时,相邻的节点保证当前广播的消息会复制到新的节点上;当有节点失效时,相邻的节点会接管保证本次广播的消息会复制到下一个节点。在master节点和slave节点上的这些gm形成一个group,group(gm_group)的信息会记录在mnesia中。不同的镜像队列形成不同的group。消息从master节点对应的gm发出后,顺着链表依次传送到所有的节点,由于所有节点组成一个循环链表,master节点对应的gm最终会收到自己发送的消息,这个时候master节点就知道消息已经复制到所有的slave节点了。另外需要注意的是,每一个新节点的加入都会先清空这个节点原有数据,下图是新节点加入集群的一个简单模型:

消息的同步:
将新节点加入已存在的镜像队列,在默认情况下ha-sync-mode=manual,镜像队列中的消息不会主动同步到新节点,除非显式调用同步命令。当调用同步命令后,队列开始阻塞,无法对其进行操作,直到同步完毕。

总结

镜像集群模式通过从主节点拷贝消息的方式使所有节点都能保留一份数据,一旦主节点崩溃,备节点就能完成替换从而继续对外提供服务。这解决了节点宕机带来的困扰,提高了服务稳定性,但是它并不能实现负载均衡,因为每个操作都要在所有节点做一遍,这无疑降低了系统性能。再者当消息大量入队时,集群内部的网络带宽会因此时的同步通讯被大大消耗掉,因此对于可靠性要求高、性能要求不高且消息量并不多的场景比较适用。如果对高可用和负载均衡都有要求的场景则需要结合HAProxy(实现节点间负载均衡)和keepalived(实现HAproxy的主备模式)中间件搭配使用,下面我们将对这种场景的部署进行全流程概述。

3. 消费端可能丢失消息

消费端采用自动ack机制,还没有处理完毕,消费端宕机。

消费者完成一个任务可能需要一段时间,如果其中一个消费者处理一个长的任务并仅只完成了部分突然它挂掉了,会导致消息丢失。因为RabbitMQ 一旦向消费者传递了一条消息,便立即将该消息标记为删除。

解决方案:改为手动ack,当消息正确处理完成后,再通知mq。消费端处理消息异常后,回传nack,这样mq会把这条消息投递到另外一个消费端上。

消息应答的方法

  1. Channel.basicAck(long deliveryTag, boolean multiple):用于肯定确认。RabbitMQ 已知道该消息并且成功的处理消息,可以将其丢弃了

deliveryTag:该消息的index
multiple:是否批量.。true:将一次性ack所有小于deliveryTag的消息。

multiple参数解析
true 代表批量应答
比如说 channel 上有传送 tag 的消息 5,6,7,8 当前 tag 是 8
那么此时 5-8 的消息都会被确认收到消息应答
false 同上面相比
只会应答 tag=8 的消息 5,6,7 这三个消息依然不会被确认收到消息应答
在这里插入图片描述

  1. Channel.void basicNack(long deliveryTag, boolean multiple, boolean requeue) :用于否定确认

deliveryTag:该消息的index。
multiple:是否批量。true:将一次性拒绝所有小于deliveryTag的消息。
requeue:被拒绝的是否重新入队列。

  1. Channel.basicReject(long deliveryTag, boolean requeue):用于否定确认 (推荐使用)

deliveryTag:该消息的index。
requeue:被拒绝的是否重新入队列。

basicNack()和basicReject()的区别在于:basicNack()可以批量拒绝,basicReject()一次只能拒接一条消息。

demo

    @RabbitHandler
    @RabbitListener(queues = RabbitMqConstant.xxx , concurrency = "1-1")
    public void receiveQueueCommonLocal(Channel channel, Message message) {
        String messageBody = new String(message.getBody());
        //System.out.println("messageBody===>"+messageBody);
        try {
            //todo 业务逻辑
            /*手动确认成功
             * 参数:
             * deliveryTag:该消息的index
             * multiple:是否批量处理.true:将一次性ack所有小于deliveryTag的消息
             * **/
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
        } catch (Exception e) {
            e.printStackTrace();
            log.error("receiveQueueCommonLocal=====>ERROR:{}--josn:{}", ExceptionUtil.getMessage(e), messageBody);
            try {
                //手动确认回滚 拒绝deliveryTag对应的消息,第二个参数是否requeue,true则重新入队列,否则丢弃或者进入死信队列。
                channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
            } catch (IOException ex) {
                throw new RuntimeException(ex);
            }
        }
    }

文章来源:https://blog.51cto.com/u_15840568/5784352
https://zhuanlan.zhihu.com/p/79545722
集群:https://blog.csdn.net/weixin_43498985/article/details/122185972
消费者ack:https://blog.csdn.net/m0_64337991/article/details/122755297
https://zhuanlan.zhihu.com/p/483289106?utm_id=0

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

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

相关文章

shell脚本之“sed“命令

文章目录 1.sed编辑器概述2.sed命令常用选项3.sed命令常用操作4.sed命令演示操作部分5.总结 1.sed编辑器概述 sed是一种流编辑器&#xff0c;流编辑器会在编辑器处理数据之前基于预先提供的一组规则来编辑数据流。 sed编辑器可以根据命令来处理数据流中的数据&#xff0c;这些…

C嘎嘎~~[类 下篇 之 日期类的实现]

类 下篇 之 日期类的实现 6.const成员6.1 const成员的引入6.2const成员的概念 7.日期类的实现 6.const成员 6.1 const成员的引入 class Date { public:// 构造函数Date(int year 2023, int month 5, int day 5){_year year;_month month;_day day;}void Print(){cout &…

【STL】vector的使用

目录 前言 默认成员函数 构造函数 拷贝构造 赋值重载 迭代器 正向迭代器 反向迭代器 容量管理 查看容量和大小 扩容 判空 访问数据 下标访问 边界访问 数据修改 尾插尾删 指定位置插入删除 迭代器失效 清空 ​编辑 交换 查找数据 vector可以代替strin…

VOACAP 软件的简单介绍

VOACAP 软件可以预测短波通信中的最高可用频率、最佳传输频率、角度、延迟、反射点高度、信噪比、收发增益等参数&#xff0c;它可以直接输出文本文件&#xff0c;或者以图表输出&#xff0c;同时&#xff0c;它也可以绘制某一参数随时间、距离的变化图表。 该软件的下载安装可…

C语言从入门到精通第18天(指针和函数的联用)

指针和函数的联用 一级指针作为函数的形参二级指针 一级指针作为函数的形参 当函数的形参为数组时&#xff0c;我们定义函数如下&#xff1a; 语法: 数据类型 函数名(数据类型 数组名) 例如 : void func(int a[],int a){ 语句; } 但是在实际使用中我们通常用指针的形式进行…

GEE:如何进行对MOD09GA数据集进行水体/云掩膜并计算NDVI将其导出至云盘?

目录 01 为什么用GEE而不是传统的下载ENVIArcGIS&#xff1f; 02 操作详解 01 为什么用GEE而不是传统的下载ENVIArcGIS&#xff1f; 由于地理空间数据云中缺少2015年10月份的NDVI月合成影像&#xff0c;于是查看了地理空间数据云的NDVI数据集处理的一些介绍如下(地理空间数据…

【Linux内核】信号量semaphore机制

信号量实现方法 信号量机制是一种用于控制并发访问的同步机制&#xff0c;常用于多进程或多线程之间的协调。在Linux内核中&#xff0c;信号量机制是通过struct semaphore结构体来实现的。 每个semaphore结构体包含一个计数器和一个等待队列&#xff0c;它们用于跟踪当前可用…

Linux 并发与竞争

一、并发与竞争 1、并发 Linux 系统是个多任务操作系统&#xff0c;会存在多个任务同时访问同一片内存区域&#xff0c;这些任务可 能会相互覆盖这段内存中的数据&#xff0c;造成内存数据混乱。 多线程并发访问&#xff0c; Linux 是多任务(线程)的系统&#xff0c;所以多线…

命令firewalld和firewall-cmd用法

firewalld命令跟firewall-cmd 1.启动firewalld服务 systemctl start firewalld.service2.关闭firewalld服务 systemctl stop firewalld.service3.重启firewalld服务 systemctl restart firewalld.service4.查看firewalld状态 systemctl status firewalld.service5.开机自启…

接口测试怎么做?全网最详细从接口测试到接口自动化详解,看这篇就够了...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 抛出一个问题&…

孙鑫VC++第三章 2.基于MFC的程序框架剖析

目录 1. MFC向导生成类 2. 框架流程 2.1 WinMain 2.2 全局对象&#xff1a;theApp 2.3 AfxWinMain函数 1.AfxWinMain&#xff1a; 2.AfxGetThread函数&#xff08;thrdcore.cpp&#xff09;&#xff1a; 3.AfxGetApp是一个全局函数&#xff0c;定义于&#xff08;afxwin1…

原型/原型链/构造函数/类

认识构造函数 为什么有构造函数 因为一般的创建对象的方式一次只能创建一个对象, 利用工厂模式创建的对象&#xff0c;对象的类型都是Object类型 什么是构造函数 构造函数也称之为构造器&#xff08;constructor&#xff09;&#xff0c;通常是我们在创建对象时会调用的函数…

Uni-app项目应用总结(一)

目录 一.新建uniapp项目 第一步&#xff1a;下载HBuilder 第二步:创建uni-app项目 第三步&#xff1a;运行uni-app 二.uni-app组件使用 三.uni-app路由跳转  1.页面路由配置    (1)在pages.json中配置页面路由    (2)在pages.json中配置底部导航栏 2.路由跳转方法…

【输配电路 DZY-104端子排中间继电器 接通、信号转换 JOSEF约瑟】

DZY-104端子排中间继电器品牌:JOSEF约瑟型号:DZY-104名称:端子排式中间继电器触点容量:5A/250V功率消耗:≤1.5W/≤3W/≤7W/≤3VA/≤7VA/≥5W绝缘电阻:≥10MΩ 系列型号&#xff1a; DZY-101端子排中间继电器&#xff1b; DZY-104端子排中间继电器&#xff1b; DZY-105端子排…

华南农业大学|图像处理与分析技术综合测试|题目解答:求芒果单层坏损率

设计任务 对于一幅芒果果实内部的 CT 断层图像&#xff0c;试采用图像处理与分析技术&#xff0c;设计适当的算法和程序&#xff0c;首先分割出其中的坏损区域&#xff0c;然后计算其像素面积占整个果肉区域的百分比&#xff08;单层坏损率&#xff09;。请按统一要求写出算法…

nuc980 uboot 2017.11 移植:env 保存位置选择问题

开发环境 Win10 64位 ubuntu 20.04 虚拟机 VMware Workstation 16 Pro 开发板&#xff1a;NK-980IOT&#xff08;NUC980DK61Y&#xff09; gcc 交叉编译工具链&#xff1a; ARM 官方 gcc version 11.2.1 20220111 NUC980 uboot 版本 &#xff1a;尝试移植到 u-boot-2017.1…

科普 “平均工资又涨了”

周四晚上做了一个图&#xff0c;发了一则朋友圈&#xff0c;科普了一下为什么平均工资一直在涨&#xff1a; 曲线是 drawio 画的&#xff0c;不是类似 geogebra 画的精确数学函数&#xff0c;误差比较大&#xff0c;但大概就是这个意思。 收入应该是无标度分形的幂律分布&am…

孙鑫VC++第三章 4.窗口类、窗口类对象与窗口三者之间关系

目录 1. 创建CWnd 2. WinMain 3. 创建CButton 1. 创建CWnd 模拟CWnd类的封装过程。在解决方案ch04下添加一个新的空项目&#xff0c;项目名称为&#xff1a;WinMain&#xff0c;在项目创建完成后&#xff0c;将WinMain项目设为启动项目。 接下来在WinMain项目中添加一个名…

【C++ 学习 ④】- 类和对象(下)

目录 一、初始化列表 1.1 - 定义 1.2 - 使用初始化列表的原因 1.3 - 成员变量的初始化顺序 二、静态成员 2.1 - 静态成员变量 2.2 - 静态成员函数 三、友元 3.1 - 友元函数 3.2 - 友元类 四、内部类 五、匿名对象 5.1 - 匿名对象的特性 5.2 - 匿名对象的使用场景…

3.View的绘制流程

View是在什么时候显示在屏幕上面的?(如:MainActivity的布局文件activity_main.xml) setContentView最终的结果是将解析的xml文件中的View添加到DecorView中. 那么这个DecorView是什么时候添加到Window(PhoneWindow)的呢? DecorView是在ActivityThread.java的handleResumeA…