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

B站搜索建库架构优化实践

前言

搜索是B站的重要基础功能,需要对包括视频、评论、图文等海量的站内优质资源建立索引,处理来自用户每日数亿的检索请求。离线索引数据的正确、高效产出是搜索业务的基础。我们在这里分享搜索离线架构整体的改造实践:从周期长,流程复杂的手工构建流程,改造为高容量、高性能、易迭代的分布式建库架构的过程。

业务背景

B站是一个典型的多资源搜索场景,除了视频外,还接入了包括UP主、番剧影视(PGC)、直播等几十种不同类型的资源。除了资源类型多以外,各种资源的数据源的形式也多种多样,包括数据库、上游业务接口、Hive表等等。这些数据通过离线近线的聚合和构建,以全量和增量实时流两种方式生产出索引,在线上的服务中生效。

图片

实际业务中,除了搜索业务自己维护的视频MySQL数据库外,还接入了不同形式的数据来源,一并添加到全量/增量索引数据中构建。这些数据有的仅T+1更新全量;有的则会提供增量数据流和接口,接入时也希望数据变更能在搜索索引中实时生效。

全量(base)索引:以文件形式提供,包含某一特定时间点之前的全部数据,通常每天产出、更新一次。

增量(delta)索引:以数据流(消息队列)的形式提供,每条消息对应一份稿件的完整信息。增量索引能实时同步更新,适用于时效性要求高的场景,满足用户对新稿件的检索需求。

这些第三方数据由于各种原因(MySQL容量或性能限制,数据维护团队不同等),不能直接集成到搜索的MySQL数据库中。这些数据的引入在当前的离线和近线建库流程中是独立的,随着数据的种类增加,搜索离线建库流程也逐渐复杂起来。

索引数据产出流程

以视频索引为例,构建视频索引时除了搜索自己维护的MySQL中视频信息外,还需要接入多种第三方数据到索引中,如接入以下三类数据:

  • 数据A:第三方数据库+binlog

  • 数据B:不开放DB直接访问,以导出的全量snapshot+接口+数据流形式间接提供

  • 数据C:Hive表

全量数据产出

为了获取稿件信息,全量建库流程需要先后将这些不同的数据源获取到本地,合并为新的全量数据集,再构建出二进制全量索引。

图片

增量数据产出

对于增量索引,我们希望将每条稿件的完整字段(即包含搜索MySQL+数据源A/B/C的所有字段信息)聚合为一条增量索引消息下发。

我们监听binlog A和数据流B,但只关心其中有变化的稿件id,通过变更搜索MySQL中相应稿件的一个标志位,触发该稿件的一条binlog及后续的增量建库流程。

在后续的建库流程中,处理binlog时增加根据ID查询MySQL A和接口B的逻辑,这样就能从MySQL A的查询结果,接口B的响应和本地的文件C中拿到稿件的对应数据,写入到增量索引数据流中。

图片

注意这里对MySQL A和接口B有着严格的时效性要求,即查询得到的数据不能比数据流下发的数据版本更旧,否则无法保证数据的最终一致性。

举个例子,假如MySQL A的binlog来自主库,而按ID查询时访问了MySQL A的从库。

当A中某个稿件的字段发生变化时,在处理主库的对应binlog时会从从库中查询当前值。由于主库从库之间的同步有一定延迟,这时有可能查询到旧值而不是主库中的新值,导致新值不会在索引中生效,除非该稿件有其他变更再次触发binlog处理。

将MySQL A的binlog调整为由对应从库导出则可避免该问题发生。

索引生效流程

B站的检索引擎和正排服务加载索引数据的过程如下:

  1. 更新全量索引:服务周期性地获取新的全量索引文件,解析元数据,将索引加载到内存中。

  2. 消费增量数据流:加载了全量索引后,服务会从元数据指定的时间点开始消费增量索引数据流,直到消费进度接近当前的最新产出。消费到的数据会在服务侧实时地建立增量索引,以支持查询。当一份稿件同时出现在增量索引和全量索引时,全量索引中的数据被覆盖;稿件在数据流中出现多次时,增量索引中的旧值也会被新值覆盖。

  3. 提供服务:当增量数据流的产出进度被消费追平后,服务进入就绪状态,开始处理请求。

问题与挑战

随着搜索业务复杂度的增加和数据规模的增长,现有建库逻辑在效率和资源层面逐渐难以为继。

性能

新投稿的增多和历史投稿的积累,让搜索MySQL在建库过程中的负载越来越大,开始出现性能瓶颈。

造成MySQL高负载的主要场景有:

1. 表结构变更:需要复制全表数据。数据复制期间,数据库负载显著增加。

2. 新增字段并批量导入:将新字段导入数据库时需要大量写入,增加数据库压力。

  • 缓解措施:控制写入过程,在负载低峰期小批量执行。

3. 全量索引构建时的扫库操作:索引构建流程每次需要先查询搜索MySQL数据库获取全量稿件信息,dump到文件中,再进行索引构建。当在基线外另行构建索引以支持AB实验时,数据库的查询负载也会相应地倍增。

  • 缓解措施:错开不同构建任务的执行时间;将扫库结果和元数据保存下来,供多个建库任务复用。

当MySQL负载高时,主从数据库同步会出现延迟,导致索引数据更新不及时。用户不能及时看到最新的投稿和变更,体验受损害。

我们在日常实验和迭代时,都必须时刻考虑对数据库的性能影响,而缓解数据库压力的措施往往导致迭代周期变长,不同的需求上的索引迭代也不能并行推进。性能瓶颈和稳定性风险成为了索引迭代的主要障碍。

维护成本

  1. 搜索索引原始数据没有统一的存储承载,数据可能来自多个数据库、文件、甚至接口。离线和近线需要各自维护复杂的拼接逻辑,导致迭代困难,开发周期长。数据不符合预期时,难以定位原因。

  2. 全量数据和增量数据的产出逻辑差异大,没有机制能保证两套链路上最终的产出结果一致。数据的不一致可能影响业务效果。

  3. 全量索引是单机构建,构建周期,实例部署和数据分片都需要人工维护,每次迭代都消耗大量人力成本。

  4. 增量数据接入新的实时数据时,数据提供方需要同时提供全量、增量数据和查询接口。搜索业务和数据提供方的对接、开发成本都较高。

资源

每次构建索引时,都需要对原始数据重新切词分析,构建正排、倒排索引。即使数据和策略没有变化也需要重复计算。这些计算会消耗大量资源,并且增加索引构建所需的时间。

设计目标

反思索引构建流程中的问题,不管是复杂的多数据源合并流程、还是MySQL的性能压力延迟风险,归根结底都是存储设施能力不足,不能直接承载全部索引数据导致的问题复杂化。我们期望将索引依赖的全部数据聚合到统一的存储设施中,作为基准数据源,直接基于该数据源导出全量数据和增量数据流,并将后续的建库任务彻底分布式化,达到数据统一、可扩展性大幅增强的目的。

预期新的架构设计可解决当前的痛点:

  1. 用分布式的存储设施承接搜索数据,后续的构建任务也进行分布式化改造,让建库链路中不再有单点的性能瓶颈。

  2. 全量和增量都从统一的存储设施中获取,完全屏蔽原有不同数据来源对建库过程的影响,可以让建库的流程简单化,降低开发维护的成本。

  3. 有了足够的存储能力之后,我们可以将稿件切词这类较为稳定的中间计算结果保存下来,只在有数据或策略有变化时重新计算。节省计算资源的同时进一步缩短全量构建周期。

方案设计

既然MySQL不能满足我们的需求,我们的人力也不允许从零开始造一个轮子来维护索引数据,首要的问题是找到一个合适的存储设施来作为基础,在其之上开发数据处理和建库逻辑。

存储选型

我们希望新的存储方案可以具有以下特性:

  • 高容量:可以存入目前全部的索引数据。易于水平拓展。

  • 高吞吐量:允许大批量的数据写入和导出。

  • 低延迟:可以快速随机读写单个稿件数据。

  • 易于迭代:数据的结构可以灵活迭代,无需Online DDL操作(https://dev.mysql.com/doc/refman/8.4/en/innodb-online-ddl-operations.html)。

我们从现有的数据库系统中寻求合适的选型:对于数据灵活迭代的需求排除了MySQL,TiDB等关系型数据库;批量更新稿件部分字段的需求对文档存储性的NoSQL数据库不友好;HBase,Cassandra等列族存储的数据模型比较合适,但简单写入查询延迟较高;KV(键值)存储延迟低,但数据模型不太匹配。

最终我们选择在KV存储的基础上封装行式(Row-Oriented)数据库的形式,以达到兼顾吞吐与延迟的效果,并选择了b站内部存储组件Taishan作为底层KV存储。

Taishan是B站内部的高性能分布式KV存储,具备多分片水平拓展能力,可应对大规模存储需求,简单读写的延迟也较低,此外还具有以下特性:

  • 有序映射:key是有序的,支持scan(范围查询)。

  • 支持 CompareAndSwap 操作:基于CAS原子操作可实现乐观锁,在并发的数据更新时防止冲突。

  • 高效的数据导出:Taishan 支持快速将数据全量导出到对象存储中,而不需要全表scan。

架构设计

我们以Taishan为基础存储设施,在其上建立了强大的数据存储层(基于表格存储模型)和统一的数据接入和导出层。

图片

引入存储层后,我们将原始数据源跟具体的建库逻辑隔离,并抽象出可高度复用的数据导入导出逻辑,显著降低了维护成本和后续开发成本。

离线和近线使用相同的数据来源并复用导出逻辑,从根本上消除了全量索引和增量索引数据不一致的可能。

高容量的存储层允许我们以增量计算形式来源空间换取时间:将一些中间计算结果(如切词和Embedding等)也一并保存,仅在相关稿件属性有变化时触发计算并更新;建库时直接使用保存下来的结果,计算量大幅减少。

在全部数据都经Taishan聚合后,离线近线的流程变得清晰直观起来,同时在根本上消除了全量和增量数据不一致的可能性。增量计算的引入也减少了大量重复的计算量,节省了资源。新的数据流程使得迭代和维护都大为简化。

数据存储层

表格模型

基于KV存储之上封装出表格存储模型:

  • 行:每一行代表一个稿件,通过稿件ID标识。

  • 列:每一列存储稿件的若干相关字段,通过字段族名(CF,column family)标识。

  • 单元格:行和列的组合确定一个单元格,对应KV存储中的一条记录。

图片

如上表所示,稿件ID和行一一对应。而稿件的具体字段和列(CF)是多对多的关系:

  • 同一稿件的若干相关字段可以打包保存在一列(如title和uname),这样会大幅减少KV的访问次数,提高效率。

  • 表格中不同的列可以保存相同的字段。例如:

  • 这里eb,eb1两列分别对应doc_embedding字段的不同版本。指定不同的列组合(fs,seg,eb)/(fs,seg,eb1),可以构建出两版包含相同(title,uname,title_term,uname_term),以及不同版本doc_embedding的索引数据。

支持并发写入

Taishan的CAS支持允许我们在不同写入方之间通过乐观锁来同步,避免多个写入方同时更新一个单元格时发生写入丢失。

如果一列的写入方唯一,也可以不使用CAS直接写入。

支持并发写入消除了将多个字段放进同一CF的后续维护风险。即使同一列后续要增加新的写入方,也无需对该列进行拆分改造。

Key设计

Key由ID和字段族名组成。下图是稿件ID1234567890的seg列对应的实际Key内容。ID和列名间用 “:”来分隔。

图片

Data Orientation

Key的设计决定了数据排列方式。底层Taishan存储中,数据按Key的字符串顺序连续排列。

ID1:CF1ID1:CF2
ID2:CF1ID2:CF2

我们将稿件ID放在列名称前,这样表格数据在Taishan中实际存放形式如下:

ID1:CF1

ID1:CF2

ID2:CF1

ID2:CF2

实际使用场景中我们主要按行扫描(获取相同ID的所有字段值),较少按列扫描(获取某一字段下所有取值)。同一ID下所有列连续分布的排列使得按ID(行)读取时有更好的访问局部性和Cache命中率。

同时,ID使用大端序int64保存。大端序的特点是作为字符串看待时的顺序和数字的升序一致,让遍历过程总是按ID升序执行,符合人的直觉与习惯。

Value设计

Value总是以一个varint N作为header,该varint标识随后的N个字节保存的是元数据,其余的字节则用于存储实际数据。

下图是一个包含header、元数据(大小为8字节)和数据(大小为5字节)的value。

图片

通过这种设计,我们得到了一个高效、灵活且近似于传统表格存储的系统,可以为索引建库提供强大的支持。

序列化

抛弃JSON

在最初的建库流程中,我们使用JSON Lines文件格式保存原始的稿件数据,例如:

video.jsonl

{"id": 81403056, "title": "高燃舞台演绎B站最美的夜", "uname": "哔哩哔哩晚会", "doc_embedding": [0.168322, 0.015824, 0.091791, -0.2059]}{"id": 613621262, "title": "【触手猴】「強風オールバック」を弾いてみた【Piano】", "uname": "marasy_触手猴", "doc_embedding": [0.007262, 0.040466, 0.028768, 0.161083]}

JSON格式具有良好的可读性,但效率和性能不足,主要体现在:

  • 存储效率低:每条记录中都会重复保存字段名及符号(如 {} 和 ""),浪费大量存储空间。

  • 序列化性能低:文本格式的解析性能较差。

如果将完整的稿件字段拆分到多个列中分别保存,导出和查询时的序列化消耗会更加严重。

转向protobuf

为了解决上述问题,我们选择了Protocol Buffers(protobuf)格式来序列化稿件字段。以下是简化的Video消息的protobuf定义:

video.proto

message Video {    int64 id = 1;    string title = 2;    string uname = 3;    repeated float doc_embedding = 4;}

protobuf的优势主要有:

  • 存储空间优化:protobuf使用field number(以varint形式存储)来区分字段,相比于JSON中存储完整字段名,大大节省了存储空间。

  • 性能提升:protobuf的反序列化速度优于JSON。

  • 类型安全:和JSON相比,protobuf为数据字段提供了强类型保证,增强了我们对数据的信心。严格的数据类型也从源头上消除了JSON 固有的最大安全整数(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER)问题。

高效地合并

对于protobuf消息,多个序列化后的数据块(buffer)可以直接拼接来达到合并的效果:

Video v;
v.ParseFromString(buf1 + buf2);

​​​​​​​

这与将其分布反序列化后再执行合并是等效的:

Video v1, v2;
v1.ParseFromString(buf1);
v2.ParseFromString(buf2);
v1.MergeFrom(v2);

​​​​​​​

假设稿件的标题保存在列1,稿件Embedding信息保存在列2,buf1/buf2分别是对应单元格查询的数据。我们只需要直接concate未经反序列化的两段buffer即可拼接出稿件完整数据。

在使用Cord(https://protobuf.dev/reference/cpp/cpp-generated/#cord)或zero_copy_stream(https://protobuf.dev/reference/cpp/api-docs/google.protobuf.io.zero_copy_stream/)的情况下,连这一次拼接也可以省去。

变更数据流

Taishan原生支持binlog导出,可以将变更的Key和Value导出到数据流。
但直接使用Taishan的binlog会有以下问题:

  • 大批量写入数据时,会产出大量消息,对后续的整个近线链路乃至线上服务造成压力。

  • Value只包含变更的列,获取完整的数据仍需按ID扫描Taishan表。

为了灵活控制变处理,我们封装了写入层,在数据写入Taishan完成后另外输出一条变更消息,示例如下:

{"id": 613621262, "cf_changed": ["eb"]} // 稿件av613621262的eb列发生了变更

数据变更的场合是否输出对应变更消息到流中是可指定的,批量写入数据时我们选择不触发。

我们也省略了变更后的值,需要的消费者可以直接查询Taishan表的主节点获取最新的字段。

查询从节点可能得到更新前的旧值,破环数据的最终一致性。

数据接入层

存储方案和序列化方式的确定后,我们开始将现有的数据统一接入到Taishan中。具体而言是将原有的数据库全量增量以及T+1更新的数据全部写入上文所说的Taishan表格,并按需同步到变更数据流。

T+1数据接入

T+1更新的数据没有实时的增量更新,只需要定时写入全量。

写入的逻辑是高度复用的,同过指定配置将hive表/TSV/CSV/JSON Lines文件映射到Taishan中的对应列。

图片

从新版全量中被删除的数据需要额外的处理:我们需要跟上一版数据对比,找出这些被删除的数据,将这些数据同步从Taishan中删除。

实时数据接入

实时数据接入需要将数据实时写入Taishan,并同步到变更数据流。写入的顺序必须为先写Taishan再写变更数据流,保证后续处理变更流时能从Taishan主节点查到最新取值。

图片

由于我们不能对上游提供的变更数据流的字段定义做任何的限制和假设,处理增量的Worker需要开发少量的解析逻辑。

增量写入只写入了最近有变更的数据,为了让Taishan保存全部的历史数据,在实时数量接入后,我们还需要再获取一份全量数据(产出时间在增量接入后),将这些数据也写入Taishan中。

这的全量写入过程和T+1数据接入类似,区别在于只需要初始化时执行一次,后续的变更都可以通过增量来获取。另外T+1全量写入时需要考虑和增量写入冲突的情况,具体在后文介绍。

写入冲突处理

全量vs增量

包含实时数据的数据在导入时往往也需要刷入一份全量数据做初始化。

一般来说,全量中的数据会比来自数据流的旧,直接写入会将来自数据流的新值覆盖。

因此写入时需要利用CAS,仅当满足Precondition:值不存在时,才会写入。

增量vs增量

同一列可以有多个写入方,比如两个worker消费两条不同的数据流写入同一列中的两个不同字段。此时需要先读取稿件的该列的旧值,更新其中的新字段后,将完整的新值写入。

当两个worker同时运行时,可能发生写写冲突(Write-Write Conflict),导致一个worker写入的新值,被另外的worker覆盖而丢失。

在这种场景下,我们同样使用CAS,在计算出新值后,仅当满足Precondition:值==旧值时,才会写入新值,当Precondition不满足时,必须重试

增量计算

我们将一些中间计算结果也保存在Taishan中。典型的场景如稿件标题的切词和向量化的结果。具体而言,如果使用的计算策略和稿件标题没有变化,切词和向量化的结果可以认为是稳定的。因此我们可以保存这些结果来避免重复计算,只在稿件的属性有变化时更新计算结果。

增量更新切词结果的工作流如下图所示:

图片

和实时数据接入类似,在首次接入时也需要对全部已有数据进行一次切词,让历史稿件也有切词结果。初始化过程同样需要使用CAS来规避写写冲突。

数据导出层

数据进入Taishan后,我们需要从中导出需要的数据来构建全量和增量索引。

对于一份索引,其全量和增量构建任务的配置是共用的,获取到实际数据后的处理逻辑也是一致的。

Taishan中不同的列可以保存字段的多个版本,具体构建全量和增量索引时选用哪些列中的字段,需要在配置中指定。配置中以白名单的形式指定列,无需担心新增列对已有构建任务产生影响。

不同版本的索引大部分情况下只需要调整配置再另行部署即可产出。

图片

全量数据导出

Taishan会每日定期将全量数据备份到公司内部的对象存储。我们通过备份数据好的数据,遍历其中的全部KV,也就是按行遍历表格,取出指定的列来获取全量索引数据。Taishan备份效率是非常高的,通常在分钟级,这也大幅地减少了扫库的时间开销。

增量数据导出

增量数据的导出通过消费变更数据流实现。消费后对每条消息(ID+变更的CF)反查Taishan,获取所需的完整字段。

数据迭代

新的存储机制简化了索引的迭代流程,只需要将新数据写入Taishan,然后调整构建任务关注的列即可。迭代的开发量大幅下降,基本只需复用现有流程,调整配置后部署新任务。

图片

随着新存储方案和增量计算等优化的落地,索引构建的周期缩短了一半以上。迭代周期缩短的同时,消除了链路上的性能瓶颈和延迟风险。

分布式构建

搜索索引最早都是通过物理机crontab定时执行脚本实现。脚本执行各种扫库操作,将数据加载到内存中,并产出一份完整的JSON Lines文本文件作为原始索引数据集,然后调用indexer进行全量切词和构建正排/倒排索引。物理机部署稳定性没有保证且难以维护,我们首先把构建迁移到K8S集群上,尝试以服务形式部署。这样虽然保证了建库任务的稳定性,但是建库任务资源利用率很低。建库服务构建时需要大量资源,但在大多数时间里是不消耗任何资源的,容器依然需要占据相应的CPU/内存资源。虽然通过超配(低软限高硬限)并错开任务触发时间可以来减少资源空置的情况,但维护较为繁琐。最终我们选择将建库迁移到业界广泛使用的分布式计算框架Spark上,利用Spark潮汐资源进行索引构建,一方面可以加大构建并发,另一方面也可以对资源进行更充分的利用。

为了能够尽可能的复用及降低维护成本,从流程上进行抽象,将索引构建流程分以下主要步骤:

  1. 读取数据:从配置中加载并进行数据读取,Taishan源在对象存储的导出文件为sst格式,使用官方开源的JNI库进行二次封装。

  2. 解码 :Taishan非Spark原生支持的数据源,需要额外开发解析逻辑。

  3. 再分片:由于导出的单一文件分片数据量较大,一次性读取将占用大量内存,甚至OOM。为解决上述问题,参考Spark的cache机制,在读取并解码文件数据流的过程时先将文件载入到指定内存buffer中,若buffer装满则生成一个分片(partition)并写入hdfs中。以此将较少的文件分片(如128个)拆分为较多的hdfs文件分片(如5000个),便于Spark的后续处理。

  4. 稿件处理:通过配置对稿件数据进行一系列的处理操作,如过滤、字段映射等。

  5. 编码 & 索引构建:将稿件内容转化为索引需要的特定编码形式,如Flatbuffer、Protobuffer。并根据索引类型和Meta信息,产出索引。

  6. 压缩打包:对构建出的索引及Meta数据进行打包,产出最终索引文件。 

除更省资源外,Spark构建的任务并发度也更高,进一步缩短了构建时间,最终达到小时级别。

总结与展望

通过这一系列的技术更新和流程优化,我们的索引构建架构最终从早期的单机构建发展到分布式的数据存储和建库任务,能力更强的同时也更易维护和迭代,索引构建周期实现了从天级到小时级别的飞跃式进步,为业务的未来发展奠定了坚实基础。

参考

  • https://protobuf.dev/programming-guides/encoding/#varints

  • https://dev.mysql.com/doc/refman/8.4/en/innodb-online-ddl-operations.html

  • https://research.google/pubs/large-scale-incremental-processing-using-distributed-transactions-and-notifications/

  • https://protobuf.dev/programming-guides/encoding/#last-one-wins

-End-

作者丨网管、HevLfreis、瑚太朗、穅泊

相关文章:

B站搜索建库架构优化实践

前言 搜索是B站的重要基础功能,需要对包括视频、评论、图文等海量的站内优质资源建立索引,处理来自用户每日数亿的检索请求。离线索引数据的正确、高效产出是搜索业务的基础。我们在这里分享搜索离线架构整体的改造实践:从周期长,…...

XSS反射实战

目录 1.XSS向量编码 2.xss靶场训练(easy) 2.1第一关 2.2第二关 方法一 方法二 2.3第三关 2.4第四关 2.5第五关 2.6第六关 2.7第七关 第一种方法: 第二种方法: 第三个方法: 2.8第八关 1.XSS向量编码 &…...

远程消息传递的艺术:NSDistantObject在Objective-C中的妙用

标题:远程消息传递的艺术:NSDistantObject在Objective-C中的妙用 引言 在Objective-C的丰富生态中,NSDistantObject扮演着至关重要的角色,特别是在处理分布式系统中的远程消息传递。它允许对象之间跨越不同地址空间进行通信&…...

指向派生类的基类指针、强转为 void* 再转为基类指针、此时调用虚函数会发生什么?

指向派生类的基类指针、强转为 void* 再转为基类指针、此时调用虚函数会发生什么? 1、无论指针类型怎么转,类对象内存没有发生任何变化,还是vfptr指向虚函数表,下面是成员变量,这在编译阶段就已经确定好了&#xff1b…...

操作系统(Linux实战)-进程创建、同步与锁、通信、调度算法-学习笔记

1. 进程的基础概念 1.1 进程是什么? 定义: 进程是操作系统管理的一个程序实例。它包含程序代码及其当前活动的状态。每个进程有自己的内存地址空间,拥有独立的栈、堆、全局变量等。操作系统通过进程来分配资源(如 CPU 时间、内…...

react的setState中为什么不能用++?

背景: 在使用react的过程中产生了一些困惑,handleClick函数的功能是记录点击次数,handleClick函数被绑定到按钮中,每点击一次将通过this.state.counter将累计的点击次数显示在页面上 困惑: 为什么不能直接写prevStat…...

2.2算法的时间复杂度与空间复杂度——经典OJ

本博客的OJ标题均已插入超链接,点击可直接跳转~ 一、消失的数字 1、题目描述 数组nums包含从0到n的所有整数,但其中缺了一个。请编写代码找出那个缺失的整数。你有办法在O(n)时间内完成吗? 2、题目分析 (1)numsS…...

【CentOS 】DHCP 更改为静态 IP 地址并且遇到无法联网

文章目录 引言解决方式标题1. **编辑网络配置文件**:标题2. **确保配置文件包含以下内容**:特别注意 标题3. **重启网络服务**:标题4. **检查配置是否生效**:标题5. **测试网络连接**:标题6. **检查路由表**&#xff1…...

Linux 操作系统 --- 信号

序言 在本篇内容中,将为大家介绍在操作系统中的一个重要的机制 — 信号。大家可能感到疑惑,好像我在使用 Linux 的过程中并没有接触过信号,这是啥呀?其实我们经常遇到过,当我们运行的进程当进程尝试访问非法内存地址时…...

黑马前端——days09_css

案例 1 页面框架文件 <!DOCTYPE html> <html lang"zh-CN"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><meta http-equiv"X-UA-Compati…...

【Python爬虫】技术深度探索与实践

目录 引言 第一部分&#xff1a;Python爬虫基础 1.1 网络基础 1.2 Python爬虫基本流程 第二部分&#xff1a;进阶技术 2.1 动态网页抓取 2.2 异步编程与并发 2.3 反爬虫机制与应对 第三部分&#xff1a;实践案例 第四部分&#xff1a;法律与道德考量 第五部分&#x…...

智启万象|挖掘广告变现潜力,保障支付安全便捷

谷歌致力于为开发者提供 先进的广告变现与支付解决方案 一起回顾 2024 Google 开发者大会 了解如何利用谷歌最新工具和功能 提高变现收入&#xff0c;优化用户体验&#xff0c;保障交易安全 让变现更上一层楼 广告检查器是谷歌 AdMob 平台最新推出的高级测试工具&#xff0c;开…...

函数递归,匿名、内置行数,模块和包,开发规范

一、递归与二分法 一&#xff09;递归 1、递归调用的定义 递归调用&#xff1a;在调用一个函数的过程中&#xff0c;直接或间接地调用了函数本身 2、递归分为两类&#xff1a;直接与间接 #直接 def func():print(from func)func()func() # 间接 def foo():print(from foo)bar…...

Springboot3 整合swagger

一、pom.xml <dependency><groupId>org.springdoc</groupId><artifactId>springdoc-openapi-starter-webmvc-api</artifactId><version>2.1.0</version></dependency> 二、application.yml # SpringDoc配置 # springdoc:swa…...

查看同一网段内所有设备的ip

使用命令提示符&#xff08;CMD&#xff09;进行扫描 查看本机IP地址 首先通过 ipconfig /all 命令查看本机的IP地址&#xff0c;确定你的网段&#xff0c;例如 192.168.1.。 Ping网段内每个IP地址 接着使用循环命令&#xff1a; for /L %i IN (1,1,254) DO ping -w 1 -n …...

Spark MLlib 特征工程(上)

文章目录 Spark MLlib 特征工程(上)特征工程预处理 Encoding:StringIndexer特征构建:VectorAssembler特征选择:ChiSqSelector归一化:MinMaxScaler模型训练总结Spark MLlib 特征工程(上) 前面我们一起构建了一个简单的线性回归模型,来预测美国爱荷华州的房价。从模型效果来…...

《SPSS零基础入门教程》学习笔记——03.变量的统计描述

文章目录 3.1 连续变量&#xff08;1&#xff09;集中趋势&#xff08;2&#xff09;离散趋势&#xff08;3&#xff09;分布特征 3.2 分类变量&#xff08;1&#xff09;单个分类变量&#xff08;2&#xff09;多个分类变量 3.1 连续变量 &#xff08;1&#xff09;集中趋势 …...

2024年杭州市网络与信息安全管理员(网络安全管理员)职业技能竞赛的通知

2024年杭州市网络与信息安全管理员&#xff08;网络安全管理员&#xff09;职业技能竞赛的通知 一、组织机构 本次竞赛由杭州市总工会牵头&#xff0c;杭州市人力资源和社会保障局联合主办&#xff0c;杭州市萧山区总工会承办&#xff0c;浙江省北大信息技术高等研究院协办。…...

SpringBoot参数校验详解

前言 在web开发时&#xff0c;对于请求参数&#xff0c;一般上都需要进行参数合法性校验的&#xff0c;原先的写法时一个个字段一个个去判断&#xff0c;这种方式太不通用了&#xff0c;Hibernate Validator 是 Bean Validation 规范的参考实现&#xff0c;用于在 Java 应用中…...

安全基础学习-SHA-1(Secure Hash Algorithm 1)算法

SHA-1(Secure Hash Algorithm 1)是一种密码学哈希函数,用于将任意长度的输入数据(消息)转换成一个固定长度的输出(哈希值或摘要),长度为160位(20字节)。SHA-1的主要用途包括数据完整性验证、数字签名、密码存储等。 1、SHA-1 的特性 定长输出:无论输入数据长度是多…...

leetcode350. 两个数组的交集 II,哈希表

leetcode350. 两个数组的交集 II 给你两个整数数组 nums1 和 nums2 &#xff0c;请你以数组形式返回两数组的交集。返回结果中每个元素出现的次数&#xff0c;应与元素在两个数组中都出现的次数一致&#xff08;如果出现次数不一致&#xff0c;则考虑取较小值&#xff09;。可…...

基于YOLOv8的缺陷检测任务模型训练

文章目录 一、引言二、环境说明三、缺陷检测任务模型训练详解3.1 PCB数据集3.1.1 数据集简介3.1.2 数据集下载3.1.3 构建yolo格式的数据集 3.2 基于ultralytics训练YOLOv83.2.1 安装依赖包3.2.2 ultralytics的训练规范说明3.2.3 创建训练配置文件3.2.4 下载预训练模型3.2.5 训练…...

【upload]-ini-[SUCTF 2019]CheckIn-笔记

上传图片木马文件后看到&#xff0c;检查的文件内容&#xff0c;包含<? 一句话木马提示 检查的文件格式 用如下图片木马&#xff0c;加上GIF89a绕过图片和<?检查 GIF89a <script languagephp>eval($_POST[cmd])</script> .user.ini实际上就是一个可以由用…...

uniapp条件编译使用教学(#ifdef、#ifndef)

#ifdef //仅在xxx平台使用#ifndef //除了在xxx平台使用#endif // 结束 标识平台APP-PLUSAPPMP微信小程序/支付宝小程序/百度小程序/头条小程序/QQ小程序MP-WEIXIN微信小程序MP-ALIPAY支付宝小程序MP-BAIDU百度小程序MP-TOUTIAO头条小程序MP-QQQQ小程序H5H5APP-PLUS-NVUEApp nv…...

NXP i.MX8系列平台开发讲解 - 4.1.2 GNSS 篇(二) - 卫星导航定位原理

专栏文章目录传送门&#xff1a;返回专栏目录 Hi, 我是你们的老朋友&#xff0c;主要专注于嵌入式软件开发&#xff0c;有兴趣不要忘记点击关注【码思途远】 文章目录 关注星号公众号&#xff0c;不容错过精彩 作者&#xff1a;HywelStar Hi, 我是你们的老朋友HywelStar, 根…...

怎样在 SQL 中对一个包含销售数据的表按照销售额进行降序排序?

在当今数字化商业的浪潮中&#xff0c;数据就是企业的宝贵资产。对于销售数据的有效管理和分析&#xff0c;能够为企业的决策提供关键的支持。而在 SQL 中&#xff0c;对销售数据按照销售额进行降序排序&#xff0c;是一项基础但极其重要的操作。 想象一下&#xff0c;您面前有…...

DIAdem 与 LabVIEW

DIAdem 和 LabVIEW 都是 NI (National Instruments) 公司开发的产品&#xff0c;尽管它们有不同的核心功能和用途&#xff0c;但它们在工程、测试和测量领域中常常一起使用&#xff0c;以形成一个完整的数据采集、分析、处理和报告生成的解决方案。 1. 功能和用途 LabVIEW (Lab…...

UE虚幻引擎可以云渲染吗?应用趋势与挑战了解

虚幻云渲染技术是基于虚幻引擎的云端渲染技术&#xff0c;将虚幻引擎的渲染计算任务通过云计算的方式进行处理和渲染、并将渲染结果传输到终端设备上进行展示。虚幻引擎云渲染技术在近年来得到了迅猛的发展&#xff0c;并在各个领域得到了广泛的应用&#xff0c;包括游戏、电影…...

实战分享:DefenderUI在企业环境中的部署与应用

前言 想象一下&#xff0c;你的电脑就像一座坚固的城堡&#xff0c;但城门却时常被一些不速之客窥探甚至企图入侵&#xff1b;Defender&#xff0c;作为城堡自带的守护者&#xff0c;实力自然不容小觑&#xff1b;但你是否觉得它有时候太过低调&#xff0c;有些隐藏技能还没完…...

中英双语介绍金融经济中的鹰派 (Hawkish)和鸽派 (Dovish)

中文版 在金融和经济政策中&#xff0c;“鹰派”和“鸽派”是两种对货币政策和经济管理有不同立场的群体。 鹰派 (Hawkish) 鹰派倾向于担心通货膨胀的风险&#xff0c;通常支持较高的利率和更紧的货币政策&#xff0c;以防止经济过热和控制物价上涨。具体特征包括&#xff1…...

Android 开发中常用的布局类型及其选择指南

在 Android 开发过程中,选择正确的布局类型对于构建高效、美观且响应式的用户界面至关重要。本文将介绍 Android 中几种最常用的布局类型,并对比它们的特点和适用场景,帮助开发者们做出明智的选择。 1. LinearLayout - 线性布局 特点: LinearLayout 是最基本的布局类型之一…...

短视频SDK解决方案,降低行业开发门槛

美摄科技匠心打造了一款集前沿技术与极致体验于一体的短视频SDK解决方案&#xff0c;它不仅重新定义了短视频创作的边界&#xff0c;更以行业标杆级的短视频特效&#xff0c;让每一帧画面都闪耀不凡光芒。 【技术赋能&#xff0c;创意无限】 美摄科技的短视频SDK&#xff0c;…...

【C++】String常见函数用法

一、string类对象的常见构造 我们可采取以下的方式进行构造&#xff0c;以下是常用的接口&#xff1a; //生成空字符串 string; //拷贝构造函数 string(const string& str); //用C-string来构造string类对象 string(const char* s); //string类对象中包含n个字符c strin…...

LeetCode49.字母异位词分组

题目大意 给你一个字符串数组&#xff0c;请你将字母异位词组合在一起。可以按任意顺序返回结果列表。 字母异位词是由重新排列源单词的所有字母得到的一个新单词。 思路分析 示例 1: 输入: strs ["eat", "tea", "tan", "ate", &…...

Nginx日志按天分割

需求、日志按照天的单位进行分割存储。 如果你直接百度&#xff0c;可能会搜到很多教你用各种脚本或是三方插件来按天分割的&#xff0c;这边我用nginx服务本身来分割日志。 方法一 通过使用 $time_iso8601 变量和 map 指令&#xff0c;实现了日志文件按天分割的功能。以下是…...

文本摘要简介

文本摘要是从一段长文本中提取出最重要的信息&#xff0c;并生成一个简短而有意义的摘要。这个过程可以分为两种主要方法&#xff1a; 抽取式摘要&#xff08;Extractive Summarization&#xff09;&#xff1a;从原文中直接提取出关键句子或段落&#xff0c;组成摘要…...

3.MySQL面试题之Redis 和 Mysql 如何保证数据一致性?

Redis 和 MySQL 数据一致性是分布式系统中的一个常见挑战。保证数据一致性通常涉及几种策略&#xff0c;我会详细解释这些策略并提供相应的代码示例。 先更新数据库&#xff0c;再更新缓存 这种方法先更新 MySQL&#xff0c;然后更新或删除 Redis 缓存。 Transactional publ…...

浅谈TCP协议、UDP协议

一、介绍说明 TCP&#xff08;传输控制协议&#xff09; 面向连接&#xff1a;TCP在数据传输之前必须建立连接。这通过一个称为三次握手的过程来完成&#xff0c;确保连接的两端都准备好进行数据传输。 可靠性&#xff1a;TCP提供可靠的数据传输&#xff0c;确保数据包正确无…...

SQL业务题: 从不订购的客户

1️⃣题目 Customers 表&#xff1a; ---------------------- | Column Name | Type | ---------------------- | id | int | | name | varchar | ---------------------- 在 SQL 中&#xff0c;id 是该表的主键。 该表的每一行都表示客户的 ID 和名…...

怎么直接在PDF上修改内容?随心编辑PDF内容

PDF(Portable Document Format)作为一种专用于阅读而非编辑的文档格式&#xff0c;其设计的核心目的是保持文档格式的一致性&#xff0c;确保文档在不同平台和设备上都能以相同的布局和格式呈现。然而&#xff0c;在实际工作和生活中&#xff0c;我们经常需要对PDF文档进行编辑…...

聊天室项目测试报告

项目介绍 本项目是一个基于Spring Boot框架开发的聊天室应用。一个实时的文本消息交流平台&#xff0c;允许多个用户同时在线聊天。系统采用了Spring Boot作为后端框架&#xff0c;集成了WebSocket技术以实现消息的实时推送与接收提供一个简单、易用且功能完备的在线聊天环境。…...

语音识别(实时语音转录)——funasr的详细部署和使用教程(包括实时语音转录)

阿里达摩院开源大型端到端语音识别工具包FunASR&#xff1a; FunASR提供了在大规模工业语料库上训练的模型&#xff0c;并能够将其部署到应用程序中。工具包的核心模型是Paraformer&#xff0c;这是一个非自回归的端到端语音识别模型&#xff0c;经过手动注释的普通话语音识别…...

【网络编程】TCP机械臂测试

通过w(红色臂角度增大)s&#xff08;红色臂角度减小&#xff09;d&#xff08;蓝色臂角度增大&#xff09;a&#xff08;蓝色臂角度减小&#xff09;按键控制机械臂 注意&#xff1a;关闭计算机的杀毒软件&#xff0c;电脑管家&#xff0c;防火墙 1&#xff09;基于TCP服务器…...

笔记:在WPF中如何注册控件级全局事件和应用程序级全局事件

一、目的&#xff1a;在WPF中如何注册控件级全局事件和应用程序级全局事件 二、实现 应用程序级全局事件 //注册应用程序级全局事件 EventManager.RegisterClassHandler(typeof(Button), Button.ClickEvent, new RoutedEventHandler(ic_event_Click)); 如上代码既会注册全局…...

【Linux系列】telnet使用入门

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…...

音视频相关知识

H.264编码格式 音频 PCM就是要把声音从模拟信号转换成数字信号的一种技术&#xff0c;他的原理简单地说就是利用一个固定的频率对模拟信号进行采样。 pcm是无损音频音频文件格式...

数据结构--第七天

递归 -递归的概念 递归其实就是一种解决问题的办法&#xff0c;在C语言中&#xff1a;递归就是函数自己调用自己 -递归的思想 递归的思考方式就是把大事化小的过程 递归的递就是递推的意思&#xff0c;归就是回归的意思 &#xff08;递归是少量的代码完成大量的运算&#xff09…...

代码随想录Day34:62.不同路径、63.不同路径II、343.整数拆分、96.不同的二叉搜索树

62. 不同路径 一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角&#xff08;在下图中标记为 “Finish” &#xff09;。 问总共有多少条不同的路径&…...

【信息学奥赛一本通】1008:计算(a+b)/c的值

1008&#xff1a;计算(ab)/c的值 时间限制: 1000 ms 内存限制: 66536 KB 提交数:164836 通过数: 142434 【题目描述】 给定3个整数a、b、c&#xff0c;计算表达式abc的值。 【输入】 输入仅一行&#xff0c;包括三个整数a、b、c, 数与数之间以一个空格分开。(&#xff0d;10,…...

使用 jstat 进行 Java 应用程序性能监控

jstat 使用经验笔记 1. 简介 jstat 是 Java 开发工具包 (JDK) 中的一个命令行工具&#xff0c;用于监控 Java 虚拟机 (JVM) 的运行时状态&#xff0c;特别是垃圾回收 (Garbage Collection, GC) 的行为。通过使用 jstat&#xff0c;你可以监控和诊断 Java 应用程序的内存使用情…...