基于Apache Hudi + MinIO 构建流式数据湖

2023-11-10

Apache Hudi 是一个流式数据湖平台,将核心仓库和数据库功能直接引入数据湖。Hudi 不满足于将自己称为 Delta 或 Apache Iceberg 之类的开放文件格式,它提供表、事务、更新/删除、高级索引、流式摄取服务、数据聚簇/压缩优化和并发性。Hudi 于 2016 年推出,牢牢扎根于 Hadoop 生态系统,解释了名称背后的含义:Hadoop Upserts Deletes and Incrementals。它是为管理 HDFS 上大型分析数据集的存储而开发的。Hudi 的主要目的是减少流数据摄取过程中的延迟。

随着时间的推移,Hudi 已经发展到使用云存储[1]和对象存储,包括 MinIO。Hudi 从 HDFS 的转变与世界的大趋势齐头并进,将传统的 HDFS 抛在脑后,以实现高性能、可扩展和云原生对象存储。Hudi 承诺提供优化,使 Apache Spark、Flink、Presto、Trino 和其他的分析工作负载更快,这与 MinIO 对大规模云原生应用程序性能的承诺非常吻合。在生产中使用 Hudi 的公司包括 Uber[2]、亚马逊[3]、字节跳动[4]和 Robinhood[5]。这些是世界上一些最大的流式数据湖[6]。Hudi 在这个用例中的关键在于它提供了一个增量数据处理栈,可以对列数据进行低延迟处理。通常系统使用 Apache Parquet 或 ORC 等开放文件格式将数据写入一次,并将其存储在高度可扩展的对象存储或分布式文件系统之上。Hudi 作为数据平面来摄取、转换和管理这些数据。Hudi 使用 Hadoop FileSystem API[7] 与存储交互,该 API 与从 HDFS 到对象存储到内存文件系统的各种实现兼容(但不一定最佳)。

Hudi 文件格式

Hudi 使用基本文件和增量日志文件来存储对给定基本文件的更新/更改。基本文件可以是 Parquet(列)或 HFile(索引),增量日志保存为 Avro(行),因为在发生更改时记录对基本文件的更改是有意义的。Hudi 将给定基本文件的所有更改编码为一系列块。块可以是数据块、删除块或回滚块。这些块被合并以便派生更新的基础文件。这种编码还创建了一个独立的日志。

表格式由表的文件布局、表的模式(Schema)和跟踪表更改的元数据组成。Hudi 强制执行模式写入,与强调流处理一致,以确保管道不会因非向后兼容的更改而中断。Hudi 将给定表/分区的文件分组在一起,并在记录键和文件组之间进行映射。如上所述,所有更新都记录到特定文件组的增量日志文件中。这种设计比 Hive ACID 更高效,后者必须将所有数据记录与所有基本文件合并以处理查询。Hudi 的设计预计基于键的快速更新插入和删除,因为它使用文件组的增量日志,而不是整个数据集。

时间线对于理解Hudi至关重要,因为它是所有 Hudi 表元数据的真实事件日志的来源。时间线存储在 .hoodie 文件夹中,在我们的例子中是存储桶。事件将保留在时间线上直到它们被删除。整个表和文件组都存在时间线,通过将增量日志应用于原始基本文件,可以重建文件组。为了优化频繁的写入/提交,Hudi 的设计使元数据相对于整个表的大小保持较小。时间线上的新事件被保存到内部元数据表中,并作为一系列读取时合并的表实现,从而提供低写入放大。因此,Hudi 可以快速吸收元数据的快速变化。此外元数据表使用 HFile 基本文件格式,通过一组索引键查找进一步优化性能,避免读取整个元数据表。作为表一部分的所有物理文件路径都包含在元数据中,以避免昂贵且耗时的云文件列表。

Hudi写入

Hudi 写入架构具有 ACID 事务支持的高性能写入层,可实现非常快速的增量更改,例如更新和删除。典型的 Hudi 架构依赖 Spark 或 Flink 管道将数据传递到 Hudi 表。Hudi 写入路径经过优化,比简单地将 Parquet 或 Avro 文件写入磁盘更有效。Hudi 分析写入操作并将它们分类为增量操作(insert, upsert, delete)或批量操作(insert_overwrite, insert_overwrite_table, delete_partition, bulk_insert),然后应用必要的优化[8]。Hudi 写入器还负责维护元数据。对于每条记录,都会写入该记录唯一的提交时间和序列号(这类似于 Kafka 偏移量),从而可以派生记录级别的更改。用户还可以在传入数据流中指定事件时间字段,并使用元数据和 Hudi 时间线跟踪它们。这可以显着改进流处理,因为 Hudi 包含每个记录的到达时间和事件时间,从而可以为复杂的流处理管道构建强大的水印[9]。

Hudi读取

写入器和读取器之间的快照隔离允许从所有主要数据湖查询引擎(包括 Spark、Hive、Flink、Prest、Trino 和 Impala)中一致地查询表快照。与 Parquet 和 Avro 一样,Hudi 表可以被 Snowflake[10] 和 SQL Server[11] 等作为外部表读取。Hudi 读取器非常轻量,尽可能使用特定于引擎的向量化读取器和缓存,例如 Presto 和 Spark。当 Hudi 必须为查询合并基本文件和日志文件时,Hudi 使用可溢出映射和延迟读取等机制提高合并性能,同时还提供读取优化查询。Hudi 包含许多非常强大的增量查询功能,元数据是其中的核心,允许将大型提交作为较小的块使用,并完全解耦数据的写入和增量查询。通过有效使用元数据,时间旅行非常容易实现,其只是另一个具有定义起点和终点的增量查询。Hudi 在任何给定时间点以原子方式将键映射到单个文件组,支持 Hudi 表上的完整 CDC 功能。正如上面 Hudi 写入器部分所讨论的,每个表都由文件组组成,每个文件组都有自己的自包含元数据。

Hudi核心特性

Hudi 最大的优势在于它摄取流式和批处理数据的速度。通过提供 upsert 功能,Hudi 执行任务的速度比重写整个表或分区快几个数量级。为了利用 Hudi 的摄取速度,数据湖库需要一个具有高 IOPS 和吞吐量的存储层。MinIO 的可扩展性和高性能的结合正是 Hudi 所需要的。MinIO 能够满足为实时企业数据湖提供动力所需的性能——最近的一项基准测试[12]在 GET 上实现了 325 GiB/s (349 GB/s),在 PUT 上实现了 165 GiB/s (177 GB/s) 32 个现成的 NVMe SSD 节点。活跃的企业 Hudi 数据湖存储大量小型 Parquet 和 Avro 文件。MinIO 包括许多小文件优化[13],可实现更快的数据湖。小对象与元数据一起保存,减少了读取和写入小文件(如 Hudi 元数据和索引)所需的 IOPS。模式(Schema) 是每个 Hudi 表的关键组件。Hudi 可以强制执行模式,也可以允许模式演变,以便流数据管道可以适应而不会中断。此外Hudi 强制执行 Schema-on-Writer 以确保更改不会破坏管道。Hudi 依靠 Avro 来存储、管理和发展表的模式。Hudi 为数据湖提供 ACID 事务保证。Hudi 确保原子写入:以原子方式向时间线提交提交,并给出一个时间戳,该时间戳表示该操作被视为发生的时间。Hudi 隔离了写入器、表 和 读取器进程之间的快照,因此每个进程都对表的一致快照进行操作。Hudi 通过写入器之间的乐观并发控制 (OCC) 以及表服务和写入器之间以及多个表服务之间的基于 MVCC 的非阻塞并发控制来完善这一点。

Hudi 整合 MinIO 教程

本教程将引导您设置 Spark、Hudi 和 MinIO,并介绍一些基本的 Hudi 功能。本教程基于 Apache Hudi Spark 指南[14],适用于云原生 MinIO 对象存储。请注意,使用版本化存储桶会增加 Hudi 的一些维护开销。任何被删除的对象都会创建一个删除标记[15]。随着 Hudi 使用 Cleaner 实用程序[16]清理文件,删除标记的数量会随着时间的推移而增加。正确配置生命周期管理[17]以清理这些删除标记很重要,因为如果删除标记的数量达到 1000 个,List 操作可能会阻塞。Hudi 项目维护人员建议使用生命周期规则在一天后清理删除标记。

前提条件

  • • 下载并安装[18] Apache Spark。
  • • 下载并安装[19] MinIO。记录控制台的 IP 地址、TCP 端口、访问密钥和密钥。
  • • 下载并安装[20] MinIO 客户端。
  • • 下载 AWS 和 AWS Hadoop 库并将它们添加到您的类路径中,以便使用 S3A 处理对象存储。
    • • AWS:aws-java-sdk:1.10.34(或更高版本)
    • • Hadoop:hadoop-aws:2.7.3(或更高版本)
  • • 下载 Jar 文件[21],解压缩并将它们复制到 /opt/spark/jars

创建一个 MinIO 存储桶

使用 MinIO Client 创建一个存储 Hudi 数据的存储桶:

mc alias set myminio http://<your-MinIO-IP:port> <your-MinIO-access-key>  <your-MinIO-secret-key>
mc mb myminio/hudi

复制

使用 Hudi 启动 Spark

使用配置为使用 MinIO 进行存储的 Hudi 启动 Spark shell。确保使用您的 MinIO 设置为 S3A 配置条目。

spark-shell \
--packages org.apache.hudi:hudi-spark3.3-bundle_2.12:0.12.0,org.apache.hadoop:hadoop-aws:3.3.4 \
--conf 'spark.serializer=org.apache.spark.serializer.KryoSerializer' \
--conf 'spark.sql.catalog.spark_catalog=org.apache.spark.sql.hudi.catalog.HoodieCatalog' \
--conf 'spark.sql.extensions=org.apache.spark.sql.hudi.HoodieSparkSessionExtension' \
--conf 'spark.hadoop.fs.s3a.access.key=<your-MinIO-access-key>' \
--conf 'spark.hadoop.fs.s3a.secret.key=<your-MinIO-secret-key>'\
--conf 'spark.hadoop.fs.s3a.endpoint=<your-MinIO-IP>:9000' \
--conf 'spark.hadoop.fs.s3a.path.style.access=true' \
--conf 'fs.s3a.signing-algorithm=S3SignerType'

复制

然后在 Spark 中初始化 Hudi。

import org.apache.hudi.QuickstartUtils._
import scala.collection.JavaConversions._
import org.apache.spark.sql.SaveMode._
import org.apache.hudi.DataSourceReadOptions._
import org.apache.hudi.DataSourceWriteOptions._
import org.apache.hudi.config.HoodieWriteConfig._
import org.apache.hudi.common.model.HoodieRecord

复制

请注意,可以使用外部配置文件[22] 简化配置。

创建表

尝试使用 Scala 创建一个简单的小型 Hudi 表。Hudi DataGenerator 是一种基于示例行程模式生成示例插入和更新的快速简便的方法。

val tableName = "hudi_trips_cow"
val basePath = "s3a://hudi/hudi_trips_cow"
val dataGen = new DataGenerator

复制

向Hudi插入数据,向MinIO写表

下面将生成新的行程数据,将它们加载到 DataFrame 中,并将我们刚刚创建的 DataFrame 作为 Hudi 表写入 MinIO。如果表已经存在,模式(覆盖)将覆盖并重新创建表。行程数据依赖于记录键(uuid)、分区字段(地区/国家/城市)和逻辑(ts)来确保行程记录对于每个分区都是唯一的。我们将使用默认的写入操作 upsert。当没有更新的工作负载时可以使用 insert 或 bulk_insert ,这会更快。

val inserts = convertToStringList(dataGen.generateInserts(10))
val df = spark.read.json(spark.sparkContext.parallelize(inserts, 2))
df.write.format("hudi").
options(getQuickstartWriteConfigs).
option(PRECOMBINE_FIELD_OPT_KEY, "ts").
option(RECORDKEY_FIELD_OPT_KEY, "uuid").
option(PARTITIONPATH_FIELD_OPT_KEY, "partitionpath").
option(TABLE_NAME, tableName).
mode(Overwrite).
save(basePath)

复制

打开浏览器并使用您的访问密钥和密钥在http://<your-MinIO-IP>:<port>登录 MinIO。您将在存储桶中看到 Hudi 表。

该存储桶还包含一个包含元数据的 .hoodie路径,以及包含americasasia数据的路径。

看看元数据,这是完成整个教程后 .hoodie路径的截图。我们可以看到我在 2022 年 9 月 13 日星期二 9:02、10:37、10:48、10:52 和 10:56 修改了表。

查询数据

让我们将 Hudi 数据加载到 DataFrame 中并运行示例查询。

// spark-shell
val tripsSnapshotDF = spark.
read.
format("hudi").
load(basePath)
tripsSnapshotDF.createOrReplaceTempView("hudi_trips_snapshot")
spark.sql("select fare, begin_lon, begin_lat, ts from  hudi_trips_snapshot where fare > 20.0").show()
spark.sql("select _hoodie_commit_time, _hoodie_record_key, _hoodie_partition_path, rider, driver, fare from  hudi_trips_snapshot").show()

复制

Hudi时间旅行

我们可以去看 1988 年的 Hootie and the Blowfish 音乐会。每次写入 Hudi 表都会创建新的快照。将快照视为可用于时间旅行查询的表版本。尝试一些时间旅行查询(您必须更改时间戳以与您相关)。

spark.read.
format("hudi").
option("as.of.instant", "2022-09-13 09:02:08.200").
load(basePath)

复制

更新数据

这个过程类似于我们之前插入新数据的过程。为了展示 Hudi 更新数据的能力,我们将对现有行程记录生成更新,将它们加载到 DataFrame 中,然后将 DataFrame 写入已经保存在 MinIO 中的 Hudi 表中。请注意我们使用的是追加保存模式。一般准则是使用追加模式,除非您正在创建新表,因此不会覆盖任何记录。使用 Hudi 的一种典型方式是实时摄取流数据,将它们附加到表中,然后根据刚刚附加的内容编写一些合并和更新现有记录的逻辑。或者如果表已存在,则使用覆盖模式写入会删除并重新创建表。

// spark-shell
val updates = convertToStringList(dataGen.generateUpdates(10))
val df = spark.read.json(spark.sparkContext.parallelize(updates, 2))
df.write.format("hudi").
options(getQuickstartWriteConfigs).
option(PRECOMBINE_FIELD_OPT_KEY, "ts").
option(RECORDKEY_FIELD_OPT_KEY, "uuid").
option(PARTITIONPATH_FIELD_OPT_KEY, "partitionpath").
option(TABLE_NAME, tableName).
mode(Append).
save(basePath)

复制

查询数据将显示更新的行程记录。

增量查询

Hudi 可以使用增量查询提供自给定时间戳以来更改的记录流。我们需要做的就是提供一个开始时间,从该时间开始更改将被流式传输以查看通过当前提交的更改,并且我们可以使用结束时间来限制流。增量查询对于 Hudi 来说非常重要,因为它允许您在批处理数据上构建流式管道。

// spark-shell
// reload data
spark.
read.
format("hudi").
load(basePath).
createOrReplaceTempView("hudi_trips_snapshot")
val commits = spark.sql("select distinct(_hoodie_commit_time) as commitTime from  hudi_trips_snapshot order by commitTime").map(k => k.getString(0)).take(50)
val beginTime = commits(commits.length - 2) // commit time we are interested in
// incrementally query data
val tripsIncrementalDF = spark.read.format("hudi").
option(QUERY_TYPE_OPT_KEY, QUERY_TYPE_INCREMENTAL_OPT_VAL).
option(BEGIN_INSTANTTIME_OPT_KEY, beginTime).
load(basePath)
tripsIncrementalDF.createOrReplaceTempView("hudi_trips_incremental")
spark.sql("select `_hoodie_commit_time`, fare, begin_lon, begin_lat, ts from  hudi_trips_incremental where fare > 20.0").show()

复制

时间点查询

Hudi 可以查询到特定时间和日期的数据。

// spark-shell
val beginTime = "000" // Represents all commits > this time.
val endTime = commits(commits.length - 2) // commit time we are interested in
//incrementally query data
val tripsPointInTimeDF = spark.read.format("hudi").
option(QUERY_TYPE_OPT_KEY, QUERY_TYPE_INCREMENTAL_OPT_VAL).
option(BEGIN_INSTANTTIME_OPT_KEY, beginTime).
option(END_INSTANTTIME_OPT_KEY, endTime).
load(basePath)
tripsPointInTimeDF.createOrReplaceTempView("hudi_trips_point_in_time")
spark.sql("select `_hoodie_commit_time`, fare, begin_lon, begin_lat, ts from hudi_trips_point_in_time where fare > 20.0").show()

复制

使用软删除删除数据

Hudi 支持两种不同的删除记录方式。软删除保留记录键并将所有其他字段的值清空。软删除保留在 MinIO 中,并且仅使用硬删除从数据湖中删除。

// spark-shell
spark.
read.
format("hudi").
load(basePath).
createOrReplaceTempView("hudi_trips_snapshot")
// fetch total records count
spark.sql("select uuid, partitionpath from hudi_trips_snapshot").count()
spark.sql("select uuid, partitionpath from hudi_trips_snapshot where rider is not null").count()
// fetch two records for soft deletes
val softDeleteDs = spark.sql("select * from hudi_trips_snapshot").limit(2)
// prepare the soft deletes by ensuring the appropriate fields are nullified
val nullifyColumns = softDeleteDs.schema.fields.
map(field => (field.name, field.dataType.typeName)).
filter(pair => (!HoodieRecord.HOODIE_META_COLUMNS.contains(pair._1)
                && !Array("ts", "uuid", "partitionpath").contains(pair._1)))
val softDeleteDf = nullifyColumns.
foldLeft(softDeleteDs.drop(HoodieRecord.HOODIE_META_COLUMNS: _*))(
  (ds, col) => ds.withColumn(col._1, lit(null).cast(col._2)))
// simply upsert the table after setting these fields to null
softDeleteDf.write.format("hudi").
options(getQuickstartWriteConfigs).
option(OPERATION_OPT_KEY, "upsert").
option(PRECOMBINE_FIELD_OPT_KEY, "ts").
option(RECORDKEY_FIELD_OPT_KEY, "uuid").
option(PARTITIONPATH_FIELD_OPT_KEY, "partitionpath").
option(TABLE_NAME, tableName).
mode(Append).
save(basePath)
// reload data
spark.
read.
format("hudi").
load(basePath).
createOrReplaceTempView("hudi_trips_snapshot")
// This should return the same total count as before
spark.sql("select uuid, partitionpath from hudi_trips_snapshot").count()
// This should return (total - 2) count as two records are updated with nulls
spark.sql("select uuid, partitionpath from hudi_trips_snapshot where rider is not null").count()

复制

使用硬删除删除数据

相比之下,硬删除就是我们认为的删除。记录键和相关字段将从表中删除。

// spark-shell
// fetch total records count
spark.sql("select uuid, partitionpath from hudi_trips_snapshot").count()
// fetch two records to be deleted
val ds = spark.sql("select uuid, partitionpath from hudi_trips_snapshot").limit(2)
// issue deletes
val deletes = dataGen.generateDeletes(ds.collectAsList())
val hardDeleteDf = spark.read.json(spark.sparkContext.parallelize(deletes, 2))
hardDeleteDf.write.format("hudi").
options(getQuickstartWriteConfigs).
option(OPERATION_OPT_KEY,"delete").
option(PRECOMBINE_FIELD_OPT_KEY, "ts").
option(RECORDKEY_FIELD_OPT_KEY, "uuid").
option(PARTITIONPATH_FIELD_OPT_KEY, "partitionpath").
option(TABLE_NAME, tableName).
mode(Append).
save(basePath)
// run the same read query as above.
val roAfterDeleteViewDF = spark.
read.
format("hudi").
load(basePath)
roAfterDeleteViewDF.registerTempTable("hudi_trips_snapshot")
// fetch should return (total - 2) records
spark.sql("select uuid, partitionpath from hudi_trips_snapshot").count()

复制

写覆盖

当数据湖获得更新现有数据的能力时,它就变成了Lakehouse。我们将生成一些新的行程数据,然后覆盖我们现有的数据。此操作比 Hudi 一次为您计算整个目标分区的 upsert 更快。在这里我们指定配置以绕过 upsert 将为您执行的自动索引、预组合和重新分区。

// spark-shell
spark.
read.format("hudi").
load(basePath).
select("uuid","partitionpath").
sort("partitionpath","uuid").
show(100, false)
val inserts = convertToStringList(dataGen.generateInserts(10))
val df = spark.
read.json(spark.sparkContext.parallelize(inserts, 2)).
filter("partitionpath = 'americas/united_states/san_francisco'")
df.write.format("hudi").
options(getQuickstartWriteConfigs).
option(OPERATION.key(),"insert_overwrite").
option(PRECOMBINE_FIELD.key(), "ts").
option(RECORDKEY_FIELD.key(), "uuid").
option(PARTITIONPATH_FIELD.key(), "partitionpath").
option(TBL_NAME.key(), tableName).
mode(Append).
save(basePath)
// Should have different keys now for San Francisco alone, from query before.
spark.
read.format("hudi").
load(basePath).
select("uuid","partitionpath").
sort("partitionpath","uuid").
show(100, false)

复制

Schema演进和分区

模式演进允许您更改 Hudi 表的模式以适应数据随时间发生的变化。下面是一些关于如何查询和发展模式和分区的示例。如需更深入的讨论,请参阅 Schema Evolution | Apache Hudi[23]。请注意如果您运行这些命令,它们将改变 Hudi 表模式,使其与本教程不同。

-- Alter table name
ALTER TABLE oldTableName RENAME TO newTableName
-- Alter table add columns
ALTER TABLE tableIdentifier ADD COLUMNS(colAndType (,colAndType)*)
-- Alter table column type
ALTER TABLE tableIdentifier CHANGE COLUMN colName colName colType
-- Alter table properties
ALTER TABLE tableIdentifier SET TBLPROPERTIES (key = 'value')
#Alter table examples
--rename to:
ALTER TABLE hudi_cow_nonpcf_tbl RENAME TO hudi_cow_nonpcf_tbl2;
--add column:
ALTER TABLE hudi_cow_nonpcf_tbl2 add columns(remark string);
--change column:
ALTER TABLE hudi_cow_nonpcf_tbl2 change column uuid uuid bigint;
--set properties;
alter table hudi_cow_nonpcf_tbl2 set tblproperties (hoodie.keep.max.commits = '10');

复制

目前,show partitions仅适用于文件系统,因为它基于文件系统表路径。本教程使用 Spark 来展示 Hudi 的功能。但是Hudi 可以支持多种表类型/查询类型,并且可以从 Hive、Spark、Presto 等查询引擎查询 Hudi 表。Hudi 项目有一个演示视频[24],它在基于 Docker 的设置上展示了所有这些,所有相关系统都在本地运行。

总结

Apache Hudi 是第一个用于数据湖的开放表格式,在流式架构中值得考虑。Hudi 社区和生态系统生机勃勃,越来越重视用 Hudi/对象存储替换 Hadoop/HDFS,以实现云原生流式数据湖。将 MinIO 用于 Hudi 存储为多云数据湖和分析铺平了道路。MinIO 包括主动-主动复制以在本地、公共/私有云和边缘位置之间同步数据,从而实现企业所需的出色功能,例如地理负载平衡和快速热热故障转移。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

基于Apache Hudi + MinIO 构建流式数据湖 的相关文章

随机推荐

  • 为什么要使用ConstraintLayout?

    本文为博主原创文章 转载请注明出处 http blog csdn net jingsummer article details 78615360 源码地址 ConstraintLayoutDemo 相信大家对ConstraintLayout
  • Java 设计模式(十二):享元模式

    享元模式 GitHub 地址 https github com yifanzheng java design patterns 享元模式 Flyweight Design Pattern 顾名思义就是共享单元 享元模式的意图是复用对象 节省
  • STM32的独立看门狗

    独立看门狗时钟频率一般以40KHz 但不是非常准确 变化范围在15 47KHz 看门狗在开启后不能关闭 除非复位 1 寄存器 关键字寄存器 IWDG KR 用来写指令 指令有0xAAAA 把IWDG RLR的值载入到计数器 避免 产生复位
  • [BAPI]外向交货单按批次拆分[BAPI_OUTB_DELIVERY_CHANGE]

    下面介绍SAP SD的销售订单外向交货单按批次拆分 Batch Managed 包括前台操作和调用bapi BAPI OUTB DELIVERY CHANGE 实现 1 前台操作 按批次拆分操作 外向交货单Tcode VL02n amp l
  • Java final 详解

    一 final 基础使用 1 1 修饰类 当某个类的整体定义为 final 时 就表明了你不能打算继承该类 而且也不允许别人这么做 即这个类是不能有子类的 注意 final 类中的所有方法都隐式为 final 因为无法覆盖他们 所以在 fi
  • 泛型中K TVE? Object等分别代表什么含义。

    E一Element 在集合中使用 因为集合中存放的是元素 T Type Java类 K Key 键 V Value 值 N Number 数值类型 表示不确定的java类型 无限制通配符类型 S U V 2nd 3rd 4th types
  • ArcGIS API 4.x for Js 点击地图获取经纬度

    需求 鼠标点击地图后获取当前的经纬度 效果 需添加API esri geometry support webMercatorUtils 代码 view on click function e geom webMercatorUtils xy
  • 规范的建表语句

    CREATE TABLE student info id INT NOT NULL AUTO INCREMENT COMMENT 主键 stu name VARCHAR 10 NOT NULL DEFAULT COMMENT 姓名 stu
  • Python基础语法入门(第十五天)——装饰器传参与匿名函数

    在上篇文章中留下了一个问题 装饰器的传参如何实现 其实对于这个问题来说 首先要搞清楚传参的顺序是什么 我们已知的是装饰器的本质就是函数 那么在这嵌套了多层的函数中每一个函数接收的参数是哪一个 作用域哪个范围 这就是实现装饰器传参前需要解决的
  • IDEA+Maven创建javaweb项目out.print()报错

    IDEA Maven创建javaweb项目out print 报错 从原型中创建如图所示 注意不要选错 选好项目目录设置好项目名称之后下一步 注意这个地方的Maven路径 用户设置文件 本地仓库要选择自己的 不要使用IDEA自带的MAven
  • Sqlite3简介

    SQLite3 简介 SQLite3 是一种轻量级的嵌入式数据库引擎 被广泛应用于各种应用程序中 包括移动设备 桌面应用程序和嵌入式系统 它以其简单 高效和零配置的特点而受到开发者的喜爱 以下是 SQLite3 的一些重要特点 嵌入式数据库
  • Android手机9008模式刷机教程(以小米手机为例)

    机型 红米1s电信版2013028 故障 一开始可以进入fastboot 后来无法进入fastboot 无限重启 后来开机键无反应 大家都知道 刷机的方式有很多种 大体来讲 我们刷机一般采用以下几种方式 1 卡刷 即进入recovery模式
  • 理解golang调度

    线程模型 在细说 Go 的调度模型之前 先来说说一般意义的线程模型 线程模型一般分三种 由用户级线程和 OS 线程的不同对应关系决定的 N 1 即全部用户线程都映射到一个OS线程上 上下文切换成本最低 但无法利用多核资源 1 1 一个用户线
  • 真香!Jenkins 主从模式解决问题So Easy~

    01 Jenkins 能干什么 Jenkins 是一个开源软件项目 是基于 Java 开发的一种持续集成工具 用于监控持续重复的工作 旨在提供一个开放易用的软件平台 使软件项目可以进行持续集成 中文官网 https jenkins io z
  • 二级教程python语言程序设计答案_全国计算机等级考试二级教程-Python语言程序设计(2018年版)编程题-参考答案......

    习题3 基本数据类型 1 获得用户输入的一个整数 输出该整数百位及以上的数字 i input 请输入一个整数 print i 2 复制代码 2 获得用户输入的一个字符串 将字符串按照空格分割 然后逐行打印出来 i input 请输入一个带空
  • unbuntn X64 安装vsftpd

    Fortunately the good folks at The Fronteer Group have backported vsftp until the full release of ver 3 of vsftp comes ou
  • 【限时免费】20天拿下华为OD笔试之【栈】2023B-仿 LISP 运算【欧弟算法】全网注释最详细分类最全的华为OD真题题解

    栈 2023B 仿 LISP 运算 题目描述与示例 题目描述 LISP 语言唯一的语法就是括号要配对 形如 OP P1 P2 括号内元素由单个空格分割 其中第一个元素 OP 为操作符 后续元素均为其参数 参数个数取决于操作符类型 注意 参数
  • JavaScript slice() 和 splice()

    slice slice 从已有的数组中返回选定的元素 返回一个新的数组 包含从 start 到 end 不包括该元素 的 arrayObject 中的元素 arrayObject slice start end 参数 start 必须 规定
  • 抽象类与接口(Java)

    二者的特性 方法类型 接口只能有抽象方法 抽象类可以有抽象方法和非抽象方法 从Java 8 开始 抽象类可以有默认 default 方法和静态方法 Final变量 接口中的变量默认是final的 抽象类可能包含非final的变量 变量类型
  • 基于Apache Hudi + MinIO 构建流式数据湖

    Apache Hudi 是一个流式数据湖平台 将核心仓库和数据库功能直接引入数据湖 Hudi 不满足于将自己称为 Delta 或 Apache Iceberg 之类的开放文件格式 它提供表 事务 更新 删除 高级索引 流式摄取服务 数据聚簇