Hadoop 工作原理是什么?
Hadoop 是一个开源的分布式计算框架,主要由 HDFS(Hadoop 分布式文件系统)和 MapReduce 计算模型两部分组成 。
HDFS 工作原理
HDFS 采用主从架构,有一个 NameNode 和多个 DataNode。NameNode 负责管理文件系统的命名空间,维护文件和目录的元数据信息,如文件名、文件目录结构、文件属性等。DataNode 则负责存储实际的数据块,并根据 NameNode 的指令进行数据块的读写操作。文件在 HDFS 中被切分成固定大小的数据块,默认是 128MB,这些数据块会被复制到多个 DataNode 上以实现容错和高可用性。当客户端要读取文件时,它会先向 NameNode 请求获取文件的数据块位置信息,然后直接从相应的 DataNode 上读取数据。写入文件时,客户端先向 NameNode 请求上传文件,NameNode 会根据文件大小和集群中 DataNode 的使用情况,选择合适的 DataNode 让客户端上传数据块,同时进行数据块的复制操作。
MapReduce 工作原理
MapReduce 主要用于大规模数据集的并行计算。它将计算过程分为两个阶段:Map 阶段和 Reduce 阶段。在 Map 阶段,数据会被并行地处理,每个 Map 任务处理一部分数据,将输入数据解析成键值对,并进行自定义的映射操作,生成中间结果。然后,中间结果会被按照键进行分组,并传输到 Reduce 任务所在的节点。在 Reduce 阶段,每个 Reduce 任务对收到的一组中间结果进行汇总、合并等操作,最终输出计算结果。整个过程中,Hadoop 会自动处理任务的调度、数据的传输和容错等问题,用户只需关注业务逻辑的实现。
Hive 内外部表的区别与应用场景有哪些?
区别
-
数据存储位置
- 内部表:数据存储在 Hive 的默认数据仓库目录下,由 Hive 完全管理,删除表时,数据也会被同时删除。
- 外部表:数据存储在用户指定的外部路径,Hive 仅管理表的元数据,删除表时,仅删除表的元数据,数据依然保留在外部存储路径中。
-
数据格式和文件组织
- 内部表:Hive 会按照其默认的文件格式和组织方式来存储数据,如文本文件格式,以特定的目录结构来存储表数据和分区数据。
- 外部表:数据可以是任意格式和组织方式,只要 Hive 能够识别其对应的 SerDe(序列化和反序列化)来读取数据即可。
-
数据加载和维护
- 内部表:数据加载主要通过 Hive 的 LOAD 语句将数据文件移动到 Hive 的数据仓库目录下。
- 外部表:数据加载通常是通过创建表时指定外部数据路径,Hive 不会移动或复制数据,只是在元数据中记录数据的位置。数据的维护和更新可以由外部系统直接操作数据文件,Hive 会在查询时感知到数据的变化。
-
性能和安全性
- 内部表:由于数据存储在 Hive 的管理之下,在一些情况下,Hive 可以对数据进行更好的优化,如基于文件统计信息的查询优化。但同时也意味着数据的安全性完全依赖于 Hive 的权限管理。
- 外部表:性能方面可能会因为数据格式和存储方式的多样性而有所不同,但在安全性上,可以结合外部存储系统的权限管理来提供更灵活的安全策略。
应用场景
-
内部表的应用场景
- 数据仓库构建:当构建企业级的数据仓库时,数据主要由 Hive 进行管理和处理,使用内部表可以方便地进行数据的组织、清洗、转换和分析。例如,将从各个业务系统抽取过来的数据存储为内部表,按照星型或雪花型模型构建数据仓库的维度表和事实表。
- 数据分析项目:在一些数据分析项目中,数据的生命周期完全由 Hive 和相关的数据分析工具控制,使用内部表可以简化数据管理流程,提高数据分析效率。如对网站日志数据进行分析,将日志数据存储为内部表,方便进行各种维度的统计和分析。
-
外部表的应用场景
- 数据共享和集成:当需要与外部系统共享数据或集成多个不同来源的数据时,外部表非常有用。例如,将 Hive 与 HBase 集成,通过创建外部表指向 HBase 中的表,实现 Hive 对 HBase 数据的查询和分析,同时不影响 HBase 中数据的独立性和管理方式。
- 数据迁移和临时存储:在进行数据迁移项目时,可以先创建外部表指向源数据,然后在 Hive 中进行数据处理和转换,最后将处理后的数据存储到目标位置,而源数据依然保留在原地。另外,对于一些临时需要在 Hive 中分析的数据,如临时从外部文件系统挂载过来的数据,使用外部表可以避免数据的重复存储和管理开销 。
Flink 的 slot 和并行度之间存在怎样的关系?
在 Flink 中,slot 和并行度是两个紧密相关的概念,它们共同影响着 Flink 作业的资源分配和执行效率。
slot 的概念
slot 是 Flink 中资源分配的基本单位,它代表了 TaskManager 中的一个固定资源子集。每个 TaskManager 可以拥有多个 slot,这些 slot 用于执行具体的任务。slot 可以看作是一个容器,它封装了一定的计算资源,如 CPU、内存等,任务在这些 slot 中运行,并且每个 slot 可以独立地分配和管理资源,不同的 slot 之间相互隔离,互不干扰。
并行度的概念
并行度则是指在 Flink 作业中,同一操作或任务可以同时执行的实例数量。例如,一个数据源的并行度为 3,表示有 3 个不同的数据源实例在同时读取数据;一个 Map 操作的并行度为 5,表示有 5 个 Map 任务实例在并行地处理数据。并行度决定了作业在处理数据时的并行程度,较高的并行度可以提高作业的处理速度和吞吐量,但也需要更多的资源支持。
两者的关系
-
资源分配的基础:slot 是并行度的资源载体,并行度的实现依赖于 slot 的分配。当设置一个算子的并行度时,Flink 会尝试为该算子分配相应数量的 slot 来执行任务。例如,如果一个 Map 算子的并行度为 4,那么 Flink 会在集群中寻找 4 个可用的 slot 来分配给这 4 个 Map 任务实例。
-
数量匹配与执行效率:通常情况下,为了充分利用集群资源并提高作业的执行效率,我们希望并行度和 slot 的数量能够合理匹配。如果并行度设置过高,而 slot 数量不足,那么部分任务将无法及时获取到 slot 资源而处于等待状态,导致作业的执行延迟;反之,如果并行度设置过低,而 slot 数量过多,那么会有部分 slot 闲置,造成资源的浪费。理想的情况是,根据作业的负载和集群的资源状况,合理调整并行度和 slot 的数量,使它们达到一个相对平衡的状态,以充分发挥集群的计算能力。
-
动态调整的关联:Flink 支持在作业运行时动态调整并行度,这也会对 slot 的分配产生影响。当增加并行度时,Flink 需要找到更多的 slot 来分配给新增加的任务实例;而当减少并行度时,Flink 会释放一些不再需要的 slot 资源。这种动态调整的能力使得 Flink 能够根据实时的作业负载和资源使用情况,灵活地优化资源分配,提高集群的利用率和作业的性能。
Flink 中 exactly once 精确一次消费是如何实现的?
在 Flink 中,实现 exactly once 精确一次消费主要依赖于其强大的分布式快照机制和两阶段提交协议,以下是具体的实现原理:
分布式快照机制(Chandy-Lamport 算法)
Flink 通过分布式快照机制来记录作业的全局状态,以便在出现故障时能够准确地恢复到之前的状态。在作业运行过程中,Flink 会周期性地生成快照,每个快照包含了所有算子的状态信息。对于数据源算子,快照中记录了当前已经读取到的数据源的位置信息;对于中间算子,快照中记录了算子的计算状态,如窗口的统计信息、聚合结果等;对于输出算子,快照中记录了已经输出到外部系统的数据的相关信息。当出现故障时,Flink 可以根据最近的完整快照来恢复作业的状态,重新从快照记录的位置开始读取数据和处理数据,从而保证数据不会被重复处理或丢失。
两阶段提交协议
在数据输出阶段,Flink 使用两阶段提交协议来确保数据只会被精确地写入外部系统一次。具体来说,当一个事务开始时,Flink 首先会在所有的并行任务中预提交数据,即将数据写入到一个临时的存储位置,但此时数据对外不可见。然后,Flink 等待所有的任务都完成预提交后,再发起全局的提交操作,将临时存储位置的数据正式提交到外部系统中,使其对外可见。如果在预提交或提交过程中出现故障,Flink 会根据分布式快照机制恢复到之前的状态,并重新执行提交操作,直到所有的数据都被精确地写入外部系统一次。
检查点机制与容错
Flink 的检查点机制是实现 exactly once 的关键部分。在作业运行过程中,Flink 会定期触发检查点操作,将所有算子的状态和当前的输入位置等信息保存到持久化存储中,如 HDFS。当作业发生故障时,Flink 会自动从最近的一个成功的检查点恢复作业,重新读取数据并重新执行计算,确保数据的处理结果与没有发生故障时完全一致。同时,Flink 的容错机制还能够处理各种类型的故障,如节点故障、网络故障等,保证作业的连续性和数据的准确性。
幂等写入和事务支持
除了上述机制外,Flink 还依赖于外部系统的幂等写入和事务支持来实现 exactly once。幂等写入意味着多次写入相同的数据不会改变最终的结果,这样即使在恢复过程中数据被重复写入,也不会影响最终的正确性。许多现代的分布式存储系统和消息队列都提供了幂等写入的功能。对于支持事务的外部系统,Flink 可以利用其事务机制来保证数据的原子性和一致性,进一步确保 exactly once 的语义。
Flink 窗口有哪些类型及各自的应用场景?
Flink 提供了多种类型的窗口,用于对无限流数据进行分组和处理,以下是一些常见的窗口类型及其应用场景:
时间窗口
-
滚动时间窗口
- 定义:滚动时间窗口按照固定的时间间隔划分窗口,窗口之间没有重叠,每个数据元素只会属于一个窗口。例如,设置滚动时间窗口大小为 5 分钟,那么数据会被划分到每 5 分钟一个的窗口中,如 [0:00-0:05)、[0:05-0:10) 等。
- 应用场景:适用于对实时数据进行周期性的统计分析,如每隔 5 分钟统计一次网站的访问量、每分钟统计一次传感器数据的平均值等。这种窗口简单直观,便于实现对固定时间周期内的数据进行统一处理。
-
滑动时间窗口
- 定义:滑动时间窗口也是基于时间来划分窗口,但窗口之间有一定的重叠,通过设置窗口大小和滑动步长来确定。例如,设置窗口大小为 10 分钟,滑动步长为 5 分钟,那么数据会被划分到多个有重叠的窗口中,如 [0:00-0:10)、[0:05-0:15) 等。
- 应用场景:常用于对实时数据进行更精细的分析,需要同时考虑当前时间段和近期时间段的数据变化趋势。比如,在实时监控股票价格时,通过滑动时间窗口可以同时观察到过去 10 分钟内的价格波动情况以及最近 5 分钟内的价格变化趋势,以便及时发现价格的异常波动。
-
会话窗口
- 定义:会话窗口根据数据的时间间隔来划分窗口,当数据的时间间隔超过设定的会话超时时间时,就会划分出新的窗口。例如,设置会话超时时间为 30 分钟,如果在 30 分钟内没有新的数据到达,那么当前的会话窗口就会关闭,新的数据会开启一个新的会话窗口。
- 应用场景:常用于分析用户的行为会话,如在网站分析中,用于统计用户在一次会话中的页面浏览量、停留时间等。它可以根据用户的实际操作时间动态地划分窗口,更好地捕捉用户的行为模式和特征。
计数窗口
-
滚动计数窗口
- 定义:滚动计数窗口按照固定的元素数量划分窗口,每个窗口包含固定数量的数据元素,窗口之间没有重叠。例如,设置滚动计数窗口大小为 100,那么每 100 个数据元素就会形成一个窗口。
- 应用场景:适用于对数据量进行固定数量的分组处理,如在处理一批订单数据时,每 100 个订单作为一个窗口进行统计分析,计算每个窗口内订单的总金额、平均金额等指标。
-
滑动计数窗口
- 定义:滑动计数窗口同样基于元素数量划分窗口,但窗口之间有重叠,通过设置窗口大小和滑动步长来确定。例如,设置窗口大小为 200,滑动步长为 100,那么数据会被划分到多个有重叠的窗口中,每个窗口包含 200 个数据元素,且相邻窗口之间有 100 个数据元素的重叠。
- 应用场景:在需要对数据进行连续的、有重叠的分组统计时非常有用。比如,在分析网络数据包时,通过滑动计数窗口可以同时观察到当前 200 个数据包的特征以及最近 100 个数据包的变化情况,有助于及时发现网络流量的异常变化。
Hive 的 udf 如何自定义及使用?
在 Hive 中,UDF(User-Defined Function)即用户自定义函数,可分为以下几种类型:UDF、UDAF(User-Defined Aggregation Function)和 UDTF(User-Defined Table-Generating Function) 。以下是自定义及使用 UDF 的一般步骤:
自定义 UDF
- 首先,需要创建一个 Java 类来实现自定义的 UDF 逻辑。这个类要继承自 org.apache.hadoop.hive.ql.exec.UDF 类。
- 然后,在类中实现 evaluate 方法,该方法是 UDF 的核心,用于定义具体的函数功能。例如,如果要实现一个简单的字符串拼接 UDF,可以在 evaluate 方法中接收两个字符串参数,然后返回拼接后的字符串。
使用 UDF
- 编译并打包自定义的 UDF 类为一个 JAR 文件。
- 在 Hive 中,使用 ADD JAR 命令将打包好的 JAR 文件添加到 Hive 的 classpath 中。例如:ADD JAR /path/to/your/udf.jar;
- 通过 CREATE FUNCTION 语句来创建一个临时函数,将其与自定义的 UDF 类关联起来。语法为:CREATE TEMPORARY FUNCTION function_name AS 'fully_qualified_class_name'; 其中 function_name 是在 Hive 中使用的函数名,fully_qualified_class_name 是自定义 UDF 类的全限定名。
- 创建好函数后,就可以在 Hive 的 SQL 语句中像使用内置函数一样使用自定义的 UDF 了。例如,如果创建了一个名为 concat_strings 的字符串拼接 UDF,就可以在 SELECT 语句中使用它:SELECT concat_strings (column1, column2) FROM table_name;
自定义 UDAF 和 UDTF 的过程与 UDF 类似,但在实现上有一些不同。UDAF 需要继承 org.apache.hadoop.hive.ql.exec.UDAF 类,并实现 init、iterate、terminatePartial、merge 和 terminate 等方法来实现聚合逻辑。UDTF 则需要继承 org.apache.hadoop.hive.ql.exec.UDTF 类,并实现 process 和 close 方法来实现生成多个输出行的逻辑 。通过自定义 UDF 及其相关函数,可以在 Hive 中扩展其功能,满足各种特定的数据处理需求。
Hbase 和 clickhouse 有哪些区别?
数据模型方面
- Hbase:是基于列存储的非关系型数据库,数据存储在列族中,适合存储半结构化或非结构化数据。它以稀疏的、多维的、有序的映射表形式存储数据,行键是唯一标识一行数据的主键,列族可以包含多个列限定符,不同行可以有不同的列。
- ClickHouse:是列式数据库管理系统,数据以列的形式存储,在处理分析型查询时具有高效性。它支持嵌套的数据结构,数据模型相对更灵活,支持类似于 SQL 的查询语言,可方便地进行复杂的数据分析操作。
存储结构方面
- Hbase:基于 Hadoop 的 HDFS 存储,数据存储在分布式文件系统上,具有高容错性和可扩展性。它将数据按照 Region 进行划分,每个 Region 由多个 Store 组成,Store 对应一个列族,数据的读写通过 RegionServer 来完成。
- ClickHouse:使用自己的本地文件系统存储数据,支持数据的快速读写和高效压缩。它将数据存储在表中,表由多个分区组成,分区可以按照时间、地域等维度进行划分,数据的查询可以通过分区裁剪来提高性能。
读写性能方面
- Hbase:适合海量数据的随机读写,尤其是对写入性能要求较高的场景。它采用了 LSM 树(Log-Structured Merge-Tree)的存储结构,写入数据时先写入内存中的 MemStore,然后定期将 MemStore 中的数据刷新到磁盘上的 StoreFile 中,这种方式可以保证写入的高效性。但在复杂查询和分析方面性能相对较弱。
- ClickHouse:主要用于数据分析和查询,对读性能进行了高度优化。它采用了向量化执行引擎和数据压缩技术,能够快速地处理大量数据的查询请求。在处理聚合查询、分组查询等复杂分析操作时表现出色,但写入性能相对 Hbase 较弱,不太适合高并发的写入场景。
应用场景方面
- Hbase:常用于实时大数据分析、物联网数据存储、用户画像等场景,能够实时地处理和存储海量的用户行为数据、设备数据等。
- ClickHouse:广泛应用于数据分析、数据仓库、商业智能等领域,适合对数据进行快速查询和分析,如互联网公司的用户行为分析、广告数据分析、金融行业的风险评估和数据分析等。
ElasticSearch 中使用的倒排索引原理是什么?你自己用 ES 做过什么小 Demo,用过哪个版本?ES 和数据库之间的数据同步策略有哪些?
倒排索引原理
ElasticSearch 使用倒排索引来实现快速的全文搜索。倒排索引是一种数据结构,它将文档中的每个单词作为索引项,记录了该单词在哪些文档中出现以及出现的位置等信息。具体来说:
- 首先,当向 ElasticSearch 中写入文档时,它会对文档进行分词处理,将文档内容分割成一个个的单词。
- 然后,为每个单词建立一个倒排索引项,索引项中包含了单词本身以及一个列表,列表中记录了包含该单词的所有文档的编号(通常称为文档 ID)以及单词在文档中的位置等信息。
- 当进行搜索时,用户输入的搜索词也会被分词,然后根据分词后的单词去查找倒排索引,找到包含这些单词的所有文档,再根据一些相关性算法对这些文档进行排序,最终返回最相关的文档给用户。
例如,有三个文档:文档 1 为 “我爱自然语言处理”,文档 2 为 “自然语言处理很有趣”,文档 3 为 “我喜欢编程”。经过分词和建立倒排索引后,对于单词 “自然语言处理”,其倒排索引项中会记录它出现在文档 1 和文档 2 中;对于单词 “我”,其倒排索引项中会记录它出现在文档 1 和文档 3 中,以此类推。当用户搜索 “自然语言处理” 时,就可以快速找到文档 1 和文档 2 。
个人使用 ES 的小 Demo 及版本
我曾经使用 ElasticSearch 7.10 版本做过一个简单的电影信息搜索 Demo。首先,创建了一个名为 “movies” 的索引,定义了电影的名称、类型、导演、演员等字段。然后,使用 Python 的 Elasticsearch 库向索引中批量插入了一些电影数据,例如《泰坦尼克号》《阿甘正传》等电影的信息。接着,实现了一个简单的搜索功能,用户可以输入电影名称、演员名字等关键词进行搜索,通过调用 ES 的搜索 API,根据关键词在倒排索引中查找相关的电影信息,并将结果展示给用户。在这个 Demo 中,体验到了 ES 强大的全文搜索能力和快速的查询响应速度。
ES 和数据库之间的数据同步策略
- 基于日志的同步:通过解析数据库的事务日志,如 MySQL 的 Binlog,来获取数据库的变更数据,然后将这些变更数据同步到 ElasticSearch 中。这种方式可以实时地捕获数据库的变化,保证数据的一致性,但需要对数据库的日志格式有深入的了解,并且需要一定的开发工作来实现日志的解析和数据的同步。
- 定时任务同步:使用定时任务,定期从数据库中查询数据,然后将查询到的数据与 ElasticSearch 中的数据进行比较,找出差异并进行同步。这种方式比较简单,容易实现,但数据同步的实时性较差,可能会存在一定的数据延迟。
- 数据库触发器同步:在数据库中创建触发器,当数据库中的数据发生变化时,触发器会自动调用相应的程序或脚本,将变更数据同步到 ElasticSearch 中。这种方式可以保证数据的实时同步,但会增加数据库的负担,并且在复杂的业务场景下,触发器的维护和管理可能会比较困难。
- 使用数据同步工具:有一些专门的数据同步工具,如 Logstash、Sqoop 等,可以方便地实现数据库和 ElasticSearch 之间的数据同步。例如,Logstash 可以配置输入源为数据库,输出目标为 ElasticSearch,通过定义一些转换规则和配置参数,就可以实现数据的抽取、转换和加载(ETL)操作,将数据库中的数据同步到 ElasticSearch 中。
数据仓库为什么要分层,数仓分层的架构是怎样的?
数据仓库分层的原因
- 解耦数据处理流程:将复杂的数据处理过程分解为多个层次,每个层次专注于特定的任务和功能,使得数据的抽取、转换、加载(ETL)等操作更加清晰和易于管理。不同层次之间通过接口进行交互,降低了各层之间的耦合度,便于维护和扩展。
- 提高数据质量:在分层的过程中,可以在不同层次对数据进行清洗、转换和验证,及时发现和处理数据中的错误和不一致性,从而提高数据的准确性和可靠性。
- 提升数据处理效率:通过分层,可以对数据进行预处理和优化,如对数据进行聚合、汇总等操作,减少了数据的冗余和复杂性,使得在进行数据分析和查询时能够更快地获取所需数据,提高了数据处理的效率。
- 便于团队协作和数据共享:分层架构使得不同的团队可以专注于不同的层次,如数据采集团队负责数据的抽取和加载到原始数据层,数据开发团队负责数据的清洗和转换等中间层的处理,数据分析团队则主要使用数据集市层进行数据分析。这样有利于团队之间的分工协作,同时也方便了数据在不同团队之间的共享和复用。
数据仓库分层架构
- ODS(Operational Data Store)层:即操作数据存储层,是数据仓库的第一层,主要用于存储从各个数据源抽取过来的原始数据,数据基本保持与源数据一致,不做过多的处理。其目的是为了保留数据的原始性和完整性,以便在后续的处理中能够追溯数据的来源。
- DWD(Data Warehouse Detail)层:数据仓库明细层,是在 ODS 层的基础上,对原始数据进行清洗、转换、去重等操作,将数据按照主题域进行分类和组织,形成规范化的明细数据。该层的数据具有一致性和准确性,是后续数据处理的基础。
- DWS(Data Warehouse Summary)层:数据仓库汇总层,主要是对 DWD 层的数据进行轻度汇总和聚合,按照一定的业务规则和维度进行统计和计算,生成一些中间汇总数据。这些汇总数据可以为数据分析和决策提供更高效的支持,减少了在查询时对明细数据的计算量。
- ADS(Application Data Store)层:应用数据存储层,也称为数据集市层,是根据不同的业务需求和分析主题,从 DWS 层或 DWD 层抽取数据并进行进一步的加工和处理,形成面向特定应用的数据集合。该层的数据直接面向数据分析和决策人员,满足各种具体的业务分析和报表需求。
除了上述主要层次外,还可能包括一些其他的辅助层,如元数据管理层,用于管理数据仓库中的元数据信息,包括数据的来源、定义、转换规则等;以及缓冲层,用于在数据处理过程中临时存储数据,提高数据处理的性能和稳定性。
数据仓库建模有哪些方法和策略?
数据仓库建模方法
- 维度建模
- 事实表与维度表:维度建模以事实表为中心,事实表存储业务过程的度量值,如销售额、销售量等,而维度表则存储与事实表相关的维度信息,如时间、地点、产品等。通过事实表与维度表的关联,可以方便地进行数据分析和查询。
- 星型模型与雪花模型:星型模型是一种简单的维度模型,事实表位于中心,周围连接着多个维度表,形状像星星。雪花模型则是在星型模型的基础上,对维度表进行了规范化处理,将一些维度表进一步分解为多个子维度表,形成类似雪花的结构。雪花模型可以减少数据冗余,但查询复杂度相对较高;星型模型则更适合于快速查询和分析。
- 实体关系建模
- 基于 ER 图:这种建模方法强调数据之间的实体关系,通过绘制实体关系图(ER 图)来描述数据仓库中的各种实体、实体的属性以及实体之间的关系。它更注重数据的完整性和一致性,适合于对数据关系要求较为严格的场景,如金融、电信等行业的数据仓库建模。
- 规范化处理:在实体关系建模中,通常会对数据进行规范化处理,以消除数据冗余和异常,提高数据的存储效率和维护性。但过度的规范化可能会导致查询性能下降,因此需要在规范化和性能之间进行权衡。
数据仓库建模策略
- 自顶向下建模
- 确定业务需求和主题域:首先从企业的整体业务需求出发,确定数据仓库的主题域,如销售、财务、人力资源等。然后根据主题域分析业务流程和数据需求,构建高层次的数据模型框架。
- 逐步细化和完善:在高层次模型的基础上,逐步向下细化,确定每个主题域中的实体、属性和关系,以及数据的粒度和层次结构。这种策略适合于对企业业务有全面了解,且业务需求相对稳定的情况,可以保证数据仓库的整体架构的合理性和一致性。
- 自底向上建模
- 从数据源出发:先从现有的数据源入手,对数据源中的数据进行分析和整理,识别出数据的实体、属性和关系,构建基础的数据模型。
- 整合和汇总:然后根据业务需求,将基础数据模型进行整合和汇总,逐步构建出数据仓库的整体模型。这种策略适合于数据源复杂多样,且业务需求不太明确的情况,可以快速地构建出可用的数据模型,然后在实践中不断完善和扩展。
- 混合建模:将自顶向下和自底向上两种建模策略相结合,在整体架构上采用自顶向下的方式,确定数据仓库的主题域和高层次模型,在具体的模型构建上则采用自底向上的方式,从数据源出发逐步构建和完善模型。这种策略可以充分发挥两种建模策略的优势,既能够保证数据仓库的整体架构的合理性,又能够灵活地适应数据源和业务需求的变化。
Java 多线程是如何实现的,启动的方式有哪些?
Java 多线程的实现主要有以下两种方式:
- 继承 Thread 类:定义一个类继承自 Thread 类,然后重写 run () 方法,在 run () 方法中编写线程要执行的任务逻辑。例如:
class MyThread extends Thread {@Overridepublic void run() {System.out.println("线程执行的任务");}
}
创建该类的实例后,通过调用 start () 方法来启动线程,而不是直接调用 run () 方法,直接调用 run () 方法只是在当前线程中执行 run () 方法内的代码,并不会启动新线程。如:MyThread myThread = new MyThread(); myThread.start();
。
- 实现 Runnable 接口:定义一个类实现 Runnable 接口,并重写 run () 方法。然后创建该类的实例,将其作为参数传递给 Thread 类的构造函数来创建 Thread 对象,最后调用 start () 方法启动线程。例如:
class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println("线程执行的任务");}
}
使用时:MyRunnable myRunnable = new MyRunnable(); Thread thread = new Thread(myRunnable); thread.start();
。
Java 还提供了通过 Callable 和 Future 来实现多线程的方式,这种方式可以获取线程执行的返回结果。先定义一个实现 Callable 接口的类,重写 call () 方法,然后通过 FutureTask 包装 Callable 对象,再将 FutureTask 作为参数创建 Thread 并启动。 例如:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;class MyCallable implements Callable<Integer> {@Overridepublic Integer call() throws Exception {return 1 + 2;}
}public class Main {public static void main(String[] args) throws ExecutionException, InterruptedException {MyCallable myCallable = new MyCallable();FutureTask<Integer> futureTask = new FutureTask<>(myCallable);Thread thread = new Thread(futureTask);thread.start();Integer result = futureTask.get();System.out.println(result); }
}
接口和类有什么区别,在实际使用中如何选择?
接口和类在 Java 中有诸多区别,以下从几个方面来阐述:
- 定义和组成
- 类:是对现实世界中事物的抽象,它包含了属性和方法。属性用于描述对象的状态,方法用于描述对象的行为。例如,定义一个 “人” 的类,可以有姓名、年龄等属性,以及走路、说话等方法。
class Person {private String name;private int age;public void walk() {System.out.println(name + " is walking");}public void talk() {System.out.println(name + " is talking");}
}
- 接口:主要定义了一组方法签名,但不包含方法的具体实现。它通常用于规定一组相关类应该具有的行为规范。比如,定义一个 “可飞行” 的接口,其中有一个 “fly” 方法签名。
interface Flyable {void fly();
}
- 继承和实现
- 类:支持单继承,一个类只能继承自一个父类。通过继承,子类可以获得父类的属性和方法,并且可以根据需要进行扩展和修改。
class Student extends Person {private String school;public void study() {System.out.println("The student is studying");}
}
- 接口:一个类可以实现多个接口,从而具备多种行为规范。这使得类的设计更加灵活,能够满足多样化的需求。
class Bird implements Flyable {@Overridepublic void fly() {System.out.println("The bird is flying");}
}
- 访问修饰符和默认方法
- 类:可以使用各种访问修饰符来控制成员的可见性,如 public、private、protected 等。类中的方法如果没有被修饰符修饰,默认是包访问权限。
- 接口:方法默认是 public abstract 的,属性默认是 public static final 的。从 Java 8 开始,接口中可以定义默认方法,即带有默认实现的方法,这使得在不破坏接口向后兼容性的前提下,可以为接口添加新的方法。
在实际使用中,如果要描述一个具有明确属性和行为的具体事物,通常使用类。例如,创建各种业务实体类,如用户类、订单类等。如果要定义一组规范或契约,让不同的类去遵循和实现,就使用接口。比如,定义数据访问层的接口,让不同的数据库实现类去实现该接口,以方便切换数据库。当需要让一个类具备多种行为特征时,也会使用接口,通过实现多个接口来达到目的 。
Java 线程有哪些状态?
Java 线程主要有以下几种状态:
-
新建状态(New):当使用 new 关键字创建一个线程对象时,线程就处于新建状态。此时线程只是一个空的对象,还没有分配系统资源,也没有开始执行。例如:
Thread thread = new Thread();
这里的 thread 就处于新建状态。 -
就绪状态(Runnable):当调用线程的 start () 方法后,线程进入就绪状态。此时线程已经获得了除 CPU 时间片以外的所有系统资源,只要 CPU 分配时间片给它,就可以立即执行。处于就绪状态的线程会被放入线程调度器的就绪队列中,等待调度。比如上述创建的 thread 线程,调用
thread.start();
后就进入就绪状态。 -
运行状态(Running):当就绪状态的线程获得 CPU 时间片后,就进入运行状态,开始执行线程的 run () 方法。在一个单核 CPU 系统中,同一时刻只有一个线程处于运行状态;而在多核 CPU 系统中,可能有多个线程同时处于运行状态。
-
阻塞状态(Blocked):线程在运行过程中,可能会因为某些原因而暂停执行,进入阻塞状态。例如,当线程调用了一个阻塞式的 IO 方法,如
InputStream.read()
,在读取数据时如果没有数据可读,线程就会阻塞,直到有数据可读为止。或者当线程调用了synchronized
关键字修饰的方法或代码块,而该方法或代码块已经被其他线程占用时,当前线程也会阻塞等待。 -
等待状态(Waiting):线程进入等待状态通常是因为调用了一些特定的方法,如
Object.wait()
、Thread.join()
等。处于等待状态的线程会释放其持有的锁,直到其他线程调用了相应的notify()
或notifyAll()
方法来唤醒它。例如,在生产者消费者模式中,消费者线程发现缓冲区为空时,可能会调用wait()
方法进入等待状态,等待生产者生产数据后唤醒。 -
定时等待状态(Timed Waiting):与等待状态类似,但它可以在指定的时间后自动唤醒。比如调用了
Thread.sleep(long millis)
方法,线程会进入定时等待状态,在指定的毫秒数后自动苏醒继续执行。或者调用了Object.wait(long timeout)
方法,在超时时间到达后也会自动苏醒。 -
终止状态(Terminated):当线程的 run () 方法执行完毕,或者线程执行过程中出现了未捕获的异常导致线程提前结束,线程就进入终止状态。此时线程的生命周期结束,不再占用任何系统资源。
常见的线程池有哪些?请讲讲你熟悉的并发编程以及掌握得比较好的方面。
常见的线程池主要有以下几种:
- FixedThreadPool:固定大小的线程池,它在创建时就指定了线程池的大小,线程数量始终保持不变。当有新任务提交时,如果线程池中有空闲线程,则直接使用空闲线程执行任务;如果没有空闲线程,则任务会进入阻塞队列等待,直到有线程空闲。这种线程池适用于处理 CPU 密集型任务,因为它可以控制并发线程的数量,避免过多的线程竞争 CPU 资源导致性能下降。例如:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class Main {public static void main(String[] args) {// 创建固定大小为5的线程池ExecutorService executorService = Executors.newFixedThreadPool(5); for (int i = 0; i < 10; i++) {final int taskId = i;executorService.execute(() -> {System.out.println("执行任务 " + taskId + ",线程:" + Thread.currentThread().getName());});}// 关闭线程池executorService.shutdown(); }
}
- CachedThreadPool:可缓存的线程池,它的线程数量不固定,会根据任务的数量动态地创建和销毁线程。如果有新任务提交时,线程池中有空闲线程,则使用空闲线程执行任务;如果没有空闲线程,则创建新线程执行任务。当线程空闲时间超过一定时间(默认 60 秒)后,线程会被自动回收。这种线程池适用于处理大量的短期异步任务,例如网络请求的处理等,因为它可以快速地响应新任务的提交,提高任务的处理效率。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class Main {public static void main(String[] args) {// 创建可缓存的线程池ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < 10; i++) {final int taskId = i;executorService.execute(() -> {System.out.println("执行任务 " + taskId + ",线程:" + Thread.currentThread().getName());});}// 关闭线程池executorService.shutdown(); }
}
- SingleThreadExecutor:单线程的线程池,它只有一个线程在工作,所有提交的任务都按照提交的顺序依次执行。这种线程池适用于需要保证任务顺序执行的场景,例如任务之间有先后依赖关系,或者需要将任务依次提交到一个共享资源进行处理等。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class Main {public static void main(String[] args) {// 创建单线程的线程池ExecutorService executorService = Executors.newSingleThreadExecutor(); for (int i = 0; i < 10; i++) {final int taskId = i;executorService.execute(() -> {System.out.println("执行任务 " + taskId + ",线程:" + Thread.currentThread().getName());});}// 关闭线程池executorService.shutdown(); }
}
生产者消费者模式是如何实现的?
生产者消费者模式是一种经典的多线程设计模式,它主要用于解决生产者和消费者之间的协作问题,使得生产者和消费者可以在不同的线程中并发执行,提高系统的整体性能和资源利用率。以下是一种基于 Java 的实现方式:
- 共享资源类:首先需要定义一个共享资源类,用于存储和管理生产者生产的数据以及消费者消费的数据。这个类通常包含一个缓冲区,用于存放数据,以及一些用于操作缓冲区的方法,如添加数据和取出数据的方法。例如:
class Buffer {private final int[] buffer;private int count;private int putIndex;private int takeIndex;public Buffer(int size) {buffer = new int[size];count = 0;putIndex = 0;takeIndex = 0;}// 生产者调用的方法,向缓冲区添加数据public synchronized void put(int value) throws InterruptedException {while (count == buffer.length) {// 如果缓冲区已满,生产者等待wait();}buffer[putIndex] = value;putIndex = (putIndex + 1) % buffer.length;count++;// 通知消费者有数据可消费notifyAll();}// 消费者调用的方法,从缓冲区取出数据public synchronized int take() throws InterruptedException {while (count == 0) {// 如果缓冲区为空,消费者等待wait();}int value = buffer[takeIndex];takeIndex = (takeIndex + 1) % buffer.length;count--;// 通知生产者可以继续生产notifyAll();return value;}
}
- 生产者类:定义生产者类,实现 Runnable 接口,在其 run () 方法中不断地生产数据,并将数据放入共享缓冲区。例如:
class Producer implements Runnable {private final Buffer buffer;public Producer(Buffer buffer) {this.buffer = buffer;}@Overridepublic void run() {for (int i = 0; i < 10; i++) {try {buffer.put(i);System.out.println("生产者生产了:" + i);Thread.sleep((int) (Math.random() * 1000));} catch (InterruptedException e) {e.printStackTrace();}}}
}
- 消费者类:定义消费者类,同样实现 Runnable 接口,在其 run () 方法中不断地从共享缓冲区取出数据并进行消费。例如:
class Consumer implements Runnable {private final Buffer buffer;public Consumer(Buffer buffer) {this.buffer = buffer;}@Overridepublic void run() {for (int i = 0; i < 10; i++) {try {int value = buffer.take();System.out.println("消费者消费了:" + value);Thread.sleep((int) (Math.random() * 1000));} catch (InterruptedException e) {e.printStackTrace();}}}
}
- 测试类:在测试类中创建共享缓冲区、生产者和消费者对象,并启动相应的线程。例如:
public class Main {public static void main(String[] args) {Buffer buffer = new Buffer(5);Producer producer = new Producer(buffer);Consumer consumer = new Consumer(buffer);Thread producerThread = new Thread(producer);Thread consumerThread = new Thread(consumer);producerThread.start();consumerThread.start();try {producerThread.join();consumerThread.join();} catch (InterruptedException e) {e.printStackTrace();}}
}
在上述实现中,通过使用synchronized
关键字和wait()
、notifyAll()
方法来实现生产者和消费者之间的同步和协作。生产者在缓冲区满时等待,消费者在缓冲区空时等待,当有数据生产或消费后,通过notifyAll()
方法唤醒等待的线程,从而保证了数据的正确生产和消费。
死锁避免有哪些策略?
死锁是指多个进程在运行过程中因争夺资源而造成的一种僵局,若无外力作用,这些进程都将无法向前推进。死锁避免的策略主要有以下几种:
- 破坏互斥条件:这是最理想的策略,但在很多情况下难以实现。例如,有些资源本身的性质决定了它在一段时间内只能被一个进程使用,如打印机。不过,对于一些可同时共享访问的资源,如只读文件,可以通过允许多个进程同时访问来破坏互斥条件。
- 破坏占有且等待条件:可以要求进程在申请资源时一次性申请它所需要的所有资源,而不是在占有部分资源的情况下再去等待其他资源。这样可以避免进程在等待其他资源时还占有一些资源,从而防止死锁的发生 。
- 破坏不可抢占条件:当一个进程已经占有了某些资源,在它申请新的资源且得不到满足时,系统可以强行收回它已经占有的资源。但这种策略实现起来比较复杂,且可能会导致一些进程之前的工作白费,需要谨慎使用。
- 破坏循环等待条件:可以通过给资源编号,规定进程按照资源编号递增的顺序申请资源。这样就可以避免出现循环等待的情况。因为如果每个进程都按照顺序申请资源,就不会出现一个进程等待另一个进程所占有的低编号资源,而另一个进程又等待这个进程所占有的高编号资源的情况。
请写一个单例模式(如 DCL),并说明两次检查的作用和区别,单例模式还有其他哪些写法,各有什么优缺点?
以下是一个双重检查锁定(DCL)的单例模式示例代码:
public class Singleton {private static volatile Singleton instance;private Singleton() {}public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}
}
第一次检查 if (instance == null)
是为了避免不必要的同步开销。如果实例已经被创建,那么直接返回实例,不需要进入同步块。第二次检查是在同步块内部,主要是为了防止多个线程同时通过了第一次检查,然后在同步块内又多次创建实例。因为可能有多个线程同时等待获取锁,当第一个线程创建了实例并释放锁后,其他线程获取锁后如果不再次检查,就会再次创建实例。
单例模式的其他写法及优缺点如下:
- 饿汉式单例
public class Singleton {private static final Singleton instance = new Singleton();private Singleton() {}public static Singleton getInstance() {return instance;}
}
优点是实现简单,在类加载时就创建了实例,所以是线程安全的。缺点是如果实例在整个程序运行过程中都没有被使用,那么就会造成资源的浪费。
- 懒汉式单例(非线程安全)
public class Singleton {private static Singleton instance;private Singleton() {}public static Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;}
}
优点是只有在第一次使用时才创建实例,节省了资源。缺点是在多线程环境下不安全,可能会创建多个实例。
- 静态内部类单例
public class Singleton {private Singleton() {}private static class SingletonHolder {private static final Singleton instance = new Singleton();}public static Singleton getInstance() {return SingletonHolder.instance;}
}
优点是既实现了懒加载,又保证了线程安全,同时利用了类加载机制的特性,不会因为加锁等操作而影响性能。缺点是代码相对复杂一些,理解起来有一定难度。
ZooKeeper 在项目中通常扮演什么角色,发挥怎样的作用?ZooKeeper 的集群如何搭建?
ZooKeeper 在项目中的角色和作用
- 分布式协调服务:在分布式系统中,不同的节点需要相互协作,ZooKeeper 可以作为协调者,帮助各个节点就某些关键信息达成共识,比如在分布式锁的实现中,多个节点通过 ZooKeeper 来竞争锁资源,ZooKeeper 确保只有一个节点能成功获取锁。
- 配置管理:集中管理分布式系统中的配置信息。当配置发生变化时,ZooKeeper 可以通知各个相关节点,使它们及时更新配置,确保整个系统的一致性。例如,一个分布式的大数据处理系统,其各个节点的处理参数等配置可以统一放在 ZooKeeper 中管理。
- 服务发现:让分布式系统中的服务提供者和服务消费者能够相互发现对方。服务提供者在 ZooKeeper 上注册自己的服务信息,服务消费者通过 ZooKeeper 查找并获取服务提供者的信息,从而实现服务的调用,如微服务架构中的服务注册与发现。
- 集群管理:监控分布式集群中各个节点的状态,包括节点的上线、下线等情况。当有节点加入或离开集群时,ZooKeeper 可以及时通知其他节点,以便集群进行相应的调整,如 Hadoop 集群中可以利用 ZooKeeper 来管理节点的状态。
ZooKeeper 集群的搭建
- 环境准备:首先需要准备多台服务器,确保服务器之间网络畅通。安装好 Java 运行环境,因为 ZooKeeper 是用 Java 编写的,依赖 Java 运行。
- 下载和配置:在每台服务器上下载 ZooKeeper 的安装包,并解压。进入 ZooKeeper 的配置目录,修改配置文件
zoo.cfg
。主要配置参数包括tickTime
,它定义了 ZooKeeper 中的基本时间单位,如设置为 2000,表示 2 秒;initLimit
规定了从节点与主节点初始连接时能容忍的心跳数,一般设置为 10 ;syncLimit
规定了从节点与主节点之间同步数据时能容忍的心跳数,通常设置为 5。还需要配置dataDir
,指定 ZooKeeper 存储数据的目录。 - 集群配置:在
zoo.cfg
文件中配置集群信息,格式为server.id=host:port:port
。其中id
是每个节点的唯一标识,host
是节点的主机名或 IP 地址,第一个port
是节点之间通信的端口,第二个port
是用于选举的端口。例如,server.1=node1:2888:3888
。 - 启动和验证:在每台服务器上分别启动 ZooKeeper 服务,通过查看日志文件或使用 ZooKeeper 的客户端工具连接到集群,验证集群是否正常工作。可以使用
zkCli.sh
客户端工具连接到集群,执行一些简单的命令,如ls /
查看根目录下的节点信息,以确认集群搭建成功。
ZooKeeper 的选举策略是怎样的?
ZooKeeper 的选举策略主要是基于过半机制和投票机制来实现的。
当 ZooKeeper 集群中的主节点(Leader)出现故障或新节点加入集群时,会触发选举过程。每个节点在启动时都会给自己投票,投票信息包括自己的服务器 ID 和最新的事务 ID。服务器 ID 是在配置文件中为每个节点指定的唯一标识,事务 ID 则是节点处理的最新事务的编号,代表了节点的数据新旧程度。
然后,节点会将自己的投票信息发送给集群中其他节点。收到投票信息的节点会比较自己的投票和收到的投票,根据一定的规则来决定是否更新自己的投票。比较的规则主要是先比较事务 ID,事务 ID 越大,表示数据越新,越有可能成为 Leader。如果事务 ID 相同,则比较服务器 ID,服务器 ID 越大越优先。
在选举过程中,每个节点都会不断收集其他节点的投票信息,当一个节点收到超过半数的相同投票时,就认为该投票对应的节点当选为 Leader。这里的半数是指集群中节点数量的一半加 1 ,例如,一个集群中有 5 个节点,那么需要收到 3 个相同的投票才能确定 Leader。
当 Leader 选举出来后,其他节点会根据 Leader 的状态来更新自己的状态,如成为 Follower 或 Observer。Follower 会与 Leader 保持同步,接收并执行 Leader 发送的指令和事务。Observer 则主要用于读取数据,不参与选举和事务的投票,但会同步 Leader 的数据,以提供数据的读取服务,减轻 Leader 和 Follower 的负担。
Map 和 flatmap 有什么区别?
在 Java 8 的 Stream API 以及一些其他的函数式编程库中,Map 和 FlatMap 是两个常用的操作符,它们有以下区别:
- 功能和操作逻辑
- Map:对集合中的每个元素应用一个指定的函数,将每个元素转换为另一个元素,然后返回一个新的集合,新集合的元素个数与原集合相同。例如,对于一个整数集合
List<Integer>
,使用map
操作将每个整数加 1 ,那么原集合中的每个元素都经过加 1 的转换后生成一个新的集合。 - FlatMap:首先对集合中的每个元素应用一个指定的函数,这个函数返回的是一个集合,然后将这些返回的集合中的所有元素合并到一个新的集合中并返回。也就是说,
flatMap
操作会将多个小集合合并成一个大集合。例如,对于一个字符串集合,每个字符串包含多个单词,使用flatMap
操作将每个字符串分割成单词的集合,然后将所有这些单词的集合合并成一个总的单词集合。
- Map:对集合中的每个元素应用一个指定的函数,将每个元素转换为另一个元素,然后返回一个新的集合,新集合的元素个数与原集合相同。例如,对于一个整数集合
- 返回值类型
- Map:返回值是一个与原集合元素个数相同的新集合,集合中每个元素是原集合元素经过转换后的结果。
- FlatMap:返回值是一个包含了多个子集合中所有元素的单一集合,其元素个数通常会多于原集合的元素个数,具体取决于每个元素经过转换后返回的子集合的元素个数总和。
- 应用场景
- Map:适用于对集合中的每个元素进行独立的、一对一的转换操作,且不改变集合的元素个数和结构。比如将一组数据的单位进行换算,或者对一组对象的某个属性进行统一的修改等场景。
- FlatMap:常用于将嵌套的集合结构扁平化处理的场景,如处理包含多个子列表的列表,将其合并为一个单一的列表;或者在处理文本数据时,将一段文本分割成单词列表,再将多个文本的单词列表合并为一个总的单词列表等情况。
Redis 有哪些数据类型和数据格式?
Redis 支持多种数据类型,每种数据类型都有其特定的数据格式和应用场景,以下是主要的数据类型:
- 字符串(String)
- 数据格式:可以是简单的字符串、整数或浮点数。字符串的长度没有明确限制,但太大的字符串会占用较多内存。
- 应用场景:常用于缓存用户的会话信息,如存储用户登录后的令牌(token);还可用于存储一些简单的配置信息,如网站的全局配置参数;也能用于实现简单的计数器,如记录网站的访问次数等。
- 列表(List)
- 数据格式:是一个有序的字符串列表,可以在列表的两端进行插入和删除操作。列表中的每个元素都是一个字符串。
- 应用场景:可用于实现消息队列,生产者将消息依次插入列表的一端,消费者从列表的另一端取出消息进行处理;还能用于存储用户的操作历史记录,如浏览历史、搜索历史等,方便用户回溯操作。
- 集合(Set)
- 数据格式:是一个无序的、不包含重复元素的字符串集合。集合中的元素是唯一的。
- 应用场景:常用于存储用户的标签信息,如用户的兴趣爱好标签;还可用于实现社交关系中的共同好友功能,通过求两个集合的交集来获取共同好友列表。
- 有序集合(Sorted Set)
- 数据格式:与集合类似,但每个元素都关联了一个分数,通过分数可以对元素进行排序。元素也是不重复的字符串。
- 应用场景:常用于实现排行榜功能,如游戏中的玩家分数排行榜,根据玩家的分数对玩家进行排序;还可用于按照时间顺序存储和排序事件,如存储系统中的日志事件,按照事件发生的时间戳作为分数进行排序。
- 哈希(Hash)
- 数据格式:是一个键值对集合,其中键和值都是字符串。可以将多个相关的字段和值存储在一个哈希中,类似于数据库中的行数据。
- 应用场景:适合存储对象的属性信息,如存储用户的详细信息,包括姓名、年龄、性别等,以用户 ID 作为哈希的键,用户的各个属性作为哈希的字段和值;也可用于存储商品的信息,如商品的名称、价格、库存等。
ZooKeeper 在项目中通常扮演什么角色,发挥怎样的作用?
ZooKeeper 在项目中扮演着多种关键角色并发挥重要作用。首先,它常作为分布式协调服务,用于管理和协调分布式系统中各个节点的工作。比如在 Hadoop 集群中,它协调 NameNode 和 DataNode 之间的工作,确保集群的正常运行和数据的一致性。其次,它可用于配置管理,将系统的配置信息集中存储在 ZooKeeper 中,各个应用节点可以从这里获取配置,当配置发生变化时,能及时通知到各个节点,实现配置的动态更新,像一些大型网站的应用配置管理就会用到。再者,它能实现分布式锁,在多个进程或线程竞争同一资源时,通过 ZooKeeper 提供的机制来实现互斥访问,保证资源的安全性和一致性。此外,还可用于服务发现,让客户端能够快速找到可用的服务节点,例如在微服务架构中,帮助服务消费者快速定位到服务提供者。
ZooKeeper 的集群如何搭建?
搭建 ZooKeeper 集群主要有以下步骤。第一步,准备多台服务器,确保服务器之间网络连通且配置基本一致。第二步,在每台服务器上安装 JDK 并配置好环境变量,因为 ZooKeeper 是用 Java 编写的,依赖于 JDK 运行。第三步,下载并解压 ZooKeeper 安装包到指定目录。第四步,配置 ZooKeeper 的配置文件 zoo.cfg ,主要配置项包括集群中各个节点的服务器地址和端口号,以及数据存储目录等。例如,设置 server.1=ip1:port1:port2,表示集群中的第一个节点的 IP 地址、选举端口和数据同步端口。第五步,为每个节点创建一个标识文件 myid,文件内容为该节点在集群中的编号,与配置文件中的 server.x 相对应。第六步,依次启动每台服务器上的 ZooKeeper 服务,通过执行启动脚本启动,然后可以通过查看日志文件或使用相关命令来检查集群是否搭建成功以及各个节点的状态。
ZooKeeper 的选举策略是怎样的?
ZooKeeper 的选举策略主要基于过半机制。当集群中的 Leader 节点出现故障或不可用时,就会触发选举过程。在选举时,每个节点都会将自己的投票信息发送给集群中的其他节点,投票信息主要包括节点的 zxid(事务 ID)和 myid 。zxid 是节点处理的最大事务 ID,它反映了节点的数据更新程度,myid 则是节点在集群中的唯一标识。节点收到其他节点的投票后,会比较投票信息,优先比较 zxid,zxid 越大说明数据越新,若 zxid 相同则比较 myid,myid 越大越优先。然后每个节点根据收到的投票信息,更新自己的投票并再次发送出去,不断重复这个过程,直到有一个节点获得超过半数节点的投票,该节点就成为新的 Leader。这样的选举策略可以保证选出的 Leader 节点具有最新的数据,并且在集群中具有较高的稳定性和可靠性,从而确保 ZooKeeper 集群的正常运行和数据的一致性。
Map 和 flatmap 有什么区别?
Map 和 flatMap 是在许多编程语言的集合操作或函数式编程中常见的操作符,它们有明显区别。Map 操作主要是对集合中的每个元素进行一对一的转换操作,它将一个集合中的每个元素通过指定的函数进行处理,然后返回一个新的集合,新集合的元素个数与原集合相同。例如,对于一个整数集合,使用 Map 操作将每个整数乘以 2,那么得到的新集合就是原集合中每个整数乘以 2 后的结果集合。而 flatMap 操作则不同,它首先对集合中的每个元素应用一个函数,这个函数返回的是一个集合,然后将这些返回的集合进行扁平化处理,最终得到一个单一的集合。比如有一个包含多个字符串的集合,对其使用 flatMap 操作,通过一个函数将每个字符串分割成单个字符的集合,最后 flatMap 会将所有这些单个字符的集合合并成一个包含所有字符的单一集合。总的来说,Map 是对每个元素进行独立的转换,而 flatMap 是先对每个元素进行集合转换然后再进行扁平化合并。
Redis 有哪些数据类型和数据格式?
Redis 支持多种数据类型和数据格式,主要包括以下几种。一是字符串类型(String),它是最基本的数据类型,可以存储任何形式的字符串,包括整数、浮点数等,其格式就是简单的字符串格式。例如,可以用字符串类型存储用户的姓名、年龄等信息。二是哈希类型(Hash),它类似于 Java 中的 HashMap,用于存储键值对集合,格式是一个包含多个键值对的哈希表。比如可以用哈希类型存储一个用户的多个属性,如姓名、年龄、地址等,通过一个用户 ID 作为键来关联这些属性。三是列表类型(List),它是一个有序的字符串列表,可以在列表的两端进行插入和删除操作,格式是一个有序的字符串序列。例如,可以用列表类型存储一个消息队列,消息按照先后顺序依次存入列表。四是集合类型(Set),它是一个无序的、不包含重复元素的集合,格式是一个包含多个元素的集合,元素之间无序且唯一。比如可以用集合类型存储一个班级中所有学生的学号,确保学号的唯一性。五是有序集合类型(Sorted Set),它与集合类似,但每个元素都关联一个分数,Redis 会根据分数对元素进行排序,格式是包含元素和对应分数的有序集合。例如,可以用有序集合存储游戏中的玩家排名,分数为玩家的游戏得分,根据得分进行排名。此外,Redis 还支持一些特殊的数据类型和格式,如 HyperLogLog 用于基数估算,Geospatial 用于地理位置信息存储等 。
RPC 的流程是怎样的
RPC(Remote Procedure Call),即远程过程调用,其核心流程主要包含以下几个关键步骤:
- 客户端流程 :
- 代理生成:客户端程序首先根据服务接口定义,通过动态代理等方式生成一个本地的代理对象。这个代理对象的作用是隐藏远程调用的细节,让客户端代码看起来就像是在调用本地方法一样。
- 方法调用:客户端代码调用代理对象的方法,传递相应的参数。此时,代理对象会负责将方法调用信息和参数进行封装。
- 数据序列化:将封装好的调用信息和参数序列化为适合在网络上传输的二进制数据格式。常见的序列化方式有 Protocol Buffers、Thrift 等。
- 网络传输:通过网络将序列化后的数据发送到远程服务器端。这通常依赖于底层的网络协议,如 TCP/IP 。
- 服务器端流程 :
- 网络接收:服务器端通过网络监听,接收客户端发送过来的序列化数据。
- 数据反序列化:将接收到的二进制数据反序列化为原始的调用信息和参数,以便后续进行方法调用。
- 方法执行:根据反序列化得到的调用信息,找到对应的服务实现类和方法,并执行该方法,同时传入反序列化后的参数。
- 结果返回:服务方法执行完毕后,将结果进行序列化,然后通过网络将序列化后的结果数据返回给客户端。
- 客户端接收处理:客户端接收到服务器返回的序列化结果数据后,进行反序列化操作,得到最终的结果,并将结果返回给调用者,至此整个 RPC 调用流程结束。
RPC 的模式和 HTTP 有什么区别?例如网站请求一个 URL 使用 HTTP,而分布式集群的内部使用了比如 RPC,其中存在什么区别?可以结合 Netty 的一些基础进行阐述
- 通信协议方面 :
- HTTP:是一种文本协议,数据以明文形式传输,通常基于 TCP/IP 协议栈。其请求和响应消息的格式是固定的,如请求行、请求头、请求体,响应状态行、响应头、响应体等,易于理解和调试,但传输效率相对较低,尤其是在传输大量数据时。
- RPC:通常使用二进制协议,如 Protocol Buffers、Thrift 等,将数据序列化为紧凑的二进制格式进行传输,这种方式传输效率高,数据体积小,适合在网络中快速传输大量数据,但可读性较差。
- 调用方式方面 :
- HTTP:通过 URL 进行调用,客户端发送 HTTP 请求到指定的 URL,服务器根据 URL 来识别和处理请求。例如,在浏览器中输入一个网址,就是向服务器发送一个 HTTP 请求。
- RPC:通过函数调用的方式进行远程调用,客户端像调用本地函数一样调用远程服务的方法,对开发者来说更加直观和自然,屏蔽了网络通信的细节。
- 参数传递方面 :
- HTTP:参数可以通过 URL 中的查询字符串、请求体等方式传递。对于 GET 请求,参数通常放在 URL 的查询字符串中;对于 POST 等请求,可以将参数放在请求体中,以表单数据、JSON 等格式传输。
- RPC:直接以函数参数的形式传递,与本地函数调用的参数传递方式相似,更加直接和高效,不需要像 HTTP 那样对参数进行额外的包装和解析。
- 接口描述方面 :
- HTTP:通常使用 RESTful 架构来描述接口,通过 HTTP 方法(GET、POST、PUT、DELETE 等)和 URL 来定义资源和对资源的操作。这种方式具有良好的可读性和可维护性,适合与各种客户端进行交互。
- RPC:一般使用接口定义语言(IDL)来描述接口,如 Protocol Buffers 的.proto 文件、Thrift 的.thrift 文件等。IDL 可以定义接口的方法签名、参数类型、返回值类型等,然后通过代码生成工具生成不同编程语言的客户端和服务器端代码,方便跨语言调用。
- 性能表现方面 :
- HTTP:由于是文本协议,解析和传输的开销相对较大,性能相对较低。但随着 HTTP/2、HTTP/3 等新版本的出现,性能有了很大提升。
- RPC:使用二进制协议和一些性能优化技术,如连接池、批处理等,通常具有更高的性能,尤其是在处理高并发、高吞吐量的场景时优势明显。
- 应用场景方面 :
- HTTP:广泛应用于 Web 应用程序,用于浏览器与服务器之间的通信,如加载网页、提交表单、获取数据等。也适用于构建 RESTful 风格的 API 服务,方便不同系统之间的集成。
- RPC:主要用于分布式系统内部的通信,如微服务架构中各个服务之间的调用,适合处理高并发、低延迟的业务场景,能够提供高效的远程调用支持 。
有场景题:假如有老师、学生、社会人士三种角色的用户请求同时打入,且请求过多无法处理,这时该如何解决?可以从消息队列、削峰以及 Ngnix 多服务器负载均衡等方面思考
- 消息队列的应用:
- 缓冲请求:引入消息队列,如 RabbitMQ、Kafka 等,将三种角色的用户请求先放入消息队列中,而不是直接由后端服务器处理。这样可以起到缓冲的作用,避免大量请求直接冲击后端服务器,导致服务器崩溃。
- 异步处理:后端服务器可以从消息队列中按照一定的规则和顺序获取请求并进行处理,实现异步处理机制。例如,可以根据请求的优先级、时间戳等因素来确定处理顺序,优先处理重要或紧急的请求。
- 削峰的策略:
- 限制流量:在系统入口处设置流量限制,根据服务器的处理能力,合理限制单位时间内的请求数量。当请求流量超过设定的阈值时,拒绝部分请求,返回友好的提示信息给用户,引导用户稍后重试。
- 平滑处理:通过算法对请求进行平滑处理,避免瞬间的流量高峰。例如,可以采用令牌桶算法或漏桶算法,控制请求的发送速率,使请求在时间上更加均匀地分布,减轻后端服务器的压力。
- Nginx 多服务器负载均衡的实施:
- 服务器集群部署:部署多台后端服务器,形成一个服务器集群。Nginx 作为负载均衡器,接收来自前端的所有用户请求。
- 负载均衡策略选择:根据实际情况选择合适的负载均衡策略。例如,可以采用轮询策略,将请求依次分配到不同的后端服务器上,保证每台服务器的负载相对均衡;也可以采用加权轮询策略,根据服务器的性能差异为不同服务器设置不同的权重,性能好的服务器分配更多的请求。