首页 文章 精选 留言 我的

精选列表

搜索[全模态理解],共10000篇文章
优秀的个人博客,低调大师

Lakehouse系列 | StarRocks支持Apache Hudi原理解析

作者:王日宇,StarRocks Active Contributor,阿里云高级研发工程师 近年来,随着大数据分析技术的进步,大量业务场景对数据仓库的实时性提出了更高的要求,Lakehouse 架构逐渐被各大公司熟悉和接受,Apache Hudi(以下简称 Hudi)、Apache Iceberg(以下简称 Iceberg)、Delta Lake 都被看成是下一代数据湖的解决方案,并被称为数据湖技术三剑客。 StarRocks 作为当前最流行的 OLAP 分析引擎之一,阿里云EMR OLAP 团队与 StarRocks 社区在 2021 年就开始合作,共同设计并实现了 StarRocks Connector 框架,用于统一外部数据源的接入分析,并且基于 Connector 框架完成了对 Hudi、Iceberg、Delta Lake 等主流数据湖表格式的接入。 本文围绕以 Hudi 为中心的数据湖生态,详细介绍 StarRocks 是如何实现快速分析 Hudi 数据湖格式,并给出完整的使用案例。 #01 StarRocks 支持 Hudi 的现状 — Hudi 提供了两种类型的表: Copy On Write 表(简称 COW ) Merge On Read 表(简称 MOR ) StarRocks 早在 2.2 版本就正式发布了对 Hudi COW 表的 Snapshot 查询分析支持。对于 MOR 表的 Snapshot/ Read Optimized 查询支持功能,目前已在主分支可用,预计会在 2.5 版本正式对外发布。届时,StarRocks 将会完成对 Hudi 数据湖格式的全面支持。 最新发布的 StarRocks 2.4 版本使用全新 Connector 框架对 Apache Hive(以下简称 Hive)/Iceberg/Hudi 等表格式的读取特性进行了重构和改造,使用户无需再手动逐个创建外表,直接简单配置 Hudi Catalog ,便可查询到数据库下所有 Hudi 表格式数据。 #02 StarRocks 支持 Hudi 的技术解析 — 在讲解 StarRocks 支持 Hudi 技术解析前,我们先简单了解一些 Hudi 表的基本知识。 1、Hudi数据湖格式简介 Hudi 将数据仓库和数据库的核心功能直接引入数据湖,并提供了表、事务、高效的更新/删除、高级索引、流式摄取服务、小文件管理、压缩优化和读写并发隔离等技术,同时将数据保持为开源文件格式(Parquet,ORC,Avro)。 2、Hudi表类型 Copy On Write 表:简称 COW,这类 Hudi 表使用列存储格式(例如 Parquet)存储数据,如果有数据写入,则会对整个 Parquet 文件进行复制。COW 表具备列存储读取效率高的优点,适用于对读取效率要求高的场景。 Merge On Read 表:简称 MOR,这类 Hudi 表使用列存储格式(例如 Parquet)和行存储格式(Avro)共同存储数据。当数据更新时,将更新写入到行文件中,然后根据作业配置,以同步或异步方式将两种格式的数据文件进行合并并生成新的列存储文件(Compaction)。MOR 表具备行存储写入效率高的优点,适用于对写入效率要求高的场景。 Hudi 对于两种类型的表,提供了不同的查询类型: Snapshot Queries:快照查询,查询数据的最新快照,即表的最新数据 Incremental Queries:增量查询,可以查询指定时间范围内新增或修改的数据 Read Optimized Queries:读取优化查询,该查询为 MOR 表特有,仅查询 Parquet 文件中的数据,即牺牲一部分数据时效性来换取最佳的读取性能 3、Hudi数据文件的组织方式和概念 Hudi 将单个目录下的数据划分为多个 File group,每个 File group下包含多个 File Slice,File Slice 是 Hudi 文件组织系统里的最小管理单位,它由 0 个或 1 个 base 数据文件(列存储)和 0 个或多个增量 delta log 文件(行存储)组成。 对于 COW 表,则 File Slice 不包含 delta log 文件,只有 1 个 base 数据文件 4、StarRocks架构简介 StarRocks 整体架构分为两大部分:FE 和 BE,FE 使用 Java 语言,BE 使用 C++ 语言。 下面我们分别介绍 FE 和 BE 在外部表读取时对应的主要功能。 ​ FE 的主要作用有: 1. 缓存外部表元数据。其中,元数据信息分为两类: 数据表的基本元信息,如表结构 Schema、存储位置、分区信息、不同类别的统计信息等; 数据表下的数据文件信息,如某个分区下的文件列表、大小、压缩格式等。 2. 查询规划。包括解析 SQL 文本,生成逻辑执行计划和物理执行计划,对执行计划进行优化,如分区/列裁剪,Join Reorder 等。 3. 查询调度。使用上述的外部表元数据,根据数据的分布情况分配可用的 BE 节点资源等信息,规划每个 BE 需要执行的具体计算任务,并使用算法保证数据本地性的同时,尽可能让每个 BE 节点并行地读取大致相同的数据量以提高执行效率。 BE 主要作用有: 1. 执行 FE 分配的计算任务。如 Scan、Join、Shuffle、Aggregate 等。 2. 向 FE 汇报执行状态,传输执行结果。 5、Hudi的读取支持介绍 Hudi 的格式支持开发工作主要对 FE/BE 在外表上的对应功能来针对性实现和优化。 FE 端改造: 1. 在外部表的元信息方面,增加存储 Hudi 特有的元信息: 表类型,用来存储 Hudi 表类型。取值为 COW 或 MOR; Input Format,用来区分 MOR 的查询类型。Hudi 的 MOR 表在写入端(如 Spark)建表时,会额外生成 <table name>_rt 和 <table name>_ro 两张表,分别对应同一张 MOR 表的 Snapshot 查询表和 Read Optimized 查询表。Snapshot 查询表的 Input Format 信息取值为 org.apache.hudi.hadoop.realtime.HoodieParquetRealtimeInputFormat,Read Optimized 查询的 Input Format 信息取值为 org.apache.hudi.hadoop.HoodieParquetInputFormat; 数据文件元信息方面,增加 Optional 的 Delta Logs 文件列表字段,用来存储 Hudi MOR 表 Base 数据文件相对应的 Logs 更新文件信息。 2. 将 Hudi 元信息采集模块集成到全新 Catalog 框架内,只需要创建一次Hudi Catalog,后续便可以直接查询该 Catalog 下的所有 Hudi 表,不再需要以前的手工创建外部表的繁琐过程。 (https://docs.starrocks.io/zh-cn/latest/data_source/catalog/hudi_catalog) 全新 Catalog 框架结构: 3. CBO 优化器和执行计划优化: 通过 Hive Metastore 读取和缓存 Hudi 分区信息,应用到分区数据裁剪优化规则里; 通过 Hive Metastore 读取和缓存 Hudi 表的相关统计信息,应用到 Join Reorder 等 CBO 优化规则里。 4.表结构解析完成 Hudi 表字段类型映射: 通过读取 Hudi 表的 Avro Schema,在各个字段上完成 Hudi Type 到 StarRocks Type 映射,具体如下: 备注:即将发布的 StarRocks 2.5.0 版本会支持 Struct、Map 数据类型的读取,Hudi COW 表也会同步支持。 5. 平衡 IO 请求优化: StarRocks 查询传统的 Hive 表的时候,若单个数据文件的大小过大,会对这个文件进行 File Split 均匀拆分,形成多个 Scan Range。但在 Hudi 表的实现里,我们直接对 Base file 和对应 Delta Logs(即 File Slice)形成一个 Scan Range,除了有潜在的 Merge 逻辑数据问题之外,还有 IO 效率取舍,即使做了 File Split 拆分,比如拆成 3 份,其对应的 Delta Logs 就需要跟着拆分后的 File Slice 重复读取3次,这很可能会加剧存储系统的压力。在工程实现上,我们在 Scan Range 里添加了 useJNIReader 的标识字段,用来指示 BE 侧执行选择 Native Reader 还是 JNI reader,这两个 Reader 的具体用途见下文 BE 端改造部分。 6. 元数据请求优化: 通过批量以及异步的方式访问 Hive Metastore 获取 Hudi 表分区统计信息,在 FE 对其进行缓存并计算表级别统计信息用于查询优化,同时缓存 Scan 节点所要扫描的文件元数据信息来减少 Namenode 的压力并提升查询速度。用户可通过查询来触发元数据的异步加载,使得用户无需手动刷新元数据即可访问最新数据文件。 FE 和 BE 传输改造: 由于 FE 和 BE 使用 Thrift 协议传输信息,分别按照表元数据和 Scan Range 的改动,相对应添加了 Thrift 字段,确保 BE 端可以正确拿到执行计算所需要的信息。 BE 端改造: StarRocks BE 端本身自己实现了高效的 Native Reader,用来读取不同的数据格式,如 ORC/Parquet 等。Native Reader 做了很多 IO 优化的实现,如: 延迟物化:根据查询请求特征进行优化,先将参与谓词计算的列读出来,然后根据计算谓词的过滤结果,再将过滤结果下推读取其他列,并跳过被过滤的行,从而达到减少 IO 请求次数,提升查询性能。 Coalesce IO:读取数据文件时,合并小的 IO 请求(如小的列,小的 Row Group),减少 IO 次数,增加每次 IO 的请求量,提升查询性能。 - BE 端 Hudi Reader 设计方案评估过程 在实现 Hudi MOR 的支持过程中,在设计阶段我们考虑了两种可行方案: 1.使用 C++ 在 BE 全新实现一套 Hudi Reader,用来读取 Avro 格式的 Delta logs 文件和 Parquet 格式的 Base 文件,然后参考 Hudi 社区 Java 版本的 merge 逻辑完成合并,最终输出结果数据。这种方案可以带来 native 执行速度快的优势,但缺点也很明显,自行维护成本太高,且不方便及时更新社区的进展。 2. 在 BE 端使用 JNI 调用 Hudi 社区的 Java SDK,直接使用社区 SDK 读取 File Slice 的结果数据,然后传给 BE 端使用。与第一种方案相比,优缺点正好相反,但代码维护工作量会大大减少。同时,我们考虑到 Java 在大数据生态的优势地位,想统一抽象一个通用的 JNI Reader,让所有基于 Java 实现的数据源 SDK,都可以通过 JNI Reader 方便地接入到 StarRocks 里来。 因此,我们最终选择了第二套基于 JNI 方案,并把实现目标从原来的仅支持 Hudi MOR 数据格式扩展到实现通用的 JNI Connector。 - JNI Connector 设计介绍 在实现过程中,我们对 JNI Connector 做了大量的优化,同时借鉴了 Spark Photon 的内存设计和实现思路,让 C++ 和 Java 使用同一块 Native Memory 共享数据,Java 端使用 JVM 的 Native 操作类 Unsafe 直接将 Hudi Java SDK 的结果数据写入 Native Memory 里,C++ 便可以直接使用这块内存进行数据计算,从而减少数据多次内存拷贝的开销。实际上,类似这样的跨语言共享内存的编程模式在阿里云 EMR 技术产品里实践落地非常广泛,如基于开源 Apache Spark 做了深度优化的 EMR Spark,对 OSS 透明加速的 Jindo 等都有类似的设计和思想。接下来,我们将展开讲讲这块的具体设计思路。 数据列存储设计 在 BE 端,所有数据列是存储在 Column 的数据结构里,不同类型的数据列对应不同的 Column 数据结构。例如: 存储 INT 数据列使用的数据结构是 FixedLengthColumn<int32_t> 存储 VARCHAR类型数据列使用的数据结构是 BinaryColumnBase<uint32_t> 这些对应关系可以参考type_traints.h 源文件。 (https://github.com/StarRocks/starrocks/blob/main/be/src/column/type_traits.h) 实现 JNI Connector 工程上比较复杂的地方是,需要根据不同类型的数据类型安排对应的内存布局实现,且要尽量做到与 BE 端对应数据类型的内存布局相同,进而尽量减少 BE 端读取内存数据到 Column 时转化的开销。 对于 INT、LONG、DOUBLE 等定长类型的数据列,对应的 Column 数据结构是FixedLengthColumn<Cpp_type>,其底层存储使用了 vector<Cpp_type>,即:将数据列存储在一段连续的内存,对于这种类型,我们只需将 Java SDK 读出来的数据列按顺序依次放在 Native Memory 里即可。 对于 VARCHAR 这类变长类型的数据列,对应的 Column 数据结构是 BinaryColumnBase<Cpp_type>,以 BinaryColumn 为例,以其底层存储使用了 vector<uint8_t> 和 vector<uint_32>,vector<uint8_t> 即连续的字节流用来存储数据(Bytes),vector<uint_32> 即连续的 int 序列用来存储行索引(Offsets)。对于这种类型,我们需要将 Java SDK 读出来的数据转换成字节流放在 vector<uint8_t> 对应的 Native Memory 里,将行号放在 vector<uint_32> 对应的 Native Memory 里。 vector<uint8_t> 和 vector<uint_32> 读取变长数据的方法如下图所示: 例如,获取数据列的第x行,通过 Offsets 获取该行的起始索引 start=offsets(x) 和下一行的起始索引 end=offsets(x+1),然后使用这两个索引通过 Bytes 获取具体数据对应的字节流 bytes.slice(start,end),最后按照字段类型做相应的转换即可。 注:为方便表述,上面例子做了一定的简化,对于 Nullable 数据列,实际上还有FixedLengthColumn<uint8_t> 的数据结构,对应一段连续的 Bool 数据,用来表示对应行号的列数据取值是否为 Null,用来加速 Nullable 列的计算效率,减少数据列的存储空间。 内存分配和管理设计 由于 BE 端是通过 Memory Tracker 进行内存管理的,我们在实现 JNI Reader 时,由 Memory Tracker 统一进行内存的分配和销毁,避免直接使用 JVM Unsafe 分配 Native Memory,来保证内存统计的准确性,并防止出现错误的内存使用导致的各类问题。 编程框架设计 在编程框架上,我们完整定义了 JNI Connector 的 Java 接口,让开发者不需要进行复杂的 JNI 编程以及相关的内存管理,只需要继承 ConnectorScanner 抽象类,实现 open(), getNext(), close() 这三个抽象方法,然后在 BE 侧给出具体实现类的名称,便可以完整使用我们的 JNI Connector, 读取各类 Java 数据源的数据。 混合使用 Native Reader 和 JNI Reader 设计 在 Hudi Reader 的实现里,我们有一个优化方案。上文提到,我们在 Scan range 里添加了 useJNIReader 的字段。这个字段具体取值在 FE 端通过下面逻辑控制: 判断当前表的数据类型:如果是 MOR 表,查询类型为 Snapshot,且该 File Slice 包含一个或一个以上的 Delta log,useJNIReader 便置为 True,其他情况都为 False。 这样带来的效果是,对于同一个 MOR 表的 Snapshot 读取,BE 端实际上会同时混合使用两种不同的 Reader 去拿数据,使用 Native reader 读取不包含 Delta Logs 的 File Slice,使用 JNI Reader 读取包含 Delta Logs 的 File Slice,不同的 Reader 混合使用可以进一步提升读取效率,且这些加速对于用户都是透明无感知的。 6、使用案例 1. 创建 hudi catalog CREATE EXTERNAL CATALOG "hudi_catalog" PROPERTIES ( "type" = "hudi", "hive.metastore.uris"= "thrift://xxx:9083" -- hive metastore地址 ); 2. 执行查询 select * from hudi_catalog.<db_name>.<table_name>; +------+------+---------------------+------------------------+--------------------+-----------------------+------------------------------------------------------------------------+ | id | age | _hoodie_commit_time | _hoodie_partition_path | _hoodie_record_key | _hoodie_commit_seqno | _hoodie_file_name | +------+------+---------------------+------------------------+--------------------+-----------------------+------------------------------------------------------------------------+ | 3 | 33 | 20220422111823231 | age=33 | id:3 | 20220422111823231_0_3 | 29d8d367-93f9-4bd3-aae4-47852e443e36-0_0-128-114_20220422111823231.orc | +------+------+---------------------+------------------------+--------------------+-----------------------+------------------------------------------------------------------------+ 7、性能测试 测试环境: 阿里云 EMR Presto 集群,阿里云 EMR StarRocks 集群 硬件配置: Master: ecs.g7.4xlarge 16 vCPU 64 GiB Worker: ecs.g7.4xlarge 16 vCPU 64 GiB * 5 软件配置: StarRocks 版本:master 分支(Hudi MOR 读取特性会在2.5.0正式发布) StarRocks 软件配置:默认配置 Presto 版本:0.277 Presto 软件配置: query.max-memory-per-node=40GB query.max-memory=200GB COW 表 Snapshot 查询 TPCH-100 测试结果 ​ 对于 COW 表 Snapshot 查询,使用 StarRocks 相对 Presto 大约会带来5 倍的性能提升。 MOR 表 Snapshot 查询 TPCH-100 测试结果 对于 MOR 表 Snapshot 查询,如上文描述,混合使用了 Native Reader 和 JNI Reader,因此,我们在 StarRocks 上测试读取不同比例更新数据(带有 Delta Logs 的 File Slice)的执行时间,来得到不同更新比例场景下的查询效率数据。从测试结果可以看出,更新数据比例越小,StarRocks 实际执行时使用 Native Reader 的比例也就越高,执行效率也越好。 根据社区的经验,最常见的库表更新场景里,在 Hudi 表做 Compaction 操作前,带有 Delta Logs 的 File Slice 占整个表的比例大约在 20% 左右,在这种条件下,使用 StarRocks 相对 Presto 大约会带来2-3 倍的性能提升。 对于 MOR 表 Read Optimized 查询,StarRocks 内部执行原理和流程与 COW 表相同,性能提升数据也一致。 #03 未来开发方向 — 当前 Hudi Scan Range 没有对 Base File 的大小进行 File Split 拆分,是因为考虑了上文所述可能带来的 IO 损失和 Merge 逻辑问题,但实际上,COW 表和 Read Optimized 查询 MOR 表还是依旧可以采用 File Split 逻辑的,针对这些场景还是可以进一步提高 Scan 并发度,提升执行效率。 当前 JNI Connector 还不支持复杂的数据类型如 Array、Struct、Map,接下来需要针对这些类型设计对应的 Native Memory 布局,完成这些类型的支持。 当前 JNI Connector 在 Decimal、Date、Datetime 这三种数据类型的处理上,Java 端是写入原始 String 到内存,然后在 BE 端读取并转换成 StarRocks 内部的对应类型表示,这些内存转换过程会有一定的时间损耗,后续需要进行针对性的优化,尽可能消除这个转换过程。 当前 Hudi 表的部分基础元信息,如 Table location,是通过 Hive Metastore 等外部服务获取的。但在某些应用场景里,用户可能不会将 Hudi 表的元数据同步到 Hive Metastore 里,后续需要将这类 Hudi 表支持起来,在元信息获取流程上解耦外部服务,通过 Hudi Client SDK 完成相关操作。 进一步与 Apache Hudi 社区交流和合作,推动开发 C++ 版本的 Hudi SDK。 支持对 Hudi 表数据的 Incremental Query 查询类型 。 未来,阿里云 EMR OLAP 团队与 StarRocks 社区会继续紧密合作,推出功能更加完善、简单易用、极速统一 的 OLAP 数据湖分析技术和产品。 参考资料: https://hudi.apache.org/ https://help.aliyun.com/document_detail/405463.html?spm=a2c4g.11186623.0.0.36917cc3wBuyu6 关于 StarRocks 面世两年多来,StarRocks 一直专注打造世界顶级的新一代极速全场景 MPP 数据库,帮助企业建立“极速统一”的数据分析新范式,助力企业全面数字化经营。 当前已经帮助腾讯、携程、顺丰、Airbnb 、滴滴、京东、众安保险等超过 170 家大型用户构建了全新的数据分析能力,生产环境中稳定运行的 StarRocks 服务器数目达数千台。 2021 年 9 月,StarRocks 源代码开放,在 GitHub 上的星数已超过 3500 个。StarRocks 的全球社区飞速成长,至今已有超百位贡献者,社群用户突破 7000 人,吸引几十家国内外行业头部企业参与共建。

优秀的个人博客,低调大师

每日一博 | Redis GeoHash 核心原理解析

1. 引言 小麦同学是个吃货+技术宅,平日里就喜欢拿着手机地图点点按按来查询一些好玩的东西。某一天到北海公园游玩,肚肚饿了,于是乎打开手机地图,搜索北海公园附近的餐馆,并选了其中一家用餐。饱暖思yin欲的麦叔饭后思考地图后台如何根据自己所在位置查询来查询附近餐馆的呢?苦思冥想了半天,小麦想出了个方法:计算所在位置P与北京所有餐馆的距离,然后返回距离<=1000米的餐馆。小得意了一会儿,小麦发现北京的餐馆何其多啊,这样计算不得了,于是想了,既然知道经纬度了,那它应该知道自己在西城区,那应该计算所在位置P与西城区所有餐馆的距离啊,机机运用了递归的思想,想到了西城区也很多餐馆啊,应该计算所在位置P与所在街道所有餐馆的距离,这样计算量又小了,效率也提升了。 小麦的计算思想很朴素,就是通过过滤的方法来减小参与计算的餐馆数目,从某种角度上讲,机机在使用索引技术。 一提到索引,大家脑子里马上浮现出B树索引,因为大量的数据库(如MySQL、oracle、PostgreSQL等)都在使用B树。B树索引本质上是对索引字段进行排序,然后通过类似二分查找的方法进行快速查找,即它要求索引的字段是可排序的,一般而言,可排序的是一维字段,比如时间、年龄、薪水等等。但是对于空间上的一个点(二维,包括经度和纬度),如何排序呢?又如何索引呢?解决的方法很多,下文介绍一种方法来解决这一问题。 思想:如果能通过某种方法将二维的点数据转换成一维的数据,那样不就可以继续使用B树索引了嘛。那这种方法真的存在嘛,答案是肯定的。目前很火的GeoHash算法就是运用了上述思想,下面我们就开始GeoHash之旅吧。 2. 感性认识 先来两个干货,在线查看GPS某个区域的GeoHash值。 1. http://geohash.gofreerange.com/ 在这里插入图片描述 2. http://www.geohash.cn/ 跟更好用些 3. 通俗说 GeoHash将二维的经纬度转换成字符串,比如下图展示了北京9个区域的GeoHash字符串,分别是WX4ER,WX4G2、WX4G3等等,每一个字符串代表了某一矩形区域。也就是说,这个矩形区域内所有的点(经纬度坐标)都共享相同的GeoHash字符串,这样既可以保护隐私(只表示大概区域位置而不是具体的点),又比较容易做缓存,比如左上角这个区域内的用户不断发送位置信息请求餐馆数据,由于这些用户的GeoHash字符串都是WX4ER,所以可以把WX4ER当作key,把该区域的餐馆信息当作value来进行缓存,而如果不使用GeoHash的话,由于区域内的用户传来的经纬度是各不相同的,很难做缓存。字符串越长,表示的范围越精确。如图所示,5位的编码能表示10平方千米范围的矩形区域,而6位编码能表示更精细的区域(约0.34平方千米)字符串相似的表示距离相近(特殊情况后文阐述),这样可以利用字符串的前缀匹配来查询附近的POI信息。如下两个图所示,第一个在城区,第二个在郊区,城区的GeoHash字符串之间比较相似,郊区的字符串之间也比较相似,而城区和郊区的GeoHash字符串相似程度要低些。通过上面的介绍我们知道了GeoHash就是一种将经纬度转换成字符串的方法,并且使得在大部分情况下,字符串前缀匹配越多的距离越近,回到我们的案例,根据所在位置查询来查询附近餐馆时,只需要将所在位置经纬度转换成GeoHash字符串,并与各个餐馆的GeoHash字符串进行前缀匹配,匹配越多的距离越近。 4. GeoHash算法的步骤 下面以北海公园附近随便一个位置为例介绍GeoHash算法的计算步骤,先用百度 GPS反定位系统查找看下经纬度。纬度=116.395371,经度=39.931957。 根据经纬度计算GeoHash二进制编码 地球纬度区间是[-90,90], 北海公园的纬度是39.928167,可以通过下面算法对纬度39.928167进行逼近编码: 区间[-90,90]进行二分为[-90,0),[0,90],称为左右区间,可以确定39.928167属于右区间[0,90],给标记为1; 接着将区间[0,90]进行二分为 [0,45),[45,90],可以确定39.928167属于左区间 [0,45),给标记为0; 递归上述过程39.928167总是属于某个区间[a,b]。随着每次迭代区间[a,b]总在缩小,并越来越逼近39.928167; 如果给定的纬度x(39.928167)属于左区间,则记录0,如果属于右区间则记录1,这样随着算法的进行会产生一个序列1011100,序列的长度跟给定的区间划分次数有关。 39.928167 根据纬度算编码 bit min mid max 1 -90.000 0.000 90.000 0 0.000 45.000 90.000 1 0.000 22.500 45.000 1 22.500 33.750 45.000 1 33.750 39.375 45.000 0 39.375 42.188 45.000 0 39.375 40.7815 42.188 0 39.375 40.07825 40.7815 1 39.375 39.726625 40.07825 1 39.726625 39.9024375 40.07825 同理,地球经度区间是[-180,180],可以对经度116.389550进行编码。根据经度算编码 bit min mid max 1 -180 0.000 180 1 0.000 90 180 0 90 135 180 1 90 112.5 135 0 112.5 123.75 135 0 112.5 118.125 123.75 1 112.5 115.3125 118.125 0 115.3125 116.71875 118.125 1 115.3125 116.015625 116.71875 1 116.015625 116.3671875 116.71875 组码 通过上述计算,纬度产生的编码为10111 00011,经度产生的编码为11010 01011。偶数位放经度,奇数位放纬度,把2串编码组合生成新串:11100 11101 00100 01111。最后使用用0-9、b-z(去掉a, i, l, o)这32个字母进行base32编码,首先将11100 11101 00100 01111转成十进制,对应着28、29、4、15,十进制对应的编码就是wx4g。同理,将编码转换成经纬度的解码算法与之相反,具体不再赘述。 5. GeoHash Base32编码长度与精度 可以看出,当geohash base32编码长度为8时,精度在19米左右,而当编码长度为9时,精度在2米左右,编码长度需要根据数据情况进行选择。 一、经纬度距离换算 在纬度相等的情况下: 经度每隔0.00001度,距离相差约1米; 每隔0.0001度,距离相差约10米; 每隔0.001度,距离相差约100米; 每隔0.01度,距离相差约1000米; 每隔0.1度,距离相差约10000米。 在经度相等的情况下: 纬度每隔0.00001度,距离相差约1.1米; 每隔0.0001度,距离相差约11米; 每隔0.001度,距离相差约111米; 每隔0.01度,距离相差约1113米; 每隔0.1度,距离相差约11132米。 6. GeoHash算法 上文讲了GeoHash的计算步骤,仅仅说明是什么而没有说明为什么?为什么分别给经度和维度编码?为什么需要将经纬度两串编码交叉组合成一串编码?本节试图回答这一问题。 如下图所示,我们将二进制编码的结果填写到空间中,当将空间划分为四块时候,编码的顺序分别是左下角00,左上角01,右下脚10,右上角11,也就是类似于Z的曲线,当我们递归的将各个块分解成更小的子块时,编码的顺序是自相似的(分形),每一个子快也形成Z曲线,这种类型的曲线被称为Peano空间填充曲线。 这种类型的空间填充曲线的优点是将二维空间转换成一维曲线(事实上是分形维),对大部分而言,编码相似的距离也相近, 但Peano空间填充曲线最大的缺点就是突变性,有些编码相邻但距离却相差很远,比如0111与1000,编码是相邻的,但距离相差很大。 除Peano空间填充曲线外,还有很多空间填充曲线,如图所示,其中效果公认较好是Hilbert空间填充曲线,相较于Peano曲线而言,Hilbert曲线没有较大的突变。为什么GeoHash不选择Hilbert空间填充曲线呢?可能是Peano曲线思路以及计算上比较简单吧,事实上,Peano曲线就是一种四叉树线性编码方式。 7. 使用注意点 1. 临界问题 由于GeoHash是将区域划分为一个个规则矩形,并对每个矩形进行编码,这样在查询附近POI信息时会导致以下问题,比如红色的点是我们的位置,绿色的两个点分别是附近的两个餐馆,但是在查询的时候会发现距离较远餐馆的GeoHash编码与我们一样(因为在同一个GeoHash区域块上),而较近餐馆的GeoHash编码与我们不一致。这个问题往往产生在边界处。 解决的思路很简单,我们查询时,除了使用定位点的GeoHash编码进行匹配外,还使用周围8个区域的GeoHash编码,这样可以避免这个问题。 2. 注意点 我们已经知道现有的GeoHash算法使用的是Peano空间填充曲线,这种曲线会产生突变,造成了编码虽然相似但距离可能相差很大的问题,因此在查询附近餐馆时候,首先筛选GeoHash编码相似的POI(point of interest)点,然后进行实际距离计算。 3. 使用心得 GeoHash只是空间索引的一种方式,特别适合点数据,而对线、面数据采用R树索引更有优势(可为什么需要空间索引)。 GeoHash值可以区分精度,位数越多,精度越高,表达的地理位置越精细;如一位的GeoHash值把地球划分为32个矩形,8位的geohash值把地球划分为32^8个小矩形 适合根据某个经纬度坐标position计算出GeoHash值,然后和数据库中精度更高的GeoHash值做前缀比较 8.空间索引 常见问题:如何根据自己所在位置查询来查询附近50米的POI(point of interest,比如商家、景点等)呢(图1a)?每个POI都有经纬度信息,用图1b的SQL语句在mySQL中建立了POI_spatial的表,其中lat和lng两个字段来代表纬度和经度。为后续分析方便起见,我人造了40万个POI数据。 方法一:暴力方法 该方法的思路很直接:计算位置与所有POI的距离,并保留距离小于50米的POI。 插句题外话,计算经纬度之间的距离不能像求欧式距离那样平方开根号,因为地球是个不规整的球体(图2a),普通计算适合都是默认按最简单的完美球体假设,两点之间的距离函数应该如图2b所示。 该方法的复杂度为:40万*距离函数。我们将球体距离函数写为mysql存储过程distance,之后我们执行查询操作(图3),发现花费了4.66秒。 方法二:矩形过滤方法 该方法采用逐步细化的方式,一般分为两部: 先用矩形框过滤(图4a),判断一个点在矩形框内很简单,只要进行两次判断(LtMin<lat<LtMax; LnMin<lng<LnMax),落在矩形框内的POI个数为n(n<<40万); 用球面距离公式计算位置与矩形框内n个POI的距离(图4b),并保留距离小于50米的POI 矩形过滤方法的复杂度:40万矩形过滤函数 + n距离函数(n<<40万)。 根据这个思路我们执行SQl查询(图5)(注:经度或纬度每隔0.001度,距离相差约100米,由此推算出矩形左下角和右上角坐标),发现过滤后正好剩下两个POI。 此查询花费了0.36秒,相比于方法一查询时间大大降低,但是对于一次查询来说还是很长。时间长的原因在于遍历了40万次。 方法三:B树对经度或纬度建立索引 方法二耗时的原因在于执行了遍历操作,为了不进行遍历,我们自然想到了索引。我们对纬度进行了B树索引。 altertablepoi_spatialaddindexlatindex(lat);altertablepoi_spatialaddindexlngindex(lng); 此方法包括三个步骤: 通过B树快速找到某纬度范围的POI(图6a),个数为m(m<40万),复杂度为Log(40万)*过滤函数; 在步骤a过滤得到的m个POI中查找某经度范围的POI(图6b),个数为n(n<m),复杂度为m*过滤函数; 用球面距离公式计算位置与步骤b得到的n个POI的距离(图6c),并保留距离小于50米的POI 执行SQL查询(图7),发现时间已经大大降低,从方法2的0.36秒下降到0.01秒 三、B树能索引空间数据吗? 这时候有人会说了:方法三效果如此好,能够满足我们附近POI查询问题啊,看来B树用来索引空间数据也是可以的嘛!那么B树真的能够索引空间数据吗? 只能对经度或纬度索引(一维索引),与期望的不符 我们期待的是快速找出落在某一空间范围的POI(如矩形)(图8a),而不是快速找出落在某纬度或经度范围的POI(图8b),想象一下,我要查询北京某区的POI,但是B树索引不仅给我找出了北京的,还有与北京同一维度的天津、大同、甚至国外城市的POI,当数据量很大时,效率很低。 当数据是多维,比如三维(x,y,z),B树怎么索引?比如z可能是高程值,也可能是时间。有人会说B树其实可以对多个字段进行索引,但这时需要指定优先级,形成一个组合字段,而空间数据在各个维度方向上不存在优先级,我们不能说纬度比经度更重要,也不能说纬度比高程更重要。 当空间数据不是点,而是线(道路、地铁、河流等),面(行政区边界、建筑物等),B树怎么索引?对于面来说,它由一系列首尾相连的经纬度坐标点组成,一个面可能有成百上千个坐标,这时数据库怎么存储,B树怎么索引,这些都是问题。 既然传统的索引不能很好的索引空间数据,我们自然需要一种方法能对空间数据进行索引,即空间索引。 参考 Java实现GPS范围查找 浙大大佬通俗说GPS 本文分享自微信公众号 - sowhat1412(sowhat9094)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

优秀的个人博客,低调大师

Android 启动优化(六)- 深入理解布局优化

前言 说到 Android 启动优化,你一般会想到什么呢? Android 多线程异步加载 Android 首页懒加载 对,这是两种很常见的优化手段,但是如果让你主导这件事情,你会如何开始呢? 梳理现有的业务,哪些是一定要在启动初始化的,哪些是不必要的 需要在启动初始化的,哪些是可以在主线程初始化的,哪些是可以在子线程初始化的 当我们把任务丢到子线程初始化,这时候,我们又会遇到两个问题。 在首页,我们需要用到这个库,如果直接使用,这个库可能还没有初始化,这时候直接调用该库,会发生异常,你要怎么解决 当我们的任务相互依赖时,比如 A 依赖于 B, C 也依赖于 B,要怎么解决这种依赖关系。 这些你有想过嘛。答案都在这几篇文章里面了,这里我就不展开讲了。有疑问的可以一起探讨探讨。 Android 启动优化(一) - 有向无环图 Android 启动优化(二) - 拓扑排序的原理以及解题思路 Android 启动优化(三)- AnchorTask 开源了 Android 启动优化(四)- AnchorTask 是怎么实现的 Android 启动优化(五)- AnchorTask 1.0.0 版本正式发布了 接下来,我们来说一下布局优化相关的。 布局优化的现状与发展趋势 耗时原因 众所周知,布局加载一直是耗时的重灾区。特别是启动阶段,作为第一个 View 加载,更是耗时。 而布局加载之所以耗时,有两个原因。 读取 xml 文件,这是一个 IO 操作。 解析 xml 对象,反射创建 View 一些很常见的做法是 减少布局嵌套层数,减少过度绘制 空界面,错误界面等界面进行懒加载那除了这些做法,我们还有哪些手段可以优化呢? 解决方案 异步加载 采用代码的方式编写布局 异步加载 google 很久之前提供了 AsyncLayoutInflater,异步加载的方案,不过这种方式有蛮多坑的,下文会介绍 采用代码的方式编写布局 代码编写的方式编写布局,我们可能想到使用 java 声明布局,对于稍微复杂一点的布局,这种方式是不可取的,存在维护性查,修改困难等问题。为了解决这个问题,github 上面诞生了一系列优秀的开源库。 litho: https://github.com/facebook/litho X2C: https://github.com/iReaderAndroid/X2C 为了即保留xml的优点,又解决它带来的性能问题,我们开发了X2C方案。即在编译生成APK期间,将需要翻译的layout翻译生成对应的java文件,这样对于开发人员来说写布局还是写原来的xml,但对于程序来说,运行时加载的是对应的java文件.我们采用APT(Annotation Processor Tool)+ JavaPoet技术来完成编译期间【注解】->【解注解】->【翻译xml】->【生成java】整个流程的操作。 这两个开源库在大型的项目基本不会使用,不过他们的价值是值得肯定的,核心思想很有意义。 xml 布局加载耗时的问题, google 也想改善这种现状,最近 Compose beta 发布了,他是采用声明式 UI 的方式来编写布局,避免了 xml 带来的耗时。同时,还支持布局实时预览。这个应该是以后的发展趋势。 compose-samples: https://github.com/android/compose-samples 小结 上面讲了布局优化的现状与发展趋势,接下来我们一起来看一下,有哪些布局优化手段,可以应用到项目中的。 渐进式加载 异步加载 compose 声明式 UI 渐进式加载 什么是渐进式加载 渐进式加载,简单来说,就是一部分一部分加载,当前帧加载完成之后,再去加载下一帧。 一种极致的做法是,加载 xml 文件,就想加载一个空白的 xml,布局全部使用 ViewStub 标签进行懒加载。 这样设计的好处是可以减缓同一时刻,加载 View 带来的压力,通常的做法是我们先加载核心部分的 View,再逐步去加载其他 View。 有人可能会这样问了,这样的设计很鸡肋,有什么用呢? 确实,在高端机上面作用不明显,甚至可能看不出来,但是在中低端机上面,带来的效果还是很明显的。在我们项目当中,复杂的页面首帧耗时约可以减少 30%。 优点:适配成本低,在中低端机上面效果明显。 缺点:还是需要在主线程读取 xml 文件 核心伪代码 1start(){2loadA(){3loadB(){4loadC()5}6}7} 上面的这种写法,是可以的,但是这种做法,有一个很明显的缺点,就是会造成回调嵌套层数过多。当然,我们也可以使用 RxJava 来解决这种问题。但是,如果项目中没用 Rxjava,引用进来,会造成包 size 增加。 一个简单的做法就是使用队列的思想,将所有的 ViewStubTask 添加到队列当中,当当前的 ViewStubTask 加载完成,才加载下一个,这样可以避免回调嵌套层数过多的问题。 改造之后的代码见 1valdecorView=this.window.decorView2ViewStubTaskManager.instance(decorView)3.addTask(ViewStubTaskContent(decorView))4.addTask(ViewStubTaskTitle(decorView))5.addTask(ViewStubTaskBottom(decorView))6.start() 1classViewStubTaskManagerprivateconstructor(valdecorView:View):Runnable{ 2 3privatevariViewStubTask:IViewStubTask?=null 4 5companionobject{ 6 7constvalTAG="ViewStubTaskManager" 8 9@JvmStatic10funinstance(decorView:View):ViewStubTaskManager{11returnViewStubTaskManager(decorView)12}13}1415privatevalqueue:MutableList<ViewStubTask>=CopyOnWriteArrayList()16privatevallist:MutableList<ViewStubTask>=CopyOnWriteArrayList()171819funsetCallBack(iViewStubTask:IViewStubTask?):ViewStubTaskManager{20this.iViewStubTask=iViewStubTask21returnthis22}2324funaddTask(viewStubTasks:List<ViewStubTask>):ViewStubTaskManager{25queue.addAll(viewStubTasks)26list.addAll(viewStubTasks)27returnthis28}2930funaddTask(viewStubTask:ViewStubTask):ViewStubTaskManager{31queue.add(viewStubTask)32list.add(viewStubTask)33returnthis34}353637funstart(){38if(isEmpty()){39return40}41iViewStubTask?.beforeTaskExecute()42//指定decorView绘制下一帧的时候会回调里面的runnable43ViewCompat.postOnAnimation(decorView,this)44}4546funstop(){47queue.clear()48list.clear()49decorView.removeCallbacks(null)50}5152privatefunisEmpty()=queue.isEmpty()||queue.size==05354overridefunrun(){55if(!isEmpty()){56//当队列不为空的时候,先加载当前viewStubTask57valviewStubTask=queue.removeAt(0)58viewStubTask.inflate()59iViewStubTask?.onTaskExecute(viewStubTask)60//加载完成之后,再postOnAnimation加载下一个61ViewCompat.postOnAnimation(decorView,this)62}else{63iViewStubTask?.afterTaskExecute()64}6566}6768funnotifyOnDetach(){69list.forEach{70it.onDetach()71}72list.clear()73}7475funnotifyOnDataReady(){76list.forEach{77it.onDataReady()78}79}8081}8283interfaceIViewStubTask{8485funbeforeTaskExecute()8687funonTaskExecute(viewStubTask:ViewStubTask)8889funafterTaskExecute()909192} 源码地址:https://github.com/gdutxiaoxu/AnchorTask,核心代码主要在ViewStubTask,ViewStubTaskManager, 有兴趣的可以看看 异步加载 异步加载,简单来说,就是在子线程创建 View。在实际应用中,我们通常会先预加载 View,常用的方案有: 在合适的时候,启动子线程 inflate layout。然后取的时候,直接去缓存里面查找 View 是否已经创建好了,是的话,直接使用缓存。否则,等待子线程 inlfate 完成。 AsyncLayoutInflater 官方提供了一个类,可以来进行异步的inflate,但是有两个缺点: 每次都要现场new一个出来 异步加载的view只能通过callback回调才能获得(死穴) 因此,我们可以仿造官方的 AsyncLayoutInflater 进行改造。核心代码在 AsyncInflateManager。主要介绍两个方法。 asyncInflate方法,在子线程 inflateView,并将加载结果存放到 mInflateMap 里面。 1@UiThread 2funasyncInflate( 3context:Context, 4varargitems:AsyncInflateItem? 5){ 6items.forEach{item-> 7if(item==null||item.layoutResId==0||mInflateMap.containsKey(item.inflateKey)||item.isCancelled()||item.isInflating()){ 8return 9}10mInflateMap[item.inflateKey]=item11onAsyncInflateReady(item)12inflateWithThreadPool(context,item)13}1415} getInflatedView方法,用来获得异步inflate出来的view,核心思想如下 先从缓存结果里面拿 View,拿到了view直接返回 没拿到view,但是子线程在inflate中,等待返回 如果还没开始inflate,由UI线程进行inflate 1/** 2*用来获得异步inflate出来的view 3* 4*@paramcontext 5*@paramlayoutResId需要拿的layoutId 6*@paramparentcontainer 7*@paraminflateKey每一个View会对应一个inflateKey,因为可能许多地方用的同一个layout,但是需要inflate多个,用InflateKey进行区分 8*@paraminflater外部传进来的inflater,外面如果有inflater,传进来,用来进行可能的SyncInflate, 9*@return最后inflate出来的view10*/11@UiThread12fungetInflatedView(13context:Context?,14layoutResId:Int,15parent:ViewGroup?,16inflateKey:String?,17inflater:LayoutInflater18):View{19if(!TextUtils.isEmpty(inflateKey)&&mInflateMap.containsKey(inflateKey)){20valitem=mInflateMap[inflateKey]21vallatch=mInflateLatchMap[inflateKey]22if(item!=null){23valresultView=item.inflatedView24if(resultView!=null){25//拿到了view直接返回26removeInflateKey(item)27replaceContextForView(resultView,context)28Log.i(TAG,"getInflatedViewfromcache:inflateKeyis$inflateKey")29returnresultView30}3132if(item.isInflating()&&latch!=null){33//没拿到view,但是在inflate中,等待返回34try{35latch.await()36}catch(e:InterruptedException){37Log.e(TAG,e.message,e)38}39removeInflateKey(item)40if(resultView!=null){41Log.i(TAG,"getInflatedViewfromOtherThread:inflateKeyis$inflateKey")42replaceContextForView(resultView,context)43returnresultView44}45}4647//如果还没开始inflate,则设置为false,UI线程进行inflate48item.setCancelled(true)49}50}51Log.i(TAG,"getInflatedViewfromUI:inflateKeyis$inflateKey")52//拿异步inflate的View失败,UI线程inflate53returninflater.inflate(layoutResId,parent,false)54} 简单 Demo 示范 第一步:选择在合适的时机调用AsyncUtils#asyncInflate方法预加载 View, 1objectAsyncUtils{ 2 3funasyncInflate(context:Context){ 4valasyncInflateItem= 5AsyncInflateItem( 6LAUNCH_FRAGMENT_MAIN, 7R.layout.fragment_asny, 8null, 9null10)11AsyncInflateManager.instance.asyncInflate(context,asyncInflateItem)12}1314funisHomeFragmentOpen()=15getSP("async_config").getBoolean("home_fragment_switch",true)16} 第二步:在获取 View 的时候,先去缓存里面查找 View 1overridefunonCreateView( 2inflater:LayoutInflater,container:ViewGroup?, 3savedInstanceState:Bundle? 4):View?{ 5//Inflatethelayoutforthisfragment 6valstartTime=System.currentTimeMillis() 7valhomeFragmentOpen=AsyncUtils.isHomeFragmentOpen() 8valinflatedView:View 910inflatedView=AsyncInflateManager.instance.getInflatedView(11context,12R.layout.fragment_asny,13container,14LAUNCH_FRAGMENT_MAIN,15inflater16)1718Log.i(19TAG,20"onCreateView:homeFragmentOpenis$homeFragmentOpen,timeInstanceis${System.currentTimeMillis()-startTime},${inflatedView.context}"21)22returninflatedView23//returninflater.inflate(R.layout.fragment_asny,container,false)24} 优缺点 优点: 可以大大减少 View 创建的时间,使用这种方案之后,获取 View 的时候基本在 10ms 之内的。 缺点 由于 View 是提前创建的,并且会存在在一个 map,需要根据自己的业务场景将 View 从 map 中移除,不然会发生内存泄露 View 如果缓存起来,记得在合适的时候重置 view 的状态,不然有时候会发生奇奇怪怪的现象。 总结 参考文章:https://juejin.cn/post/6844903924965572615 View 的渐进式加载,在 JectPack compose 没有推广之后,推荐使用这种方案,适配成本低 View 的异步加载方案,虽然效果显著,但是适配成本也高,没搞好,容易发生内存泄露 JectPack compose 声明式 UI,基本是未来的趋势,有兴趣的可以提前了解一下他。 这篇文章,加上一些 Demo,足足花了我几个晚上的时间,觉得不错的话可以关注一下我的微信公众号程序员徐公,小弟在此感谢各位大佬们。主要分享 Android 开发相关的知识,包括 java,kotlin, Android 技术 面试相关的东西,包括常见的面试题目,面试经验分享 算法相关的知识,比如怎么学习算法,leetcode 常见算法总结 一些时事点评,主要是关于互联网的,比如小米高管屌丝事件,拼多多女员工猝死事件等 源码地址:https://github.com/gdutxiaoxu/AnchorTask Android 启动优化(一) - 有向无环图 Android 启动优化(五)- AnchorTask 1.0.0 版本正式发布了 什么?Android webView 的字体突然变小了 徐公啐啐念 - 蹭热点也要有底线 本文分享自微信公众号 - 徐公码字(stormjun94)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册