贝壳找房基于Flink+Paimon进行全量数据实时分组排序的实践

摘要:本文投稿自贝壳家装数仓团队,在结合家装业务场景下所探索出的一种基于 Flink+Paimon 的排序方案。这种方案可以在实时环境对全量数据进行准确的分组排序,同时减少对内存资源的消耗。在这一方案中,引入了“事件时间分段”的概念,以避免 Flink State 中冗余数据对排序结果的干扰,在保证排序结果准确性的同时,减少了对内存的消耗。并且基于数据湖组件 Paimon 的聚合模型和 Audit Log 数据在数据湖内构建了拉链表,为排序结果提供了灵活的历史数据基础。内容主要分为以下四个部分:

  1. 背景
  2. 现有的实时分组排序的一些做法及其特点
  3. Flink+Paimon进行全量数据实时分组排序
  4. 总结与展望

一、背景

家装家居业务,作为贝壳“一体三翼”战略的重要组成部分,在房屋回归居住属性的大背景下,正在迅猛发展。在过去的2023年中,家装家居业务合同额为133亿元,可比口径同比增长93%;净收入取得74%的可比口径同比增幅,达到109亿元。其中,北京和杭州的合同额突破20亿,上海全年合同额超10亿,合同额超过5亿的城市共有6个。此外,家装家居业务在更多城市跑出正循环,全年有11个城市实现运营利润为正。

在家装业务中,随着设计师对客户需求和期望的逐步了解和把握,一个装修项目往往会产生多份合同。在合同签订的过程中,业务上期望通过合同的顺序、内容、金额等信息实时匹配相应的风控和运营策略,以实现智能额度、动态尾款等等功能,为客户提供更加优质的服务体验。要实现这些功能,就需要对全量合同数据进行分组排序。然而,在实时场景中,这是一个不小的挑战。为了解决这个问题,我们进行了一系列的探索和尝试。

二、现有的实时分组排序的一些做法及其特点

首先,结合以往的经验,我们梳理了现有在实时链路中进行分组排序的方法。

第一种方法是基于Flink引擎自带的排序能力进行分组排序。该方法实现简单,逻辑直观,但是比较依赖内存资源。随着数据量的增长,排序任务所需要的内存资源会不断增加,这会给本就紧张的内存资源带来更大的压力。

第二种方法是基于离线处理结果和实时处理结果的融合计算分组排序。这种方法可以在一定程度上缓解内存压力,并且复用已有的离线计算结果。然而,同时维护并迭代离线和实时两套数据链路,会带来不小的运维压力。

第三种方法则是通过Redis、Hbase等NoSQL组件来存储和累计实时排序结果。这种占用内存少,不依赖批处理结果。然而,由于无法提供类似事务的ACID保证,存在数据脏读,进而伴随一定程度的统计误差。

通过上诉分析可以看到,在实时链路中,现有的分组排序方案在处理全量数据时,很难同时兼顾内存资源、数据链路和数据准确性三个方面。

三、Flink+Paimon进行全量数据实时分组排序

在实时数据的处理中,我们希望在需要保证数据时效性的前提下,避免依赖额外的批处理逻辑,同时减少对内存的消耗。

以Paimon为代表的数据湖组件的兴起为我们提供了全新的思路。它具备强大的数据存储和加工能力,并且能够顺滑地与Flink引擎配合,进一步扩大了Flink生态的实时处理领域的领先优势。

接下来,我们将详细介绍基于Flink+Paimon进行全量数据实时排序的新方式。整体思路可以概括为两个核心步骤:第一步,利用Flink引擎接收增量流入的数据,依托其State在内存中对数据进行排序,并将排序结果存储到数据湖中;第二步,从数据湖中读取历史时点的累计结果,结合第一步得到的内存排序结果一起计算得出最终的准确结果。

这两个步骤循环配合,互为起止,形成了一个完整的数据处理流程。为了方便表述说明,在介绍第一步的时候,我们假设第二步所产出的数据已经就绪。而在介绍第二步的时候,我们将展开讲解究竟如何产出了第一步所依赖的数据。

3.1基于历史数据的实时分组排序

在不引入额外的批处理逻辑,并且不把全量数据保留在内存中的前提下,想要实现分组排序,需要解决两个问题。其一,需要引入其他组件暂存结果,并且需要保证Flink引擎对于暂存结果的写入和读取不能同时针对同一条记录,否则会造成脏读;其二,未及时过期的Flink State数据与暂存结果之间不能出现重叠,否则会造成双算,导致结果偏高。

我们借鉴了Flink引擎中的滚动窗口思路,引入一个“事件时间分段”的概念,将实时数据根据事件时间分段,以得到准确的分组排序结果。

接下来,我们举个例子来具体说明数据的处理过程。现在假设一个项目A,它在历史上已经签约过两份合同,分别为C1和C2,它们的签约时间对应为T1和T2。实时数据中新流入一条合同签约记录C3,对应的时间为T3。将事件时间进行取整处理后,我们得到它们分别归属于起点为TG1和TG2的两个时间分段内。

SQL代码可以参考如下样例:

select project_id  -- 项目id
    ,contract_id -- 合同id
    ,sign_time  -- 签约时间
    ,FROM_UNIXTIME(UNIX_TIMESTAMP(sign_time,'yyyy-MM-dd HH:mm:ss')/(24*60*60)*(24*60*60)
           ,'yyyy-MM-dd HH:mm:ss'
           ) as event_time_group_start_point -- 事件时间分段起点
 from default_catalog.default_database.mysql_project_contract_table -- 项目合同信息

接下来,在Flink SQL的排序代码中,将项目id和事件时间段的起点一同作为分组主键,对数据按照事件时间的进行排序。SQL代码可以参考如下样例:

insert into paimon_catalog.paimon_db.dwd_project_contract_memory_result_table -- 实时数据排序结果
​
select project_id  -- 项目id      
    ,contract_id -- 合同id  
    ,sign_time  -- 签约时间         
    ,event_time       
    ,event_time_group_start_point -- 事件时间分段起点`
    ,DENSE_RANK() over (PARTITION BY project_id ,event_time_group_start_point ORDER BY event_time asc) as mem_rn -- 实时数据排序结果
 from paimon_catalog.paimon_db.dwd_project_contract_table -- 项目合同信息`

然后,我们需要根据每个事件时间分段的起点,找到对应的累计结果快照。累计结果快照代表了截至某个时点,各个装修项目已签约的合同数。(累计结果快照具体的实现方式将在3.2详细展开。)

读取到的累计结果快照代表截止各分段起点的历史结果,而实时排序结果则代表了自各分段起点起之后的增量结果。将这两个结果相加,我们就得到了准确的排序结果。

这个方法中,读入的累计结果是之前时刻的快照,它已经是既定事实,不会再被更改,进而避免了脏读;各个累计结果与增量结果之间以事件时间分段起点为边界,进而避免了双算问题。

此外,在实际应用中,我们还需要考虑如何处理历史State的逐步失效问题。由于State的生命周期是有限的,随着时间的推移,一些早期的State可能会被清除或失效。为了应对这种情况,我们可以将State的生命周期设置成略长于事件时间分段的间隔。这样,即使一些早期的State失效了,我们仍然能够确保最新的事件时间分段内的数据能够在内存中进行完整的排序,进而持续得到准确结果。

当然,对于那些已经失效的历史事件时间分段,其排序结果可能会因为数据清除而逐渐变小。为了解决这个问题,我们需要在排序操作的输出端取得每行数据排序结果的最大值。因此,在实际应用中,我们使用Paimon聚合模型来承接排序结果。

到目前为止,我们已经基于历史数据和有限的内存消耗在实时环境中实现了全量数据的分组排序。但是,这一操作的基础,是我们一直都是在假设的这样一份“随叫随到”的累计结果快照。那么,这种历史数据究竟是以如何生成、存储、和使用的呢?接下来我们将详细讨论这一部分。

3.2如何通过Paimon存储和呈现历史累计数据

接下来,让我们讨论如何生成并且呈现出各个事件时间分段起点的历史累计结果。

基于分组排序结果,我们可以在Paimon中采用聚合模型,以业务分组字段作为主键,获取各个分组中最大的事件时间和最大的排序结果。在我们的例子中,就是以项目id作为主键,得到项目A最新一份合同的签约时间,以及当前项目A所对应的合同中,最大的分组排序结果是多少。

SQL代码可以参考如下样例:

-- 分组内聚合的累计结果
CREATE TABLE if not exists paimon_catalog.paimon_db.dwd_project_contract_final_result_max_table (
   project_id   bigint COMMENT '项目id'
  ,max_rn     bigint COMMENT '该项目下当前最大的rn值'
  ,max_event_time string  COMMENT '该项目下当前最大的event_time'  
  ,PRIMARY KEY (project_id) NOT ENFORCED
) 
WITH (
   'merge-engine' = 'aggregation'
  ,'changelog-producer' = 'lookup'
  ,'fields.max_rn.aggregate-function' = 'max'
  ,'fields.max_rn.ignore-retract'='true'
  ,'fields.max_event_time.aggregate-function' = 'max'
  ,'fields.max_event_time.ignore-retract'='true'
);
​
insert into paimon_catalog.paimon_db.dwd_project_contract_final_result_max_table  
​
select project_id -- 项目id       
    ,asc_rn   -- 分组排序结果
    ,sign_time  -- 合同签约时间(事件时间)
 from paimon_catalog.paimon_db.dwd_project_contract_final_result_table -- 分组排序结果

随着数据的不断流入,累计结果表中的记录会被不断地更新,呈现出一个又一个版本。为了把累计结果的各个版本反映到历史时间轴上,我们想到了离线数据开发中的拉链表概念。

构建拉链表的关键步骤,是将相邻版本的数据记录拼接到同一行,以得到一个版本的起止时间。Paimon提供了一系列系统表,其中的Audit Log表记录了数据变更的完整日志,为构建累计结果的版本变更信息提供了可能。

基于累计结果表的Audit Log记录,我们创建了一张Paimon表来存储累计结果每个版本的起止时间信息。这张表包含两组字段:第一组字段用于表示累计结果每个版本的起始时刻,我们通过不断解析累计结果的-U日志来更新这些字段;第二组字段则用于表示累计结果每个版本的终止时间,我们利用累计结果的+U日志来实时更新这些字段。通过这种方式,我们得以追踪并体现出了累计结果的版本变更信息。

SQL代码可以参考如下样例:

CREATE TABLE if not exists paimon_catalog.paimon_db.dwd_project_contract_final_result_table_max_history_table (
   project_id      bigint COMMENT '项目id'
  ,pre_max_rn      bigint COMMENT '该项目下当前最大的rn值'
  ,pre_max_event_time  string COMMENT '该项目下当前最大的event_time'  
  ,next_max_event_time string COMMENT '被更新后该项目下当前最大的event_time'  
  ,PRIMARY KEY (project_id) NOT ENFORCED
)
WITH (
   'merge-engine' = 'aggregation'
  ,'changelog-producer' = 'lookup'
  ,'fields.pre_max_rn.aggregate-function' = 'max'
  ,'fields.pre_max_rn.ignore-retract'='true'
  ,'fields.pre_max_event_time.aggregate-function' = 'max'
  ,'fields.pre_max_event_time.ignore-retract'='true'
  ,'fields.next_max_event_time.aggregate-function' = 'max'
  ,'fields.next_max_event_time.ignore-retract'='true'
);INSERT INTO paimon_catalog.paimon_db.dwd_project_contract_final_result_table_max_history_table
select project_id   
    ,max_rn     
    ,max_event_time   
    ,'0000-00-00 00:00:00'
 from paimon_catalog.paimon_db.dwd_project_contract_final_result_max_table$audit_log
 where rowkind = '-U'
​
 union all
​
select project_id   
    ,0
    ,'0000-00-00 00:00:00'    
    ,max_event_time  
 from paimon_catalog.paimon_db.dwd_project_contract_final_result_max_table$audit_log
 where rowkind = '+U'

现在,我们已经成功记录了累计结果的版本变更信息。接下来的目标,就是展现历史上所有的版本变更记录。为此,我们再次将目光投向了Audit Log表。累计结果的版本变更信息表对应的Audit Log表中记录了其中各个版本出现的痕迹。因此,我们决定读取其中的+I和+U数据。

在读取这些数据时,我们还需要进行一定的过滤操作。因为在数据更新的过程中,会出现起止时间相等的版本。这种情况通常是由于Audit Log表中下一次的-U和上一次+U所包含的信息相同所导致的。为了避免冗余信息的干扰,我们需要在读取数据时进行甄别,过滤掉这些起止时间相等的版本。

经过这样的处理,就能够将所有的历史版本平铺呈现出来。

除了这些历史版本之外,拉链表中仍然需要包含当前位置的最新版本。所以,在展示历史版本的同时,还需要将当前最新结果合并到拉链表中,以确保数据的完整性和准确性。

SQL代码可以参考如下样例:

CREATE TABLE if not exists paimon_catalog.paimon_db.dwd_project_contract_base_version_table (
   project_id      bigint COMMENT '项目id'
  ,base_ver_start_time  string  COMMENT '该版本开始时间'
  ,base_ver_end_time   string  COMMENT '该版本结束时间'
  ,base_ver_max_rn    bigint COMMENT '该版本最大的rn'
  ,PRIMARY KEY (project_id,base_ver_start_time) NOT ENFORCED
)
WITH (
   'merge-engine' = 'deduplicate'
  ,'changelog-producer' = 'lookup'
);INSERT INTO paimon_catalog.paimon_db.dwd_project_contract_base_version_table -- 累计结果拉链表
select project_id      
    ,pre_max_event_time  as base_ver_start_time
    ,next_max_event_time as base_ver_end_time
    ,pre_max_rn      as base_ver_max_rn  
 from paimon_catalog.paimon_db.dwd_project_contract_final_result_max_history_table$audit_log -- 累计结果的版本变更信息的 audit log
 where rowkind in ('+I','+U')
  and pre_max_event_time <> next_max_event_time
​
 union all 
​
select project_id   
    ,max_event_time     as base_ver_start_time
    ,'9999-99-99 99:99:99' as base_ver_end_time   
    ,max_rn         as base_ver_max_rn
 from paimon_catalog.paimon_db.dwd_project_contract_final_result_max_table -- 累计结果

3.3 Flink+Paimon全量数据实时分组排序链路全景

在深入剖析了整体方案的两大核心操作思路及其具体实现后,让我们从整体角度审视整个方案的架构和各个组件之间的交互关系。

整个方案由若干Flink任务和Paimon模型组成,整体链路如下图。在这个方案中,实时流入的数据是整个流程的起点。这些数据在经过一系列的排序和处理后,会不断更新数据湖中的累计结果拉链表。这个拉链表不仅记录了累计结果的历史变化,还为后续的实时任务提供了历史时点的累计结果快照。通过这种方式,数据在整个方案中实现了循环流转和持续更新。

在这个流程中,通过实时数据与Paimon表进行look up join的方式,实现内存排序结果与累计结果快照的关联。在关联过程中,需要在匹配分组业务主键的同时,将事件时间分段起点与拉链表中各个版本的起止时间进行比较。

SQL代码可以参考如下样例:

CREATE TABLE if not exists paimon_catalog.paimon_db.dwd_project_contract_final_result_table (
   project_id           bigint    COMMENT '项目id'
  ,contract_id          bigint    COMMENT '合同id'
  ,sign_time           string    COMMENT '合同签约时间'
  ,event_time           TIMESTAMP(3) COMMENT 'sign_time作为event_time'
  ,event_time_group_start_point  STRING    COMMENT '业务时间分段起点'
  ,mem_rn             bigint    COMMENT '这条数据在内存排序中排出来的rn'
  ,base_ver_start_time      string    COMMENT '该版本开始时间'
  ,base_ver_end_time       string    COMMENT '该版本结束时间'
  ,base_ver_max_rn        bigint    COMMENT '该版本最大的rn'
  ,asc_rn             bigint    COMMENT '最终排序结果'
  ,PRIMARY KEY (project_id,contract_id,sign_time,event_time,event_time_group_start_point) NOT ENFORCED
)
WITH (
   'merge-engine' = 'aggregation'
  ,'changelog-producer' = 'lookup'
  ,'fields.mem_rn.aggregate-function' = 'max'
  ,'fields.mem_rn.ignore-retract'='true'
  ,'fields.base_ver_start_time.ignore-retract'='true'
  ,'fields.base_ver_end_time.ignore-retract'='true'  
  ,'fields.base_ver_max_rn.aggregate-function' = 'max'  
  ,'fields.base_ver_max_rn.ignore-retract' = 'true'    
  ,'fields.asc_rn.aggregate-function' = 'max'  
  ,'fields.asc_rn.ignore-retract'='true'         
);
​
insert into paimon_catalog.paimon_db.dwd_project_contract_final_result_table
​
-- 实时数据 lookup join 累计结果的历史快照
select s.project_id  
    ,s.contract_id
    ,s.sign_time  
    ,s.event_time
    ,s.event_step_start_point
    ,s.mem_rn
    ,coalesce(base.base_ver_start_time ,'0000-00-00 00:00:00') as base_ver_start_time
    ,coalesce(base.base_ver_end_time  ,'0000-00-00 00:00:00') as base_ver_end_time  
    ,coalesce(base.base_ver_max_rn   ,0) as base_ver_max_rn   
    ,s.mem_rn + coalesce(base.base_ver_max_rn,0) as asc_rn
 from paimon_catalog.paimon_db.dwd_project_contract_memory_result_table s -- 实时数据的内存排序结果
 left join paimon_catalog.paimon_db.dwd_project_contract_base_version_table FOR SYSTEM_TIME AS OF s.proctime AS base -- 累计结果拉链表
  on s.project_id = base.project_id
  and s.event_step_start_point >= base.base_ver_start_time 
  and s.event_step_start_point < base.base_ver_end_time

3.4 特定情况下的脏读及自修正方式

实际运行过程中,考虑到集群的计算负载以及HDFS的底层操作,数据湖中各表的更新需要一个短暂的时间。当所处理数据的业务时间刚好在业务时间段起点附近时,仍然可能出现脏读。

如图所示,C1和C2两条数据在业务时间分段起点TG2前后出现。当C1流入之后,累计结果拉链无法立刻将TG2时刻的快照版本从TG2-1更新到TG2-2。进而导致C2关联到的累计结果为未包含C1结果的TG2-1。

对于家装业务而言,由于作业人员通常在白天作业,因此我们可以将事件时间分段的交界点调节为每日零点,从而规避这一问题。

此处我们跳出家装业务,考虑这种特定情况的通用解法。在关联任务中,除了使用内存排序结果关联拉链表以外,还可以同时使用拉链表的变更记录来反向关联内存排序数据。这样能够将拉链表最新的版本变动同步到实时数据当中,进而修正C2的最终排序结果。

四、总结与展望

随着Flink引擎能力的不断增强以及Flink生态中像Paimon这样优秀项目的崛起,数仓开发人员在实时数据处理方面拥有了更多优秀的工具。这些新的功能和特性使得我们能够更加灵活地应对各种数据需求。

“新居住”的时代正在到来。贝壳家装数仓将不断吸收新理念、新技术,继续深入挖掘数据价值,持续打磨贴合家装场景的数据大脑,引领家装行业的数据实践,进一步助力贝壳家装业务为消费者提供更优质、更便捷的服务体验,让“家的美好,一站实现”的美好愿景成为更多家庭的现实。

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

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

相关文章

上涨至13.6分!当之无愧的顶级期刊,影响因子“狂飙”,门槛较低,2个月可录!

本期解析 1、2024年6月20日&#xff0c;科睿唯安正式公布2024年度《期刊引证报告》。 2、本次主要解析Elsevier旗下一本TOP顶刊&#xff0c;期刊表现优秀&#xff0c;在最新的影响因子更新中由12.8上涨至13.6&#xff0c;是一本妥妥评职高分宝刊&#xff01;接下来看看它是否…

Stable Diffusion【真人模型】:人像光影摄影极限写实真实感大模型

大家好&#xff0c;我是极客菌 今天和大家分享一个基于SD1.5的真人大模型&#xff1a;人像光影摄影极限写实真实感大模型。 该模型具有以下特点&#xff1a; 真实肤感&#xff08;在面部肌理和皮肤肌理上均有加强学习&#xff0c;拒绝ai出图假的问题&#xff09; 永不脱妆&a…

经典老动画电影大全,老动画片大全集全部百度网盘,资源下载百度云

当今社会越来越重视学前教育了&#xff0c;今儿童启蒙的教育也越来越受人们的关注和重视。为了满足社会对未来人才的需要&#xff0c;学前教育成为当今教育领域重要角色的一环。当今动画篇是主流&#xff0c;内容精彩纷呈&#xff0c;越来越受到儿童的喜爱。 儿童的语言敏感期&…

ee trade:黄金投资是选择短线交易还是长线投资

黄金投资既可以通过短线交易获取快速收益&#xff0c;也可以采取长线投资策略获得稳健回报。本文将详细比较这两种策略的特点和适用性&#xff0c;为新手投资者提供参考。 短线交易 短线交易指在较短的时间内多次买卖以获取利润&#xff0c;通常交易周期为数日到数周。以下是…

PointCloudLib-KDtree-如何使用 KdTree 进行搜索

在本教程中,我们将介绍如何使用 KdTree 查找特定点或位置的 K 个最近邻,然后我们还将介绍如何查找用户指定的某个半径内的所有邻居(在本例中为随机)。 理论入门 k-d 树或 k 维树是计算机科学中使用的一种数据结构,用于在具有 k 维的空间中组织一定数量的点。它是一个二叉…

用热传感器提高散热片的效率

每天一篇行业发展资讯&#xff0c;让大家更及时了解外面的世界。 更多资讯&#xff0c;请关注B站/公众号【莱歌数字】&#xff0c;有视频教程~~ 散热器的尺寸通常是根据功率、气流、设备热约束和物理几何形状的要求进行冷却应用的。 更大的功耗需要更高性能的散热器或更大、…

记录一个Xshell使用中Xmanager...X11转发的提示问题

希望文章能给到你启发和灵感&#xff5e; 如果觉得有帮助的话&#xff0c;点赞关注收藏支持一下博主哦&#xff5e; 阅读指南 一、环境说明1.1 硬件环境1.2 软件环境 二、问题和错误三、解决四、理解和延伸一下 一、环境说明 考虑环境因素&#xff0c;大家适当的对比自己的软硬…

DBdoctor功能介绍

绍DBdoctor的主要功能&#xff0c;按照事件先后涵盖了事前、事中、事后三个阶段。事前的主动问题发现、SQL性能评估、自动巡检与报表、空间预测与诊断&#xff1b;事中的性能洞察、根因诊断、锁分析、优化建议&#xff1b;事后的审计分析、根因推导、问题快照。按照使用者包含了…

手持小风扇品牌有哪些?分享口碑最好的五款手持小风扇

手持小风扇在炎热的夏季成为了许多人解暑的好帮手。它们不仅轻便便携&#xff0c;随时随地都能为我们带来清凉和舒适。然而&#xff0c;市场上手持小风扇的品牌繁多&#xff0c;让人眼花缭乱。为了帮助大家做出更明智的选择&#xff0c;接下来我们将分享口碑最好的五款手持小风…

RAG | (ACL24规划-检索增强)PlanRAG:一种用于生成大型语言模型作为决策者的规划检索增强生成方法

原文&#xff1a;PlanRAG: A Plan-then-Retrieval Augmented Generation for Generative Large Language Models as Decision Makers 地址&#xff1a;https://arxiv.org/abs/2406.12430 代码&#xff1a;https://github.com/myeon9h/PlanRAG 出版&#xff1a;ACL 24 机构: 韩国…

MQTT自动回复消息工具

点击下载《MQTT自动回复消息工具V1.0.0》 1. 前言 在进行IoT系统开发时&#xff0c;各个小组成员通常是同步进行项目开发&#xff0c;经常会遇到设备端和前后端开发人员开发进度不协调的情况&#xff0c;此时接口还没开发完&#xff0c;也没有可以调试的环境&#xff0c;只能…

Vue - HTML基础学习

一、元素及属性 1.元素 <p>我是一级标题</p>2.嵌套元素 把元素放到其他元素之中——这被称作嵌套。 <p>我是<strong>一级</strong>标题</p>3.块级元素 块级元素在页面中以块的形式展现&#xff0c;会换行&#xff0c;可嵌套内联元素。 …

RAID详解及配置实战

目录 一、RAID磁盘阵列及详解 1.1 了解RAID 1.1.1 简单理解 1.1.2 对比了解 1.2 RAID磁盘阵列介绍 1.3 RAID功能实现 1.4 RAID实现的方式 1.5 RAID级别详解 1.5.1 RAID -0 1.5.2 RAID -1 1.5.3 RAID -5 1.5.4 RAID -10&#xff08;RAID 10&#xff09; 1.6 阵列卡…

基于Java微信小程序校园自助打印系统设计和实现(源码+LW+调试文档+讲解等)

&#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN作者、博客专家、全栈领域优质创作者&#xff0c;博客之星、平台优质作者、专注于Java、小程序技术领域和毕业项目实战✌&#x1f497; &#x1f31f;文末获取源码数据库&#x1f31f;感兴趣的可以先收藏起来&#xff0c;还…

《Windows API每日一练》6.2 客户区鼠标消息

第五章已经讲到&#xff0c;Windows只会把键盘消息发送到当前具有输入焦点的窗口。鼠标消息则不同&#xff1a;当鼠标经过窗口或在窗口内被单击&#xff0c;则即使该窗口是非活动窗口或不带输入焦点&#xff0c; 窗口过程还是会收到鼠标消息。Windows定义了 21种鼠标消息。不过…

股掌柜:解读全球行情,实时资讯满足全方位投资需求

近年来&#xff0c;随着信息技术的飞速发展&#xff0c;金融交易也逐渐向极速化的方向发展。极速交易成为了投资者们追求高效、稳定、及时的首选。在全球行情实时变动的背景下&#xff0c;了解市场动态和全球资讯成为了投资者们最为看重的需求。只有及时把握市场脉搏&#xff0…

Snipaste--一款截屏神奇分享,桌面置顶显示截图

桌面置顶显示截图! 桌面置顶显示截图! 桌面置顶显示截图! 官网&#xff1a; https://zh.snipaste.com/ 介绍&#xff1a;Snipaste 是一个简单但强大的截图工具&#xff0c;也可以让你将截图贴回到屏幕上&#xff01;下载并打开 Snipaste&#xff0c;按下 F1 来开始截图&#xf…

Hive笔记-6

6.2.8 聚合函数 1) 语法 count(*)&#xff0c;表示统计所有行数&#xff0c;包含null值&#xff1b; count(某列)&#xff0c;表示该列一共有多少行&#xff0c;不包含null值&#xff1b; max()&#xff0c;求最大值&#xff0c;不包含null&#xff0c;除非所有值都是null&a…

CppTest单元测试框架(更新)

目录 1 背景2 设计3 实现4 使用4.1 主函数4.2 使用方法 1 背景 前面文章单元测试之CppTest测试框架中讲述利用宏ADD_SUITE将测试用例自动增加到测试框架中。但在使用中发现一个问题&#xff0c;就是通过宏ADD_SUITE增加多个测试Suite时&#xff0c;每次运行时都是所有测试Suit…

GraphQL:简介

GraphQL 图片来源&#xff1a; 我们将探索GraphQL 的基础知识&#xff0c;并学习如何使用Apollo将其与 React 和 React Native 等前端框架连接起来。这将帮助您了解如何使用 GraphQL、React、React Native 和 Apollo 构建现代、高效的应用程序。 什么是 GraphQL&#xff1f;…