From ca7c99802bcd6ef7890a4f82e09b32ab52966f41 Mon Sep 17 00:00:00 2001 From: heibaiying <2806718453@qq.com> Date: Wed, 31 Jul 2019 17:18:07 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=98=85=E8=AF=BB=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- notes/Azkaban_Flow_1.0_的使用.md | 46 +- notes/Azkaban_Flow_2.0_的使用.md | 40 +- notes/Azkaban简介.md | 38 +- notes/Flume整合Kafka.md | 18 +- notes/Flume简介及基本使用.md | 80 +- notes/HDFS-Java-API.md | 38 +- notes/HDFS常用Shell命令.md | 22 +- notes/Hadoop-HDFS.md | 40 +- notes/Hadoop-MapReduce.md | 768 +++++++------- notes/Hadoop-YARN.md | 256 ++--- notes/Hbase_Java_API.md | 72 +- notes/Hbase_Shell.md | 30 +- notes/Hbase协处理器详解.md | 96 +- notes/Hbase容灾与备份.md | 392 ++++---- notes/Hbase的SQL中间层_Phoenix.md | 46 +- notes/Hbase简介.md | 176 ++-- notes/Hbase系统架构及数据结构.md | 100 +- notes/Hbase过滤器详解.md | 102 +- ...veCLI和Beeline命令行的基本使用.md | 62 +- notes/Hive分区表和分桶表.md | 40 +- notes/Hive常用DDL操作.md | 38 +- notes/Hive常用DML操作.md | 86 +- notes/Hive数据查询详解.md | 76 +- notes/Hive简介及核心概念.md | 84 +- notes/Hive视图和索引.md | 36 +- notes/Kafka消费者详解.md | 80 +- notes/Kafka深入理解分区副本机制.md | 72 +- notes/Kafka生产者详解.md | 60 +- notes/Kafka简介.md | 22 +- notes/Scala函数和闭包.md | 62 +- notes/Scala列表和集.md | 112 +-- notes/Scala基本数据类型和运算符.md | 70 +- notes/Scala数组.md | 52 +- notes/Scala映射和元组.md | 42 +- notes/Scala模式匹配.md | 48 +- notes/Scala流程控制语句.md | 38 +- notes/Scala简介及开发环境配置.md | 36 +- notes/Scala类和对象.md | 96 +- notes/Scala类型参数.md | 934 +++++++++--------- notes/Scala继承和特质.md | 100 +- notes/Scala隐式转换和隐式参数.md | 44 +- notes/Scala集合类型.md | 38 +- notes/SparkSQL_Dataset和DataFrame简介.md | 68 +- notes/SparkSQL外部数据源.md | 72 +- notes/SparkSQL常用聚合函数.md | 54 +- notes/SparkSQL联结操作.md | 30 +- notes/Spark_RDD.md | 92 +- notes/Spark_Streaming与流处理.md | 22 +- notes/Spark_Streaming基本操作.md | 68 +- notes/Spark_Streaming整合Flume.md | 48 +- notes/Spark_Streaming整合Kafka.md | 68 +- notes/Spark_Structured_API的基本使用.md | 50 +- notes/Spark_Transformation和Action算子.md | 82 +- notes/Spark简介.md | 188 ++-- notes/Spark累加器与广播变量.md | 210 ++-- notes/Spark部署模式与作业提交.md | 80 +- notes/Spring+Mybtais+Phoenix整合.md | 735 +++++++------- notes/Sqoop基本使用.md | 96 +- notes/Sqoop简介与安装.md | 18 +- notes/Storm三种打包方式对比分析.md | 74 +- notes/Storm和流处理简介.md | 22 +- notes/Storm核心概念详解.md | 74 +- notes/Storm编程模型详解.md | 114 +-- notes/Storm集成HBase和HDFS.md | 54 +- notes/Storm集成Kakfa.md | 44 +- notes/Storm集成Redis详解.md | 64 +- notes/Zookeeper_ACL权限控制.md | 50 +- notes/Zookeeper_Java客户端Curator.md | 52 +- notes/Zookeeper常用Shell命令.md | 48 +- notes/Zookeeper简介及核心概念.md | 80 +- .../Azkaban_3.x_编译及部署.md | 42 +- notes/installation/HBase单机环境搭建.md | 44 +- notes/installation/HBase集群环境搭建.md | 44 +- .../installation/Hadoop单机环境搭建.md | 48 +- .../installation/Hadoop集群环境搭建.md | 48 +- notes/installation/Linux下Flume的安装.md | 12 +- notes/installation/Linux下JDK安装.md | 6 +- notes/installation/Linux下Python安装.md | 10 +- .../Linux环境下Hive的安装部署.md | 34 +- notes/installation/Spark开发环境搭建.md | 38 +- notes/installation/Spark集群环境搭建.md | 36 +- notes/installation/Storm单机环境搭建.md | 12 +- notes/installation/Storm集群环境搭建.md | 28 +- ...keeper单机环境和集群环境搭建.md | 24 +- ...于Zookeeper搭建Hadoop高可用集群.md | 120 +-- ...于Zookeeper搭建Kafka高可用集群.md | 40 +- .../虚拟机静态IP及多IP配置.md | 16 +- notes/大数据学习路线.md | 36 +- notes/大数据常用软件安装指南.md | 38 +- notes/大数据应用常用打包方式.md | 66 +- notes/资料分享与工具推荐.md | 30 +- 91 files changed, 4059 insertions(+), 4058 deletions(-) diff --git a/notes/Azkaban_Flow_1.0_的使用.md b/notes/Azkaban_Flow_1.0_的使用.md index e94e214..cfac6e9 100644 --- a/notes/Azkaban_Flow_1.0_的使用.md +++ b/notes/Azkaban_Flow_1.0_的使用.md @@ -14,24 +14,24 @@ ## 一、简介 -Azkaban主要通过界面上传配置文件来进行任务的调度。它有两个重要的概念: +Azkaban 主要通过界面上传配置文件来进行任务的调度。它有两个重要的概念: - **Job**: 你需要执行的调度任务; -- **Flow**:一个获取多个Job及它们之间的依赖关系所组成的图表叫做Flow。 +- **Flow**:一个获取多个 Job 及它们之间的依赖关系所组成的图表叫做 Flow。 -目前 Azkaban 3.x 同时支持 Flow 1.0 和 Flow 2.0,本文主要讲解 Flow 1.0的使用,下一篇文章会讲解Flow 2.0的使用。 +目前 Azkaban 3.x 同时支持 Flow 1.0 和 Flow 2.0,本文主要讲解 Flow 1.0 的使用,下一篇文章会讲解 Flow 2.0 的使用。 ## 二、基本任务调度 ### 2.1 新建项目 -在Azkaban主界面可以创建对应的项目: +在 Azkaban 主界面可以创建对应的项目:
### 2.2 任务配置 -新建任务配置文件`Hello-Azkaban.job`,内容如下。这里的任务很简单,就是输出一句`'Hello Azkaban!'` : +新建任务配置文件 `Hello-Azkaban.job`,内容如下。这里的任务很简单,就是输出一句 `'Hello Azkaban!'` : ```shell #command.job @@ -41,27 +41,27 @@ command=echo 'Hello Azkaban!' ### 2.3 打包上传 -将`Hello-Azkaban.job `打包为`zip`压缩文件: +将 `Hello-Azkaban.job ` 打包为 `zip` 压缩文件:
-通过Web UI 界面上传: +通过 Web UI 界面上传:
-上传成功后可以看到对应的Flows: +上传成功后可以看到对应的 Flows:
### 2.4 执行任务 -点击页面上的`Execute Flow`执行任务: +点击页面上的 `Execute Flow` 执行任务:
### 2.5 执行结果 -点击`detail`可以查看到任务的执行日志: +点击 `detail` 可以查看到任务的执行日志:
@@ -71,7 +71,7 @@ command=echo 'Hello Azkaban!' ### 3.1 依赖配置 -这里假设我们有五个任务(TaskA——TaskE),D 任务需要在A,B,C任务执行完成后才能执行,而 E 任务则需要在 D 任务执行完成后才能执行,这种情况下需要使用`dependencies`属性定义其依赖关系。各任务配置如下: +这里假设我们有五个任务(TaskA——TaskE),D 任务需要在 A,B,C 任务执行完成后才能执行,而 E 任务则需要在 D 任务执行完成后才能执行,这种情况下需要使用 `dependencies` 属性定义其依赖关系。各任务配置如下: **Task-A.job** : @@ -112,13 +112,13 @@ dependencies=Task-D ### 3.2 压缩上传 -压缩后进行上传,这里需要注意的是一个Project只能接收一个压缩包,这里我还沿用上面的Project,默认后面的压缩包会覆盖前面的压缩包: +压缩后进行上传,这里需要注意的是一个 Project 只能接收一个压缩包,这里我还沿用上面的 Project,默认后面的压缩包会覆盖前面的压缩包:
### 3.3 依赖关系 -多个任务存在依赖时,默认采用最后一个任务的文件名作为Flow的名称,其依赖关系如图: +多个任务存在依赖时,默认采用最后一个任务的文件名作为 Flow 的名称,其依赖关系如图:
@@ -126,11 +126,11 @@ dependencies=Task-D
-从这个案例可以看出,Flow1.0无法通过一个job文件来完成多个任务的配置,但是Flow 2.0 就很好的解决了这个问题。 +从这个案例可以看出,Flow1.0 无法通过一个 job 文件来完成多个任务的配置,但是 Flow 2.0 就很好的解决了这个问题。 ## 四、调度HDFS作业 -步骤与上面的步骤一致,这里以查看HDFS上的文件列表为例。命令建议采用完整路径,配置文件如下: +步骤与上面的步骤一致,这里以查看 HDFS 上的文件列表为例。命令建议采用完整路径,配置文件如下: ```shell type=command @@ -143,7 +143,7 @@ command=/usr/app/hadoop-2.6.0-cdh5.15.2/bin/hadoop fs -ls / ## 五、调度MR作业 -MR作业配置: +MR 作业配置: ```shell type=command @@ -163,7 +163,7 @@ type=command command=/usr/app/hive-1.1.0-cdh5.15.2/bin/hive -f 'test.sql' ``` -其中`test.sql`内容如下,创建一张雇员表,然后查看其结构: +其中 `test.sql` 内容如下,创建一张雇员表,然后查看其结构: ```sql CREATE DATABASE IF NOT EXISTS hive; @@ -179,11 +179,11 @@ sal double, comm double, deptno int ) ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t'; --- 查看emp表的信息 +-- 查看 emp 表的信息 desc emp; ``` -打包的时候将`job`文件与`sql`文件一并进行打包: +打包的时候将 `job` 文件与 `sql` 文件一并进行打包:
@@ -193,11 +193,11 @@ desc emp; ## 七、在线修改作业配置 -在测试时,我们可能需要频繁修改配置,如果每次修改都要重新打包上传,这会比较麻烦。所以Azkaban支持配置的在线修改,点击需要修改的Flow,就可以进入详情页面: +在测试时,我们可能需要频繁修改配置,如果每次修改都要重新打包上传,这会比较麻烦。所以 Azkaban 支持配置的在线修改,点击需要修改的 Flow,就可以进入详情页面:
-在详情页面点击`Eidt`按钮可以进入编辑页面: +在详情页面点击 `Eidt` 按钮可以进入编辑页面:
@@ -207,7 +207,7 @@ desc emp; ## 附:可能出现的问题 -如果出现以下异常,多半是因为执行主机内存不足,Azkaban要求执行主机的可用内存必须大于3G才能执行任务: +如果出现以下异常,多半是因为执行主机内存不足,Azkaban 要求执行主机的可用内存必须大于 3G 才能执行任务: ```shell Cannot request memory (Xms 0 kb, Xmx 0 kb) from system for job @@ -215,7 +215,7 @@ Cannot request memory (Xms 0 kb, Xmx 0 kb) from system for job
-如果你的执行主机没办法增大内存,那么可以通过修改`plugins/jobtypes/`目录下的`commonprivate.properties` 文件来关闭内存检查,配置如下: +如果你的执行主机没办法增大内存,那么可以通过修改 `plugins/jobtypes/` 目录下的 `commonprivate.properties` 文件来关闭内存检查,配置如下: ```shell memCheck.enabled=false diff --git a/notes/Azkaban_Flow_2.0_的使用.md b/notes/Azkaban_Flow_2.0_的使用.md index 39eb5e7..d080b91 100644 --- a/notes/Azkaban_Flow_2.0_的使用.md +++ b/notes/Azkaban_Flow_2.0_的使用.md @@ -13,20 +13,20 @@ ### 1.1 Flow 2.0 的产生 -Azkaban 目前同时支持 Flow 1.0 和 Flow2.0 ,但是官方文档上更推荐使用Flow 2.0,因为Flow 1.0会在将来的版本被移除。Flow 2.0的主要设计思想是提供1.0所没有的流级定义。用户可以将属于给定流的所有`job / properties`文件合并到单个流定义文件中,其内容采用YAML语法进行定义,同时还支持在流中再定义流,称为为嵌入流或子流。 +Azkaban 目前同时支持 Flow 1.0 和 Flow2.0 ,但是官方文档上更推荐使用 Flow 2.0,因为 Flow 1.0 会在将来的版本被移除。Flow 2.0 的主要设计思想是提供 1.0 所没有的流级定义。用户可以将属于给定流的所有 `job / properties` 文件合并到单个流定义文件中,其内容采用 YAML 语法进行定义,同时还支持在流中再定义流,称为为嵌入流或子流。 ### 1.2 基本结构 -项目zip将包含多个流YAML文件,一个项目YAML文件以及可选库和源代码。Flow YAML文件的基本结构如下: +项目 zip 将包含多个流 YAML 文件,一个项目 YAML 文件以及可选库和源代码。Flow YAML 文件的基本结构如下: -+ 每个Flow都在单个YAML文件中定义; ++ 每个 Flow 都在单个 YAML 文件中定义; + 流文件以流名称命名,如:`my-flow-name.flow`; -+ 包含DAG中的所有节点; ++ 包含 DAG 中的所有节点; + 每个节点可以是作业或流程; -+ 每个节点 可以拥有 name, type, config, dependsOn 和 nodes sections等属性; -+ 通过列出dependsOn列表中的父节点来指定节点依赖性; ++ 每个节点 可以拥有 name, type, config, dependsOn 和 nodes sections 等属性; ++ 通过列出 dependsOn 列表中的父节点来指定节点依赖性; + 包含与流相关的其他配置; -+ 当前properties文件中流的所有常见属性都将迁移到每个流YAML文件中的config部分。 ++ 当前 properties 文件中流的所有常见属性都将迁移到每个流 YAML 文件中的 config 部分。 官方提供了一个比较完善的配置样例,如下: @@ -112,7 +112,7 @@ nodes: ## 二、YAML语法 -想要使用 Flow 2.0 进行工作流的配置,首先需要了解YAML 。YAML 是一种简洁的非标记语言,有着严格的格式要求的,如果你的格式配置失败,上传到Azkaban的时候就会抛出解析异常。 +想要使用 Flow 2.0 进行工作流的配置,首先需要了解 YAML 。YAML 是一种简洁的非标记语言,有着严格的格式要求的,如果你的格式配置失败,上传到 Azkaban 的时候就会抛出解析异常。 ### 2.1 基本规则 @@ -121,7 +121,7 @@ nodes: 3. 缩进长度没有限制,只要元素对齐就表示这些元素属于一个层级; 4. 使用#表示注释 ; 5. 字符串默认不用加单双引号,但单引号和双引号都可以使用,双引号表示不需要对特殊字符进行转义; -6. YAML中提供了多种常量结构,包括:整数,浮点数,字符串,NULL,日期,布尔,时间。 +6. YAML 中提供了多种常量结构,包括:整数,浮点数,字符串,NULL,日期,布尔,时间。 ### 2.2 对象的写法 @@ -159,20 +159,20 @@ key: 支持单引号和双引号,但双引号不会对特殊字符进行转义: ```yaml -s1: '内容\n字符串' -s2: "内容\n字符串" +s1: '内容\n 字符串' +s2: "内容\n 字符串" 转换后: -{ s1: '内容\\n字符串', s2: '内容\n字符串' } +{ s1: '内容\\n 字符串', s2: '内容\n 字符串' } ``` ### 2.6 特殊符号 -一个YAML文件中可以包括多个文档,使用`---`进行分割。 +一个 YAML 文件中可以包括多个文档,使用 `---` 进行分割。 ### 2.7 配置引用 -Flow 2.0 建议将公共参数定义在`config`下,并通过`${}`进行引用。 +Flow 2.0 建议将公共参数定义在 `config` 下,并通过 `${}` 进行引用。 @@ -180,7 +180,7 @@ Flow 2.0 建议将公共参数定义在`config`下,并通过`${}`进行引用 ### 3.1 任务配置 -新建`flow`配置文件: +新建 `flow` 配置文件: ```yaml nodes: @@ -190,7 +190,7 @@ nodes: command: echo "Hello Azkaban Flow 2.0." ``` -在当前的版本中,Azkaban同时支持 Flow 1.0 和 Flow 2.0,如果你希望以2.0的方式运行,则需要新建一个`project`文件,指明是使用的是Flow 2.0: +在当前的版本中,Azkaban 同时支持 Flow 1.0 和 Flow 2.0,如果你希望以 2.0 的方式运行,则需要新建一个 `project` 文件,指明是使用的是 Flow 2.0: ```shell azkaban-flow-version: 2.0 @@ -204,13 +204,13 @@ azkaban-flow-version: 2.0 ### 3.3 执行结果 -由于在1.0 版本中已经介绍过Web UI的使用,这里就不再赘述。对于1.0和2.0版本,只有配置方式有所不同,其他上传执行的方式都是相同的。执行结果如下: +由于在 1.0 版本中已经介绍过 Web UI 的使用,这里就不再赘述。对于 1.0 和 2.0 版本,只有配置方式有所不同,其他上传执行的方式都是相同的。执行结果如下:
## 四、多任务调度 -和1.0给出的案例一样,这里假设我们有五个任务(jobA——jobE), D 任务需要在A,B,C任务执行完成后才能执行,而 E 任务则需要在 D 任务执行完成后才能执行,相关配置文件应如下。可以看到在1.0中我们需要分别定义五个配置文件,而在2.0中我们只需要一个配置文件即可完成配置。 +和 1.0 给出的案例一样,这里假设我们有五个任务(jobA——jobE), D 任务需要在 A,B,C 任务执行完成后才能执行,而 E 任务则需要在 D 任务执行完成后才能执行,相关配置文件应如下。可以看到在 1.0 中我们需要分别定义五个配置文件,而在 2.0 中我们只需要一个配置文件即可完成配置。 ```yaml nodes: @@ -250,7 +250,7 @@ nodes: ## 五、内嵌流 -Flow2.0 支持在一个Flow中定义另一个Flow,称为内嵌流或者子流。这里给出一个内嵌流的示例,其`Flow`配置如下: +Flow2.0 支持在一个 Flow 中定义另一个 Flow,称为内嵌流或者子流。这里给出一个内嵌流的示例,其 `Flow` 配置如下: ```yaml nodes: @@ -279,7 +279,7 @@ nodes: command: echo "This is job A" ``` -内嵌流的DAG图如下: +内嵌流的 DAG 图如下:
diff --git a/notes/Azkaban简介.md b/notes/Azkaban简介.md index 00aa3f6..ef5b5d2 100644 --- a/notes/Azkaban简介.md +++ b/notes/Azkaban简介.md @@ -5,32 +5,32 @@ #### 1.1 背景 -一个完整的大数据分析系统,必然由很多任务单元(如数据收集、数据清洗、数据存储、数据分析等)组成,所有的任务单元及其之间的依赖关系组成了复杂的工作流。复杂的工作流管理涉及到很多问题: +一个完整的大数据分析系统,必然由很多任务单元 (如数据收集、数据清洗、数据存储、数据分析等) 组成,所有的任务单元及其之间的依赖关系组成了复杂的工作流。复杂的工作流管理涉及到很多问题: - 如何定时调度某个任务? - 如何在某个任务执行完成后再去执行另一个任务? - 如何在任务失败时候发出预警? - ...... -面对这些问题,工作流调度系统应运而生。Azkaban就是其中之一。 +面对这些问题,工作流调度系统应运而生。Azkaban 就是其中之一。 #### 1.2 功能 -Azkaban产生于LinkedIn,并经过多年生产环境的检验,它具备以下功能: +Azkaban 产生于 LinkedIn,并经过多年生产环境的检验,它具备以下功能: -- 兼容任何版本的Hadoop -- 易于使用的Web UI -- 可以使用简单的Web页面进行工作流上传 +- 兼容任何版本的 Hadoop +- 易于使用的 Web UI +- 可以使用简单的 Web 页面进行工作流上传 - 支持按项目进行独立管理 - 定时任务调度 - 模块化和可插入 - 身份验证和授权 - 跟踪用户操作 - 支持失败和成功的电子邮件提醒 -- SLA警报和自动查杀失败任务 +- SLA 警报和自动查杀失败任务 - 重试失败的任务 -Azkaban的设计理念是在保证功能实现的基础上兼顾易用性,其页面风格清晰明朗,下面是其WEB UI界面: +Azkaban 的设计理念是在保证功能实现的基础上兼顾易用性,其页面风格清晰明朗,下面是其 WEB UI 界面:
@@ -40,26 +40,26 @@ Azkaban 和 Oozie 都是目前使用最为广泛的工作流调度程序,其 #### 功能对比 -- 两者均可以调度Linux命令、MapReduce、Spark、Pig、Java、Hive等工作流任务; +- 两者均可以调度 Linux 命令、MapReduce、Spark、Pig、Java、Hive 等工作流任务; - 两者均可以定时执行工作流任务。 #### 工作流定义 -- Azkaban使用Properties(Flow 1.0)和YAML(Flow 2.0)文件定义工作流; -- Oozie使用Hadoop流程定义语言(hadoop process defination language,HPDL)来描述工作流,HPDL是一种XML流程定义语言。 +- Azkaban 使用 Properties(Flow 1.0) 和 YAML(Flow 2.0) 文件定义工作流; +- Oozie 使用 Hadoop 流程定义语言(hadoop process defination language,HPDL)来描述工作流,HPDL 是一种 XML 流程定义语言。 #### 资源管理 -- Azkaban有较严格的权限控制,如用户对工作流进行读/写/执行等操作; -- Oozie暂无严格的权限控制。 +- Azkaban 有较严格的权限控制,如用户对工作流进行读/写/执行等操作; +- Oozie 暂无严格的权限控制。 #### 运行模式 + Azkaban 3.x 提供了两种运行模式: - + **solo server model(单服务模式)** :元数据默认存放在内置的H2数据库(可以修改为MySQL),该模式中`webServer`(管理服务器)和 `executorServer`(执行服务器)运行在同一个进程中,进程名是`AzkabanSingleServer`。该模式适用于小规模工作流的调度。 - + **multiple-executor(分布式多服务模式)** :存放元数据的数据库为MySQL,MySQL应采用主从模式进行备份和容错。这种模式下`webServer`和`executorServer`在不同进程中运行,彼此之间互不影响,适合用于生产环境。 + + **solo server model(单服务模式)** :元数据默认存放在内置的 H2 数据库(可以修改为 MySQL),该模式中 `webServer`(管理服务器) 和 `executorServer`(执行服务器) 运行在同一个进程中,进程名是 `AzkabanSingleServer`。该模式适用于小规模工作流的调度。 + + **multiple-executor(分布式多服务模式)** :存放元数据的数据库为 MySQL,MySQL 应采用主从模式进行备份和容错。这种模式下 `webServer` 和 `executorServer` 在不同进程中运行,彼此之间互不影响,适合用于生产环境。 -+ Oozie使用Tomcat等Web容器来展示Web页面,默认使用derby存储工作流的元数据,由于derby过于轻量,实际使用中通常用MySQL代替。 ++ Oozie 使用 Tomcat 等 Web 容器来展示 Web 页面,默认使用 derby 存储工作流的元数据,由于 derby 过于轻量,实际使用中通常用 MySQL 代替。 @@ -67,10 +67,10 @@ Azkaban 和 Oozie 都是目前使用最为广泛的工作流调度程序,其 ## 三、总结 -如果你的工作流不是特别复杂,推荐使用轻量级的Azkaban,主要有以下原因: +如果你的工作流不是特别复杂,推荐使用轻量级的 Azkaban,主要有以下原因: -+ **安装方面**:Azkaban 3.0 之前都是提供安装包的,直接解压部署即可。Azkaban 3.0 之后的版本需要编译,这个编译是基于gradle的,自动化程度比较高; ++ **安装方面**:Azkaban 3.0 之前都是提供安装包的,直接解压部署即可。Azkaban 3.0 之后的版本需要编译,这个编译是基于 gradle 的,自动化程度比较高; + **页面设计**:所有任务的依赖关系、执行结果、执行日志都可以从界面上直观查看到; -+ **配置方面**:Azkaban Flow 1.0 基于Properties文件来定义工作流,这个时候的限制可能会多一点。但是在Flow 2.0 就支持了YARM。YARM语法更加灵活简单,著名的微服务框架Spring Boot就采用的YAML代替了繁重的XML。 ++ **配置方面**:Azkaban Flow 1.0 基于 Properties 文件来定义工作流,这个时候的限制可能会多一点。但是在 Flow 2.0 就支持了 YARM。YARM 语法更加灵活简单,著名的微服务框架 Spring Boot 就采用的 YAML 代替了繁重的 XML。 diff --git a/notes/Flume整合Kafka.md b/notes/Flume整合Kafka.md index f18aaac..ed362d7 100644 --- a/notes/Flume整合Kafka.md +++ b/notes/Flume整合Kafka.md @@ -15,7 +15,7 @@ 先说一下,为什么要使用 Flume + Kafka? -以实时流处理项目为例,由于采集的数据量可能存在峰值和峰谷,假设是一个电商项目,那么峰值通常出现在秒杀时,这时如果直接将Flume聚合后的数据输入到Storm等分布式计算框架中,可能就会超过集群的处理能力,这时采用Kafka就可以起到削峰的作用。Kafka天生为大数据场景而设计,具有高吞吐的特性,能很好地抗住峰值数据的冲击。 +以实时流处理项目为例,由于采集的数据量可能存在峰值和峰谷,假设是一个电商项目,那么峰值通常出现在秒杀时,这时如果直接将 Flume 聚合后的数据输入到 Storm 等分布式计算框架中,可能就会超过集群的处理能力,这时采用 Kafka 就可以起到削峰的作用。Kafka 天生为大数据场景而设计,具有高吞吐的特性,能很好地抗住峰值数据的冲击。
@@ -23,11 +23,11 @@ ## 二、整合流程 -Flume发送数据到Kafka上主要是通过`KafkaSink`来实现的,主要步骤如下: +Flume 发送数据到 Kafka 上主要是通过 `KafkaSink` 来实现的,主要步骤如下: ### 1. 启动Zookeeper和Kafka -这里启动一个单节点的Kafka作为测试: +这里启动一个单节点的 Kafka 作为测试: ```shell # 启动Zookeeper @@ -39,7 +39,7 @@ bin/kafka-server-start.sh config/server.properties ### 2. 创建主题 -创建一个主题`flume-kafka`,之后Flume收集到的数据都会发到这个主题上: +创建一个主题 `flume-kafka`,之后 Flume 收集到的数据都会发到这个主题上: ```shell # 创建主题 @@ -56,7 +56,7 @@ bin/kafka-topics.sh --zookeeper hadoop001:2181 --list ### 3. 启动kafka消费者 -启动一个消费者,监听我们刚才创建的`flume-kafka`主题: +启动一个消费者,监听我们刚才创建的 `flume-kafka` 主题: ```shell # bin/kafka-console-consumer.sh --bootstrap-server hadoop001:9092 --topic flume-kafka @@ -66,7 +66,7 @@ bin/kafka-topics.sh --zookeeper hadoop001:2181 --list ### 4. 配置Flume -新建配置文件`exec-memory-kafka.properties`,文件内容如下。这里我们监听一个名为`kafka.log`的文件,当文件内容有变化时,将新增加的内容发送到Kafka的`flume-kafka`主题上。 +新建配置文件 `exec-memory-kafka.properties`,文件内容如下。这里我们监听一个名为 `kafka.log` 的文件,当文件内容有变化时,将新增加的内容发送到 Kafka 的 `flume-kafka` 主题上。 ```properties a1.sources = s1 @@ -107,10 +107,10 @@ flume-ng agent \ ### 6. 测试 -向监听的`/tmp/kafka.log `文件中追加内容,查看Kafka消费者的输出: +向监听的 `/tmp/kafka.log ` 文件中追加内容,查看 Kafka 消费者的输出:
-可以看到`flume-kafka`主题的消费端已经收到了对应的消息: +可以看到 `flume-kafka` 主题的消费端已经收到了对应的消息: -
\ No newline at end of file +
diff --git a/notes/Flume简介及基本使用.md b/notes/Flume简介及基本使用.md index 0abd4af..41d3f22 100644 --- a/notes/Flume简介及基本使用.md +++ b/notes/Flume简介及基本使用.md @@ -15,11 +15,11 @@ ## 一、Flume简介 -Apache Flume是一个分布式,高可用的数据收集系统。它可以从不同的数据源收集数据,经过聚合后发送到存储系统中,通常用于日志数据的收集。Flume 分为 NG 和 OG (1.0 之前)两个版本,NG在OG的基础上进行了完全的重构,是目前使用最为广泛的版本。下面的介绍均以NG为基础。 +Apache Flume 是一个分布式,高可用的数据收集系统。它可以从不同的数据源收集数据,经过聚合后发送到存储系统中,通常用于日志数据的收集。Flume 分为 NG 和 OG (1.0 之前) 两个版本,NG 在 OG 的基础上进行了完全的重构,是目前使用最为广泛的版本。下面的介绍均以 NG 为基础。 ## 二、Flume架构和基本概念 -下图为Flume的基本架构图: +下图为 Flume 的基本架构图: @@ -27,7 +27,7 @@ Apache Flume是一个分布式,高可用的数据收集系统。它可以从 ### 2.1 基本架构 -外部数据源以特定格式向Flume发送`events` (事件),当`source`接收到`events`时,它将其存储到一个或多个`channel`,`channe`会一直保存`events`直到它被`sink`所消费。`sink`的主要功能从`channel`中读取`events`,并将其存入外部存储系统或转发到下一个`source`,成功后再从`channel`中移除`events`。 +外部数据源以特定格式向 Flume 发送 `events` (事件),当 `source` 接收到 `events` 时,它将其存储到一个或多个 `channel`,`channe` 会一直保存 `events` 直到它被 `sink` 所消费。`sink` 的主要功能从 `channel` 中读取 `events`,并将其存入外部存储系统或转发到下一个 `source`,成功后再从 `channel` 中移除 `events`。 @@ -35,40 +35,40 @@ Apache Flume是一个分布式,高可用的数据收集系统。它可以从 **1. Event** -`Evnet`是Flume NG数据传输的基本单元。类似于JMS和消息系统中的消息。一个`Evnet`由标题和正文组成:前者是键/值映射,后者是任意字节数组。 +`Evnet` 是 Flume NG 数据传输的基本单元。类似于 JMS 和消息系统中的消息。一个 `Evnet` 由标题和正文组成:前者是键/值映射,后者是任意字节数组。 **2. Source** -数据收集组件,从外部数据源收集数据,并存储到Channel中。 +数据收集组件,从外部数据源收集数据,并存储到 Channel 中。 **3. Channel** -`Channel`是源和接收器之间的管道,用于临时存储数据。可以是内存或持久化的文件系统: +`Channel` 是源和接收器之间的管道,用于临时存储数据。可以是内存或持久化的文件系统: -+ `Memory Channel` : 使用内存,优点是速度快,但数据可能会丢失(如突然宕机); ++ `Memory Channel` : 使用内存,优点是速度快,但数据可能会丢失 (如突然宕机); + `File Channel` : 使用持久化的文件系统,优点是能保证数据不丢失,但是速度慢。 **4. Sink** -`Sink`的主要功能从`Channel`中读取`Evnet`,并将其存入外部存储系统或将其转发到下一个`Source`,成功后再从`Channel`中移除`Event`。 +`Sink` 的主要功能从 `Channel` 中读取 `Evnet`,并将其存入外部存储系统或将其转发到下一个 `Source`,成功后再从 `Channel` 中移除 `Event`。 **5. Agent** -是一个独立的(JVM)进程,包含`Source`、 `Channel`、 `Sink`等组件。 +是一个独立的 (JVM) 进程,包含 `Source`、 `Channel`、 `Sink` 等组件。 ### 2.3 组件种类 -Flume中的每一个组件都提供了丰富的类型,适用于不同场景: +Flume 中的每一个组件都提供了丰富的类型,适用于不同场景: -- Source类型 :内置了几十种类型,如`Avro Source`,`Thrift Source`,`Kafka Source`,`JMS Source`; +- Source 类型 :内置了几十种类型,如 `Avro Source`,`Thrift Source`,`Kafka Source`,`JMS Source`; -- Sink类型 :`HDFS Sink`,`Hive Sink`,`HBaseSinks`,`Avro Sink`等; +- Sink 类型 :`HDFS Sink`,`Hive Sink`,`HBaseSinks`,`Avro Sink` 等; -- Channel类型 :`Memory Channel`,`JDBC Channel`,`Kafka Channel`,`File Channel`等。 +- Channel 类型 :`Memory Channel`,`JDBC Channel`,`Kafka Channel`,`File Channel` 等。 -对于Flume的使用,除非有特别的需求,否则通过组合内置的各种类型的Source,Sink和Channel就能满足大多数的需求。在 [Flume官网](http://flume.apache.org/releases/content/1.9.0/FlumeUserGuide.html)上对所有类型组件的配置参数均以表格的方式做了详尽的介绍,并附有配置样例;同时不同版本的参数可能略有所不同,所以使用时建议选取官网对应版本的User Guide作为主要参考资料。 +对于 Flume 的使用,除非有特别的需求,否则通过组合内置的各种类型的 Source,Sink 和 Channel 就能满足大多数的需求。在 [Flume 官网](http://flume.apache.org/releases/content/1.9.0/FlumeUserGuide.html) 上对所有类型组件的配置参数均以表格的方式做了详尽的介绍,并附有配置样例;同时不同版本的参数可能略有所不同,所以使用时建议选取官网对应版本的 User Guide 作为主要参考资料。 @@ -84,7 +84,7 @@ Flume 支持多种架构模式,分别介绍如下
-Flume支持跨越多个Agent的数据传递,这要求前一个Agent的Sink和下一个Agent的Source都必须是`Avro`类型,Sink指向Source所在主机名(或IP地址)和端口(详细配置见下文案例三)。 +Flume 支持跨越多个 Agent 的数据传递,这要求前一个 Agent 的 Sink 和下一个 Agent 的 Source 都必须是 `Avro` 类型,Sink 指向 Source 所在主机名 (或 IP 地址) 和端口(详细配置见下文案例三)。 ### 3.2 Consolidation @@ -94,21 +94,21 @@ Flume支持跨越多个Agent的数据传递,这要求前一个Agent的Sink和
-日志收集中常常存在大量的客户端(比如分布式web服务),Flume支持使用多个Agent分别收集日志,然后通过一个或者多个Agent聚合后再存储到文件系统中。 +日志收集中常常存在大量的客户端(比如分布式 web 服务),Flume 支持使用多个 Agent 分别收集日志,然后通过一个或者多个 Agent 聚合后再存储到文件系统中。 ### 3.3 Multiplexing the flow
-Flume支持从一个Source向多个Channel,也就是向多个Sink传递事件,这个操作称之为`Fan Out`(扇出)。默认情况下`Fan Out`是向所有的Channel复制`Event`,即所有Channel收到的数据都是相同的。同时Flume也支持在`Source`上自定义一个复用选择器(multiplexing selector) 来实现自定义的路由规则。 +Flume 支持从一个 Source 向多个 Channel,也就是向多个 Sink 传递事件,这个操作称之为 `Fan Out`(扇出)。默认情况下 `Fan Out` 是向所有的 Channel 复制 `Event`,即所有 Channel 收到的数据都是相同的。同时 Flume 也支持在 `Source` 上自定义一个复用选择器 (multiplexing selector) 来实现自定义的路由规则。 ## 四、Flume配置格式 -Flume配置通常需要以下两个步骤: +Flume 配置通常需要以下两个步骤: -1. 分别定义好Agent的Sources,Sinks,Channels,然后将Sources和Sinks与通道进行绑定。需要注意的是一个Source可以配置多个Channel,但一个Sink只能配置一个Channel。基本格式如下: +1. 分别定义好 Agent 的 Sources,Sinks,Channels,然后将 Sources 和 Sinks 与通道进行绑定。需要注意的是一个 Source 可以配置多个 Channel,但一个 Sink 只能配置一个 Channel。基本格式如下: ```shell .sources = @@ -122,7 +122,7 @@ Flume配置通常需要以下两个步骤: .sinks..channel = ``` -2. 分别定义Source,Sink,Channel的具体属性。基本格式如下: +2. 分别定义 Source,Sink,Channel 的具体属性。基本格式如下: ```shell @@ -139,29 +139,29 @@ Flume配置通常需要以下两个步骤: ## 五、Flume的安装部署 -为方便大家后期查阅,本仓库中所有软件的安装均单独成篇,Flume的安装见: +为方便大家后期查阅,本仓库中所有软件的安装均单独成篇,Flume 的安装见: -[Linux环境下Flume的安装部署](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Linux%E4%B8%8BFlume%E7%9A%84%E5%AE%89%E8%A3%85.md) +[Linux 环境下 Flume 的安装部署](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Linux%E4%B8%8BFlume%E7%9A%84%E5%AE%89%E8%A3%85.md) ## 六、Flume使用案例 -介绍几个Flume的使用案例: +介绍几个 Flume 的使用案例: -+ 案例一:使用Flume监听文件内容变动,将新增加的内容输出到控制台。 -+ 案例二:使用Flume监听指定目录,将目录下新增加的文件存储到HDFS。 -+ 案例三:使用Avro将本服务器收集到的日志数据发送到另外一台服务器。 ++ 案例一:使用 Flume 监听文件内容变动,将新增加的内容输出到控制台。 ++ 案例二:使用 Flume 监听指定目录,将目录下新增加的文件存储到 HDFS。 ++ 案例三:使用 Avro 将本服务器收集到的日志数据发送到另外一台服务器。 ### 6.1 案例一 需求: 监听文件内容变动,将新增加的内容输出到控制台。 -实现: 主要使用`Exec Source`配合`tail`命令实现。 +实现: 主要使用 `Exec Source` 配合 `tail` 命令实现。 #### 1. 配置 -新建配置文件`exec-memory-logger.properties`,其内容如下: +新建配置文件 `exec-memory-logger.properties`,其内容如下: ```properties #指定agent的sources,sinks,channels @@ -211,9 +211,9 @@ flume-ng agent \ ### 6.2 案例二 -需求: 监听指定目录,将目录下新增加的文件存储到HDFS。 +需求: 监听指定目录,将目录下新增加的文件存储到 HDFS。 -实现:使用`Spooling Directory Source`和`HDFS Sink`。 +实现:使用 `Spooling Directory Source` 和 `HDFS Sink`。 #### 1. 配置 @@ -257,7 +257,7 @@ flume-ng agent \ #### 3. 测试 -拷贝任意文件到监听目录下,可以从日志看到文件上传到HDFS的路径: +拷贝任意文件到监听目录下,可以从日志看到文件上传到 HDFS 的路径: ```shell # cp log.txt logs/ @@ -265,7 +265,7 @@ flume-ng agent \
-查看上传到HDFS上的文件内容与本地是否一致: +查看上传到 HDFS 上的文件内容与本地是否一致: ```shell # hdfs dfs -cat /flume/events/19-04-09/13/log.txt.1554788567801 @@ -279,11 +279,11 @@ flume-ng agent \ 需求: 将本服务器收集到的数据发送到另外一台服务器。 -实现:使用`avro sources`和`avro Sink`实现。 +实现:使用 `avro sources` 和 `avro Sink` 实现。 #### 1. 配置日志收集Flume -新建配置`netcat-memory-avro.properties`,监听文件内容变化,然后将新的文件内容通过`avro sink`发送到hadoop001这台服务器的8888端口: +新建配置 `netcat-memory-avro.properties`,监听文件内容变化,然后将新的文件内容通过 `avro sink` 发送到 hadoop001 这台服务器的 8888 端口: ```properties #指定agent的sources,sinks,channels @@ -312,7 +312,7 @@ a1.channels.c1.transactionCapacity = 100 #### 2. 配置日志聚合Flume -使用 `avro source`监听hadoop001服务器的8888端口,将获取到内容输出到控制台: +使用 `avro source` 监听 hadoop001 服务器的 8888 端口,将获取到内容输出到控制台: ```properties #指定agent的sources,sinks,channels @@ -342,7 +342,7 @@ a2.channels.c2.transactionCapacity = 100 #### 3. 启动 -启动日志聚集Flume: +启动日志聚集 Flume: ```shell flume-ng agent \ @@ -351,7 +351,7 @@ flume-ng agent \ --name a2 -Dflume.root.logger=INFO,console ``` -在启动日志收集Flume: +在启动日志收集 Flume: ```shell flume-ng agent \ @@ -360,16 +360,16 @@ flume-ng agent \ --name a1 -Dflume.root.logger=INFO,console ``` -这里建议按以上顺序启动,原因是`avro.source`会先与端口进行绑定,这样`avro sink`连接时才不会报无法连接的异常。但是即使不按顺序启动也是没关系的,`sink`会一直重试,直至建立好连接。 +这里建议按以上顺序启动,原因是 `avro.source` 会先与端口进行绑定,这样 `avro sink` 连接时才不会报无法连接的异常。但是即使不按顺序启动也是没关系的,`sink` 会一直重试,直至建立好连接。
#### 4.测试 -向文件`tmp/log.txt`中追加内容: +向文件 `tmp/log.txt` 中追加内容:
-可以看到已经从8888端口监听到内容,并成功输出到控制台: +可以看到已经从 8888 端口监听到内容,并成功输出到控制台:
diff --git a/notes/HDFS-Java-API.md b/notes/HDFS-Java-API.md index 35c2afd..f5ec70d 100644 --- a/notes/HDFS-Java-API.md +++ b/notes/HDFS-Java-API.md @@ -21,7 +21,7 @@ ## 一、 简介 -想要使用HDFS API,需要导入依赖`hadoop-client`。如果是CDH版本的Hadoop,还需要额外指明其仓库地址: +想要使用 HDFS API,需要导入依赖 `hadoop-client`。如果是 CDH 版本的 Hadoop,还需要额外指明其仓库地址: ```xml @@ -42,7 +42,7 @@ - + cloudera @@ -75,7 +75,7 @@ ### 2.1 FileSystem -FileSystem是所有HDFS操作的主入口。由于之后的每个单元测试都需要用到它,这里使用`@Before`注解进行标注。 +FileSystem 是所有 HDFS 操作的主入口。由于之后的每个单元测试都需要用到它,这里使用 `@Before` 注解进行标注。 ```java private static final String HDFS_PATH = "hdfs://192.168.0.106:8020"; @@ -86,7 +86,7 @@ private static FileSystem fileSystem; public void prepare() { try { Configuration configuration = new Configuration(); - // 这里我启动的是单节点的Hadoop,所以副本系数设置为1,默认值为3 + // 这里我启动的是单节点的 Hadoop,所以副本系数设置为 1,默认值为 3 configuration.set("dfs.replication", "1"); fileSystem = FileSystem.get(new URI(HDFS_PATH), configuration, HDFS_USER); } catch (IOException e) { @@ -122,7 +122,7 @@ public void mkDir() throws Exception { ### 2.3 创建指定权限的目录 -`FsPermission(FsAction u, FsAction g, FsAction o)` 的三个参数分别对应:创建者权限,同组其他用户权限,其他用户权限,权限值定义在`FsAction`枚举类中。 +`FsPermission(FsAction u, FsAction g, FsAction o)` 的三个参数分别对应:创建者权限,同组其他用户权限,其他用户权限,权限值定义在 `FsAction` 枚举类中。 ```java @Test @@ -178,7 +178,7 @@ public void readToString() throws Exception { } ``` -`inputStreamToString`是一个自定义方法,代码如下: +`inputStreamToString` 是一个自定义方法,代码如下: ```java /** @@ -228,8 +228,8 @@ public void rename() throws Exception { public void delete() throws Exception { /* * 第二个参数代表是否递归删除 - * + 如果path是一个目录且递归删除为true, 则删除该目录及其中所有文件; - * + 如果path是一个目录但递归删除为false,则会则抛出异常。 + * + 如果 path 是一个目录且递归删除为 true, 则删除该目录及其中所有文件; + * + 如果 path 是一个目录但递归删除为 false,则会则抛出异常。 */ boolean result = fileSystem.delete(new Path("/hdfs-api/test/b.txt"), true); System.out.println(result); @@ -268,7 +268,7 @@ public void copyFromLocalFile() throws Exception { public void progress() { fileCount++; - // progress方法每上传大约64KB的数据后就会被调用一次 + // progress 方法每上传大约 64KB 的数据后就会被调用一次 System.out.println("上传进度:" + (fileCount * 64 * 1024 / fileSize) * 100 + " %"); } }); @@ -288,11 +288,11 @@ public void copyToLocalFile() throws Exception { Path src = new Path("/hdfs-api/test/kafka.tgz"); Path dst = new Path("D:\\app\\"); /* - * 第一个参数控制下载完成后是否删除源文件,默认是true,即删除; - * 最后一个参数表示是否将RawLocalFileSystem用作本地文件系统; - * RawLocalFileSystem默认为false,通常情况下可以不设置, - * 但如果你在执行时候抛出NullPointerException异常,则代表你的文件系统与程序可能存在不兼容的情况(window下常见), - * 此时可以将RawLocalFileSystem设置为true + * 第一个参数控制下载完成后是否删除源文件,默认是 true,即删除; + * 最后一个参数表示是否将 RawLocalFileSystem 用作本地文件系统; + * RawLocalFileSystem 默认为 false,通常情况下可以不设置, + * 但如果你在执行时候抛出 NullPointerException 异常,则代表你的文件系统与程序可能存在不兼容的情况 (window 下常见), + * 此时可以将 RawLocalFileSystem 设置为 true */ fileSystem.copyToLocalFile(false, src, dst, true); } @@ -306,13 +306,13 @@ public void copyToLocalFile() throws Exception { public void listFiles() throws Exception { FileStatus[] statuses = fileSystem.listStatus(new Path("/hdfs-api")); for (FileStatus fileStatus : statuses) { - //fileStatus的toString方法被重写过,直接打印可以看到所有信息 + //fileStatus 的 toString 方法被重写过,直接打印可以看到所有信息 System.out.println(fileStatus.toString()); } } ``` -`FileStatus`中包含了文件的基本信息,比如文件路径,是否是文件夹,修改时间,访问时间,所有者,所属组,文件权限,是否是符号链接等,输出内容示例如下: +`FileStatus` 中包含了文件的基本信息,比如文件路径,是否是文件夹,修改时间,访问时间,所有者,所属组,文件权限,是否是符号链接等,输出内容示例如下: ```properties FileStatus{ @@ -373,16 +373,16 @@ public void getFileBlockLocations() throws Exception { } ``` -块输出信息有三个值,分别是文件的起始偏移量(offset),文件大小(length),块所在的主机名(hosts)。 +块输出信息有三个值,分别是文件的起始偏移量 (offset),文件大小 (length),块所在的主机名 (hosts)。 ``` 0,57028557,hadoop001 ``` -这里我上传的文件只有57M(小于128M),且程序中设置了副本系数为1,所有只有一个块信息。 +这里我上传的文件只有 57M(小于 128M),且程序中设置了副本系数为 1,所有只有一个块信息。

-**以上所有测试用例下载地址**:[HDFS Java API](https://github.com/heibaiying/BigData-Notes/tree/master/code/Hadoop/hdfs-java-api) \ No newline at end of file +**以上所有测试用例下载地址**:[HDFS Java API](https://github.com/heibaiying/BigData-Notes/tree/master/code/Hadoop/hdfs-java-api) diff --git a/notes/HDFS常用Shell命令.md b/notes/HDFS常用Shell命令.md index 2db24ad..933eceb 100644 --- a/notes/HDFS常用Shell命令.md +++ b/notes/HDFS常用Shell命令.md @@ -29,7 +29,7 @@ hadoop fs -rm hadoop fs -rm -R ``` -**4. 从本地加载文件到HDFS** +**4. 从本地加载文件到 HDFS** ```shell # 二选一执行即可 @@ -38,7 +38,7 @@ hadoop fs - copyFromLocal [localsrc] [dst] ``` -**5. 从HDFS导出文件到本地** +**5. 从 HDFS 导出文件到本地** ```shell # 二选一执行即可 @@ -78,7 +78,7 @@ hadoop fs -mv [src] [dst] **10. 统计当前目录下各文件大小** + 默认单位字节 + -s : 显示所有文件大小总和, -+ -h : 将以更友好的方式显示文件大小(例如64.0m而不是67108864) ++ -h : 将以更友好的方式显示文件大小(例如 64.0m 而不是 67108864) ```shell hadoop fs -du ``` @@ -103,7 +103,7 @@ hadoop fs -df -h / ```shell hadoop fs -setrep [-R] [-w] ``` -+ 更改文件的复制因子。如果path是目录,则更改其下所有文件的复制因子 ++ 更改文件的复制因子。如果 path 是目录,则更改其下所有文件的复制因子 + -w : 请求命令是否等待复制完成 ```shell @@ -127,13 +127,13 @@ hadoop fs -chown [-R] [OWNER][:[GROUP]] URI [URI ] hadoop fs -test - [defsz] URI ``` 可选选项: -+ -d:如果路径是目录,返回0。 -+ -e:如果路径存在,则返回0。 -+ -f:如果路径是文件,则返回0。 -+ -s:如果路径不为空,则返回0。 -+ -r:如果路径存在且授予读权限,则返回0。 -+ -w:如果路径存在且授予写入权限,则返回0。 -+ -z:如果文件长度为零,则返回0。 ++ -d:如果路径是目录,返回 0。 ++ -e:如果路径存在,则返回 0。 ++ -f:如果路径是文件,则返回 0。 ++ -s:如果路径不为空,则返回 0。 ++ -r:如果路径存在且授予读权限,则返回 0。 ++ -w:如果路径存在且授予写入权限,则返回 0。 ++ -z:如果文件长度为零,则返回 0。 ```shell # 示例 diff --git a/notes/Hadoop-HDFS.md b/notes/Hadoop-HDFS.md index 2ebc61d..fce3e7f 100644 --- a/notes/Hadoop-HDFS.md +++ b/notes/Hadoop-HDFS.md @@ -29,7 +29,7 @@ ## 一、介绍 -**HDFS** (**Hadoop Distributed File System**)是Hadoop下的分布式文件系统,具有高容错、高吞吐量等特性,可以部署在低成本的硬件上。 +**HDFS** (**Hadoop Distributed File System**)是 Hadoop 下的分布式文件系统,具有高容错、高吞吐量等特性,可以部署在低成本的硬件上。 @@ -39,40 +39,40 @@ ### 2.1 HDFS 架构 -HDFS 遵循主/从架构,由单个NameNode(NN)和多个DataNode(DN)组成: +HDFS 遵循主/从架构,由单个 NameNode(NN) 和多个 DataNode(DN) 组成: -- **NameNode** : 负责执行有关`文件系统命名空间`的操作,例如打开,关闭、重命名文件和目录等。它同时还负责集群元数据的存储,记录着文件中各个数据块的位置信息。 +- **NameNode** : 负责执行有关 ` 文件系统命名空间 ` 的操作,例如打开,关闭、重命名文件和目录等。它同时还负责集群元数据的存储,记录着文件中各个数据块的位置信息。 - **DataNode**:负责提供来自文件系统客户端的读写请求,执行块的创建,删除等操作。 ### 2.2 文件系统命名空间 -HDFS的`文件系统命名空间`的层次结构与大多数文件系统类似(如Linux), 支持目录和文件的创建、移动、删除和重命名等操作,支持配置用户和访问权限,但不支持硬链接和软连接。`NameNode`负责维护文件系统名称空间,记录对名称空间或其属性的任何更改。 +HDFS 的 ` 文件系统命名空间 ` 的层次结构与大多数文件系统类似 (如 Linux), 支持目录和文件的创建、移动、删除和重命名等操作,支持配置用户和访问权限,但不支持硬链接和软连接。`NameNode` 负责维护文件系统名称空间,记录对名称空间或其属性的任何更改。 ### 2.3 数据复制 -由于Hadoop被设计运行在廉价的机器上,这意味着硬件是不可靠的,为了保证容错性,HDFS提供了数据复制机制。HDFS 将每一个文件存储为一系列**块**,每个块由多个副本来保证容错,块的大小和复制因子可以自行配置(默认情况下,块大小是128M,默认复制因子是3)。 +由于 Hadoop 被设计运行在廉价的机器上,这意味着硬件是不可靠的,为了保证容错性,HDFS 提供了数据复制机制。HDFS 将每一个文件存储为一系列**块**,每个块由多个副本来保证容错,块的大小和复制因子可以自行配置(默认情况下,块大小是 128M,默认复制因子是 3)。
### 2.4 数据复制的实现原理 -大型的HDFS实例在通常分布在多个机架的多台服务器上,不同机架上的两台服务器之间通过交换机进行通讯。在大多数情况下,同一机架中的服务器间的网络带宽大于不同机架中的服务器之间的带宽。因此HDFS采用机架感知副本放置策略,对于常见情况,当复制因子为3时,HDFS的放置策略是: +大型的 HDFS 实例在通常分布在多个机架的多台服务器上,不同机架上的两台服务器之间通过交换机进行通讯。在大多数情况下,同一机架中的服务器间的网络带宽大于不同机架中的服务器之间的带宽。因此 HDFS 采用机架感知副本放置策略,对于常见情况,当复制因子为 3 时,HDFS 的放置策略是: -在写入程序位于`datanode`上时,就优先将写入文件的一个副本放置在该`datanode`上,否则放在随机`datanode`上。之后在另一个远程机架上的任意一个节点上放置另一个副本,并在该机架上的另一个节点上放置最后一个副本。此策略可以减少机架间的写入流量,从而提高写入性能。 +在写入程序位于 `datanode` 上时,就优先将写入文件的一个副本放置在该 `datanode` 上,否则放在随机 `datanode` 上。之后在另一个远程机架上的任意一个节点上放置另一个副本,并在该机架上的另一个节点上放置最后一个副本。此策略可以减少机架间的写入流量,从而提高写入性能。
-如果复制因子大于3,则随机确定第4个和之后副本的放置位置,同时保持每个机架的副本数量低于上限,上限值通常为`(复制系数 - 1)/机架数量 + 2`,需要注意的是不允许同一个`dataNode`上具有同一个块的多个副本。 +如果复制因子大于 3,则随机确定第 4 个和之后副本的放置位置,同时保持每个机架的副本数量低于上限,上限值通常为 `(复制系数 - 1)/机架数量 + 2`,需要注意的是不允许同一个 `dataNode` 上具有同一个块的多个副本。 ### 2.5 副本的选择 -为了最大限度地减少带宽消耗和读取延迟,HDFS在执行读取请求时,优先读取距离读取器最近的副本。如果在与读取器节点相同的机架上存在副本,则优先选择该副本。如果HDFS群集跨越多个数据中心,则优先选择本地数据中心上的副本。 +为了最大限度地减少带宽消耗和读取延迟,HDFS 在执行读取请求时,优先读取距离读取器最近的副本。如果在与读取器节点相同的机架上存在副本,则优先选择该副本。如果 HDFS 群集跨越多个数据中心,则优先选择本地数据中心上的副本。 @@ -80,17 +80,17 @@ HDFS的`文件系统命名空间`的层次结构与大多数文件系统类似( #### 1. 心跳机制和重新复制 -每个DataNode定期向NameNode发送心跳消息,如果超过指定时间没有收到心跳消息,则将DataNode标记为死亡。NameNode不会将任何新的IO请求转发给标记为死亡的DataNode,也不会再使用这些DataNode上的数据。 由于数据不再可用,可能会导致某些块的复制因子小于其指定值,NameNode会跟踪这些块,并在必要的时候进行重新复制。 +每个 DataNode 定期向 NameNode 发送心跳消息,如果超过指定时间没有收到心跳消息,则将 DataNode 标记为死亡。NameNode 不会将任何新的 IO 请求转发给标记为死亡的 DataNode,也不会再使用这些 DataNode 上的数据。 由于数据不再可用,可能会导致某些块的复制因子小于其指定值,NameNode 会跟踪这些块,并在必要的时候进行重新复制。 #### 2. 数据的完整性 -由于存储设备故障等原因,存储在DataNode上的数据块也会发生损坏。为了避免读取到已经损坏的数据而导致错误,HDFS提供了数据完整性校验机制来保证数据的完整性,具体操作如下: +由于存储设备故障等原因,存储在 DataNode 上的数据块也会发生损坏。为了避免读取到已经损坏的数据而导致错误,HDFS 提供了数据完整性校验机制来保证数据的完整性,具体操作如下: -当客户端创建HDFS文件时,它会计算文件的每个块的`校验和`,并将`校验和`存储在同一HDFS命名空间下的单独的隐藏文件中。当客户端检索文件内容时,它会验证从每个DataNode接收的数据是否与存储在关联校验和文件中的`校验和`匹配。如果匹配失败,则证明数据已经损坏,此时客户端会选择从其他DataNode获取该块的其他可用副本。 +当客户端创建 HDFS 文件时,它会计算文件的每个块的 ` 校验和 `,并将 ` 校验和 ` 存储在同一 HDFS 命名空间下的单独的隐藏文件中。当客户端检索文件内容时,它会验证从每个 DataNode 接收的数据是否与存储在关联校验和文件中的 ` 校验和 ` 匹配。如果匹配失败,则证明数据已经损坏,此时客户端会选择从其他 DataNode 获取该块的其他可用副本。 #### 3.元数据的磁盘故障 -`FsImage`和`EditLog`是HDFS的核心数据,这些数据的意外丢失可能会导致整个HDFS服务不可用。为了避免这个问题,可以配置NameNode使其支持`FsImage`和`EditLog`多副本同步,这样`FsImage`或`EditLog`的任何改变都会引起每个副本`FsImage`和`EditLog`的同步更新。 +`FsImage` 和 `EditLog` 是 HDFS 的核心数据,这些数据的意外丢失可能会导致整个 HDFS 服务不可用。为了避免这个问题,可以配置 NameNode 使其支持 `FsImage` 和 `EditLog` 多副本同步,这样 `FsImage` 或 `EditLog` 的任何改变都会引起每个副本 `FsImage` 和 `EditLog` 的同步更新。 #### 4.支持快照 @@ -102,23 +102,23 @@ HDFS的`文件系统命名空间`的层次结构与大多数文件系统类似( ### 3.1 高容错 -由于HDFS 采用数据的多副本方案,所以部分硬件的损坏不会导致全部数据的丢失。 +由于 HDFS 采用数据的多副本方案,所以部分硬件的损坏不会导致全部数据的丢失。 ### 3.2 高吞吐量 -HDFS设计的重点是支持高吞吐量的数据访问,而不是低延迟的数据访问。 +HDFS 设计的重点是支持高吞吐量的数据访问,而不是低延迟的数据访问。 ### 3.3 大文件支持 -HDFS适合于大文件的存储,文档的大小应该是是GB到TB级别的。 +HDFS 适合于大文件的存储,文档的大小应该是是 GB 到 TB 级别的。 ### 3.3 简单一致性模型 -HDFS更适合于一次写入多次读取(write-once-read-many)的访问模型。支持将内容追加到文件末尾,但不支持数据的随机访问,不能从文件任意位置新增数据。 +HDFS 更适合于一次写入多次读取 (write-once-read-many) 的访问模型。支持将内容追加到文件末尾,但不支持数据的随机访问,不能从文件任意位置新增数据。 ### 3.4 跨平台移植性 -HDFS具有良好的跨平台移植性,这使得其他大数据计算框架都将其作为数据持久化存储的首选方案。 +HDFS 具有良好的跨平台移植性,这使得其他大数据计算框架都将其作为数据持久化存储的首选方案。 @@ -156,7 +156,7 @@ HDFS具有良好的跨平台移植性,这使得其他大数据计算框架都 -**第三部分:DataNode故障处理** +**第三部分:DataNode 故障处理**
@@ -171,6 +171,6 @@ HDFS具有良好的跨平台移植性,这使得其他大数据计算框架都 ## 参考资料 1. [Apache Hadoop 2.9.2 > HDFS Architecture](http://hadoop.apache.org/docs/stable/hadoop-project-dist/hadoop-hdfs/HdfsDesign.html) -2. Tom White . hadoop权威指南 [M] . 清华大学出版社 . 2017. +2. Tom White . hadoop 权威指南 [M] . 清华大学出版社 . 2017. 3. [翻译经典 HDFS 原理讲解漫画](https://blog.csdn.net/hudiefenmu/article/details/37655491) diff --git a/notes/Hadoop-MapReduce.md b/notes/Hadoop-MapReduce.md index 7355533..4a168ec 100644 --- a/notes/Hadoop-MapReduce.md +++ b/notes/Hadoop-MapReduce.md @@ -1,384 +1,384 @@ -# 分布式计算框架——MapReduce - - - - - - -## 一、MapReduce概述 - -Hadoop MapReduce是一个分布式计算框架,用于编写批处理应用程序。编写好的程序可以提交到Hadoop集群上用于并行处理大规模的数据集。 - -MapReduce作业通过将输入的数据集拆分为独立的块,这些块由`map`以并行的方式处理,框架对`map`的输出进行排序,然后输入到`reduce`中。MapReduce框架专门用于``键值对处理,它将作业的输入视为一组``对,并生成一组``对作为输出。输出和输出的`key`和`value`都必须实现[Writable](http://hadoop.apache.org/docs/stable/api/org/apache/hadoop/io/Writable.html) 接口。 - -``` -(input) -> map -> -> combine -> -> reduce -> (output) -``` - - - -## 二、MapReduce编程模型简述 - -这里以词频统计为例进行说明,MapReduce处理的流程如下: - -
- -1. **input** : 读取文本文件; - -2. **splitting** : 将文件按照行进行拆分,此时得到的`K1`行数,`V1`表示对应行的文本内容; - -3. **mapping** : 并行将每一行按照空格进行拆分,拆分得到的`List(K2,V2)`,其中`K2`代表每一个单词,由于是做词频统计,所以`V2`的值为1,代表出现1次; -4. **shuffling**:由于`Mapping`操作可能是在不同的机器上并行处理的,所以需要通过`shuffling`将相同`key`值的数据分发到同一个节点上去合并,这样才能统计出最终的结果,此时得到`K2`为每一个单词,`List(V2)`为可迭代集合,`V2`就是Mapping中的V2; -5. **Reducing** : 这里的案例是统计单词出现的总次数,所以`Reducing`对`List(V2)`进行归约求和操作,最终输出。 - -MapReduce编程模型中`splitting` 和`shuffing`操作都是由框架实现的,需要我们自己编程实现的只有`mapping`和`reducing`,这也就是MapReduce这个称呼的来源。 - - - -## 三、combiner & partitioner - -
- -### 3.1 InputFormat & RecordReaders - -`InputFormat`将输出文件拆分为多个`InputSplit`,并由`RecordReaders`将`InputSplit`转换为标准的键值对,作为map的输出。这一步的意义在于只有先进行逻辑拆分并转为标准的键值对格式后,才能为多个`map`提供输入,以便进行并行处理。 - - - -### 3.2 Combiner - -`combiner`是`map`运算后的可选操作,它实际上是一个本地化的`reduce`操作,它主要是在`map`计算出中间文件后做一个简单的合并重复`key`值的操作。这里以词频统计为例: - -`map`在遇到一个hadoop的单词时就会记录为1,但是这篇文章里hadoop可能会出现n多次,那么`map`输出文件冗余就会很多,因此在`reduce`计算前对相同的key做一个合并操作,那么需要传输的数据量就会减少,传输效率就可以得到提升。 - -但并非所有场景都适合使用`combiner`,使用它的原则是`combiner`的输出不会影响到`reduce`计算的最终输入,例如:求总数,最大值,最小值时都可以使用`combiner`,但是做平均值计算则不能使用`combiner`。 - -不使用combiner的情况: - -
- -使用combiner的情况: - -
- - - -可以看到使用combiner的时候,需要传输到reducer中的数据由12keys,降低到10keys。降低的幅度取决于你keys的重复率,下文词频统计案例会演示用combiner降低数百倍的传输量。 - -### 3.3 Partitioner - -`partitioner`可以理解成分类器,将`map`的输出按照key值的不同分别分给对应的`reducer`,支持自定义实现,下文案例会给出演示。 - - - -## 四、MapReduce词频统计案例 - -### 4.1 项目简介 - -这里给出一个经典的词频统计的案例:统计如下样本数据中每个单词出现的次数。 - -```properties -Spark HBase -Hive Flink Storm Hadoop HBase Spark -Flink -HBase Storm -HBase Hadoop Hive Flink -HBase Flink Hive Storm -Hive Flink Hadoop -HBase Hive -Hadoop Spark HBase Storm -HBase Hadoop Hive Flink -HBase Flink Hive Storm -Hive Flink Hadoop -HBase Hive -``` - -为方便大家开发,我在项目源码中放置了一个工具类`WordCountDataUtils`,用于模拟产生词频统计的样本,生成的文件支持输出到本地或者直接写到HDFS上。 - -> 项目完整源码下载地址:[hadoop-word-count](https://github.com/heibaiying/BigData-Notes/tree/master/code/Hadoop/hadoop-word-count) - - - -### 4.2 项目依赖 - -想要进行MapReduce编程,需要导入`hadoop-client`依赖: - -```xml - - org.apache.hadoop - hadoop-client - ${hadoop.version} - -``` - -### 4.3 WordCountMapper - -将每行数据按照指定分隔符进行拆分。这里需要注意在MapReduce中必须使用Hadoop定义的类型,因为Hadoop预定义的类型都是可序列化,可比较的,所有类型均实现了`WritableComparable`接口。 - -```java -public class WordCountMapper extends Mapper { - - @Override - protected void map(LongWritable key, Text value, Context context) throws IOException, - InterruptedException { - String[] words = value.toString().split("\t"); - for (String word : words) { - context.write(new Text(word), new IntWritable(1)); - } - } - -} -``` - -`WordCountMapper`对应下图的Mapping操作: - -
- - - -`WordCountMapper`继承自`Mappe`类,这是一个泛型类,定义如下: - -```java -WordCountMapper extends Mapper - -public class Mapper { - ...... -} -``` - -+ **KEYIN** : `mapping`输入key的类型,即每行的偏移量(每行第一个字符在整个文本中的位置),`Long`类型,对应Hadoop中的`LongWritable`类型; -+ **VALUEIN** : `mapping`输入value的类型,即每行数据;`String`类型,对应Hadoop中`Text`类型; -+ **KEYOUT** :`mapping`输出的key的类型,即每个单词;`String`类型,对应Hadoop中`Text`类型; -+ **VALUEOUT**:`mapping`输出value的类型,即每个单词出现的次数;这里用`int`类型,对应`IntWritable`类型。 - - - -### 4.4 WordCountReducer - -在Reduce中进行单词出现次数的统计: - -```java -public class WordCountReducer extends Reducer { - - @Override - protected void reduce(Text key, Iterable values, Context context) throws IOException, - InterruptedException { - int count = 0; - for (IntWritable value : values) { - count += value.get(); - } - context.write(key, new IntWritable(count)); - } -} -``` - -如下图,`shuffling`的输出是reduce的输入。这里的key是每个单词,values是一个可迭代的数据类型,类似`(1,1,1,...)`。 - -
- -### 4.4 WordCountApp - -组装MapReduce作业,并提交到服务器运行,代码如下: - -```java - -/** - * 组装作业 并提交到集群运行 - */ -public class WordCountApp { - - - // 这里为了直观显示参数 使用了硬编码,实际开发中可以通过外部传参 - private static final String HDFS_URL = "hdfs://192.168.0.107:8020"; - private static final String HADOOP_USER_NAME = "root"; - - public static void main(String[] args) throws Exception { - - // 文件输入路径和输出路径由外部传参指定 - if (args.length < 2) { - System.out.println("Input and output paths are necessary!"); - return; - } - - // 需要指明hadoop用户名,否则在HDFS上创建目录时可能会抛出权限不足的异常 - System.setProperty("HADOOP_USER_NAME", HADOOP_USER_NAME); - - Configuration configuration = new Configuration(); - // 指明HDFS的地址 - configuration.set("fs.defaultFS", HDFS_URL); - - // 创建一个Job - Job job = Job.getInstance(configuration); - - // 设置运行的主类 - job.setJarByClass(WordCountApp.class); - - // 设置Mapper和Reducer - job.setMapperClass(WordCountMapper.class); - job.setReducerClass(WordCountReducer.class); - - // 设置Mapper输出key和value的类型 - job.setMapOutputKeyClass(Text.class); - job.setMapOutputValueClass(IntWritable.class); - - // 设置Reducer输出key和value的类型 - job.setOutputKeyClass(Text.class); - job.setOutputValueClass(IntWritable.class); - - // 如果输出目录已经存在,则必须先删除,否则重复运行程序时会抛出异常 - FileSystem fileSystem = FileSystem.get(new URI(HDFS_URL), configuration, HADOOP_USER_NAME); - Path outputPath = new Path(args[1]); - if (fileSystem.exists(outputPath)) { - fileSystem.delete(outputPath, true); - } - - // 设置作业输入文件和输出文件的路径 - FileInputFormat.setInputPaths(job, new Path(args[0])); - FileOutputFormat.setOutputPath(job, outputPath); - - // 将作业提交到群集并等待它完成,参数设置为true代表打印显示对应的进度 - boolean result = job.waitForCompletion(true); - - // 关闭之前创建的fileSystem - fileSystem.close(); - - // 根据作业结果,终止当前运行的Java虚拟机,退出程序 - System.exit(result ? 0 : -1); - - } -} -``` - -需要注意的是:如果不设置`Mapper`操作的输出类型,则程序默认它和`Reducer`操作输出的类型相同。 - -### 4.5 提交到服务器运行 - -在实际开发中,可以在本机配置hadoop开发环境,直接在IDE中启动进行测试。这里主要介绍一下打包提交到服务器运行。由于本项目没有使用除Hadoop外的第三方依赖,直接打包即可: - -```shell -# mvn clean package -``` - -使用以下命令提交作业: - -```shell -hadoop jar /usr/appjar/hadoop-word-count-1.0.jar \ -com.heibaiying.WordCountApp \ -/wordcount/input.txt /wordcount/output/WordCountApp -``` - -作业完成后查看HDFS上生成目录: - -```shell -# 查看目录 -hadoop fs -ls /wordcount/output/WordCountApp - -# 查看统计结果 -hadoop fs -cat /wordcount/output/WordCountApp/part-r-00000 -``` - -
- - - -## 五、词频统计案例进阶之Combiner - -### 5.1 代码实现 - -想要使用`combiner`功能只要在组装作业时,添加下面一行代码即可: - -```java -// 设置Combiner -job.setCombinerClass(WordCountReducer.class); -``` - -### 5.2 执行结果 - -加入`combiner`后统计结果是不会有变化的,但是可以从打印的日志看出`combiner`的效果: - -没有加入`combiner`的打印日志: - -
- -加入`combiner`后的打印日志如下: - -
- -这里我们只有一个输入文件并且小于128M,所以只有一个Map进行处理。可以看到经过combiner后,records由`3519`降低为`6`(样本中单词种类就只有6种),在这个用例中combiner就能极大地降低需要传输的数据量。 - - - -## 六、词频统计案例进阶之Partitioner - -### 6.1 默认的Partitioner - -这里假设有个需求:将不同单词的统计结果输出到不同文件。这种需求实际上比较常见,比如统计产品的销量时,需要将结果按照产品种类进行拆分。要实现这个功能,就需要用到自定义`Partitioner`。 - -这里先介绍下MapReduce默认的分类规则:在构建job时候,如果不指定,默认的使用的是`HashPartitioner`:对key值进行哈希散列并对`numReduceTasks`取余。其实现如下: - -```java -public class HashPartitioner extends Partitioner { - - public int getPartition(K key, V value, - int numReduceTasks) { - return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks; - } - -} -``` - -### 6.2 自定义Partitioner - -这里我们继承`Partitioner`自定义分类规则,这里按照单词进行分类: - -```java -public class CustomPartitioner extends Partitioner { - - public int getPartition(Text text, IntWritable intWritable, int numPartitions) { - return WordCountDataUtils.WORD_LIST.indexOf(text.toString()); - } -} -``` - -在构建`job`时候指定使用我们自己的分类规则,并设置`reduce`的个数: - -```java -// 设置自定义分区规则 -job.setPartitionerClass(CustomPartitioner.class); -// 设置reduce个数 -job.setNumReduceTasks(WordCountDataUtils.WORD_LIST.size()); -``` - - - -### 6.3 执行结果 - -执行结果如下,分别生成6个文件,每个文件中为对应单词的统计结果: - -
- - - - - -## 参考资料 - -1. [分布式计算框架MapReduce](https://zhuanlan.zhihu.com/p/28682581) -2. [Apache Hadoop 2.9.2 > MapReduce Tutorial](http://hadoop.apache.org/docs/stable/hadoop-mapreduce-client/hadoop-mapreduce-client-core/MapReduceTutorial.html) -3. [MapReduce - Combiners]( https://www.tutorialscampus.com/tutorials/map-reduce/combiners.htm) - - - +# 分布式计算框架——MapReduce + + + + + + +## 一、MapReduce概述 + +Hadoop MapReduce 是一个分布式计算框架,用于编写批处理应用程序。编写好的程序可以提交到 Hadoop 集群上用于并行处理大规模的数据集。 + +MapReduce 作业通过将输入的数据集拆分为独立的块,这些块由 `map` 以并行的方式处理,框架对 `map` 的输出进行排序,然后输入到 `reduce` 中。MapReduce 框架专门用于 `` 键值对处理,它将作业的输入视为一组 `` 对,并生成一组 `` 对作为输出。输出和输出的 `key` 和 `value` 都必须实现[Writable](http://hadoop.apache.org/docs/stable/api/org/apache/hadoop/io/Writable.html) 接口。 + +``` +(input) -> map -> -> combine -> -> reduce -> (output) +``` + + + +## 二、MapReduce编程模型简述 + +这里以词频统计为例进行说明,MapReduce 处理的流程如下: + +
+ +1. **input** : 读取文本文件; + +2. **splitting** : 将文件按照行进行拆分,此时得到的 `K1` 行数,`V1` 表示对应行的文本内容; + +3. **mapping** : 并行将每一行按照空格进行拆分,拆分得到的 `List(K2,V2)`,其中 `K2` 代表每一个单词,由于是做词频统计,所以 `V2` 的值为 1,代表出现 1 次; +4. **shuffling**:由于 `Mapping` 操作可能是在不同的机器上并行处理的,所以需要通过 `shuffling` 将相同 `key` 值的数据分发到同一个节点上去合并,这样才能统计出最终的结果,此时得到 `K2` 为每一个单词,`List(V2)` 为可迭代集合,`V2` 就是 Mapping 中的 V2; +5. **Reducing** : 这里的案例是统计单词出现的总次数,所以 `Reducing` 对 `List(V2)` 进行归约求和操作,最终输出。 + +MapReduce 编程模型中 `splitting` 和 `shuffing` 操作都是由框架实现的,需要我们自己编程实现的只有 `mapping` 和 `reducing`,这也就是 MapReduce 这个称呼的来源。 + + + +## 三、combiner & partitioner + +
+ +### 3.1 InputFormat & RecordReaders + +`InputFormat` 将输出文件拆分为多个 `InputSplit`,并由 `RecordReaders` 将 `InputSplit` 转换为标准的键值对,作为 map 的输出。这一步的意义在于只有先进行逻辑拆分并转为标准的键值对格式后,才能为多个 `map` 提供输入,以便进行并行处理。 + + + +### 3.2 Combiner + +`combiner` 是 `map` 运算后的可选操作,它实际上是一个本地化的 `reduce` 操作,它主要是在 `map` 计算出中间文件后做一个简单的合并重复 `key` 值的操作。这里以词频统计为例: + +`map` 在遇到一个 hadoop 的单词时就会记录为 1,但是这篇文章里 hadoop 可能会出现 n 多次,那么 `map` 输出文件冗余就会很多,因此在 `reduce` 计算前对相同的 key 做一个合并操作,那么需要传输的数据量就会减少,传输效率就可以得到提升。 + +但并非所有场景都适合使用 `combiner`,使用它的原则是 `combiner` 的输出不会影响到 `reduce` 计算的最终输入,例如:求总数,最大值,最小值时都可以使用 `combiner`,但是做平均值计算则不能使用 `combiner`。 + +不使用 combiner 的情况: + +
+ +使用 combiner 的情况: + +
+ + + +可以看到使用 combiner 的时候,需要传输到 reducer 中的数据由 12keys,降低到 10keys。降低的幅度取决于你 keys 的重复率,下文词频统计案例会演示用 combiner 降低数百倍的传输量。 + +### 3.3 Partitioner + +`partitioner` 可以理解成分类器,将 `map` 的输出按照 key 值的不同分别分给对应的 `reducer`,支持自定义实现,下文案例会给出演示。 + + + +## 四、MapReduce词频统计案例 + +### 4.1 项目简介 + +这里给出一个经典的词频统计的案例:统计如下样本数据中每个单词出现的次数。 + +```properties +Spark HBase +Hive Flink Storm Hadoop HBase Spark +Flink +HBase Storm +HBase Hadoop Hive Flink +HBase Flink Hive Storm +Hive Flink Hadoop +HBase Hive +Hadoop Spark HBase Storm +HBase Hadoop Hive Flink +HBase Flink Hive Storm +Hive Flink Hadoop +HBase Hive +``` + +为方便大家开发,我在项目源码中放置了一个工具类 `WordCountDataUtils`,用于模拟产生词频统计的样本,生成的文件支持输出到本地或者直接写到 HDFS 上。 + +> 项目完整源码下载地址:[hadoop-word-count](https://github.com/heibaiying/BigData-Notes/tree/master/code/Hadoop/hadoop-word-count) + + + +### 4.2 项目依赖 + +想要进行 MapReduce 编程,需要导入 `hadoop-client` 依赖: + +```xml + + org.apache.hadoop + hadoop-client + ${hadoop.version} + +``` + +### 4.3 WordCountMapper + +将每行数据按照指定分隔符进行拆分。这里需要注意在 MapReduce 中必须使用 Hadoop 定义的类型,因为 Hadoop 预定义的类型都是可序列化,可比较的,所有类型均实现了 `WritableComparable` 接口。 + +```java +public class WordCountMapper extends Mapper { + + @Override + protected void map(LongWritable key, Text value, Context context) throws IOException, + InterruptedException { + String[] words = value.toString().split("\t"); + for (String word : words) { + context.write(new Text(word), new IntWritable(1)); + } + } + +} +``` + +`WordCountMapper` 对应下图的 Mapping 操作: + +
+ + + +`WordCountMapper` 继承自 `Mappe` 类,这是一个泛型类,定义如下: + +```java +WordCountMapper extends Mapper + +public class Mapper { + ...... +} +``` + ++ **KEYIN** : `mapping` 输入 key 的类型,即每行的偏移量 (每行第一个字符在整个文本中的位置),`Long` 类型,对应 Hadoop 中的 `LongWritable` 类型; ++ **VALUEIN** : `mapping` 输入 value 的类型,即每行数据;`String` 类型,对应 Hadoop 中 `Text` 类型; ++ **KEYOUT** :`mapping` 输出的 key 的类型,即每个单词;`String` 类型,对应 Hadoop 中 `Text` 类型; ++ **VALUEOUT**:`mapping` 输出 value 的类型,即每个单词出现的次数;这里用 `int` 类型,对应 `IntWritable` 类型。 + + + +### 4.4 WordCountReducer + +在 Reduce 中进行单词出现次数的统计: + +```java +public class WordCountReducer extends Reducer { + + @Override + protected void reduce(Text key, Iterable values, Context context) throws IOException, + InterruptedException { + int count = 0; + for (IntWritable value : values) { + count += value.get(); + } + context.write(key, new IntWritable(count)); + } +} +``` + +如下图,`shuffling` 的输出是 reduce 的输入。这里的 key 是每个单词,values 是一个可迭代的数据类型,类似 `(1,1,1,...)`。 + +
+ +### 4.4 WordCountApp + +组装 MapReduce 作业,并提交到服务器运行,代码如下: + +```java + +/** + * 组装作业 并提交到集群运行 + */ +public class WordCountApp { + + + // 这里为了直观显示参数 使用了硬编码,实际开发中可以通过外部传参 + private static final String HDFS_URL = "hdfs://192.168.0.107:8020"; + private static final String HADOOP_USER_NAME = "root"; + + public static void main(String[] args) throws Exception { + + // 文件输入路径和输出路径由外部传参指定 + if (args.length < 2) { + System.out.println("Input and output paths are necessary!"); + return; + } + + // 需要指明 hadoop 用户名,否则在 HDFS 上创建目录时可能会抛出权限不足的异常 + System.setProperty("HADOOP_USER_NAME", HADOOP_USER_NAME); + + Configuration configuration = new Configuration(); + // 指明 HDFS 的地址 + configuration.set("fs.defaultFS", HDFS_URL); + + // 创建一个 Job + Job job = Job.getInstance(configuration); + + // 设置运行的主类 + job.setJarByClass(WordCountApp.class); + + // 设置 Mapper 和 Reducer + job.setMapperClass(WordCountMapper.class); + job.setReducerClass(WordCountReducer.class); + + // 设置 Mapper 输出 key 和 value 的类型 + job.setMapOutputKeyClass(Text.class); + job.setMapOutputValueClass(IntWritable.class); + + // 设置 Reducer 输出 key 和 value 的类型 + job.setOutputKeyClass(Text.class); + job.setOutputValueClass(IntWritable.class); + + // 如果输出目录已经存在,则必须先删除,否则重复运行程序时会抛出异常 + FileSystem fileSystem = FileSystem.get(new URI(HDFS_URL), configuration, HADOOP_USER_NAME); + Path outputPath = new Path(args[1]); + if (fileSystem.exists(outputPath)) { + fileSystem.delete(outputPath, true); + } + + // 设置作业输入文件和输出文件的路径 + FileInputFormat.setInputPaths(job, new Path(args[0])); + FileOutputFormat.setOutputPath(job, outputPath); + + // 将作业提交到群集并等待它完成,参数设置为 true 代表打印显示对应的进度 + boolean result = job.waitForCompletion(true); + + // 关闭之前创建的 fileSystem + fileSystem.close(); + + // 根据作业结果,终止当前运行的 Java 虚拟机,退出程序 + System.exit(result ? 0 : -1); + + } +} +``` + +需要注意的是:如果不设置 `Mapper` 操作的输出类型,则程序默认它和 `Reducer` 操作输出的类型相同。 + +### 4.5 提交到服务器运行 + +在实际开发中,可以在本机配置 hadoop 开发环境,直接在 IDE 中启动进行测试。这里主要介绍一下打包提交到服务器运行。由于本项目没有使用除 Hadoop 外的第三方依赖,直接打包即可: + +```shell +# mvn clean package +``` + +使用以下命令提交作业: + +```shell +hadoop jar /usr/appjar/hadoop-word-count-1.0.jar \ +com.heibaiying.WordCountApp \ +/wordcount/input.txt /wordcount/output/WordCountApp +``` + +作业完成后查看 HDFS 上生成目录: + +```shell +# 查看目录 +hadoop fs -ls /wordcount/output/WordCountApp + +# 查看统计结果 +hadoop fs -cat /wordcount/output/WordCountApp/part-r-00000 +``` + +
+ + + +## 五、词频统计案例进阶之Combiner + +### 5.1 代码实现 + +想要使用 `combiner` 功能只要在组装作业时,添加下面一行代码即可: + +```java +// 设置 Combiner +job.setCombinerClass(WordCountReducer.class); +``` + +### 5.2 执行结果 + +加入 `combiner` 后统计结果是不会有变化的,但是可以从打印的日志看出 `combiner` 的效果: + +没有加入 `combiner` 的打印日志: + +
+ +加入 `combiner` 后的打印日志如下: + +
+ +这里我们只有一个输入文件并且小于 128M,所以只有一个 Map 进行处理。可以看到经过 combiner 后,records 由 `3519` 降低为 `6`(样本中单词种类就只有 6 种),在这个用例中 combiner 就能极大地降低需要传输的数据量。 + + + +## 六、词频统计案例进阶之Partitioner + +### 6.1 默认的Partitioner + +这里假设有个需求:将不同单词的统计结果输出到不同文件。这种需求实际上比较常见,比如统计产品的销量时,需要将结果按照产品种类进行拆分。要实现这个功能,就需要用到自定义 `Partitioner`。 + +这里先介绍下 MapReduce 默认的分类规则:在构建 job 时候,如果不指定,默认的使用的是 `HashPartitioner`:对 key 值进行哈希散列并对 `numReduceTasks` 取余。其实现如下: + +```java +public class HashPartitioner extends Partitioner { + + public int getPartition(K key, V value, + int numReduceTasks) { + return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks; + } + +} +``` + +### 6.2 自定义Partitioner + +这里我们继承 `Partitioner` 自定义分类规则,这里按照单词进行分类: + +```java +public class CustomPartitioner extends Partitioner { + + public int getPartition(Text text, IntWritable intWritable, int numPartitions) { + return WordCountDataUtils.WORD_LIST.indexOf(text.toString()); + } +} +``` + +在构建 `job` 时候指定使用我们自己的分类规则,并设置 `reduce` 的个数: + +```java +// 设置自定义分区规则 +job.setPartitionerClass(CustomPartitioner.class); +// 设置 reduce 个数 +job.setNumReduceTasks(WordCountDataUtils.WORD_LIST.size()); +``` + + + +### 6.3 执行结果 + +执行结果如下,分别生成 6 个文件,每个文件中为对应单词的统计结果: + +
+ + + + + +## 参考资料 + +1. [分布式计算框架 MapReduce](https://zhuanlan.zhihu.com/p/28682581) +2. [Apache Hadoop 2.9.2 > MapReduce Tutorial](http://hadoop.apache.org/docs/stable/hadoop-mapreduce-client/hadoop-mapreduce-client-core/MapReduceTutorial.html) +3. [MapReduce - Combiners]( https://www.tutorialscampus.com/tutorials/map-reduce/combiners.htm) + + + diff --git a/notes/Hadoop-YARN.md b/notes/Hadoop-YARN.md index ca03678..3024895 100644 --- a/notes/Hadoop-YARN.md +++ b/notes/Hadoop-YARN.md @@ -1,128 +1,128 @@ -# 集群资源管理器——YARN - - - - - -## 一、hadoop yarn 简介 - -**Apache YARN** (Yet Another Resource Negotiator) 是hadoop 2.0 引入的集群资源管理系统。用户可以将各种服务框架部署在YARN上,由YARN进行统一地管理和资源分配。 - -
- - - -## 二、YARN架构 - -
- -### 1. ResourceManager - -`ResourceManager`通常在独立的机器上以后台进程的形式运行,它是整个集群资源的主要协调者和管理者。`ResourceManager`负责给用户提交的所有应用程序分配资源,它根据应用程序优先级、队列容量、ACLs、数据位置等信息,做出决策,然后以共享的、安全的、多租户的方式制定分配策略,调度集群资源。 - -### 2. NodeManager - -`NodeManager`是YARN集群中的每个具体节点的管理者。主要负责该节点内所有容器的生命周期的管理,监视资源和跟踪节点健康。具体如下: - -- 启动时向`ResourceManager`注册并定时发送心跳消息,等待`ResourceManager`的指令; -- 维护`Container`的生命周期,监控`Container`的资源使用情况; -- 管理任务运行时的相关依赖,根据`ApplicationMaster`的需要,在启动`Container`之前将需要的程序及其依赖拷贝到本地。 - -### 3. ApplicationMaster - -在用户提交一个应用程序时,YARN会启动一个轻量级的进程`ApplicationMaster`。`ApplicationMaster`负责协调来自 `ResourceManager`的资源,并通过`NodeManager` 监视容器内资源的使用情况,同时还负责任务的监控与容错。具体如下: - -- 根据应用的运行状态来决定动态计算资源需求; -- 向`ResourceManager`申请资源,监控申请的资源的使用情况; -- 跟踪任务状态和进度,报告资源的使用情况和应用的进度信息; -- 负责任务的容错。 - -### 4. Contain - -`Container`是YARN中的资源抽象,它封装了某个节点上的多维度资源,如内存、CPU、磁盘、网络等。当AM向RM申请资源时,RM为AM返回的资源是用`Container`表示的。YARN会为每个任务分配一个`Container`,该任务只能使用该`Container`中描述的资源。`ApplicationMaster`可在`Container`内运行任何类型的任务。例如,`MapReduce ApplicationMaster`请求一个容器来启动 map 或 reduce 任务,而`Giraph ApplicationMaster`请求一个容器来运行 Giraph 任务。 - - - - - -## 三、YARN工作原理简述 - -
- -1. `Client`提交作业到YARN上; - -2. `Resource Manager`选择一个`Node Manager`,启动一个`Container`并运行`Application Master`实例; - -3. `Application Master`根据实际需要向`Resource Manager`请求更多的`Container`资源(如果作业很小, 应用管理器会选择在其自己的JVM中运行任务); - -4. `Application Master`通过获取到的`Container`资源执行分布式计算。 - - - -## 四、YARN工作原理详述 - -
- - - -#### 1. 作业提交 - -client调用job.waitForCompletion方法,向整个集群提交MapReduce作业 (第1步) 。新的作业ID(应用ID)由资源管理器分配(第2步)。作业的client核实作业的输出, 计算输入的split, 将作业的资源(包括Jar包,配置文件, split信息)拷贝给HDFS(第3步)。 最后, 通过调用资源管理器的submitApplication()来提交作业(第4步)。 - -#### 2. 作业初始化 - -当资源管理器收到submitApplciation()的请求时, 就将该请求发给调度器(scheduler), 调度器分配container, 然后资源管理器在该container内启动应用管理器进程, 由节点管理器监控(第5步)。 - -MapReduce作业的应用管理器是一个主类为MRAppMaster的Java应用,其通过创造一些bookkeeping对象来监控作业的进度, 得到任务的进度和完成报告(第6步)。然后其通过分布式文件系统得到由客户端计算好的输入split(第7步),然后为每个输入split创建一个map任务, 根据mapreduce.job.reduces创建reduce任务对象。 - -#### 3. 任务分配 - -如果作业很小, 应用管理器会选择在其自己的JVM中运行任务。 - -如果不是小作业, 那么应用管理器向资源管理器请求container来运行所有的map和reduce任务(第8步)。这些请求是通过心跳来传输的, 包括每个map任务的数据位置,比如存放输入split的主机名和机架(rack),调度器利用这些信息来调度任务,尽量将任务分配给存储数据的节点, 或者分配给和存放输入split的节点相同机架的节点。 - -#### 4. 任务运行 - -当一个任务由资源管理器的调度器分配给一个container后,应用管理器通过联系节点管理器来启动container(第9步)。任务由一个主类为YarnChild的Java应用执行, 在运行任务之前首先本地化任务需要的资源,比如作业配置,JAR文件, 以及分布式缓存的所有文件(第10步。 最后, 运行map或reduce任务(第11步)。 - -YarnChild运行在一个专用的JVM中, 但是YARN不支持JVM重用。 - -#### 5. 进度和状态更新 - -YARN中的任务将其进度和状态(包括counter)返回给应用管理器, 客户端每秒(通mapreduce.client.progressmonitor.pollinterval设置)向应用管理器请求进度更新, 展示给用户。 - -#### 6. 作业完成 - -除了向应用管理器请求作业进度外, 客户端每5分钟都会通过调用waitForCompletion()来检查作业是否完成,时间间隔可以通过mapreduce.client.completion.pollinterval来设置。作业完成之后, 应用管理器和container会清理工作状态, OutputCommiter的作业清理方法也会被调用。作业的信息会被作业历史服务器存储以备之后用户核查。 - - - -## 五、提交作业到YARN上运行 - -这里以提交Hadoop Examples中计算Pi的MApReduce程序为例,相关Jar包在Hadoop安装目录的`share/hadoop/mapreduce`目录下: - -```shell -# 提交格式: hadoop jar jar包路径 主类名称 主类参数 -# hadoop jar hadoop-mapreduce-examples-2.6.0-cdh5.15.2.jar pi 3 3 -``` - - - -## 参考资料 - -1. [初步掌握Yarn的架构及原理](https://www.cnblogs.com/codeOfLife/p/5492740.html) - -2. [Apache Hadoop 2.9.2 > Apache Hadoop YARN](http://hadoop.apache.org/docs/stable/hadoop-yarn/hadoop-yarn-site/YARN.html) - - - +# 集群资源管理器——YARN + + + + + +## 一、hadoop yarn 简介 + +**Apache YARN** (Yet Another Resource Negotiator) 是 hadoop 2.0 引入的集群资源管理系统。用户可以将各种服务框架部署在 YARN 上,由 YARN 进行统一地管理和资源分配。 + +
+ + + +## 二、YARN架构 + +
+ +### 1. ResourceManager + +`ResourceManager` 通常在独立的机器上以后台进程的形式运行,它是整个集群资源的主要协调者和管理者。`ResourceManager` 负责给用户提交的所有应用程序分配资源,它根据应用程序优先级、队列容量、ACLs、数据位置等信息,做出决策,然后以共享的、安全的、多租户的方式制定分配策略,调度集群资源。 + +### 2. NodeManager + +`NodeManager` 是 YARN 集群中的每个具体节点的管理者。主要负责该节点内所有容器的生命周期的管理,监视资源和跟踪节点健康。具体如下: + +- 启动时向 `ResourceManager` 注册并定时发送心跳消息,等待 `ResourceManager` 的指令; +- 维护 `Container` 的生命周期,监控 `Container` 的资源使用情况; +- 管理任务运行时的相关依赖,根据 `ApplicationMaster` 的需要,在启动 `Container` 之前将需要的程序及其依赖拷贝到本地。 + +### 3. ApplicationMaster + +在用户提交一个应用程序时,YARN 会启动一个轻量级的进程 `ApplicationMaster`。`ApplicationMaster` 负责协调来自 `ResourceManager` 的资源,并通过 `NodeManager` 监视容器内资源的使用情况,同时还负责任务的监控与容错。具体如下: + +- 根据应用的运行状态来决定动态计算资源需求; +- 向 `ResourceManager` 申请资源,监控申请的资源的使用情况; +- 跟踪任务状态和进度,报告资源的使用情况和应用的进度信息; +- 负责任务的容错。 + +### 4. Contain + +`Container` 是 YARN 中的资源抽象,它封装了某个节点上的多维度资源,如内存、CPU、磁盘、网络等。当 AM 向 RM 申请资源时,RM 为 AM 返回的资源是用 `Container` 表示的。YARN 会为每个任务分配一个 `Container`,该任务只能使用该 `Container` 中描述的资源。`ApplicationMaster` 可在 `Container` 内运行任何类型的任务。例如,`MapReduce ApplicationMaster` 请求一个容器来启动 map 或 reduce 任务,而 `Giraph ApplicationMaster` 请求一个容器来运行 Giraph 任务。 + + + + + +## 三、YARN工作原理简述 + +
+ +1. `Client` 提交作业到 YARN 上; + +2. `Resource Manager` 选择一个 `Node Manager`,启动一个 `Container` 并运行 `Application Master` 实例; + +3. `Application Master` 根据实际需要向 `Resource Manager` 请求更多的 `Container` 资源(如果作业很小, 应用管理器会选择在其自己的 JVM 中运行任务); + +4. `Application Master` 通过获取到的 `Container` 资源执行分布式计算。 + + + +## 四、YARN工作原理详述 + +
+ + + +#### 1. 作业提交 + +client 调用 job.waitForCompletion 方法,向整个集群提交 MapReduce 作业 (第 1 步) 。新的作业 ID(应用 ID) 由资源管理器分配 (第 2 步)。作业的 client 核实作业的输出, 计算输入的 split, 将作业的资源 (包括 Jar 包,配置文件, split 信息) 拷贝给 HDFS(第 3 步)。 最后, 通过调用资源管理器的 submitApplication() 来提交作业 (第 4 步)。 + +#### 2. 作业初始化 + +当资源管理器收到 submitApplciation() 的请求时, 就将该请求发给调度器 (scheduler), 调度器分配 container, 然后资源管理器在该 container 内启动应用管理器进程, 由节点管理器监控 (第 5 步)。 + +MapReduce 作业的应用管理器是一个主类为 MRAppMaster 的 Java 应用,其通过创造一些 bookkeeping 对象来监控作业的进度, 得到任务的进度和完成报告 (第 6 步)。然后其通过分布式文件系统得到由客户端计算好的输入 split(第 7 步),然后为每个输入 split 创建一个 map 任务, 根据 mapreduce.job.reduces 创建 reduce 任务对象。 + +#### 3. 任务分配 + +如果作业很小, 应用管理器会选择在其自己的 JVM 中运行任务。 + +如果不是小作业, 那么应用管理器向资源管理器请求 container 来运行所有的 map 和 reduce 任务 (第 8 步)。这些请求是通过心跳来传输的, 包括每个 map 任务的数据位置,比如存放输入 split 的主机名和机架 (rack),调度器利用这些信息来调度任务,尽量将任务分配给存储数据的节点, 或者分配给和存放输入 split 的节点相同机架的节点。 + +#### 4. 任务运行 + +当一个任务由资源管理器的调度器分配给一个 container 后,应用管理器通过联系节点管理器来启动 container(第 9 步)。任务由一个主类为 YarnChild 的 Java 应用执行, 在运行任务之前首先本地化任务需要的资源,比如作业配置,JAR 文件, 以及分布式缓存的所有文件 (第 10 步。 最后, 运行 map 或 reduce 任务 (第 11 步)。 + +YarnChild 运行在一个专用的 JVM 中, 但是 YARN 不支持 JVM 重用。 + +#### 5. 进度和状态更新 + +YARN 中的任务将其进度和状态 (包括 counter) 返回给应用管理器, 客户端每秒 (通 mapreduce.client.progressmonitor.pollinterval 设置) 向应用管理器请求进度更新, 展示给用户。 + +#### 6. 作业完成 + +除了向应用管理器请求作业进度外, 客户端每 5 分钟都会通过调用 waitForCompletion() 来检查作业是否完成,时间间隔可以通过 mapreduce.client.completion.pollinterval 来设置。作业完成之后, 应用管理器和 container 会清理工作状态, OutputCommiter 的作业清理方法也会被调用。作业的信息会被作业历史服务器存储以备之后用户核查。 + + + +## 五、提交作业到YARN上运行 + +这里以提交 Hadoop Examples 中计算 Pi 的 MApReduce 程序为例,相关 Jar 包在 Hadoop 安装目录的 `share/hadoop/mapreduce` 目录下: + +```shell +# 提交格式: hadoop jar jar包路径 主类名称 主类参数 +# hadoop jar hadoop-mapreduce-examples-2.6.0-cdh5.15.2.jar pi 3 3 +``` + + + +## 参考资料 + +1. [初步掌握 Yarn 的架构及原理](https://www.cnblogs.com/codeOfLife/p/5492740.html) + +2. [Apache Hadoop 2.9.2 > Apache Hadoop YARN](http://hadoop.apache.org/docs/stable/hadoop-yarn/hadoop-yarn-site/YARN.html) + + + diff --git a/notes/Hbase_Java_API.md b/notes/Hbase_Java_API.md index 5792b0c..09c69b1 100644 --- a/notes/Hbase_Java_API.md +++ b/notes/Hbase_Java_API.md @@ -11,19 +11,19 @@ ## 一、简述 -截至到目前(2019.04),HBase 有两个主要的版本,分别是1.x 和 2.x ,两个版本的Java API有所不同,1.x 中某些方法在2.x中被标识为`@deprecated`过时。所以下面关于API的样例,我会分别给出1.x和2.x两个版本。完整的代码见本仓库: +截至到目前 (2019.04),HBase 有两个主要的版本,分别是 1.x 和 2.x ,两个版本的 Java API 有所不同,1.x 中某些方法在 2.x 中被标识为 `@deprecated` 过时。所以下面关于 API 的样例,我会分别给出 1.x 和 2.x 两个版本。完整的代码见本仓库: >+ [Java API 1.x Examples](https://github.com/heibaiying/BigData-Notes/tree/master/code/Hbase/hbase-java-api-1.x) > >+ [Java API 2.x Examples](https://github.com/heibaiying/BigData-Notes/tree/master/code/Hbase/hbase-java-api-2.x) -同时你使用的客户端的版本必须与服务端版本保持一致,如果用2.x版本的客户端代码去连接1.x版本的服务端,会抛出`NoSuchColumnFamilyException`等异常。 +同时你使用的客户端的版本必须与服务端版本保持一致,如果用 2.x 版本的客户端代码去连接 1.x 版本的服务端,会抛出 `NoSuchColumnFamilyException` 等异常。 ## 二、Java API 1.x 基本使用 #### 2.1 新建Maven工程,导入项目依赖 -要使用Java API 操作HBase,需要引入`hbase-client`。这里选取的`HBase Client`的版本为`1.2.0`。 +要使用 Java API 操作 HBase,需要引入 `hbase-client`。这里选取的 `HBase Client` 的版本为 `1.2.0`。 ```xml @@ -53,7 +53,7 @@ public class HBaseUtils { } /** - * 创建HBase表 + * 创建 HBase 表 * * @param tableName 表名 * @param columnFamilies 列族的数组 @@ -79,7 +79,7 @@ public class HBaseUtils { /** - * 删除hBase表 + * 删除 hBase 表 * * @param tableName 表名 */ @@ -142,7 +142,7 @@ public class HBaseUtils { /** - * 根据rowKey获取指定行的数据 + * 根据 rowKey 获取指定行的数据 * * @param tableName 表名 * @param rowKey 唯一标识 @@ -160,7 +160,7 @@ public class HBaseUtils { /** - * 获取指定行指定列(cell)的最新版本的数据 + * 获取指定行指定列 (cell) 的最新版本的数据 * * @param tableName 表名 * @param rowKey 唯一标识 @@ -227,8 +227,8 @@ public class HBaseUtils { * 检索表中指定数据 * * @param tableName 表名 - * @param startRowKey 起始RowKey - * @param endRowKey 终止RowKey + * @param startRowKey 起始 RowKey + * @param endRowKey 终止 RowKey * @param filterList 过滤器 */ @@ -292,7 +292,7 @@ public class HBaseUtils { ### 2.3 单元测试 -以单元测试的方式对上面封装的API进行测试。 +以单元测试的方式对上面封装的 API 进行测试。 ```java public class HBaseUtilsTest { @@ -396,7 +396,7 @@ public class HBaseUtilsTest { #### 3.1 新建Maven工程,导入项目依赖 -这里选取的`HBase Client`的版本为最新的`2.1.4`。 +这里选取的 `HBase Client` 的版本为最新的 `2.1.4`。 ```xml @@ -408,13 +408,13 @@ public class HBaseUtilsTest { #### 3.2 API 的基本使用 -2.x 版本相比于1.x 废弃了一部分方法,关于废弃的方法在源码中都会指明新的替代方法,比如,在2.x中创建表时:`HTableDescriptor`和`HColumnDescriptor`等类都标识为废弃,取而代之的是使用`TableDescriptorBuilder`和`ColumnFamilyDescriptorBuilder`来定义表和列族。 +2.x 版本相比于 1.x 废弃了一部分方法,关于废弃的方法在源码中都会指明新的替代方法,比如,在 2.x 中创建表时:`HTableDescriptor` 和 `HColumnDescriptor` 等类都标识为废弃,取而代之的是使用 `TableDescriptorBuilder` 和 `ColumnFamilyDescriptorBuilder` 来定义表和列族。
-以下为HBase 2.x 版本Java API的使用示例: +以下为 HBase 2.x 版本 Java API 的使用示例: ```java public class HBaseUtils { @@ -434,7 +434,7 @@ public class HBaseUtils { } /** - * 创建HBase表 + * 创建 HBase 表 * * @param tableName 表名 * @param columnFamilies 列族的数组 @@ -461,7 +461,7 @@ public class HBaseUtils { /** - * 删除hBase表 + * 删除 hBase 表 * * @param tableName 表名 */ @@ -524,7 +524,7 @@ public class HBaseUtils { /** - * 根据rowKey获取指定行的数据 + * 根据 rowKey 获取指定行的数据 * * @param tableName 表名 * @param rowKey 唯一标识 @@ -542,7 +542,7 @@ public class HBaseUtils { /** - * 获取指定行指定列(cell)的最新版本的数据 + * 获取指定行指定列 (cell) 的最新版本的数据 * * @param tableName 表名 * @param rowKey 唯一标识 @@ -609,8 +609,8 @@ public class HBaseUtils { * 检索表中指定数据 * * @param tableName 表名 - * @param startRowKey 起始RowKey - * @param endRowKey 终止RowKey + * @param startRowKey 起始 RowKey + * @param endRowKey 终止 RowKey * @param filterList 过滤器 */ @@ -676,9 +676,9 @@ public class HBaseUtils { ## 四、正确连接Hbase -在上面的代码中,在类加载时就初始化了Connection连接,并且之后的方法都是复用这个Connection,这时我们可能会考虑是否可以使用自定义连接池来获取更好的性能表现?实际上这是没有必要的。 +在上面的代码中,在类加载时就初始化了 Connection 连接,并且之后的方法都是复用这个 Connection,这时我们可能会考虑是否可以使用自定义连接池来获取更好的性能表现?实际上这是没有必要的。 -首先官方对于`Connection`的使用说明如下: +首先官方对于 `Connection` 的使用说明如下: ```properties Connection Pooling For applications which require high-end multithreaded @@ -686,8 +686,8 @@ access (e.g., web-servers or application servers that may serve many application threads in a single JVM), you can pre-create a Connection, as shown in the following example: -对于高并发多线程访问的应用程序(例如,在单个JVM中存在的为多个线程服务的Web服务器或应用程序服务器), -您只需要预先创建一个Connection。例子如下: +对于高并发多线程访问的应用程序(例如,在单个 JVM 中存在的为多个线程服务的 Web 服务器或应用程序服务器), +您只需要预先创建一个 Connection。例子如下: // Create a connection to the cluster. Configuration conf = HBaseConfiguration.create(); @@ -697,7 +697,7 @@ try (Connection connection = ConnectionFactory.createConnection(conf); } ``` -之所以能这样使用,这是因为Connection并不是一个简单的socket连接,[接口文档](https://hbase.apache.org/apidocs/org/apache/hadoop/hbase/client/Connection.html)中对Connection的表述是: +之所以能这样使用,这是因为 Connection 并不是一个简单的 socket 连接,[接口文档](https://hbase.apache.org/apidocs/org/apache/hadoop/hbase/client/Connection.html) 中对 Connection 的表述是: ```properties A cluster connection encapsulating lower level individual connections to actual servers and a @@ -705,25 +705,25 @@ connection to zookeeper. Connections are instantiated through the ConnectionFac The lifecycle of the connection is managed by the caller, who has to close() the connection to release the resources. -Connection是一个集群连接,封装了与多台服务器(Matser/Region Server)的底层连接以及与zookeeper的连接。 -连接通过ConnectionFactory 类实例化。连接的生命周期由调用者管理,调用者必须使用close()关闭连接以释放资源。 +Connection 是一个集群连接,封装了与多台服务器(Matser/Region Server)的底层连接以及与 zookeeper 的连接。 +连接通过 ConnectionFactory 类实例化。连接的生命周期由调用者管理,调用者必须使用 close() 关闭连接以释放资源。 ``` -之所以封装这些连接,是因为HBase客户端需要连接三个不同的服务角色: +之所以封装这些连接,是因为 HBase 客户端需要连接三个不同的服务角色: -+ **Zookeeper** :主要用于获取`meta`表的位置信息,Master的信息; -+ **HBase Master** :主要用于执行HBaseAdmin接口的一些操作,例如建表等; ++ **Zookeeper** :主要用于获取 `meta` 表的位置信息,Master 的信息; ++ **HBase Master** :主要用于执行 HBaseAdmin 接口的一些操作,例如建表等; + **HBase RegionServer** :用于读、写数据。
-Connection对象和实际的Socket连接之间的对应关系如下图: +Connection 对象和实际的 Socket 连接之间的对应关系如下图:
-> 上面两张图片引用自博客:[连接HBase的正确姿势](https://yq.aliyun.com/articles/581702?spm=a2c4e.11157919.spm-cont-list.1.146c27aeFxoMsN%20%E8%BF%9E%E6%8E%A5HBase%E7%9A%84%E6%AD%A3%E7%A1%AE%E5%A7%BF%E5%8A%BF) +> 上面两张图片引用自博客:[连接 HBase 的正确姿势](https://yq.aliyun.com/articles/581702?spm=a2c4e.11157919.spm-cont-list.1.146c27aeFxoMsN%20%E8%BF%9E%E6%8E%A5HBase%E7%9A%84%E6%AD%A3%E7%A1%AE%E5%A7%BF%E5%8A%BF) -在HBase客户端代码中,真正对应Socket连接的是`RpcConnection`对象。HBase使用`PoolMap`这种数据结构来存储客户端到HBase服务器之间的连接。`PoolMap`的内部有一个`ConcurrentHashMap`实例,其key是`ConnectionId`(封装了服务器地址和用户ticket),value是一个`RpcConnection`对象的资源池。当HBase需要连接一个服务器时,首先会根据`ConnectionId`找到对应的连接池,然后从连接池中取出一个连接对象。 +在 HBase 客户端代码中,真正对应 Socket 连接的是 `RpcConnection` 对象。HBase 使用 `PoolMap` 这种数据结构来存储客户端到 HBase 服务器之间的连接。`PoolMap` 的内部有一个 `ConcurrentHashMap` 实例,其 key 是 `ConnectionId`(封装了服务器地址和用户 ticket),value 是一个 `RpcConnection` 对象的资源池。当 HBase 需要连接一个服务器时,首先会根据 `ConnectionId` 找到对应的连接池,然后从连接池中取出一个连接对象。 ```java @InterfaceAudience.Private @@ -740,7 +740,7 @@ public class PoolMap implements Map { ..... ``` -HBase中提供了三种资源池的实现,分别是`Reusable`,`RoundRobin`和`ThreadLocal`。具体实现可以通`hbase.client.ipc.pool.type`配置项指定,默认为`Reusable`。连接池的大小也可以通过`hbase.client.ipc.pool.size`配置项指定,默认为1,即每个Server 1个连接。也可以通过修改配置实现: +HBase 中提供了三种资源池的实现,分别是 `Reusable`,`RoundRobin` 和 `ThreadLocal`。具体实现可以通 `hbase.client.ipc.pool.type` 配置项指定,默认为 `Reusable`。连接池的大小也可以通过 `hbase.client.ipc.pool.size` 配置项指定,默认为 1,即每个 Server 1 个连接。也可以通过修改配置实现: ```java config.set("hbase.client.ipc.pool.type",...); @@ -748,14 +748,14 @@ config.set("hbase.client.ipc.pool.size",...); connection = ConnectionFactory.createConnection(config); ``` -由此可以看出HBase中Connection类已经实现了对连接的管理功能,所以我们不必在Connection上在做额外的管理。 +由此可以看出 HBase 中 Connection 类已经实现了对连接的管理功能,所以我们不必在 Connection 上在做额外的管理。 -另外,Connection是线程安全的,但Table和Admin却不是线程安全的,因此正确的做法是一个进程共用一个Connection对象,而在不同的线程中使用单独的Table和Admin对象。Table和Admin的获取操作`getTable()`和`getAdmin()`都是轻量级,所以不必担心性能的消耗,同时建议在使用完成后显示的调用`close()`方法来关闭它们。 +另外,Connection 是线程安全的,但 Table 和 Admin 却不是线程安全的,因此正确的做法是一个进程共用一个 Connection 对象,而在不同的线程中使用单独的 Table 和 Admin 对象。Table 和 Admin 的获取操作 `getTable()` 和 `getAdmin()` 都是轻量级,所以不必担心性能的消耗,同时建议在使用完成后显示的调用 `close()` 方法来关闭它们。 ## 参考资料 -1. [连接HBase的正确姿势](https://yq.aliyun.com/articles/581702?spm=a2c4e.11157919.spm-cont-list.1.146c27aeFxoMsN%20%E8%BF%9E%E6%8E%A5HBase%E7%9A%84%E6%AD%A3%E7%A1%AE%E5%A7%BF%E5%8A%BF) +1. [连接 HBase 的正确姿势](https://yq.aliyun.com/articles/581702?spm=a2c4e.11157919.spm-cont-list.1.146c27aeFxoMsN%20%E8%BF%9E%E6%8E%A5HBase%E7%9A%84%E6%AD%A3%E7%A1%AE%E5%A7%BF%E5%8A%BF) 2. [Apache HBase ™ Reference Guide](http://hbase.apache.org/book.htm) diff --git a/notes/Hbase_Shell.md b/notes/Hbase_Shell.md index 23c2dc9..d9417f2 100644 --- a/notes/Hbase_Shell.md +++ b/notes/Hbase_Shell.md @@ -29,7 +29,7 @@ ## 一、基本命令 -打开Hbase Shell: +打开 Hbase Shell: ```shell # hbase shell @@ -68,7 +68,7 @@ list #### 2.2 创建表 - **命令格式**: create '表名称', '列族名称1','列族名称2','列名称N' + **命令格式**: create '表名称', '列族名称 1','列族名称 2','列名称 N' ```shell # 创建一张名为Student的表,包含基本信息(baseInfo)、学校信息(schoolInfo)两个列族 @@ -85,7 +85,7 @@ describe 'Student' #### 2.4 表的启用/禁用 -enable和disable可以启用/禁用这个表,is_enabled和is_disabled来检查表是否被禁用 +enable 和 disable 可以启用/禁用这个表,is_enabled 和 is_disabled 来检查表是否被禁用 ```shell # 禁用表 @@ -136,7 +136,7 @@ alter 'Student', {NAME => 'teacherInfo', METHOD => 'delete'} #### 3.3 更改列族存储版本的限制 -默认情况下,列族只存储一个版本的数据,如果需要存储多个版本的数据,则需要修改列族的属性。修改后可通过`desc`命令查看。 +默认情况下,列族只存储一个版本的数据,如果需要存储多个版本的数据,则需要修改列族的属性。修改后可通过 `desc` 命令查看。 ```shell alter 'Student',{NAME=>'baseInfo',VERSIONS=>3} @@ -194,13 +194,13 @@ delete 'Student','rowkey3','baseInfo:name' ## 四、查询 -hbase中访问数据有两种基本的方式: +hbase 中访问数据有两种基本的方式: -+ 按指定rowkey获取数据:get方法; ++ 按指定 rowkey 获取数据:get 方法; -+ 按指定条件获取数据:scan方法。 ++ 按指定条件获取数据:scan 方法。 -`scan`可以设置begin和end参数来访问一个范围内所有的数据。get本质上就是begin和end相等的一种特殊的scan。 +`scan` 可以设置 begin 和 end 参数来访问一个范围内所有的数据。get 本质上就是 begin 和 end 相等的一种特殊的 scan。 #### 4.1Get查询 @@ -232,9 +232,9 @@ scan 'Student', {COLUMN=>'baseInfo'} scan 'Student', {COLUMNS=> 'baseInfo:birthday'} ``` -除了列`(COLUMNS)`修饰词外,HBase还支持`Limit`(限制查询结果行数),`STARTROW`(`ROWKEY`起始行,会先根据这个`key`定位到`region`,再向后扫描)、`STOPROW`(结束行)、`TIMERANGE`(限定时间戳范围)、`VERSIONS`(版本数)、和`FILTER`(按条件过滤行)等。 +除了列 `(COLUMNS)` 修饰词外,HBase 还支持 `Limit`(限制查询结果行数),`STARTROW`(`ROWKEY` 起始行,会先根据这个 `key` 定位到 `region`,再向后扫描)、`STOPROW`(结束行)、`TIMERANGE`(限定时间戳范围)、`VERSIONS`(版本数)、和 `FILTER`(按条件过滤行)等。 -如下代表从`rowkey2`这个`rowkey`开始,查找下两个行的最新3个版本的name列的数据: +如下代表从 `rowkey2` 这个 `rowkey` 开始,查找下两个行的最新 3 个版本的 name 列的数据: ```shell scan 'Student', {COLUMNS=> 'baseInfo:name',STARTROW => 'rowkey2',STOPROW => 'wrowkey4',LIMIT=>2, VERSIONS=>3} @@ -242,32 +242,32 @@ scan 'Student', {COLUMNS=> 'baseInfo:name',STARTROW => 'rowkey2',STOPROW => 'wro #### 4.5 条件过滤 -Filter可以设定一系列条件来进行过滤。如我们要查询值等于24的所有数据: +Filter 可以设定一系列条件来进行过滤。如我们要查询值等于 24 的所有数据: ```shell scan 'Student', FILTER=>"ValueFilter(=,'binary:24')" ``` -值包含yale的所有数据: +值包含 yale 的所有数据: ```shell scan 'Student', FILTER=>"ValueFilter(=,'substring:yale')" ``` -列名中的前缀为birth的: +列名中的前缀为 birth 的: ```shell scan 'Student', FILTER=>"ColumnPrefixFilter('birth')" ``` -FILTER中支持多个过滤条件通过括号、AND和OR进行组合: +FILTER 中支持多个过滤条件通过括号、AND 和 OR 进行组合: ```shell # 列名中的前缀为birth且列值中包含1998的数据 scan 'Student', FILTER=>"ColumnPrefixFilter('birth') AND ValueFilter ValueFilter(=,'substring:1998')" ``` -`PrefixFilter`用于对Rowkey的前缀进行判断: +`PrefixFilter` 用于对 Rowkey 的前缀进行判断: ```shell scan 'Student', FILTER=>"PrefixFilter('wr')" diff --git a/notes/Hbase协处理器详解.md b/notes/Hbase协处理器详解.md index 53aef69..5cb5b24 100644 --- a/notes/Hbase协处理器详解.md +++ b/notes/Hbase协处理器详解.md @@ -20,7 +20,7 @@ ## 一、简述 -在使用HBase时,如果你的数据量达到了数十亿行或数百万列,此时能否在查询中返回大量数据将受制于网络的带宽,即便网络状况允许,但是客户端的计算处理也未必能够满足要求。在这种情况下,协处理器(Coprocessors)应运而生。它允许你将业务计算代码放入在RegionServer的协处理器中,将处理好的数据再返回给客户端,这可以极大地降低需要传输的数据量,从而获得性能上的提升。同时协处理器也允许用户扩展实现HBase目前所不具备的功能,如权限校验、二级索引、完整性约束等。 +在使用 HBase 时,如果你的数据量达到了数十亿行或数百万列,此时能否在查询中返回大量数据将受制于网络的带宽,即便网络状况允许,但是客户端的计算处理也未必能够满足要求。在这种情况下,协处理器(Coprocessors)应运而生。它允许你将业务计算代码放入在 RegionServer 的协处理器中,将处理好的数据再返回给客户端,这可以极大地降低需要传输的数据量,从而获得性能上的提升。同时协处理器也允许用户扩展实现 HBase 目前所不具备的功能,如权限校验、二级索引、完整性约束等。 @@ -30,24 +30,24 @@ #### 1. 功能 -Observer协处理器类似于关系型数据库中的触发器,当发生某些事件的时候这类协处理器会被Server端调用。通常可以用来实现下面功能: +Observer 协处理器类似于关系型数据库中的触发器,当发生某些事件的时候这类协处理器会被 Server 端调用。通常可以用来实现下面功能: -+ **权限校验**:在执行`Get`或`Put`操作之前,您可以使用`preGet`或`prePut`方法检查权限; -+ **完整性约束**: HBase不支持关系型数据库中的外键功能,可以通过触发器在插入或者删除数据的时候,对关联的数据进行检查; ++ **权限校验**:在执行 `Get` 或 `Put` 操作之前,您可以使用 `preGet` 或 `prePut` 方法检查权限; ++ **完整性约束**: HBase 不支持关系型数据库中的外键功能,可以通过触发器在插入或者删除数据的时候,对关联的数据进行检查; + **二级索引**: 可以使用协处理器来维护二级索引。
#### 2. 类型 -当前Observer协处理器有以下四种类型: +当前 Observer 协处理器有以下四种类型: + **RegionObserver** : - 允许您观察Region上的事件,例如Get和Put操作。 + 允许您观察 Region 上的事件,例如 Get 和 Put 操作。 + **RegionServerObserver** : - 允许您观察与RegionServer操作相关的事件,例如启动,停止或执行合并,提交或回滚。 + 允许您观察与 RegionServer 操作相关的事件,例如启动,停止或执行合并,提交或回滚。 + **MasterObserver** : - 允许您观察与HBase Master相关的事件,例如表创建,删除或schema修改。 + 允许您观察与 HBase Master 相关的事件,例如表创建,删除或 schema 修改。 + **WalObserver** : 允许您观察与预写日志(WAL)相关的事件。 @@ -55,11 +55,11 @@ Observer协处理器类似于关系型数据库中的触发器,当发生某些 #### 3. 接口 -以上四种类型的Observer协处理器均继承自`Coprocessor`接口,这四个接口中分别定义了所有可用的钩子方法,以便在对应方法前后执行特定的操作。通常情况下,我们并不会直接实现上面接口,而是继承其Base实现类,Base实现类只是简单空实现了接口中的方法,这样我们在实现自定义的协处理器时,就不必实现所有方法,只需要重写必要方法即可。 +以上四种类型的 Observer 协处理器均继承自 `Coprocessor` 接口,这四个接口中分别定义了所有可用的钩子方法,以便在对应方法前后执行特定的操作。通常情况下,我们并不会直接实现上面接口,而是继承其 Base 实现类,Base 实现类只是简单空实现了接口中的方法,这样我们在实现自定义的协处理器时,就不必实现所有方法,只需要重写必要方法即可。
-这里以`RegionObservers `为例,其接口类中定义了所有可用的钩子方法,下面截取了部分方法的定义,多数方法都是成对出现的,有`pre`就有`post`: +这里以 `RegionObservers ` 为例,其接口类中定义了所有可用的钩子方法,下面截取了部分方法的定义,多数方法都是成对出现的,有 `pre` 就有 `post`:
@@ -72,32 +72,32 @@ Observer协处理器类似于关系型数据库中的触发器,当发生某些 + 客户端发出 put 请求 + 该请求被分派给合适的 RegionServer 和 region + coprocessorHost 拦截该请求,然后在该表的每个 RegionObserver 上调用 prePut() -+ 如果没有被`prePut()`拦截,该请求继续送到 region,然后进行处理 -+ region 产生的结果再次被 CoprocessorHost 拦截,调用`postPut()` -+ 假如没有`postPut()`拦截该响应,最终结果被返回给客户端 ++ 如果没有被 `prePut()` 拦截,该请求继续送到 region,然后进行处理 ++ region 产生的结果再次被 CoprocessorHost 拦截,调用 `postPut()` ++ 假如没有 `postPut()` 拦截该响应,最终结果被返回给客户端 -如果大家了解Spring,可以将这种执行方式类比于其AOP的执行原理即可,官方文档当中也是这样类比的: +如果大家了解 Spring,可以将这种执行方式类比于其 AOP 的执行原理即可,官方文档当中也是这样类比的: >If you are familiar with Aspect Oriented Programming (AOP), you can think of a coprocessor as applying advice by intercepting a request and then running some custom code,before passing the request on to its final destination (or even changing the destination). > ->如果您熟悉面向切面编程(AOP),您可以将协处理器视为通过拦截请求然后运行一些自定义代码来使用Advice,然后将请求传递到其最终目标(或者更改目标)。 +>如果您熟悉面向切面编程(AOP),您可以将协处理器视为通过拦截请求然后运行一些自定义代码来使用 Advice,然后将请求传递到其最终目标(或者更改目标)。 ### 2.2 Endpoint协处理器 -Endpoint协处理器类似于关系型数据库中的存储过程。客户端可以调用Endpoint协处理器在服务端对数据进行处理,然后再返回。 +Endpoint 协处理器类似于关系型数据库中的存储过程。客户端可以调用 Endpoint 协处理器在服务端对数据进行处理,然后再返回。 -以聚集操作为例,如果没有协处理器,当用户需要找出一张表中的最大数据,即 max 聚合操作,就必须进行全表扫描,然后在客户端上遍历扫描结果,这必然会加重了客户端处理数据的压力。利用 Coprocessor,用户可以将求最大值的代码部署到 HBase Server 端,HBase将利用底层 cluster 的多个节点并发执行求最大值的操作。即在每个 Region 范围内执行求最大值的代码,将每个 Region 的最大值在 Region Server 端计算出来,仅仅将该 max 值返回给客户端。之后客户端只需要将每个 Region 的最大值进行比较而找到其中最大的值即可。 +以聚集操作为例,如果没有协处理器,当用户需要找出一张表中的最大数据,即 max 聚合操作,就必须进行全表扫描,然后在客户端上遍历扫描结果,这必然会加重了客户端处理数据的压力。利用 Coprocessor,用户可以将求最大值的代码部署到 HBase Server 端,HBase 将利用底层 cluster 的多个节点并发执行求最大值的操作。即在每个 Region 范围内执行求最大值的代码,将每个 Region 的最大值在 Region Server 端计算出来,仅仅将该 max 值返回给客户端。之后客户端只需要将每个 Region 的最大值进行比较而找到其中最大的值即可。 ## 三、协处理的加载方式 -要使用我们自己开发的协处理器,必须通过静态(使用HBase配置)或动态(使用HBase Shell或Java API)加载它。 +要使用我们自己开发的协处理器,必须通过静态(使用 HBase 配置)或动态(使用 HBase Shell 或 Java API)加载它。 -+ 静态加载的协处理器称之为 **System Coprocessor**(系统级协处理器),作用范围是整个HBase上的所有表,需要重启HBase服务; -+ 动态加载的协处理器称之为 **Table Coprocessor**(表处理器),作用于指定的表,不需要重启HBase服务。 ++ 静态加载的协处理器称之为 **System Coprocessor**(系统级协处理器),作用范围是整个 HBase 上的所有表,需要重启 HBase 服务; ++ 动态加载的协处理器称之为 **Table Coprocessor**(表处理器),作用于指定的表,不需要重启 HBase 服务。 其加载和卸载方式分别介绍如下。 @@ -109,7 +109,7 @@ Endpoint协处理器类似于关系型数据库中的存储过程。客户端可 静态加载分以下三步: -1. 在`hbase-site.xml`定义需要加载的协处理器。 +1. 在 `hbase-site.xml` 定义需要加载的协处理器。 ```xml @@ -118,44 +118,44 @@ Endpoint协处理器类似于关系型数据库中的存储过程。客户端可 ``` - ` `标签的值必须是下面其中之一: + ` ` 标签的值必须是下面其中之一: - + RegionObservers 和 Endpoints协处理器:`hbase.coprocessor.region.classes` - + WALObservers协处理器: `hbase.coprocessor.wal.classes` - + MasterObservers协处理器:`hbase.coprocessor.master.classes` + + RegionObservers 和 Endpoints 协处理器:`hbase.coprocessor.region.classes` + + WALObservers 协处理器: `hbase.coprocessor.wal.classes` + + MasterObservers 协处理器:`hbase.coprocessor.master.classes` - ``必须是协处理器实现类的全限定类名。如果为加载指定了多个类,则类名必须以逗号分隔。 + `` 必须是协处理器实现类的全限定类名。如果为加载指定了多个类,则类名必须以逗号分隔。 -2. 将jar(包含代码和所有依赖项)放入HBase安装目录中的`lib`目录下; +2. 将 jar(包含代码和所有依赖项) 放入 HBase 安装目录中的 `lib` 目录下; -3. 重启HBase。 +3. 重启 HBase。
### 4.2 静态卸载 -1. 从hbase-site.xml中删除配置的协处理器的\元素及其子元素; +1. 从 hbase-site.xml 中删除配置的协处理器的\元素及其子元素; -2. 从类路径或HBase的lib目录中删除协处理器的JAR文件(可选); +2. 从类路径或 HBase 的 lib 目录中删除协处理器的 JAR 文件(可选); -3. 重启HBase。 +3. 重启 HBase。 ## 五、动态加载与卸载 -使用动态加载协处理器,不需要重新启动HBase。但动态加载的协处理器是基于每个表加载的,只能用于所指定的表。 +使用动态加载协处理器,不需要重新启动 HBase。但动态加载的协处理器是基于每个表加载的,只能用于所指定的表。 此外,在使用动态加载必须使表脱机(disable)以加载协处理器。动态加载通常有两种方式:Shell 和 Java API 。 > 以下示例基于两个前提: > > 1. coprocessor.jar 包含协处理器实现及其所有依赖项。 -> 2. JAR 包存放在HDFS上的路径为:hdfs:// \:\ / user / \ /coprocessor.jar +> 2. JAR 包存放在 HDFS 上的路径为:hdfs:// \:\ / user / \ /coprocessor.jar ### 5.1 HBase Shell动态加载 -1. 使用HBase Shell禁用表 +1. 使用 HBase Shell 禁用表 ```shell hbase > disable 'tableName' @@ -169,12 +169,12 @@ user//coprocessor.jar| org.myname.hbase.Coprocessor.RegionObserverE arg1=1,arg2=2' ``` -`Coprocessor`包含由管道(|)字符分隔的四个参数,按顺序解释如下: +`Coprocessor` 包含由管道(|)字符分隔的四个参数,按顺序解释如下: -+ **JAR包路径**:通常为JAR包在HDFS上的路径。关于路径以下两点需要注意: -+ 允许使用通配符,例如:`hdfs://:/user//*.jar` 来添加指定的JAR包; ++ **JAR 包路径**:通常为 JAR 包在 HDFS 上的路径。关于路径以下两点需要注意: ++ 允许使用通配符,例如:`hdfs://:/user//*.jar` 来添加指定的 JAR 包; -+ 可以使指定目录,例如:`hdfs://:/user//` ,这会添加目录中的所有JAR包,但不会搜索子目录中的JAR包。 ++ 可以使指定目录,例如:`hdfs://:/user//` ,这会添加目录中的所有 JAR 包,但不会搜索子目录中的 JAR 包。 + **类名**:协处理器的完整类名。 + **优先级**:协处理器的优先级,遵循数字的自然序,即值越小优先级越高。可以为空,在这种情况下,将分配默认优先级值。 @@ -192,7 +192,7 @@ hbase > enable 'tableName' hbase > describe 'tableName' ``` -协处理器出现在`TABLE_ATTRIBUTES`属性中则代表加载成功。 +协处理器出现在 `TABLE_ATTRIBUTES` 属性中则代表加载成功。
@@ -241,7 +241,7 @@ admin.modifyTable(tableName, hTableDescriptor); admin.enableTable(tableName); ``` -在HBase 0.96及其以后版本中,HTableDescriptor的addCoprocessor()方法提供了一种更为简便的加载方法。 +在 HBase 0.96 及其以后版本中,HTableDescriptor 的 addCoprocessor() 方法提供了一种更为简便的加载方法。 ```java TableName tableName = TableName.valueOf("users"); @@ -291,7 +291,7 @@ admin.enableTable(tableName); ## 六、协处理器案例 -这里给出一个简单的案例,实现一个类似于Redis中`append` 命令的协处理器,当我们对已有列执行put操作时候,HBase默认执行的是update操作,这里我们修改为执行append操作。 +这里给出一个简单的案例,实现一个类似于 Redis 中 `append` 命令的协处理器,当我们对已有列执行 put 操作时候,HBase 默认执行的是 update 操作,这里我们修改为执行 append 操作。 ```shell # redis append 命令示例 @@ -316,7 +316,7 @@ hbase > create 'magazine','article','picture' > 完整代码可见本仓库:[hbase-observer-coprocessor](https://github.com/heibaiying/BigData-Notes/tree/master/code/Hbase\hbase-observer-coprocessor) -新建Maven工程,导入下面依赖: +新建 Maven 工程,导入下面依赖: ```xml @@ -331,7 +331,7 @@ hbase > create 'magazine','article','picture' ``` -继承`BaseRegionObserver`实现我们自定义的`RegionObserver`,对相同的`article:content`执行put命令时,将新插入的内容添加到原有内容的末尾,代码如下: +继承 `BaseRegionObserver` 实现我们自定义的 `RegionObserver`,对相同的 `article:content` 执行 put 命令时,将新插入的内容添加到原有内容的末尾,代码如下: ```java public class AppendRegionObserver extends BaseRegionObserver { @@ -369,7 +369,7 @@ public class AppendRegionObserver extends BaseRegionObserver { ### 6.3 打包项目 -使用maven命令进行打包,打包后的文件名为`hbase-observer-coprocessor-1.0-SNAPSHOT.jar` +使用 maven 命令进行打包,打包后的文件名为 `hbase-observer-coprocessor-1.0-SNAPSHOT.jar` ```shell # mvn clean package @@ -411,7 +411,7 @@ hbase > enable 'magazine' hbase > desc 'magazine' ``` -协处理器出现在`TABLE_ATTRIBUTES`属性中则代表加载成功,如下图: +协处理器出现在 `TABLE_ATTRIBUTES` 属性中则代表加载成功,如下图:
@@ -426,7 +426,7 @@ hbase > put 'magazine', 'rowkey1','article:content','World' hbase > get 'magazine','rowkey1','article:content' ``` -可以看到对于指定列的值已经执行了append操作: +可以看到对于指定列的值已经执行了 append 操作:
@@ -439,7 +439,7 @@ hbase > put 'magazine', 'rowkey1','article:author','lisi' hbase > get 'magazine','rowkey1','article:author' ``` -可以看到对于正常的列还是执行update操作: +可以看到对于正常的列还是执行 update 操作:
@@ -487,4 +487,4 @@ hbase > get 'magazine','rowkey1','article:content' 1. [Apache HBase Coprocessors](http://hbase.apache.org/book.html#cp) 2. [Apache HBase Coprocessor Introduction](https://blogs.apache.org/hbase/entry/coprocessor_introduction) -3. [HBase高階知識](https://www.itread01.com/content/1546245908.html) +3. [HBase 高階知識](https://www.itread01.com/content/1546245908.html) diff --git a/notes/Hbase容灾与备份.md b/notes/Hbase容灾与备份.md index af56fb2..d56157f 100644 --- a/notes/Hbase容灾与备份.md +++ b/notes/Hbase容灾与备份.md @@ -1,196 +1,196 @@ -# Hbase容灾与备份 - - - -## 一、前言 - -本文主要介绍Hbase常用的三种简单的容灾备份方案,即**CopyTable**、**Export**/**Import**、**Snapshot**。分别介绍如下: - - - -## 二、CopyTable - -### 2.1 简介 - -**CopyTable**可以将现有表的数据复制到新表中,具有以下特点: - -- 支持时间区间 、row区间 、改变表名称 、改变列族名称 、以及是否Copy已被删除的数据等功能; -- 执行命令前,需先创建与原表结构相同的新表; -- `CopyTable`的操作是基于HBase Client API进行的,即采用`scan`进行查询, 采用`put`进行写入。 - -### 2.2 命令格式 - -```shell -Usage: CopyTable [general options] [--starttime=X] [--endtime=Y] [--new.name=NEW] [--peer.adr=ADR] -``` - -### 2.3 常用命令 - -1. 同集群下CopyTable - -```shell -hbase org.apache.hadoop.hbase.mapreduce.CopyTable --new.name=tableCopy tableOrig -``` - -2. 不同集群下CopyTable - -```shell -# 两表名称相同的情况 -hbase org.apache.hadoop.hbase.mapreduce.CopyTable \ ---peer.adr=dstClusterZK:2181:/hbase tableOrig - -# 也可以指新的表名 -hbase org.apache.hadoop.hbase.mapreduce.CopyTable \ ---peer.adr=dstClusterZK:2181:/hbase \ ---new.name=tableCopy tableOrig -``` - - -3. 下面是一个官方给的比较完整的例子,指定开始和结束时间,集群地址,以及只复制指定的列族: - -```shell -hbase org.apache.hadoop.hbase.mapreduce.CopyTable \ ---starttime=1265875194289 \ ---endtime=1265878794289 \ ---peer.adr=server1,server2,server3:2181:/hbase \ ---families=myOldCf:myNewCf,cf2,cf3 TestTable -``` - -### 2.4 更多参数 - -可以通过`--help`查看更多支持的参数 - -```shell -# hbase org.apache.hadoop.hbase.mapreduce.CopyTable --help -``` - -
- - - -## 三、Export/Import - -### 3.1 简介 - -- `Export`支持导出数据到HDFS, `Import`支持从HDFS导入数据。`Export`还支持指定导出数据的开始时间和结束时间,因此可以用于增量备份。 -- `Export`导出与`CopyTable`一样,依赖HBase的`scan`操作 - -### 3.2 命令格式 - -```shell -# Export -hbase org.apache.hadoop.hbase.mapreduce.Export [ [ []]] - -# Inport -hbase org.apache.hadoop.hbase.mapreduce.Import -``` - -+ 导出的`outputdir`目录可以不用预先创建,程序会自动创建。导出完成后,导出文件的所有权将由执行导出命令的用户所拥有。 -+ 默认情况下,仅导出给定`Cell`的最新版本,而不管历史版本。要导出多个版本,需要将``参数替换为所需的版本数。 - -### 3.3 常用命令 - -1. 导出命令 - -```shell -hbase org.apache.hadoop.hbase.mapreduce.Export tableName hdfs路径/tableName.db -``` - -2. 导入命令 - -``` -hbase org.apache.hadoop.hbase.mapreduce.Import tableName hdfs路径/tableName.db -``` - - - -## 四、Snapshot - -### 4.1 简介 - -HBase的快照(Snapshot)功能允许您获取表的副本(包括内容和元数据),并且性能开销很小。因为快照存储的仅仅是表的元数据和HFiles的信息。快照的`clone`操作会从该快照创建新表,快照的`restore`操作会将表的内容还原到快照节点。`clone`和`restore`操作不需要复制任何数据,因为底层HFiles(包含HBase表数据的文件)不会被修改,修改的只是表的元数据信息。 - -### 4.2 配置 - -HBase快照功能默认没有开启,如果要开启快照,需要在`hbase-site.xml`文件中添加如下配置项: - -```xml - - hbase.snapshot.enabled - true - -``` - - - -### 4.3 常用命令 - -快照的所有命令都需要在Hbase Shell交互式命令行中执行。 - -#### 1. Take a Snapshot - -```shell -# 拍摄快照 -hbase> snapshot '表名', '快照名' -``` - -默认情况下拍摄快照之前会在内存中执行数据刷新。以保证内存中的数据包含在快照中。但是如果你不希望包含内存中的数据,则可以使用`SKIP_FLUSH`选项禁止刷新。 - -```shell -# 禁止内存刷新 -hbase> snapshot '表名', '快照名', {SKIP_FLUSH => true} -``` - -#### 2. Listing Snapshots - -```shell -# 获取快照列表 -hbase> list_snapshots -``` - -#### 3. Deleting Snapshots - -```shell -# 删除快照 -hbase> delete_snapshot '快照名' -``` - -#### 4. Clone a table from snapshot - -```shell -# 从现有的快照创建一张新表 -hbase> clone_snapshot '快照名', '新表名' -``` - -#### 5. Restore a snapshot - -将表恢复到快照节点,恢复操作需要先禁用表 - -```shell -hbase> disable '表名' -hbase> restore_snapshot '快照名' -``` - -这里需要注意的是:是如果HBase配置了基于Replication的主从复制,由于Replication在日志级别工作,而快照在文件系统级别工作,因此在还原之后,会出现副本与主服务器处于不同的状态的情况。这时候可以先停止同步,所有服务器还原到一致的数据点后再重新建立同步。 - - - -## 参考资料 - -1. [Online Apache HBase Backups with CopyTable](https://blog.cloudera.com/blog/2012/06/online-hbase-backups-with-copytable-2/) -2. [Apache HBase ™ Reference Guide](http://hbase.apache.org/book.htm) +# Hbase容灾与备份 + + + +## 一、前言 + +本文主要介绍 Hbase 常用的三种简单的容灾备份方案,即**CopyTable**、**Export**/**Import**、**Snapshot**。分别介绍如下: + + + +## 二、CopyTable + +### 2.1 简介 + +**CopyTable**可以将现有表的数据复制到新表中,具有以下特点: + +- 支持时间区间 、row 区间 、改变表名称 、改变列族名称 、以及是否 Copy 已被删除的数据等功能; +- 执行命令前,需先创建与原表结构相同的新表; +- `CopyTable` 的操作是基于 HBase Client API 进行的,即采用 `scan` 进行查询, 采用 `put` 进行写入。 + +### 2.2 命令格式 + +```shell +Usage: CopyTable [general options] [--starttime=X] [--endtime=Y] [--new.name=NEW] [--peer.adr=ADR] +``` + +### 2.3 常用命令 + +1. 同集群下 CopyTable + +```shell +hbase org.apache.hadoop.hbase.mapreduce.CopyTable --new.name=tableCopy tableOrig +``` + +2. 不同集群下 CopyTable + +```shell +# 两表名称相同的情况 +hbase org.apache.hadoop.hbase.mapreduce.CopyTable \ +--peer.adr=dstClusterZK:2181:/hbase tableOrig + +# 也可以指新的表名 +hbase org.apache.hadoop.hbase.mapreduce.CopyTable \ +--peer.adr=dstClusterZK:2181:/hbase \ +--new.name=tableCopy tableOrig +``` + + +3. 下面是一个官方给的比较完整的例子,指定开始和结束时间,集群地址,以及只复制指定的列族: + +```shell +hbase org.apache.hadoop.hbase.mapreduce.CopyTable \ +--starttime=1265875194289 \ +--endtime=1265878794289 \ +--peer.adr=server1,server2,server3:2181:/hbase \ +--families=myOldCf:myNewCf,cf2,cf3 TestTable +``` + +### 2.4 更多参数 + +可以通过 `--help` 查看更多支持的参数 + +```shell +# hbase org.apache.hadoop.hbase.mapreduce.CopyTable --help +``` + +
+ + + +## 三、Export/Import + +### 3.1 简介 + +- `Export` 支持导出数据到 HDFS, `Import` 支持从 HDFS 导入数据。`Export` 还支持指定导出数据的开始时间和结束时间,因此可以用于增量备份。 +- `Export` 导出与 `CopyTable` 一样,依赖 HBase 的 `scan` 操作 + +### 3.2 命令格式 + +```shell +# Export +hbase org.apache.hadoop.hbase.mapreduce.Export [ [ []]] + +# Inport +hbase org.apache.hadoop.hbase.mapreduce.Import +``` + ++ 导出的 `outputdir` 目录可以不用预先创建,程序会自动创建。导出完成后,导出文件的所有权将由执行导出命令的用户所拥有。 ++ 默认情况下,仅导出给定 `Cell` 的最新版本,而不管历史版本。要导出多个版本,需要将 `` 参数替换为所需的版本数。 + +### 3.3 常用命令 + +1. 导出命令 + +```shell +hbase org.apache.hadoop.hbase.mapreduce.Export tableName hdfs 路径/tableName.db +``` + +2. 导入命令 + +``` +hbase org.apache.hadoop.hbase.mapreduce.Import tableName hdfs 路径/tableName.db +``` + + + +## 四、Snapshot + +### 4.1 简介 + +HBase 的快照 (Snapshot) 功能允许您获取表的副本 (包括内容和元数据),并且性能开销很小。因为快照存储的仅仅是表的元数据和 HFiles 的信息。快照的 `clone` 操作会从该快照创建新表,快照的 `restore` 操作会将表的内容还原到快照节点。`clone` 和 `restore` 操作不需要复制任何数据,因为底层 HFiles(包含 HBase 表数据的文件) 不会被修改,修改的只是表的元数据信息。 + +### 4.2 配置 + +HBase 快照功能默认没有开启,如果要开启快照,需要在 `hbase-site.xml` 文件中添加如下配置项: + +```xml + + hbase.snapshot.enabled + true + +``` + + + +### 4.3 常用命令 + +快照的所有命令都需要在 Hbase Shell 交互式命令行中执行。 + +#### 1. Take a Snapshot + +```shell +# 拍摄快照 +hbase> snapshot '表名', '快照名' +``` + +默认情况下拍摄快照之前会在内存中执行数据刷新。以保证内存中的数据包含在快照中。但是如果你不希望包含内存中的数据,则可以使用 `SKIP_FLUSH` 选项禁止刷新。 + +```shell +# 禁止内存刷新 +hbase> snapshot '表名', '快照名', {SKIP_FLUSH => true} +``` + +#### 2. Listing Snapshots + +```shell +# 获取快照列表 +hbase> list_snapshots +``` + +#### 3. Deleting Snapshots + +```shell +# 删除快照 +hbase> delete_snapshot '快照名' +``` + +#### 4. Clone a table from snapshot + +```shell +# 从现有的快照创建一张新表 +hbase> clone_snapshot '快照名', '新表名' +``` + +#### 5. Restore a snapshot + +将表恢复到快照节点,恢复操作需要先禁用表 + +```shell +hbase> disable '表名' +hbase> restore_snapshot '快照名' +``` + +这里需要注意的是:是如果 HBase 配置了基于 Replication 的主从复制,由于 Replication 在日志级别工作,而快照在文件系统级别工作,因此在还原之后,会出现副本与主服务器处于不同的状态的情况。这时候可以先停止同步,所有服务器还原到一致的数据点后再重新建立同步。 + + + +## 参考资料 + +1. [Online Apache HBase Backups with CopyTable](https://blog.cloudera.com/blog/2012/06/online-hbase-backups-with-copytable-2/) +2. [Apache HBase ™ Reference Guide](http://hbase.apache.org/book.htm) diff --git a/notes/Hbase的SQL中间层_Phoenix.md b/notes/Hbase的SQL中间层_Phoenix.md index 1529a6d..8de7718 100644 --- a/notes/Hbase的SQL中间层_Phoenix.md +++ b/notes/Hbase的SQL中间层_Phoenix.md @@ -23,9 +23,9 @@ ## 一、Phoenix简介 -`Phoenix`是HBase的开源SQL中间层,它允许你使用标准JDBC的方式来操作HBase上的数据。在`Phoenix`之前,如果你要访问HBase,只能调用它的Java API,但相比于使用一行SQL就能实现数据查询,HBase的API还是过于复杂。`Phoenix`的理念是`we put sql SQL back in NOSQL`,即你可以使用标准的SQL就能完成对HBase上数据的操作。同时这也意味着你可以通过集成`Spring Data JPA`或`Mybatis`等常用的持久层框架来操作HBase。 +`Phoenix` 是 HBase 的开源 SQL 中间层,它允许你使用标准 JDBC 的方式来操作 HBase 上的数据。在 `Phoenix` 之前,如果你要访问 HBase,只能调用它的 Java API,但相比于使用一行 SQL 就能实现数据查询,HBase 的 API 还是过于复杂。`Phoenix` 的理念是 `we put sql SQL back in NOSQL`,即你可以使用标准的 SQL 就能完成对 HBase 上数据的操作。同时这也意味着你可以通过集成 `Spring Data JPA` 或 `Mybatis` 等常用的持久层框架来操作 HBase。 -其次`Phoenix`的性能表现也非常优异,`Phoenix`查询引擎会将SQL查询转换为一个或多个HBase Scan,通过并行执行来生成标准的JDBC结果集。它通过直接使用HBase API以及协处理器和自定义过滤器,可以为小型数据查询提供毫秒级的性能,为千万行数据的查询提供秒级的性能。同时Phoenix还拥有二级索引等HBase不具备的特性,因为以上的优点,所以`Phoenix`成为了HBase最优秀的SQL中间层。 +其次 `Phoenix` 的性能表现也非常优异,`Phoenix` 查询引擎会将 SQL 查询转换为一个或多个 HBase Scan,通过并行执行来生成标准的 JDBC 结果集。它通过直接使用 HBase API 以及协处理器和自定义过滤器,可以为小型数据查询提供毫秒级的性能,为千万行数据的查询提供秒级的性能。同时 Phoenix 还拥有二级索引等 HBase 不具备的特性,因为以上的优点,所以 `Phoenix` 成为了 HBase 最优秀的 SQL 中间层。
@@ -43,7 +43,7 @@ ### 2.1 下载并解压 -官方针对Apache版本和CDH版本的HBase均提供了安装包,按需下载即可。官方下载地址: http://phoenix.apache.org/download.html +官方针对 Apache 版本和 CDH 版本的 HBase 均提供了安装包,按需下载即可。官方下载地址: http://phoenix.apache.org/download.html ```shell # 下载 @@ -54,9 +54,9 @@ tar tar apache-phoenix-4.14.0-cdh5.14.2-bin.tar.gz ### 2.2 拷贝Jar包 -按照官方文档的说明,需要将`phoenix server jar`添加到所有`Region Servers`的安装目录的`lib`目录下。 +按照官方文档的说明,需要将 `phoenix server jar` 添加到所有 `Region Servers` 的安装目录的 `lib` 目录下。 -这里由于我搭建的是HBase伪集群,所以只需要拷贝到当前机器的HBase的lib目录下。如果是真实集群,则使用scp命令分发到所有`Region Servers`机器上。 +这里由于我搭建的是 HBase 伪集群,所以只需要拷贝到当前机器的 HBase 的 lib 目录下。如果是真实集群,则使用 scp 命令分发到所有 `Region Servers` 机器上。 ```shell cp /usr/app/apache-phoenix-4.14.0-cdh5.14.2-bin/phoenix-4.14.0-cdh5.14.2-server.jar /usr/app/hbase-1.2.0-cdh5.15.2/lib @@ -73,10 +73,10 @@ start-hbase.sh ### 2.4 启动Phoenix -在Phoenix解压目录下的`bin`目录下执行如下命令,需要指定Zookeeper的地址: +在 Phoenix 解压目录下的 `bin` 目录下执行如下命令,需要指定 Zookeeper 的地址: -+ 如果HBase采用Standalone模式或者伪集群模式搭建,则默认采用内置的 Zookeeper服务,端口为2181; -+ 如果是HBase是集群模式并采用外置的Zookeeper集群,则按照自己的实际情况进行指定。 ++ 如果 HBase 采用 Standalone 模式或者伪集群模式搭建,则默认采用内置的 Zookeeper 服务,端口为 2181; ++ 如果是 HBase 是集群模式并采用外置的 Zookeeper 集群,则按照自己的实际情况进行指定。 ```shell # ./sqlline.py hadoop001:2181 @@ -84,7 +84,7 @@ start-hbase.sh ### 2.5 启动结果 -启动后则进入了Phoenix交互式SQL命令行,可以使用`!table`或`!tables`查看当前所有表的信息 +启动后则进入了 Phoenix 交互式 SQL 命令行,可以使用 `!table` 或 `!tables` 查看当前所有表的信息
@@ -104,13 +104,13 @@ CREATE TABLE IF NOT EXISTS us_population (
-新建的表会按照特定的规则转换为HBase上的表,关于表的信息,可以通过Hbase Web UI 进行查看: +新建的表会按照特定的规则转换为 HBase 上的表,关于表的信息,可以通过 Hbase Web UI 进行查看:
### 3.2 插入数据 -Phoenix 中插入数据采用的是`UPSERT`而不是`INSERT`,因为Phoenix并没有更新操作,插入相同主键的数据就视为更新,所以`UPSERT`就相当于`UPDATE`+`INSERT` +Phoenix 中插入数据采用的是 `UPSERT` 而不是 `INSERT`,因为 Phoenix 并没有更新操作,插入相同主键的数据就视为更新,所以 `UPSERT` 就相当于 `UPDATE`+`INSERT` ```shell UPSERT INTO us_population VALUES('NY','New York',8143197); @@ -165,27 +165,27 @@ ORDER BY sum(population) DESC; ### 3.7 扩展 -从上面的操作中可以看出,Phoenix支持大多数标准的SQL语法。关于Phoenix支持的语法、数据类型、函数、序列等详细信息,因为涉及内容很多,可以参考其官方文档,官方文档上有详细的说明: +从上面的操作中可以看出,Phoenix 支持大多数标准的 SQL 语法。关于 Phoenix 支持的语法、数据类型、函数、序列等详细信息,因为涉及内容很多,可以参考其官方文档,官方文档上有详细的说明: -+ **语法(Grammar)** :https://phoenix.apache.org/language/index.html ++ **语法 (Grammar)** :https://phoenix.apache.org/language/index.html -+ **函数(Functions)** :http://phoenix.apache.org/language/functions.html ++ **函数 (Functions)** :http://phoenix.apache.org/language/functions.html -+ **数据类型(Datatypes)** :http://phoenix.apache.org/language/datatypes.html ++ **数据类型 (Datatypes)** :http://phoenix.apache.org/language/datatypes.html -+ **序列(Sequences)** :http://phoenix.apache.org/sequences.html ++ **序列 (Sequences)** :http://phoenix.apache.org/sequences.html -+ **联结查询(Joins)** :http://phoenix.apache.org/joins.html ++ **联结查询 (Joins)** :http://phoenix.apache.org/joins.html ## 四、Phoenix Java API -因为Phoenix遵循JDBC规范,并提供了对应的数据库驱动`PhoenixDriver`,这使得采用Java语言对其进行操作的时候,就如同对其他关系型数据库一样,下面给出基本的使用示例。 +因为 Phoenix 遵循 JDBC 规范,并提供了对应的数据库驱动 `PhoenixDriver`,这使得采用 Java 语言对其进行操作的时候,就如同对其他关系型数据库一样,下面给出基本的使用示例。 ### 4.1 引入Phoenix core JAR包 -如果是maven项目,直接在maven中央仓库找到对应的版本,导入依赖即可: +如果是 maven 项目,直接在 maven 中央仓库找到对应的版本,导入依赖即可: ```xml @@ -196,7 +196,7 @@ ORDER BY sum(population) DESC;
``` -如果是普通项目,则可以从Phoenix解压目录下找到对应的JAR包,然后手动引入: +如果是普通项目,则可以从 Phoenix 解压目录下找到对应的 JAR 包,然后手动引入:
@@ -217,8 +217,8 @@ public class PhoenixJavaApi { Class.forName("org.apache.phoenix.jdbc.PhoenixDriver"); /* - * 指定数据库地址,格式为 jdbc:phoenix:Zookeeper地址 - * 如果HBase采用Standalone模式或者伪集群模式搭建,则HBase默认使用内置的Zookeeper,默认端口为2181 + * 指定数据库地址,格式为 jdbc:phoenix:Zookeeper 地址 + * 如果 HBase 采用 Standalone 模式或者伪集群模式搭建,则 HBase 默认使用内置的 Zookeeper,默认端口为 2181 */ Connection connection = DriverManager.getConnection("jdbc:phoenix:192.168.200.226:2181"); @@ -243,7 +243,7 @@ public class PhoenixJavaApi { -实际的开发中我们通常都是采用第三方框架来操作数据库,如`mybatis`,`Hibernate`,`Spring Data`等。关于Phoenix与这些框架的整合步骤参见下一篇文章:[Spring/Spring Boot + Mybatis + Phoenix](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Spring+Mybtais+Phoenix整合.md) +实际的开发中我们通常都是采用第三方框架来操作数据库,如 `mybatis`,`Hibernate`,`Spring Data` 等。关于 Phoenix 与这些框架的整合步骤参见下一篇文章:[Spring/Spring Boot + Mybatis + Phoenix](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Spring+Mybtais+Phoenix 整合.md) # 参考资料 diff --git a/notes/Hbase简介.md b/notes/Hbase简介.md index ab26bea..51dbbb6 100644 --- a/notes/Hbase简介.md +++ b/notes/Hbase简介.md @@ -1,88 +1,88 @@ -# HBase简介 - - - -## 一、Hadoop的局限 - -HBase是一个构建在Hadoop文件系统之上的面向列的数据库管理系统。 - -
- -要想明白为什么产生HBase,就需要先了解一下Hadoop存在的限制?Hadoop可以通过HDFS来存储结构化、半结构甚至非结构化的数据,它是传统数据库的补充,是海量数据存储的最佳方法,它针对大文件的存储,批量访问和流式访问都做了优化,同时也通过多副本解决了容灾问题。 - -但是Hadoop的缺陷在于它只能执行批处理,并且只能以顺序方式访问数据,这意味着即使是最简单的工作,也必须搜索整个数据集,无法实现对数据的随机访问。实现数据的随机访问是传统的关系型数据库所擅长的,但它们却不能用于海量数据的存储。在这种情况下,必须有一种新的方案来解决海量数据存储和随机访问的问题,HBase就是其中之一(HBase,Cassandra,couchDB,Dynamo和MongoDB都能存储海量数据并支持随机访问)。 - -> 注:数据结构分类: -> -> - 结构化数据:即以关系型数据库表形式管理的数据; -> - 半结构化数据:非关系模型的,有基本固定结构模式的数据,例如日志文件、XML文档、JSON文档、Email等; -> - 非结构化数据:没有固定模式的数据,如WORD、PDF、PPT、EXL,各种格式的图片、视频等。 - - - -## 二、HBase简介 - -HBase是一个构建在Hadoop文件系统之上的面向列的数据库管理系统。 - -HBase是一种类似于`Google’s Big Table`的数据模型,它是Hadoop生态系统的一部分,它将数据存储在HDFS上,客户端可以通过HBase实现对HDFS上数据的随机访问。它具有以下特性: - -+ 不支持复杂的事务,只支持行级事务,即单行数据的读写都是原子性的; -+ 由于是采用HDFS作为底层存储,所以和HDFS一样,支持结构化、半结构化和非结构化的存储; -+ 支持通过增加机器进行横向扩展; -+ 支持数据分片; -+ 支持RegionServers之间的自动故障转移; -+ 易于使用的Java客户端 API; -+ 支持BlockCache和布隆过滤器; -+ 过滤器支持谓词下推。 - - - -## 三、HBase Table - -HBase是一个面向`列`的数据库管理系统,这里更为确切的而说,HBase是一个面向`列族`的数据库管理系统。表 schema 仅定义列族,表具有多个列族,每个列族可以包含任意数量的列,列由多个单元格(cell )组成,单元格可以存储多个版本的数据,多个版本数据以时间戳进行区分。 - -下图为HBase中一张表的: - -+ RowKey为行的唯一标识,所有行按照RowKey的字典序进行排序; -+ 该表具有两个列族,分别是personal和office; -+ 其中列族personal拥有name、city、phone三个列,列族office拥有tel、addres两个列。 - -
- -> *图片引用自 : HBase是列式存储数据库吗* *https://www.iteblog.com/archives/2498.html* - -Hbase的表具有以下特点: - -- 容量大:一个表可以有数十亿行,上百万列; - -- 面向列:数据是按照列存储,每一列都单独存放,数据即索引,在查询时可以只访问指定列的数据,有效地降低了系统的I/O负担; - -- 稀疏性:空 (null) 列并不占用存储空间,表可以设计的非常稀疏 ; - -- 数据多版本:每个单元中的数据可以有多个版本,按照时间戳排序,新的数据在最上面; - -- 存储类型:所有数据的底层存储格式都是字节数组(byte[])。 - - - -## 四、Phoenix - -`Phoenix`是HBase的开源SQL中间层,它允许你使用标准JDBC的方式来操作HBase上的数据。在`Phoenix`之前,如果你要访问HBase,只能调用它的Java API,但相比于使用一行SQL就能实现数据查询,HBase的API还是过于复杂。`Phoenix`的理念是`we put sql SQL back in NOSQL`,即你可以使用标准的SQL就能完成对HBase上数据的操作。同时这也意味着你可以通过集成`Spring Data JPA`或`Mybatis`等常用的持久层框架来操作HBase。 - -其次`Phoenix`的性能表现也非常优异,`Phoenix`查询引擎会将SQL查询转换为一个或多个HBase Scan,通过并行执行来生成标准的JDBC结果集。它通过直接使用HBase API以及协处理器和自定义过滤器,可以为小型数据查询提供毫秒级的性能,为千万行数据的查询提供秒级的性能。同时Phoenix还拥有二级索引等HBase不具备的特性,因为以上的优点,所以`Phoenix`成为了HBase最优秀的SQL中间层。 - - - - - -## 参考资料 - -1. [HBase - Overview](https://www.tutorialspoint.com/hbase/hbase_overview.htm) - - - +# HBase简介 + + + +## 一、Hadoop的局限 + +HBase 是一个构建在 Hadoop 文件系统之上的面向列的数据库管理系统。 + +
+ +要想明白为什么产生 HBase,就需要先了解一下 Hadoop 存在的限制?Hadoop 可以通过 HDFS 来存储结构化、半结构甚至非结构化的数据,它是传统数据库的补充,是海量数据存储的最佳方法,它针对大文件的存储,批量访问和流式访问都做了优化,同时也通过多副本解决了容灾问题。 + +但是 Hadoop 的缺陷在于它只能执行批处理,并且只能以顺序方式访问数据,这意味着即使是最简单的工作,也必须搜索整个数据集,无法实现对数据的随机访问。实现数据的随机访问是传统的关系型数据库所擅长的,但它们却不能用于海量数据的存储。在这种情况下,必须有一种新的方案来解决海量数据存储和随机访问的问题,HBase 就是其中之一 (HBase,Cassandra,couchDB,Dynamo 和 MongoDB 都能存储海量数据并支持随机访问)。 + +> 注:数据结构分类: +> +> - 结构化数据:即以关系型数据库表形式管理的数据; +> - 半结构化数据:非关系模型的,有基本固定结构模式的数据,例如日志文件、XML 文档、JSON 文档、Email 等; +> - 非结构化数据:没有固定模式的数据,如 WORD、PDF、PPT、EXL,各种格式的图片、视频等。 + + + +## 二、HBase简介 + +HBase 是一个构建在 Hadoop 文件系统之上的面向列的数据库管理系统。 + +HBase 是一种类似于 `Google’s Big Table` 的数据模型,它是 Hadoop 生态系统的一部分,它将数据存储在 HDFS 上,客户端可以通过 HBase 实现对 HDFS 上数据的随机访问。它具有以下特性: + ++ 不支持复杂的事务,只支持行级事务,即单行数据的读写都是原子性的; ++ 由于是采用 HDFS 作为底层存储,所以和 HDFS 一样,支持结构化、半结构化和非结构化的存储; ++ 支持通过增加机器进行横向扩展; ++ 支持数据分片; ++ 支持 RegionServers 之间的自动故障转移; ++ 易于使用的 Java 客户端 API; ++ 支持 BlockCache 和布隆过滤器; ++ 过滤器支持谓词下推。 + + + +## 三、HBase Table + +HBase 是一个面向 ` 列 ` 的数据库管理系统,这里更为确切的而说,HBase 是一个面向 ` 列族 ` 的数据库管理系统。表 schema 仅定义列族,表具有多个列族,每个列族可以包含任意数量的列,列由多个单元格(cell )组成,单元格可以存储多个版本的数据,多个版本数据以时间戳进行区分。 + +下图为 HBase 中一张表的: + ++ RowKey 为行的唯一标识,所有行按照 RowKey 的字典序进行排序; ++ 该表具有两个列族,分别是 personal 和 office; ++ 其中列族 personal 拥有 name、city、phone 三个列,列族 office 拥有 tel、addres 两个列。 + +
+ +> *图片引用自 : HBase 是列式存储数据库吗* *https://www.iteblog.com/archives/2498.html* + +Hbase 的表具有以下特点: + +- 容量大:一个表可以有数十亿行,上百万列; + +- 面向列:数据是按照列存储,每一列都单独存放,数据即索引,在查询时可以只访问指定列的数据,有效地降低了系统的 I/O 负担; + +- 稀疏性:空 (null) 列并不占用存储空间,表可以设计的非常稀疏 ; + +- 数据多版本:每个单元中的数据可以有多个版本,按照时间戳排序,新的数据在最上面; + +- 存储类型:所有数据的底层存储格式都是字节数组 (byte[])。 + + + +## 四、Phoenix + +`Phoenix` 是 HBase 的开源 SQL 中间层,它允许你使用标准 JDBC 的方式来操作 HBase 上的数据。在 `Phoenix` 之前,如果你要访问 HBase,只能调用它的 Java API,但相比于使用一行 SQL 就能实现数据查询,HBase 的 API 还是过于复杂。`Phoenix` 的理念是 `we put sql SQL back in NOSQL`,即你可以使用标准的 SQL 就能完成对 HBase 上数据的操作。同时这也意味着你可以通过集成 `Spring Data JPA` 或 `Mybatis` 等常用的持久层框架来操作 HBase。 + +其次 `Phoenix` 的性能表现也非常优异,`Phoenix` 查询引擎会将 SQL 查询转换为一个或多个 HBase Scan,通过并行执行来生成标准的 JDBC 结果集。它通过直接使用 HBase API 以及协处理器和自定义过滤器,可以为小型数据查询提供毫秒级的性能,为千万行数据的查询提供秒级的性能。同时 Phoenix 还拥有二级索引等 HBase 不具备的特性,因为以上的优点,所以 `Phoenix` 成为了 HBase 最优秀的 SQL 中间层。 + + + + + +## 参考资料 + +1. [HBase - Overview](https://www.tutorialspoint.com/hbase/hbase_overview.htm) + + + diff --git a/notes/Hbase系统架构及数据结构.md b/notes/Hbase系统架构及数据结构.md index e56af3a..4ee2140 100644 --- a/notes/Hbase系统架构及数据结构.md +++ b/notes/Hbase系统架构及数据结构.md @@ -21,23 +21,23 @@ ## 一、基本概念 -一个典型的Hbase Table 表如下: +一个典型的 Hbase Table 表如下:
### 1.1 Row Key (行键) -`Row Key`是用来检索记录的主键。想要访问HBase Table中的数据,只有以下三种方式: +`Row Key` 是用来检索记录的主键。想要访问 HBase Table 中的数据,只有以下三种方式: -+ 通过指定的`Row Key`进行访问; ++ 通过指定的 `Row Key` 进行访问; -+ 通过Row Key的range进行访问,即访问指定范围内的行; ++ 通过 Row Key 的 range 进行访问,即访问指定范围内的行; + 进行全表扫描。 -`Row Key`可以是任意字符串,存储时数据按照`Row Key`的字典序进行排序。这里需要注意以下两点: +`Row Key` 可以是任意字符串,存储时数据按照 `Row Key` 的字典序进行排序。这里需要注意以下两点: -+ 因为字典序对Int排序的结果是1,10,100,11,12,13,14,15,16,17,18,19,2,20,21,…,9,91,92,93,94,95,96,97,98,99。如果你使用整型的字符串作为行键,那么为了保持整型的自然序,行键必须用0作左填充。 ++ 因为字典序对 Int 排序的结果是 1,10,100,11,12,13,14,15,16,17,18,19,2,20,21,…,9,91,92,93,94,95,96,97,98,99。如果你使用整型的字符串作为行键,那么为了保持整型的自然序,行键必须用 0 作左填充。 + 行的一次读写操作时原子性的 (不论一次读写多少列)。 @@ -45,31 +45,31 @@ ### 1.2 Column Family(列族) -HBase表中的每个列,都归属于某个列族。列族是表的Schema的一部分,所以列族需要在创建表时进行定义。列族的所有列都以列族名作为前缀,例如`courses:history`,`courses:math`都属于`courses`这个列族。 +HBase 表中的每个列,都归属于某个列族。列族是表的 Schema 的一部分,所以列族需要在创建表时进行定义。列族的所有列都以列族名作为前缀,例如 `courses:history`,`courses:math` 都属于 `courses` 这个列族。 ### 1.3 Column Qualifier (列限定符) -列限定符,你可以理解为是具体的列名,例如`courses:history`,`courses:math`都属于`courses`这个列族,它们的列限定符分别是`history`和`math`。需要注意的是列限定符不是表Schema的一部分,你可以在插入数据的过程中动态创建列。 +列限定符,你可以理解为是具体的列名,例如 `courses:history`,`courses:math` 都属于 `courses` 这个列族,它们的列限定符分别是 `history` 和 `math`。需要注意的是列限定符不是表 Schema 的一部分,你可以在插入数据的过程中动态创建列。 ### 1.4 Column(列) -HBase中的列由列族和列限定符组成,它们由`:`(冒号)进行分隔,即一个完整的列名应该表述为`列族名 :列限定符`。 +HBase 中的列由列族和列限定符组成,它们由 `:`(冒号) 进行分隔,即一个完整的列名应该表述为 ` 列族名 :列限定符 `。 ### 1.5 Cell -`Cell`是行,列族和列限定符的组合,并包含值和时间戳。你可以等价理解为关系型数据库中由指定行和指定列确定的一个单元格,但不同的是HBase中的一个单元格是由多个版本的数据组成的,每个版本的数据用时间戳进行区分。 +`Cell` 是行,列族和列限定符的组合,并包含值和时间戳。你可以等价理解为关系型数据库中由指定行和指定列确定的一个单元格,但不同的是 HBase 中的一个单元格是由多个版本的数据组成的,每个版本的数据用时间戳进行区分。 ### 1.6 Timestamp(时间戳) -HBase 中通过`row key`和`column`确定的为一个存储单元称为`Cell`。每个`Cell`都保存着同一份数据的多个版本。版本通过时间戳来索引,时间戳的类型是 64位整型,时间戳可以由HBase在数据写入时自动赋值,也可以由客户显式指定。每个`Cell`中,不同版本的数据按照时间戳倒序排列,即最新的数据排在最前面。 +HBase 中通过 `row key` 和 `column` 确定的为一个存储单元称为 `Cell`。每个 `Cell` 都保存着同一份数据的多个版本。版本通过时间戳来索引,时间戳的类型是 64 位整型,时间戳可以由 HBase 在数据写入时自动赋值,也可以由客户显式指定。每个 `Cell` 中,不同版本的数据按照时间戳倒序排列,即最新的数据排在最前面。 @@ -77,32 +77,32 @@ HBase 中通过`row key`和`column`确定的为一个存储单元称为`Cell`。 ### 2.1 Regions -HBase Table中的所有行按照`Row Key`的字典序排列。HBase Tables 通过行键的范围(row key range)被水平切分成多个`Region`, 一个`Region`包含了在start key 和 end key之间的所有行。 +HBase Table 中的所有行按照 `Row Key` 的字典序排列。HBase Tables 通过行键的范围 (row key range) 被水平切分成多个 `Region`, 一个 `Region` 包含了在 start key 和 end key 之间的所有行。
-每个表一开始只有一个`Region`,随着数据不断增加,`Region`会不断增大,当增大到一个阀值的时候,`Region`就会等分为两个新的`Region`。当Table中的行不断增多,就会有越来越多的`Region`。 +每个表一开始只有一个 `Region`,随着数据不断增加,`Region` 会不断增大,当增大到一个阀值的时候,`Region` 就会等分为两个新的 `Region`。当 Table 中的行不断增多,就会有越来越多的 `Region`。
-`Region`是HBase中**分布式存储和负载均衡的最小单元**。这意味着不同的`Region`可以分布在不同的`Region Server`上。但一个`Region`是不会拆分到多个Server上的。 +`Region` 是 HBase 中**分布式存储和负载均衡的最小单元**。这意味着不同的 `Region` 可以分布在不同的 `Region Server` 上。但一个 `Region` 是不会拆分到多个 Server 上的。
### 2.2 Region Server -`Region Server`运行在HDFS的DataNode上。它具有以下组件: +`Region Server` 运行在 HDFS 的 DataNode 上。它具有以下组件: - **WAL(Write Ahead Log,预写日志)**:用于存储尚未进持久化存储的数据记录,以便在发生故障时进行恢复。 -- **BlockCache**:读缓存。它将频繁读取的数据存储在内存中,如果存储不足,它将按照`最近最少使用原则`清除多余的数据。 -- **MemStore**:写缓存。它存储尚未写入磁盘的新数据,并会在数据写入磁盘之前对其进行排序。每个Region上的每个列族都有一个MemStore。 -- **HFile** :将行数据按照Key\Values的形式存储在文件系统上。 +- **BlockCache**:读缓存。它将频繁读取的数据存储在内存中,如果存储不足,它将按照 ` 最近最少使用原则 ` 清除多余的数据。 +- **MemStore**:写缓存。它存储尚未写入磁盘的新数据,并会在数据写入磁盘之前对其进行排序。每个 Region 上的每个列族都有一个 MemStore。 +- **HFile** :将行数据按照 Key\Values 的形式存储在文件系统上。
-Region Server存取一个子表时,会创建一个Region对象,然后对表的每个列族创建一个`Store`实例,每个`Store`会有 0 个或多个`StoreFile`与之对应,每个`StoreFile`则对应一个`HFile`,HFile 就是实际存储在HDFS上的文件。 +Region Server 存取一个子表时,会创建一个 Region 对象,然后对表的每个列族创建一个 `Store` 实例,每个 `Store` 会有 0 个或多个 `StoreFile` 与之对应,每个 `StoreFile` 则对应一个 `HFile`,HFile 就是实际存储在 HDFS 上的文件。
@@ -112,47 +112,47 @@ Region Server存取一个子表时,会创建一个Region对象,然后对表 ### 3.1 系统架构 -HBase系统遵循Master/Salve架构,由三种不同类型的组件组成: +HBase 系统遵循 Master/Salve 架构,由三种不同类型的组件组成: **Zookeeper** -1. 保证任何时候,集群中只有一个Master; +1. 保证任何时候,集群中只有一个 Master; -2. 存贮所有Region的寻址入口; +2. 存贮所有 Region 的寻址入口; -3. 实时监控Region Server的状态,将Region Server的上线和下线信息实时通知给Master; +3. 实时监控 Region Server 的状态,将 Region Server 的上线和下线信息实时通知给 Master; -4. 存储HBase的Schema,包括有哪些Table,每个Table有哪些Column Family等信息。 +4. 存储 HBase 的 Schema,包括有哪些 Table,每个 Table 有哪些 Column Family 等信息。 **Master** -1. 为Region Server分配Region ; +1. 为 Region Server 分配 Region ; -2. 负责Region Server的负载均衡 ; +2. 负责 Region Server 的负载均衡 ; -3. 发现失效的Region Server并重新分配其上的Region; +3. 发现失效的 Region Server 并重新分配其上的 Region; -4. GFS上的垃圾文件回收; +4. GFS 上的垃圾文件回收; -5. 处理Schema的更新请求。 +5. 处理 Schema 的更新请求。 **Region Server** -1. Region Server负责维护Master分配给它的Region ,并处理发送到Region上的IO请求; +1. Region Server 负责维护 Master 分配给它的 Region ,并处理发送到 Region 上的 IO 请求; -2. Region Server负责切分在运行过程中变得过大的Region。 +2. Region Server 负责切分在运行过程中变得过大的 Region。
### 3.2 组件间的协作 - HBase使用ZooKeeper作为分布式协调服务来维护集群中的服务器状态。 Zookeeper负责维护可用服务列表,并提供服务故障通知等服务: + HBase 使用 ZooKeeper 作为分布式协调服务来维护集群中的服务器状态。 Zookeeper 负责维护可用服务列表,并提供服务故障通知等服务: -+ 每个Region Server都会在ZooKeeper上创建一个临时节点,Master通过Zookeeper的Watcher机制对节点进行监控,从而可以发现新加入的Region Server或故障退出的Region Server; ++ 每个 Region Server 都会在 ZooKeeper 上创建一个临时节点,Master 通过 Zookeeper 的 Watcher 机制对节点进行监控,从而可以发现新加入的 Region Server 或故障退出的 Region Server; -+ 所有Masters会竞争性地在Zookeeper上创建同一个临时节点,由于Zookeeper只能有一个同名节点,所以必然只有一个Master能够创建成功,此时该Master就是主Master,主Master会定期向Zookeeper发送心跳。备用Masters则通过Watcher机制对主HMaster所在节点进行监听; ++ 所有 Masters 会竞争性地在 Zookeeper 上创建同一个临时节点,由于 Zookeeper 只能有一个同名节点,所以必然只有一个 Master 能够创建成功,此时该 Master 就是主 Master,主 Master 会定期向 Zookeeper 发送心跳。备用 Masters 则通过 Watcher 机制对主 HMaster 所在节点进行监听; -+ 如果主Master未能定时发送心跳,则其持有的Zookeeper会话会过期,相应的临时节点也会被删除,这会触发定义在该节点上的Watcher事件,使得备用的Master Servers得到通知。所有备用的Master Servers在接到通知后,会再次去竞争性地创建临时节点,完成主Master的选举。 ++ 如果主 Master 未能定时发送心跳,则其持有的 Zookeeper 会话会过期,相应的临时节点也会被删除,这会触发定义在该节点上的 Watcher 事件,使得备用的 Master Servers 得到通知。所有备用的 Master Servers 在接到通知后,会再次去竞争性地创建临时节点,完成主 Master 的选举。
@@ -162,19 +162,19 @@ HBase系统遵循Master/Salve架构,由三种不同类型的组件组成: ### 4.1 写入数据的流程 -1. Client向Region Server提交写请求; +1. Client 向 Region Server 提交写请求; -2. Region Server找到目标Region; +2. Region Server 找到目标 Region; -3. Region检查数据是否与Schema一致; +3. Region 检查数据是否与 Schema 一致; 4. 如果客户端没有指定版本,则获取当前系统时间作为数据版本; -5. 将更新写入WAL Log; +5. 将更新写入 WAL Log; -6. 将更新写入Memstore; +6. 将更新写入 Memstore; -7. 判断Memstore存储是否已满,如果存储已满则需要flush为Store Hfile文件。 +7. 判断 Memstore 存储是否已满,如果存储已满则需要 flush 为 Store Hfile 文件。 > 更为详细写入流程可以参考:[HBase - 数据写入流程解析](http://hbasefly.com/2016/03/23/hbase_writer/) @@ -182,25 +182,25 @@ HBase系统遵循Master/Salve架构,由三种不同类型的组件组成: ### 4.2 读取数据的流程 -以下是客户端首次读写HBase上数据的流程: +以下是客户端首次读写 HBase 上数据的流程: -1. 客户端从Zookeeper获取`META`表所在的Region Server; +1. 客户端从 Zookeeper 获取 `META` 表所在的 Region Server; -2. 客户端访问`META`表所在的Region Server,从`META`表中查询到访问行键所在的Region Server,之后客户端将缓存这些信息以及`META`表的位置; +2. 客户端访问 `META` 表所在的 Region Server,从 `META` 表中查询到访问行键所在的 Region Server,之后客户端将缓存这些信息以及 `META` 表的位置; -3. 客户端从行键所在的Region Server上获取数据。 +3. 客户端从行键所在的 Region Server 上获取数据。 -如果再次读取,客户端将从缓存中获取行键所在的Region Server。这样客户端就不需要再次查询`META`表,除非Region移动导致缓存失效,这样的话,则将会重新查询并更新缓存。 +如果再次读取,客户端将从缓存中获取行键所在的 Region Server。这样客户端就不需要再次查询 `META` 表,除非 Region 移动导致缓存失效,这样的话,则将会重新查询并更新缓存。 -注:`META`表是HBase中一张特殊的表,它保存了所有Region的位置信息,META表自己的位置信息则存储在ZooKeeper上。 +注:`META` 表是 HBase 中一张特殊的表,它保存了所有 Region 的位置信息,META 表自己的位置信息则存储在 ZooKeeper 上。
> 更为详细读取数据流程参考: > -> [HBase原理-数据读取流程解析](http://hbasefly.com/2016/12/21/hbase-getorscan/) +> [HBase 原理-数据读取流程解析](http://hbasefly.com/2016/12/21/hbase-getorscan/) > -> [HBase原理-迟到的‘数据读取流程部分细节](http://hbasefly.com/2017/06/11/hbase-scan-2/) +> [HBase 原理-迟到的‘数据读取流程部分细节](http://hbasefly.com/2017/06/11/hbase-scan-2/) @@ -212,7 +212,7 @@ HBase系统遵循Master/Salve架构,由三种不同类型的组件组成: + [HBase Architectural Components](https://mapr.com/blog/in-depth-look-hbase-architecture/#.VdMxvWSqqko) -+ [Hbase系统架构及数据结构](https://www.open-open.com/lib/view/open1346821084631.html) ++ [Hbase 系统架构及数据结构](https://www.open-open.com/lib/view/open1346821084631.html) 官方文档: diff --git a/notes/Hbase过滤器详解.md b/notes/Hbase过滤器详解.md index f56882c..e8a6e5d 100644 --- a/notes/Hbase过滤器详解.md +++ b/notes/Hbase过滤器详解.md @@ -28,7 +28,7 @@ ## 一、HBase过滤器简介 -Hbase提供了种类丰富的过滤器(filter)来提高数据处理的效率,用户可以通过内置或自定义的过滤器来对数据进行过滤,所有的过滤器都在服务端生效,即谓词下推(predicate push down)。这样可以保证过滤掉的数据不会被传送到客户端,从而减轻网络传输和客户端处理的压力。 +Hbase 提供了种类丰富的过滤器(filter)来提高数据处理的效率,用户可以通过内置或自定义的过滤器来对数据进行过滤,所有的过滤器都在服务端生效,即谓词下推(predicate push down)。这样可以保证过滤掉的数据不会被传送到客户端,从而减轻网络传输和客户端处理的压力。
@@ -38,14 +38,14 @@ Hbase提供了种类丰富的过滤器(filter)来提高数据处理的效率 ### 2.1 Filter接口和FilterBase抽象类 -Filter接口中定义了过滤器的基本方法,FilterBase抽象类实现了Filter接口。所有内置的过滤器则直接或者间接继承自FilterBase抽象类。用户只需要将定义好的过滤器通过`setFilter`方法传递给`Scan`或`put`的实例即可。 +Filter 接口中定义了过滤器的基本方法,FilterBase 抽象类实现了 Filter 接口。所有内置的过滤器则直接或者间接继承自 FilterBase 抽象类。用户只需要将定义好的过滤器通过 `setFilter` 方法传递给 `Scan` 或 `put` 的实例即可。 ```java setFilter(Filter filter) ``` ```java - // Scan 中定义的setFilter + // Scan 中定义的 setFilter @Override public Scan setFilter(Filter filter) { super.setFilter(filter); @@ -54,7 +54,7 @@ setFilter(Filter filter) ``` ```java - // Get 中定义的setFilter + // Get 中定义的 setFilter @Override public Get setFilter(Filter filter) { super.setFilter(filter); @@ -62,9 +62,9 @@ setFilter(Filter filter) } ``` -FilterBase的所有子类过滤器如下:
+FilterBase 的所有子类过滤器如下:
-> 说明:上图基于当前时间点(2019.4)最新的Hbase-2.1.4 ,下文所有说明均基于此版本。 +> 说明:上图基于当前时间点(2019.4)最新的 Hbase-2.1.4 ,下文所有说明均基于此版本。 @@ -76,7 +76,7 @@ HBase 内置过滤器可以分为三类:分别是比较过滤器,专用过 ## 三、比较过滤器 -所有比较过滤器均继承自`CompareFilter`。创建一个比较过滤器需要两个参数,分别是**比较运算符**和**比较器实例**。 +所有比较过滤器均继承自 `CompareFilter`。创建一个比较过滤器需要两个参数,分别是**比较运算符**和**比较器实例**。 ```java public CompareFilter(final CompareOp compareOp,final ByteArrayComparable comparator) { @@ -95,7 +95,7 @@ HBase 内置过滤器可以分为三类:分别是比较过滤器,专用过 - GREATER (>) - NO_OP (排除所有符合条件的值) -比较运算符均定义在枚举类`CompareOperator`中 +比较运算符均定义在枚举类 `CompareOperator` 中 ```java @InterfaceAudience.Public @@ -110,42 +110,42 @@ public enum CompareOperator { } ``` -> 注意:在 1.x 版本的HBase中,比较运算符定义在`CompareFilter.CompareOp`枚举类中,但在2.0之后这个类就被标识为 @deprecated ,并会在3.0移除。所以2.0之后版本的HBase需要使用 `CompareOperator`这个枚举类。 +> 注意:在 1.x 版本的 HBase 中,比较运算符定义在 `CompareFilter.CompareOp` 枚举类中,但在 2.0 之后这个类就被标识为 @deprecated ,并会在 3.0 移除。所以 2.0 之后版本的 HBase 需要使用 `CompareOperator` 这个枚举类。 > ### 3.2 比较器 -所有比较器均继承自`ByteArrayComparable`抽象类,常用的有以下几种: +所有比较器均继承自 `ByteArrayComparable` 抽象类,常用的有以下几种:
-- **BinaryComparator** : 使用`Bytes.compareTo(byte [],byte [])`按字典序比较指定的字节数组。 +- **BinaryComparator** : 使用 `Bytes.compareTo(byte [],byte [])` 按字典序比较指定的字节数组。 - **BinaryPrefixComparator** : 按字典序与指定的字节数组进行比较,但只比较到这个字节数组的长度。 -- **RegexStringComparator** : 使用给定的正则表达式与指定的字节数组进行比较。仅支持`EQUAL`和`NOT_EQUAL`操作。 -- **SubStringComparator** : 测试给定的子字符串是否出现在指定的字节数组中,比较不区分大小写。仅支持`EQUAL`和`NOT_EQUAL`操作。 +- **RegexStringComparator** : 使用给定的正则表达式与指定的字节数组进行比较。仅支持 `EQUAL` 和 `NOT_EQUAL` 操作。 +- **SubStringComparator** : 测试给定的子字符串是否出现在指定的字节数组中,比较不区分大小写。仅支持 `EQUAL` 和 `NOT_EQUAL` 操作。 - **NullComparator** :判断给定的值是否为空。 - **BitComparator** :按位进行比较。 -`BinaryPrefixComparator` 和 `BinaryComparator`的区别不是很好理解,这里举例说明一下: +`BinaryPrefixComparator` 和 `BinaryComparator` 的区别不是很好理解,这里举例说明一下: -在进行`EQUAL`的比较时,如果比较器传入的是`abcd`的字节数组,但是待比较数据是`abcdefgh`: +在进行 `EQUAL` 的比较时,如果比较器传入的是 `abcd` 的字节数组,但是待比较数据是 `abcdefgh`: -+ 如果使用的是`BinaryPrefixComparator`比较器,则比较以`abcd`字节数组的长度为准,即`efgh`不会参与比较,这时候认为`abcd`与`abcdefgh` 是满足`EQUAL`条件的; -+ 如果使用的是`BinaryComparator`比较器,则认为其是不相等的。 ++ 如果使用的是 `BinaryPrefixComparator` 比较器,则比较以 `abcd` 字节数组的长度为准,即 `efgh` 不会参与比较,这时候认为 `abcd` 与 `abcdefgh` 是满足 `EQUAL` 条件的; ++ 如果使用的是 `BinaryComparator` 比较器,则认为其是不相等的。 ### 3.3 比较过滤器种类 -比较过滤器共有五个(Hbase 1.x 版本和2.x 版本相同),见下图: +比较过滤器共有五个(Hbase 1.x 版本和 2.x 版本相同),见下图:
+ **RowFilter** :基于行键来过滤数据; + **FamilyFilterr** :基于列族来过滤数据; + **QualifierFilterr** :基于列限定符(列名)来过滤数据; -+ **ValueFilterr** :基于单元格(cell) 的值来过滤数据; ++ **ValueFilterr** :基于单元格 (cell) 的值来过滤数据; + **DependentColumnFilter** :指定一个参考列来过滤其他列的过滤器,过滤的原则是基于参考列的时间戳来进行筛选 。 -前四种过滤器的使用方法相同,均只要传递比较运算符和运算器实例即可构建,然后通过`setFilter`方法传递给`scan`: +前四种过滤器的使用方法相同,均只要传递比较运算符和运算器实例即可构建,然后通过 `setFilter` 方法传递给 `scan`: ```java Filter filter = new RowFilter(CompareOperator.LESS_OR_EQUAL, @@ -153,11 +153,11 @@ public enum CompareOperator { scan.setFilter(filter); ``` -`DependentColumnFilter`的使用稍微复杂一点,这里单独做下说明。 +`DependentColumnFilter` 的使用稍微复杂一点,这里单独做下说明。 ### 3.4 DependentColumnFilter -可以把`DependentColumnFilter`理解为**一个valueFilter和一个时间戳过滤器的组合**。`DependentColumnFilter`有三个带参构造器,这里选择一个参数最全的进行说明: +可以把 `DependentColumnFilter` 理解为**一个 valueFilter 和一个时间戳过滤器的组合**。`DependentColumnFilter` 有三个带参构造器,这里选择一个参数最全的进行说明: ```java DependentColumnFilter(final byte [] family, final byte[] qualifier, @@ -167,7 +167,7 @@ DependentColumnFilter(final byte [] family, final byte[] qualifier, + **family** :列族 + **qualifier** :列限定符(列名) -+ **dropDependentColumn** :决定参考列是否被包含在返回结果内,为true时表示参考列被返回,为false时表示被丢弃 ++ **dropDependentColumn** :决定参考列是否被包含在返回结果内,为 true 时表示参考列被返回,为 false 时表示被丢弃 + **op** :比较运算符 + **valueComparator** :比较器 @@ -182,24 +182,24 @@ DependentColumnFilter dependentColumnFilter = new DependentColumnFilter( new BinaryPrefixComparator(Bytes.toBytes("xiaolan"))); ``` -+ 首先会去查找`student:name`中值以`xiaolan`开头的所有数据获得`参考数据集`,这一步等同于valueFilter过滤器; ++ 首先会去查找 `student:name` 中值以 `xiaolan` 开头的所有数据获得 ` 参考数据集 `,这一步等同于 valueFilter 过滤器; -+ 其次再用参考数据集中所有数据的时间戳去检索其他列,获得时间戳相同的其他列的数据作为`结果数据集`,这一步等同于时间戳过滤器; ++ 其次再用参考数据集中所有数据的时间戳去检索其他列,获得时间戳相同的其他列的数据作为 ` 结果数据集 `,这一步等同于时间戳过滤器; -+ 最后如果`dropDependentColumn`为true,则返回`参考数据集`+`结果数据集`,若为false,则抛弃参考数据集,只返回`结果数据集`。 ++ 最后如果 `dropDependentColumn` 为 true,则返回 ` 参考数据集 `+` 结果数据集 `,若为 false,则抛弃参考数据集,只返回 ` 结果数据集 `。 ## 四、专用过滤器 -专用过滤器通常直接继承自`FilterBase`,适用于范围更小的筛选规则。 +专用过滤器通常直接继承自 `FilterBase`,适用于范围更小的筛选规则。 ### 4.1 单列列值过滤器 (SingleColumnValueFilter) 基于某列(参考列)的值决定某行数据是否被过滤。其实例有以下方法: -+ **setFilterIfMissing(boolean filterIfMissing)** :默认值为false,即如果该行数据不包含参考列,其依然被包含在最后的结果中;设置为true时,则不包含; -+ **setLatestVersionOnly(boolean latestVersionOnly)** :默认为true,即只检索参考列的最新版本数据;设置为false,则检索所有版本数据。 ++ **setFilterIfMissing(boolean filterIfMissing)** :默认值为 false,即如果该行数据不包含参考列,其依然被包含在最后的结果中;设置为 true 时,则不包含; ++ **setLatestVersionOnly(boolean latestVersionOnly)** :默认为 true,即只检索参考列的最新版本数据;设置为 false,则检索所有版本数据。 ```shell SingleColumnValueFilter singleColumnValueFilter = new SingleColumnValueFilter( @@ -213,11 +213,11 @@ scan.setFilter(singleColumnValueFilter); ### 4.2 单列列值排除器 (SingleColumnValueExcludeFilter) -`SingleColumnValueExcludeFilter`继承自上面的`SingleColumnValueFilter`,过滤行为与其相反。 +`SingleColumnValueExcludeFilter` 继承自上面的 `SingleColumnValueFilter`,过滤行为与其相反。 ### 4.3 行键前缀过滤器 (PrefixFilter) -基于RowKey值决定某行数据是否被过滤。 +基于 RowKey 值决定某行数据是否被过滤。 ```java PrefixFilter prefixFilter = new PrefixFilter(Bytes.toBytes("xxx")); @@ -235,7 +235,7 @@ ColumnPrefixFilter columnPrefixFilter = new ColumnPrefixFilter(Bytes.toBytes("xx ### 4.5 分页过滤器 (PageFilter) -可以使用这个过滤器实现对结果按行进行分页,创建PageFilter实例的时候需要传入每页的行数。 +可以使用这个过滤器实现对结果按行进行分页,创建 PageFilter 实例的时候需要传入每页的行数。 ```java public PageFilter(final long pageSize) { @@ -246,17 +246,17 @@ public PageFilter(final long pageSize) { 下面的代码体现了客户端实现分页查询的主要逻辑,这里对其进行一下解释说明: -客户端进行分页查询,需要传递`startRow`(起始RowKey),知道起始`startRow`后,就可以返回对应的pageSize行数据。这里唯一的问题就是,对于第一次查询,显然`startRow`就是表格的第一行数据,但是之后第二次、第三次查询我们并不知道`startRow`,只能知道上一次查询的最后一条数据的RowKey(简单称之为`lastRow`)。 +客户端进行分页查询,需要传递 `startRow`(起始 RowKey),知道起始 `startRow` 后,就可以返回对应的 pageSize 行数据。这里唯一的问题就是,对于第一次查询,显然 `startRow` 就是表格的第一行数据,但是之后第二次、第三次查询我们并不知道 `startRow`,只能知道上一次查询的最后一条数据的 RowKey(简单称之为 `lastRow`)。 -我们不能将`lastRow`作为新一次查询的`startRow`传入,因为scan的查询区间是[startRow,endRow) ,即前开后闭区间,这样`startRow`在新的查询也会被返回,这条数据就重复了。 +我们不能将 `lastRow` 作为新一次查询的 `startRow` 传入,因为 scan 的查询区间是[startRow,endRow) ,即前开后闭区间,这样 `startRow` 在新的查询也会被返回,这条数据就重复了。 -同时在不使用第三方数据库存储RowKey的情况下,我们是无法通过知道`lastRow`的下一个RowKey的,因为RowKey的设计可能是连续的也有可能是不连续的。 +同时在不使用第三方数据库存储 RowKey 的情况下,我们是无法通过知道 `lastRow` 的下一个 RowKey 的,因为 RowKey 的设计可能是连续的也有可能是不连续的。 -由于Hbase的RowKey是按照字典序进行排序的。这种情况下,就可以在`lastRow`后面加上`0` ,作为`startRow`传入,因为按照字典序的规则,某个值加上`0` 后的新值,在字典序上一定是这个值的下一个值,对于HBase来说下一个RowKey在字典序上一定也是等于或者大于这个新值的。 +由于 Hbase 的 RowKey 是按照字典序进行排序的。这种情况下,就可以在 `lastRow` 后面加上 `0` ,作为 `startRow` 传入,因为按照字典序的规则,某个值加上 `0` 后的新值,在字典序上一定是这个值的下一个值,对于 HBase 来说下一个 RowKey 在字典序上一定也是等于或者大于这个新值的。 -所以最后传入`lastRow`+`0`,如果等于这个值的RowKey存在就从这个值开始scan,否则从字典序的下一个RowKey开始scan。 +所以最后传入 `lastRow`+`0`,如果等于这个值的 RowKey 存在就从这个值开始 scan,否则从字典序的下一个 RowKey 开始 scan。 -> 25个字母以及数字字符,字典排序如下: +> 25 个字母以及数字字符,字典排序如下: > > `'0' < '1' < '2' < ... < '9' < 'a' < 'b' < ... < 'z'` @@ -272,7 +272,7 @@ while (true) { Scan scan = new Scan(); scan.setFilter(filter); if (lastRow != null) { - // 如果不是首行 则lastRow + 0 + // 如果不是首行 则 lastRow + 0 byte[] startRow = Bytes.add(lastRow, POSTFIX); System.out.println("start row: " + Bytes.toStringBinary(startRow)); @@ -293,7 +293,7 @@ while (true) { System.out.println("total rows: " + totalRows); ``` ->需要注意的是在多台Regin Services上执行分页过滤的时候,由于并行执行的过滤器不能共享它们的状态和边界,所以有可能每个过滤器都会在完成扫描前获取了PageCount行的结果,这种情况下会返回比分页条数更多的数据,分页过滤器就有失效的可能。 +>需要注意的是在多台 Regin Services 上执行分页过滤的时候,由于并行执行的过滤器不能共享它们的状态和边界,所以有可能每个过滤器都会在完成扫描前获取了 PageCount 行的结果,这种情况下会返回比分页条数更多的数据,分页过滤器就有失效的可能。 @@ -308,7 +308,7 @@ scan.setFilter(timestampsFilter); ### 4.7 首次行键过滤器 (FirstKeyOnlyFilter) -`FirstKeyOnlyFilter`只扫描每行的第一列,扫描完第一列后就结束对当前行的扫描,并跳转到下一行。相比于全表扫描,其性能更好,通常用于行数统计的场景,因为如果某一行存在,则行中必然至少有一列。 +`FirstKeyOnlyFilter` 只扫描每行的第一列,扫描完第一列后就结束对当前行的扫描,并跳转到下一行。相比于全表扫描,其性能更好,通常用于行数统计的场景,因为如果某一行存在,则行中必然至少有一列。 ```java FirstKeyOnlyFilter firstKeyOnlyFilter = new FirstKeyOnlyFilter(); @@ -321,13 +321,13 @@ scan.set(firstKeyOnlyFilter); ### 5.1 SkipFilter过滤器 -`SkipFilter`包装一个过滤器,当被包装的过滤器遇到一个需要过滤的KeyValue实例时,则拓展过滤整行数据。下面是一个使用示例: +`SkipFilter` 包装一个过滤器,当被包装的过滤器遇到一个需要过滤的 KeyValue 实例时,则拓展过滤整行数据。下面是一个使用示例: ```java -// 定义ValueFilter过滤器 +// 定义 ValueFilter 过滤器 Filter filter1 = new ValueFilter(CompareOperator.NOT_EQUAL, new BinaryComparator(Bytes.toBytes("xxx"))); -// 使用SkipFilter进行包装 +// 使用 SkipFilter 进行包装 Filter filter2 = new SkipFilter(filter1); ``` @@ -335,7 +335,7 @@ Filter filter2 = new SkipFilter(filter1); ### 5.2 WhileMatchFilter过滤器 -`WhileMatchFilter`包装一个过滤器,当被包装的过滤器遇到一个需要过滤的KeyValue实例时,`WhileMatchFilter`则结束本次扫描,返回已经扫描到的结果。下面是其使用示例: +`WhileMatchFilter` 包装一个过滤器,当被包装的过滤器遇到一个需要过滤的 KeyValue 实例时,`WhileMatchFilter` 则结束本次扫描,返回已经扫描到的结果。下面是其使用示例: ```java Filter filter1 = new RowFilter(CompareOperator.NOT_EQUAL, @@ -353,7 +353,7 @@ scanner1.close(); System.out.println("--------------------"); -// 使用WhileMatchFilter进行包装 +// 使用 WhileMatchFilter 进行包装 Filter filter2 = new WhileMatchFilter(filter1); scan.setFilter(filter2); @@ -383,11 +383,11 @@ rowKey2/student:name/1555035007025/Put/vlen=8/seqid=0 rowKey3/student:name/1555035007037/Put/vlen=8/seqid=0 ``` -可以看到被包装后,只返回了`rowKey4`之前的数据。 +可以看到被包装后,只返回了 `rowKey4` 之前的数据。 ## 六、FilterList -以上都是讲解单个过滤器的作用,当需要多个过滤器共同作用于一次查询的时候,就需要使用`FilterList`。`FilterList`支持通过构造器或者`addFilter`方法传入多个过滤器。 +以上都是讲解单个过滤器的作用,当需要多个过滤器共同作用于一次查询的时候,就需要使用 `FilterList`。`FilterList` 支持通过构造器或者 `addFilter` 方法传入多个过滤器。 ```java // 构造器传入 @@ -400,10 +400,10 @@ public FilterList(final Filter... filters) public void addFilter(Filter filter) ``` -多个过滤器组合的结果由`operator`参数定义 ,其可选参数定义在`Operator`枚举类中。只有`MUST_PASS_ALL`和`MUST_PASS_ONE`两个可选的值: +多个过滤器组合的结果由 `operator` 参数定义 ,其可选参数定义在 `Operator` 枚举类中。只有 `MUST_PASS_ALL` 和 `MUST_PASS_ONE` 两个可选的值: -+ **MUST_PASS_ALL** :相当于AND,必须所有的过滤器都通过才认为通过; -+ **MUST_PASS_ONE** :相当于OR,只有要一个过滤器通过则认为通过。 ++ **MUST_PASS_ALL** :相当于 AND,必须所有的过滤器都通过才认为通过; ++ **MUST_PASS_ONE** :相当于 OR,只有要一个过滤器通过则认为通过。 ```java @InterfaceAudience.Public diff --git a/notes/HiveCLI和Beeline命令行的基本使用.md b/notes/HiveCLI和Beeline命令行的基本使用.md index 0a8ec51..a760a76 100644 --- a/notes/HiveCLI和Beeline命令行的基本使用.md +++ b/notes/HiveCLI和Beeline命令行的基本使用.md @@ -25,15 +25,15 @@ ### 1.1 Help -使用`hive -H`或者 `hive --help`命令可以查看所有命令的帮助,显示如下: +使用 `hive -H` 或者 `hive --help` 命令可以查看所有命令的帮助,显示如下: ``` usage: hive -d,--define Variable subsitution to apply to hive commands. e.g. -d A=B or --define A=B --定义用户自定义变量 --database Specify the database to use -- 指定使用的数据库 - -e SQL from command line -- 执行指定的SQL - -f SQL from files --执行SQL脚本 + -e SQL from command line -- 执行指定的 SQL + -f SQL from files --执行 SQL 脚本 -H,--help Print help information -- 打印帮助信息 --hiveconf Use value for given property --自定义配置 --hivevar Variable subsitution to apply to hive --自定义变量 @@ -45,11 +45,11 @@ usage: hive ### 1.2 交互式命令行 -直接使用`Hive`命令,不加任何参数,即可进入交互式命令行。 +直接使用 `Hive` 命令,不加任何参数,即可进入交互式命令行。 ### 1.3 执行SQL命令 -在不进入交互式命令行的情况下,可以使用`hive -e `执行SQL命令。 +在不进入交互式命令行的情况下,可以使用 `hive -e ` 执行 SQL 命令。 ```sql hive -e 'select * from emp'; @@ -61,7 +61,7 @@ hive -e 'select * from emp'; ### 1.4 执行SQL脚本 -用于执行的sql脚本可以在本地文件系统,也可以在HDFS上。 +用于执行的 sql 脚本可以在本地文件系统,也可以在 HDFS 上。 ```shell # 本地文件系统 @@ -71,7 +71,7 @@ hive -f /usr/file/simple.sql; hive -f hdfs://hadoop001:8020/tmp/simple.sql; ``` -其中`simple.sql`内容如下: +其中 `simple.sql` 内容如下: ```sql select * from emp; @@ -79,7 +79,7 @@ select * from emp; ### 1.5 配置Hive变量 -可以使用`--hiveconf`设置Hive运行时的变量。 +可以使用 `--hiveconf` 设置 Hive 运行时的变量。 ```sql hive -e 'select * from emp' \ @@ -87,27 +87,27 @@ hive -e 'select * from emp' \ --hiveconf mapred.reduce.tasks=4; ``` -> hive.exec.scratchdir:指定HDFS上目录位置,用于存储不同map/reduce阶段的执行计划和这些阶段的中间输出结果。 +> hive.exec.scratchdir:指定 HDFS 上目录位置,用于存储不同 map/reduce 阶段的执行计划和这些阶段的中间输出结果。 ### 1.6 配置文件启动 -使用`-i`可以在进入交互模式之前运行初始化脚本,相当于指定配置文件启动。 +使用 `-i` 可以在进入交互模式之前运行初始化脚本,相当于指定配置文件启动。 ```shell hive -i /usr/file/hive-init.conf; ``` -其中`hive-init.conf`的内容如下: +其中 `hive-init.conf` 的内容如下: ```sql set hive.exec.mode.local.auto = true; ``` -> hive.exec.mode.local.auto 默认值为false,这里设置为true ,代表开启本地模式。 +> hive.exec.mode.local.auto 默认值为 false,这里设置为 true ,代表开启本地模式。 ### 1.7 用户自定义变量 -`--define `和`--hivevar `在功能上是等价的,都是用来实现自定义变量,这里给出一个示例: +`--define ` 和 `--hivevar ` 在功能上是等价的,都是用来实现自定义变量,这里给出一个示例: 定义变量: @@ -135,15 +135,15 @@ hive > select ${hivevar:j} from emp; ### 2.1 HiveServer2 -Hive内置了HiveServer和HiveServer2服务,两者都允许客户端使用多种编程语言进行连接,但是HiveServer不能处理多个客户端的并发请求,所以产生了HiveServer2。 +Hive 内置了 HiveServer 和 HiveServer2 服务,两者都允许客户端使用多种编程语言进行连接,但是 HiveServer 不能处理多个客户端的并发请求,所以产生了 HiveServer2。 -HiveServer2(HS2)允许远程客户端可以使用各种编程语言向Hive提交请求并检索结果,支持多客户端并发访问和身份验证。HS2是由多个服务组成的单个进程,其包括基于Thrift的Hive服务(TCP或HTTP)和用于Web UI的Jetty Web服务器。 +HiveServer2(HS2)允许远程客户端可以使用各种编程语言向 Hive 提交请求并检索结果,支持多客户端并发访问和身份验证。HS2 是由多个服务组成的单个进程,其包括基于 Thrift 的 Hive 服务(TCP 或 HTTP)和用于 Web UI 的 Jetty Web 服务器。 - HiveServer2拥有自己的CLI(Beeline),Beeline是一个基于SQLLine的JDBC客户端。由于HiveServer2是Hive开发维护的重点(Hive0.15后就不再支持hiveserver),所以Hive CLI已经不推荐使用了,官方更加推荐使用Beeline。 + HiveServer2 拥有自己的 CLI(Beeline),Beeline 是一个基于 SQLLine 的 JDBC 客户端。由于 HiveServer2 是 Hive 开发维护的重点 (Hive0.15 后就不再支持 hiveserver),所以 Hive CLI 已经不推荐使用了,官方更加推荐使用 Beeline。 ### 2.1 Beeline -Beeline拥有更多可使用参数,可以使用`beeline --help` 查看,完整参数如下: +Beeline 拥有更多可使用参数,可以使用 `beeline --help` 查看,完整参数如下: ```properties Usage: java org.apache.hive.cli.beeline.BeeLine @@ -192,22 +192,22 @@ Usage: java org.apache.hive.cli.beeline.BeeLine ### 2.3 常用参数 -在Hive CLI中支持的参数,Beeline都支持,常用的参数如下。更多参数说明可以参见官方文档 [Beeline Command Options](https://cwiki.apache.org/confluence/display/Hive/HiveServer2+Clients#HiveServer2Clients-Beeline%E2%80%93NewCommandLineShell) +在 Hive CLI 中支持的参数,Beeline 都支持,常用的参数如下。更多参数说明可以参见官方文档 [Beeline Command Options](https://cwiki.apache.org/confluence/display/Hive/HiveServer2+Clients#HiveServer2Clients-Beeline%E2%80%93NewCommandLineShell) | 参数 | 说明 | | -------------------------------------- | ------------------------------------------------------------ | | **-u \** | 数据库地址 | | **-n \** | 用户名 | | **-p \** | 密码 | -| **-d \** | 驱动(可选) | -| **-e \** | 执行SQL命令 | -| **-f \** | 执行SQL脚本 | +| **-d \** | 驱动 (可选) | +| **-e \** | 执行 SQL 命令 | +| **-f \** | 执行 SQL 脚本 | | **-i (or)--init \** | 在进入交互模式之前运行初始化脚本 | | **--property-file \** | 指定配置文件 | | **--hiveconf** *property**=**value* | 指定配置属性 | | **--hivevar** *name**=**value* | 用户自定义属性,在会话级别有效 | -示例: 使用用户名和密码连接Hive +示例: 使用用户名和密码连接 Hive ```shell $ beeline -u jdbc:hive2://localhost:10000 -n username -p password @@ -217,18 +217,18 @@ $ beeline -u jdbc:hive2://localhost:10000 -n username -p password ## 三、Hive配置 -可以通过三种方式对Hive的相关属性进行配置,分别介绍如下: +可以通过三种方式对 Hive 的相关属性进行配置,分别介绍如下: ### 3.1 配置文件 -方式一为使用配置文件,使用配置文件指定的配置是永久有效的。Hive有以下三个可选的配置文件: +方式一为使用配置文件,使用配置文件指定的配置是永久有效的。Hive 有以下三个可选的配置文件: -+ hive-site.xml :Hive的主要配置文件; ++ hive-site.xml :Hive 的主要配置文件; + hivemetastore-site.xml: 关于元数据的配置; -+ hiveserver2-site.xml:关于HiveServer2的配置。 ++ hiveserver2-site.xml:关于 HiveServer2 的配置。 -示例如下,在hive-site.xml配置`hive.exec.scratchdir`: +示例如下,在 hive-site.xml 配置 `hive.exec.scratchdir`: ```xml @@ -240,7 +240,7 @@ $ beeline -u jdbc:hive2://localhost:10000 -n username -p password ### 3.2 hiveconf -方式二为在启动命令行(Hive CLI / Beeline)的时候使用`--hiveconf`指定配置,这种方式指定的配置作用于整个Session。 +方式二为在启动命令行 (Hive CLI / Beeline) 的时候使用 `--hiveconf` 指定配置,这种方式指定的配置作用于整个 Session。 ``` hive --hiveconf hive.exec.scratchdir=/tmp/mydir @@ -248,7 +248,7 @@ hive --hiveconf hive.exec.scratchdir=/tmp/mydir ### 3.3 set -方式三为在交互式环境下(Hive CLI / Beeline),使用set命令指定。这种设置的作用范围也是Session级别的,配置对于执行该命令后的所有命令生效。set兼具设置参数和查看参数的功能。如下: +方式三为在交互式环境下 (Hive CLI / Beeline),使用 set 命令指定。这种设置的作用范围也是 Session 级别的,配置对于执行该命令后的所有命令生效。set 兼具设置参数和查看参数的功能。如下: ```shell 0: jdbc:hive2://hadoop001:10000> set hive.exec.scratchdir=/tmp/mydir; @@ -263,12 +263,12 @@ No rows affected (0.025 seconds) ### 3.4 配置优先级 -配置的优先顺序如下(由低到高): +配置的优先顺序如下 (由低到高): `hive-site.xml` - >` hivemetastore-site.xml `- > `hiveserver2-site.xml` - >` -- hiveconf`- > `set` ### 3.5 配置参数 -Hive可选的配置参数非常多,在用到时查阅官方文档即可[AdminManual Configuration](https://cwiki.apache.org/confluence/display/Hive/AdminManual+Configuration) +Hive 可选的配置参数非常多,在用到时查阅官方文档即可[AdminManual Configuration](https://cwiki.apache.org/confluence/display/Hive/AdminManual+Configuration) diff --git a/notes/Hive分区表和分桶表.md b/notes/Hive分区表和分桶表.md index 32585de..19fdf8e 100644 --- a/notes/Hive分区表和分桶表.md +++ b/notes/Hive分区表和分桶表.md @@ -11,11 +11,11 @@ ### 1.1 概念 -Hive中的表对应为HDFS上的指定目录,在查询数据时候,默认会对全表进行扫描,这样时间和性能的消耗都非常大。 +Hive 中的表对应为 HDFS 上的指定目录,在查询数据时候,默认会对全表进行扫描,这样时间和性能的消耗都非常大。 -**分区为HDFS上表目录的子目录**,数据按照分区存储在子目录中。如果查询的`where`字句的中包含分区条件,则直接从该分区去查找,而不是扫描整个表目录,合理的分区设计可以极大提高查询速度和性能。 +**分区为 HDFS 上表目录的子目录**,数据按照分区存储在子目录中。如果查询的 `where` 字句的中包含分区条件,则直接从该分区去查找,而不是扫描整个表目录,合理的分区设计可以极大提高查询速度和性能。 ->这里说明一下分区表并Hive独有的概念,实际上这个概念非常常见。比如在我们常用的Oracle数据库中,当表中的数据量不断增大,查询数据的速度就会下降,这时也可以对表进行分区。表进行分区后,逻辑上表仍然是一张完整的表,只是将表中的数据存放到多个表空间(物理文件上),这样查询数据时,就不必要每次都扫描整张表,从而提升查询性能。 +>这里说明一下分区表并 Hive 独有的概念,实际上这个概念非常常见。比如在我们常用的 Oracle 数据库中,当表中的数据量不断增大,查询数据的速度就会下降,这时也可以对表进行分区。表进行分区后,逻辑上表仍然是一张完整的表,只是将表中的数据存放到多个表空间(物理文件上),这样查询数据时,就不必要每次都扫描整张表,从而提升查询性能。 ### 1.2 使用场景 @@ -23,7 +23,7 @@ Hive中的表对应为HDFS上的指定目录,在查询数据时候,默认会 ### 1.3 创建分区表 -在Hive中可以使用`PARTITIONED BY`子句创建分区表。表可以包含一个或多个分区列,程序会为分区列中的每个不同值组合创建单独的数据目录。下面的我们创建一张雇员表作为测试: +在 Hive 中可以使用 `PARTITIONED BY` 子句创建分区表。表可以包含一个或多个分区列,程序会为分区列中的每个不同值组合创建单独的数据目录。下面的我们创建一张雇员表作为测试: ```shell CREATE EXTERNAL TABLE emp_partition( @@ -53,13 +53,13 @@ LOAD DATA LOCAL INPATH "/usr/file/emp30.txt" OVERWRITE INTO TABLE emp_partition ### 1.5 查看分区目录 -这时候我们直接查看表目录,可以看到表目录下存在两个子目录,分别是`deptno=20`和`deptno=30`,这就是分区目录,分区目录下才是我们加载的数据文件。 +这时候我们直接查看表目录,可以看到表目录下存在两个子目录,分别是 `deptno=20` 和 `deptno=30`,这就是分区目录,分区目录下才是我们加载的数据文件。 ```shell # hadoop fs -ls hdfs://hadoop001:8020/hive/emp_partition/ ``` -这时候当你的查询语句的`where`包含`deptno=20`,则就去对应的分区目录下进行查找,而不用扫描全表。 +这时候当你的查询语句的 `where` 包含 `deptno=20`,则就去对应的分区目录下进行查找,而不用扫描全表。
@@ -69,15 +69,15 @@ LOAD DATA LOCAL INPATH "/usr/file/emp30.txt" OVERWRITE INTO TABLE emp_partition ### 1.1 简介 -分区提供了一个隔离数据和优化查询的可行方案,但是并非所有的数据集都可以形成合理的分区,分区的数量也不是越多越好,过多的分区条件可能会导致很多分区上没有数据。同时Hive会限制动态分区可以创建的最大分区数,用来避免过多分区文件对文件系统产生负担。鉴于以上原因,Hive还提供了一种更加细粒度的数据拆分方案:分桶表(bucket Table)。 +分区提供了一个隔离数据和优化查询的可行方案,但是并非所有的数据集都可以形成合理的分区,分区的数量也不是越多越好,过多的分区条件可能会导致很多分区上没有数据。同时 Hive 会限制动态分区可以创建的最大分区数,用来避免过多分区文件对文件系统产生负担。鉴于以上原因,Hive 还提供了一种更加细粒度的数据拆分方案:分桶表 (bucket Table)。 -分桶表会将指定列的值进行哈希散列,并对bucket(桶数量)取余,然后存储到对应的bucket(桶)中。 +分桶表会将指定列的值进行哈希散列,并对 bucket(桶数量)取余,然后存储到对应的 bucket(桶)中。 ### 1.2 理解分桶表 -单从概念上理解分桶表可能会比较晦涩,其实和分区一样,分桶这个概念同样不是Hive独有的,对于Java开发人员而言,这可能是一个每天都会用到的概念,因为Hive中的分桶概念和Java数据结构中的HashMap的分桶概念是一致的。 +单从概念上理解分桶表可能会比较晦涩,其实和分区一样,分桶这个概念同样不是 Hive 独有的,对于 Java 开发人员而言,这可能是一个每天都会用到的概念,因为 Hive 中的分桶概念和 Java 数据结构中的 HashMap 的分桶概念是一致的。 -当调用HashMap的put()方法存储数据时,程序会先对key值调用hashCode()方法计算出hashcode,然后对数组长度取模计算出index,最后将数据存储在数组index位置的链表上,链表达到一定阈值后会转换为红黑树(JDK1.8+)。下图为HashMap的数据结构图: +当调用 HashMap 的 put() 方法存储数据时,程序会先对 key 值调用 hashCode() 方法计算出 hashcode,然后对数组长度取模计算出 index,最后将数据存储在数组 index 位置的链表上,链表达到一定阈值后会转换为红黑树 (JDK1.8+)。下图为 HashMap 的数据结构图:
@@ -85,7 +85,7 @@ LOAD DATA LOCAL INPATH "/usr/file/emp30.txt" OVERWRITE INTO TABLE emp_partition ### 1.3 创建分桶表 -在Hive中,我们可以通过`CLUSTERED BY`指定分桶列,并通过`SORTED BY`指定桶中数据的排序参考列。下面为分桶表建表语句示例: +在 Hive 中,我们可以通过 `CLUSTERED BY` 指定分桶列,并通过 `SORTED BY` 指定桶中数据的排序参考列。下面为分桶表建表语句示例: ```sql CREATE EXTERNAL TABLE emp_bucket( @@ -97,37 +97,37 @@ LOAD DATA LOCAL INPATH "/usr/file/emp30.txt" OVERWRITE INTO TABLE emp_partition sal DECIMAL(7,2), comm DECIMAL(7,2), deptno INT) - CLUSTERED BY(empno) SORTED BY(empno ASC) INTO 4 BUCKETS --按照员工编号散列到四个bucket中 + CLUSTERED BY(empno) SORTED BY(empno ASC) INTO 4 BUCKETS --按照员工编号散列到四个 bucket 中 ROW FORMAT DELIMITED FIELDS TERMINATED BY "\t" LOCATION '/hive/emp_bucket'; ``` ### 1.4 加载数据到分桶表 -这里直接使用`Load`语句向分桶表加载数据,数据时可以加载成功的,但是数据并不会分桶。 +这里直接使用 `Load` 语句向分桶表加载数据,数据时可以加载成功的,但是数据并不会分桶。 -这是由于分桶的实质是对指定字段做了hash散列然后存放到对应文件中,这意味着向分桶表中插入数据是必然要通过MapReduce,且Reducer的数量必须等于分桶的数量。由于以上原因,分桶表的数据通常只能使用CTAS(CREATE TABLE AS SELECT)方式插入,因为CTAS操作会触发MapReduce。加载数据步骤如下: +这是由于分桶的实质是对指定字段做了 hash 散列然后存放到对应文件中,这意味着向分桶表中插入数据是必然要通过 MapReduce,且 Reducer 的数量必须等于分桶的数量。由于以上原因,分桶表的数据通常只能使用 CTAS(CREATE TABLE AS SELECT) 方式插入,因为 CTAS 操作会触发 MapReduce。加载数据步骤如下: #### 1. 设置强制分桶 ```sql -set hive.enforce.bucketing = true; --Hive 2.x不需要这一步 +set hive.enforce.bucketing = true; --Hive 2.x 不需要这一步 ``` -在Hive 0.x and 1.x版本,必须使用设置`hive.enforce.bucketing = true`,表示强制分桶,允许程序根据表结构自动选择正确数量的Reducer和cluster by column来进行分桶。 +在 Hive 0.x and 1.x 版本,必须使用设置 `hive.enforce.bucketing = true`,表示强制分桶,允许程序根据表结构自动选择正确数量的 Reducer 和 cluster by column 来进行分桶。 #### 2. CTAS导入数据 ```sql -INSERT INTO TABLE emp_bucket SELECT * FROM emp; --这里的emp表就是一张普通的雇员表 +INSERT INTO TABLE emp_bucket SELECT * FROM emp; --这里的 emp 表就是一张普通的雇员表 ``` -可以从执行日志看到CTAS触发MapReduce操作,且Reducer数量和建表时候指定bucket数量一致: +可以从执行日志看到 CTAS 触发 MapReduce 操作,且 Reducer 数量和建表时候指定 bucket 数量一致:
### 1.5 查看分桶文件 -bucket(桶)本质上就是表目录下的具体文件: +bucket(桶) 本质上就是表目录下的具体文件:
@@ -135,7 +135,7 @@ bucket(桶)本质上就是表目录下的具体文件: ## 三、分区表和分桶表结合使用 -分区表和分桶表的本质都是将数据按照不同粒度进行拆分,从而使得在查询时候不必扫描全表,只需要扫描对应的分区或分桶,从而提升查询效率。两者可以结合起来使用,从而保证表数据在不同粒度上都能得到合理的拆分。下面是Hive官方给出的示例: +分区表和分桶表的本质都是将数据按照不同粒度进行拆分,从而使得在查询时候不必扫描全表,只需要扫描对应的分区或分桶,从而提升查询效率。两者可以结合起来使用,从而保证表数据在不同粒度上都能得到合理的拆分。下面是 Hive 官方给出的示例: ```sql CREATE TABLE page_view_bucketed( diff --git a/notes/Hive常用DDL操作.md b/notes/Hive常用DDL操作.md index cdbb27a..6961997 100644 --- a/notes/Hive常用DDL操作.md +++ b/notes/Hive常用DDL操作.md @@ -51,9 +51,9 @@ USE database_name; 语法: ```sql -CREATE (DATABASE|SCHEMA) [IF NOT EXISTS] database_name --DATABASE|SCHEMA是等价的 +CREATE (DATABASE|SCHEMA) [IF NOT EXISTS] database_name --DATABASE|SCHEMA 是等价的 [COMMENT database_comment] --数据库注释 - [LOCATION hdfs_path] --存储在HDFS上的位置 + [LOCATION hdfs_path] --存储在 HDFS 上的位置 [WITH DBPROPERTIES (property_name=property_value, ...)]; --指定额外属性 ``` @@ -91,7 +91,7 @@ DESC DATABASE EXTENDED hive_test; DROP (DATABASE|SCHEMA) [IF EXISTS] database_name [RESTRICT|CASCADE]; ``` -+ 默认行为是RESTRICT,如果数据库中存在表则删除失败。要想删除库及其中的表,可以使用CASCADE级联删除。 ++ 默认行为是 RESTRICT,如果数据库中存在表则删除失败。要想删除库及其中的表,可以使用 CASCADE 级联删除。 示例: @@ -159,7 +159,7 @@ CREATE [TEMPORARY] [EXTERNAL] TABLE [IF NOT EXISTS] [db_name.]table_name -- LOCATION '/hive/emp_external'; ``` -使用 `desc format emp_external`命令可以查看表的详细信息如下: +使用 `desc format emp_external` 命令可以查看表的详细信息如下:
@@ -192,14 +192,14 @@ CREATE [TEMPORARY] [EXTERNAL] TABLE [IF NOT EXISTS] [db_name.]table_name -- sal DECIMAL(7,2), comm DECIMAL(7,2), deptno INT) - CLUSTERED BY(empno) SORTED BY(empno ASC) INTO 4 BUCKETS --按照员工编号散列到四个bucket中 + CLUSTERED BY(empno) SORTED BY(empno ASC) INTO 4 BUCKETS --按照员工编号散列到四个 bucket 中 ROW FORMAT DELIMITED FIELDS TERMINATED BY "\t" LOCATION '/hive/emp_bucket'; ``` ### 2.6 倾斜表 -通过指定一个或者多个列经常出现的值(严重偏斜),Hive会自动将涉及到这些值的数据拆分为单独的文件。在查询时,如果涉及到倾斜值,它就直接从独立文件中获取数据,而不是扫描所有文件,这使得性能得到提升。 +通过指定一个或者多个列经常出现的值(严重偏斜),Hive 会自动将涉及到这些值的数据拆分为单独的文件。在查询时,如果涉及到倾斜值,它就直接从独立文件中获取数据,而不是扫描所有文件,这使得性能得到提升。 ```sql CREATE EXTERNAL TABLE emp_skewed( @@ -211,14 +211,14 @@ CREATE [TEMPORARY] [EXTERNAL] TABLE [IF NOT EXISTS] [db_name.]table_name -- sal DECIMAL(7,2), comm DECIMAL(7,2) ) - SKEWED BY (empno) ON (66,88,100) --指定empno的倾斜值66,88,100 + SKEWED BY (empno) ON (66,88,100) --指定 empno 的倾斜值 66,88,100 ROW FORMAT DELIMITED FIELDS TERMINATED BY "\t" LOCATION '/hive/emp_skewed'; ``` ### 2.7 临时表 -临时表仅对当前session可见,临时表的数据将存储在用户的暂存目录中,并在会话结束后删除。如果临时表与永久表表名相同,则对该表名的任何引用都将解析为临时表,而不是永久表。临时表还具有以下两个限制: +临时表仅对当前 session 可见,临时表的数据将存储在用户的暂存目录中,并在会话结束后删除。如果临时表与永久表表名相同,则对该表名的任何引用都将解析为临时表,而不是永久表。临时表还具有以下两个限制: + 不支持分区列; + 不支持创建索引。 @@ -264,14 +264,14 @@ CREATE TEMPORARY EXTERNAL TABLE IF NOT EXISTS emp_co LIKE emp ### 2.10 加载数据到表 -加载数据到表中属于DML操作,这里为了方便大家测试,先简单介绍一下加载本地数据到表中: +加载数据到表中属于 DML 操作,这里为了方便大家测试,先简单介绍一下加载本地数据到表中: ```sql --- 加载数据到emp表中 +-- 加载数据到 emp 表中 load data local inpath "/usr/file/emp.txt" into table emp; ``` -其中emp.txt的内容如下,你可以直接复制使用,也可以到本仓库的[resources](https://github.com/heibaiying/BigData-Notes/tree/master/resources)目录下载: +其中 emp.txt 的内容如下,你可以直接复制使用,也可以到本仓库的[resources](https://github.com/heibaiying/BigData-Notes/tree/master/resources) 目录下载: ```txt 7369 SMITH CLERK 7902 1980-12-17 00:00:00 800.00 20 @@ -309,7 +309,7 @@ ALTER TABLE table_name RENAME TO new_table_name; 示例: ```sql -ALTER TABLE emp_temp RENAME TO new_emp; --把emp_temp表重命名为new_emp +ALTER TABLE emp_temp RENAME TO new_emp; --把 emp_temp 表重命名为 new_emp ``` @@ -329,7 +329,7 @@ ALTER TABLE table_name [PARTITION partition_spec] CHANGE [COLUMN] col_old_name c -- 修改字段名和类型 ALTER TABLE emp_temp CHANGE empno empno_new INT; --- 修改字段sal的名称 并将其放置到empno字段后 +-- 修改字段 sal 的名称 并将其放置到 empno 字段后 ALTER TABLE emp_temp CHANGE sal sal_new decimal(7,2) AFTER ename; -- 为字段增加注释 @@ -359,7 +359,7 @@ ALTER TABLE emp_temp ADD COLUMNS (address STRING COMMENT 'home address'); TRUNCATE TABLE table_name [PARTITION (partition_column = partition_col_value, ...)]; ``` -+ 目前只有内部表才能执行TRUNCATE操作,外部表执行时会抛出异常`Cannot truncate non-managed table XXXX`。 ++ 目前只有内部表才能执行 TRUNCATE 操作,外部表执行时会抛出异常 `Cannot truncate non-managed table XXXX`。 示例: @@ -377,8 +377,8 @@ TRUNCATE TABLE emp_mgt_ptn PARTITION (deptno=20); DROP TABLE [IF EXISTS] table_name [PURGE]; ``` -+ 内部表:不仅会删除表的元数据,同时会删除HDFS上的数据; -+ 外部表:只会删除表的元数据,不会删除HDFS上的数据; ++ 内部表:不仅会删除表的元数据,同时会删除 HDFS 上的数据; ++ 外部表:只会删除表的元数据,不会删除 HDFS 上的数据; + 删除视图引用的表时,不会给出警告(但视图已经无效了,必须由用户删除或重新创建)。 @@ -413,7 +413,7 @@ SHOW (DATABASES|SCHEMAS) [LIKE 'identifier_with_wildcards']; SHOW DATABASES like 'hive*'; ``` -LIKE子句允许使用正则表达式进行过滤,但是SHOW语句当中的LIKE子句只支持`*`(通配符)和`|`(条件或)两个符号。例如`employees`,`emp *`,`emp * | * ees`,所有这些都将匹配名为`employees`的数据库。 +LIKE 子句允许使用正则表达式进行过滤,但是 SHOW 语句当中的 LIKE 子句只支持 `*`(通配符)和 `|`(条件或)两个符号。例如 `employees`,`emp *`,`emp * | * ees`,所有这些都将匹配名为 `employees` 的数据库。 **2. 查看表的列表** @@ -428,7 +428,7 @@ SHOW TABLES IN default; **3. 查看视图列表** ```sql -SHOW VIEWS [IN/FROM database_name] [LIKE 'pattern_with_wildcards']; --仅支持Hive 2.2.0 + +SHOW VIEWS [IN/FROM database_name] [LIKE 'pattern_with_wildcards']; --仅支持 Hive 2.2.0 + ``` **4. 查看表的分区列表** @@ -447,4 +447,4 @@ SHOW CREATE TABLE ([db_name.]table_name|view_name); ## 参考资料 -[LanguageManual DDL](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL) \ No newline at end of file +[LanguageManual DDL](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL) diff --git a/notes/Hive常用DML操作.md b/notes/Hive常用DML操作.md index af21b29..6d01f02 100644 --- a/notes/Hive常用DML操作.md +++ b/notes/Hive常用DML操作.md @@ -18,24 +18,24 @@ LOAD DATA [LOCAL] INPATH 'filepath' [OVERWRITE] INTO TABLE tablename [PARTITION (partcol1=val1, partcol2=val2 ...)] ``` -- `LOCAL`关键字代表从本地文件系统加载文件,省略则代表从HDFS上加载文件: -+ 从本地文件系统加载文件时, `filepath`可以是绝对路径也可以是相对路径(建议使用绝对路径); +- `LOCAL` 关键字代表从本地文件系统加载文件,省略则代表从 HDFS 上加载文件: ++ 从本地文件系统加载文件时, `filepath` 可以是绝对路径也可以是相对路径 (建议使用绝对路径); -+ 从HDFS加载文件时候,`filepath`为文件完整的URL地址:如`hdfs://namenode:port/user/hive/project/ data1` ++ 从 HDFS 加载文件时候,`filepath` 为文件完整的 URL 地址:如 `hdfs://namenode:port/user/hive/project/ data1` -- `filepath`可以是文件路径(在这种情况下Hive会将文件移动到表中),也可以目录路径(在这种情况下,Hive会将该目录中的所有文件移动到表中); +- `filepath` 可以是文件路径 (在这种情况下 Hive 会将文件移动到表中),也可以目录路径 (在这种情况下,Hive 会将该目录中的所有文件移动到表中); -- 如果使用OVERWRITE关键字,则将删除目标表(或分区)的内容,使用新的数据填充;不使用此关键字,则数据以追加的方式加入; +- 如果使用 OVERWRITE 关键字,则将删除目标表(或分区)的内容,使用新的数据填充;不使用此关键字,则数据以追加的方式加入; - 加载的目标可以是表或分区。如果是分区表,则必须指定加载数据的分区; -- 加载文件的格式必须与建表时使用` STORED AS`指定的存储格式相同。 +- 加载文件的格式必须与建表时使用 ` STORED AS` 指定的存储格式相同。 > 使用建议: > -> **不论是本地路径还是URL都建议使用完整的**。虽然可以使用不完整的URL地址,此时Hive将使用hadoop中的fs.default.name配置来推断地址,但是为避免不必要的错误,建议使用完整的本地路径或URL地址; +> **不论是本地路径还是 URL 都建议使用完整的**。虽然可以使用不完整的 URL 地址,此时 Hive 将使用 hadoop 中的 fs.default.name 配置来推断地址,但是为避免不必要的错误,建议使用完整的本地路径或 URL 地址; > -> **加载对象是分区表时建议显示指定分区**。在Hive 3.0之后,内部将加载(LOAD)重写为INSERT AS SELECT,此时如果不指定分区,INSERT AS SELECT将假设最后一组列是分区列,如果该列不是表定义的分区,它将抛出错误。为避免错误,还是建议显示指定分区。 +> **加载对象是分区表时建议显示指定分区**。在 Hive 3.0 之后,内部将加载 (LOAD) 重写为 INSERT AS SELECT,此时如果不指定分区,INSERT AS SELECT 将假设最后一组列是分区列,如果该列不是表定义的分区,它将抛出错误。为避免错误,还是建议显示指定分区。 ### 1.2 示例 @@ -55,15 +55,15 @@ INTO TABLE tablename [PARTITION (partcol1=val1, partcol2=val2 ...)] ROW FORMAT DELIMITED FIELDS TERMINATED BY "\t"; ``` -从HDFS上加载数据到分区表: +从 HDFS 上加载数据到分区表: ```sql LOAD DATA INPATH "hdfs://hadoop001:8020/mydir/emp.txt" OVERWRITE INTO TABLE emp_ptn PARTITION (deptno=20); ``` -> emp.txt文件可在本仓库的resources目录中下载 +> emp.txt 文件可在本仓库的 resources 目录中下载 -加载后表中数据如下,分区列deptno全部赋值成20: +加载后表中数据如下,分区列 deptno 全部赋值成 20:
@@ -79,15 +79,15 @@ INSERT INTO TABLE tablename1 [PARTITION (partcol1=val1, partcol2=val2 ...)] select_statement1 FROM from_statement; ``` -+ Hive 0.13.0开始,建表时可以通过使用TBLPROPERTIES(“immutable”=“true”)来创建不可变表(immutable table) ,如果不可以变表中存在数据,则INSERT INTO失败。(注:INSERT OVERWRITE的语句不受`immutable`属性的影响); ++ Hive 0.13.0 开始,建表时可以通过使用 TBLPROPERTIES(“immutable”=“true”)来创建不可变表 (immutable table) ,如果不可以变表中存在数据,则 INSERT INTO 失败。(注:INSERT OVERWRITE 的语句不受 `immutable` 属性的影响); + 可以对表或分区执行插入操作。如果表已分区,则必须通过指定所有分区列的值来指定表的特定分区; -+ 从Hive 1.1.0开始,TABLE关键字是可选的; ++ 从 Hive 1.1.0 开始,TABLE 关键字是可选的; -+ 从Hive 1.2.0开始 ,可以采用INSERT INTO tablename(z,x,c1)指明插入列; ++ 从 Hive 1.2.0 开始 ,可以采用 INSERT INTO tablename(z,x,c1) 指明插入列; -+ 可以将SELECT语句的查询结果插入多个表(或分区),称为多表插入。语法如下: ++ 可以将 SELECT 语句的查询结果插入多个表(或分区),称为多表插入。语法如下: ```sql FROM from_statement @@ -107,22 +107,22 @@ INSERT INTO TABLE tablename PARTITION (partcol1[=val1], partcol2[=val2] ...) select_statement FROM from_statement; ``` -在向分区表插入数据时候,分区列名是必须的,但是列值是可选的。如果给出了分区列值,我们将其称为静态分区,否则它是动态分区。动态分区列必须在SELECT语句的列中最后指定,并且与它们在PARTITION()子句中出现的顺序相同。 +在向分区表插入数据时候,分区列名是必须的,但是列值是可选的。如果给出了分区列值,我们将其称为静态分区,否则它是动态分区。动态分区列必须在 SELECT 语句的列中最后指定,并且与它们在 PARTITION() 子句中出现的顺序相同。 -注意:Hive 0.9.0之前的版本动态分区插入是默认禁用的,而0.9.0之后的版本则默认启用。以下是动态分区的相关配置: +注意:Hive 0.9.0 之前的版本动态分区插入是默认禁用的,而 0.9.0 之后的版本则默认启用。以下是动态分区的相关配置: | 配置 | 默认值 | 说明 | | ------------------------------------------ | -------- | ------------------------------------------------------------ | -| `hive.exec.dynamic.partition` | `true` | 需要设置为true才能启用动态分区插入 | -| `hive.exec.dynamic.partition.mode` | `strict` | 在严格模式(strict)下,用户必须至少指定一个静态分区,以防用户意外覆盖所有分区,在非严格模式下,允许所有分区都是动态的 | -| `hive.exec.max.dynamic.partitions.pernode` | 100 | 允许在每个mapper/reducer节点中创建的最大动态分区数 | +| `hive.exec.dynamic.partition` | `true` | 需要设置为 true 才能启用动态分区插入 | +| `hive.exec.dynamic.partition.mode` | `strict` | 在严格模式 (strict) 下,用户必须至少指定一个静态分区,以防用户意外覆盖所有分区,在非严格模式下,允许所有分区都是动态的 | +| `hive.exec.max.dynamic.partitions.pernode` | 100 | 允许在每个 mapper/reducer 节点中创建的最大动态分区数 | | `hive.exec.max.dynamic.partitions` | 1000 | 允许总共创建的最大动态分区数 | -| `hive.exec.max.created.files` | 100000 | 作业中所有mapper/reducer创建的HDFS文件的最大数量 | +| `hive.exec.max.created.files` | 100000 | 作业中所有 mapper/reducer 创建的 HDFS 文件的最大数量 | | `hive.error.on.empty.partition` | `false` | 如果动态分区插入生成空结果,是否抛出异常 | ### 2.3 示例 -1. 新建emp表,作为查询对象表 +1. 新建 emp 表,作为查询对象表 ```sql CREATE TABLE emp( @@ -136,26 +136,26 @@ CREATE TABLE emp( deptno INT) ROW FORMAT DELIMITED FIELDS TERMINATED BY "\t"; - -- 加载数据到emp表中 这里直接从本地加载 + -- 加载数据到 emp 表中 这里直接从本地加载 load data local inpath "/usr/file/emp.txt" into table emp; ``` -​ 完成后`emp`表中数据如下: +​ 完成后 `emp` 表中数据如下:
-2. 为清晰演示,先清空`emp_ptn`表中加载的数据: +2. 为清晰演示,先清空 `emp_ptn` 表中加载的数据: ```sql TRUNCATE TABLE emp_ptn; ``` -3. 静态分区演示:从`emp`表中查询部门编号为20的员工数据,并插入`emp_ptn`表中,语句如下: +3. 静态分区演示:从 `emp` 表中查询部门编号为 20 的员工数据,并插入 `emp_ptn` 表中,语句如下: ```sql INSERT OVERWRITE TABLE emp_ptn PARTITION (deptno=20) SELECT empno,ename,job,mgr,hiredate,sal,comm FROM emp WHERE deptno=20; ``` -​ 完成后`emp_ptn`表中数据如下: +​ 完成后 `emp_ptn` 表中数据如下:
@@ -165,12 +165,12 @@ SELECT empno,ename,job,mgr,hiredate,sal,comm FROM emp WHERE deptno=20; -- 由于我们只有一个分区,且还是动态分区,所以需要关闭严格默认。因为在严格模式下,用户必须至少指定一个静态分区 set hive.exec.dynamic.partition.mode=nonstrict; --- 动态分区 此时查询语句的最后一列为动态分区列,即deptno +-- 动态分区 此时查询语句的最后一列为动态分区列,即 deptno INSERT OVERWRITE TABLE emp_ptn PARTITION (deptno) SELECT empno,ename,job,mgr,hiredate,sal,comm,deptno FROM emp WHERE deptno=30; ``` -​ 完成后`emp_ptn`表中数据如下: +​ 完成后 `emp_ptn` 表中数据如下:
@@ -184,8 +184,8 @@ VALUES ( value [, value ...] ) ``` + 使用时必须为表中的每个列都提供值。不支持只向部分列插入值(可以为缺省值的列提供空值来消除这个弊端); -+ 如果目标表表支持ACID及其事务管理器,则插入后自动提交; -+ 不支持支持复杂类型(array, map, struct, union)的插入。 ++ 如果目标表表支持 ACID 及其事务管理器,则插入后自动提交; ++ 不支持支持复杂类型 (array, map, struct, union) 的插入。 @@ -193,7 +193,7 @@ VALUES ( value [, value ...] ) ### 4.1 语法 -更新和删除的语法比较简单,和关系型数据库一致。需要注意的是这两个操作都只能在支持ACID的表,也就是事务表上才能执行。 +更新和删除的语法比较简单,和关系型数据库一致。需要注意的是这两个操作都只能在支持 ACID 的表,也就是事务表上才能执行。 ```sql -- 更新 @@ -207,7 +207,7 @@ DELETE FROM tablename [WHERE expression] **1. 修改配置** -首先需要更改`hive-site.xml`,添加如下配置,开启事务支持,配置完成后需要重启Hive服务。 +首先需要更改 `hive-site.xml`,添加如下配置,开启事务支持,配置完成后需要重启 Hive 服务。 ```xml @@ -238,11 +238,11 @@ DELETE FROM tablename [WHERE expression] **2. 创建测试表** -创建用于测试的事务表,建表时候指定属性`transactional = true`则代表该表是事务表。需要注意的是,按照[官方文档](https://cwiki.apache.org/confluence/display/Hive/Hive+Transactions)的说明,目前Hive中的事务表有以下限制: +创建用于测试的事务表,建表时候指定属性 `transactional = true` 则代表该表是事务表。需要注意的是,按照[官方文档](https://cwiki.apache.org/confluence/display/Hive/Hive+Transactions) 的说明,目前 Hive 中的事务表有以下限制: -+ 必须是buckets Table; -+ 仅支持ORC文件格式; -+ 不支持LOAD DATA ...语句。 ++ 必须是 buckets Table; ++ 仅支持 ORC 文件格式; ++ 不支持 LOAD DATA ...语句。 ```sql CREATE TABLE emp_ts( @@ -259,7 +259,7 @@ TBLPROPERTIES ("transactional"="true"); INSERT INTO TABLE emp_ts VALUES (1,"ming"),(2,"hong"); ``` -插入数据依靠的是MapReduce作业,执行成功后数据如下: +插入数据依靠的是 MapReduce 作业,执行成功后数据如下:
@@ -273,7 +273,7 @@ UPDATE emp_ts SET ename = "lan" WHERE empno=1; DELETE FROM emp_ts WHERE empno=2; ``` -更新和删除数据依靠的也是MapReduce作业,执行成功后数据如下: +更新和删除数据依靠的也是 MapReduce 作业,执行成功后数据如下:
@@ -288,11 +288,11 @@ INSERT OVERWRITE [LOCAL] DIRECTORY directory1 SELECT ... FROM ... ``` -+ OVERWRITE关键字表示输出文件存在时,先删除后再重新写入; ++ OVERWRITE 关键字表示输出文件存在时,先删除后再重新写入; -+ 和Load语句一样,建议无论是本地路径还是URL地址都使用完整的; ++ 和 Load 语句一样,建议无论是本地路径还是 URL 地址都使用完整的; -+ 写入文件系统的数据被序列化为文本,其中列默认由^A分隔,行由换行符分隔。如果列不是基本类型,则将其序列化为JSON格式。其中行分隔符不允许自定义,但列分隔符可以自定义,如下: ++ 写入文件系统的数据被序列化为文本,其中列默认由^A 分隔,行由换行符分隔。如果列不是基本类型,则将其序列化为 JSON 格式。其中行分隔符不允许自定义,但列分隔符可以自定义,如下: ```sql -- 定义列分隔符为'\t' @@ -306,7 +306,7 @@ INSERT OVERWRITE [LOCAL] DIRECTORY directory1 ### 5.2 示例 -这里我们将上面创建的`emp_ptn`表导出到本地文件系统,语句如下: +这里我们将上面创建的 `emp_ptn` 表导出到本地文件系统,语句如下: ```sql INSERT OVERWRITE LOCAL DIRECTORY '/usr/file/ouput' diff --git a/notes/Hive数据查询详解.md b/notes/Hive数据查询详解.md index aba37ce..676702f 100644 --- a/notes/Hive数据查询详解.md +++ b/notes/Hive数据查询详解.md @@ -33,7 +33,7 @@ 为了演示查询操作,这里需要预先创建三张表,并加载测试数据。 -> 数据文件emp.txt和dept.txt可以从本仓库的[resources](https://github.com/heibaiying/BigData-Notes/tree/master/resources)目录下载。 +> 数据文件 emp.txt 和 dept.txt 可以从本仓库的[resources](https://github.com/heibaiying/BigData-Notes/tree/master/resources) 目录下载。 ### 1.1 员工表 @@ -110,7 +110,7 @@ SELECT * FROM emp; ### 2.2 WHERE ```sql --- 查询10号部门中员工编号大于 7782 的员工信息 +-- 查询 10 号部门中员工编号大于 7782 的员工信息 SELECT * FROM emp WHERE empno > 7782 AND deptno = 10; ``` @@ -118,7 +118,7 @@ SELECT * FROM emp WHERE empno > 7782 AND deptno = 10; ### 2.3 DISTINCT -Hive支持使用DISTINCT关键字去重。 +Hive 支持使用 DISTINCT 关键字去重。 ```sql -- 查询所有工作类型 @@ -129,7 +129,7 @@ SELECT DISTINCT job FROM emp; ### 2.4 分区查询 -分区查询(Partition Based Queries),可以指定某个分区或者分区范围。 +分区查询 (Partition Based Queries),可以指定某个分区或者分区范围。 ```sql -- 查询分区表中部门编号在[20,40]之间的员工 @@ -142,7 +142,7 @@ WHERE emp_ptn.deptno >= 20 AND emp_ptn.deptno <= 40; ### 2.5 LIMIT ```sql --- 查询薪资最高的5名员工 +-- 查询薪资最高的 5 名员工 SELECT * FROM emp ORDER BY sal DESC LIMIT 5; ``` @@ -150,7 +150,7 @@ SELECT * FROM emp ORDER BY sal DESC LIMIT 5; ### 2.6 GROUP BY -Hive支持使用GROUP BY进行分组聚合操作。 +Hive 支持使用 GROUP BY 进行分组聚合操作。 ```sql set hive.map.aggr=true; @@ -159,20 +159,20 @@ set hive.map.aggr=true; SELECT deptno,SUM(sal) FROM emp GROUP BY deptno; ``` -`hive.map.aggr`控制程序如何进行聚合。默认值为false。如果设置为true,Hive会在map阶段就执行一次聚合。这可以提高聚合效率,但需要消耗更多内存。 +`hive.map.aggr` 控制程序如何进行聚合。默认值为 false。如果设置为 true,Hive 会在 map 阶段就执行一次聚合。这可以提高聚合效率,但需要消耗更多内存。 ### 2.7 ORDER AND SORT -可以使用ORDER BY或者Sort BY对查询结果进行排序,排序字段可以是整型也可以是字符串:如果是整型,则按照大小排序;如果是字符串,则按照字典序排序。ORDER BY 和 SORT BY 的区别如下: +可以使用 ORDER BY 或者 Sort BY 对查询结果进行排序,排序字段可以是整型也可以是字符串:如果是整型,则按照大小排序;如果是字符串,则按照字典序排序。ORDER BY 和 SORT BY 的区别如下: -+ 使用ORDER BY时会有一个Reducer对全部查询结果进行排序,可以保证数据的全局有序性; -+ 使用SORT BY时只会在每个Reducer中进行排序,这可以保证每个Reducer的输出数据是有序的,但不能保证全局有序。 ++ 使用 ORDER BY 时会有一个 Reducer 对全部查询结果进行排序,可以保证数据的全局有序性; ++ 使用 SORT BY 时只会在每个 Reducer 中进行排序,这可以保证每个 Reducer 的输出数据是有序的,但不能保证全局有序。 -由于ORDER BY的时间可能很长,如果你设置了严格模式(hive.mapred.mode = strict),则其后面必须再跟一个`limit`子句。 +由于 ORDER BY 的时间可能很长,如果你设置了严格模式 (hive.mapred.mode = strict),则其后面必须再跟一个 `limit` 子句。 -> 注 :hive.mapred.mode默认值是nonstrict ,也就是非严格模式。 +> 注 :hive.mapred.mode 默认值是 nonstrict ,也就是非严格模式。 ```sql -- 查询员工工资,结果按照部门升序,按照工资降序排列 @@ -183,10 +183,10 @@ SELECT empno, deptno, sal FROM emp ORDER BY deptno ASC, sal DESC; ### 2.8 HAVING -可以使用HAVING对分组数据进行过滤。 +可以使用 HAVING 对分组数据进行过滤。 ```sql --- 查询工资总和大于9000的所有部门 +-- 查询工资总和大于 9000 的所有部门 SELECT deptno,SUM(sal) FROM emp GROUP BY deptno HAVING SUM(sal)>9000; ``` @@ -194,11 +194,11 @@ SELECT deptno,SUM(sal) FROM emp GROUP BY deptno HAVING SUM(sal)>9000; ### 2.9 DISTRIBUTE BY -默认情况下,MapReduce程序会对Map输出结果的Key值进行散列,并均匀分发到所有Reducer上。如果想要把具有相同Key值的数据分发到同一个Reducer进行处理,这就需要使用DISTRIBUTE BY字句。 +默认情况下,MapReduce 程序会对 Map 输出结果的 Key 值进行散列,并均匀分发到所有 Reducer 上。如果想要把具有相同 Key 值的数据分发到同一个 Reducer 进行处理,这就需要使用 DISTRIBUTE BY 字句。 -需要注意的是,DISTRIBUTE BY虽然能保证具有相同Key值的数据分发到同一个Reducer,但是不能保证数据在Reducer上是有序的。情况如下: +需要注意的是,DISTRIBUTE BY 虽然能保证具有相同 Key 值的数据分发到同一个 Reducer,但是不能保证数据在 Reducer 上是有序的。情况如下: -把以下5个数据发送到两个Reducer上进行处理: +把以下 5 个数据发送到两个 Reducer 上进行处理: ```properties k1 @@ -208,7 +208,7 @@ k3 k1 ``` -Reducer1得到如下乱序数据: +Reducer1 得到如下乱序数据: ```properties k1 @@ -217,17 +217,17 @@ k1 ``` -Reducer2得到数据如下: +Reducer2 得到数据如下: ```properties k4 k3 ``` -如果想让Reducer上的数据时有序的,可以结合`SORT BY`使用(示例如下),或者使用下面我们将要介绍的CLUSTER BY。 +如果想让 Reducer 上的数据时有序的,可以结合 `SORT BY` 使用 (示例如下),或者使用下面我们将要介绍的 CLUSTER BY。 ```sql --- 将数据按照部门分发到对应的Reducer上处理 +-- 将数据按照部门分发到对应的 Reducer 上处理 SELECT empno, deptno, sal FROM emp DISTRIBUTE BY deptno SORT BY deptno ASC; ``` @@ -235,7 +235,7 @@ SELECT empno, deptno, sal FROM emp DISTRIBUTE BY deptno SORT BY deptno ASC; ### 2.10 CLUSTER BY -如果`SORT BY`和`DISTRIBUTE BY`指定的是相同字段,且SORT BY排序规则是ASC,此时可以使用`CLUSTER BY`进行替换,同时`CLUSTER BY`可以保证数据在全局是有序的。 +如果 `SORT BY` 和 `DISTRIBUTE BY` 指定的是相同字段,且 SORT BY 排序规则是 ASC,此时可以使用 `CLUSTER BY` 进行替换,同时 `CLUSTER BY` 可以保证数据在全局是有序的。 ```sql SELECT empno, deptno, sal FROM emp CLUSTER BY deptno ; @@ -245,9 +245,9 @@ SELECT empno, deptno, sal FROM emp CLUSTER BY deptno ; ## 三、多表联结查询 -Hive支持内连接,外连接,左外连接,右外连接,笛卡尔连接,这和传统数据库中的概念是一致的,可以参见下图。 +Hive 支持内连接,外连接,左外连接,右外连接,笛卡尔连接,这和传统数据库中的概念是一致的,可以参见下图。 -需要特别强调:JOIN语句的关联条件必须用ON指定,不能用WHERE指定,否则就会先做笛卡尔积,再过滤,这会导致你得不到预期的结果(下面的演示会有说明)。 +需要特别强调:JOIN 语句的关联条件必须用 ON 指定,不能用 WHERE 指定,否则就会先做笛卡尔积,再过滤,这会导致你得不到预期的结果 (下面的演示会有说明)。
@@ -256,7 +256,7 @@ Hive支持内连接,外连接,左外连接,右外连接,笛卡尔连接 ### 3.1 INNER JOIN ```sql --- 查询员工编号为7369的员工的详细信息 +-- 查询员工编号为 7369 的员工的详细信息 SELECT e.*,d.* FROM emp e JOIN dept d ON e.deptno = d.deptno @@ -268,7 +268,7 @@ SELECT a.val, b.val, c.val FROM a JOIN b ON (a.key = b.key1) JOIN c ON (c.key = ### 3.2 LEFT OUTER JOIN -LEFT OUTER JOIN 和 LEFT JOIN是等价的。 +LEFT OUTER JOIN 和 LEFT JOIN 是等价的。 ```sql -- 左连接 @@ -286,7 +286,7 @@ FROM emp e RIGHT OUTER JOIN dept d ON e.deptno = d.deptno; ``` -执行右连接后,由于40号部门下没有任何员工,所以此时员工信息为NULL。这个查询可以很好的复述上面提到的——JOIN语句的关联条件必须用ON指定,不能用WHERE指定。你可以把ON改成WHERE,你会发现无论如何都查不出40号部门这条数据,因为笛卡尔运算不会有(NULL, 40)这种情况。 +执行右连接后,由于 40 号部门下没有任何员工,所以此时员工信息为 NULL。这个查询可以很好的复述上面提到的——JOIN 语句的关联条件必须用 ON 指定,不能用 WHERE 指定。你可以把 ON 改成 WHERE,你会发现无论如何都查不出 40 号部门这条数据,因为笛卡尔运算不会有 (NULL, 40) 这种情况。
@@ -303,7 +303,7 @@ ON e.deptno = d.deptno; LEFT SEMI JOIN (左半连接)是 IN/EXISTS 子查询的一种更高效的实现。 + JOIN 子句中右边的表只能在 ON 子句中设置过滤条件; -+ 查询结果只包含左边表的数据,所以只能SELECT左表中的列。 ++ 查询结果只包含左边表的数据,所以只能 SELECT 左表中的列。 ```sql -- 查询在纽约办公的所有员工信息 @@ -318,7 +318,7 @@ WHERE emp.deptno IN (SELECT deptno FROM dept WHERE loc="NEW YORK"); ### 3.6 JOIN -笛卡尔积连接,这个连接日常的开发中可能很少遇到,且性能消耗比较大,基于这个原因,如果在严格模式下(hive.mapred.mode = strict),Hive会阻止用户执行此操作。 +笛卡尔积连接,这个连接日常的开发中可能很少遇到,且性能消耗比较大,基于这个原因,如果在严格模式下 (hive.mapred.mode = strict),Hive 会阻止用户执行此操作。 ```sql SELECT * FROM emp JOIN dept; @@ -330,13 +330,13 @@ SELECT * FROM emp JOIN dept; ### 4.1 STREAMTABLE -在多表进行联结的时候,如果每个ON字句都使用到共同的列(如下面的`b.key`),此时Hive会进行优化,将多表JOIN在同一个map / reduce作业上进行。同时假定查询的最后一个表(如下面的 c 表)是最大的一个表,在对每行记录进行JOIN操作时,它将尝试将其他的表缓存起来,然后扫描最后那个表进行计算。因此用户需要保证查询的表的大小从左到右是依次增加的。 +在多表进行联结的时候,如果每个 ON 字句都使用到共同的列(如下面的 `b.key`),此时 Hive 会进行优化,将多表 JOIN 在同一个 map / reduce 作业上进行。同时假定查询的最后一个表(如下面的 c 表)是最大的一个表,在对每行记录进行 JOIN 操作时,它将尝试将其他的表缓存起来,然后扫描最后那个表进行计算。因此用户需要保证查询的表的大小从左到右是依次增加的。 ```sql `SELECT a.val, b.val, c.val FROM a JOIN b ON (a.key = b.key) JOIN c ON (c.key = b.key)` ``` -然后,用户并非需要总是把最大的表放在查询语句的最后面,Hive提供了`/*+ STREAMTABLE() */`标志,用于标识最大的表,示例如下: +然后,用户并非需要总是把最大的表放在查询语句的最后面,Hive 提供了 `/*+ STREAMTABLE() */` 标志,用于标识最大的表,示例如下: ```sql SELECT /*+ STREAMTABLE(d) */ e.*,d.* @@ -349,7 +349,7 @@ WHERE job='CLERK'; ### 4.2 MAPJOIN -如果所有表中只有一张表是小表,那么Hive把这张小表加载到内存中。这时候程序会在map阶段直接拿另外一个表的数据和内存中表数据做匹配,由于在map就进行了JOIN操作,从而可以省略reduce过程,这样效率可以提升很多。Hive中提供了`/*+ MAPJOIN() */`来标记小表,示例如下: +如果所有表中只有一张表是小表,那么 Hive 把这张小表加载到内存中。这时候程序会在 map 阶段直接拿另外一个表的数据和内存中表数据做匹配,由于在 map 就进行了 JOIN 操作,从而可以省略 reduce 过程,这样效率可以提升很多。Hive 中提供了 `/*+ MAPJOIN() */` 来标记小表,示例如下: ```sql SELECT /*+ MAPJOIN(d) */ e.*,d.* @@ -372,20 +372,20 @@ SELECT current_database() ## 六、本地模式 -在上面演示的语句中,大多数都会触发MapReduce, 少部分不会触发,比如`select * from emp limit 5`就不会触发MR,此时Hive只是简单的读取数据文件中的内容,然后格式化后进行输出。在需要执行MapReduce的查询中,你会发现执行时间可能会很长,这时候你可以选择开启本地模式。 +在上面演示的语句中,大多数都会触发 MapReduce, 少部分不会触发,比如 `select * from emp limit 5` 就不会触发 MR,此时 Hive 只是简单的读取数据文件中的内容,然后格式化后进行输出。在需要执行 MapReduce 的查询中,你会发现执行时间可能会很长,这时候你可以选择开启本地模式。 ```sql --本地模式默认关闭,需要手动开启此功能 SET hive.exec.mode.local.auto=true; ``` -启用后,Hive将分析查询中每个map-reduce作业的大小,如果满足以下条件,则可以在本地运行它: +启用后,Hive 将分析查询中每个 map-reduce 作业的大小,如果满足以下条件,则可以在本地运行它: -- 作业的总输入大小低于:hive.exec.mode.local.auto.inputbytes.max(默认为128MB); -- map-tasks的总数小于:hive.exec.mode.local.auto.tasks.max(默认为4); -- 所需的reduce任务总数为1或0。 +- 作业的总输入大小低于:hive.exec.mode.local.auto.inputbytes.max(默认为 128MB); +- map-tasks 的总数小于:hive.exec.mode.local.auto.tasks.max(默认为 4); +- 所需的 reduce 任务总数为 1 或 0。 -因为我们测试的数据集很小,所以你再次去执行上面涉及MR操作的查询,你会发现速度会有显著的提升。 +因为我们测试的数据集很小,所以你再次去执行上面涉及 MR 操作的查询,你会发现速度会有显著的提升。 diff --git a/notes/Hive简介及核心概念.md b/notes/Hive简介及核心概念.md index 27a2ce0..33ed759 100644 --- a/notes/Hive简介及核心概念.md +++ b/notes/Hive简介及核心概念.md @@ -15,14 +15,14 @@ ## 一、简介 -Hive是一个构建在Hadoop之上的数据仓库,它可以将结构化的数据文件映射成表,并提供类SQL查询功能,用于查询的SQL语句会被转化为MapReduce作业,然后提交到Hadoop上运行。 +Hive 是一个构建在 Hadoop 之上的数据仓库,它可以将结构化的数据文件映射成表,并提供类 SQL 查询功能,用于查询的 SQL 语句会被转化为 MapReduce 作业,然后提交到 Hadoop 上运行。 **特点**: -1. 简单、容易上手(提供了类似sql的查询语言hql),使得精通sql但是不了解Java编程的人也能很好地进行大数据分析; -3. 灵活性高,可以自定义用户函数(UDF)和存储格式; +1. 简单、容易上手 (提供了类似 sql 的查询语言 hql),使得精通 sql 但是不了解 Java 编程的人也能很好地进行大数据分析; +3. 灵活性高,可以自定义用户函数 (UDF) 和存储格式; 4. 为超大的数据集设计的计算和存储能力,集群扩展容易; -5. 统一的元数据管理,可与presto/impala/sparksql等共享数据; +5. 统一的元数据管理,可与 presto/impala/sparksql 等共享数据; 5. 执行延迟高,不适合做数据的实时处理,但适合做海量数据的离线处理。 @@ -33,29 +33,29 @@ Hive是一个构建在Hadoop之上的数据仓库,它可以将结构化的数 ### 2.1 command-line shell & thrift/jdbc -可以用command-line shell和thrift/jdbc两种方式来操作数据: +可以用 command-line shell 和 thrift/jdbc 两种方式来操作数据: -+ **command-line shell**:通过hive命令行的的方式来操作数据; -+ **thrift/jdbc**:通过thrift协议按照标准的JDBC的方式操作数据。 ++ **command-line shell**:通过 hive 命令行的的方式来操作数据; ++ **thrift/jdbc**:通过 thrift 协议按照标准的 JDBC 的方式操作数据。 ### 2.2 Metastore -在Hive中,表名、表结构、字段名、字段类型、表的分隔符等统一被称为元数据。所有的元数据默认存储在Hive内置的derby数据库中,但由于derby只能有一个实例,也就是说不能有多个命令行客户端同时访问,所以在实际生产环境中,通常使用MySQL代替derby。 +在 Hive 中,表名、表结构、字段名、字段类型、表的分隔符等统一被称为元数据。所有的元数据默认存储在 Hive 内置的 derby 数据库中,但由于 derby 只能有一个实例,也就是说不能有多个命令行客户端同时访问,所以在实际生产环境中,通常使用 MySQL 代替 derby。 -Hive进行的是统一的元数据管理,就是说你在Hive上创建了一张表,然后在presto/impala/sparksql 中都是可以直接使用的,它们会从Metastore中获取统一的元数据信息,同样的你在presto/impala/sparksql中创建一张表,在Hive中也可以直接使用。 +Hive 进行的是统一的元数据管理,就是说你在 Hive 上创建了一张表,然后在 presto/impala/sparksql 中都是可以直接使用的,它们会从 Metastore 中获取统一的元数据信息,同样的你在 presto/impala/sparksql 中创建一张表,在 Hive 中也可以直接使用。 ### 2.3 HQL的执行流程 -Hive在执行一条HQL的时候,会经过以下步骤: +Hive 在执行一条 HQL 的时候,会经过以下步骤: -1. 语法解析:Antlr定义SQL的语法规则,完成SQL词法,语法解析,将SQL转化为抽象 语法树AST Tree; -2. 语义解析:遍历AST Tree,抽象出查询的基本组成单元QueryBlock; -3. 生成逻辑执行计划:遍历QueryBlock,翻译为执行操作树OperatorTree; -4. 优化逻辑执行计划:逻辑层优化器进行OperatorTree变换,合并不必要的ReduceSinkOperator,减少shuffle数据量; -5. 生成物理执行计划:遍历OperatorTree,翻译为MapReduce任务; -6. 优化物理执行计划:物理层优化器进行MapReduce任务的变换,生成最终的执行计划。 +1. 语法解析:Antlr 定义 SQL 的语法规则,完成 SQL 词法,语法解析,将 SQL 转化为抽象 语法树 AST Tree; +2. 语义解析:遍历 AST Tree,抽象出查询的基本组成单元 QueryBlock; +3. 生成逻辑执行计划:遍历 QueryBlock,翻译为执行操作树 OperatorTree; +4. 优化逻辑执行计划:逻辑层优化器进行 OperatorTree 变换,合并不必要的 ReduceSinkOperator,减少 shuffle 数据量; +5. 生成物理执行计划:遍历 OperatorTree,翻译为 MapReduce 任务; +6. 优化物理执行计划:物理层优化器进行 MapReduce 任务的变换,生成最终的执行计划。 -> 关于Hive SQL的详细执行流程可以参考美团技术团队的文章:[Hive SQL的编译过程](https://tech.meituan.com/2014/02/12/hive-sql-to-mapreduce.html) +> 关于 Hive SQL 的详细执行流程可以参考美团技术团队的文章:[Hive SQL 的编译过程](https://tech.meituan.com/2014/02/12/hive-sql-to-mapreduce.html) @@ -63,14 +63,14 @@ Hive在执行一条HQL的时候,会经过以下步骤: ### 3.1 基本数据类型 -Hive表中的列支持以下基本数据类型: +Hive 表中的列支持以下基本数据类型: | 大类 | 类型 | | --------------------------------------- | ------------------------------------------------------------ | -| **Integers(整型)** | TINYINT—1字节的有符号整数
SMALLINT—2字节的有符号整数
INT—4字节的有符号整数
BIGINT—8字节的有符号整数 | +| **Integers(整型)** | TINYINT—1 字节的有符号整数
SMALLINT—2 字节的有符号整数
INT—4 字节的有符号整数
BIGINT—8 字节的有符号整数 | | **Boolean(布尔型)** | BOOLEAN—TRUE/FALSE | | **Floating point numbers(浮点型)** | FLOAT— 单精度浮点型
DOUBLE—双精度浮点型 | -| **Fixed point numbers(定点数)** | DECIMAL—用户自定义精度定点数,比如DECIMAL(7,2) | +| **Fixed point numbers(定点数)** | DECIMAL—用户自定义精度定点数,比如 DECIMAL(7,2) | | **String types(字符串)** | STRING—指定字符集的字符序列
VARCHAR—具有最大长度限制的字符序列
CHAR—固定长度的字符序列 | | **Date and time types(日期时间类型)** | TIMESTAMP — 时间戳
TIMESTAMP WITH LOCAL TIME ZONE — 时间戳,纳秒精度
DATE—日期类型 | | **Binary types(二进制类型)** | BINARY—字节序列 | @@ -82,7 +82,7 @@ Hive表中的列支持以下基本数据类型: ### 3.2 隐式转换 -Hive中基本数据类型遵循以下的层次结构,按照这个层次结构,子类型到祖先类型允许隐式转换。例如INT类型的数据允许隐式转换为BIGINT类型。额外注意的是:按照类型层次结构允许将STRING类型隐式转换为DOUBLE类型。 +Hive 中基本数据类型遵循以下的层次结构,按照这个层次结构,子类型到祖先类型允许隐式转换。例如 INT 类型的数据允许隐式转换为 BIGINT 类型。额外注意的是:按照类型层次结构允许将 STRING 类型隐式转换为 DOUBLE 类型。
@@ -92,9 +92,9 @@ Hive中基本数据类型遵循以下的层次结构,按照这个层次结构 | 类型 | 描述 | 示例 | | ---------- | ------------------------------------------------------------ | -------------------------------------- | -| **STRUCT** | 类似于对象,是字段的集合,字段的类型可以不同,可以使用 `名称.字段名`方式进行访问 | STRUCT ('xiaoming', 12 , '2018-12-12') | -| **MAP** | 键值对的集合,可以使用`名称[key]`的方式访问对应的值 | map('a', 1, 'b', 2) | -| **ARRAY** | 数组是一组具有相同类型和名称的变量的集合,可以使用`名称[index]`访问对应的值 | ARRAY('a', 'b', 'c', 'd') | +| **STRUCT** | 类似于对象,是字段的集合,字段的类型可以不同,可以使用 ` 名称.字段名 ` 方式进行访问 | STRUCT ('xiaoming', 12 , '2018-12-12') | +| **MAP** | 键值对的集合,可以使用 ` 名称[key]` 的方式访问对应的值 | map('a', 1, 'b', 2) | +| **ARRAY** | 数组是一组具有相同类型和名称的变量的集合,可以使用 ` 名称[index]` 访问对应的值 | ARRAY('a', 'b', 'c', 'd') | @@ -116,16 +116,16 @@ CREATE TABLE students( ## 四、内容格式 -当数据存储在文本文件中,必须按照一定格式区别行和列,如使用逗号作为分隔符的CSV文件(Comma-Separated Values)或者使用制表符作为分隔值的TSV文件(Tab-Separated Values)。但此时也存在一个缺点,就是正常的文件内容中也可能出现逗号或者制表符。 +当数据存储在文本文件中,必须按照一定格式区别行和列,如使用逗号作为分隔符的 CSV 文件 (Comma-Separated Values) 或者使用制表符作为分隔值的 TSV 文件 (Tab-Separated Values)。但此时也存在一个缺点,就是正常的文件内容中也可能出现逗号或者制表符。 -所以Hive默认使用了几个平时很少出现的字符,这些字符一般不会作为内容出现在文件中。Hive默认的行和列分隔符如下表所示。 +所以 Hive 默认使用了几个平时很少出现的字符,这些字符一般不会作为内容出现在文件中。Hive 默认的行和列分隔符如下表所示。 | 分隔符 | 描述 | | --------------- | ------------------------------------------------------------ | | **\n** | 对于文本文件来说,每行是一条记录,所以可以使用换行符来分割记录 | -| **^A (Ctrl+A)** | 分割字段(列),在CREATE TABLE语句中也可以使用八进制编码 `\001` 来表示 | -| **^B** | 用于分割 ARRAY 或者 STRUCT 中的元素,或者用于 MAP 中键值对之间的分割,
在CREATE TABLE语句中也可以使用八进制编码`\002` 表示 | -| **^C** | 用于 MAP 中键和值之间的分割,在CREATE TABLE语句中也可以使用八进制编码`\003` 表示 | +| **^A (Ctrl+A)** | 分割字段 (列),在 CREATE TABLE 语句中也可以使用八进制编码 `\001` 来表示 | +| **^B** | 用于分割 ARRAY 或者 STRUCT 中的元素,或者用于 MAP 中键值对之间的分割,
在 CREATE TABLE 语句中也可以使用八进制编码 `\002` 表示 | +| **^C** | 用于 MAP 中键和值之间的分割,在 CREATE TABLE 语句中也可以使用八进制编码 `\003` 表示 | 使用示例如下: @@ -144,22 +144,22 @@ CREATE TABLE page_view(viewTime INT, userid BIGINT) ### 5.1 支持的存储格式 -Hive会在HDFS为每个数据库上创建一个目录,数据库中的表是该目录的子目录,表中的数据会以文件的形式存储在对应的表目录下。Hive支持以下几种文件存储格式: +Hive 会在 HDFS 为每个数据库上创建一个目录,数据库中的表是该目录的子目录,表中的数据会以文件的形式存储在对应的表目录下。Hive 支持以下几种文件存储格式: | 格式 | 说明 | | ---------------- | ------------------------------------------------------------ | -| **TextFile** | 存储为纯文本文件。 这是Hive默认的文件存储格式。这种存储方式数据不做压缩,磁盘开销大,数据解析开销大。 | -| **SequenceFile** | SequenceFile是Hadoop API提供的一种二进制文件,它将数据以的形式序列化到文件中。这种二进制文件内部使用Hadoop的标准的Writable 接口实现序列化和反序列化。它与Hadoop API中的MapFile 是互相兼容的。Hive中的SequenceFile 继承自Hadoop API 的SequenceFile,不过它的key为空,使用value存放实际的值,这样是为了避免MR在运行map阶段进行额外的排序操作。 | -| **RCFile** | RCFile文件格式是FaceBook开源的一种Hive的文件存储格式,首先将表分为几个行组,对每个行组内的数据按列存储,每一列的数据都是分开存储。 | -| **ORC Files** | ORC是在一定程度上扩展了RCFile,是对RCFile的优化。 | -| **Avro Files** | Avro是一个数据序列化系统,设计用于支持大批量数据交换的应用。它的主要特点有:支持二进制序列化方式,可以便捷,快速地处理大量数据;动态语言友好,Avro提供的机制使动态语言可以方便地处理Avro数据。 | -| **Parquet** | Parquet是基于Dremel的数据模型和算法实现的,面向分析型业务的列式存储格式。它通过按列进行高效压缩和特殊的编码技术,从而在降低存储空间的同时提高了IO效率。 | +| **TextFile** | 存储为纯文本文件。 这是 Hive 默认的文件存储格式。这种存储方式数据不做压缩,磁盘开销大,数据解析开销大。 | +| **SequenceFile** | SequenceFile 是 Hadoop API 提供的一种二进制文件,它将数据以的形式序列化到文件中。这种二进制文件内部使用 Hadoop 的标准的 Writable 接口实现序列化和反序列化。它与 Hadoop API 中的 MapFile 是互相兼容的。Hive 中的 SequenceFile 继承自 Hadoop API 的 SequenceFile,不过它的 key 为空,使用 value 存放实际的值,这样是为了避免 MR 在运行 map 阶段进行额外的排序操作。 | +| **RCFile** | RCFile 文件格式是 FaceBook 开源的一种 Hive 的文件存储格式,首先将表分为几个行组,对每个行组内的数据按列存储,每一列的数据都是分开存储。 | +| **ORC Files** | ORC 是在一定程度上扩展了 RCFile,是对 RCFile 的优化。 | +| **Avro Files** | Avro 是一个数据序列化系统,设计用于支持大批量数据交换的应用。它的主要特点有:支持二进制序列化方式,可以便捷,快速地处理大量数据;动态语言友好,Avro 提供的机制使动态语言可以方便地处理 Avro 数据。 | +| **Parquet** | Parquet 是基于 Dremel 的数据模型和算法实现的,面向分析型业务的列式存储格式。它通过按列进行高效压缩和特殊的编码技术,从而在降低存储空间的同时提高了 IO 效率。 | -> 以上压缩格式中ORC和Parquet的综合性能突出,使用较为广泛,推荐使用这两种格式。 +> 以上压缩格式中 ORC 和 Parquet 的综合性能突出,使用较为广泛,推荐使用这两种格式。 ### 5.2 指定存储格式 -通常在创建表的时候使用`STORED AS`参数指定: +通常在创建表的时候使用 `STORED AS` 参数指定: ```sql CREATE TABLE page_view(viewTime INT, userid BIGINT) @@ -183,12 +183,12 @@ CREATE TABLE page_view(viewTime INT, userid BIGINT) ## 六、内部表和外部表 -内部表又叫做管理表(Managed/Internal Table),创建表时不做任何指定,默认创建的就是内部表。想要创建外部表(External Table),则需要使用External进行修饰。 内部表和外部表主要区别如下: +内部表又叫做管理表 (Managed/Internal Table),创建表时不做任何指定,默认创建的就是内部表。想要创建外部表 (External Table),则需要使用 External 进行修饰。 内部表和外部表主要区别如下: | | 内部表 | 外部表 | | ------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | -| 数据存储位置 | 内部表数据存储的位置由hive.metastore.warehouse.dir参数指定,默认情况下表的数据存储在HDFS的`/user/hive/warehouse/数据库名.db/表名/` 目录下 | 外部表数据的存储位置创建表时由`Location`参数指定; | -| 导入数据 | 在导入数据到内部表,内部表将数据移动到自己的数据仓库目录下,数据的生命周期由Hive来进行管理 | 外部表不会将数据移动到自己的数据仓库目录下,只是在元数据中存储了数据的位置 | +| 数据存储位置 | 内部表数据存储的位置由 hive.metastore.warehouse.dir 参数指定,默认情况下表的数据存储在 HDFS 的 `/user/hive/warehouse/数据库名.db/表名/` 目录下 | 外部表数据的存储位置创建表时由 `Location` 参数指定; | +| 导入数据 | 在导入数据到内部表,内部表将数据移动到自己的数据仓库目录下,数据的生命周期由 Hive 来进行管理 | 外部表不会将数据移动到自己的数据仓库目录下,只是在元数据中存储了数据的位置 | | 删除表 | 删除元数据(metadata)和文件 | 只删除元数据(metadata) | @@ -196,7 +196,7 @@ CREATE TABLE page_view(viewTime INT, userid BIGINT) ## 参考资料 1. [Hive Getting Started](https://cwiki.apache.org/confluence/display/Hive/GettingStarted) -2. [Hive SQL的编译过程](https://tech.meituan.com/2014/02/12/hive-sql-to-mapreduce.html) +2. [Hive SQL 的编译过程](https://tech.meituan.com/2014/02/12/hive-sql-to-mapreduce.html) 3. [LanguageManual DDL](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL) 4. [LanguageManual Types](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+Types) 5. [Managed vs. External Tables](https://cwiki.apache.org/confluence/display/Hive/Managed+vs.+External+Tables) diff --git a/notes/Hive视图和索引.md b/notes/Hive视图和索引.md index 56350eb..666fb18 100644 --- a/notes/Hive视图和索引.md +++ b/notes/Hive视图和索引.md @@ -11,7 +11,7 @@ ### 1.1 简介 -Hive 中的视图和RDBMS中视图的概念一致,都是一组数据的逻辑表示,本质上就是一条SELECT语句的结果集。视图是纯粹的逻辑对象,没有关联的存储(Hive 3.0.0引入的物化视图除外),当查询引用视图时,Hive可以将视图的定义与查询结合起来,例如将查询中的过滤器推送到视图中。 +Hive 中的视图和 RDBMS 中视图的概念一致,都是一组数据的逻辑表示,本质上就是一条 SELECT 语句的结果集。视图是纯粹的逻辑对象,没有关联的存储 (Hive 3.0.0 引入的物化视图除外),当查询引用视图时,Hive 可以将视图的定义与查询结合起来,例如将查询中的过滤器推送到视图中。 ### 1.2 创建视图 @@ -23,19 +23,19 @@ CREATE VIEW [IF NOT EXISTS] [db_name.]view_name -- 视图名称 AS SELECT ...; ``` -在Hive中可以使用`CREATE VIEW`创建视图,如果已存在具有相同名称的表或视图,则会抛出异常,建议使用`IF NOT EXISTS`预做判断。在使用视图时候需要注意以下事项: +在 Hive 中可以使用 `CREATE VIEW` 创建视图,如果已存在具有相同名称的表或视图,则会抛出异常,建议使用 `IF NOT EXISTS` 预做判断。在使用视图时候需要注意以下事项: -- 视图是只读的,不能用作LOAD / INSERT / ALTER的目标; +- 视图是只读的,不能用作 LOAD / INSERT / ALTER 的目标; - 在创建视图时候视图就已经固定,对基表的后续更改(如添加列)将不会反映在视图; - 删除基表并不会删除视图,需要手动删除视图; -- 视图可能包含ORDER BY和LIMIT子句。如果引用视图的查询语句也包含这类子句,其执行优先级低于视图对应字句。例如,视图`custom_view`指定LIMIT 5,查询语句为`select * from custom_view LIMIT 10`,此时结果最多返回5行。 +- 视图可能包含 ORDER BY 和 LIMIT 子句。如果引用视图的查询语句也包含这类子句,其执行优先级低于视图对应字句。例如,视图 `custom_view` 指定 LIMIT 5,查询语句为 `select * from custom_view LIMIT 10`,此时结果最多返回 5 行。 -- 创建视图时,如果未提供列名,则将从SELECT语句中自动派生列名; +- 创建视图时,如果未提供列名,则将从 SELECT 语句中自动派生列名; -- 创建视图时,如果SELECT语句中包含其他表达式,例如x + y,则列名称将以\_C0,\_C1等形式生成; +- 创建视图时,如果 SELECT 语句中包含其他表达式,例如 x + y,则列名称将以\_C0,\_C1 等形式生成; ```sql CREATE VIEW IF NOT EXISTS custom_view AS SELECT empno, empno+deptno , 1+2 FROM emp; @@ -105,18 +105,18 @@ ALTER VIEW custom_view SET TBLPROPERTIES ('create'='heibaiying','date'='2019-05- ### 2.1 简介 -Hive在0.7.0引入了索引的功能,索引的设计目标是提高表某些列的查询速度。如果没有索引,带有谓词的查询(如'WHERE table1.column = 10')会加载整个表或分区并处理所有行。但是如果column存在索引,则只需要加载和处理文件的一部分。 +Hive 在 0.7.0 引入了索引的功能,索引的设计目标是提高表某些列的查询速度。如果没有索引,带有谓词的查询(如'WHERE table1.column = 10')会加载整个表或分区并处理所有行。但是如果 column 存在索引,则只需要加载和处理文件的一部分。 ### 2.2 索引原理 -在指定列上建立索引,会产生一张索引表(表结构如下),里面的字段包括:索引列的值、该值对应的HDFS文件路径、该值在文件中的偏移量。在查询涉及到索引字段时,首先到索引表查找索引列值对应的HDFS文件路径及偏移量,这样就避免了全表扫描。 +在指定列上建立索引,会产生一张索引表(表结构如下),里面的字段包括:索引列的值、该值对应的 HDFS 文件路径、该值在文件中的偏移量。在查询涉及到索引字段时,首先到索引表查找索引列值对应的 HDFS 文件路径及偏移量,这样就避免了全表扫描。 ```properties +--------------+----------------+----------+--+ | col_name | data_type | comment | +--------------+----------------+----------+--+ | empno | int | 建立索引的列 | -| _bucketname | string | HDFS文件路径 | +| _bucketname | string | HDFS 文件路径 | | _offsets | array | 偏移量 | +--------------+----------------+----------+--+ ``` @@ -162,7 +162,7 @@ DROP INDEX [IF EXISTS] index_name ON table_name; ALTER INDEX index_name ON table_name [PARTITION partition_spec] REBUILD; ``` -重建索引。如果指定了PARTITION,则仅重建该分区的索引。 +重建索引。如果指定了 PARTITION,则仅重建该分区的索引。 @@ -170,7 +170,7 @@ ALTER INDEX index_name ON table_name [PARTITION partition_spec] REBUILD; ### 3.1 创建索引 -在emp表上针对`empno`字段创建名为`emp_index`,索引数据存储在`emp_index_table`索引表中 +在 emp 表上针对 `empno` 字段创建名为 `emp_index`,索引数据存储在 `emp_index_table` 索引表中 ```sql create index emp_index on table emp(empno) as @@ -187,13 +187,13 @@ in table emp_index_table ; alter index emp_index on emp rebuild; ``` -Hive会启动MapReduce作业去建立索引,建立好后查看索引表数据如下。三个表字段分别代表:索引列的值、该值对应的HDFS文件路径、该值在文件中的偏移量。 +Hive 会启动 MapReduce 作业去建立索引,建立好后查看索引表数据如下。三个表字段分别代表:索引列的值、该值对应的 HDFS 文件路径、该值在文件中的偏移量。
### 3.3 自动使用索引 -默认情况下,虽然建立了索引,但是Hive在查询时候是不会自动去使用索引的,需要开启相关配置。开启配置后,涉及到索引列的查询就会使用索引功能去优化查询。 +默认情况下,虽然建立了索引,但是 Hive 在查询时候是不会自动去使用索引的,需要开启相关配置。开启配置后,涉及到索引列的查询就会使用索引功能去优化查询。 ```sql SET hive.input.format=org.apache.hadoop.hive.ql.io.HiveInputFormat; @@ -215,14 +215,14 @@ SHOW INDEX ON emp; ## 四、索引的缺陷 -索引表最主要的一个缺陷在于:索引表无法自动rebuild,这也就意味着如果表中有数据新增或删除,则必须手动rebuild,重新执行MapReduce作业,生成索引表数据。 +索引表最主要的一个缺陷在于:索引表无法自动 rebuild,这也就意味着如果表中有数据新增或删除,则必须手动 rebuild,重新执行 MapReduce 作业,生成索引表数据。 -同时按照[官方文档](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+Indexing)的说明,Hive会从3.0开始移除索引功能,主要基于以下两个原因: +同时按照[官方文档](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+Indexing) 的说明,Hive 会从 3.0 开始移除索引功能,主要基于以下两个原因: -- 具有自动重写的物化视图(Materialized View)可以产生与索引相似的效果(Hive 2.3.0增加了对物化视图的支持,在3.0之后正式引入)。 +- 具有自动重写的物化视图 (Materialized View) 可以产生与索引相似的效果(Hive 2.3.0 增加了对物化视图的支持,在 3.0 之后正式引入)。 - 使用列式存储文件格式(Parquet,ORC)进行存储时,这些格式支持选择性扫描,可以跳过不需要的文件或块。 -> ORC内置的索引功能可以参阅这篇文章:[Hive性能优化之ORC索引–Row Group Index vs Bloom Filter Index](http://lxw1234.com/archives/2016/04/632.htm) +> ORC 内置的索引功能可以参阅这篇文章:[Hive 性能优化之 ORC 索引–Row Group Index vs Bloom Filter Index](http://lxw1234.com/archives/2016/04/632.htm) @@ -232,5 +232,5 @@ SHOW INDEX ON emp; 1. [Create/Drop/Alter View](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL#LanguageManualDDL-Create/Drop/AlterView) 2. [Materialized views](https://cwiki.apache.org/confluence/display/Hive/Materialized+views) -3. [Hive索引](http://lxw1234.com/archives/2015/05/207.htm) +3. [Hive 索引](http://lxw1234.com/archives/2015/05/207.htm) 4. [Overview of Hive Indexes](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+Indexing) diff --git a/notes/Kafka消费者详解.md b/notes/Kafka消费者详解.md index c075591..6943447 100644 --- a/notes/Kafka消费者详解.md +++ b/notes/Kafka消费者详解.md @@ -19,7 +19,7 @@ ## 一、消费者和消费者群组 -在Kafka中,消费者通常是消费者群组的一部分,多个消费者群组共同读取同一个主题时,彼此之间互不影响。Kafka之所以要引入消费者群组这个概念是因为Kafka消费者经常会做一些高延迟的操作,比如把数据写到数据库或HDFS ,或者进行耗时的计算,在这些情况下,单个消费者无法跟上数据生成的速度。此时可以增加更多的消费者,让它们分担负载,分别处理部分分区的消息,这就是Kafka实现横向伸缩的主要手段。 +在 Kafka 中,消费者通常是消费者群组的一部分,多个消费者群组共同读取同一个主题时,彼此之间互不影响。Kafka 之所以要引入消费者群组这个概念是因为 Kafka 消费者经常会做一些高延迟的操作,比如把数据写到数据库或 HDFS ,或者进行耗时的计算,在这些情况下,单个消费者无法跟上数据生成的速度。此时可以增加更多的消费者,让它们分担负载,分别处理部分分区的消息,这就是 Kafka 实现横向伸缩的主要手段。
@@ -27,42 +27,42 @@
-可以看到即便消费者Consumer5空闲了,但是也不会去读取任何一个分区的数据,这同时也提醒我们在使用时应该合理设置消费者的数量,以免造成闲置和额外开销。 +可以看到即便消费者 Consumer5 空闲了,但是也不会去读取任何一个分区的数据,这同时也提醒我们在使用时应该合理设置消费者的数量,以免造成闲置和额外开销。 ## 二、分区再均衡 因为群组里的消费者共同读取主题的分区,所以当一个消费者被关闭或发生崩溃时,它就离开了群组,原本由它读取的分区将由群组里的其他消费者来读取。同时在主题发生变化时 , 比如添加了新的分区,也会发生分区与消费者的重新分配,分区的所有权从一个消费者转移到另一个消费者,这样的行为被称为再均衡。正是因为再均衡,所以消费费者群组才能保证高可用性和伸缩性。 -消费者通过向群组协调器所在的broker发送心跳来维持它们和群组的从属关系以及它们对分区的所有权。只要消费者以正常的时间间隔发送心跳,就被认为是活跃的,说明它还在读取分区里的消息。消费者会在轮询消息或提交偏移量时发送心跳。如果消费者停止发送心跳的时间足够长,会话就会过期,群组协调器认为它已经死亡,就会触发再均衡。 +消费者通过向群组协调器所在的 broker 发送心跳来维持它们和群组的从属关系以及它们对分区的所有权。只要消费者以正常的时间间隔发送心跳,就被认为是活跃的,说明它还在读取分区里的消息。消费者会在轮询消息或提交偏移量时发送心跳。如果消费者停止发送心跳的时间足够长,会话就会过期,群组协调器认为它已经死亡,就会触发再均衡。 ## 三、创建Kafka消费者 在创建消费者的时候以下以下三个选项是必选的: -- **bootstrap.servers** :指定broker的地址清单,清单里不需要包含所有的broker地址,生产者会从给定的broker里查找broker的信息。不过建议至少要提供两个broker的信息作为容错; +- **bootstrap.servers** :指定 broker 的地址清单,清单里不需要包含所有的 broker 地址,生产者会从给定的 broker 里查找 broker 的信息。不过建议至少要提供两个 broker 的信息作为容错; - **key.deserializer** :指定键的反序列化器; - **value.deserializer** :指定值的反序列化器。 -除此之外你还需要指明你需要想订阅的主题,可以使用如下两个API : +除此之外你还需要指明你需要想订阅的主题,可以使用如下两个 API : + **consumer.subscribe(Collection\ topics)** :指明需要订阅的主题的集合; + **consumer.subscribe(Pattern pattern)** :使用正则来匹配需要订阅的集合。 -最后只需要通过轮询API(`poll`)向服务器定时请求数据。一旦消费者订阅了主题,轮询就会处理所有的细节,包括群组协调、分区再均衡、发送心跳和获取数据,这使得开发者只需要关注从分区返回的数据,然后进行业务处理。 示例如下: +最后只需要通过轮询 API(`poll`) 向服务器定时请求数据。一旦消费者订阅了主题,轮询就会处理所有的细节,包括群组协调、分区再均衡、发送心跳和获取数据,这使得开发者只需要关注从分区返回的数据,然后进行业务处理。 示例如下: ```scala String topic = "Hello-Kafka"; String group = "group1"; Properties props = new Properties(); props.put("bootstrap.servers", "hadoop001:9092"); -/*指定分组ID*/ +/*指定分组 ID*/ props.put("group.id", group); props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); KafkaConsumer consumer = new KafkaConsumer<>(props); -/*订阅主题(s)*/ +/*订阅主题 (s)*/ consumer.subscribe(Collections.singletonList(topic)); try { @@ -79,13 +79,13 @@ try { } ``` -> 本篇文章的所有示例代码可以从Github上进行下载:[kafka-basis](https://github.com/heibaiying/BigData-Notes/tree/master/code/Kafka/kafka-basis) +> 本篇文章的所有示例代码可以从 Github 上进行下载:[kafka-basis](https://github.com/heibaiying/BigData-Notes/tree/master/code/Kafka/kafka-basis) ## 三、 自动提交偏移量 ### 3.1 偏移量的重要性 -Kafka的每一条消息都有一个偏移量属性,记录了其在分区中的位置,偏移量是一个单调递增的整数。消费者通过往一个叫作 `_consumer_offset` 的特殊主题发送消息,消息里包含每个分区的偏移量。 如果消费者一直处于运行状态,那么偏移量就没有 +Kafka 的每一条消息都有一个偏移量属性,记录了其在分区中的位置,偏移量是一个单调递增的整数。消费者通过往一个叫作 `_consumer_offset` 的特殊主题发送消息,消息里包含每个分区的偏移量。 如果消费者一直处于运行状态,那么偏移量就没有 什么用处。不过,如果有消费者退出或者新分区加入,此时就会触发再均衡。完成再均衡之后,每个消费者可能分配到新的分区,而不是之前处理的那个。为了能够继续之前的工作,消费者需要读取每个分区最后一次提交的偏移量,然后从偏移量指定的地方继续处理。 因为这个原因,所以如果不能正确提交偏移量,就可能会导致数据丢失或者重复出现消费,比如下面情况: + 如果提交的偏移量小于客户端处理的最后一个消息的偏移量 ,那么处于两个偏移量之间的消息就会被重复消费; @@ -93,26 +93,26 @@ Kafka的每一条消息都有一个偏移量属性,记录了其在分区中的 ### 3.2 自动提交偏移量 -Kafka支持自动提交和手动提交偏移量两种方式。这里先介绍比较简单的自动提交: +Kafka 支持自动提交和手动提交偏移量两种方式。这里先介绍比较简单的自动提交: -只需要将消费者的`enable.auto.commit`属性配置为`true`即可完成自动提交的配置。 此时每隔固定的时间,消费者就会把`poll()`方法接收到的最大偏移量进行提交,提交间隔由`auto.commit.interval.ms`属性进行配置,默认值是5s。 +只需要将消费者的 `enable.auto.commit` 属性配置为 `true` 即可完成自动提交的配置。 此时每隔固定的时间,消费者就会把 `poll()` 方法接收到的最大偏移量进行提交,提交间隔由 `auto.commit.interval.ms` 属性进行配置,默认值是 5s。 -使用自动提交是存在隐患的,假设我们使用默认的 5s 提交时间间隔,在最近一次提交之后的 3s 发生了再均衡,再均衡之后,消费者从最后一次提交的偏移量位置开始读取消息。这个时候偏移量已经落后了 3s ,所以在这 3s 内到达的消息会被重复处理。可以通过修改提交时间间隔来更频繁地提交偏移量,减小可能出现重复消息的时间窗,不过这种情况是无法完全避免的。基于这个原因,Kafka也提供了手动提交偏移量的API,使得用户可以更为灵活的提交偏移量。 +使用自动提交是存在隐患的,假设我们使用默认的 5s 提交时间间隔,在最近一次提交之后的 3s 发生了再均衡,再均衡之后,消费者从最后一次提交的偏移量位置开始读取消息。这个时候偏移量已经落后了 3s ,所以在这 3s 内到达的消息会被重复处理。可以通过修改提交时间间隔来更频繁地提交偏移量,减小可能出现重复消息的时间窗,不过这种情况是无法完全避免的。基于这个原因,Kafka 也提供了手动提交偏移量的 API,使得用户可以更为灵活的提交偏移量。 ## 四、手动提交偏移量 -用户可以通过将`enable.auto.commit`设为`false`,然后手动提交偏移量。基于用户需求手动提交偏移量可以分为两大类: +用户可以通过将 `enable.auto.commit` 设为 `false`,然后手动提交偏移量。基于用户需求手动提交偏移量可以分为两大类: + 手动提交当前偏移量:即手动提交当前轮询的最大偏移量; + 手动提交固定偏移量:即按照业务需求,提交某一个固定的偏移量。 -而按照Kafka API,手动提交偏移量又可以分为同步提交和异步提交。 +而按照 Kafka API,手动提交偏移量又可以分为同步提交和异步提交。 ### 4.1 同步提交 -通过调用`consumer.commitSync()`来进行同步提交,不传递任何参数时提交的是当前轮询的最大偏移量。 +通过调用 `consumer.commitSync()` 来进行同步提交,不传递任何参数时提交的是当前轮询的最大偏移量。 ```java while (true) { @@ -125,11 +125,11 @@ while (true) { } ``` -如果某个提交失败,同步提交还会进行重试,这可以保证数据能够最大限度提交成功,但是同时也会降低程序的吞吐量。基于这个原因,Kafka还提供了异步提交的API。 +如果某个提交失败,同步提交还会进行重试,这可以保证数据能够最大限度提交成功,但是同时也会降低程序的吞吐量。基于这个原因,Kafka 还提供了异步提交的 API。 ### 4.2 异步提交 -异步提交可以提高程序的吞吐量,因为此时你可以尽管请求数据,而不用等待Broker的响应。代码如下: +异步提交可以提高程序的吞吐量,因为此时你可以尽管请求数据,而不用等待 Broker 的响应。代码如下: ```java while (true) { @@ -151,7 +151,7 @@ while (true) { } ``` -异步提交存在的问题是,在提交失败的时候不会进行自动重试,实际上也不能进行自动重试。假设程序同时提交了200和300的偏移量,此时200的偏移量失败的,但是紧随其后的300的偏移量成功了,此时如果重试就会存在200覆盖300偏移量的可能。同步提交就不存在这个问题,因为在同步提交的情况下,300的提交请求必须等待服务器返回200提交请求的成功反馈后才会发出。基于这个原因,某些情况下,需要同时组合同步和异步两种提交方式。 +异步提交存在的问题是,在提交失败的时候不会进行自动重试,实际上也不能进行自动重试。假设程序同时提交了 200 和 300 的偏移量,此时 200 的偏移量失败的,但是紧随其后的 300 的偏移量成功了,此时如果重试就会存在 200 覆盖 300 偏移量的可能。同步提交就不存在这个问题,因为在同步提交的情况下,300 的提交请求必须等待服务器返回 200 提交请求的成功反馈后才会发出。基于这个原因,某些情况下,需要同时组合同步和异步两种提交方式。 > 注:虽然程序不能在失败时候进行自动重试,但是我们是可以手动进行重试的,你可以通过一个 Map offsets 来维护你提交的每个分区的偏移量,然后当失败时候,你可以判断失败的偏移量是否小于你维护的同主题同分区的最后提交的偏移量,如果小于则代表你已经提交了更大的偏移量请求,此时不需要重试,否则就可以进行手动重试。 @@ -183,7 +183,7 @@ try { ### 4.4 提交特定偏移量 -在上面同步和异步提交的API中,实际上我们都没有对commit方法传递参数,此时默认提交的是当前轮询的最大偏移量,如果你需要提交特定的偏移量,可以调用它们的重载方法。 +在上面同步和异步提交的 API 中,实际上我们都没有对 commit 方法传递参数,此时默认提交的是当前轮询的最大偏移量,如果你需要提交特定的偏移量,可以调用它们的重载方法。 ```java /*同步提交特定偏移量*/ @@ -192,7 +192,7 @@ commitSync(Map offsets) commitAsync(Map offsets, OffsetCommitCallback callback) ``` -需要注意的是,因为你可以订阅多个主题,所以`offsets`中必须要包含所有主题的每个分区的偏移量,示例代码如下: +需要注意的是,因为你可以订阅多个主题,所以 `offsets` 中必须要包含所有主题的每个分区的偏移量,示例代码如下: ```java try { @@ -203,7 +203,7 @@ try { /*记录每个主题的每个分区的偏移量*/ TopicPartition topicPartition = new TopicPartition(record.topic(), record.partition()); OffsetAndMetadata offsetAndMetadata = new OffsetAndMetadata(record.offset()+1, "no metaData"); - /*TopicPartition重写过hashCode和equals方法,所以能够保证同一主题和分区的实例不会被重复添加*/ + /*TopicPartition 重写过 hashCode 和 equals 方法,所以能够保证同一主题和分区的实例不会被重复添加*/ offsets.put(topicPartition, offsetAndMetadata); } /*提交特定偏移量*/ @@ -218,7 +218,7 @@ try { ## 五、监听分区再均衡 -因为分区再均衡会导致分区与消费者的重新划分,有时候你可能希望在再均衡前执行一些操作:比如提交已经处理但是尚未提交的偏移量,关闭数据库连接等。此时可以在订阅主题时候,调用`subscribe`的重载方法传入自定义的分区再均衡监听器。 +因为分区再均衡会导致分区与消费者的重新划分,有时候你可能希望在再均衡前执行一些操作:比如提交已经处理但是尚未提交的偏移量,关闭数据库连接等。此时可以在订阅主题时候,调用 `subscribe` 的重载方法传入自定义的分区再均衡监听器。 ```java /*订阅指定集合内的所有主题*/ @@ -255,7 +255,7 @@ try { System.out.println(record); TopicPartition topicPartition = new TopicPartition(record.topic(), record.partition()); OffsetAndMetadata offsetAndMetadata = new OffsetAndMetadata(record.offset() + 1, "no metaData"); - /*TopicPartition重写过hashCode和equals方法,所以能够保证同一主题和分区的实例不会被重复添加*/ + /*TopicPartition 重写过 hashCode 和 equals 方法,所以能够保证同一主题和分区的实例不会被重复添加*/ offsets.put(topicPartition, offsetAndMetadata); } consumer.commitAsync(offsets, null); @@ -269,12 +269,12 @@ try { ## 六 、退出轮询 -Kafka提供了`consumer.wakeup()`方法用于退出轮询,它通过抛出`WakeupException`异常来跳出循环。需要注意的是,在退出线程时最好显示的调用`consumer.close()` , 此时消费者会提交任何还没有提交的东西,并向群组协调器发送消息,告知自己要离开群组,接下来就会触发再均衡 ,而不需要等待会话超时。 +Kafka 提供了 `consumer.wakeup()` 方法用于退出轮询,它通过抛出 `WakeupException` 异常来跳出循环。需要注意的是,在退出线程时最好显示的调用 `consumer.close()` , 此时消费者会提交任何还没有提交的东西,并向群组协调器发送消息,告知自己要离开群组,接下来就会触发再均衡 ,而不需要等待会话超时。 -下面的示例代码为监听控制台输出,当输入`exit`时结束轮询,关闭消费者并退出程序: +下面的示例代码为监听控制台输出,当输入 `exit` 时结束轮询,关闭消费者并退出程序: ```java -/*调用wakeup优雅的退出*/ +/*调用 wakeup 优雅的退出*/ final Thread mainThread = Thread.currentThread(); new Thread(() -> { Scanner sc = new Scanner(System.in); @@ -301,10 +301,10 @@ try { } } } catch (WakeupException e) { - //对于wakeup()调用引起的WakeupException异常可以不必处理 + //对于 wakeup() 调用引起的 WakeupException 异常可以不必处理 } finally { consumer.close(); - System.out.println("consumer关闭"); + System.out.println("consumer 关闭"); } ``` @@ -312,7 +312,7 @@ try { ## 七、独立的消费者 -因为Kafka的设计目标是高吞吐和低延迟,所以在Kafka中,消费者通常都是从属于某个群组的,这是因为单个消费者的处理能力是有限的。但是某些时候你的需求可能很简单,比如可能只需要一个消费者从一个主题的所有分区或者某个特定的分区读取数据,这个时候就不需要消费者群组和再均衡了, 只需要把主题或者分区分配给消费者,然后开始读取消息井提交偏移量即可。 +因为 Kafka 的设计目标是高吞吐和低延迟,所以在 Kafka 中,消费者通常都是从属于某个群组的,这是因为单个消费者的处理能力是有限的。但是某些时候你的需求可能很简单,比如可能只需要一个消费者从一个主题的所有分区或者某个特定的分区读取数据,这个时候就不需要消费者群组和再均衡了, 只需要把主题或者分区分配给消费者,然后开始读取消息井提交偏移量即可。 在这种情况下,就不需要订阅主题, 取而代之的是消费者为自己分配分区。 一个消费者可以订阅主题(井加入消费者群组),或者为自己分配分区,但不能同时做这两件事情。 分配分区的示例代码如下: @@ -320,7 +320,7 @@ try { List partitions = new ArrayList<>(); List partitionInfos = consumer.partitionsFor(topic); -/*可以指定读取哪些分区 如这里假设只读取主题的0分区*/ +/*可以指定读取哪些分区 如这里假设只读取主题的 0 分区*/ for (PartitionInfo partition : partitionInfos) { if (partition.partition()==0){ partitions.add(new TopicPartition(partition.topic(), partition.partition())); @@ -347,19 +347,19 @@ while (true) { ### 1. fetch.min.byte -消费者从服务器获取记录的最小字节数。如果可用的数据量小于设置值,broker会等待有足够的可用数据时才会把它返回给消费者。 +消费者从服务器获取记录的最小字节数。如果可用的数据量小于设置值,broker 会等待有足够的可用数据时才会把它返回给消费者。 ### 2. fetch.max.wait.ms -broker返回给消费者数据的等待时间,默认是500ms。 +broker 返回给消费者数据的等待时间,默认是 500ms。 ### 3. max.partition.fetch.bytes -该属性指定了服务器从每个分区返回给消费者的最大字节数,默认为1MB。 +该属性指定了服务器从每个分区返回给消费者的最大字节数,默认为 1MB。 ### 4. session.timeout.ms -消费者在被认为死亡之前可以与服务器断开连接的时间,默认是3s。 +消费者在被认为死亡之前可以与服务器断开连接的时间,默认是 3s。 ### 5. auto.offset.reset @@ -370,23 +370,23 @@ broker返回给消费者数据的等待时间,默认是500ms。 ### 6. enable.auto.commit -是否自动提交偏移量,默认值是true。为了避免出现重复消费和数据丢失,可以把它设置为false。 +是否自动提交偏移量,默认值是 true。为了避免出现重复消费和数据丢失,可以把它设置为 false。 ### 7. client.id -客户端id,服务器用来识别消息的来源。 +客户端 id,服务器用来识别消息的来源。 ### 8. max.poll.records -单次调用`poll()`方法能够返回的记录数量。 +单次调用 `poll()` 方法能够返回的记录数量。 ### 9. receive.buffer.bytes & send.buffer.byte -这两个参数分别指定TCP socket 接收和发送数据包缓冲区的大小,-1代表使用操作系统的默认值。 +这两个参数分别指定 TCP socket 接收和发送数据包缓冲区的大小,-1 代表使用操作系统的默认值。 ## 参考资料 -1. Neha Narkhede, Gwen Shapira ,Todd Palino(著) , 薛命灯(译) . Kafka权威指南 . 人民邮电出版社 . 2017-12-26 +1. Neha Narkhede, Gwen Shapira ,Todd Palino(著) , 薛命灯 (译) . Kafka 权威指南 . 人民邮电出版社 . 2017-12-26 diff --git a/notes/Kafka深入理解分区副本机制.md b/notes/Kafka深入理解分区副本机制.md index 1452bb4..39e2764 100644 --- a/notes/Kafka深入理解分区副本机制.md +++ b/notes/Kafka深入理解分区副本机制.md @@ -22,46 +22,46 @@ ## 一、Kafka集群 -Kafka使用Zookeeper来维护集群成员(brokers)的信息。每个broker都有一个唯一标识`broker.id`,用于标识自己在集群中的身份,可以在配置文件`server.properties`中进行配置,或者由程序自动生成。下面是Kafka brokers集群自动创建的过程: +Kafka 使用 Zookeeper 来维护集群成员 (brokers) 的信息。每个 broker 都有一个唯一标识 `broker.id`,用于标识自己在集群中的身份,可以在配置文件 `server.properties` 中进行配置,或者由程序自动生成。下面是 Kafka brokers 集群自动创建的过程: -+ 每一个broker启动的时候,它会在Zookeeper的`/brokers/ids`路径下创建一个`临时节点`,并将自己的`broker.id`写入,从而将自身注册到集群; -+ 当有多个broker时,所有broker会竞争性地在Zookeeper上创建`/controller`节点,由于Zookeeper上的节点不会重复,所以必然只会有一个broker创建成功,此时该broker称为controller broker。它除了具备其他broker的功能外,**还负责管理主题分区及其副本的状态**。 -+ 当broker出现宕机或者主动退出从而导致其持有的Zookeeper会话超时时,会触发注册在Zookeeper上的watcher事件,此时Kafka会进行相应的容错处理;如果宕机的是controller broker时,还会触发新的controller选举。 ++ 每一个 broker 启动的时候,它会在 Zookeeper 的 `/brokers/ids` 路径下创建一个 ` 临时节点 `,并将自己的 `broker.id` 写入,从而将自身注册到集群; ++ 当有多个 broker 时,所有 broker 会竞争性地在 Zookeeper 上创建 `/controller` 节点,由于 Zookeeper 上的节点不会重复,所以必然只会有一个 broker 创建成功,此时该 broker 称为 controller broker。它除了具备其他 broker 的功能外,**还负责管理主题分区及其副本的状态**。 ++ 当 broker 出现宕机或者主动退出从而导致其持有的 Zookeeper 会话超时时,会触发注册在 Zookeeper 上的 watcher 事件,此时 Kafka 会进行相应的容错处理;如果宕机的是 controller broker 时,还会触发新的 controller 选举。 ## 二、副本机制 -为了保证高可用,kafka的分区是多副本的,如果一个副本丢失了,那么还可以从其他副本中获取分区数据。但是这要求对应副本的数据必须是完整的,这是Kafka数据一致性的基础,所以才需要使用`controller broker`来进行专门的管理。下面将详解介绍Kafka的副本机制。 +为了保证高可用,kafka 的分区是多副本的,如果一个副本丢失了,那么还可以从其他副本中获取分区数据。但是这要求对应副本的数据必须是完整的,这是 Kafka 数据一致性的基础,所以才需要使用 `controller broker` 来进行专门的管理。下面将详解介绍 Kafka 的副本机制。 ### 2.1 分区和副本 -Kafka 的主题被分为多个分区 ,分区是Kafka最基本的存储单位。每个分区可以有多个副本(可以在创建主题时使用` replication-factor`参数进行指定)。其中一个副本是首领副本(Leader replica),所有的事件都直接发送给首领副本;其他副本是跟随者副本(Follower replica),需要通过复制来保持与首领副本数据一致,当首领副本不可用时,其中一个跟随者副本将成为新首领。 +Kafka 的主题被分为多个分区 ,分区是 Kafka 最基本的存储单位。每个分区可以有多个副本 (可以在创建主题时使用 ` replication-factor` 参数进行指定)。其中一个副本是首领副本 (Leader replica),所有的事件都直接发送给首领副本;其他副本是跟随者副本 (Follower replica),需要通过复制来保持与首领副本数据一致,当首领副本不可用时,其中一个跟随者副本将成为新首领。
### 2.2 ISR机制 -每个分区都有一个ISR(in-sync Replica)列表,用于维护所有同步的、可用的副本。首领副本必然是同步副本,而对于跟随者副本来说,它需要满足以下条件才能被认为是同步副本: +每个分区都有一个 ISR(in-sync Replica) 列表,用于维护所有同步的、可用的副本。首领副本必然是同步副本,而对于跟随者副本来说,它需要满足以下条件才能被认为是同步副本: -+ 与Zookeeper之间有一个活跃的会话,即必须定时向Zookeeper发送心跳; ++ 与 Zookeeper 之间有一个活跃的会话,即必须定时向 Zookeeper 发送心跳; + 在规定的时间内从首领副本那里低延迟地获取过消息。 -如果副本不满足上面条件的话,就会被从ISR列表中移除,直到满足条件才会被再次加入。 +如果副本不满足上面条件的话,就会被从 ISR 列表中移除,直到满足条件才会被再次加入。 -这里给出一个主题创建的示例:使用`--replication-factor`指定副本系数为3,创建成功后使用`--describe `命令可以看到分区0的有0,1,2三个副本,且三个副本都在ISR列表中,其中1为首领副本。 +这里给出一个主题创建的示例:使用 `--replication-factor` 指定副本系数为 3,创建成功后使用 `--describe ` 命令可以看到分区 0 的有 0,1,2 三个副本,且三个副本都在 ISR 列表中,其中 1 为首领副本。
### 2.3 不完全的首领选举 -对于副本机制,在broker级别有一个可选的配置参数`unclean.leader.election.enable`,默认值为fasle,代表禁止不完全的首领选举。这是针对当首领副本挂掉且ISR中没有其他可用副本时,是否允许某个不完全同步的副本成为首领副本,这可能会导致数据丢失或者数据不一致,在某些对数据一致性要求较高的场景(如金融领域),这可能无法容忍的,所以其默认值为false,如果你能够允许部分数据不一致的话,可以配置为true。 +对于副本机制,在 broker 级别有一个可选的配置参数 `unclean.leader.election.enable`,默认值为 fasle,代表禁止不完全的首领选举。这是针对当首领副本挂掉且 ISR 中没有其他可用副本时,是否允许某个不完全同步的副本成为首领副本,这可能会导致数据丢失或者数据不一致,在某些对数据一致性要求较高的场景 (如金融领域),这可能无法容忍的,所以其默认值为 false,如果你能够允许部分数据不一致的话,可以配置为 true。 ### 2.4 最少同步副本 -ISR机制的另外一个相关参数是`min.insync.replicas` , 可以在broker或者主题级别进行配置,代表ISR列表中至少要有几个可用副本。这里假设设置为2,那么当可用副本数量小于该值时,就认为整个分区处于不可用状态。此时客户端再向分区写入数据时候就会抛出异常`org.apache.kafka.common.errors.NotEnoughReplicasExceptoin: Messages are rejected since there are fewer in-sync replicas than required。` +ISR 机制的另外一个相关参数是 `min.insync.replicas` , 可以在 broker 或者主题级别进行配置,代表 ISR 列表中至少要有几个可用副本。这里假设设置为 2,那么当可用副本数量小于该值时,就认为整个分区处于不可用状态。此时客户端再向分区写入数据时候就会抛出异常 `org.apache.kafka.common.errors.NotEnoughReplicasExceptoin: Messages are rejected since there are fewer in-sync replicas than required。` ### 2.5 发送确认 -Kafka在生产者上有一个可选的参数ack,该参数指定了必须要有多少个分区副本收到消息,生产者才会认为消息写入成功: +Kafka 在生产者上有一个可选的参数 ack,该参数指定了必须要有多少个分区副本收到消息,生产者才会认为消息写入成功: - **acks=0** :消息发送出去就认为已经成功了,不会等待任何来自服务器的响应; - **acks=1** : 只要集群的首领节点收到消息,生产者就会收到一个来自服务器成功响应; @@ -71,44 +71,44 @@ Kafka在生产者上有一个可选的参数ack,该参数指定了必须要有 ### 3.1 元数据请求机制 -在所有副本中,只有领导副本才能进行消息的读写处理。由于不同分区的领导副本可能在不同的broker上,如果某个broker收到了一个分区请求,但是该分区的领导副本并不在该broker上,那么它就会向客户端返回一个`Not a Leader for Partition`的错误响应。 为了解决这个问题,Kafka提供了元数据请求机制。 +在所有副本中,只有领导副本才能进行消息的读写处理。由于不同分区的领导副本可能在不同的 broker 上,如果某个 broker 收到了一个分区请求,但是该分区的领导副本并不在该 broker 上,那么它就会向客户端返回一个 `Not a Leader for Partition` 的错误响应。 为了解决这个问题,Kafka 提供了元数据请求机制。 -首先集群中的每个broker都会缓存所有主题的分区副本信息,客户端会定期发送发送元数据请求,然后将获取的元数据进行缓存。定时刷新元数据的时间间隔可以通过为客户端配置`metadata.max.age.ms`来进行指定。有了元数据信息后,客户端就知道了领导副本所在的broker,之后直接将读写请求发送给对应的broker即可。 +首先集群中的每个 broker 都会缓存所有主题的分区副本信息,客户端会定期发送发送元数据请求,然后将获取的元数据进行缓存。定时刷新元数据的时间间隔可以通过为客户端配置 `metadata.max.age.ms` 来进行指定。有了元数据信息后,客户端就知道了领导副本所在的 broker,之后直接将读写请求发送给对应的 broker 即可。 -如果在定时请求的时间间隔内发生的分区副本的选举,则意味着原来缓存的信息可能已经过时了,此时还有可能会收到`Not a Leader for Partition`的错误响应,这种情况下客户端会再次求发出元数据请求,然后刷新本地缓存,之后再去正确的broker上执行对应的操作,过程如下图: +如果在定时请求的时间间隔内发生的分区副本的选举,则意味着原来缓存的信息可能已经过时了,此时还有可能会收到 `Not a Leader for Partition` 的错误响应,这种情况下客户端会再次求发出元数据请求,然后刷新本地缓存,之后再去正确的 broker 上执行对应的操作,过程如下图:
### 3.2 数据可见性 -需要注意的是,并不是所有保存在分区首领上的数据都可以被客户端读取到,为了保证数据一致性,只有被所有同步副本(ISR中所有副本)都保存了的数据才能被客户端读取到。 +需要注意的是,并不是所有保存在分区首领上的数据都可以被客户端读取到,为了保证数据一致性,只有被所有同步副本 (ISR 中所有副本) 都保存了的数据才能被客户端读取到。
### 3.3 零拷贝 -Kafka所有数据的写入和读取都是通过零拷贝来实现的。传统拷贝与零拷贝的区别如下: +Kafka 所有数据的写入和读取都是通过零拷贝来实现的。传统拷贝与零拷贝的区别如下: #### 传统模式下的四次拷贝与四次上下文切换 -以将磁盘文件通过网络发送为例。传统模式下,一般使用如下伪代码所示的方法先将文件数据读入内存,然后通过Socket将内存中的数据发送出去。 +以将磁盘文件通过网络发送为例。传统模式下,一般使用如下伪代码所示的方法先将文件数据读入内存,然后通过 Socket 将内存中的数据发送出去。 ```java buffer = File.read Socket.send(buffer) ``` -这一过程实际上发生了四次数据拷贝。首先通过系统调用将文件数据读入到内核态Buffer(DMA拷贝),然后应用程序将内存态Buffer数据读入到用户态Buffer(CPU拷贝),接着用户程序通过Socket发送数据时将用户态Buffer数据拷贝到内核态Buffer(CPU拷贝),最后通过DMA拷贝将数据拷贝到NIC Buffer。同时,还伴随着四次上下文切换,如下图所示: +这一过程实际上发生了四次数据拷贝。首先通过系统调用将文件数据读入到内核态 Buffer(DMA 拷贝),然后应用程序将内存态 Buffer 数据读入到用户态 Buffer(CPU 拷贝),接着用户程序通过 Socket 发送数据时将用户态 Buffer 数据拷贝到内核态 Buffer(CPU 拷贝),最后通过 DMA 拷贝将数据拷贝到 NIC Buffer。同时,还伴随着四次上下文切换,如下图所示:
#### sendfile和transferTo实现零拷贝 -Linux 2.4+内核通过`sendfile`系统调用,提供了零拷贝。数据通过DMA拷贝到内核态Buffer后,直接通过DMA拷贝到NIC Buffer,无需CPU拷贝。这也是零拷贝这一说法的来源。除了减少数据拷贝外,因为整个读文件到网络发送由一个`sendfile`调用完成,整个过程只有两次上下文切换,因此大大提高了性能。零拷贝过程如下图所示: +Linux 2.4+ 内核通过 `sendfile` 系统调用,提供了零拷贝。数据通过 DMA 拷贝到内核态 Buffer 后,直接通过 DMA 拷贝到 NIC Buffer,无需 CPU 拷贝。这也是零拷贝这一说法的来源。除了减少数据拷贝外,因为整个读文件到网络发送由一个 `sendfile` 调用完成,整个过程只有两次上下文切换,因此大大提高了性能。零拷贝过程如下图所示:
-从具体实现来看,Kafka的数据传输通过TransportLayer来完成,其子类`PlaintextTransportLayer`的`transferFrom`方法通过调用Java NIO中FileChannel的`transferTo`方法实现零拷贝,如下所示: +从具体实现来看,Kafka 的数据传输通过 TransportLayer 来完成,其子类 `PlaintextTransportLayer` 的 `transferFrom` 方法通过调用 Java NIO 中 FileChannel 的 `transferTo` 方法实现零拷贝,如下所示: ```java @Override @@ -117,19 +117,19 @@ public long transferFrom(FileChannel fileChannel, long position, long count) thr } ``` -**注:** `transferTo`和`transferFrom`并不保证一定能使用零拷贝。实际上是否能使用零拷贝与操作系统相关,如果操作系统提供`sendfile`这样的零拷贝系统调用,则这两个方法会通过这样的系统调用充分利用零拷贝的优势,否则并不能通过这两个方法本身实现零拷贝。 +**注:** `transferTo` 和 `transferFrom` 并不保证一定能使用零拷贝。实际上是否能使用零拷贝与操作系统相关,如果操作系统提供 `sendfile` 这样的零拷贝系统调用,则这两个方法会通过这样的系统调用充分利用零拷贝的优势,否则并不能通过这两个方法本身实现零拷贝。 ## 四、物理存储 ### 4.1 分区分配 -在创建主题时,Kafka会首先决定如何在broker间分配分区副本,它遵循以下原则: +在创建主题时,Kafka 会首先决定如何在 broker 间分配分区副本,它遵循以下原则: -+ 在所有broker上均匀地分配分区副本; -+ 确保分区的每个副本分布在不同的broker上; -+ 如果使用了`broker.rack`参数为broker指定了机架信息,那么会尽可能的把每个分区的副本分配到不同机架的broker上,以避免一个机架不可用而导致整个分区不可用。 ++ 在所有 broker 上均匀地分配分区副本; ++ 确保分区的每个副本分布在不同的 broker 上; ++ 如果使用了 `broker.rack` 参数为 broker 指定了机架信息,那么会尽可能的把每个分区的副本分配到不同机架的 broker 上,以避免一个机架不可用而导致整个分区不可用。 -基于以上原因,如果你在一个单节点上创建一个3副本的主题,通常会抛出下面的异常: +基于以上原因,如果你在一个单节点上创建一个 3 副本的主题,通常会抛出下面的异常: ```properties Error while executing topic command : org.apache.kafka.common.errors.InvalidReplicationFactor @@ -138,18 +138,18 @@ Exception: Replication factor: 3 larger than available brokers: 1. ### 4.2 分区数据保留规则 -保留数据是 Kafka 的一个基本特性, 但是Kafka不会一直保留数据,也不会等到所有消费者都读取了消息之后才删除消息。相反, Kafka为每个主题配置了数据保留期限,规定数据被删除之前可以保留多长时间,或者清理数据之前可以保留的数据量大小。分别对应以下四个参数: +保留数据是 Kafka 的一个基本特性, 但是 Kafka 不会一直保留数据,也不会等到所有消费者都读取了消息之后才删除消息。相反, Kafka 为每个主题配置了数据保留期限,规定数据被删除之前可以保留多长时间,或者清理数据之前可以保留的数据量大小。分别对应以下四个参数: - `log.retention.bytes` :删除数据前允许的最大数据量;默认值-1,代表没有限制; -- `log.retention.ms`:保存数据文件的毫秒数,如果未设置,则使用`log.retention.minutes`中的值,默认为null; -- `log.retention.minutes`:保留数据文件的分钟数,如果未设置,则使用`log.retention.hours`中的值,默认为null; -- `log.retention.hours`:保留数据文件的小时数,默认值为168,也就是一周。 +- `log.retention.ms`:保存数据文件的毫秒数,如果未设置,则使用 `log.retention.minutes` 中的值,默认为 null; +- `log.retention.minutes`:保留数据文件的分钟数,如果未设置,则使用 `log.retention.hours` 中的值,默认为 null; +- `log.retention.hours`:保留数据文件的小时数,默认值为 168,也就是一周。 -因为在一个大文件里查找和删除消息是很费时的,也很容易出错,所以Kafka把分区分成若干个片段,当前正在写入数据的片段叫作活跃片段。活动片段永远不会被删除。如果按照默认值保留数据一周,而且每天使用一个新片段,那么你就会看到,在每天使用一个新片段的同时会删除一个最老的片段,所以大部分时间该分区会有7个片段存在。 +因为在一个大文件里查找和删除消息是很费时的,也很容易出错,所以 Kafka 把分区分成若干个片段,当前正在写入数据的片段叫作活跃片段。活动片段永远不会被删除。如果按照默认值保留数据一周,而且每天使用一个新片段,那么你就会看到,在每天使用一个新片段的同时会删除一个最老的片段,所以大部分时间该分区会有 7 个片段存在。 ### 4.3 文件格式 -通常保存在磁盘上的数据格式与生产者发送过来消息格式是一样的。 如果生产者发送的是压缩过的消息,那么同一个批次的消息会被压缩在一起,被当作“包装消息”进行发送(格式如下所示) ,然后保存到磁盘上。之后消费者读取后再自己解压这个包装消息,获取每条消息的具体信息。 +通常保存在磁盘上的数据格式与生产者发送过来消息格式是一样的。 如果生产者发送的是压缩过的消息,那么同一个批次的消息会被压缩在一起,被当作“包装消息”进行发送 (格式如下所示) ,然后保存到磁盘上。之后消费者读取后再自己解压这个包装消息,获取每条消息的具体信息。
@@ -157,5 +157,5 @@ Exception: Replication factor: 3 larger than available brokers: 1. ## 参考资料 -1. Neha Narkhede, Gwen Shapira ,Todd Palino(著) , 薛命灯(译) . Kafka权威指南 . 人民邮电出版社 . 2017-12-26 -2. [Kafka高性能架构之道](http://www.jasongj.com/kafka/high_throughput/) +1. Neha Narkhede, Gwen Shapira ,Todd Palino(著) , 薛命灯 (译) . Kafka 权威指南 . 人民邮电出版社 . 2017-12-26 +2. [Kafka 高性能架构之道](http://www.jasongj.com/kafka/high_throughput/) diff --git a/notes/Kafka生产者详解.md b/notes/Kafka生产者详解.md index 2bb4834..2e7c5ed 100644 --- a/notes/Kafka生产者详解.md +++ b/notes/Kafka生产者详解.md @@ -13,11 +13,11 @@ ## 一、生产者发送消息的过程 -首先介绍一下Kafka生产者发送消息的过程: +首先介绍一下 Kafka 生产者发送消息的过程: -+ Kafka会将发送消息包装为ProducerRecord对象, ProducerRecord对象包含了目标主题和要发送的内容,同时还可以指定键和分区。在发送ProducerRecord对象前,生产者会先把键和值对象序列化成字节数组,这样它们才能够在网络上传输。 -+ 接下来,数据被传给分区器。如果之前已经在ProducerRecord对象里指定了分区,那么分区器就不会再做任何事情。如果没有指定分区 ,那么分区器会根据ProducerRecord对象的键来选择一个分区,紧接着,这条记录被添加到一个记录批次里,这个批次里的所有消息会被发送到相同的主题和分区上。有一个独立的线程负责把这些记录批次发送到相应的broker上。 -+ 服务器在收到这些消息时会返回一个响应。如果消息成功写入Kafka,就返回一个RecordMetaData对象,它包含了主题和分区信息,以及记录在分区里的偏移量。如果写入失败,则会返回一个错误。生产者在收到错误之后会尝试重新发送消息,如果达到指定的重试次数后还没有成功,则直接抛出异常,不再重试。 ++ Kafka 会将发送消息包装为 ProducerRecord 对象, ProducerRecord 对象包含了目标主题和要发送的内容,同时还可以指定键和分区。在发送 ProducerRecord 对象前,生产者会先把键和值对象序列化成字节数组,这样它们才能够在网络上传输。 ++ 接下来,数据被传给分区器。如果之前已经在 ProducerRecord 对象里指定了分区,那么分区器就不会再做任何事情。如果没有指定分区 ,那么分区器会根据 ProducerRecord 对象的键来选择一个分区,紧接着,这条记录被添加到一个记录批次里,这个批次里的所有消息会被发送到相同的主题和分区上。有一个独立的线程负责把这些记录批次发送到相应的 broker 上。 ++ 服务器在收到这些消息时会返回一个响应。如果消息成功写入 Kafka,就返回一个 RecordMetaData 对象,它包含了主题和分区信息,以及记录在分区里的偏移量。如果写入失败,则会返回一个错误。生产者在收到错误之后会尝试重新发送消息,如果达到指定的重试次数后还没有成功,则直接抛出异常,不再重试。
@@ -25,7 +25,7 @@ ### 2.1 项目依赖 -本项目采用Maven构建,想要调用Kafka生产者API,需要导入`kafka-clients`依赖,如下: +本项目采用 Maven 构建,想要调用 Kafka 生产者 API,需要导入 `kafka-clients` 依赖,如下: ```xml @@ -37,9 +37,9 @@ ### 2.2 创建生产者 -创建Kafka生产者时,以下三个属性是必须指定的: +创建 Kafka 生产者时,以下三个属性是必须指定的: -+ **bootstrap.servers** :指定broker的地址清单,清单里不需要包含所有的broker地址,生产者会从给定的broker里查找broker的信息。不过建议至少要提供两个broker的信息作为容错; ++ **bootstrap.servers** :指定 broker 的地址清单,清单里不需要包含所有的 broker 地址,生产者会从给定的 broker 里查找 broker 的信息。不过建议至少要提供两个 broker 的信息作为容错; + **key.serializer** :指定键的序列化器; + **value.serializer** :指定值的序列化器。 @@ -71,13 +71,13 @@ public class SimpleProducer { } ``` -> 本篇文章的所有示例代码可以从Github上进行下载:[kafka-basis](https://github.com/heibaiying/BigData-Notes/tree/master/code/Kafka/kafka-basis) +> 本篇文章的所有示例代码可以从 Github 上进行下载:[kafka-basis](https://github.com/heibaiying/BigData-Notes/tree/master/code/Kafka/kafka-basis) ### 2.3 测试 #### 1. 启动Kakfa -Kafka的运行依赖于zookeeper,需要预先启动,可以启动Kafka内置的zookeeper,也可以启动自己安装的: +Kafka 的运行依赖于 zookeeper,需要预先启动,可以启动 Kafka 内置的 zookeeper,也可以启动自己安装的: ```shell # zookeeper启动命令 @@ -87,7 +87,7 @@ bin/zkServer.sh start bin/zookeeper-server-start.sh config/zookeeper.properties ``` -启动单节点kafka用于测试: +启动单节点 kafka 用于测试: ```shell # bin/kafka-server-start.sh config/server.properties @@ -116,7 +116,7 @@ bin/kafka-topics.sh --create \ #### 4. 运行项目 -此时可以看到消费者控制台,输出如下,这里`kafka-console-consumer`只会打印出值信息,不会打印出键信息。 +此时可以看到消费者控制台,输出如下,这里 `kafka-console-consumer` 只会打印出值信息,不会打印出键信息。
@@ -124,7 +124,7 @@ bin/kafka-topics.sh --create \ ### 2.4 可能出现的问题 -在这里可能出现的一个问题是:生产者程序在启动后,一直处于等待状态。这通常出现在你使用默认配置启动Kafka的情况下,此时需要对`server.properties`文件中的`listeners`配置进行更改: +在这里可能出现的一个问题是:生产者程序在启动后,一直处于等待状态。这通常出现在你使用默认配置启动 Kafka 的情况下,此时需要对 `server.properties` 文件中的 `listeners` 配置进行更改: ```shell # hadoop001 为我启动kafka服务的主机名,你可以换成自己的主机名或者ip地址 @@ -135,11 +135,11 @@ listeners=PLAINTEXT://hadoop001:9092 ## 二、发送消息 -上面的示例程序调用了`send`方法发送消息后没有做任何操作,在这种情况下,我们没有办法知道消息发送的结果。想要知道消息发送的结果,可以使用同步发送或者异步发送来实现。 +上面的示例程序调用了 `send` 方法发送消息后没有做任何操作,在这种情况下,我们没有办法知道消息发送的结果。想要知道消息发送的结果,可以使用同步发送或者异步发送来实现。 ### 2.1 同步发送 -在调用`send`方法后可以接着调用`get()`方法,`send`方法的返回值是一个Future\对象,RecordMetadata里面包含了发送消息的主题、分区、偏移量等信息。改写后的代码如下: +在调用 `send` 方法后可以接着调用 `get()` 方法,`send` 方法的返回值是一个 Future\对象,RecordMetadata 里面包含了发送消息的主题、分区、偏移量等信息。改写后的代码如下: ```scala for (int i = 0; i < 10; i++) { @@ -155,7 +155,7 @@ for (int i = 0; i < 10; i++) { } ``` -此时得到的输出如下:偏移量和调用次数有关,所有记录都分配到了0分区,这是因为在创建`Hello-Kafka`主题时候,使用`--partitions`指定其分区数为1,即只有一个分区。 +此时得到的输出如下:偏移量和调用次数有关,所有记录都分配到了 0 分区,这是因为在创建 `Hello-Kafka` 主题时候,使用 `--partitions` 指定其分区数为 1,即只有一个分区。 ```shell topic=Hello-Kafka, partition=0, offset=40 @@ -172,7 +172,7 @@ topic=Hello-Kafka, partition=0, offset=49 ### 2.2 异步发送 -通常我们并不关心发送成功的情况,更多关注的是失败的情况,因此Kafka提供了异步发送和回调函数。 代码如下: +通常我们并不关心发送成功的情况,更多关注的是失败的情况,因此 Kafka 提供了异步发送和回调函数。 代码如下: ```scala for (int i = 0; i < 10; i++) { @@ -196,10 +196,10 @@ for (int i = 0; i < 10; i++) { ## 三、自定义分区器 -Kafka有着默认的分区机制: +Kafka 有着默认的分区机制: -+ 如果键值为 null, 则使用轮询(Round Robin)算法将消息均衡地分布到各个分区上; -+ 如果键值不为null,那么Kafka会使用内置的散列算法对键进行散列,然后分布到各个分区上。 ++ 如果键值为 null, 则使用轮询 (Round Robin) 算法将消息均衡地分布到各个分区上; ++ 如果键值不为 null,那么 Kafka 会使用内置的散列算法对键进行散列,然后分布到各个分区上。 某些情况下,你可能有着自己的分区需求,这时候可以采用自定义分区器实现。这里给出一个自定义分区器的示例: @@ -222,7 +222,7 @@ public class CustomPartitioner implements Partitioner { @Override public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) { - /*key值为分数,当分数大于分数线时候,分配到1分区,否则分配到0分区*/ + /*key 值为分数,当分数大于分数线时候,分配到 1 分区,否则分配到 0 分区*/ return (Integer) key >= passLine ? 1 : 0; } @@ -278,7 +278,7 @@ public class ProducerWithPartitioner { --topic Kafka-Partitioner-Test ``` -此时输入如下,可以看到分数大于等于6分的都被分到1分区,而小于6分的都被分到了0分区。 +此时输入如下,可以看到分数大于等于 6 分的都被分到 1 分区,而小于 6 分的都被分到了 0 分区。 ```shell score:6, partition=1, @@ -299,7 +299,7 @@ score:5, partition=0, ## 四、生产者其他属性 -上面生产者的创建都仅指定了服务地址,键序列化器、值序列化器,实际上Kafka的生产者还有很多可配置属性,如下: +上面生产者的创建都仅指定了服务地址,键序列化器、值序列化器,实际上 Kafka 的生产者还有很多可配置属性,如下: ### 1. acks @@ -315,7 +315,7 @@ acks 参数指定了必须要有多少个分区副本收到消息,生产者才 ### 3. compression.type -默认情况下,发送的消息不会被压缩。如果想要进行压缩,可以配置此参数,可选值有snappy,gzip,lz4。 +默认情况下,发送的消息不会被压缩。如果想要进行压缩,可以配置此参数,可选值有 snappy,gzip,lz4。 ### 4. retries @@ -331,29 +331,29 @@ acks 参数指定了必须要有多少个分区副本收到消息,生产者才 ### 7. clent.id -客户端id,服务器用来识别消息的来源。 +客户端 id,服务器用来识别消息的来源。 ### 8. max.in.flight.requests.per.connection -指定了生产者在收到服务器响应之前可以发送多少个消息。它的值越高,就会占用越多的内存,不过也会提升吞吐量,把它设置为1可以保证消息是按照发送的顺序写入服务器,即使发生了重试。 +指定了生产者在收到服务器响应之前可以发送多少个消息。它的值越高,就会占用越多的内存,不过也会提升吞吐量,把它设置为 1 可以保证消息是按照发送的顺序写入服务器,即使发生了重试。 ### 9. timeout.ms, request.timeout.ms & metadata.fetch.timeout.ms -- timeout.ms 指定了borker等待同步副本返回消息的确认时间; +- timeout.ms 指定了 borker 等待同步副本返回消息的确认时间; - request.timeout.ms 指定了生产者在发送数据时等待服务器返回响应的时间; - metadata.fetch.timeout.ms 指定了生产者在获取元数据(比如分区首领是谁)时等待服务器返回响应的时间。 ### 10. max.block.ms -指定了在调用`send()`方法或使用`partitionsFor()`方法获取元数据时生产者的阻塞时间。当生产者的发送缓冲区已满,或者没有可用的元数据时,这些方法会阻塞。在阻塞时间达到max.block.ms 时,生产者会抛出超时异常。 +指定了在调用 `send()` 方法或使用 `partitionsFor()` 方法获取元数据时生产者的阻塞时间。当生产者的发送缓冲区已满,或者没有可用的元数据时,这些方法会阻塞。在阻塞时间达到 max.block.ms 时,生产者会抛出超时异常。 ### 11. max.request.size -该参数用于控制生产者发送的请求大小。它可以指发送的单个消息的最大值,也可以指单个请求里所有消息总的大小。例如,假设这个值为1000K ,那么可以发送的单个最大消息为1000K ,或者生产者可以在单个请求里发送一个批次,该批次包含了 1000 个消息,每个消息大小为1K。 +该参数用于控制生产者发送的请求大小。它可以指发送的单个消息的最大值,也可以指单个请求里所有消息总的大小。例如,假设这个值为 1000K ,那么可以发送的单个最大消息为 1000K ,或者生产者可以在单个请求里发送一个批次,该批次包含了 1000 个消息,每个消息大小为 1K。 ### 12. receive.buffer.bytes & send.buffer.byte -这两个参数分别指定TCP socket 接收和发送数据包缓冲区的大小,-1代表使用操作系统的默认值。 +这两个参数分别指定 TCP socket 接收和发送数据包缓冲区的大小,-1 代表使用操作系统的默认值。 @@ -361,4 +361,4 @@ acks 参数指定了必须要有多少个分区副本收到消息,生产者才 ## 参考资料 -1. Neha Narkhede, Gwen Shapira ,Todd Palino(著) , 薛命灯(译) . Kafka权威指南 . 人民邮电出版社 . 2017-12-26 +1. Neha Narkhede, Gwen Shapira ,Todd Palino(著) , 薛命灯 (译) . Kafka 权威指南 . 人民邮电出版社 . 2017-12-26 diff --git a/notes/Kafka简介.md b/notes/Kafka简介.md index 2ccdc20..5179a3d 100644 --- a/notes/Kafka简介.md +++ b/notes/Kafka简介.md @@ -12,25 +12,25 @@ ## 一、简介 -ApacheKafka是一个分布式的流处理平台。它具有以下特点: +ApacheKafka 是一个分布式的流处理平台。它具有以下特点: -+ 支持消息的发布和订阅,类似于RabbtMQ、ActiveMQ等消息队列; ++ 支持消息的发布和订阅,类似于 RabbtMQ、ActiveMQ 等消息队列; + 支持数据实时处理; + 能保证消息的可靠性投递; + 支持消息的持久化存储,并通过多副本分布式的存储方案来保证消息的容错; -+ 高吞吐率,单Broker可以轻松处理数千个分区以及每秒百万级的消息量。 ++ 高吞吐率,单 Broker 可以轻松处理数千个分区以及每秒百万级的消息量。 ## 二、基本概念 ### 2.1 Messages And Batches -Kafka的基本数据单元被称为message(消息),为减少网络开销,提高效率,多个消息会被放入同一批次(Batch)中后再写入。 +Kafka 的基本数据单元被称为 message(消息),为减少网络开销,提高效率,多个消息会被放入同一批次 (Batch) 中后再写入。 ### 2.2 Topics And Partitions -Kafka的消息通过Topics(主题)进行分类,一个主题可以被分为若干个Partitions(分区),一个分区就是一个提交日志(commit log)。消息以追加的方式写入分区,然后以先入先出的顺序读取。Kafka通过分区来实现数据的冗余和伸缩性,分区可以分布在不同的服务器上,这意味着一个Topic可以横跨多个服务器,以提供比单个服务器更强大的性能。 +Kafka 的消息通过 Topics(主题) 进行分类,一个主题可以被分为若干个 Partitions(分区),一个分区就是一个提交日志 (commit log)。消息以追加的方式写入分区,然后以先入先出的顺序读取。Kafka 通过分区来实现数据的冗余和伸缩性,分区可以分布在不同的服务器上,这意味着一个 Topic 可以横跨多个服务器,以提供比单个服务器更强大的性能。 -由于一个Topic包含多个分区,因此无法在整个Topic范围内保证消息的顺序性,但可以保证消息在单个分区内的顺序性。 +由于一个 Topic 包含多个分区,因此无法在整个 Topic 范围内保证消息的顺序性,但可以保证消息在单个分区内的顺序性。
@@ -42,7 +42,7 @@ Kafka的消息通过Topics(主题)进行分类,一个主题可以被分为若 #### 2. 消费者 -消费者是消费者群组的一部分,消费者负责消费消息。消费者可以订阅一个或者多个主题,并按照消息生成的顺序来读取它们。消费者通过检查消息的偏移量(offset)来区分读取过的消息。偏移量是一个不断递增的数值,在创建消息时,Kafka会把它添加到其中,在给定的分区里,每个消息的偏移量都是唯一的。消费者把每个分区最后读取的偏移量保存在Zookeeper或Kafka上,如果消费者关闭或者重启,它还可以重新获取该偏移量,以保证读取状态不会丢失。 +消费者是消费者群组的一部分,消费者负责消费消息。消费者可以订阅一个或者多个主题,并按照消息生成的顺序来读取它们。消费者通过检查消息的偏移量 (offset) 来区分读取过的消息。偏移量是一个不断递增的数值,在创建消息时,Kafka 会把它添加到其中,在给定的分区里,每个消息的偏移量都是唯一的。消费者把每个分区最后读取的偏移量保存在 Zookeeper 或 Kafka 上,如果消费者关闭或者重启,它还可以重新获取该偏移量,以保证读取状态不会丢失。
@@ -52,11 +52,11 @@ Kafka的消息通过Topics(主题)进行分类,一个主题可以被分为若 ### 2.4 Brokers And Clusters -一个独立的Kafka服务器被称为Broker。Broker接收来自生产者的消息,为消息设置偏移量,并提交消息到磁盘保存。Broker为消费者提供服务,对读取分区的请求做出响应,返回已经提交到磁盘的消息。 +一个独立的 Kafka 服务器被称为 Broker。Broker 接收来自生产者的消息,为消息设置偏移量,并提交消息到磁盘保存。Broker 为消费者提供服务,对读取分区的请求做出响应,返回已经提交到磁盘的消息。 -Broker是集群(Cluster)的组成部分。每一个集群都会选举出一个Broker作为集群控制器(Controller),集群控制器负责管理工作,包括将分区分配给Broker和监控Broker。 +Broker 是集群 (Cluster) 的组成部分。每一个集群都会选举出一个 Broker 作为集群控制器 (Controller),集群控制器负责管理工作,包括将分区分配给 Broker 和监控 Broker。 -在集群中,一个分区(Partition)从属一个Broker,该Broker被称为分区的首领(Leader)。一个分区可以分配给多个Brokers,这个时候会发生分区复制。这种复制机制为分区提供了消息冗余,如果有一个Broker失效,其他Broker可以接管领导权。 +在集群中,一个分区 (Partition) 从属一个 Broker,该 Broker 被称为分区的首领 (Leader)。一个分区可以分配给多个 Brokers,这个时候会发生分区复制。这种复制机制为分区提供了消息冗余,如果有一个 Broker 失效,其他 Broker 可以接管领导权。
@@ -64,4 +64,4 @@ Broker是集群(Cluster)的组成部分。每一个集群都会选举出一个Br ## 参考资料 -Neha Narkhede, Gwen Shapira ,Todd Palino(著) , 薛命灯(译) . Kafka权威指南 . 人民邮电出版社 . 2017-12-26 +Neha Narkhede, Gwen Shapira ,Todd Palino(著) , 薛命灯 (译) . Kafka 权威指南 . 人民邮电出版社 . 2017-12-26 diff --git a/notes/Scala函数和闭包.md b/notes/Scala函数和闭包.md index f492a36..482255c 100644 --- a/notes/Scala函数和闭包.md +++ b/notes/Scala函数和闭包.md @@ -20,7 +20,7 @@ ### 1.1 函数与方法 -Scala中函数与方法的区别非常小,如果函数作为某个对象的成员,这样的函数被称为方法,否则就是一个正常的函数。 +Scala 中函数与方法的区别非常小,如果函数作为某个对象的成员,这样的函数被称为方法,否则就是一个正常的函数。 ```scala // 定义方法 @@ -32,18 +32,18 @@ println(multi1(3)) //输出 9 println(multi2(3)) //输出 9 ``` -也可以使用`def`定义函数: +也可以使用 `def` 定义函数: ```scala def multi3 = (x: Int) => {x * x} -println(multi3(3)) //输出9 +println(multi3(3)) //输出 9 ``` -`multi2`和`multi3`本质上没有区别,这是因为函数是一等公民,`val multi2 = (x: Int) => {x * x}`这个语句相当于是使用`def`预先定义了函数,之后赋值给变量`multi2`。 +`multi2` 和 `multi3` 本质上没有区别,这是因为函数是一等公民,`val multi2 = (x: Int) => {x * x}` 这个语句相当于是使用 `def` 预先定义了函数,之后赋值给变量 `multi2`。 ### 1.2 函数类型 -上面我们说过`multi2`和`multi3`本质上是一样的,那么作为函数它们是什么类型的?两者的类型实际上都是`Int => Int`,前面一个Int代表输入参数类型,后面一个Int代表返回值类型。 +上面我们说过 `multi2` 和 `multi3` 本质上是一样的,那么作为函数它们是什么类型的?两者的类型实际上都是 `Int => Int`,前面一个 Int 代表输入参数类型,后面一个 Int 代表返回值类型。 ```scala scala> val multi2 = (x: Int) => {x * x} @@ -59,19 +59,19 @@ multi4: (Int, String) => String = $$Lambda$1093/1039732747@2eb4fe7 ### 1.3 一等公民&匿名函数 -在Scala中函数是一等公民,这意味着不仅可以定义函数并调用它们,还可以将它们作为值进行传递: +在 Scala 中函数是一等公民,这意味着不仅可以定义函数并调用它们,还可以将它们作为值进行传递: ```scala import scala.math.ceil object ScalaApp extends App { - // 将函数ceil赋值给变量fun,使用下划线(_)指明是ceil函数但不传递参数 + // 将函数 ceil 赋值给变量 fun,使用下划线 (_) 指明是 ceil 函数但不传递参数 val fun = ceil _ - println(fun(2.3456)) //输出3.0 + println(fun(2.3456)) //输出 3.0 } ``` -在Scala中你不必给每一个函数都命名,如`(x: Int) => 3 * x`就是一个匿名函数: +在 Scala 中你不必给每一个函数都命名,如 `(x: Int) => 3 * x` 就是一个匿名函数: ```scala object ScalaApp extends App { @@ -93,7 +93,7 @@ object ScalaApp extends App { #### 1. 可变长度参数列表 -在Java中如果你想要传递可变长度的参数,需要使用`String ...args`这种形式,Scala中等效的表达为`args: String*`。 +在 Java 中如果你想要传递可变长度的参数,需要使用 `String ...args` 这种形式,Scala 中等效的表达为 `args: String*`。 ```scala object ScalaApp extends App { @@ -134,7 +134,7 @@ object ScalaApp extends App { def detail(name: String, age: Int = 88): Unit = println(name + ":" + age) - // 如果没有传递age值,则使用默认值 + // 如果没有传递 age 值,则使用默认值 detail("heibaiying") detail("heibaiying", 12) @@ -147,14 +147,14 @@ object ScalaApp extends App { ```scala var more = 10 -// addMore一个闭包函数:因为其捕获了自由变量more从而闭合了该函数字面量 +// addMore 一个闭包函数:因为其捕获了自由变量 more 从而闭合了该函数字面量 val addMore = (x: Int) => x + more ``` -如上函数`addMore`中有两个变量x和more: +如上函数 `addMore` 中有两个变量 x 和 more: -+ **x** : 是一个绑定变量(bound variable),因为其是该函数的入参,在函数的上下文中有明确的定义; -+ **more** : 是一个自由变量(free variable),因为函数字面量本生并没有给more赋予任何含义。 ++ **x** : 是一个绑定变量 (bound variable),因为其是该函数的入参,在函数的上下文中有明确的定义; ++ **more** : 是一个自由变量 (free variable),因为函数字面量本生并没有给 more 赋予任何含义。 按照定义:在创建函数时,如果需要捕获自由变量,那么包含指向被捕获变量的引用的函数就被称为闭包函数。 @@ -166,18 +166,18 @@ val addMore = (x: Int) => x + more + 闭包内部对自由变量的修改,在闭包外部也是可见的。 ```scala -// 声明more变量 +// 声明 more 变量 scala> var more = 10 more: Int = 10 -// more变量必须已经被声明,否则下面的语句会报错 +// more 变量必须已经被声明,否则下面的语句会报错 scala> val addMore = (x: Int) => {x + more} addMore: Int => Int = $$Lambda$1076/1844473121@876c4f0 scala> addMore(10) res7: Int = 20 -// 注意这里是给more变量赋值,而不是重新声明more变量 +// 注意这里是给 more 变量赋值,而不是重新声明 more 变量 scala> more=1000 more: Int = 1000 @@ -190,7 +190,7 @@ res8: Int = 1010 自由变量可能随着程序的改变而改变,从而产生多个副本,但是闭包永远指向创建时候有效的那个变量副本。 ```scala -// 第一次声明more变量 +// 第一次声明 more 变量 scala> var more = 10 more: Int = 10 @@ -202,7 +202,7 @@ addMore10: Int => Int = $$Lambda$1077/1144251618@1bdaa13c scala> addMore10(9) res9: Int = 19 -// 重新声明more变量 +// 重新声明 more 变量 scala> var more = 100 more: Int = 100 @@ -210,20 +210,20 @@ more: Int = 100 scala> val addMore100 = (x: Int) => {x + more} addMore100: Int => Int = $$Lambda$1078/626955849@4d0be2ac -// 引用的是重新声明more变量 +// 引用的是重新声明 more 变量 scala> addMore100(9) res10: Int = 109 -// 引用的还是第一次声明的more变量 +// 引用的还是第一次声明的 more 变量 scala> addMore10(9) res11: Int = 19 -// 对于全局而言more还是100 +// 对于全局而言 more 还是 100 scala> more res12: Int = 100 ``` -从上面的示例可以看出重新声明`more`后,全局的`more`的值是100,但是对于闭包函数`addMore10`还是引用的是值为10的`more`,这是由虚拟机来实现的,虚拟机会保证`more`变量在重新声明后,原来的被捕获的变量副本继续在堆上保持存活。 +从上面的示例可以看出重新声明 `more` 后,全局的 `more` 的值是 100,但是对于闭包函数 `addMore10` 还是引用的是值为 10 的 `more`,这是由虚拟机来实现的,虚拟机会保证 `more` 变量在重新声明后,原来的被捕获的变量副本继续在堆上保持存活。 ## 三、高阶函数 @@ -239,7 +239,7 @@ object ScalaApp extends App { x * x } - // 2.定义高阶函数: 第一个参数是类型为Int => Int的函数 + // 2.定义高阶函数: 第一个参数是类型为 Int => Int 的函数 def multi(fun: Int => Int, x: Int) = { fun(x) * 100 } @@ -265,10 +265,10 @@ object ScalaApp extends App { } ``` -这里当你调用curriedSum时候,实际上是连着做了两次传统的函数调用,实际执行的柯里化过程如下: +这里当你调用 curriedSum 时候,实际上是连着做了两次传统的函数调用,实际执行的柯里化过程如下: -+ 第一次调用接收一个名为`x`的Int型参数,返回一个用于第二次调用的函数,假设`x`为2,则返回函数`2+y`; -+ 返回的函数接收参数`y`,并计算并返回值`2+3`的值。 ++ 第一次调用接收一个名为 `x` 的 Int 型参数,返回一个用于第二次调用的函数,假设 `x` 为 2,则返回函数 `2+y`; ++ 返回的函数接收参数 `y`,并计算并返回值 `2+3` 的值。 想要获得柯里化的中间返回的函数其实也比较简单: @@ -278,7 +278,7 @@ object ScalaApp extends App { def curriedSum(x: Int)(y: Int) = x + y println(curriedSum(2)(3)) //输出 5 - // 获取传入值为10返回的中间函数 10 + y + // 获取传入值为 10 返回的中间函数 10 + y val plus: Int => Int = curriedSum(10)_ println(plus(3)) //输出值 13 } @@ -301,8 +301,8 @@ object ScalaApp extends App { ## 参考资料 -1. Martin Odersky . Scala编程(第3版)[M] . 电子工业出版社 . 2018-1-1 -2. 凯.S.霍斯特曼 . 快学Scala(第2版)[M] . 电子工业出版社 . 2017-7 +1. Martin Odersky . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1 +2. 凯.S.霍斯特曼 . 快学 Scala(第 2 版)[M] . 电子工业出版社 . 2017-7 diff --git a/notes/Scala列表和集.md b/notes/Scala列表和集.md index e33ade7..208d42e 100644 --- a/notes/Scala列表和集.md +++ b/notes/Scala列表和集.md @@ -17,25 +17,25 @@ ## 一、List字面量 -List是Scala中非常重要的一个数据结构,其与Array(数组)非常类似,但是List是不可变的,和Java中的List一样,其底层实现是链表。 +List 是 Scala 中非常重要的一个数据结构,其与 Array(数组) 非常类似,但是 List 是不可变的,和 Java 中的 List 一样,其底层实现是链表。 ```scala scala> val list = List("hadoop", "spark", "storm") list: List[String] = List(hadoop, spark, storm) -// List是不可变 +// List 是不可变 scala> list(1) = "hive" :9: error: value update is not a member of List[String] ``` ## 二、List类型 -Scala中List具有以下两个特性: +Scala 中 List 具有以下两个特性: -+ **同构(homogeneous)**:同一个List中的所有元素都必须是相同的类型; -+ **协变(covariant)**:如果S是T的子类型,那么`List[S]`就是`List[T]`的子类型,例如`List[String]`是`List[Object]`的子类型。 ++ **同构 (homogeneous)**:同一个 List 中的所有元素都必须是相同的类型; ++ **协变 (covariant)**:如果 S 是 T 的子类型,那么 `List[S]` 就是 `List[T]` 的子类型,例如 `List[String]` 是 `List[Object]` 的子类型。 -需要特别说明的是空列表的类型为`List[Nothing]`: +需要特别说明的是空列表的类型为 `List[Nothing]`: ```scala scala> List() @@ -44,7 +44,7 @@ res1: List[Nothing] = List() ## 三、构建List -所有List都由两个基本单元构成:`Nil` 和`::`(读作"cons")。即列表要么是空列表(Nil),要么是由一个head加上一个tail组成,而tail又是一个List。我们在上面使用的`List("hadoop", "spark", "storm")`最终也是被解释为` "hadoop"::"spark":: "storm"::Nil`。 +所有 List 都由两个基本单元构成:`Nil` 和 `::`(读作"cons")。即列表要么是空列表 (Nil),要么是由一个 head 加上一个 tail 组成,而 tail 又是一个 List。我们在上面使用的 `List("hadoop", "spark", "storm")` 最终也是被解释为 ` "hadoop"::"spark":: "storm"::Nil`。 ```scala scala> val list01 = "hadoop"::"spark":: "storm"::Nil @@ -57,7 +57,7 @@ list02: List[String] = List(hadoop, spark, storm) ## 四、模式匹配 -Scala支持展开列表以实现模式匹配。 +Scala 支持展开列表以实现模式匹配。 ```scala scala> val list = List("hadoop", "spark", "storm") @@ -92,16 +92,16 @@ object ScalaApp extends App { // 2.返回列表中的第一个元素 list.head - // 3.返回列表中除第一个元素外的所有元素 这里输出List(spark, storm) + // 3.返回列表中除第一个元素外的所有元素 这里输出 List(spark, storm) list.tail - // 4.tail和head可以结合使用 + // 4.tail 和 head 可以结合使用 list.tail.head - // 5.返回列表中的最后一个元素 与head相反 + // 5.返回列表中的最后一个元素 与 head 相反 list.init - // 6.返回列表中除了最后一个元素之外的其他元素 与tail相反 这里输出List(hadoop, spark) + // 6.返回列表中除了最后一个元素之外的其他元素 与 tail 相反 这里输出 List(hadoop, spark) list.last // 7.使用下标访问元素 @@ -118,7 +118,7 @@ object ScalaApp extends App { ### 5.2 indices -indices方法返回所有下标。 +indices 方法返回所有下标。 ```scala scala> list.indices @@ -127,8 +127,8 @@ res2: scala.collection.immutable.Range = Range(0, 1, 2) ### 5.3 take & drop & splitAt -- **take**:获取前n个元素; -- **drop**:删除前n个元素; +- **take**:获取前 n 个元素; +- **drop**:删除前 n 个元素; - **splitAt**:从第几个位置开始拆分。 ```scala @@ -144,7 +144,7 @@ res5: (List[String], List[String]) = (List(hadoop, spark),List(storm)) ### 5.4 flatten -flatten接收一个由列表组成的列表,并将其进行扁平化操作,返回单个列表。 +flatten 接收一个由列表组成的列表,并将其进行扁平化操作,返回单个列表。 ```scala scala> List(List(1, 2), List(3), List(), List(4, 5)).flatten @@ -153,7 +153,7 @@ res6: List[Int] = List(1, 2, 3, 4, 5) ### 5.5 zip & unzip -对两个List执行`zip`操作结果如下,返回对应位置元素组成的元组的列表,`unzip`则执行反向操作。 +对两个 List 执行 `zip` 操作结果如下,返回对应位置元素组成的元组的列表,`unzip` 则执行反向操作。 ```scala scala> val list = List("hadoop", "spark", "storm") @@ -168,14 +168,14 @@ res7: (List[String], List[Int]) = (List(hadoop, spark, storm),List(10, 20, 30)) ### 5.6 toString & mkString -toString 返回List的字符串表现形式。 +toString 返回 List 的字符串表现形式。 ```scala scala> list.toString res8: String = List(hadoop, spark, storm) ``` -如果想改变List的字符串表现形式,可以使用mkString。mkString有三个重载方法,方法定义如下: +如果想改变 List 的字符串表现形式,可以使用 mkString。mkString 有三个重载方法,方法定义如下: ```scala // start:前缀 sep:分隔符 end:后缀 @@ -220,7 +220,7 @@ object ScalaApp extends App { } ``` -toArray和toList用于List和数组之间的互相转换。 +toArray 和 toList 用于 List 和数组之间的互相转换。 ```scala scala> val array = list.toArray @@ -230,7 +230,7 @@ scala> array.toList res13: List[String] = List(hadoop, spark, storm) ``` -copyToArray将List中的元素拷贝到数组中指定位置。 +copyToArray 将 List 中的元素拷贝到数组中指定位置。 ```scala object ScalaApp extends App { @@ -250,14 +250,14 @@ object ScalaApp extends App { ### 6.1 列表转换:map & flatMap & foreach -map 与 Java 8 函数式编程中的map类似,都是对List中每一个元素执行指定操作。 +map 与 Java 8 函数式编程中的 map 类似,都是对 List 中每一个元素执行指定操作。 ```scala scala> List(1,2,3).map(_+10) res15: List[Int] = List(11, 12, 13) ``` -flatMap 与 map 类似,但如果List中的元素还是List,则会对其进行flatten操作。 +flatMap 与 map 类似,但如果 List 中的元素还是 List,则会对其进行 flatten 操作。 ```scala scala> list.map(_.toList) @@ -267,7 +267,7 @@ scala> list.flatMap(_.toList) res17: List[Char] = List(h, a, d, o, o, p, s, p, a, r, k, s, t, o, r, m) ``` -foreach 要求右侧的操作是一个返回值为Unit的函数,你也可以简单理解为执行一段没有返回值代码。 +foreach 要求右侧的操作是一个返回值为 Unit 的函数,你也可以简单理解为执行一段没有返回值代码。 ```scala scala> var sum = 0 @@ -281,21 +281,21 @@ res19: Int = 15 ### 6.2 列表过滤:filter & partition & find & takeWhile & dropWhile & span -filter用于筛选满足条件元素,返回新的List。 +filter 用于筛选满足条件元素,返回新的 List。 ```scala scala> List(1, 2, 3, 4, 5) filter (_ % 2 == 0) res20: List[Int] = List(2, 4) ``` -partition会按照筛选条件对元素进行分组,返回类型是tuple(元组)。 +partition 会按照筛选条件对元素进行分组,返回类型是 tuple(元组)。 ```scala scala> List(1, 2, 3, 4, 5) partition (_ % 2 == 0) res21: (List[Int], List[Int]) = (List(2, 4),List(1, 3, 5)) ``` -find查找第一个满足条件的值,由于可能并不存在这样的值,所以返回类型是`Option`,可以通过`getOrElse`在不存在满足条件值的情况下返回默认值。 +find 查找第一个满足条件的值,由于可能并不存在这样的值,所以返回类型是 `Option`,可以通过 `getOrElse` 在不存在满足条件值的情况下返回默认值。 ```scala scala> List(1, 2, 3, 4, 5) find (_ % 2 == 0) @@ -305,14 +305,14 @@ val result: Option[Int] = List(1, 2, 3, 4, 5) find (_ % 2 == 0) result.getOrElse(10) ``` -takeWhile遍历元素,直到遇到第一个不符合条件的值则结束遍历,返回所有遍历到的值。 +takeWhile 遍历元素,直到遇到第一个不符合条件的值则结束遍历,返回所有遍历到的值。 ```scala scala> List(1, 2, 3, -4, 5) takeWhile (_ > 0) res23: List[Int] = List(1, 2, 3) ``` -dropWhile遍历元素,直到遇到第一个不符合条件的值则结束遍历,返回所有未遍历到的值。 +dropWhile 遍历元素,直到遇到第一个不符合条件的值则结束遍历,返回所有未遍历到的值。 ```scala // 第一个值就不满足条件,所以返回列表中所有的值 @@ -324,7 +324,7 @@ scala> List(1, 2, 3, -4, 5) dropWhile (_ < 3) res26: List[Int] = List(3, -4, 5) ``` -span遍历元素,直到遇到第一个不符合条件的值则结束遍历,将遍历到的值和未遍历到的值分别放入两个List中返回,返回类型是tuple(元组)。 +span 遍历元素,直到遇到第一个不符合条件的值则结束遍历,将遍历到的值和未遍历到的值分别放入两个 List 中返回,返回类型是 tuple(元组)。 ```scala scala> List(1, 2, 3, -4, 5) span (_ > 0) @@ -335,14 +335,14 @@ res27: (List[Int], List[Int]) = (List(1, 2, 3),List(-4, 5)) ### 6.3 列表检查:forall & exists -forall检查List中所有元素,如果所有元素都满足条件,则返回true。 +forall 检查 List 中所有元素,如果所有元素都满足条件,则返回 true。 ```scala scala> List(1, 2, 3, -4, 5) forall ( _ > 0 ) res28: Boolean = false ``` -exists检查List中的元素,如果某个元素已经满足条件,则返回true。 +exists 检查 List 中的元素,如果某个元素已经满足条件,则返回 true。 ```scala scala> List(1, 2, 3, -4, 5) exists (_ > 0 ) @@ -353,7 +353,7 @@ res29: Boolean = true ### 6.4 列表排序:sortWith -sortWith对List中所有元素按照指定规则进行排序,由于List是不可变的,所以排序返回一个新的List。 +sortWith 对 List 中所有元素按照指定规则进行排序,由于 List 是不可变的,所以排序返回一个新的 List。 ```scala scala> List(1, -3, 4, 2, 6) sortWith (_ < _) @@ -370,11 +370,11 @@ res33: List[String] = List(azkaban, hadoop, spark, hive) ## 七、List对象的方法 -上面介绍的所有方法都是List类上的方法,下面介绍的是List伴生对象中的方法。 +上面介绍的所有方法都是 List 类上的方法,下面介绍的是 List 伴生对象中的方法。 ### 7.1 List.range -List.range可以产生指定的前闭后开区间内的值组成的List,它有三个可选参数: start(开始值),end(结束值,不包含),step(步长)。 +List.range 可以产生指定的前闭后开区间内的值组成的 List,它有三个可选参数: start(开始值),end(结束值,不包含),step(步长)。 ```scala scala> List.range(1, 5) @@ -389,7 +389,7 @@ res36: List[Int] = List(9, 6, 3) ### 7.2 List.fill -List.fill使用指定值填充List。 +List.fill 使用指定值填充 List。 ```scala scala> List.fill(3)("hello") @@ -401,7 +401,7 @@ res38: List[List[String]] = List(List(world, world, world), List(world, world, w ### 7.3 List.concat -List.concat用于拼接多个List。 +List.concat 用于拼接多个 List。 ```scala scala> List.concat(List('a', 'b'), List('c')) @@ -418,18 +418,18 @@ res41: List[Nothing] = List() ## 八、处理多个List -当多个List被放入同一个tuple中时候,可以通过zipped对多个List进行关联处理。 +当多个 List 被放入同一个 tuple 中时候,可以通过 zipped 对多个 List 进行关联处理。 ```scala -// 两个List对应位置的元素相乘 +// 两个 List 对应位置的元素相乘 scala> (List(10, 20), List(3, 4, 5)).zipped.map(_ * _) res42: List[Int] = List(30, 80) -// 三个List的操作也是一样的 +// 三个 List 的操作也是一样的 scala> (List(10, 20), List(3, 4, 5), List(100, 200)).zipped.map(_ * _ + _) res43: List[Int] = List(130, 280) -// 判断第一个List中元素的长度与第二个List中元素的值是否相等 +// 判断第一个 List 中元素的长度与第二个 List 中元素的值是否相等 scala> (List("abc", "de"), List(3, 2)).zipped.forall(_.length == _) res44: Boolean = true ``` @@ -438,7 +438,7 @@ res44: Boolean = true ## 九、缓冲列表ListBuffer -上面介绍的List,由于其底层实现是链表,这意味着能快速访问List头部元素,但对尾部元素的访问则比较低效,这时候可以采用`ListBuffer`,ListBuffer提供了在常量时间内往头部和尾部追加元素。 +上面介绍的 List,由于其底层实现是链表,这意味着能快速访问 List 头部元素,但对尾部元素的访问则比较低效,这时候可以采用 `ListBuffer`,ListBuffer 提供了在常量时间内往头部和尾部追加元素。 ```scala import scala.collection.mutable.ListBuffer @@ -451,7 +451,7 @@ object ScalaApp extends App { buffer += 2 // 2.在头部追加元素 3 +=: buffer - // 3. ListBuffer转List + // 3. ListBuffer 转 List val list: List[Int] = buffer.toList println(list) } @@ -463,14 +463,14 @@ object ScalaApp extends App { ## 十、集(Set) -Set是不重复元素的集合。分为可变Set和不可变Set。 +Set 是不重复元素的集合。分为可变 Set 和不可变 Set。 ### 10.1 可变Set ```scala object ScalaApp extends App { - // 可变Set + // 可变 Set val mutableSet = new collection.mutable.HashSet[Int] // 1.添加元素 @@ -483,13 +483,13 @@ object ScalaApp extends App { // 2.移除元素 mutableSet.remove(2) - // 3.调用mkString方法 输出1,3,4 + // 3.调用 mkString 方法 输出 1,3,4 println(mutableSet.mkString(",")) - // 4. 获取Set中最小元素 + // 4. 获取 Set 中最小元素 println(mutableSet.min) - // 5. 获取Set中最大元素 + // 5. 获取 Set 中最大元素 println(mutableSet.max) } @@ -497,12 +497,12 @@ object ScalaApp extends App { ### 10.2 不可变Set -不可变Set没有add方法,可以使用`+`添加元素,但是此时会返回一个新的不可变Set,原来的Set不变。 +不可变 Set 没有 add 方法,可以使用 `+` 添加元素,但是此时会返回一个新的不可变 Set,原来的 Set 不变。 ```scala object ScalaApp extends App { - // 不可变Set + // 不可变 Set val immutableSet = new collection.immutable.HashSet[Int] val ints: HashSet[Int] = immutableSet+1 @@ -516,19 +516,19 @@ object ScalaApp extends App { ### 10.3 Set间操作 -多个Set之间可以进行求交集或者合集等操作。 +多个 Set 之间可以进行求交集或者合集等操作。 ```scala object ScalaApp extends App { - // 声明有序Set + // 声明有序 Set val mutableSet = collection.mutable.SortedSet(1, 2, 3, 4, 5) val immutableSet = collection.immutable.SortedSet(3, 4, 5, 6, 7) - // 两个Set的合集 输出:TreeSet(1, 2, 3, 4, 5, 6, 7) + // 两个 Set 的合集 输出:TreeSet(1, 2, 3, 4, 5, 6, 7) println(mutableSet ++ immutableSet) - // 两个Set的交集 输出:TreeSet(3, 4, 5) + // 两个 Set 的交集 输出:TreeSet(3, 4, 5) println(mutableSet intersect immutableSet) } @@ -538,5 +538,5 @@ object ScalaApp extends App { ## 参考资料 -1. Martin Odersky . Scala编程(第3版)[M] . 电子工业出版社 . 2018-1-1 -2. 凯.S.霍斯特曼 . 快学Scala(第2版)[M] . 电子工业出版社 . 2017-7 +1. Martin Odersky . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1 +2. 凯.S.霍斯特曼 . 快学 Scala(第 2 版)[M] . 电子工业出版社 . 2017-7 diff --git a/notes/Scala基本数据类型和运算符.md b/notes/Scala基本数据类型和运算符.md index 48105ae..6f96ca1 100644 --- a/notes/Scala基本数据类型和运算符.md +++ b/notes/Scala基本数据类型和运算符.md @@ -10,31 +10,31 @@ ### 1.1 类型支持 -Scala 拥有下表所示的数据类型,其中Byte、Short、Int、Long和Char类型统称为整数类型,整数类型加上Float和Double统称为数值类型。Scala数值类型的取值范围和Java对应类型的取值范围相同。 +Scala 拥有下表所示的数据类型,其中 Byte、Short、Int、Long 和 Char 类型统称为整数类型,整数类型加上 Float 和 Double 统称为数值类型。Scala 数值类型的取值范围和 Java 对应类型的取值范围相同。 | 数据类型 | 描述 | | -------- | ------------------------------------------------------------ | -| Byte | 8位有符号补码整数。数值区间为 -128 到 127 | -| Short | 16位有符号补码整数。数值区间为 -32768 到 32767 | -| Int | 32位有符号补码整数。数值区间为 -2147483648 到 2147483647 | -| Long | 64位有符号补码整数。数值区间为 -9223372036854775808 到 9223372036854775807 | +| Byte | 8 位有符号补码整数。数值区间为 -128 到 127 | +| Short | 16 位有符号补码整数。数值区间为 -32768 到 32767 | +| Int | 32 位有符号补码整数。数值区间为 -2147483648 到 2147483647 | +| Long | 64 位有符号补码整数。数值区间为 -9223372036854775808 到 9223372036854775807 | | Float | 32 位, IEEE 754 标准的单精度浮点数 | | Double | 64 位 IEEE 754 标准的双精度浮点数 | -| Char | 16位无符号Unicode字符, 区间值为 U+0000 到 U+FFFF | +| Char | 16 位无符号 Unicode 字符, 区间值为 U+0000 到 U+FFFF | | String | 字符序列 | -| Boolean | true或false | -| Unit | 表示无值,等同于Java中的void。用作不返回任何结果的方法的结果类型。Unit只有一个实例值,写成()。 | +| Boolean | true 或 false | +| Unit | 表示无值,等同于 Java 中的 void。用作不返回任何结果的方法的结果类型。Unit 只有一个实例值,写成 ()。 | | Null | null 或空引用 | -| Nothing | Nothing类型在Scala的类层级的最低端;它是任何其他类型的子类型。 | -| Any | Any是所有其他类的超类 | -| AnyRef | AnyRef类是Scala里所有引用类(reference class)的基类 | +| Nothing | Nothing 类型在 Scala 的类层级的最低端;它是任何其他类型的子类型。 | +| Any | Any 是所有其他类的超类 | +| AnyRef | AnyRef 类是 Scala 里所有引用类 (reference class) 的基类 | ### 1.2 定义变量 -Scala的变量分为两种,val和var,其区别如下: +Scala 的变量分为两种,val 和 var,其区别如下: -+ **val** : 类似于Java中的final变量,一旦初始化就不能被重新赋值; -+ **var** :类似于Java中的非final变量,在整个声明周期内var可以被重新赋值; ++ **val** : 类似于 Java 中的 final 变量,一旦初始化就不能被重新赋值; ++ **var** :类似于 Java 中的非 final 变量,在整个声明周期内 var 可以被重新赋值; ```scala scala> val a=1 @@ -52,7 +52,7 @@ b: Int = 2 ### 1.3 类型推断 -在上面的演示中,并没有声明a是Int类型,但是程序还是把a当做Int类型,这就是Scala的类型推断。在大多数情况下,你都无需指明变量的类型,程序会自动进行推断。如果你想显式的声明类型,可以在变量后面指定,如下: +在上面的演示中,并没有声明 a 是 Int 类型,但是程序还是把 a 当做 Int 类型,这就是 Scala 的类型推断。在大多数情况下,你都无需指明变量的类型,程序会自动进行推断。如果你想显式的声明类型,可以在变量后面指定,如下: ```scala scala> val c:String="hello scala" @@ -61,7 +61,7 @@ c: String = hello scala ### 1.4 Scala解释器 -在scala命令行中,如果没有对输入的值指定赋值的变量,则输入的值默认会赋值给`resX`(其中X是一个从0开始递增的整数),`res`是result的缩写,这个变量可以在后面的语句中进行引用。 +在 scala 命令行中,如果没有对输入的值指定赋值的变量,则输入的值默认会赋值给 `resX`(其中 X 是一个从 0 开始递增的整数),`res` 是 result 的缩写,这个变量可以在后面的语句中进行引用。 ```scala scala> 5 @@ -78,7 +78,7 @@ scala> println(res1) ## 二、字面量 -Scala和Java字面量在使用上很多相似,比如都使用F或f表示浮点型,都使用L或l表示Long类型。下文主要介绍两者差异部分。 +Scala 和 Java 字面量在使用上很多相似,比如都使用 F 或 f 表示浮点型,都使用 L 或 l 表示 Long 类型。下文主要介绍两者差异部分。 ```scala scala> 1.2 @@ -102,7 +102,7 @@ res5: Long = 1 ### 2.1 整数字面量 -Scala支持10进制和16进制,但不支持八进制字面量和以0开头的整数字面量。 +Scala 支持 10 进制和 16 进制,但不支持八进制字面量和以 0 开头的整数字面量。 ```scala scala> 012 @@ -113,7 +113,7 @@ scala> 012 #### 1. 字符字面量 -字符字面量由一对单引号和中间的任意Unicode字符组成。你可以显式的给出原字符、也可以使用字符的Unicode码来表示,还可以包含特殊的转义字符。 +字符字面量由一对单引号和中间的任意 Unicode 字符组成。你可以显式的给出原字符、也可以使用字符的 Unicode 码来表示,还可以包含特殊的转义字符。 ```scala scala> '\u0041' @@ -137,7 +137,7 @@ res3: String = hello world #### 3.原生字符串 -Scala提供了`""" ... """`语法,通过三个双引号来表示原生字符串和多行字符串,使用该种方式,原生字符串中的特殊字符不会被转义。 +Scala 提供了 `""" ... """` 语法,通过三个双引号来表示原生字符串和多行字符串,使用该种方式,原生字符串中的特殊字符不会被转义。 ```scala scala> "hello \tool" @@ -155,9 +155,9 @@ world ### 2.3 符号字面量 -符号字面量写法为: `'标识符` ,这里 标识符可以是任何字母或数字的组合。符号字面量会被映射成`scala.Symbol`的实例,如:符号字面量 `'x `会被编译器翻译为`scala.Symbol("x")`。符号字面量可选方法很少,只能通过`.name`获取其名称。 +符号字面量写法为: `'标识符 ` ,这里 标识符可以是任何字母或数字的组合。符号字面量会被映射成 `scala.Symbol` 的实例,如:符号字面量 `'x ` 会被编译器翻译为 `scala.Symbol("x")`。符号字面量可选方法很少,只能通过 `.name` 获取其名称。 -注意:具有相同`name`的符号字面量一定指向同一个Symbol对象,不同`name`的符号字面量一定指向不同的Symbol对象。 +注意:具有相同 `name` 的符号字面量一定指向同一个 Symbol 对象,不同 `name` 的符号字面量一定指向不同的 Symbol 对象。 ```scala scala> val sym = 'ID008 @@ -169,7 +169,7 @@ res12: String = ID008 ### 2.4 插值表达式 -Scala支持插值表达式。 +Scala 支持插值表达式。 ```scala scala> val name="xiaoming" @@ -181,19 +181,19 @@ My name is xiaoming,I'm 18. ## 三、运算符 -Scala和其他语言一样,支持大多数的操作运算符: +Scala 和其他语言一样,支持大多数的操作运算符: - 算术运算符(+,-,*,/,%) - 关系运算符(==,!=,>,<,>=,<=) -- 逻辑运算符(&&,||,!,&,|) -- 位运算符(~,&,|,^,<<,>>,>>>) -- 赋值运算符(=,+=,-=,*=,/=,%=,<<=,>>=,&=,^=,|=) +- 逻辑运算符 (&&,||,!,&,|) +- 位运算符 (~,&,|,^,<<,>>,>>>) +- 赋值运算符 (=,+=,-=,*=,/=,%=,<<=,>>=,&=,^=,|=) -以上操作符的基本使用与Java类似,下文主要介绍差异部分和注意事项。 +以上操作符的基本使用与 Java 类似,下文主要介绍差异部分和注意事项。 ### 3.1 运算符即方法 -Scala的面向对象比Java更加纯粹,在Scala中一切都是对象。所以对于`1+2`,实际上是调用了Int类中名为`+`的方法,所以1+2,也可以写成`1.+(2)`。 +Scala 的面向对象比 Java 更加纯粹,在 Scala 中一切都是对象。所以对于 `1+2`,实际上是调用了 Int 类中名为 `+` 的方法,所以 1+2,也可以写成 `1.+(2)`。 ```scala scala> 1+2 @@ -203,17 +203,17 @@ scala> 1.+(2) res15: Int = 3 ``` -Int类中包含了多个重载的`+`方法,用于分别接收不同类型的参数。 +Int 类中包含了多个重载的 `+` 方法,用于分别接收不同类型的参数。
### 3.2 逻辑运算符 -和其他语言一样,在Scala中`&&`,`||`的执行是短路的,即如果左边的表达式能确定整个结果,右边的表达式就不会被执行,这满足大多数使用场景。但是如果你需要在无论什么情况下,都执行右边的表达式,则可以使用`&`或`|`代替。 +和其他语言一样,在 Scala 中 `&&`,`||` 的执行是短路的,即如果左边的表达式能确定整个结果,右边的表达式就不会被执行,这满足大多数使用场景。但是如果你需要在无论什么情况下,都执行右边的表达式,则可以使用 `&` 或 `|` 代替。 ### 3.3 赋值运算符 -在Scala中没有Java中的`++`和`--`运算符,如果你想要实现类似的操作,只能使用`+=1`,或者`-=1`。 +在 Scala 中没有 Java 中的 `++` 和 `--` 运算符,如果你想要实现类似的操作,只能使用 `+=1`,或者 `-=1`。 ```scala scala> var a=1 @@ -236,7 +236,7 @@ res10: Int = 1
-在表格中某个字符的优先级越高,那么以这个字符打头的方法就拥有更高的优先级。如`+`的优先级大于`<`,也就意味则`+`的优先级大于以`<`开头的`<<`,所以`2<<2+2` , 实际上等价于`2<<(2+2)` : +在表格中某个字符的优先级越高,那么以这个字符打头的方法就拥有更高的优先级。如 `+` 的优先级大于 `<`,也就意味则 `+` 的优先级大于以 `<` 开头的 `<<`,所以 `2<<2+2` , 实际上等价于 `2<<(2+2)` : ```scala scala> 2<<2+2 @@ -248,7 +248,7 @@ res1: Int = 32 ### 3.5 对象相等性 -如果想要判断两个对象是否相等,可以使用`==`和`!=`,这两个操作符可以用于所有的对象,包括null。 +如果想要判断两个对象是否相等,可以使用 `==` 和 `!=`,这两个操作符可以用于所有的对象,包括 null。 ```scala scala> 1==2 @@ -271,4 +271,4 @@ res6: Boolean = true ## 参考资料 -1. Martin Odersky . Scala编程(第3版)[M] . 电子工业出版社 . 2018-1-1 +1. Martin Odersky . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1 diff --git a/notes/Scala数组.md b/notes/Scala数组.md index 18f0fc4..d7215a8 100644 --- a/notes/Scala数组.md +++ b/notes/Scala数组.md @@ -11,59 +11,59 @@ ## 一、定长数组 -在Scala中,如果你需要一个长度不变的数组,可以使用Array。但需要注意以下两点: +在 Scala 中,如果你需要一个长度不变的数组,可以使用 Array。但需要注意以下两点: -- 在Scala中使用`(index)`而不是`[index]`来访问数组中的元素,因为访问元素,对于Scala来说是方法调用,`(index)`相当于执行了`.apply(index)`方法。 -- Scala中的数组与Java中的是等价的,`Array[Int]()`在虚拟机层面就等价于Java的`int[]`。 +- 在 Scala 中使用 `(index)` 而不是 `[index]` 来访问数组中的元素,因为访问元素,对于 Scala 来说是方法调用,`(index)` 相当于执行了 `.apply(index)` 方法。 +- Scala 中的数组与 Java 中的是等价的,`Array[Int]()` 在虚拟机层面就等价于 Java 的 `int[]`。 ```scala -// 10个整数的数组,所有元素初始化为0 +// 10 个整数的数组,所有元素初始化为 0 scala> val nums=new Array[Int](10) nums: Array[Int] = Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0) -// 10个元素的字符串数组,所有元素初始化为null +// 10 个元素的字符串数组,所有元素初始化为 null scala> val strings=new Array[String](10) strings: Array[String] = Array(null, null, null, null, null, null, null, null, null, null) -// 使用指定值初始化,此时不需要new关键字 +// 使用指定值初始化,此时不需要 new 关键字 scala> val a=Array("hello","scala") a: Array[String] = Array(hello, scala) -// 使用()来访问元素 +// 使用 () 来访问元素 scala> a(0) res3: String = hello ``` ## 二、变长数组 -在scala中通过ArrayBuffer实现变长数组(又称缓冲数组)。在构建ArrayBuffer时必须给出类型参数,但不必指定长度,因为ArrayBuffer会在需要的时候自动扩容和缩容。变长数组的构建方式及常用操作如下: +在 scala 中通过 ArrayBuffer 实现变长数组 (又称缓冲数组)。在构建 ArrayBuffer 时必须给出类型参数,但不必指定长度,因为 ArrayBuffer 会在需要的时候自动扩容和缩容。变长数组的构建方式及常用操作如下: ```java import scala.collection.mutable.ArrayBuffer object ScalaApp { - // 相当于Java中的main方法 + // 相当于 Java 中的 main 方法 def main(args: Array[String]): Unit = { - // 1.声明变长数组(缓冲数组) + // 1.声明变长数组 (缓冲数组) val ab = new ArrayBuffer[Int]() // 2.在末端增加元素 ab += 1 // 3.在末端添加多个元素 ab += (2, 3, 4) - // 4.可以使用++=追加任何集合 + // 4.可以使用 ++=追加任何集合 ab ++= Array(5, 6, 7) // 5.缓冲数组可以直接打印查看 println(ab) // 6.移除最后三个元素 ab.trimEnd(3) - // 7.在第1个元素之后插入多个新元素 + // 7.在第 1 个元素之后插入多个新元素 ab.insert(1, 8, 9) - // 8.从第2个元素开始,移除3个元素,不指定第二个参数的话,默认值为1 + // 8.从第 2 个元素开始,移除 3 个元素,不指定第二个参数的话,默认值为 1 ab.remove(2, 3) // 9.缓冲数组转定长数组 val abToA = ab.toArray - // 10. 定长数组打印为其hashcode值 + // 10. 定长数组打印为其 hashcode 值 println(abToA) // 11. 定长数组转缓冲数组 val aToAb = abToA.toBuffer @@ -71,7 +71,7 @@ object ScalaApp { } ``` -需要注意的是:使用`+= `在末尾插入元素是一个高效的操作,其时间复杂度是O(1)。而使用`insert`随机插入元素的时间复杂度是O(n),因为在其插入位置之后的所有元素都要进行对应的后移,所以在`ArrayBuffer`中随机插入元素是一个低效的操作。 +需要注意的是:使用 `+= ` 在末尾插入元素是一个高效的操作,其时间复杂度是 O(1)。而使用 `insert` 随机插入元素的时间复杂度是 O(n),因为在其插入位置之后的所有元素都要进行对应的后移,所以在 `ArrayBuffer` 中随机插入元素是一个低效的操作。 ## 三、数组遍历 @@ -80,7 +80,7 @@ object ScalaApp extends App { val a = Array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) - // 1.方式一 相当于Java中的增强for循环 + // 1.方式一 相当于 Java 中的增强 for 循环 for (elem <- a) { print(elem) } @@ -103,26 +103,26 @@ object ScalaApp extends App { } ``` -这里我们没有将代码写在main方法中,而是继承自App.scala,这是Scala提供的一种简写方式,此时将代码写在类中,等价于写在main方法中,直接运行该类即可。 +这里我们没有将代码写在 main 方法中,而是继承自 App.scala,这是 Scala 提供的一种简写方式,此时将代码写在类中,等价于写在 main 方法中,直接运行该类即可。 ## 四、数组转换 -数组转换是指由现有数组产生新的数组。假设当前拥有a数组,想把a中的偶数元素乘以10后产生一个新的数组,可以采用下面两种方式来实现: +数组转换是指由现有数组产生新的数组。假设当前拥有 a 数组,想把 a 中的偶数元素乘以 10 后产生一个新的数组,可以采用下面两种方式来实现: ```scala object ScalaApp extends App { val a = Array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) - // 1.方式一 yield关键字 + // 1.方式一 yield 关键字 val ints1 = for (elem <- a if elem % 2 == 0) yield 10 * elem for (elem <- ints1) { println(elem) } - // 2.方式二 采用函数式编程的方式,这和Java 8中的函数式编程是类似的,这里采用下划线标表示其中的每个元素 + // 2.方式二 采用函数式编程的方式,这和 Java 8 中的函数式编程是类似的,这里采用下划线标表示其中的每个元素 val ints2 = a.filter(_ % 2 == 0).map(_ * 10) for (elem <- ints1) { println(elem) @@ -134,7 +134,7 @@ object ScalaApp extends App { ## 五、多维数组 -和Java中一样,多维数组由单维数组组成。 +和 Java 中一样,多维数组由单维数组组成。 ```scala object ScalaApp extends App { @@ -164,7 +164,7 @@ object ScalaApp extends App { ## 六、与Java互操作 -由于Scala的数组是使用Java的数组来实现的,所以两者之间可以相互转换。 +由于 Scala 的数组是使用 Java 的数组来实现的,所以两者之间可以相互转换。 ```scala import java.util @@ -175,9 +175,9 @@ import scala.collection.{JavaConverters, mutable} object ScalaApp extends App { val element = ArrayBuffer("hadoop", "spark", "storm") - // Scala转Java + // Scala 转 Java val javaList: util.List[String] = JavaConverters.bufferAsJavaList(element) - // Java转Scala + // Java 转 Scala val scalaBuffer: mutable.Buffer[String] = JavaConverters.asScalaBuffer(javaList) for (elem <- scalaBuffer) { println(elem) @@ -189,5 +189,5 @@ object ScalaApp extends App { ## 参考资料 -1. Martin Odersky . Scala编程(第3版)[M] . 电子工业出版社 . 2018-1-1 -2. 凯.S.霍斯特曼 . 快学Scala(第2版)[M] . 电子工业出版社 . 2017-7 \ No newline at end of file +1. Martin Odersky . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1 +2. 凯.S.霍斯特曼 . 快学 Scala(第 2 版)[M] . 电子工业出版社 . 2017-7 diff --git a/notes/Scala映射和元组.md b/notes/Scala映射和元组.md index 2ad5666..ec47e12 100644 --- a/notes/Scala映射和元组.md +++ b/notes/Scala映射和元组.md @@ -22,17 +22,17 @@ ### 1.1 构造Map ```scala -// 初始化一个空map +// 初始化一个空 map val scores01 = new HashMap[String, Int] -// 从指定的值初始化Map(方式一) +// 从指定的值初始化 Map(方式一) val scores02 = Map("hadoop" -> 10, "spark" -> 20, "storm" -> 30) -// 从指定的值初始化Map(方式二) +// 从指定的值初始化 Map(方式二) val scores03 = Map(("hadoop", 10), ("spark", 20), ("storm", 30)) ``` -采用上面方式得到的都是不可变Map(immutable map),想要得到可变Map(mutable map),则需要使用: +采用上面方式得到的都是不可变 Map(immutable map),想要得到可变 Map(mutable map),则需要使用: ```scala val scores04 = scala.collection.mutable.Map("hadoop" -> 10, "spark" -> 20, "storm" -> 30) @@ -45,7 +45,7 @@ object ScalaApp extends App { val scores = Map("hadoop" -> 10, "spark" -> 20, "storm" -> 30) - // 1.获取指定key对应的值 + // 1.获取指定 key 对应的值 println(scores("hadoop")) // 2. 如果对应的值不存在则使用默认值 @@ -55,17 +55,17 @@ object ScalaApp extends App { ### 1.3 新增/修改/删除值 -可变Map允许进行新增、修改、删除等操作。 +可变 Map 允许进行新增、修改、删除等操作。 ```scala object ScalaApp extends App { val scores = scala.collection.mutable.Map("hadoop" -> 10, "spark" -> 20, "storm" -> 30) - // 1.如果key存在则更新 + // 1.如果 key 存在则更新 scores("hadoop") = 100 - // 2.如果key不存在则新增 + // 2.如果 key 不存在则新增 scores("flink") = 40 // 3.可以通过 += 来进行多个更新或新增操作 @@ -84,7 +84,7 @@ object ScalaApp extends App { (hive,50) ``` -不可变Map不允许进行新增、修改、删除等操作,但是允许由不可变Map产生新的Map。 +不可变 Map 不允许进行新增、修改、删除等操作,但是允许由不可变 Map 产生新的 Map。 ```scala object ScalaApp extends App { @@ -125,14 +125,14 @@ object ScalaApp extends App { ### 1.5 yield关键字 -可以使用`yield`关键字从现有Map产生新的Map。 +可以使用 `yield` 关键字从现有 Map 产生新的 Map。 ```scala object ScalaApp extends App { val scores = Map("hadoop" -> 10, "spark" -> 20, "storm" -> 30) - // 1.将scores中所有的值扩大10倍 + // 1.将 scores 中所有的值扩大 10 倍 val newScore = for ((key, value) <- scores) yield (key, value * 10) for (elem <- newScore) { println(elem) } @@ -155,16 +155,16 @@ object ScalaApp extends App { ### 1.6 其他Map结构 -在使用Map时候,如果不指定,默认使用的是HashMap,如果想要使用`TreeMap`或者`LinkedHashMap`,则需要显式的指定。 +在使用 Map 时候,如果不指定,默认使用的是 HashMap,如果想要使用 `TreeMap` 或者 `LinkedHashMap`,则需要显式的指定。 ```scala object ScalaApp extends App { - // 1.使用TreeMap,按照键的字典序进行排序 + // 1.使用 TreeMap,按照键的字典序进行排序 val scores01 = scala.collection.mutable.TreeMap("B" -> 20, "A" -> 10, "C" -> 30) for (elem <- scores01) {println(elem)} - // 2.使用LinkedHashMap,按照键值对的插入顺序进行排序 + // 2.使用 LinkedHashMap,按照键值对的插入顺序进行排序 val scores02 = scala.collection.mutable.LinkedHashMap("B" -> 20, "A" -> 10, "C" -> 30) for (elem <- scores02) {println(elem)} } @@ -192,7 +192,7 @@ object ScalaApp extends App { // 2. 判断是否为空 println(scores.isEmpty) - // 3. 判断是否包含特定的key + // 3. 判断是否包含特定的 key println(scores.contains("A")) } @@ -208,10 +208,10 @@ object ScalaApp extends App { val scores = Map("hadoop" -> 10, "spark" -> 20, "storm" -> 30) - // scala map转java map + // scala map 转 java map val javaMap: util.Map[String, Int] = JavaConverters.mapAsJavaMap(scores) - // java map转scala map + // java map 转 scala map val scalaMap: mutable.Map[String, Int] = JavaConverters.mapAsScalaMap(javaMap) for (elem <- scalaMap) {println(elem)} @@ -255,9 +255,9 @@ object ScalaApp extends App { val array01 = Array("hadoop", "spark", "storm") val array02 = Array(10, 20, 30) - // 1.zip方法得到的是多个tuple组成的数组 + // 1.zip 方法得到的是多个 tuple 组成的数组 val tuples: Array[(String, Int)] = array01.zip(array02) - // 2.也可以在zip后调用toMap方法转换为Map + // 2.也可以在 zip 后调用 toMap 方法转换为 Map val map: Map[String, Int] = array01.zip(array02).toMap for (elem <- tuples) { println(elem) } @@ -278,5 +278,5 @@ object ScalaApp extends App { ## 参考资料 -1. Martin Odersky . Scala编程(第3版)[M] . 电子工业出版社 . 2018-1-1 -2. 凯.S.霍斯特曼 . 快学Scala(第2版)[M] . 电子工业出版社 . 2017-7 \ No newline at end of file +1. Martin Odersky . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1 +2. 凯.S.霍斯特曼 . 快学 Scala(第 2 版)[M] . 电子工业出版社 . 2017-7 diff --git a/notes/Scala模式匹配.md b/notes/Scala模式匹配.md index 1b06f8c..7c65267 100644 --- a/notes/Scala模式匹配.md +++ b/notes/Scala模式匹配.md @@ -13,15 +13,15 @@ ## 一、模式匹配 -Scala支持模式匹配机制,可以代替swith语句、执行类型检查、以及支持析构表达式等。 +Scala 支持模式匹配机制,可以代替 swith 语句、执行类型检查、以及支持析构表达式等。 ### 1.1 更好的swith -Scala不支持swith,可以使用模式匹配`match...case`语法代替。但是match语句与Java中的switch有以下三点不同: +Scala 不支持 swith,可以使用模式匹配 `match...case` 语法代替。但是 match 语句与 Java 中的 switch 有以下三点不同: -- Scala中的case语句支持任何类型;而Java中case语句仅支持整型、枚举和字符串常量; -- Scala中每个分支语句后面不需要写break,因为在case语句中break是隐含的,默认就有; -- 在Scala中match语句是有返回值的,而Java中switch语句是没有返回值的。如下: +- Scala 中的 case 语句支持任何类型;而 Java 中 case 语句仅支持整型、枚举和字符串常量; +- Scala 中每个分支语句后面不需要写 break,因为在 case 语句中 break 是隐含的,默认就有; +- 在 Scala 中 match 语句是有返回值的,而 Java 中 switch 语句是没有返回值的。如下: ```scala object ScalaApp extends App { @@ -66,12 +66,12 @@ object ScalaApp extends App { object ScalaApp extends App { def matchTest(x: Any) = x match { - case (0, _, _) => "匹配第一个元素为0的元组" + case (0, _, _) => "匹配第一个元素为 0 的元组" case (a, b, c) => println(a + "~" + b + "~" + c) case _ => "other" } - println(matchTest((0, 1, 2))) // 输出: 匹配第一个元素为0的元组 + println(matchTest((0, 1, 2))) // 输出: 匹配第一个元素为 0 的元组 matchTest((1, 2, 3)) // 输出: 1~2~3 println(matchTest(Array(10, 11, 12, 14))) // 输出: other } @@ -83,33 +83,33 @@ object ScalaApp extends App { object ScalaApp extends App { def matchTest[T](x: Array[T]) = x match { - case Array(0) => "匹配只有一个元素0的数组" + case Array(0) => "匹配只有一个元素 0 的数组" case Array(a, b) => println(a + "~" + b) - case Array(10, _*) => "第一个元素为10的数组" + case Array(10, _*) => "第一个元素为 10 的数组" case _ => "other" } - println(matchTest(Array(0))) // 输出: 匹配只有一个元素0的数组 + println(matchTest(Array(0))) // 输出: 匹配只有一个元素 0 的数组 matchTest(Array(1, 2)) // 输出: 1~2 - println(matchTest(Array(10, 11, 12))) // 输出: 第一个元素为10的数组 + println(matchTest(Array(10, 11, 12))) // 输出: 第一个元素为 10 的数组 println(matchTest(Array(3, 2, 1))) // 输出: other } ``` ### 1.4 提取器 -数组、列表和元组能使用模式匹配,都是依靠提取器(extractor)机制,它们伴生对象中定义了`unapply`或`unapplySeq`方法: +数组、列表和元组能使用模式匹配,都是依靠提取器 (extractor) 机制,它们伴生对象中定义了 `unapply` 或 `unapplySeq` 方法: + **unapply**:用于提取固定数量的对象; + **unapplySeq**:用于提取一个序列; -这里以数组为例,`Array.scala`定义了`unapplySeq`方法: +这里以数组为例,`Array.scala` 定义了 `unapplySeq` 方法: ```scala def unapplySeq[T](x : scala.Array[T]) : scala.Option[scala.IndexedSeq[T]] = { /* compiled code */ } ``` -`unapplySeq`返回一个序列,包含数组中的所有值,这样在模式匹配时,才能知道对应位置上的值。 +`unapplySeq` 返回一个序列,包含数组中的所有值,这样在模式匹配时,才能知道对应位置上的值。 @@ -117,7 +117,7 @@ def unapplySeq[T](x : scala.Array[T]) : scala.Option[scala.IndexedSeq[T]] = { /* ### 2.1 样例类 -样例类是一种的特殊的类,它们被经过优化以用于模式匹配,样例类的声明比较简单,只需要在`class`前面加上关键字`case`。下面给出一个样例类及其用于模式匹配的示例: +样例类是一种的特殊的类,它们被经过优化以用于模式匹配,样例类的声明比较简单,只需要在 `class` 前面加上关键字 `case`。下面给出一个样例类及其用于模式匹配的示例: ```scala //声明一个抽象类 @@ -125,27 +125,27 @@ abstract class Person{} ``` ```scala -// 样例类Employee +// 样例类 Employee case class Employee(name: String, age: Int, salary: Double) extends Person {} ``` ```scala -// 样例类Student +// 样例类 Student case class Student(name: String, age: Int) extends Person {} ``` 当你声明样例类后,编译器自动进行以下配置: -- 构造器中每个参数都默认为`val`; -- 自动地生成`equals, hashCode, toString, copy`等方法; -- 伴生对象中自动生成`apply`方法,使得可以不用new关键字就能构造出相应的对象; -- 伴生对象中自动生成`unapply`方法,以支持模式匹配。 +- 构造器中每个参数都默认为 `val`; +- 自动地生成 `equals, hashCode, toString, copy` 等方法; +- 伴生对象中自动生成 `apply` 方法,使得可以不用 new 关键字就能构造出相应的对象; +- 伴生对象中自动生成 `unapply` 方法,以支持模式匹配。 除了上面的特征外,样例类和其他类相同,可以任意添加方法和字段,扩展它们。 ### 2.3 用于模式匹配 -样例的伴生对象中自动生成`unapply`方法,所以样例类可以支持模式匹配,使用如下: +样例的伴生对象中自动生成 `unapply` 方法,所以样例类可以支持模式匹配,使用如下: ```scala object ScalaApp extends App { @@ -167,6 +167,6 @@ object ScalaApp extends App { ## 参考资料 -1. Martin Odersky . Scala编程(第3版)[M] . 电子工业出版社 . 2018-1-1 -2. 凯.S.霍斯特曼 . 快学Scala(第2版)[M] . 电子工业出版社 . 2017-7 +1. Martin Odersky . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1 +2. 凯.S.霍斯特曼 . 快学 Scala(第 2 版)[M] . 电子工业出版社 . 2017-7 diff --git a/notes/Scala流程控制语句.md b/notes/Scala流程控制语句.md index a55126f..8b1c219 100644 --- a/notes/Scala流程控制语句.md +++ b/notes/Scala流程控制语句.md @@ -13,7 +13,7 @@ ## 一、条件表达式if -Scala中的if/else语法结构与Java中的一样,唯一不同的是,Scala中的if表达式是有返回值的。 +Scala 中的 if/else 语法结构与 Java 中的一样,唯一不同的是,Scala 中的 if 表达式是有返回值的。 ```scala object ScalaApp extends App { @@ -25,13 +25,13 @@ object ScalaApp extends App { } ``` -在Java中,每行语句都需要使用`;`表示结束,但是在Scala中并不需要。除非你在单行语句中写了多行代码。 +在 Java 中,每行语句都需要使用 `;` 表示结束,但是在 Scala 中并不需要。除非你在单行语句中写了多行代码。 ## 二、块表达式 -在Scala中,可以使用`{}`块包含一系列表达式,块中最后一个表达式的值就是块的值。 +在 Scala 中,可以使用 `{}` 块包含一系列表达式,块中最后一个表达式的值就是块的值。 ```scala object ScalaApp extends App { @@ -45,7 +45,7 @@ object ScalaApp extends App { // 输出: 6 ``` -如果块中的最后一个表达式没有返回值,则块的返回值是Unit类型。 +如果块中的最后一个表达式没有返回值,则块的返回值是 Unit 类型。 ```scala scala> val result ={ val a = 1 + 1; val b = 2 + 2 } @@ -56,7 +56,7 @@ result: Unit = () ## 三、循环表达式while -Scala和大多数语言一样,支持`while`和`do ... while`表达式。 +Scala 和大多数语言一样,支持 `while` 和 `do ... while` 表达式。 ```scala object ScalaApp extends App { @@ -79,7 +79,7 @@ object ScalaApp extends App { ## 四、循环表达式for -for循环的基本使用如下: +for 循环的基本使用如下: ```scala object ScalaApp extends App { @@ -96,7 +96,7 @@ object ScalaApp extends App { } ``` -除了基本使用外,还可以使用`yield`关键字从for循环中产生Vector,这称为for推导式。 +除了基本使用外,还可以使用 `yield` 关键字从 for 循环中产生 Vector,这称为 for 推导式。 ```scala scala> for (i <- 1 to 10) yield i * 6 @@ -107,7 +107,7 @@ res1: scala.collection.immutable.IndexedSeq[Int] = Vector(6, 12, 18, 24, 30, 36, ## 五、异常处理try -和Java中一样,支持`try...catch...finally`语句。 +和 Java 中一样,支持 `try...catch...finally` 语句。 ```scala import java.io.{FileNotFoundException, FileReader} @@ -126,13 +126,13 @@ object ScalaApp extends App { } ``` -这里需要注意的是因为finally语句一定会被执行,所以不要在该语句中返回值,否则返回值会被作为整个try语句的返回值,如下: +这里需要注意的是因为 finally 语句一定会被执行,所以不要在该语句中返回值,否则返回值会被作为整个 try 语句的返回值,如下: ```scala scala> def g():Int = try return 1 finally return 2 g: ()Int -// 方法g()总会返回2 +// 方法 g() 总会返回 2 scala> g() res3: Int = 2 ``` @@ -141,7 +141,7 @@ res3: Int = 2 ## 六、条件选择表达式match -match类似于java中的switch语句。 +match 类似于 java 中的 switch 语句。 ```scala object ScalaApp extends App { @@ -160,11 +160,11 @@ object ScalaApp extends App { ``` -但是与Java中的switch有以下三点不同: +但是与 Java 中的 switch 有以下三点不同: -+ Scala中的case语句支持任何类型;而Java中case语句仅支持整型、枚举和字符串常量; -+ Scala中每个分支语句后面不需要写break,因为在case语句中break是隐含的,默认就有; -+ 在Scala中match语句是有返回值的,而Java中switch语句是没有返回值的。如下: ++ Scala 中的 case 语句支持任何类型;而 Java 中 case 语句仅支持整型、枚举和字符串常量; ++ Scala 中每个分支语句后面不需要写 break,因为在 case 语句中 break 是隐含的,默认就有; ++ 在 Scala 中 match 语句是有返回值的,而 Java 中 switch 语句是没有返回值的。如下: ```scala object ScalaApp extends App { @@ -188,13 +188,13 @@ object ScalaApp extends App { ## 七、没有break和continue -额外注意一下:Scala中并不支持Java中的break和continue关键字。 +额外注意一下:Scala 中并不支持 Java 中的 break 和 continue 关键字。 ## 八、输入与输出 -在Scala中可以使用print、println、printf打印输出,这与Java中是一样的。如果需要从控制台中获取输入,则可以使用`StdIn`中定义的各种方法。 +在 Scala 中可以使用 print、println、printf 打印输出,这与 Java 中是一样的。如果需要从控制台中获取输入,则可以使用 `StdIn` 中定义的各种方法。 ```scala val name = StdIn.readLine("Your name: ") @@ -207,5 +207,5 @@ println(s"Hello, ${name}! Next year, you will be ${age + 1}.") ## 参考资料 -1. Martin Odersky . Scala编程(第3版)[M] . 电子工业出版社 . 2018-1-1 -2. 凯.S.霍斯特曼 . 快学Scala(第2版)[M] . 电子工业出版社 . 2017-7 \ No newline at end of file +1. Martin Odersky . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1 +2. 凯.S.霍斯特曼 . 快学 Scala(第 2 版)[M] . 电子工业出版社 . 2017-7 diff --git a/notes/Scala简介及开发环境配置.md b/notes/Scala简介及开发环境配置.md index d3b4168..f280be1 100644 --- a/notes/Scala简介及开发环境配置.md +++ b/notes/Scala简介及开发环境配置.md @@ -10,7 +10,7 @@ ### 1.1 概念 -Scala全称为Scalable Language,即“可伸缩的语言”,之所以这样命名,是因为它的设计目标是希望伴随着用户的需求一起成长。Scala是一门综合了**面向对象**和**函数式编程概念**的**静态类型**的编程语言,它运行在标准的Java平台上,可以与所有的Java类库无缝协作。 +Scala 全称为 Scalable Language,即“可伸缩的语言”,之所以这样命名,是因为它的设计目标是希望伴随着用户的需求一起成长。Scala 是一门综合了**面向对象**和**函数式编程概念**的**静态类型**的编程语言,它运行在标准的 Java 平台上,可以与所有的 Java 类库无缝协作。 @@ -18,11 +18,11 @@ Scala全称为Scalable Language,即“可伸缩的语言”,之所以这样 #### 1. Scala是面向对象的 -Scala是一种面向对象的语言,每个值都是对象,每个方法都是调用。举例来说,如果你执行`1+2`,则对于Scala而言,实际是在调用Int类里定义的名为`+`的方法。 +Scala 是一种面向对象的语言,每个值都是对象,每个方法都是调用。举例来说,如果你执行 `1+2`,则对于 Scala 而言,实际是在调用 Int 类里定义的名为 `+` 的方法。 #### 2. Scala是函数式的 -Scala不只是一门纯的面对对象的语言,它也是功能完整的函数式编程语言。函数式编程以两大核心理念为指导: +Scala 不只是一门纯的面对对象的语言,它也是功能完整的函数式编程语言。函数式编程以两大核心理念为指导: + 函数是一等公民; + 程序中的操作应该将输入值映射成输出值,而不是当场修改数据。即方法不应该有副作用。 @@ -33,19 +33,19 @@ Scala不只是一门纯的面对对象的语言,它也是功能完整的函数 #### 1. 与Java的兼容 -Scala可以与Java无缝对接,其在执行时会被编译成JVM字节码,这使得其性能与Java相当。Scala可以直接调用Java中的方法、访问Java中的字段、继承Java类、实现Java接口。Scala重度复用并包装了原生的Java类型,并支持隐式转换。 +Scala 可以与 Java 无缝对接,其在执行时会被编译成 JVM 字节码,这使得其性能与 Java 相当。Scala 可以直接调用 Java 中的方法、访问 Java 中的字段、继承 Java 类、实现 Java 接口。Scala 重度复用并包装了原生的 Java 类型,并支持隐式转换。 #### 2. 精简的语法 -Scala的程序通常比较简洁,相比Java而言,代码行数会大大减少,这使得程序员对代码的阅读和理解更快,缺陷也更少。 +Scala 的程序通常比较简洁,相比 Java 而言,代码行数会大大减少,这使得程序员对代码的阅读和理解更快,缺陷也更少。 #### 3. 高级语言的特性 -Scala具有高级语言的特定,对代码进行了高级别的抽象,能够让你更好地控制程序的复杂度,保证开发的效率。 +Scala 具有高级语言的特定,对代码进行了高级别的抽象,能够让你更好地控制程序的复杂度,保证开发的效率。 #### 4. 静态类型 -Scala拥有非常先进的静态类型系统,Scala不仅拥有与Java类似的允许嵌套类的类型系统,还支持使用泛型对类型进行参数化,用交集(intersection)来组合类型,以及使用抽象类型来进行隐藏类型的细节。通过这些特性,可以更快地设计出安全易用的程序和接口。 +Scala 拥有非常先进的静态类型系统,Scala 不仅拥有与 Java 类似的允许嵌套类的类型系统,还支持使用泛型对类型进行参数化,用交集(intersection)来组合类型,以及使用抽象类型来进行隐藏类型的细节。通过这些特性,可以更快地设计出安全易用的程序和接口。 @@ -55,11 +55,11 @@ Scala拥有非常先进的静态类型系统,Scala不仅拥有与Java类似的 ### 2.1 前置条件 -Scala的运行依赖于JDK,Scala 2.12.x需要JDK 1.8+。 +Scala 的运行依赖于 JDK,Scala 2.12.x 需要 JDK 1.8+。 ### 2.2 安装Scala插件 -IDEA默认不支持Scala语言的开发,需要通过插件进行扩展。打开 IDEA,依次点击 **File** => **settings**=> **plugins** 选项卡,搜索Scala插件(如下图)。找到插件后进行安装,并重启IDEA使得安装生效。 +IDEA 默认不支持 Scala 语言的开发,需要通过插件进行扩展。打开 IDEA,依次点击 **File** => **settings**=> **plugins** 选项卡,搜索 Scala 插件 (如下图)。找到插件后进行安装,并重启 IDEA 使得安装生效。
@@ -67,7 +67,7 @@ IDEA默认不支持Scala语言的开发,需要通过插件进行扩展。打 ### 2.3 创建Scala项目 -在IDEA中依次点击 **File** => **New** => **Project** 选项卡,然后选择创建`Scala—IDEA`工程: +在 IDEA 中依次点击 **File** => **New** => **Project** 选项卡,然后选择创建 `Scala—IDEA` 工程:
@@ -77,7 +77,7 @@ IDEA默认不支持Scala语言的开发,需要通过插件进行扩展。打 #### 1. 方式一 -此时看到`Scala SDK`为空,依次点击`Create` => `Download` ,选择所需的版本后,点击`OK`按钮进行下载,下载完成点击`Finish`进入工程。 +此时看到 `Scala SDK` 为空,依次点击 `Create` => `Download` ,选择所需的版本后,点击 `OK` 按钮进行下载,下载完成点击 `Finish` 进入工程。
@@ -85,15 +85,15 @@ IDEA默认不支持Scala语言的开发,需要通过插件进行扩展。打 #### 2. 方式二 -方式一是Scala官方安装指南里使用的方式,但下载速度通常比较慢,且这种安装下并没有直接提供Scala命令行工具。所以个人推荐到官网下载安装包进行安装,下载地址:https://www.scala-lang.org/download/ +方式一是 Scala 官方安装指南里使用的方式,但下载速度通常比较慢,且这种安装下并没有直接提供 Scala 命令行工具。所以个人推荐到官网下载安装包进行安装,下载地址:https://www.scala-lang.org/download/ -这里我的系统是Windows,下载msi版本的安装包后,一直点击下一步进行安装,安装完成后会自动配置好环境变量。 +这里我的系统是 Windows,下载 msi 版本的安装包后,一直点击下一步进行安装,安装完成后会自动配置好环境变量。
-由于安装时已经自动配置好环境变量,所以IDEA会自动选择对应版本的SDK。 +由于安装时已经自动配置好环境变量,所以 IDEA 会自动选择对应版本的 SDK。
@@ -101,7 +101,7 @@ IDEA默认不支持Scala语言的开发,需要通过插件进行扩展。打 ### 2.5 创建Hello World -在工程 `src`目录上右击 **New** => **Scala class** 创建`Hello.scala`。输入代码如下,完成后点击运行按钮,成功运行则代表搭建成功。 +在工程 `src` 目录上右击 **New** => **Scala class** 创建 `Hello.scala`。输入代码如下,完成后点击运行按钮,成功运行则代表搭建成功。
@@ -111,7 +111,7 @@ IDEA默认不支持Scala语言的开发,需要通过插件进行扩展。打 ### 2.6 切换Scala版本 -在日常的开发中,由于对应软件(如Spark)的版本切换,可能导致需要切换Scala的版本,则可以在`Project Structures`中的`Global Libraries`选项卡中进行切换。 +在日常的开发中,由于对应软件(如 Spark)的版本切换,可能导致需要切换 Scala 的版本,则可以在 `Project Structures` 中的 `Global Libraries` 选项卡中进行切换。
@@ -121,7 +121,7 @@ IDEA默认不支持Scala语言的开发,需要通过插件进行扩展。打 ### 2.7 使用scala命令行 -采用`msi`方式安装,程序会自动配置好环境变量。此时可以直接使用命令行工具: +采用 `msi` 方式安装,程序会自动配置好环境变量。此时可以直接使用命令行工具:
@@ -129,5 +129,5 @@ IDEA默认不支持Scala语言的开发,需要通过插件进行扩展。打 ## 参考资料 -1. Martin Odersky(著),高宇翔(译) . Scala编程(第3版)[M] . 电子工业出版社 . 2018-1-1 +1. Martin Odersky(著),高宇翔 (译) . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1 2. https://www.scala-lang.org/download/ diff --git a/notes/Scala类和对象.md b/notes/Scala类和对象.md index d1e3a0d..881ebe2 100644 --- a/notes/Scala类和对象.md +++ b/notes/Scala类和对象.md @@ -17,36 +17,36 @@ ## 一、初识类和对象 -Scala的类与Java的类具有非常多的相似性,示例如下: +Scala 的类与 Java 的类具有非常多的相似性,示例如下: ```scala -// 1. 在scala中,类不需要用public声明,所有的类都具有公共的可见性 +// 1. 在 scala 中,类不需要用 public 声明,所有的类都具有公共的可见性 class Person { - // 2. 声明私有变量,用var修饰的变量默认拥有getter/setter属性 + // 2. 声明私有变量,用 var 修饰的变量默认拥有 getter/setter 属性 private var age = 0 - // 3.如果声明的变量不需要进行初始赋值,此时Scala就无法进行类型推断,所以需要显式指明类型 + // 3.如果声明的变量不需要进行初始赋值,此时 Scala 就无法进行类型推断,所以需要显式指明类型 private var name: String = _ - // 4. 定义方法,应指明传参类型。返回值类型不是必须的,Scala可以自动推断出来,但是为了方便调用者,建议指明 + // 4. 定义方法,应指明传参类型。返回值类型不是必须的,Scala 可以自动推断出来,但是为了方便调用者,建议指明 def growUp(step: Int): Unit = { age += step } - // 5.对于改值器方法(即改变对象状态的方法),即使不需要传入参数,也建议在声明中包含() + // 5.对于改值器方法 (即改变对象状态的方法),即使不需要传入参数,也建议在声明中包含 () def growUpFix(): Unit = { age += 10 } - // 6.对于取值器方法(即不会改变对象状态的方法),不必在声明中包含() + // 6.对于取值器方法 (即不会改变对象状态的方法),不必在声明中包含 () def currentAge: Int = { age } /** - * 7.不建议使用return关键字,默认方法中最后一行代码的计算结果为返回值 + * 7.不建议使用 return 关键字,默认方法中最后一行代码的计算结果为返回值 * 如果方法很简短,甚至可以写在同一行中 */ def getName: String = name @@ -60,11 +60,11 @@ object Person { def main(args: Array[String]): Unit = { // 8.创建类的实例 val counter = new Person() - // 9.用var修饰的变量默认拥有getter/setter属性,可以直接对其进行赋值 + // 9.用 var 修饰的变量默认拥有 getter/setter 属性,可以直接对其进行赋值 counter.age = 12 counter.growUp(8) counter.growUpFix() - // 10.用var修饰的变量默认拥有getter/setter属性,可以直接对其进行取值,输出: 30 + // 10.用 var 修饰的变量默认拥有 getter/setter 属性,可以直接对其进行取值,输出: 30 println(counter.age) // 输出: 30 println(counter.currentAge) @@ -81,17 +81,17 @@ object Person { ### 2.1 成员变量可见性 -Scala中成员变量的可见性默认都是public,如果想要保证其不被外部干扰,可以声明为private,并通过getter和setter方法进行访问。 +Scala 中成员变量的可见性默认都是 public,如果想要保证其不被外部干扰,可以声明为 private,并通过 getter 和 setter 方法进行访问。 ### 2.2 getter和setter属性 -getter和setter属性与声明变量时使用的关键字有关: +getter 和 setter 属性与声明变量时使用的关键字有关: -+ 使用var关键字:变量同时拥有getter和setter属性; -+ 使用val关键字:变量只拥有getter属性; -+ 使用private[this]:变量既没有getter属性、也没有setter属性,只能通过内部的方法访问; ++ 使用 var 关键字:变量同时拥有 getter 和 setter 属性; ++ 使用 val 关键字:变量只拥有 getter 属性; ++ 使用 private[this]:变量既没有 getter 属性、也没有 setter 属性,只能通过内部的方法访问; -需要特别说明的是:假设变量名为age,则其对应的get和set的方法名分别叫做` age`和`age_=`。 +需要特别说明的是:假设变量名为 age,则其对应的 get 和 set 的方法名分别叫做 ` age` 和 `age_=`。 ```scala class Person { @@ -99,7 +99,7 @@ class Person { private val name = "heibaiying" private var age = 12 private[this] var birthday = "2019-08-08" - // birthday只能被内部方法所访问 + // birthday 只能被内部方法所访问 def getBirthday: String = birthday } @@ -116,7 +116,7 @@ object Person { > 解释说明: > -> 示例代码中`person.age=30`在执行时内部实际是调用了方法`person.age_=(30) `,而`person.age`内部执行时实际是调用了`person.age()`方法。想要证明这一点,可以对代码进行反编译。同时为了说明成员变量可见性的问题,我们对下面这段代码进行反编译: +> 示例代码中 `person.age=30` 在执行时内部实际是调用了方法 `person.age_=(30) `,而 `person.age` 内部执行时实际是调用了 `person.age()` 方法。想要证明这一点,可以对代码进行反编译。同时为了说明成员变量可见性的问题,我们对下面这段代码进行反编译: > > ```scala > class Person { @@ -132,7 +132,7 @@ object Person { > > javap -private Person > ``` > -> 编译结果如下,从编译结果可以看到实际的get和set的方法名(因为JVM不允许在方法名中出现=,所以它被翻译成$eq),同时也验证了成员变量默认的可见性为public。 +> 编译结果如下,从编译结果可以看到实际的 get 和 set 的方法名 (因为 JVM 不允许在方法名中出现=,所以它被翻译成$eq),同时也验证了成员变量默认的可见性为 public。 > > ```java > Compiled from "Person.scala" @@ -152,7 +152,7 @@ object Person { ### 2.3 @BeanProperty -在上面的例子中可以看到我们是使用`.`来对成员变量进行访问的,如果想要额外生成和Java中一样的getXXX和setXXX方法,则需要使用@BeanProperty进行注解。 +在上面的例子中可以看到我们是使用 `.` 来对成员变量进行访问的,如果想要额外生成和 Java 中一样的 getXXX 和 setXXX 方法,则需要使用@BeanProperty 进行注解。 ```scala class Person { @@ -172,15 +172,15 @@ object Person { ### 2.4 主构造器 -和Java不同的是,Scala类的主构造器直接写在类名后面,但注意以下两点: +和 Java 不同的是,Scala 类的主构造器直接写在类名后面,但注意以下两点: -+ 主构造器传入的参数默认就是val类型的,即不可变,你没有办法在内部改变传参; -+ 写在主构造器中的代码块会在类初始化的时候被执行,功能类似于Java的静态代码块`static{}` ++ 主构造器传入的参数默认就是 val 类型的,即不可变,你没有办法在内部改变传参; ++ 写在主构造器中的代码块会在类初始化的时候被执行,功能类似于 Java 的静态代码块 `static{}` ```scala class Person(val name: String, val age: Int) { - println("功能类似于Java的静态代码块static{}") + println("功能类似于 Java 的静态代码块 static{}") def getDetail: String = { //name="heibai" 无法通过编译 @@ -196,7 +196,7 @@ object Person { } 输出: -功能类似于Java的静态代码块static{} +功能类似于 Java 的静态代码块 static{} heibaiying:20 ``` @@ -206,7 +206,7 @@ heibaiying:20 辅助构造器有两点硬性要求: -+ 辅助构造器的名称必须为this; ++ 辅助构造器的名称必须为 this; + 每个辅助构造器必须以主构造器或其他的辅助构造器的调用开始。 ```scala @@ -214,14 +214,14 @@ class Person(val name: String, val age: Int) { private var birthday = "" - // 1.辅助构造器的名称必须为this + // 1.辅助构造器的名称必须为 this def this(name: String, age: Int, birthday: String) { // 2.每个辅助构造器必须以主构造器或其他的辅助构造器的调用开始 this(name, age) this.birthday = birthday } - // 3.重写toString方法 + // 3.重写 toString 方法 override def toString: String = name + ":" + age + ":" + birthday } @@ -236,7 +236,7 @@ object Person { ### 2.6 方法传参不可变 -在Scala中,方法传参默认是val类型,即不可变,这意味着你在方法体内部不能改变传入的参数。这和Scala的设计理念有关,Scala遵循函数式编程理念,强调方法不应该有副作用。 +在 Scala 中,方法传参默认是 val 类型,即不可变,这意味着你在方法体内部不能改变传入的参数。这和 Scala 的设计理念有关,Scala 遵循函数式编程理念,强调方法不应该有副作用。 ```scala class Person() { @@ -252,29 +252,29 @@ class Person() { ## 三、对象 -Scala中的object(对象)主要有以下几个作用: +Scala 中的 object(对象) 主要有以下几个作用: -+ 因为object中的变量和方法都是静态的,所以可以用于存放工具类; ++ 因为 object 中的变量和方法都是静态的,所以可以用于存放工具类; + 可以作为单例对象的容器; + 可以作为类的伴生对象; + 可以拓展类或特质; -+ 可以拓展Enumeration来实现枚举。 ++ 可以拓展 Enumeration 来实现枚举。 ### 3.1 工具类&单例&全局静态常量&拓展特质 -这里我们创建一个对象`Utils`,代码如下: +这里我们创建一个对象 `Utils`,代码如下: ```scala object Utils { /* - *1. 相当于Java中的静态代码块static,会在对象初始化时候被执行 + *1. 相当于 Java 中的静态代码块 static,会在对象初始化时候被执行 * 这种方式实现的单例模式是饿汉式单例,即无论你的单例对象是否被用到, * 都在一开始被初始化完成 */ val person = new Person - // 2. 全局固定常量 等价于Java的public static final + // 2. 全局固定常量 等价于 Java 的 public static final val CONSTANT = "固定常量" // 3. 全局静态方法 @@ -284,18 +284,18 @@ object Utils { } ``` -其中Person类代码如下: +其中 Person 类代码如下: ```scala class Person() { - println("Person默认构造器被调用") + println("Person 默认构造器被调用") } ``` 新建测试类: ```scala -// 1.ScalaApp对象扩展自trait App +// 1.ScalaApp 对象扩展自 trait App object ScalaApp extends App { // 2.验证单例 @@ -310,7 +310,7 @@ object ScalaApp extends App { } // 输出如下: -Person默认构造器被调用 +Person 默认构造器被调用 true 固定常量 abcdefg @@ -318,7 +318,7 @@ abcdefg ### 3.2 伴生对象 -在Java中,你通常会用到既有实例方法又有静态方法的类,在Scala中,可以通过类和与类同名的伴生对象来实现。类和伴生对象必须存在与同一个文件中。 +在 Java 中,你通常会用到既有实例方法又有静态方法的类,在 Scala 中,可以通过类和与类同名的伴生对象来实现。类和伴生对象必须存在与同一个文件中。 ```scala class Person() { @@ -353,23 +353,23 @@ object Person { ### 3.3 实现枚举类 -Scala中没有直接提供枚举类,需要通过扩展`Enumeration`,并调用其中的Value方法对所有枚举值进行初始化来实现。 +Scala 中没有直接提供枚举类,需要通过扩展 `Enumeration`,并调用其中的 Value 方法对所有枚举值进行初始化来实现。 ```scala object Color extends Enumeration { - // 1.类型别名,建议声明,在import时有用 + // 1.类型别名,建议声明,在 import 时有用 type Color = Value - // 2.调用Value方法 + // 2.调用 Value 方法 val GREEN = Value - // 3.只传入id + // 3.只传入 id val RED = Value(3) // 4.只传入值 val BULE = Value("blue") - // 5.传入id和值 + // 5.传入 id 和值 val YELLOW = Value(5, "yellow") - // 6. 不传入id时,id为上一个声明变量的id+1,值默认和变量名相同 + // 6. 不传入 id 时,id 为上一个声明变量的 id+1,值默认和变量名相同 val PINK = Value } @@ -407,6 +407,6 @@ true ## 参考资料 -1. Martin Odersky . Scala编程(第3版)[M] . 电子工业出版社 . 2018-1-1 -2. 凯.S.霍斯特曼 . 快学Scala(第2版)[M] . 电子工业出版社 . 2017-7 +1. Martin Odersky . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1 +2. 凯.S.霍斯特曼 . 快学 Scala(第 2 版)[M] . 电子工业出版社 . 2017-7 diff --git a/notes/Scala类型参数.md b/notes/Scala类型参数.md index 59f05a4..32054b3 100644 --- a/notes/Scala类型参数.md +++ b/notes/Scala类型参数.md @@ -1,467 +1,467 @@ -# 类型参数 - - - - -## 一、泛型 - -Scala支持类型参数化,使得我们能够编写泛型程序。 - -### 1.1 泛型类 - -Java中使用`<>`符号来包含定义的类型参数,Scala则使用`[]`。 - -```scala -class Pair[T, S](val first: T, val second: S) { - override def toString: String = first + ":" + second -} -``` - -```scala -object ScalaApp extends App { - - // 使用时候你直接指定参数类型,也可以不指定,由程序自动推断 - val pair01 = new Pair("heibai01", 22) - val pair02 = new Pair[String,Int]("heibai02", 33) - - println(pair01) - println(pair02) -} -``` - -### 1.2 泛型方法 - -函数和方法也支持类型参数。 - -```scala -object Utils { - def getHalf[T](a: Array[T]): Int = a.length / 2 -} -``` - -## 二、类型限定 - -### 2.1 类型上界限定 - -Scala和Java一样,对于对象之间进行大小比较,要求被比较的对象实现`java.lang.Comparable`接口。所以如果想对泛型进行比较,需要限定类型上界为`java.lang.Comparable`,语法为` S <: T`,代表类型S是类型T的子类或其本身。示例如下: - -```scala -// 使用 <: 符号,限定T必须是Comparable[T]的子类型 -class Pair[T <: Comparable[T]](val first: T, val second: T) { - // 返回较小的值 - def smaller: T = if (first.compareTo(second) < 0) first else second -} -``` - -```scala -// 测试代码 -val pair = new Pair("abc", "abcd") -println(pair.smaller) // 输出 abc -``` - ->扩展:如果你想要在Java中实现类型变量限定,需要使用关键字extends来实现,等价的Java代码如下: -> ->```java ->public class Pair> { -> private T first; -> private T second; -> Pair(T first, T second) { -> this.first = first; -> this.second = second; -> } -> public T smaller() { -> return first.compareTo(second) < 0 ? first : second; -> } ->} ->``` - -### 2.2 视图界定 - -在上面的例子中,如果你使用Int类型或者Double等类型进行测试,点击运行后,你会发现程序根本无法通过编译: - -```scala -val pair1 = new Pair(10, 12) -val pair2 = new Pair(10.0, 12.0) -``` - -之所以出现这样的问题,是因为Scala中的Int类并没有实现Comparable接口。在Scala中直接继承Comparable接口的是特质Ordered,它在继承compareTo方法的基础上,额外定义了关系符方法,源码如下: - -```scala -// 除了compareTo方法外,还提供了额外的关系符方法 -trait Ordered[A] extends Any with java.lang.Comparable[A] { - def compare(that: A): Int - def < (that: A): Boolean = (this compare that) < 0 - def > (that: A): Boolean = (this compare that) > 0 - def <= (that: A): Boolean = (this compare that) <= 0 - def >= (that: A): Boolean = (this compare that) >= 0 - def compareTo(that: A): Int = compare(that) -} -``` - -之所以在日常的编程中之所以你能够执行`3>2`这样的判断操作,是因为程序执行了定义在`Predef`中的隐式转换方法`intWrapper(x: Int) `,将Int类型转换为RichInt类型,而RichInt间接混入了Ordered特质,所以能够进行比较。 - -```scala -// Predef.scala -@inline implicit def intWrapper(x: Int) = new runtime.RichInt(x) -``` - -
- -要想解决传入数值无法进行比较的问题,可以使用视图界定。语法为`T <% U`,代表T能够通过隐式转换转为U,即允许Int型参数在无法进行比较的时候转换为RichInt类型。示例如下: - -```scala -// 视图界定符号 <% -class Pair[T <% Comparable[T]](val first: T, val second: T) { - // 返回较小的值 - def smaller: T = if (first.compareTo(second) < 0) first else second -} -``` - -> 注:由于直接继承Java中Comparable接口的是特质Ordered,所以如下的视图界定和上面是等效的: -> -> ```scala -> // 隐式转换为Ordered[T] -> class Pair[T <% Ordered[T]](val first: T, val second: T) { -> def smaller: T = if (first.compareTo(second) < 0) first else second -> } -> ``` - -### 2.3 类型约束 - -如果你用的Scala是2.11+,会发现视图界定已被标识为废弃。官方推荐使用类型约束(type constraint)来实现同样的功能,其本质是使用隐式参数进行隐式转换,示例如下: - -```scala - // 1.使用隐式参数隐式转换为Comparable[T] -class Pair[T](val first: T, val second: T)(implicit ev: T => Comparable[T]) - def smaller: T = if (first.compareTo(second) < 0) first else second -} - -// 2.由于直接继承Java中Comparable接口的是特质Ordered,所以也可以隐式转换为Ordered[T] -class Pair[T](val first: T, val second: T)(implicit ev: T => Ordered[T]) { - def smaller: T = if (first.compareTo(second) < 0) first else second -} -``` - -当然,隐式参数转换也可以运用在具体的方法上: - -```scala -object PairUtils{ - def smaller[T](a: T, b: T)(implicit order: T => Ordered[T]) = if (a < b) a else b -} -``` - -### 2.4 上下文界定 - -上下文界定的形式为`T:M`,其中M是一个泛型,它要求必须存在一个类型为M[T]的隐式值,当你声明一个带隐式参数的方法时,需要定义一个隐式默认值。所以上面的程序也可以使用上下文界定进行改写: - -```scala -class Pair[T](val first: T, val second: T) { - // 请注意 这个地方用的是Ordering[T],而上面视图界定和类型约束,用的是Ordered[T],两者的区别会在后文给出解释 - def smaller(implicit ord: Ordering[T]): T = if (ord.compare(first, second) < 0) first else second -} - -// 测试 -val pair= new Pair(88, 66) -println(pair.smaller) //输出:66 -``` - -在上面的示例中,我们无需手动添加隐式默认值就可以完成转换,这是因为Scala自动引入了Ordering[Int]这个隐式值。为了更好的说明上下文界定,下面给出一个自定义类型的比较示例: - -```scala -// 1.定义一个人员类 -class Person(val name: String, val age: Int) { - override def toString: String = name + ":" + age -} - -// 2.继承Ordering[T],实现自定义比较器,按照自己的规则重写比较方法 -class PersonOrdering extends Ordering[Person] { - override def compare(x: Person, y: Person): Int = if (x.age > y.age) 1 else -1 -} - -class Pair[T](val first: T, val second: T) { - def smaller(implicit ord: Ordering[T]): T = if (ord.compare(first, second) < 0) first else second -} - - -object ScalaApp extends App { - - val pair = new Pair(new Person("hei", 88), new Person("bai", 66)) - // 3.定义隐式默认值,如果不定义,则下一行代码无法通过编译 - implicit val ImpPersonOrdering = new PersonOrdering - println(pair.smaller) //输出: bai:66 -} -``` - -### 2.5 ClassTag上下文界定 - -这里先看一个例子:下面这段代码,没有任何语法错误,但是在运行时会抛出异常:`Error: cannot find class tag for element type T`, 这是由于Scala和Java一样,都存在类型擦除,即**泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉**。对于下面的代码,在运行阶段创建Array时,你必须明确指明其类型,但是此时泛型信息已经被擦除,导致出现找不到类型的异常。 - -```scala -object ScalaApp extends App { - def makePair[T](first: T, second: T) = { - // 创建以一个数组 并赋值 - val r = new Array[T](2); r(0) = first; r(1) = second; r - } -} -``` - -Scala针对这个问题,提供了ClassTag上下文界定,即把泛型的信息存储在ClassTag中,这样在运行阶段需要时,只需要从ClassTag中进行获取即可。其语法为`T : ClassTag`,示例如下: - -```scala -import scala.reflect._ -object ScalaApp extends App { - def makePair[T : ClassTag](first: T, second: T) = { - val r = new Array[T](2); r(0) = first; r(1) = second; r - } -} -``` - -### 2.6 类型下界限定 - -2.1小节介绍了类型上界的限定,Scala同时也支持下界的限定,语法为:`U >: T`,即U必须是类型T的超类或本身。 - -```scala -// 首席执行官 -class CEO - -// 部门经理 -class Manager extends CEO - -// 本公司普通员工 -class Employee extends Manager - -// 其他公司人员 -class OtherCompany - -object ScalaApp extends App { - - // 限定:只有本公司部门经理以上人员才能获取权限 - def Check[T >: Manager](t: T): T = { - println("获得审核权限") - t - } - - // 错误写法: 省略泛型参数后,以下所有人都能获得权限,显然这是不正确的 - Check(new CEO) - Check(new Manager) - Check(new Employee) - Check(new OtherCompany) - - - // 正确写法,传入泛型参数 - Check[CEO](new CEO) - Check[Manager](new Manager) - /* - * 以下两条语句无法通过编译,异常信息为: - * do not conform to method Check's type parameter bounds(不符合方法Check的类型参数边界) - * 这种情况就完成了下界限制,即只有本公司经理及以上的人员才能获得审核权限 - */ - Check[Employee](new Employee) - Check[OtherCompany](new OtherCompany) -} -``` - -### 2.7 多重界定 - -+ 类型变量可以同时有上界和下界。 写法为 :`T > : Lower <: Upper`; - -+ 不能同时有多个上界或多个下界 。但可以要求一个类型实现多个特质,写法为 : - - `T < : Comparable[T] with Serializable with Cloneable`; - -+ 你可以有多个上下文界定,写法为`T : Ordering : ClassTag` 。 - - - -## 三、Ordering & Ordered - -上文中使用到Ordering和Ordered特质,它们最主要的区别在于分别继承自不同的Java接口:Comparable和Comparator: - -+ **Comparable**:可以理解为内置的比较器,实现此接口的对象可以与自身进行比较; -+ **Comparator**:可以理解为外置的比较器;当对象自身并没有定义比较规则的时候,可以传入外部比较器进行比较。 - -为什么Java中要同时给出这两个比较接口,这是因为你要比较的对象不一定实现了Comparable接口,而你又想对其进行比较,这时候当然你可以修改代码实现Comparable,但是如果这个类你无法修改(如源码中的类),这时候就可以使用外置的比较器。同样的问题在Scala中当然也会出现,所以Scala分别使用了Ordering和Ordered来继承它们。 - -
- - - -下面分别给出Java中Comparable和Comparator接口的使用示例: - -### 3.1 Comparable - -```java -import java.util.Arrays; -// 实现Comparable接口 -public class Person implements Comparable { - - private String name; - private int age; - - Person(String name,int age) {this.name=name;this.age=age;} - @Override - public String toString() { return name+":"+age; } - - // 核心的方法是重写比较规则,按照年龄进行排序 - @Override - public int compareTo(Person person) { - return this.age - person.age; - } - - public static void main(String[] args) { - Person[] peoples= {new Person("hei", 66), new Person("bai", 55), new Person("ying", 77)}; - Arrays.sort(peoples); - Arrays.stream(peoples).forEach(System.out::println); - } -} - -输出: -bai:55 -hei:66 -ying:77 -``` - -### 3.2 Comparator - -```java -import java.util.Arrays; -import java.util.Comparator; - -public class Person { - - private String name; - private int age; - - Person(String name,int age) {this.name=name;this.age=age;} - @Override - public String toString() { return name+":"+age; } - - public static void main(String[] args) { - Person[] peoples= {new Person("hei", 66), new Person("bai", 55), new Person("ying", 77)}; - // 这里为了直观直接使用匿名内部类,实现Comparator接口 - //如果是Java8你也可以写成Arrays.sort(peoples, Comparator.comparingInt(o -> o.age)); - Arrays.sort(peoples, new Comparator() { - @Override - public int compare(Person o1, Person o2) { - return o1.age-o2.age; - } - }); - Arrays.stream(peoples).forEach(System.out::println); - } -} -``` - -使用外置比较器还有一个好处,就是你可以随时定义其排序规则: - -```scala -// 按照年龄大小排序 -Arrays.sort(peoples, Comparator.comparingInt(o -> o.age)); -Arrays.stream(peoples).forEach(System.out::println); -// 按照名字长度倒序排列 -Arrays.sort(peoples, Comparator.comparingInt(o -> -o.name.length())); -Arrays.stream(peoples).forEach(System.out::println); -``` - -### 3.3 上下文界定的优点 - -这里再次给出上下文界定中的示例代码作为回顾: - -```scala -// 1.定义一个人员类 -class Person(val name: String, val age: Int) { - override def toString: String = name + ":" + age -} - -// 2.继承Ordering[T],实现自定义比较器,这个比较器就是一个外置比较器 -class PersonOrdering extends Ordering[Person] { - override def compare(x: Person, y: Person): Int = if (x.age > y.age) 1 else -1 -} - -class Pair[T](val first: T, val second: T) { - def smaller(implicit ord: Ordering[T]): T = if (ord.compare(first, second) < 0) first else second -} - - -object ScalaApp extends App { - - val pair = new Pair(new Person("hei", 88), new Person("bai", 66)) - // 3.在当前上下文定义隐式默认值,这就相当于传入了外置比较器 - implicit val ImpPersonOrdering = new PersonOrdering - println(pair.smaller) //输出: bai:66 -} -``` - -使用上下文界定和Ordering带来的好处是:传入`Pair`中的参数不一定需要可比较,只要在比较时传入外置比较器即可。 - -需要注意的是由于隐式默认值二义性的限制,你不能像上面Java代码一样,在同一个上下文作用域中传入两个外置比较器,即下面的代码是无法通过编译的。但是你可以在不同的上下文作用域中引入不同的隐式默认值,即使用不同的外置比较器。 - -```scala -implicit val ImpPersonOrdering = new PersonOrdering -println(pair.smaller) -implicit val ImpPersonOrdering2 = new PersonOrdering -println(pair.smaller) -``` - - - -## 四、通配符 - -在实际编码中,通常需要把泛型限定在某个范围内,比如限定为某个类及其子类。因此Scala和Java一样引入了通配符这个概念,用于限定泛型的范围。不同的是Java使用`?`表示通配符,Scala使用`_`表示通配符。 - -```scala -class Ceo(val name: String) { - override def toString: String = name -} - -class Manager(name: String) extends Ceo(name) - -class Employee(name: String) extends Manager(name) - -class Pair[T](val first: T, val second: T) { - override def toString: String = "first:" + first + ", second: " + second -} - -object ScalaApp extends App { - // 限定部门经理及以下的人才可以组队 - def makePair(p: Pair[_ <: Manager]): Unit = {println(p)} - makePair(new Pair(new Employee("heibai"), new Manager("ying"))) -} -``` - -目前Scala中的通配符在某些复杂情况下还不完善,如下面的语句在Scala 2.12 中并不能通过编译: - -```scala -def min[T <: Comparable[_ >: T]](p: Pair[T]) ={} -``` - -可以使用以下语法代替: - -```scala -type SuperComparable[T] = Comparable[_ >: T] -def min[T <: SuperComparable[T]](p: Pair[T]) = {} -``` - - - -## 参考资料 - -1. Martin Odersky . Scala编程(第3版)[M] . 电子工业出版社 . 2018-1-1 -2. 凯.S.霍斯特曼 . 快学Scala(第2版)[M] . 电子工业出版社 . 2017-7 - +# 类型参数 + + + + +## 一、泛型 + +Scala 支持类型参数化,使得我们能够编写泛型程序。 + +### 1.1 泛型类 + +Java 中使用 `<>` 符号来包含定义的类型参数,Scala 则使用 `[]`。 + +```scala +class Pair[T, S](val first: T, val second: S) { + override def toString: String = first + ":" + second +} +``` + +```scala +object ScalaApp extends App { + + // 使用时候你直接指定参数类型,也可以不指定,由程序自动推断 + val pair01 = new Pair("heibai01", 22) + val pair02 = new Pair[String,Int]("heibai02", 33) + + println(pair01) + println(pair02) +} +``` + +### 1.2 泛型方法 + +函数和方法也支持类型参数。 + +```scala +object Utils { + def getHalf[T](a: Array[T]): Int = a.length / 2 +} +``` + +## 二、类型限定 + +### 2.1 类型上界限定 + +Scala 和 Java 一样,对于对象之间进行大小比较,要求被比较的对象实现 `java.lang.Comparable` 接口。所以如果想对泛型进行比较,需要限定类型上界为 `java.lang.Comparable`,语法为 ` S <: T`,代表类型 S 是类型 T 的子类或其本身。示例如下: + +```scala +// 使用 <: 符号,限定 T 必须是 Comparable[T]的子类型 +class Pair[T <: Comparable[T]](val first: T, val second: T) { + // 返回较小的值 + def smaller: T = if (first.compareTo(second) < 0) first else second +} +``` + +```scala +// 测试代码 +val pair = new Pair("abc", "abcd") +println(pair.smaller) // 输出 abc +``` + +>扩展:如果你想要在 Java 中实现类型变量限定,需要使用关键字 extends 来实现,等价的 Java 代码如下: +> +>```java +>public class Pair> { +> private T first; +> private T second; +> Pair(T first, T second) { +> this.first = first; +> this.second = second; +> } +> public T smaller() { +> return first.compareTo(second) < 0 ? first : second; +> } +>} +>``` + +### 2.2 视图界定 + +在上面的例子中,如果你使用 Int 类型或者 Double 等类型进行测试,点击运行后,你会发现程序根本无法通过编译: + +```scala +val pair1 = new Pair(10, 12) +val pair2 = new Pair(10.0, 12.0) +``` + +之所以出现这样的问题,是因为 Scala 中的 Int 类并没有实现 Comparable 接口。在 Scala 中直接继承 Comparable 接口的是特质 Ordered,它在继承 compareTo 方法的基础上,额外定义了关系符方法,源码如下: + +```scala +// 除了 compareTo 方法外,还提供了额外的关系符方法 +trait Ordered[A] extends Any with java.lang.Comparable[A] { + def compare(that: A): Int + def < (that: A): Boolean = (this compare that) < 0 + def > (that: A): Boolean = (this compare that) > 0 + def <= (that: A): Boolean = (this compare that) <= 0 + def >= (that: A): Boolean = (this compare that) >= 0 + def compareTo(that: A): Int = compare(that) +} +``` + +之所以在日常的编程中之所以你能够执行 `3>2` 这样的判断操作,是因为程序执行了定义在 `Predef` 中的隐式转换方法 `intWrapper(x: Int) `,将 Int 类型转换为 RichInt 类型,而 RichInt 间接混入了 Ordered 特质,所以能够进行比较。 + +```scala +// Predef.scala +@inline implicit def intWrapper(x: Int) = new runtime.RichInt(x) +``` + +
+ +要想解决传入数值无法进行比较的问题,可以使用视图界定。语法为 `T <% U`,代表 T 能够通过隐式转换转为 U,即允许 Int 型参数在无法进行比较的时候转换为 RichInt 类型。示例如下: + +```scala +// 视图界定符号 <% +class Pair[T <% Comparable[T]](val first: T, val second: T) { + // 返回较小的值 + def smaller: T = if (first.compareTo(second) < 0) first else second +} +``` + +> 注:由于直接继承 Java 中 Comparable 接口的是特质 Ordered,所以如下的视图界定和上面是等效的: +> +> ```scala +> // 隐式转换为 Ordered[T] +> class Pair[T <% Ordered[T]](val first: T, val second: T) { +> def smaller: T = if (first.compareTo(second) < 0) first else second +> } +> ``` + +### 2.3 类型约束 + +如果你用的 Scala 是 2.11+,会发现视图界定已被标识为废弃。官方推荐使用类型约束 (type constraint) 来实现同样的功能,其本质是使用隐式参数进行隐式转换,示例如下: + +```scala + // 1.使用隐式参数隐式转换为 Comparable[T] +class Pair[T](val first: T, val second: T)(implicit ev: T => Comparable[T]) + def smaller: T = if (first.compareTo(second) < 0) first else second +} + +// 2.由于直接继承 Java 中 Comparable 接口的是特质 Ordered,所以也可以隐式转换为 Ordered[T] +class Pair[T](val first: T, val second: T)(implicit ev: T => Ordered[T]) { + def smaller: T = if (first.compareTo(second) < 0) first else second +} +``` + +当然,隐式参数转换也可以运用在具体的方法上: + +```scala +object PairUtils{ + def smaller[T](a: T, b: T)(implicit order: T => Ordered[T]) = if (a < b) a else b +} +``` + +### 2.4 上下文界定 + +上下文界定的形式为 `T:M`,其中 M 是一个泛型,它要求必须存在一个类型为 M[T]的隐式值,当你声明一个带隐式参数的方法时,需要定义一个隐式默认值。所以上面的程序也可以使用上下文界定进行改写: + +```scala +class Pair[T](val first: T, val second: T) { + // 请注意 这个地方用的是 Ordering[T],而上面视图界定和类型约束,用的是 Ordered[T],两者的区别会在后文给出解释 + def smaller(implicit ord: Ordering[T]): T = if (ord.compare(first, second) < 0) first else second +} + +// 测试 +val pair= new Pair(88, 66) +println(pair.smaller) //输出:66 +``` + +在上面的示例中,我们无需手动添加隐式默认值就可以完成转换,这是因为 Scala 自动引入了 Ordering[Int]这个隐式值。为了更好的说明上下文界定,下面给出一个自定义类型的比较示例: + +```scala +// 1.定义一个人员类 +class Person(val name: String, val age: Int) { + override def toString: String = name + ":" + age +} + +// 2.继承 Ordering[T],实现自定义比较器,按照自己的规则重写比较方法 +class PersonOrdering extends Ordering[Person] { + override def compare(x: Person, y: Person): Int = if (x.age > y.age) 1 else -1 +} + +class Pair[T](val first: T, val second: T) { + def smaller(implicit ord: Ordering[T]): T = if (ord.compare(first, second) < 0) first else second +} + + +object ScalaApp extends App { + + val pair = new Pair(new Person("hei", 88), new Person("bai", 66)) + // 3.定义隐式默认值,如果不定义,则下一行代码无法通过编译 + implicit val ImpPersonOrdering = new PersonOrdering + println(pair.smaller) //输出: bai:66 +} +``` + +### 2.5 ClassTag上下文界定 + +这里先看一个例子:下面这段代码,没有任何语法错误,但是在运行时会抛出异常:`Error: cannot find class tag for element type T`, 这是由于 Scala 和 Java 一样,都存在类型擦除,即**泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉**。对于下面的代码,在运行阶段创建 Array 时,你必须明确指明其类型,但是此时泛型信息已经被擦除,导致出现找不到类型的异常。 + +```scala +object ScalaApp extends App { + def makePair[T](first: T, second: T) = { + // 创建以一个数组 并赋值 + val r = new Array[T](2); r(0) = first; r(1) = second; r + } +} +``` + +Scala 针对这个问题,提供了 ClassTag 上下文界定,即把泛型的信息存储在 ClassTag 中,这样在运行阶段需要时,只需要从 ClassTag 中进行获取即可。其语法为 `T : ClassTag`,示例如下: + +```scala +import scala.reflect._ +object ScalaApp extends App { + def makePair[T : ClassTag](first: T, second: T) = { + val r = new Array[T](2); r(0) = first; r(1) = second; r + } +} +``` + +### 2.6 类型下界限定 + +2.1 小节介绍了类型上界的限定,Scala 同时也支持下界的限定,语法为:`U >: T`,即 U 必须是类型 T 的超类或本身。 + +```scala +// 首席执行官 +class CEO + +// 部门经理 +class Manager extends CEO + +// 本公司普通员工 +class Employee extends Manager + +// 其他公司人员 +class OtherCompany + +object ScalaApp extends App { + + // 限定:只有本公司部门经理以上人员才能获取权限 + def Check[T >: Manager](t: T): T = { + println("获得审核权限") + t + } + + // 错误写法: 省略泛型参数后,以下所有人都能获得权限,显然这是不正确的 + Check(new CEO) + Check(new Manager) + Check(new Employee) + Check(new OtherCompany) + + + // 正确写法,传入泛型参数 + Check[CEO](new CEO) + Check[Manager](new Manager) + /* + * 以下两条语句无法通过编译,异常信息为: + * do not conform to method Check's type parameter bounds(不符合方法 Check 的类型参数边界) + * 这种情况就完成了下界限制,即只有本公司经理及以上的人员才能获得审核权限 + */ + Check[Employee](new Employee) + Check[OtherCompany](new OtherCompany) +} +``` + +### 2.7 多重界定 + ++ 类型变量可以同时有上界和下界。 写法为 :`T > : Lower <: Upper`; + ++ 不能同时有多个上界或多个下界 。但可以要求一个类型实现多个特质,写法为 : + + `T < : Comparable[T] with Serializable with Cloneable`; + ++ 你可以有多个上下文界定,写法为 `T : Ordering : ClassTag` 。 + + + +## 三、Ordering & Ordered + +上文中使用到 Ordering 和 Ordered 特质,它们最主要的区别在于分别继承自不同的 Java 接口:Comparable 和 Comparator: + ++ **Comparable**:可以理解为内置的比较器,实现此接口的对象可以与自身进行比较; ++ **Comparator**:可以理解为外置的比较器;当对象自身并没有定义比较规则的时候,可以传入外部比较器进行比较。 + +为什么 Java 中要同时给出这两个比较接口,这是因为你要比较的对象不一定实现了 Comparable 接口,而你又想对其进行比较,这时候当然你可以修改代码实现 Comparable,但是如果这个类你无法修改 (如源码中的类),这时候就可以使用外置的比较器。同样的问题在 Scala 中当然也会出现,所以 Scala 分别使用了 Ordering 和 Ordered 来继承它们。 + +
+ + + +下面分别给出 Java 中 Comparable 和 Comparator 接口的使用示例: + +### 3.1 Comparable + +```java +import java.util.Arrays; +// 实现 Comparable 接口 +public class Person implements Comparable { + + private String name; + private int age; + + Person(String name,int age) {this.name=name;this.age=age;} + @Override + public String toString() { return name+":"+age; } + + // 核心的方法是重写比较规则,按照年龄进行排序 + @Override + public int compareTo(Person person) { + return this.age - person.age; + } + + public static void main(String[] args) { + Person[] peoples= {new Person("hei", 66), new Person("bai", 55), new Person("ying", 77)}; + Arrays.sort(peoples); + Arrays.stream(peoples).forEach(System.out::println); + } +} + +输出: +bai:55 +hei:66 +ying:77 +``` + +### 3.2 Comparator + +```java +import java.util.Arrays; +import java.util.Comparator; + +public class Person { + + private String name; + private int age; + + Person(String name,int age) {this.name=name;this.age=age;} + @Override + public String toString() { return name+":"+age; } + + public static void main(String[] args) { + Person[] peoples= {new Person("hei", 66), new Person("bai", 55), new Person("ying", 77)}; + // 这里为了直观直接使用匿名内部类,实现 Comparator 接口 + //如果是 Java8 你也可以写成 Arrays.sort(peoples, Comparator.comparingInt(o -> o.age)); + Arrays.sort(peoples, new Comparator() { + @Override + public int compare(Person o1, Person o2) { + return o1.age-o2.age; + } + }); + Arrays.stream(peoples).forEach(System.out::println); + } +} +``` + +使用外置比较器还有一个好处,就是你可以随时定义其排序规则: + +```scala +// 按照年龄大小排序 +Arrays.sort(peoples, Comparator.comparingInt(o -> o.age)); +Arrays.stream(peoples).forEach(System.out::println); +// 按照名字长度倒序排列 +Arrays.sort(peoples, Comparator.comparingInt(o -> -o.name.length())); +Arrays.stream(peoples).forEach(System.out::println); +``` + +### 3.3 上下文界定的优点 + +这里再次给出上下文界定中的示例代码作为回顾: + +```scala +// 1.定义一个人员类 +class Person(val name: String, val age: Int) { + override def toString: String = name + ":" + age +} + +// 2.继承 Ordering[T],实现自定义比较器,这个比较器就是一个外置比较器 +class PersonOrdering extends Ordering[Person] { + override def compare(x: Person, y: Person): Int = if (x.age > y.age) 1 else -1 +} + +class Pair[T](val first: T, val second: T) { + def smaller(implicit ord: Ordering[T]): T = if (ord.compare(first, second) < 0) first else second +} + + +object ScalaApp extends App { + + val pair = new Pair(new Person("hei", 88), new Person("bai", 66)) + // 3.在当前上下文定义隐式默认值,这就相当于传入了外置比较器 + implicit val ImpPersonOrdering = new PersonOrdering + println(pair.smaller) //输出: bai:66 +} +``` + +使用上下文界定和 Ordering 带来的好处是:传入 `Pair` 中的参数不一定需要可比较,只要在比较时传入外置比较器即可。 + +需要注意的是由于隐式默认值二义性的限制,你不能像上面 Java 代码一样,在同一个上下文作用域中传入两个外置比较器,即下面的代码是无法通过编译的。但是你可以在不同的上下文作用域中引入不同的隐式默认值,即使用不同的外置比较器。 + +```scala +implicit val ImpPersonOrdering = new PersonOrdering +println(pair.smaller) +implicit val ImpPersonOrdering2 = new PersonOrdering +println(pair.smaller) +``` + + + +## 四、通配符 + +在实际编码中,通常需要把泛型限定在某个范围内,比如限定为某个类及其子类。因此 Scala 和 Java 一样引入了通配符这个概念,用于限定泛型的范围。不同的是 Java 使用 `?` 表示通配符,Scala 使用 `_` 表示通配符。 + +```scala +class Ceo(val name: String) { + override def toString: String = name +} + +class Manager(name: String) extends Ceo(name) + +class Employee(name: String) extends Manager(name) + +class Pair[T](val first: T, val second: T) { + override def toString: String = "first:" + first + ", second: " + second +} + +object ScalaApp extends App { + // 限定部门经理及以下的人才可以组队 + def makePair(p: Pair[_ <: Manager]): Unit = {println(p)} + makePair(new Pair(new Employee("heibai"), new Manager("ying"))) +} +``` + +目前 Scala 中的通配符在某些复杂情况下还不完善,如下面的语句在 Scala 2.12 中并不能通过编译: + +```scala +def min[T <: Comparable[_ >: T]](p: Pair[T]) ={} +``` + +可以使用以下语法代替: + +```scala +type SuperComparable[T] = Comparable[_ >: T] +def min[T <: SuperComparable[T]](p: Pair[T]) = {} +``` + + + +## 参考资料 + +1. Martin Odersky . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1 +2. 凯.S.霍斯特曼 . 快学 Scala(第 2 版)[M] . 电子工业出版社 . 2017-7 + diff --git a/notes/Scala继承和特质.md b/notes/Scala继承和特质.md index b3556bb..72d22b9 100644 --- a/notes/Scala继承和特质.md +++ b/notes/Scala继承和特质.md @@ -20,50 +20,50 @@ ### 1.1 Scala中的继承结构 -Scala中继承关系如下图: +Scala 中继承关系如下图: -+ Any是整个继承关系的根节点; -+ AnyRef包含Scala Classes和Java Classes,等价于Java中的java.lang.Object; -+ AnyVal是所有值类型的一个标记; -+ Null是所有引用类型的子类型,唯一实例是null,可以将null赋值给除了值类型外的所有类型的变量; -+ Nothing是所有类型的子类型。 ++ Any 是整个继承关系的根节点; ++ AnyRef 包含 Scala Classes 和 Java Classes,等价于 Java 中的 java.lang.Object; ++ AnyVal 是所有值类型的一个标记; ++ Null 是所有引用类型的子类型,唯一实例是 null,可以将 null 赋值给除了值类型外的所有类型的变量; ++ Nothing 是所有类型的子类型。
### 1.2 extends & override -Scala的集成机制和Java有很多相似之处,比如都使用`extends`关键字表示继承,都使用`override`关键字表示重写父类的方法或成员变量。示例如下: +Scala 的集成机制和 Java 有很多相似之处,比如都使用 `extends` 关键字表示继承,都使用 `override` 关键字表示重写父类的方法或成员变量。示例如下: ```scala //父类 class Person { var name = "" - // 1.不加任何修饰词,默认为public,能被子类和外部访问 + // 1.不加任何修饰词,默认为 public,能被子类和外部访问 var age = 0 - // 2.使用protected修饰的变量能子类访问,但是不能被外部访问 + // 2.使用 protected 修饰的变量能子类访问,但是不能被外部访问 protected var birthday = "" - // 3.使用private修饰的变量不能被子类和外部访问 + // 3.使用 private 修饰的变量不能被子类和外部访问 private var sex = "" def setSex(sex: String): Unit = { this.sex = sex } - // 4.重写父类的方法建议使用override关键字修饰 + // 4.重写父类的方法建议使用 override 关键字修饰 override def toString: String = name + ":" + age + ":" + birthday + ":" + sex } ``` -使用`extends`关键字实现继承: +使用 `extends` 关键字实现继承: ```scala -// 1.使用extends关键字实现继承 +// 1.使用 extends 关键字实现继承 class Employee extends Person { override def toString: String = "Employee~" + super.toString - // 2.使用public或protected关键字修饰的变量能被子类访问 + // 2.使用 public 或 protected 关键字修饰的变量能被子类访问 def setBirthday(date: String): Unit = { birthday = date } @@ -92,7 +92,7 @@ object ScalaApp extends App { ### 1.3 调用超类构造器 -在Scala的类中,每个辅助构造器都必须首先调用其他构造器或主构造器,这样就导致了子类的辅助构造器永远无法直接调用超类的构造器,只有主构造器才能调用超类的构造器。所以想要调用超类的构造器,代码示例如下: +在 Scala 的类中,每个辅助构造器都必须首先调用其他构造器或主构造器,这样就导致了子类的辅助构造器永远无法直接调用超类的构造器,只有主构造器才能调用超类的构造器。所以想要调用超类的构造器,代码示例如下: ```scala class Employee(name:String,age:Int,salary:Double) extends Person(name:String,age:Int) { @@ -102,7 +102,7 @@ class Employee(name:String,age:Int,salary:Double) extends Person(name:String,age ### 1.4 类型检查和转换 -想要实现类检查可以使用`isInstanceOf`,判断一个实例是否来源于某个类或者其子类,如果是,则可以使用`asInstanceOf`进行强制类型转换。 +想要实现类检查可以使用 `isInstanceOf`,判断一个实例是否来源于某个类或者其子类,如果是,则可以使用 `asInstanceOf` 进行强制类型转换。 ```scala object ScalaApp extends App { @@ -117,7 +117,7 @@ object ScalaApp extends App { // 2. 强制类型转换 var p: Person = employee.asInstanceOf[Person] - // 3. 判断一个实例是否来源于某个类(而不是其子类) + // 3. 判断一个实例是否来源于某个类 (而不是其子类) println(employee.getClass == classOf[Employee]) } @@ -127,7 +127,7 @@ object ScalaApp extends App { #### **1. 构造顺序** -在Scala中还有一个需要注意的问题,如果你在子类中重写父类的val变量,并且超类的构造器中使用了该变量,那么可能会产生不可预期的错误。下面给出一个示例: +在 Scala 中还有一个需要注意的问题,如果你在子类中重写父类的 val 变量,并且超类的构造器中使用了该变量,那么可能会产生不可预期的错误。下面给出一个示例: ```scala // 父类 @@ -151,7 +151,7 @@ object ScalaApp extends App { } ``` -这里初始化array用到了变量range,这里你会发现实际上array既不会被初始化Array(10),也不会被初始化为Array(2),实际的输出应该如下: +这里初始化 array 用到了变量 range,这里你会发现实际上 array 既不会被初始化 Array(10),也不会被初始化为 Array(2),实际的输出应该如下: ```properties 父类的默认构造器 @@ -159,19 +159,19 @@ object ScalaApp extends App { () ``` -可以看到array被初始化为Array(0),主要原因在于父类构造器的执行顺序先于子类构造器,这里给出实际的执行步骤: +可以看到 array 被初始化为 Array(0),主要原因在于父类构造器的执行顺序先于子类构造器,这里给出实际的执行步骤: -1. 父类的构造器被调用,执行`new Array[Int](range)`语句; -2. 这里想要得到range的值,会去调用子类range()方法,因为`override val`重写变量的同时也重写了其get方法; -3. 调用子类的range()方法,自然也是返回子类的range值,但是由于子类的构造器还没有执行,这也就意味着对range赋值的`range = 2`语句还没有被执行,所以自然返回range的默认值,也就是0。 +1. 父类的构造器被调用,执行 `new Array[Int](range)` 语句; +2. 这里想要得到 range 的值,会去调用子类 range() 方法,因为 `override val` 重写变量的同时也重写了其 get 方法; +3. 调用子类的 range() 方法,自然也是返回子类的 range 值,但是由于子类的构造器还没有执行,这也就意味着对 range 赋值的 `range = 2` 语句还没有被执行,所以自然返回 range 的默认值,也就是 0。 -这里可能比较疑惑的是为什么`val range = 2`没有被执行,却能使用range变量,这里因为在虚拟机层面,是先对成员变量先分配存储空间并赋给默认值,之后才赋予给定的值。想要证明这一点其实也比较简单,代码如下: +这里可能比较疑惑的是为什么 `val range = 2` 没有被执行,却能使用 range 变量,这里因为在虚拟机层面,是先对成员变量先分配存储空间并赋给默认值,之后才赋予给定的值。想要证明这一点其实也比较简单,代码如下: ```scala class Person { - // val range: Int = 10 正常代码 array为Array(10) + // val range: Int = 10 正常代码 array 为 Array(10) val array: Array[Int] = new Array[Int](range) - val range: Int = 10 //如果把变量的声明放在使用之后,此时数据array为array(0) + val range: Int = 10 //如果把变量的声明放在使用之后,此时数据 array 为 array(0) } object Person { @@ -186,15 +186,15 @@ object Person { 想要解决上面的问题,有以下几种方法: -(1) . 将变量用final修饰,代表不允许被子类重写,即 `final val range: Int = 10 `; +(1) . 将变量用 final 修饰,代表不允许被子类重写,即 `final val range: Int = 10 `; -(2) . 将变量使用lazy修饰,代表懒加载,即只有当你实际使用到array时候,才去进行初始化; +(2) . 将变量使用 lazy 修饰,代表懒加载,即只有当你实际使用到 array 时候,才去进行初始化; ```scala lazy val array: Array[Int] = new Array[Int](range) ``` -(3) . 采用提前定义,代码如下,代表range的定义优先于超类构造器。 +(3) . 采用提前定义,代码如下,代表 range 的定义优先于超类构造器。 ```scala class Employee extends { @@ -208,13 +208,13 @@ class Employee extends { 但是这种语法也有其限制:你只能在上面代码块中重写已有的变量,而不能定义新的变量和方法,定义新的变量和方法只能写在下面代码块中。 ->**注意事项**:类的继承和下文特质(trait)的继承都存在这个问题,也同样可以通过提前定义来解决。虽然如此,但还是建议合理设计以规避该类问题。 +>**注意事项**:类的继承和下文特质 (trait) 的继承都存在这个问题,也同样可以通过提前定义来解决。虽然如此,但还是建议合理设计以规避该类问题。
## 二、抽象类 -Scala中允许使用`abstract`定义抽象类,并且通过`extends`关键字继承它。 +Scala 中允许使用 `abstract` 定义抽象类,并且通过 `extends` 关键字继承它。 定义抽象类: @@ -227,7 +227,7 @@ abstract class Person { // 2.定义抽象方法 def geDetail: String - // 3. scala的抽象类允许定义具体方法 + // 3. scala 的抽象类允许定义具体方法 def print(): Unit = { println("抽象类中的默认方法") } @@ -254,10 +254,10 @@ class Employee extends Person { ### 3.1 trait & with -Scala中没有interface这个关键字,想要实现类似的功能,可以使用特质(trait)。trait等价于Java 8中的接口,因为trait中既能定义抽象方法,也能定义具体方法,这和Java 8中的接口是类似的。 +Scala 中没有 interface 这个关键字,想要实现类似的功能,可以使用特质 (trait)。trait 等价于 Java 8 中的接口,因为 trait 中既能定义抽象方法,也能定义具体方法,这和 Java 8 中的接口是类似的。 ```scala -// 1.特质使用trait关键字修饰 +// 1.特质使用 trait 关键字修饰 trait Logger { // 2.定义抽象方法 @@ -270,10 +270,10 @@ trait Logger { } ``` -想要使用特质,需要使用`extends`关键字,而不是`implements`关键字,如果想要添加多个特质,可以使用`with`关键字。 +想要使用特质,需要使用 `extends` 关键字,而不是 `implements` 关键字,如果想要添加多个特质,可以使用 `with` 关键字。 ```scala -// 1.使用extends关键字,而不是implements,如果想要添加多个特质,可以使用with关键字 +// 1.使用 extends 关键字,而不是 implements,如果想要添加多个特质,可以使用 with 关键字 class ConsoleLogger extends Logger with Serializable with Cloneable { // 2. 实现特质中的抽象方法 @@ -310,7 +310,7 @@ class InfoLogger extends Logger { ### 3.3 带有特质的对象 -Scala支持在类定义的时混入`父类trait`,而在类实例化为具体对象的时候指明其实际使用的`子类trait`。示例如下: +Scala 支持在类定义的时混入 ` 父类 trait`,而在类实例化为具体对象的时候指明其实际使用的 ` 子类 trait`。示例如下:
@@ -327,7 +327,7 @@ trait Logger { trait ErrorLogger: ```scala -// 错误日志打印,继承自Logger +// 错误日志打印,继承自 Logger trait ErrorLogger extends Logger { // 覆盖空方法 override def log(msg: String): Unit = { @@ -339,7 +339,7 @@ trait ErrorLogger extends Logger { trait InfoLogger: ```scala -// 通知日志打印,继承自Logger +// 通知日志打印,继承自 Logger trait InfoLogger extends Logger { // 覆盖空方法 @@ -352,7 +352,7 @@ trait InfoLogger extends Logger { 具体的使用类: ```scala -// 混入trait Logger +// 混入 trait Logger class Person extends Logger { // 调用定义的抽象方法 def printDetail(detail: String): Unit = { @@ -361,12 +361,12 @@ class Person extends Logger { } ``` -这里通过main方法来测试: +这里通过 main 方法来测试: ```scala object ScalaApp extends App { - // 使用with指明需要具体使用的trait + // 使用 with 指明需要具体使用的 trait val person01 = new Person with InfoLogger val person02 = new Person with ErrorLogger val person03 = new Person with InfoLogger with ErrorLogger @@ -377,30 +377,30 @@ object ScalaApp extends App { } ``` -这里前面两个输出比较明显,因为只指明了一个具体的`trait`,这里需要说明的是第三个输出,**因为trait的调用是由右到左开始生效的**,所以这里打印出`Error:scala`。 +这里前面两个输出比较明显,因为只指明了一个具体的 `trait`,这里需要说明的是第三个输出,**因为 trait 的调用是由右到左开始生效的**,所以这里打印出 `Error:scala`。 ### 3.4 特质构造顺序 -`trait`有默认的无参构造器,但是不支持有参构造器。一个类混入多个特质后初始化顺序应该如下: +`trait` 有默认的无参构造器,但是不支持有参构造器。一个类混入多个特质后初始化顺序应该如下: ```scala // 示例 class Employee extends Person with InfoLogger with ErrorLogger {...} ``` -1. 超类首先被构造,即Person的构造器首先被执行; +1. 超类首先被构造,即 Person 的构造器首先被执行; 2. 特质的构造器在超类构造器之前,在类构造器之后;特质由左到右被构造;每个特质中,父特质首先被构造; - + Logger构造器执行(Logger是InfoLogger的父类); - + InfoLogger构造器执行; - + ErrorLogger构造器执行; + + Logger 构造器执行(Logger 是 InfoLogger 的父类); + + InfoLogger 构造器执行; + + ErrorLogger 构造器执行; 3. 所有超类和特质构造完毕,子类才会被构造。
## 参考资料 -1. Martin Odersky . Scala编程(第3版)[M] . 电子工业出版社 . 2018-1-1 -2. 凯.S.霍斯特曼 . 快学Scala(第2版)[M] . 电子工业出版社 . 2017-7 +1. Martin Odersky . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1 +2. 凯.S.霍斯特曼 . 快学 Scala(第 2 版)[M] . 电子工业出版社 . 2017-7 diff --git a/notes/Scala隐式转换和隐式参数.md b/notes/Scala隐式转换和隐式参数.md index 8627591..b980b46 100644 --- a/notes/Scala隐式转换和隐式参数.md +++ b/notes/Scala隐式转换和隐式参数.md @@ -16,7 +16,7 @@ ### 1.1 使用隐式转换 -隐式转换指的是以`implicit`关键字声明带有单个参数的转换函数,它将值从一种类型转换为另一种类型,以便使用之前类型所没有的功能。示例如下: +隐式转换指的是以 `implicit` 关键字声明带有单个参数的转换函数,它将值从一种类型转换为另一种类型,以便使用之前类型所没有的功能。示例如下: ```scala // 普通人 @@ -31,7 +31,7 @@ class Thor(val name: String) { } object Thor extends App { - // 定义隐式转换方法 将普通人转换为雷神 通常建议方法名使用source2Target,即:被转换对象To转换对象 + // 定义隐式转换方法 将普通人转换为雷神 通常建议方法名使用 source2Target,即:被转换对象 To 转换对象 implicit def person2Thor(p: Person): Thor = new Thor(p.name) // 这样普通人也能举起雷神之锤 new Person("普通人").hammer() @@ -44,7 +44,7 @@ object Thor extends App { ### 1.2 隐式转换规则 -并不是你使用`implicit`转换后,隐式转换就一定会发生,比如上面如果不调用`hammer()`方法的时候,普通人就还是普通人。通常程序会在以下情况下尝试执行隐式转换: +并不是你使用 `implicit` 转换后,隐式转换就一定会发生,比如上面如果不调用 `hammer()` 方法的时候,普通人就还是普通人。通常程序会在以下情况下尝试执行隐式转换: + 当对象访问一个不存在的成员时,即调用的方法不存在或者访问的成员变量不存在; + 当对象调用某个方法,该方法存在,但是方法的声明参数与传入参数不匹配时。 @@ -52,7 +52,7 @@ object Thor extends App { 而在以下三种情况下编译器不会尝试执行隐式转换: + 如果代码能够在不使用隐式转换的前提下通过编译,则不会使用隐式转换; -+ 编译器不会尝试同时执行多个转换,比如`convert1(convert2(a))*b`; ++ 编译器不会尝试同时执行多个转换,比如 `convert1(convert2(a))*b`; + 转换存在二义性,也不会发生转换。 这里首先解释一下二义性,上面的代码进行如下修改,由于两个隐式转换都是生效的,所以就存在了二义性: @@ -97,16 +97,16 @@ object ImplicitTest extends App { new ClassC } - // 这行代码无法通过编译,因为要调用到printB方法,需要执行两次转换C2B(D2C(ClassD)) + // 这行代码无法通过编译,因为要调用到 printB 方法,需要执行两次转换 C2B(D2C(ClassD)) new ClassD().printB(new ClassA) /* * 下面的这一行代码虽然也进行了两次隐式转换,但是两次的转换对象并不是一个对象,所以它是生效的: * 转换流程如下: - * 1. ClassC中并没有printB方法,因此隐式转换为ClassB,然后调用printB方法; - * 2. 但是printB参数类型为ClassB,然而传入的参数类型是ClassA,所以需要将参数ClassA转换为ClassB,这是第二次; + * 1. ClassC 中并没有 printB 方法,因此隐式转换为 ClassB,然后调用 printB 方法; + * 2. 但是 printB 参数类型为 ClassB,然而传入的参数类型是 ClassA,所以需要将参数 ClassA 转换为 ClassB,这是第二次; * 即: C2B(ClassC) -> ClassB.printB(ClassA) -> ClassB.printB(A2B(ClassA)) -> ClassB.printB(ClassB) - * 转换过程1的对象是ClassC,而转换过程2的转换对象是ClassA,所以虽然是一行代码两次转换,但是仍然是有效转换 + * 转换过程 1 的对象是 ClassC,而转换过程 2 的转换对象是 ClassA,所以虽然是一行代码两次转换,但是仍然是有效转换 */ new ClassC().printB(new ClassA) } @@ -163,7 +163,7 @@ object Convert { ``` ```scala -// 导入Convert下所有的隐式转换函数 +// 导入 Convert 下所有的隐式转换函数 import com.heibaiying.Convert._ object ScalaApp extends App { @@ -171,7 +171,7 @@ object ScalaApp extends App { } ``` -> 注:Scala自身的隐式转换函数大部分定义在`Predef.scala`中,你可以打开源文件查看,也可以在Scala交互式命令行中采用`:implicit -v`查看全部隐式转换函数。 +> 注:Scala 自身的隐式转换函数大部分定义在 `Predef.scala` 中,你可以打开源文件查看,也可以在 Scala 交互式命令行中采用 `:implicit -v` 查看全部隐式转换函数。
@@ -179,7 +179,7 @@ object ScalaApp extends App { ### 2.1 使用隐式参数 -在定义函数或方法时可以使用标记为`implicit`的参数,这种情况下,编译器将会查找默认值,提供给函数调用。 +在定义函数或方法时可以使用标记为 `implicit` 的参数,这种情况下,编译器将会查找默认值,提供给函数调用。 ```scala // 定义分隔符类 @@ -200,20 +200,20 @@ object ScalaApp extends App { 关于隐式参数,有两点需要注意: -1.我们上面定义`formatted`函数的时候使用了柯里化,如果你不使用柯里化表达式,按照通常习惯只有下面两种写法: +1.我们上面定义 `formatted` 函数的时候使用了柯里化,如果你不使用柯里化表达式,按照通常习惯只有下面两种写法: ```scala // 这种写法没有语法错误,但是无法通过编译 def formatted(implicit context: String, deli: Delimiters): Unit = { println(deli.left + context + deli.right) } -// 不存在这种写法,IDEA直接会直接提示语法错误 +// 不存在这种写法,IDEA 直接会直接提示语法错误 def formatted( context: String, implicit deli: Delimiters): Unit = { println(deli.left + context + deli.right) } ``` -上面第一种写法编译的时候会出现下面所示`error`信息,从中也可以看出`implicit`是作用于参数列表中每个参数的,这显然不是我们想要到达的效果,所以上面的写法采用了柯里化。 +上面第一种写法编译的时候会出现下面所示 `error` 信息,从中也可以看出 `implicit` 是作用于参数列表中每个参数的,这显然不是我们想要到达的效果,所以上面的写法采用了柯里化。 ``` not enough arguments for method formatted: @@ -228,7 +228,7 @@ implicit val brace = new Delimiters("{", "}") formatted("this is context") ``` -上面代码无法通过编译,出现错误提示`ambiguous implicit values`,即隐式值存在冲突。 +上面代码无法通过编译,出现错误提示 `ambiguous implicit values`,即隐式值存在冲突。 @@ -291,7 +291,7 @@ object ScalaApp extends App { def smaller[T] (a: T, b: T) = if (a < b) a else b ``` -在Scala中如果定义了一个如上所示的比较对象大小的泛型方法,你会发现无法通过编译。对于对象之间进行大小比较,Scala和Java一样,都要求被比较的对象需要实现java.lang.Comparable接口。在Scala中,直接继承Java中Comparable接口的是特质Ordered,它在继承compareTo方法的基础上,额外定义了关系符方法,源码如下: +在 Scala 中如果定义了一个如上所示的比较对象大小的泛型方法,你会发现无法通过编译。对于对象之间进行大小比较,Scala 和 Java 一样,都要求被比较的对象需要实现 java.lang.Comparable 接口。在 Scala 中,直接继承 Java 中 Comparable 接口的是特质 Ordered,它在继承 compareTo 方法的基础上,额外定义了关系符方法,源码如下: ```scala trait Ordered[A] extends Any with java.lang.Comparable[A] { @@ -318,14 +318,14 @@ object Pair extends App { } ``` -视图限定限制了T可以通过隐式转换`Ordered[T]`,即对象一定可以进行大小比较。在上面的代码中`smaller(1,2)`中参数`1`和`2`实际上是通过定义在`Predef`中的隐式转换方法`intWrapper`转换为`RichInt`。 +视图限定限制了 T 可以通过隐式转换 `Ordered[T]`,即对象一定可以进行大小比较。在上面的代码中 `smaller(1,2)` 中参数 `1` 和 `2` 实际上是通过定义在 `Predef` 中的隐式转换方法 `intWrapper` 转换为 `RichInt`。 ```scala // Predef.scala @inline implicit def intWrapper(x: Int) = new runtime.RichInt(x) ``` -为什么要这么麻烦执行隐式转换,原因是Scala中的Int类型并不能直接进行比较,因为其没有实现`Ordered`特质,真正实现`Ordered`特质的是`RichInt`。 +为什么要这么麻烦执行隐式转换,原因是 Scala 中的 Int 类型并不能直接进行比较,因为其没有实现 `Ordered` 特质,真正实现 `Ordered` 特质的是 `RichInt`。
@@ -333,12 +333,12 @@ object Pair extends App { #### 2. 利用隐式参数进行隐式转换 -Scala2.11+后,视图界定被标识为废弃,官方推荐使用类型限定来解决上面的问题,本质上就是使用隐式参数进行隐式转换。 +Scala2.11+ 后,视图界定被标识为废弃,官方推荐使用类型限定来解决上面的问题,本质上就是使用隐式参数进行隐式转换。 ```scala object Pair extends App { - // order既是一个隐式参数也是一个隐式转换,即如果a不存在 < 方法,则转换为order(a) Ordered[T]) = if (a < b) a else b println(smaller(1,2)) //输出 1 @@ -349,8 +349,8 @@ object Pair extends App { ## 参考资料 -1. Martin Odersky . Scala编程(第3版)[M] . 电子工业出版社 . 2018-1-1 -2. 凯.S.霍斯特曼 . 快学Scala(第2版)[M] . 电子工业出版社 . 2017-7 +1. Martin Odersky . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1 +2. 凯.S.霍斯特曼 . 快学 Scala(第 2 版)[M] . 电子工业出版社 . 2017-7 diff --git a/notes/Scala集合类型.md b/notes/Scala集合类型.md index 986f5f5..d44cd58 100644 --- a/notes/Scala集合类型.md +++ b/notes/Scala集合类型.md @@ -13,7 +13,7 @@ ## 一、集合简介 -Scala中拥有多种集合类型,主要分为可变的和不可变的集合两大类: +Scala 中拥有多种集合类型,主要分为可变的和不可变的集合两大类: + **可变集合**: 可以被修改。即可以更改,添加,删除集合中的元素; @@ -21,7 +21,7 @@ Scala中拥有多种集合类型,主要分为可变的和不可变的集合两 ## 二、集合结构 -Scala中的大部分集合类都存在三类变体,分别位于`scala.collection`, `scala.collection.immutable`, `scala.collection.mutable`包中。还有部分集合类位于`scala.collection.generic`包下。 +Scala 中的大部分集合类都存在三类变体,分别位于 `scala.collection`, `scala.collection.immutable`, `scala.collection.mutable` 包中。还有部分集合类位于 `scala.collection.generic` 包下。 - **scala.collection.immutable** :包是中的集合是不可变的; - **scala.collection.mutable** :包中的集合是可变的; @@ -33,7 +33,7 @@ val mutableSet = collection.mutable.SortedSet(1, 2, 3, 4, 5) val immutableSet = collection.immutable.SortedSet(1, 2, 3, 4, 5) ``` -如果你仅写了`Set` 而没有加任何前缀也没有进行任何`import`,则Scala默认采用不可变集合类。 +如果你仅写了 `Set` 而没有加任何前缀也没有进行任何 `import`,则 Scala 默认采用不可变集合类。 ```scala scala> Set(1,2,3,4,5) @@ -42,31 +42,31 @@ res0: scala.collection.immutable.Set[Int] = Set(5, 1, 2, 3, 4) ### 3.1 scala.collection -scala.collection包中所有集合如下图: +scala.collection 包中所有集合如下图:
### 3.2 scala.collection.mutable -scala.collection.mutable包中所有集合如下图: +scala.collection.mutable 包中所有集合如下图:
### 3.2 scala.collection.immutable -scala.collection.immutable包中所有集合如下图: +scala.collection.immutable 包中所有集合如下图:
## 三、Trait Traversable -Scala中所有集合的顶层实现是`Traversable` 。它唯一的抽象方法是`foreach`: +Scala 中所有集合的顶层实现是 `Traversable` 。它唯一的抽象方法是 `foreach`: ```scala def foreach[U](f: Elem => U) ``` -实现`Traversable`的集合类只需要实现这个抽象方法,其他方法可以从`Traversable`继承。`Traversable`中的所有可用方法如下: +实现 `Traversable` 的集合类只需要实现这个抽象方法,其他方法可以从 `Traversable` 继承。`Traversable` 中的所有可用方法如下: | **方法** | **作用** | | ----------------------------------- | ------------------------------------------------------------ | @@ -86,7 +86,7 @@ def foreach[U](f: Elem => U) | `xs.toIndexedSeq` | 将集合转化为一个 IndexedSeq | | `xs.toStream` | 将集合转化为一个延迟计算的流 | | `xs.toSet` | 将集合转化为一个 Set | -| `xs.toMap` | 将一个(key, value)对的集合转化为一个Map。 如果当前集合的元素类型不是(key, value)对形式, 则报静态类型错误。 | +| `xs.toMap` | 将一个(key, value)对的集合转化为一个 Map。 如果当前集合的元素类型不是(key, value)对形式, 则报静态类型错误。 | | **Copying:** | | | `xs copyToBuffer buf` | 拷贝集合中所有元素到缓存 buf | | `xs copyToArray(arr,s,n)` | 从索引 s 开始,将集合中最多 n 个元素复制到数组 arr。 最后两个参数是可选的。 | @@ -105,8 +105,8 @@ def foreach[U](f: Elem => U) | `xs.tail` | 除了第一个元素之外的其他元素组成的集合 | | `xs.init` | 除了最后一个元素之外的其他元素组成的集合 | | `xs slice (from, to)` | 返回给定索引范围之内的元素组成的集合 (包含 from 位置的元素但不包含 to 位置的元素) | -| `xs take n` | 返回 xs 的前n个元素组成的集合(如果无序,则返回任意n个元素) | -| `xs drop n` | 返回 xs 的后n个元素组成的集合(如果无序,则返回任意n个元素) | +| `xs take n` | 返回 xs 的前 n 个元素组成的集合(如果无序,则返回任意 n 个元素) | +| `xs drop n` | 返回 xs 的后 n 个元素组成的集合(如果无序,则返回任意 n 个元素) | | `xs takeWhile p` | 从第一个元素开始查找满足条件 p 的元素, 直到遇到一个不满足条件的元素,返回所有遍历到的值。 | | `xs dropWhile p` | 从第一个元素开始查找满足条件 p 的元素, 直到遇到一个不满足条件的元素,返回所有未遍历到的值。 | | `xs filter p` | 返回满足条件 p 的所有元素的集合 | @@ -114,7 +114,7 @@ def foreach[U](f: Elem => U) | `xs filterNot p` | 返回不满足条件 p 的所有元素组成的集合 | | **Subdivisions:** | | | `xs splitAt n` | 在给定位置拆分集合,返回一个集合对 (xs take n, xs drop n) | -| `xs span p` | 根据给定条件拆分集合,返回一个集合对(xs takeWhile p, xs dropWhile p)。即遍历元素,直到遇到第一个不符合条件的值则结束遍历,将遍历到的值和未遍历到的值分别放入两个集合返回。 | +| `xs span p` | 根据给定条件拆分集合,返回一个集合对 (xs takeWhile p, xs dropWhile p)。即遍历元素,直到遇到第一个不符合条件的值则结束遍历,将遍历到的值和未遍历到的值分别放入两个集合返回。 | | `xs partition p` | 按照筛选条件对元素进行分组 | | `xs groupBy f` | 根据鉴别器函数 f 将 xs 划分为集合映射 | | **Element Conditions:** | | @@ -139,7 +139,7 @@ def foreach[U](f: Elem => U) | `xs.stringPrefix` | 返回 xs.toString 字符串开头的集合名称 | | **Views:** | | | `xs.view` | 生成 xs 的视图 | -| `xs view (from, to)` | 生成 xs上指定索引范围内元素的视图 | +| `xs view (from, to)` | 生成 xs 上指定索引范围内元素的视图 | @@ -169,12 +169,12 @@ res5: String = [1-2-3] ## 四、Trait Iterable -Scala中所有的集合都直接或者间接实现了`Iterable`特质,`Iterable`拓展自`Traversable`,并额外定义了部分方法: +Scala 中所有的集合都直接或者间接实现了 `Iterable` 特质,`Iterable` 拓展自 `Traversable`,并额外定义了部分方法: | **方法** | **作用** | | ---------------------- | ------------------------------------------------------------ | | **Abstract Method:** | | -| `xs.iterator` | 返回一个迭代器,用于遍历 xs 中的元素, 与foreach遍历元素的顺序相同。 | +| `xs.iterator` | 返回一个迭代器,用于遍历 xs 中的元素, 与 foreach 遍历元素的顺序相同。 | | **Other Iterators:** | | | `xs grouped size` | 返回一个固定大小的迭代器 | | `xs sliding size` | 返回一个固定大小的滑动窗口的迭代器 | @@ -238,14 +238,14 @@ res8: Boolean = false | 操作符 | 描述 | 集合类型 | | ------------------------------------------------------------ | ------------------------------------------------- | --------------------- | -| coll(k)
即coll.apply(k) | 获取指定位置的元素 | Seq, Map | +| coll(k)
即 coll.apply(k) | 获取指定位置的元素 | Seq, Map | | coll :+ elem
elem +: coll | 向集合末尾或者集合头增加元素 | Seq | | coll + elem
coll + (e1, e2, ...) | 追加元素 | Seq, Map | | coll - elem
coll - (e1, e2, ...) | 删除元素 | Set, Map, ArrayBuffer | | coll ++ coll2
coll2 ++: coll | 合并集合 | Iterable | -| coll -- coll2 | 移除coll中包含的coll2中的元素 | Set, Map, ArrayBuffer | -| elem :: lst
lst2 :: lst | 把指定列表(lst2)或者元素(elem)添加到列表(lst)头部 | List | -| list ::: list2 | 合并List | List | +| coll -- coll2 | 移除 coll 中包含的 coll2 中的元素 | Set, Map, ArrayBuffer | +| elem :: lst
lst2 :: lst | 把指定列表 (lst2) 或者元素 (elem) 添加到列表 (lst) 头部 | List | +| list ::: list2 | 合并 List | List | | set \| set2
set & set2
set &~ set2 | 并集、交集、差集 | Set | | coll += elem
coll += (e1, e2, ...)
coll ++= coll2
coll -= elem
coll -= (e1, e2, ...)
coll --= coll2 | 添加或者删除元素,并将修改后的结果赋值给集合本身 | 可变集合 | | elem +=: coll
coll2 ++=: coll | 在集合头部追加元素或集合 | ArrayBuffer | diff --git a/notes/SparkSQL_Dataset和DataFrame简介.md b/notes/SparkSQL_Dataset和DataFrame简介.md index a70dc2c..1d53216 100644 --- a/notes/SparkSQL_Dataset和DataFrame简介.md +++ b/notes/SparkSQL_Dataset和DataFrame简介.md @@ -17,13 +17,13 @@ ## 一、Spark SQL简介 -Spark SQL是Spark中的一个子模块,主要用于操作结构化数据。它具有以下特点: +Spark SQL 是 Spark 中的一个子模块,主要用于操作结构化数据。它具有以下特点: -+ 能够将SQL查询与Spark程序无缝混合,允许您使用SQL或DataFrame API对结构化数据进行查询; ++ 能够将 SQL 查询与 Spark 程序无缝混合,允许您使用 SQL 或 DataFrame API 对结构化数据进行查询; + 支持多种开发语言; -+ 支持多达上百种的外部数据源,包括Hive,Avro,Parquet,ORC,JSON和JDBC等; -+ 支持HiveQL语法以及Hive SerDes和UDF,允许你访问现有的Hive仓库; -+ 支持标准的JDBC和ODBC连接; ++ 支持多达上百种的外部数据源,包括 Hive,Avro,Parquet,ORC,JSON 和 JDBC 等; ++ 支持 HiveQL 语法以及 Hive SerDes 和 UDF,允许你访问现有的 Hive 仓库; ++ 支持标准的 JDBC 和 ODBC 连接; + 支持优化器,列式存储和代码生成等特性; + 支持扩展并能保证容错。 @@ -33,7 +33,7 @@ Spark SQL是Spark中的一个子模块,主要用于操作结构化数据。它 ### 2.1 DataFrame -为了支持结构化数据的处理,Spark SQL提供了新的数据结构DataFrame。DataFrame是一个由具名列组成的数据集。它在概念上等同于关系数据库中的表或R/Python语言中的`data frame`。 由于Spark SQL支持多种语言的开发,所以每种语言都定义了`DataFrame`的抽象,主要如下: +为了支持结构化数据的处理,Spark SQL 提供了新的数据结构 DataFrame。DataFrame 是一个由具名列组成的数据集。它在概念上等同于关系数据库中的表或 R/Python 语言中的 `data frame`。 由于 Spark SQL 支持多种语言的开发,所以每种语言都定义了 `DataFrame` 的抽象,主要如下: | 语言 | 主要抽象 | | ------ | -------------------------------------------- | @@ -44,23 +44,23 @@ Spark SQL是Spark中的一个子模块,主要用于操作结构化数据。它 ### 2.2 DataFrame 对比 RDDs -DataFrame和RDDs最主要的区别在于一个面向的是结构化数据,一个面向的是非结构化数据,它们内部的数据结构如下: +DataFrame 和 RDDs 最主要的区别在于一个面向的是结构化数据,一个面向的是非结构化数据,它们内部的数据结构如下:
-DataFrame内部的有明确Scheme结构,即列名、列字段类型都是已知的,这带来的好处是可以减少数据读取以及更好地优化执行计划,从而保证查询效率。 +DataFrame 内部的有明确 Scheme 结构,即列名、列字段类型都是已知的,这带来的好处是可以减少数据读取以及更好地优化执行计划,从而保证查询效率。 -**DataFrame和RDDs应该如何选择?** +**DataFrame 和 RDDs 应该如何选择?** -+ 如果你想使用函数式编程而不是DataFrame API,则使用RDDs; -+ 如果你的数据是非结构化的(比如流媒体或者字符流),则使用RDDs, -+ 如果你的数据是结构化的(如RDBMS中的数据)或者半结构化的(如日志),出于性能上的考虑,应优先使用DataFrame。 ++ 如果你想使用函数式编程而不是 DataFrame API,则使用 RDDs; ++ 如果你的数据是非结构化的 (比如流媒体或者字符流),则使用 RDDs, ++ 如果你的数据是结构化的 (如 RDBMS 中的数据) 或者半结构化的 (如日志),出于性能上的考虑,应优先使用 DataFrame。 ### 2.3 DataSet -Dataset也是分布式的数据集合,在Spark 1.6版本被引入,它集成了RDD和DataFrame的优点,具备强类型的特点,同时支持Lambda函数,但只能在Scala和Java语言中使用。在Spark 2.0后,为了方便开发者,Spark将DataFrame和Dataset的API融合到一起,提供了结构化的API(Structured API),即用户可以通过一套标准的API就能完成对两者的操作。 +Dataset 也是分布式的数据集合,在 Spark 1.6 版本被引入,它集成了 RDD 和 DataFrame 的优点,具备强类型的特点,同时支持 Lambda 函数,但只能在 Scala 和 Java 语言中使用。在 Spark 2.0 后,为了方便开发者,Spark 将 DataFrame 和 Dataset 的 API 融合到一起,提供了结构化的 API(Structured API),即用户可以通过一套标准的 API 就能完成对两者的操作。 -> 这里注意一下:DataFrame被标记为Untyped API,而DataSet被标记为Typed API,后文会对两者做出解释。 +> 这里注意一下:DataFrame 被标记为 Untyped API,而 DataSet 被标记为 Typed API,后文会对两者做出解释。 @@ -68,27 +68,27 @@ Dataset也是分布式的数据集合,在Spark 1.6版本被引入,它集成 ### 2.4 静态类型与运行时类型安全 -静态类型(Static-typing)与运行时类型安全(runtime type-safety) 主要表现如下: +静态类型 (Static-typing) 与运行时类型安全 (runtime type-safety) 主要表现如下: -在实际使用中,如果你用的是Spark SQL的查询语句,则直到运行时你才会发现有语法错误,而如果你用的是DataFrame和 Dataset,则在编译时就可以发现错误(这节省了开发时间和整体代价)。DataFrame和Dataset主要区别在于: +在实际使用中,如果你用的是 Spark SQL 的查询语句,则直到运行时你才会发现有语法错误,而如果你用的是 DataFrame 和 Dataset,则在编译时就可以发现错误 (这节省了开发时间和整体代价)。DataFrame 和 Dataset 主要区别在于: -在DataFrame中,当你调用了API之外的函数,编译器就会报错,但如果你使用了一个不存在的字段名字,编译器依然无法发现。而Dataset的API都是用Lambda函数和JVM类型对象表示的,所有不匹配的类型参数在编译时就会被发现。 +在 DataFrame 中,当你调用了 API 之外的函数,编译器就会报错,但如果你使用了一个不存在的字段名字,编译器依然无法发现。而 Dataset 的 API 都是用 Lambda 函数和 JVM 类型对象表示的,所有不匹配的类型参数在编译时就会被发现。 -以上这些最终都被解释成关于类型安全图谱,对应开发中的语法和分析错误。在图谱中,Dataset最严格,但对于开发者来说效率最高。 +以上这些最终都被解释成关于类型安全图谱,对应开发中的语法和分析错误。在图谱中,Dataset 最严格,但对于开发者来说效率最高。
-上面的描述可能并没有那么直观,下面的给出一个IDEA中代码编译的示例: +上面的描述可能并没有那么直观,下面的给出一个 IDEA 中代码编译的示例:
-这里一个可能的疑惑是DataFrame明明是有确定的Scheme结构(即列名、列字段类型都是已知的),但是为什么还是无法对列名进行推断和错误判断,这是因为DataFrame是Untyped的。 +这里一个可能的疑惑是 DataFrame 明明是有确定的 Scheme 结构 (即列名、列字段类型都是已知的),但是为什么还是无法对列名进行推断和错误判断,这是因为 DataFrame 是 Untyped 的。 ### 2.5 Untyped & Typed -在上面我们介绍过DataFrame API被标记为`Untyped API`,而DataSet API被标记为`Typed API`。DataFrame的`Untyped`是相对于语言或API层面而言,它确实有明确的Scheme结构,即列名,列类型都是确定的,但这些信息完全由Spark来维护,Spark只会在运行时检查这些类型和指定类型是否一致。这也就是为什么在Spark 2.0之后,官方推荐把DataFrame看做是`DatSet[Row]`,Row是Spark中定义的一个`trait`,其子类中封装了列字段的信息。 +在上面我们介绍过 DataFrame API 被标记为 `Untyped API`,而 DataSet API 被标记为 `Typed API`。DataFrame 的 `Untyped` 是相对于语言或 API 层面而言,它确实有明确的 Scheme 结构,即列名,列类型都是确定的,但这些信息完全由 Spark 来维护,Spark 只会在运行时检查这些类型和指定类型是否一致。这也就是为什么在 Spark 2.0 之后,官方推荐把 DataFrame 看做是 `DatSet[Row]`,Row 是 Spark 中定义的一个 `trait`,其子类中封装了列字段的信息。 -相对而言,DataSet是`Typed`的,即强类型。如下面代码,DataSet的类型由Case Class(Scala)或者Java Bean(Java)来明确指定的,在这里即每一行数据代表一个`Person`,这些信息由JVM来保证正确性,所以字段名错误和类型错误在编译的时候就会被IDE所发现。 +相对而言,DataSet 是 `Typed` 的,即强类型。如下面代码,DataSet 的类型由 Case Class(Scala) 或者 Java Bean(Java) 来明确指定的,在这里即每一行数据代表一个 `Person`,这些信息由 JVM 来保证正确性,所以字段名错误和类型错误在编译的时候就会被 IDE 所发现。 ```scala case class Person(name: String, age: Long) @@ -101,10 +101,10 @@ val dataSet: Dataset[Person] = spark.read.json("people.json").as[Person] 这里对三者做一下简单的总结: -+ RDDs适合非结构化数据的处理,而DataFrame & DataSet更适合结构化数据和半结构化的处理; -+ DataFrame & DataSet可以通过统一的Structured API进行访问,而RDDs则更适合函数式编程的场景; -+ 相比于DataFrame而言,DataSet是强类型的(Typed),有着更为严格的静态类型检查; -+ DataSets、DataFrames、SQL的底层都依赖了RDDs API,并对外提供结构化的访问接口。 ++ RDDs 适合非结构化数据的处理,而 DataFrame & DataSet 更适合结构化数据和半结构化的处理; ++ DataFrame & DataSet 可以通过统一的 Structured API 进行访问,而 RDDs 则更适合函数式编程的场景; ++ 相比于 DataFrame 而言,DataSet 是强类型的 (Typed),有着更为严格的静态类型检查; ++ DataSets、DataFrames、SQL 的底层都依赖了 RDDs API,并对外提供结构化的访问接口。
@@ -112,16 +112,16 @@ val dataSet: Dataset[Person] = spark.read.json("people.json").as[Person] ## 四、Spark SQL的运行原理 -DataFrame、DataSet和Spark SQL的实际执行流程都是相同的: +DataFrame、DataSet 和 Spark SQL 的实际执行流程都是相同的: -1. 进行DataFrame/Dataset/SQL编程; -2. 如果是有效的代码,即代码没有编译错误,Spark会将其转换为一个逻辑计划; -3. Spark将此逻辑计划转换为物理计划,同时进行代码优化; -4. Spark然后在集群上执行这个物理计划(基于RDD操作) 。 +1. 进行 DataFrame/Dataset/SQL 编程; +2. 如果是有效的代码,即代码没有编译错误,Spark 会将其转换为一个逻辑计划; +3. Spark 将此逻辑计划转换为物理计划,同时进行代码优化; +4. Spark 然后在集群上执行这个物理计划 (基于 RDD 操作) 。 ### 4.1 逻辑计划(Logical Plan) -执行的第一个阶段是将用户代码转换成一个逻辑计划。它首先将用户代码转换成`unresolved logical plan`(未解决的逻辑计划),之所以这个计划是未解决的,是因为尽管您的代码在语法上是正确的,但是它引用的表或列可能不存在。 Spark使用`analyzer`(分析器)基于`catalog`(存储的所有表和`DataFrames`的信息)进行解析。解析失败则拒绝执行,解析成功则将结果传给`Catalyst`优化器(`Catalyst Optimizer`),优化器是一组规则的集合,用于优化逻辑计划,通过谓词下推等方式进行优化,最终输出优化后的逻辑执行计划。 +执行的第一个阶段是将用户代码转换成一个逻辑计划。它首先将用户代码转换成 `unresolved logical plan`(未解决的逻辑计划),之所以这个计划是未解决的,是因为尽管您的代码在语法上是正确的,但是它引用的表或列可能不存在。 Spark 使用 `analyzer`(分析器) 基于 `catalog`(存储的所有表和 `DataFrames` 的信息) 进行解析。解析失败则拒绝执行,解析成功则将结果传给 `Catalyst` 优化器 (`Catalyst Optimizer`),优化器是一组规则的集合,用于优化逻辑计划,通过谓词下推等方式进行优化,最终输出优化后的逻辑执行计划。
@@ -129,13 +129,13 @@ DataFrame、DataSet和Spark SQL的实际执行流程都是相同的: ### 4.2 物理计划(Physical Plan) -得到优化后的逻辑计划后,Spark就开始了物理计划过程。 它通过生成不同的物理执行策略,并通过成本模型来比较它们,从而选择一个最优的物理计划在集群上面执行的。物理规划的输出结果是一系列的RDDs和转换关系(transformations)。 +得到优化后的逻辑计划后,Spark 就开始了物理计划过程。 它通过生成不同的物理执行策略,并通过成本模型来比较它们,从而选择一个最优的物理计划在集群上面执行的。物理规划的输出结果是一系列的 RDDs 和转换关系 (transformations)。
### 4.3 执行 -在选择一个物理计划后,Spark运行其RDDs代码,并在运行时执行进一步的优化,生成本地Java字节码,最后将运行结果返回给用户。 +在选择一个物理计划后,Spark 运行其 RDDs 代码,并在运行时执行进一步的优化,生成本地 Java 字节码,最后将运行结果返回给用户。 diff --git a/notes/SparkSQL外部数据源.md b/notes/SparkSQL外部数据源.md index 3902b46..bfd08e3 100644 --- a/notes/SparkSQL外部数据源.md +++ b/notes/SparkSQL外部数据源.md @@ -42,7 +42,7 @@ ### 1.1 多数据源支持 -Spark支持以下六个核心数据源,同时Spark社区还提供了多达上百种数据源的读取方式,能够满足绝大部分使用场景。 +Spark 支持以下六个核心数据源,同时 Spark 社区还提供了多达上百种数据源的读取方式,能够满足绝大部分使用场景。 - CSV - JSON @@ -51,11 +51,11 @@ Spark支持以下六个核心数据源,同时Spark社区还提供了多达上 - JDBC/ODBC connections - Plain-text files -> 注:以下所有测试文件均可从本仓库的[resources](https://github.com/heibaiying/BigData-Notes/tree/master/resources)目录进行下载 +> 注:以下所有测试文件均可从本仓库的[resources](https://github.com/heibaiying/BigData-Notes/tree/master/resources) 目录进行下载 ### 1.2 读数据格式 -所有读取API遵循以下调用格式: +所有读取 API 遵循以下调用格式: ```scala // 格式 @@ -64,9 +64,9 @@ DataFrameReader.format(...).option("key", "value").schema(...).load() // 示例 spark.read.format("csv") .option("mode", "FAILFAST") // 读取模式 -.option("inferSchema", "true") // 是否自动推断schema +.option("inferSchema", "true") // 是否自动推断 schema .option("path", "path/to/file(s)") // 文件路径 -.schema(someSchema) // 使用预定义的schema +.schema(someSchema) // 使用预定义的 schema .load() ``` @@ -74,7 +74,7 @@ spark.read.format("csv") | 读模式 | 描述 | | --------------- | ------------------------------------------------------------ | -| `permissive` | 当遇到损坏的记录时,将其所有字段设置为null,并将所有损坏的记录放在名为_corruption t_record的字符串列中 | +| `permissive` | 当遇到损坏的记录时,将其所有字段设置为 null,并将所有损坏的记录放在名为 _corruption t_record 的字符串列中 | | `dropMalformed` | 删除格式不正确的行 | | `failFast` | 遇到格式不正确的数据时立即失败 | @@ -105,7 +105,7 @@ dataframe.write.format("csv") ## 二、CSV -CSV是一种常见的文本文件格式,其中每一行表示一条记录,记录中的每个字段用逗号分隔。 +CSV 是一种常见的文本文件格式,其中每一行表示一条记录,记录中的每个字段用逗号分隔。 ### 2.1 读取CSV文件 @@ -115,7 +115,7 @@ CSV是一种常见的文本文件格式,其中每一行表示一条记录, spark.read.format("csv") .option("header", "false") // 文件中的第一行是否为列的名称 .option("mode", "FAILFAST") // 是否快速失败 -.option("inferSchema", "true") // 是否自动推断schema +.option("inferSchema", "true") // 是否自动推断 schema .load("/usr/file/csv/dept.csv") .show() ``` @@ -151,7 +151,7 @@ df.write.format("csv").mode("overwrite").option("sep", "\t").save("/tmp/csv/dept ### 2.3 可选配置 -为节省主文篇幅,所有读写配置项见文末9.1小节。 +为节省主文篇幅,所有读写配置项见文末 9.1 小节。
@@ -163,7 +163,7 @@ df.write.format("csv").mode("overwrite").option("sep", "\t").save("/tmp/csv/dept spark.read.format("json").option("mode", "FAILFAST").load("/usr/file/json/dept.json").show(5) ``` -需要注意的是:默认不支持一条数据记录跨越多行(如下),可以通过配置`multiLine`为`true`来进行更改,其默认值为`false`。 +需要注意的是:默认不支持一条数据记录跨越多行 (如下),可以通过配置 `multiLine` 为 `true` 来进行更改,其默认值为 `false`。 ```json // 默认支持单行 @@ -185,13 +185,13 @@ df.write.format("json").mode("overwrite").save("/tmp/spark/json/dept") ### 3.3 可选配置 -为节省主文篇幅,所有读写配置项见文末9.2小节。 +为节省主文篇幅,所有读写配置项见文末 9.2 小节。
## 四、Parquet - Parquet是一个开源的面向列的数据存储,它提供了多种存储优化,允许读取单独的列非整个文件,这不仅节省了存储空间而且提升了读取效率,它是Spark是默认的文件格式。 + Parquet 是一个开源的面向列的数据存储,它提供了多种存储优化,允许读取单独的列非整个文件,这不仅节省了存储空间而且提升了读取效率,它是 Spark 是默认的文件格式。 ### 4.1 读取Parquet文件 @@ -207,12 +207,12 @@ df.write.format("parquet").mode("overwrite").save("/tmp/spark/parquet/dept") ### 2.3 可选配置 -Parquet文件有着自己的存储规则,因此其可选配置项比较少,常用的有如下两个: +Parquet 文件有着自己的存储规则,因此其可选配置项比较少,常用的有如下两个: | 读写操作 | 配置项 | 可选值 | 默认值 | 描述 | | -------- | -------------------- | ------------------------------------------------------------ | ------------------------------------------- | ------------------------------------------------------------ | | Write | compression or codec | None,
uncompressed,
bzip2,
deflate, gzip,
lz4, or snappy | None | 压缩文件格式 | -| Read | mergeSchema | true, false | 取决于配置项`spark.sql.parquet.mergeSchema` | 当为真时,Parquet数据源将所有数据文件收集的Schema合并在一起,否则将从摘要文件中选择Schema,如果没有可用的摘要文件,则从随机数据文件中选择Schema。 | +| Read | mergeSchema | true, false | 取决于配置项 `spark.sql.parquet.mergeSchema` | 当为真时,Parquet 数据源将所有数据文件收集的 Schema 合并在一起,否则将从摘要文件中选择 Schema,如果没有可用的摘要文件,则从随机数据文件中选择 Schema。 | > 更多可选配置可以参阅官方文档:https://spark.apache.org/docs/latest/sql-data-sources-parquet.html @@ -220,7 +220,7 @@ Parquet文件有着自己的存储规则,因此其可选配置项比较少, ## 五、ORC -ORC是一种自描述的、类型感知的列文件格式,它针对大型数据的读写进行了优化,也是大数据中常用的文件格式。 +ORC 是一种自描述的、类型感知的列文件格式,它针对大型数据的读写进行了优化,也是大数据中常用的文件格式。 ### 5.1 读取ORC文件 @@ -238,11 +238,11 @@ csvFile.write.format("orc").mode("overwrite").save("/tmp/spark/orc/dept") ## 六、SQL Databases -Spark同样支持与传统的关系型数据库进行数据读写。但是Spark程序默认是没有提供数据库驱动的,所以在使用前需要将对应的数据库驱动上传到安装目录下的`jars`目录中。下面示例使用的是Mysql数据库,使用前需要将对应的`mysql-connector-java-x.x.x.jar`上传到`jars`目录下。 +Spark 同样支持与传统的关系型数据库进行数据读写。但是 Spark 程序默认是没有提供数据库驱动的,所以在使用前需要将对应的数据库驱动上传到安装目录下的 `jars` 目录中。下面示例使用的是 Mysql 数据库,使用前需要将对应的 `mysql-connector-java-x.x.x.jar` 上传到 `jars` 目录下。 ### 6.1 读取数据 -读取全表数据示例如下,这里的`help_keyword`是mysql内置的字典表,只有`help_keyword_id`和`name`两个字段。 +读取全表数据示例如下,这里的 `help_keyword` 是 mysql 内置的字典表,只有 `help_keyword_id` 和 `name` 两个字段。 ```scala spark.read @@ -319,7 +319,7 @@ spark.read.jdbc("jdbc:mysql://127.0.0.1:3306/mysql", "help_keyword", predicates, +---------------+-----------+ ``` -可以使用`numPartitions`指定读取数据的并行度: +可以使用 `numPartitions` 指定读取数据的并行度: ```scala option("numPartitions", 10) @@ -336,7 +336,7 @@ val jdbcDf = spark.read.jdbc("jdbc:mysql://127.0.0.1:3306/mysql","help_keyword", colName,lowerBound,upperBound,numPartitions,props) ``` -想要验证分区内容,可以使用`mapPartitionsWithIndex`这个算子,代码如下: +想要验证分区内容,可以使用 `mapPartitionsWithIndex` 这个算子,代码如下: ```scala jdbcDf.rdd.mapPartitionsWithIndex((index, iterator) => { @@ -348,7 +348,7 @@ jdbcDf.rdd.mapPartitionsWithIndex((index, iterator) => { }).foreach(println) ``` -执行结果如下:`help_keyword`这张表只有600条左右的数据,本来数据应该均匀分布在10个分区,但是0分区里面却有319条数据,这是因为设置了下限,所有小于300的数据都会被限制在第一个分区,即0分区。同理所有大于500的数据被分配在9分区,即最后一个分区。 +执行结果如下:`help_keyword` 这张表只有 600 条左右的数据,本来数据应该均匀分布在 10 个分区,但是 0 分区里面却有 319 条数据,这是因为设置了下限,所有小于 300 的数据都会被限制在第一个分区,即 0 分区。同理所有大于 500 的数据被分配在 9 分区,即最后一个分区。
@@ -368,7 +368,7 @@ df.write ## 七、Text -Text文件在读写性能方面并没有任何优势,且不能表达明确的数据结构,所以其使用的比较少,读写操作如下: +Text 文件在读写性能方面并没有任何优势,且不能表达明确的数据结构,所以其使用的比较少,读写操作如下: ### 7.1 读取Text数据 @@ -388,15 +388,15 @@ df.write.text("/tmp/spark/txt/dept") ### 8.1 并行读 -多个Executors不能同时读取同一个文件,但它们可以同时读取不同的文件。这意味着当您从一个包含多个文件的文件夹中读取数据时,这些文件中的每一个都将成为DataFrame中的一个分区,并由可用的Executors并行读取。 +多个 Executors 不能同时读取同一个文件,但它们可以同时读取不同的文件。这意味着当您从一个包含多个文件的文件夹中读取数据时,这些文件中的每一个都将成为 DataFrame 中的一个分区,并由可用的 Executors 并行读取。 ### 8.2 并行写 -写入的文件或数据的数量取决于写入数据时DataFrame拥有的分区数量。默认情况下,每个数据分区写一个文件。 +写入的文件或数据的数量取决于写入数据时 DataFrame 拥有的分区数量。默认情况下,每个数据分区写一个文件。 ### 8.3 分区写入 -分区和分桶这两个概念和Hive中分区表和分桶表是一致的。都是将数据按照一定规则进行拆分存储。需要注意的是`partitionBy`指定的分区和RDD中分区不是一个概念:这里的**分区表现为输出目录的子目录**,数据分别存储在对应的子目录中。 +分区和分桶这两个概念和 Hive 中分区表和分桶表是一致的。都是将数据按照一定规则进行拆分存储。需要注意的是 `partitionBy` 指定的分区和 RDD 中分区不是一个概念:这里的**分区表现为输出目录的子目录**,数据分别存储在对应的子目录中。 ```scala val df = spark.read.format("json").load("/usr/file/json/emp.json") @@ -409,7 +409,7 @@ df.write.mode("overwrite").partitionBy("deptno").save("/tmp/spark/partitions") ### 8.3 分桶写入 -分桶写入就是将数据按照指定的列和桶数进行散列,目前分桶写入只支持保存为表,实际上这就是Hive的分桶表。 +分桶写入就是将数据按照指定的列和桶数进行散列,目前分桶写入只支持保存为表,实际上这就是 Hive 的分桶表。 ```scala val numberBuckets = 10 @@ -420,12 +420,12 @@ df.write.format("parquet").mode("overwrite") ### 8.5 文件大小管理 -如果写入产生小文件数量过多,这时会产生大量的元数据开销。Spark和HDFS一样,都不能很好的处理这个问题,这被称为“small file problem”。同时数据文件也不能过大,否则在查询时会有不必要的性能开销,因此要把文件大小控制在一个合理的范围内。 +如果写入产生小文件数量过多,这时会产生大量的元数据开销。Spark 和 HDFS 一样,都不能很好的处理这个问题,这被称为“small file problem”。同时数据文件也不能过大,否则在查询时会有不必要的性能开销,因此要把文件大小控制在一个合理的范围内。 -在上文我们已经介绍过可以通过分区数量来控制生成文件的数量,从而间接控制文件大小。Spark 2.2引入了一种新的方法,以更自动化的方式控制文件大小,这就是`maxRecordsPerFile`参数,它允许你通过控制写入文件的记录数来控制文件大小。 +在上文我们已经介绍过可以通过分区数量来控制生成文件的数量,从而间接控制文件大小。Spark 2.2 引入了一种新的方法,以更自动化的方式控制文件大小,这就是 `maxRecordsPerFile` 参数,它允许你通过控制写入文件的记录数来控制文件大小。 ```scala - // Spark将确保文件最多包含5000条记录 + // Spark 将确保文件最多包含 5000 条记录 df.write.option(“maxRecordsPerFile”, 5000) ``` @@ -444,12 +444,12 @@ df.write.option(“maxRecordsPerFile”, 5000) | Read | ignoreLeadingWhiteSpace | true, false | false | 是否跳过值前面的空格 | | Both | ignoreTrailingWhiteSpace | true, false | false | 是否跳过值后面的空格 | | Both | nullValue | 任意字符 | “” | 声明文件中哪个字符表示空值 | -| Both | nanValue | 任意字符 | NaN | 声明哪个值表示NaN或者缺省值 | +| Both | nanValue | 任意字符 | NaN | 声明哪个值表示 NaN 或者缺省值 | | Both | positiveInf | 任意字符 | Inf | 正无穷 | | Both | negativeInf | 任意字符 | -Inf | 负无穷 | | Both | compression or codec | None,
uncompressed,
bzip2, deflate,
gzip, lz4, or
snappy | none | 文件压缩格式 | -| Both | dateFormat | 任何能转换为 Java的
SimpleDataFormat的字符串 | yyyy-MM-dd | 日期格式 | -| Both | timestampFormat | 任何能转换为 Java的
SimpleDataFormat的字符串 | yyyy-MMdd’T’HH:mm:ss.SSSZZ | 时间戳格式 | +| Both | dateFormat | 任何能转换为 Java 的
SimpleDataFormat 的字符串 | yyyy-MM-dd | 日期格式 | +| Both | timestampFormat | 任何能转换为 Java 的
SimpleDataFormat 的字符串 | yyyy-MMdd’T’HH:mm:ss.SSSZZ | 时间戳格式 | | Read | maxColumns | 任意整数 | 20480 | 声明文件中的最大列数 | | Read | maxCharsPerColumn | 任意整数 | 1000000 | 声明一个列中的最大字符数。 | | Read | escapeQuotes | true, false | true | 是否应该转义行中的引号。 | @@ -462,8 +462,8 @@ df.write.option(“maxRecordsPerFile”, 5000) | 读\写操作 | 配置项 | 可选值 | 默认值 | | --------- | ---------------------------------- | ------------------------------------------------------------ | -------------------------------- | | Both | compression or codec | None,
uncompressed,
bzip2, deflate,
gzip, lz4, or
snappy | none | -| Both | dateFormat | 任何能转换为 Java的 SimpleDataFormat的字符串 | yyyy-MM-dd | -| Both | timestampFormat | 任何能转换为 Java的 SimpleDataFormat的字符串 | yyyy-MMdd’T’HH:mm:ss.SSSZZ | +| Both | dateFormat | 任何能转换为 Java 的 SimpleDataFormat 的字符串 | yyyy-MM-dd | +| Both | timestampFormat | 任何能转换为 Java 的 SimpleDataFormat 的字符串 | yyyy-MMdd’T’HH:mm:ss.SSSZZ | | Read | primitiveAsString | true, false | false | | Read | allowComments | true, false | false | | Read | allowUnquotedFieldNames | true, false | false | @@ -481,10 +481,10 @@ df.write.option(“maxRecordsPerFile”, 5000) | dbtable | 表名称 | | driver | 数据库驱动 | | partitionColumn,
lowerBound, upperBoun | 分区总数,上界,下界 | -| numPartitions | 可用于表读写并行性的最大分区数。如果要写的分区数量超过这个限制,那么可以调用coalesce(numpartition)重置分区数。 | +| numPartitions | 可用于表读写并行性的最大分区数。如果要写的分区数量超过这个限制,那么可以调用 coalesce(numpartition) 重置分区数。 | | fetchsize | 每次往返要获取多少行数据。此选项仅适用于读取数据。 | -| batchsize | 每次往返插入多少行数据,这个选项只适用于写入数据。默认值是1000。 | -| isolationLevel | 事务隔离级别:可以是NONE,READ_COMMITTED, READ_UNCOMMITTED,REPEATABLE_READ或SERIALIZABLE,即标准事务隔离级别。
默认值是READ_UNCOMMITTED。这个选项只适用于数据读取。 | +| batchsize | 每次往返插入多少行数据,这个选项只适用于写入数据。默认值是 1000。 | +| isolationLevel | 事务隔离级别:可以是 NONE,READ_COMMITTED, READ_UNCOMMITTED,REPEATABLE_READ 或 SERIALIZABLE,即标准事务隔离级别。
默认值是 READ_UNCOMMITTED。这个选项只适用于数据读取。 | | createTableOptions | 写入数据时自定义创建表的相关配置 | | createTableColumnTypes | 写入数据时自定义创建列的列类型 | diff --git a/notes/SparkSQL常用聚合函数.md b/notes/SparkSQL常用聚合函数.md index e8d7d76..987d847 100644 --- a/notes/SparkSQL常用聚合函数.md +++ b/notes/SparkSQL常用聚合函数.md @@ -26,17 +26,17 @@ ### 1.1 数据准备 ```scala -// 需要导入spark sql内置的函数包 +// 需要导入 spark sql 内置的函数包 import org.apache.spark.sql.functions._ val spark = SparkSession.builder().appName("aggregations").master("local[2]").getOrCreate() val empDF = spark.read.json("/usr/file/json/emp.json") -// 注册为临时视图,用于后面演示SQL查询 +// 注册为临时视图,用于后面演示 SQL 查询 empDF.createOrReplaceTempView("emp") empDF.show() ``` -> 注:emp.json可以从本仓库的[resources](https://github.com/heibaiying/BigData-Notes/tree/master/resources)目录下载。 +> 注:emp.json 可以从本仓库的[resources](https://github.com/heibaiying/BigData-Notes/tree/master/resources) 目录下载。 ### 1.2 count @@ -54,7 +54,7 @@ empDF.select(countDistinct("deptno")).show() ### 1.4 approx_count_distinct -通常在使用大型数据集时,你可能关注的只是近似值而不是准确值,这时可以使用approx_count_distinct函数,并可以使用第二个参数指定最大允许误差。 +通常在使用大型数据集时,你可能关注的只是近似值而不是准确值,这时可以使用 approx_count_distinct 函数,并可以使用第二个参数指定最大允许误差。 ```scala empDF.select(approx_count_distinct ("ename",0.1)).show() @@ -62,7 +62,7 @@ empDF.select(approx_count_distinct ("ename",0.1)).show() ### 1.5 first & last -获取DataFrame中指定列的第一个值或者最后一个值。 +获取 DataFrame 中指定列的第一个值或者最后一个值。 ```scala empDF.select(first("ename"),last("job")).show() @@ -70,7 +70,7 @@ empDF.select(first("ename"),last("job")).show() ### 1.6 min & max -获取DataFrame中指定列的最小值或者最大值。 +获取 DataFrame 中指定列的最小值或者最大值。 ```scala empDF.select(min("sal"),max("sal")).show() @@ -95,7 +95,7 @@ empDF.select(avg("sal")).show() ### 1.9 数学函数 -Spark SQL中还支持多种数学聚合函数,用于通常的数学计算,以下是一些常用的例子: +Spark SQL 中还支持多种数学聚合函数,用于通常的数学计算,以下是一些常用的例子: ```scala // 1.计算总体方差、均方差、总体标准差、样本标准差 @@ -129,7 +129,7 @@ scala> empDF.agg(collect_set("job"), collect_list("ename")).show() ```scala empDF.groupBy("deptno", "job").count().show() -//等价SQL +//等价 SQL spark.sql("SELECT deptno, job, count(*) FROM emp GROUP BY deptno, job").show() 输出: @@ -154,7 +154,7 @@ spark.sql("SELECT deptno, job, count(*) FROM emp GROUP BY deptno, job").show() empDF.groupBy("deptno").agg(count("ename").alias("人数"), sum("sal").alias("总工资")).show() // 等价语法 empDF.groupBy("deptno").agg("ename"->"count","sal"->"sum").show() -// 等价SQL +// 等价 SQL spark.sql("SELECT deptno, count(ename) ,sum(sal) FROM emp GROUP BY deptno").show() 输出: @@ -171,10 +171,10 @@ spark.sql("SELECT deptno, count(ename) ,sum(sal) FROM emp GROUP BY deptno").show ## 三、自定义聚合函数 -Scala提供了两种自定义聚合函数的方法,分别如下: +Scala 提供了两种自定义聚合函数的方法,分别如下: -- 有类型的自定义聚合函数,主要适用于DataSet; -- 无类型的自定义聚合函数,主要适用于DataFrame。 +- 有类型的自定义聚合函数,主要适用于 DataSet; +- 无类型的自定义聚合函数,主要适用于 DataFrame。 以下分别使用两种方式来自定义一个求平均值的聚合函数,这里以计算员工平均工资为例。两种自定义方式分别如下: @@ -184,7 +184,7 @@ Scala提供了两种自定义聚合函数的方法,分别如下: import org.apache.spark.sql.expressions.Aggregator import org.apache.spark.sql.{Encoder, Encoders, SparkSession, functions} -// 1.定义员工类,对于可能存在null值的字段需要使用Option进行包装 +// 1.定义员工类,对于可能存在 null 值的字段需要使用 Option 进行包装 case class Emp(ename: String, comm: scala.Option[Double], deptno: Long, empno: Long, hiredate: String, job: String, mgr: scala.Option[Long], sal: Double) @@ -193,7 +193,7 @@ case class SumAndCount(var sum: Double, var count: Long) /* 3.自定义聚合函数 * @IN 聚合操作的输入类型 - * @BUF reduction操作输出值的类型 + * @BUF reduction 操作输出值的类型 * @OUT 聚合操作的输出类型 */ object MyAverage extends Aggregator[Emp, SumAndCount, Double] { @@ -201,14 +201,14 @@ object MyAverage extends Aggregator[Emp, SumAndCount, Double] { // 4.用于聚合操作的的初始零值 override def zero: SumAndCount = SumAndCount(0, 0) - // 5.同一分区中的reduce操作 + // 5.同一分区中的 reduce 操作 override def reduce(avg: SumAndCount, emp: Emp): SumAndCount = { avg.sum += emp.sal avg.count += 1 avg } - // 6.不同分区中的merge操作 + // 6.不同分区中的 merge 操作 override def merge(avg1: SumAndCount, avg2: SumAndCount): SumAndCount = { avg1.sum += avg2.sum avg1.count += avg2.count @@ -234,12 +234,12 @@ object SparkSqlApp { import spark.implicits._ val ds = spark.read.json("file/emp.json").as[Emp] - // 10.使用内置avg()函数和自定义函数分别进行计算,验证自定义函数是否正确 + // 10.使用内置 avg() 函数和自定义函数分别进行计算,验证自定义函数是否正确 val myAvg = ds.select(MyAverage.toColumn.name("average_sal")).first() val avg = ds.select(functions.avg(ds.col("sal"))).first().get(0) - println("自定义average函数 : " + myAvg) - println("内置的average函数 : " + avg) + println("自定义 average 函数 : " + myAvg) + println("内置的 average 函数 : " + avg) } } ``` @@ -250,10 +250,10 @@ object SparkSqlApp { -关于`zero`,`reduce`,`merge`,`finish`方法的作用在上图都有说明,这里解释一下中间类型和输出类型的编码转换,这个写法比较固定,基本上就是两种情况: +关于 `zero`,`reduce`,`merge`,`finish` 方法的作用在上图都有说明,这里解释一下中间类型和输出类型的编码转换,这个写法比较固定,基本上就是两种情况: -- 自定义类型Case Class或者元组就使用`Encoders.product`方法; -- 基本类型就使用其对应名称的方法,如`scalaByte `,`scalaFloat`,`scalaShort`等,示例如下: +- 自定义类型 Case Class 或者元组就使用 `Encoders.product` 方法; +- 基本类型就使用其对应名称的方法,如 `scalaByte `,`scalaFloat`,`scalaShort` 等,示例如下: ```scala override def bufferEncoder: Encoder[SumAndCount] = Encoders.product @@ -283,7 +283,7 @@ object MyAverage extends UserDefinedAggregateFunction { // 3.聚合操作输出参数的类型 def dataType: DataType = DoubleType - // 4.此函数是否始终在相同输入上返回相同的输出,通常为true + // 4.此函数是否始终在相同输入上返回相同的输出,通常为 true def deterministic: Boolean = true // 5.定义零值 @@ -292,7 +292,7 @@ object MyAverage extends UserDefinedAggregateFunction { buffer(1) = 0L } - // 6.同一分区中的reduce操作 + // 6.同一分区中的 reduce 操作 def update(buffer: MutableAggregationBuffer, input: Row): Unit = { if (!input.isNullAt(0)) { buffer(0) = buffer.getLong(0) + input.getLong(0) @@ -300,7 +300,7 @@ object MyAverage extends UserDefinedAggregateFunction { } } - // 7.不同分区中的merge操作 + // 7.不同分区中的 merge 操作 def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = { buffer1(0) = buffer1.getLong(0) + buffer2.getLong(0) buffer1(1) = buffer1.getLong(1) + buffer2.getLong(1) @@ -326,8 +326,8 @@ object SparkSqlApp { val myAvg = spark.sql("SELECT myAverage(sal) as avg_sal FROM emp").first() val avg = spark.sql("SELECT avg(sal) as avg_sal FROM emp").first() - println("自定义average函数 : " + myAvg) - println("内置的average函数 : " + avg) + println("自定义 average 函数 : " + myAvg) + println("内置的 average 函数 : " + avg) } } ``` diff --git a/notes/SparkSQL联结操作.md b/notes/SparkSQL联结操作.md index 7ec06f3..298d8f0 100644 --- a/notes/SparkSQL联结操作.md +++ b/notes/SparkSQL联结操作.md @@ -16,7 +16,7 @@ ## 一、 数据准备 -本文主要介绍Spark SQL的多表连接,需要预先准备测试数据。分别创建员工和部门的Datafame,并注册为临时视图,代码如下: +本文主要介绍 Spark SQL 的多表连接,需要预先准备测试数据。分别创建员工和部门的 Datafame,并注册为临时视图,代码如下: ```scala val spark = SparkSession.builder().appName("aggregations").master("local[2]").getOrCreate() @@ -31,7 +31,7 @@ deptDF.createOrReplaceTempView("dept") 两表的主要字段如下: ```properties -emp员工表 +emp 员工表 |-- ENAME: 员工姓名 |-- DEPTNO: 部门编号 |-- EMPNO: 员工编号 @@ -43,19 +43,19 @@ emp员工表 ``` ```properties -dept部门表 +dept 部门表 |-- DEPTNO: 部门编号 |-- DNAME: 部门名称 |-- LOC: 部门所在城市 ``` -> 注:emp.json,dept.json可以在本仓库的[resources](https://github.com/heibaiying/BigData-Notes/tree/master/resources)目录进行下载。 +> 注:emp.json,dept.json 可以在本仓库的[resources](https://github.com/heibaiying/BigData-Notes/tree/master/resources) 目录进行下载。 ## 二、连接类型 -Spark中支持多种连接类型: +Spark 中支持多种连接类型: + **Inner Join** : 内连接; + **Full Outer Join** : 全外连接; @@ -64,23 +64,23 @@ Spark中支持多种连接类型: + **Left Semi Join** : 左半连接; + **Left Anti Join** : 左反连接; + **Natural Join** : 自然连接; -+ **Cross (or Cartesian) Join** : 交叉(或笛卡尔)连接。 ++ **Cross (or Cartesian) Join** : 交叉 (或笛卡尔) 连接。 其中内,外连接,笛卡尔积均与普通关系型数据库中的相同,如下图所示:
-这里解释一下左半连接和左反连接,这两个连接等价于关系型数据库中的`IN`和`NOT IN`字句: +这里解释一下左半连接和左反连接,这两个连接等价于关系型数据库中的 `IN` 和 `NOT IN` 字句: ```sql -- LEFT SEMI JOIN SELECT * FROM emp LEFT SEMI JOIN dept ON emp.deptno = dept.deptno --- 等价于如下的IN语句 +-- 等价于如下的 IN 语句 SELECT * FROM emp WHERE deptno IN (SELECT deptno FROM dept) -- LEFT ANTI JOIN SELECT * FROM emp LEFT ANTI JOIN dept ON emp.deptno = dept.deptno --- 等价于如下的IN语句 +-- 等价于如下的 IN 语句 SELECT * FROM emp WHERE deptno NOT IN (SELECT deptno FROM dept) ``` @@ -94,7 +94,7 @@ val joinExpression = empDF.col("deptno") === deptDF.col("deptno") // 2.连接查询 empDF.join(deptDF,joinExpression).select("ename","dname").show() -// 等价SQL如下: +// 等价 SQL 如下: spark.sql("SELECT ename,dname FROM emp JOIN dept ON emp.deptno = dept.deptno").show() ``` @@ -148,7 +148,7 @@ spark.sql("SELECT * FROM emp CROSS JOIN dept ON emp.deptno = dept.deptno").show( spark.sql("SELECT * FROM emp NATURAL JOIN dept").show() ``` -以下是一个自然连接的查询结果,程序自动推断出使用两张表都存在的dept列进行连接,其实际等价于: +以下是一个自然连接的查询结果,程序自动推断出使用两张表都存在的 dept 列进行连接,其实际等价于: ```sql spark.sql("SELECT * FROM emp JOIN dept ON emp.deptno = dept.deptno").show() @@ -162,17 +162,17 @@ spark.sql("SELECT * FROM emp JOIN dept ON emp.deptno = dept.deptno").show() ## 三、连接的执行 -在对大表与大表之间进行连接操作时,通常都会触发`Shuffle Join`,两表的所有分区节点会进行`All-to-All`的通讯,这种查询通常比较昂贵,会对网络IO会造成比较大的负担。 +在对大表与大表之间进行连接操作时,通常都会触发 `Shuffle Join`,两表的所有分区节点会进行 `All-to-All` 的通讯,这种查询通常比较昂贵,会对网络 IO 会造成比较大的负担。
-而对于大表和小表的连接操作,Spark会在一定程度上进行优化,如果小表的数据量小于Worker Node的内存空间,Spark会考虑将小表的数据广播到每一个Worker Node,在每个工作节点内部执行连接计算,这可以降低网络的IO,但会加大每个Worker Node的CPU负担。 +而对于大表和小表的连接操作,Spark 会在一定程度上进行优化,如果小表的数据量小于 Worker Node 的内存空间,Spark 会考虑将小表的数据广播到每一个 Worker Node,在每个工作节点内部执行连接计算,这可以降低网络的 IO,但会加大每个 Worker Node 的 CPU 负担。
-是否采用广播方式进行`Join`取决于程序内部对小表的判断,如果想明确使用广播方式进行`Join`,则可以在DataFrame API 中使用`broadcast`方法指定需要广播的小表: +是否采用广播方式进行 `Join` 取决于程序内部对小表的判断,如果想明确使用广播方式进行 `Join`,则可以在 DataFrame API 中使用 `broadcast` 方法指定需要广播的小表: ```scala empDF.join(broadcast(deptDF), joinExpression).show() @@ -182,4 +182,4 @@ empDF.join(broadcast(deptDF), joinExpression).show() ## 参考资料 -1. Matei Zaharia, Bill Chambers . Spark: The Definitive Guide[M] . 2018-02 \ No newline at end of file +1. Matei Zaharia, Bill Chambers . Spark: The Definitive Guide[M] . 2018-02 diff --git a/notes/Spark_RDD.md b/notes/Spark_RDD.md index 67d38ce..245f6d7 100644 --- a/notes/Spark_RDD.md +++ b/notes/Spark_RDD.md @@ -23,15 +23,15 @@ ## 一、RDD简介 -`RDD`全称为Resilient Distributed Datasets,是Spark最基本的数据抽象,它是只读的、分区记录的集合,支持并行操作,可以由外部数据集或其他RDD转换而来,它具有以下特性: +`RDD` 全称为 Resilient Distributed Datasets,是 Spark 最基本的数据抽象,它是只读的、分区记录的集合,支持并行操作,可以由外部数据集或其他 RDD 转换而来,它具有以下特性: -+ 一个RDD由一个或者多个分区(Partitions)组成。对于RDD来说,每个分区会被一个计算任务所处理,用户可以在创建RDD时指定其分区个数,如果没有指定,则默认采用程序所分配到的CPU的核心数; -+ RDD拥有一个用于计算分区的函数compute; -+ RDD会保存彼此间的依赖关系,RDD的每次转换都会生成一个新的依赖关系,这种RDD之间的依赖关系就像流水线一样。在部分分区数据丢失后,可以通过这种依赖关系重新计算丢失的分区数据,而不是对RDD的所有分区进行重新计算; -+ Key-Value型的RDD还拥有Partitioner(分区器),用于决定数据被存储在哪个分区中,目前Spark中支持HashPartitioner(按照哈希分区)和RangeParationer(按照范围进行分区); -+ 一个优先位置列表(可选),用于存储每个分区的优先位置(prefered location)。对于一个HDFS文件来说,这个列表保存的就是每个分区所在的块的位置,按照“移动数据不如移动计算“的理念,Spark在进行任务调度的时候,会尽可能的将计算任务分配到其所要处理数据块的存储位置。 ++ 一个 RDD 由一个或者多个分区(Partitions)组成。对于 RDD 来说,每个分区会被一个计算任务所处理,用户可以在创建 RDD 时指定其分区个数,如果没有指定,则默认采用程序所分配到的 CPU 的核心数; ++ RDD 拥有一个用于计算分区的函数 compute; ++ RDD 会保存彼此间的依赖关系,RDD 的每次转换都会生成一个新的依赖关系,这种 RDD 之间的依赖关系就像流水线一样。在部分分区数据丢失后,可以通过这种依赖关系重新计算丢失的分区数据,而不是对 RDD 的所有分区进行重新计算; ++ Key-Value 型的 RDD 还拥有 Partitioner(分区器),用于决定数据被存储在哪个分区中,目前 Spark 中支持 HashPartitioner(按照哈希分区) 和 RangeParationer(按照范围进行分区); ++ 一个优先位置列表 (可选),用于存储每个分区的优先位置 (prefered location)。对于一个 HDFS 文件来说,这个列表保存的就是每个分区所在的块的位置,按照“移动数据不如移动计算“的理念,Spark 在进行任务调度的时候,会尽可能的将计算任务分配到其所要处理数据块的存储位置。 -`RDD[T]`抽象类的部分相关代码如下: +`RDD[T]` 抽象类的部分相关代码如下: ```scala // 由子类实现以计算给定分区 @@ -54,28 +54,28 @@ protected def getPreferredLocations(split: Partition): Seq[String] = Nil ## 二、创建RDD -RDD有两种创建方式,分别介绍如下: +RDD 有两种创建方式,分别介绍如下: ### 2.1 由现有集合创建 -这里使用`spark-shell`进行测试,启动命令如下: +这里使用 `spark-shell` 进行测试,启动命令如下: ```shell spark-shell --master local[4] ``` -启动`spark-shell`后,程序会自动创建应用上下文,相当于执行了下面的Scala语句: +启动 `spark-shell` 后,程序会自动创建应用上下文,相当于执行了下面的 Scala 语句: ```scala val conf = new SparkConf().setAppName("Spark shell").setMaster("local[4]") val sc = new SparkContext(conf) ``` -由现有集合创建RDD,你可以在创建时指定其分区个数,如果没有指定,则采用程序所分配到的CPU的核心数: +由现有集合创建 RDD,你可以在创建时指定其分区个数,如果没有指定,则采用程序所分配到的 CPU 的核心数: ```scala val data = Array(1, 2, 3, 4, 5) -// 由现有集合创建RDD,默认分区数为程序所分配到的CPU的核心数 +// 由现有集合创建 RDD,默认分区数为程序所分配到的 CPU 的核心数 val dataRDD = sc.parallelize(data) // 查看分区数 dataRDD.getNumPartitions @@ -89,7 +89,7 @@ val dataRDD = sc.parallelize(data,2) ### 2.2 引用外部存储系统中的数据集 -引用外部存储系统中的数据集,例如本地文件系统,HDFS,HBase或支持Hadoop InputFormat的任何数据源。 +引用外部存储系统中的数据集,例如本地文件系统,HDFS,HBase 或支持 Hadoop InputFormat 的任何数据源。 ```scala val fileRDD = sc.textFile("/usr/file/emp.txt") @@ -106,10 +106,10 @@ fileRDD.take(1) 两者都可以用来读取外部文件,但是返回格式是不同的: -+ **textFile**:其返回格式是`RDD[String]` ,返回的是就是文件内容,RDD中每一个元素对应一行数据; -+ **wholeTextFiles**:其返回格式是`RDD[(String, String)]`,元组中第一个参数是文件路径,第二个参数是文件内容; ++ **textFile**:其返回格式是 `RDD[String]` ,返回的是就是文件内容,RDD 中每一个元素对应一行数据; ++ **wholeTextFiles**:其返回格式是 `RDD[(String, String)]`,元组中第一个参数是文件路径,第二个参数是文件内容; + 两者都提供第二个参数来控制最小分区数; -+ 从HDFS上读取文件时,Spark会为每个块创建一个分区。 ++ 从 HDFS 上读取文件时,Spark 会为每个块创建一个分区。 ```scala def textFile(path: String,minPartitions: Int = defaultMinPartitions): RDD[String] = withScope {...} @@ -120,11 +120,11 @@ def wholeTextFiles(path: String,minPartitions: Int = defaultMinPartitions): RDD[ ## 三、操作RDD -RDD支持两种类型的操作:*transformations*(转换,从现有数据集创建新数据集)和 *actions*(在数据集上运行计算后将值返回到驱动程序)。RDD中的所有转换操作都是惰性的,它们只是记住这些转换操作,但不会立即执行,只有遇到 *action* 操作后才会真正的进行计算,这类似于函数式编程中的惰性求值。 +RDD 支持两种类型的操作:*transformations*(转换,从现有数据集创建新数据集)和 *actions*(在数据集上运行计算后将值返回到驱动程序)。RDD 中的所有转换操作都是惰性的,它们只是记住这些转换操作,但不会立即执行,只有遇到 *action* 操作后才会真正的进行计算,这类似于函数式编程中的惰性求值。 ```scala val list = List(1, 2, 3) -// map 是一个transformations操作,而foreach是一个actions操作 +// map 是一个 transformations 操作,而 foreach 是一个 actions 操作 sc.parallelize(list).map(_ * 10).foreach(println) // 输出: 10 20 30 ``` @@ -135,38 +135,38 @@ sc.parallelize(list).map(_ * 10).foreach(println) ### 4.1 缓存级别 -Spark速度非常快的一个原因是RDD支持缓存。成功缓存后,如果之后的操作使用到了该数据集,则直接从缓存中获取。虽然缓存也有丢失的风险,但是由于RDD之间的依赖关系,如果某个分区的缓存数据丢失,只需要重新计算该分区即可。 +Spark 速度非常快的一个原因是 RDD 支持缓存。成功缓存后,如果之后的操作使用到了该数据集,则直接从缓存中获取。虽然缓存也有丢失的风险,但是由于 RDD 之间的依赖关系,如果某个分区的缓存数据丢失,只需要重新计算该分区即可。 -Spark支持多种缓存级别 : +Spark 支持多种缓存级别 : | Storage Level
(存储级别) | Meaning(含义) | | ---------------------------------------------- | ------------------------------------------------------------ | -| `MEMORY_ONLY` | 默认的缓存级别,将 RDD以反序列化的Java对象的形式存储在 JVM 中。如果内存空间不够,则部分分区数据将不再缓存。 | -| `MEMORY_AND_DISK` | 将 RDD 以反序列化的Java对象的形式存储JVM中。如果内存空间不够,将未缓存的分区数据存储到磁盘,在需要使用这些分区时从磁盘读取。 | -| `MEMORY_ONLY_SER`
| 将 RDD 以序列化的Java对象的形式进行存储(每个分区为一个 byte 数组)。这种方式比反序列化对象节省存储空间,但在读取时会增加CPU的计算负担。仅支持Java和Scala 。 | -| `MEMORY_AND_DISK_SER`
| 类似于`MEMORY_ONLY_SER`,但是溢出的分区数据会存储到磁盘,而不是在用到它们时重新计算。仅支持Java和Scala。 | -| `DISK_ONLY` | 只在磁盘上缓存RDD | +| `MEMORY_ONLY` | 默认的缓存级别,将 RDD 以反序列化的 Java 对象的形式存储在 JVM 中。如果内存空间不够,则部分分区数据将不再缓存。 | +| `MEMORY_AND_DISK` | 将 RDD 以反序列化的 Java 对象的形式存储 JVM 中。如果内存空间不够,将未缓存的分区数据存储到磁盘,在需要使用这些分区时从磁盘读取。 | +| `MEMORY_ONLY_SER`
| 将 RDD 以序列化的 Java 对象的形式进行存储(每个分区为一个 byte 数组)。这种方式比反序列化对象节省存储空间,但在读取时会增加 CPU 的计算负担。仅支持 Java 和 Scala 。 | +| `MEMORY_AND_DISK_SER`
| 类似于 `MEMORY_ONLY_SER`,但是溢出的分区数据会存储到磁盘,而不是在用到它们时重新计算。仅支持 Java 和 Scala。 | +| `DISK_ONLY` | 只在磁盘上缓存 RDD | | `MEMORY_ONLY_2`,
`MEMORY_AND_DISK_2`, etc | 与上面的对应级别功能相同,但是会为每个分区在集群中的两个节点上建立副本。 | -| `OFF_HEAP` | 与`MEMORY_ONLY_SER`类似,但将数据存储在堆外内存中。这需要启用堆外内存。 | +| `OFF_HEAP` | 与 `MEMORY_ONLY_SER` 类似,但将数据存储在堆外内存中。这需要启用堆外内存。 | > 启动堆外内存需要配置两个参数: > -> + **spark.memory.offHeap.enabled** :是否开启堆外内存,默认值为false,需要设置为true; -> + **spark.memory.offHeap.size** : 堆外内存空间的大小,默认值为0,需要设置为正值。 +> + **spark.memory.offHeap.enabled** :是否开启堆外内存,默认值为 false,需要设置为 true; +> + **spark.memory.offHeap.size** : 堆外内存空间的大小,默认值为 0,需要设置为正值。 ### 4.2 使用缓存 -缓存数据的方法有两个:`persist`和`cache` 。`cache`内部调用的也是`persist`,它是`persist`的特殊化形式,等价于`persist(StorageLevel.MEMORY_ONLY)`。示例如下: +缓存数据的方法有两个:`persist` 和 `cache` 。`cache` 内部调用的也是 `persist`,它是 `persist` 的特殊化形式,等价于 `persist(StorageLevel.MEMORY_ONLY)`。示例如下: ```scala -// 所有存储级别均定义在StorageLevel对象中 +// 所有存储级别均定义在 StorageLevel 对象中 fileRDD.persist(StorageLevel.MEMORY_AND_DISK) fileRDD.cache() ``` ### 4.3 移除缓存 -Spark会自动监视每个节点上的缓存使用情况,并按照最近最少使用(LRU)的规则删除旧数据分区。当然,你也可以使用`RDD.unpersist()`方法进行手动删除。 +Spark 会自动监视每个节点上的缓存使用情况,并按照最近最少使用(LRU)的规则删除旧数据分区。当然,你也可以使用 `RDD.unpersist()` 方法进行手动删除。 @@ -174,7 +174,7 @@ Spark会自动监视每个节点上的缓存使用情况,并按照最近最少 ### 5.1 shuffle介绍 -在Spark中,一个任务对应一个分区,通常不会跨分区操作数据。但如果遇到`reduceByKey`等操作,Spark必须从所有分区读取数据,并查找所有键的所有值,然后汇总在一起以计算每个键的最终结果 ,这称为`Shuffle`。 +在 Spark 中,一个任务对应一个分区,通常不会跨分区操作数据。但如果遇到 `reduceByKey` 等操作,Spark 必须从所有分区读取数据,并查找所有键的所有值,然后汇总在一起以计算每个键的最终结果 ,这称为 `Shuffle`。
@@ -182,26 +182,26 @@ Spark会自动监视每个节点上的缓存使用情况,并按照最近最少 ### 5.2 Shuffle的影响 -Shuffle是一项昂贵的操作,因为它通常会跨节点操作数据,这会涉及磁盘I/O,网络I/O,和数据序列化。某些Shuffle操作还会消耗大量的堆内存,因为它们使用堆内存来临时存储需要网络传输的数据。Shuffle还会在磁盘上生成大量中间文件,从Spark 1.3开始,这些文件将被保留,直到相应的RDD不再使用并进行垃圾回收,这样做是为了避免在计算时重复创建Shuffle文件。如果应用程序长期保留对这些RDD的引用,则垃圾回收可能在很长一段时间后才会发生,这意味着长时间运行的Spark作业可能会占用大量磁盘空间,通常可以使用`spark.local.dir`参数来指定这些临时文件的存储目录。 +Shuffle 是一项昂贵的操作,因为它通常会跨节点操作数据,这会涉及磁盘 I/O,网络 I/O,和数据序列化。某些 Shuffle 操作还会消耗大量的堆内存,因为它们使用堆内存来临时存储需要网络传输的数据。Shuffle 还会在磁盘上生成大量中间文件,从 Spark 1.3 开始,这些文件将被保留,直到相应的 RDD 不再使用并进行垃圾回收,这样做是为了避免在计算时重复创建 Shuffle 文件。如果应用程序长期保留对这些 RDD 的引用,则垃圾回收可能在很长一段时间后才会发生,这意味着长时间运行的 Spark 作业可能会占用大量磁盘空间,通常可以使用 `spark.local.dir` 参数来指定这些临时文件的存储目录。 ### 5.3 导致Shuffle的操作 -由于Shuffle操作对性能的影响比较大,所以需要特别注意使用,以下操作都会导致Shuffle: +由于 Shuffle 操作对性能的影响比较大,所以需要特别注意使用,以下操作都会导致 Shuffle: -+ **涉及到重新分区操作**: 如`repartition` 和 `coalesce`; -+ **所有涉及到ByKey的操作**:如`groupByKey`和`reduceByKey`,但`countByKey`除外; -+ **联结操作**:如`cogroup`和`join`。 ++ **涉及到重新分区操作**: 如 `repartition` 和 `coalesce`; ++ **所有涉及到 ByKey 的操作**:如 `groupByKey` 和 `reduceByKey`,但 `countByKey` 除外; ++ **联结操作**:如 `cogroup` 和 `join`。 ## 五、宽依赖和窄依赖 -RDD和它的父RDD(s)之间的依赖关系分为两种不同的类型: +RDD 和它的父 RDD(s) 之间的依赖关系分为两种不同的类型: -- **窄依赖(narrow dependency)**:父RDDs的一个分区最多被子RDDs一个分区所依赖; -- **宽依赖(wide dependency)**:父RDDs的一个分区可以被子RDDs的多个子分区所依赖。 +- **窄依赖 (narrow dependency)**:父 RDDs 的一个分区最多被子 RDDs 一个分区所依赖; +- **宽依赖 (wide dependency)**:父 RDDs 的一个分区可以被子 RDDs 的多个子分区所依赖。 -如下图,每一个方框表示一个RDD,带有颜色的矩形表示分区: +如下图,每一个方框表示一个 RDD,带有颜色的矩形表示分区:
@@ -209,17 +209,17 @@ RDD和它的父RDD(s)之间的依赖关系分为两种不同的类型: 区分这两种依赖是非常有用的: -+ 首先,窄依赖允许在一个集群节点上以流水线的方式(pipeline)对父分区数据进行计算,例如先执行map操作,然后执行filter操作。而宽依赖则需要计算好所有父分区的数据,然后再在节点之间进行Shuffle,这与MapReduce类似。 -+ 窄依赖能够更有效地进行数据恢复,因为只需重新对丢失分区的父分区进行计算,且不同节点之间可以并行计算;而对于宽依赖而言,如果数据丢失,则需要对所有父分区数据进行计算并再次Shuffle。 ++ 首先,窄依赖允许在一个集群节点上以流水线的方式(pipeline)对父分区数据进行计算,例如先执行 map 操作,然后执行 filter 操作。而宽依赖则需要计算好所有父分区的数据,然后再在节点之间进行 Shuffle,这与 MapReduce 类似。 ++ 窄依赖能够更有效地进行数据恢复,因为只需重新对丢失分区的父分区进行计算,且不同节点之间可以并行计算;而对于宽依赖而言,如果数据丢失,则需要对所有父分区数据进行计算并再次 Shuffle。 ## 六、DAG的生成 -RDD(s)及其之间的依赖关系组成了DAG(有向无环图),DAG定义了这些RDD(s)之间的Lineage(血统)关系,通过血统关系,如果一个RDD的部分或者全部计算结果丢失了,也可以重新进行计算。那么Spark是如何根据DAG来生成计算任务呢?主要是根据依赖关系的不同将DAG划分为不同的计算阶段(Stage): +RDD(s) 及其之间的依赖关系组成了 DAG(有向无环图),DAG 定义了这些 RDD(s) 之间的 Lineage(血统) 关系,通过血统关系,如果一个 RDD 的部分或者全部计算结果丢失了,也可以重新进行计算。那么 Spark 是如何根据 DAG 来生成计算任务呢?主要是根据依赖关系的不同将 DAG 划分为不同的计算阶段 (Stage): + 对于窄依赖,由于分区的依赖关系是确定的,其转换操作可以在同一个线程执行,所以可以划分到同一个执行阶段; -+ 对于宽依赖,由于Shuffle的存在,只能在父RDD(s)被Shuffle处理完成后,才能开始接下来的计算,因此遇到宽依赖就需要重新划分阶段。 ++ 对于宽依赖,由于 Shuffle 的存在,只能在父 RDD(s) 被 Shuffle 处理完成后,才能开始接下来的计算,因此遇到宽依赖就需要重新划分阶段。
@@ -229,7 +229,7 @@ RDD(s)及其之间的依赖关系组成了DAG(有向无环图),DAG定义了这 ## 参考资料 -1. 张安站 . Spark技术内幕:深入解析Spark内核架构设计与实现原理[M] . 机械工业出版社 . 2015-09-01 +1. 张安站 . Spark 技术内幕:深入解析 Spark 内核架构设计与实现原理[M] . 机械工业出版社 . 2015-09-01 2. [RDD Programming Guide](https://spark.apache.org/docs/latest/rdd-programming-guide.html#rdd-programming-guide) 3. [RDD:基于内存的集群计算容错抽象](http://shiyanjun.cn/archives/744.html) diff --git a/notes/Spark_Streaming与流处理.md b/notes/Spark_Streaming与流处理.md index dac88fb..b3f731b 100644 --- a/notes/Spark_Streaming与流处理.md +++ b/notes/Spark_Streaming与流处理.md @@ -14,7 +14,7 @@ ### 1.1 静态数据处理 -在流处理之前,数据通常存储在数据库,文件系统或其他形式的存储系统中。应用程序根据需要查询数据或计算数据。这就是传统的静态数据处理架构。Hadoop采用HDFS进行数据存储,采用MapReduce进行数据查询或分析,这就是典型的静态数据处理架构。 +在流处理之前,数据通常存储在数据库,文件系统或其他形式的存储系统中。应用程序根据需要查询数据或计算数据。这就是传统的静态数据处理架构。Hadoop 采用 HDFS 进行数据存储,采用 MapReduce 进行数据查询或分析,这就是典型的静态数据处理架构。
@@ -26,7 +26,7 @@ 大多数数据都是连续的流:传感器事件,网站上的用户活动,金融交易等等 ,所有这些数据都是随着时间的推移而创建的。 -接收和发送数据流并执行应用程序或分析逻辑的系统称为**流处理器**。流处理器的基本职责是确保数据有效流动,同时具备可扩展性和容错能力,Storm和Flink就是其代表性的实现。 +接收和发送数据流并执行应用程序或分析逻辑的系统称为**流处理器**。流处理器的基本职责是确保数据有效流动,同时具备可扩展性和容错能力,Storm 和 Flink 就是其代表性的实现。
@@ -47,19 +47,19 @@ ### 2.1 简介 -Spark Streaming是Spark的一个子模块,用于快速构建可扩展,高吞吐量,高容错的流处理程序。具有以下特点: +Spark Streaming 是 Spark 的一个子模块,用于快速构建可扩展,高吞吐量,高容错的流处理程序。具有以下特点: -+ 通过高级API构建应用程序,简单易用; -+ 支持多种语言,如Java,Scala和Python; -+ 良好的容错性,Spark Streaming支持快速从失败中恢复丢失的操作状态; -+ 能够和Spark其他模块无缝集成,将流处理与批处理完美结合; -+ Spark Streaming可以从HDFS,Flume,Kafka,Twitter和ZeroMQ读取数据,也支持自定义数据源。 ++ 通过高级 API 构建应用程序,简单易用; ++ 支持多种语言,如 Java,Scala 和 Python; ++ 良好的容错性,Spark Streaming 支持快速从失败中恢复丢失的操作状态; ++ 能够和 Spark 其他模块无缝集成,将流处理与批处理完美结合; ++ Spark Streaming 可以从 HDFS,Flume,Kafka,Twitter 和 ZeroMQ 读取数据,也支持自定义数据源。
### 2.2 DStream -Spark Streaming提供称为离散流(DStream)的高级抽象,用于表示连续的数据流。 DStream可以从来自Kafka,Flume和Kinesis等数据源的输入数据流创建,也可以由其他DStream转化而来。**在内部,DStream表示为一系列RDD**。 +Spark Streaming 提供称为离散流 (DStream) 的高级抽象,用于表示连续的数据流。 DStream 可以从来自 Kafka,Flume 和 Kinesis 等数据源的输入数据流创建,也可以由其他 DStream 转化而来。**在内部,DStream 表示为一系列 RDD**。
@@ -67,7 +67,7 @@ Spark Streaming提供称为离散流(DStream)的高级抽象,用于表示连 ### 2.3 Spark & Storm & Flink -storm和Flink都是真正意义上的流计算框架,但 Spark Streaming 只是将数据流进行极小粒度的拆分,拆分为多个批处理,使得其能够得到接近于流处理的效果,但其本质上还是批处理(或微批处理)。 +storm 和 Flink 都是真正意义上的流计算框架,但 Spark Streaming 只是将数据流进行极小粒度的拆分,拆分为多个批处理,使得其能够得到接近于流处理的效果,但其本质上还是批处理(或微批处理)。 @@ -76,4 +76,4 @@ storm和Flink都是真正意义上的流计算框架,但 Spark Streaming 只 ## 参考资料 1. [Spark Streaming Programming Guide](https://spark.apache.org/docs/latest/streaming-programming-guide.html) -2. [What is stream processing?](https://www.ververica.com/what-is-stream-processing) \ No newline at end of file +2. [What is stream processing?](https://www.ververica.com/what-is-stream-processing) diff --git a/notes/Spark_Streaming基本操作.md b/notes/Spark_Streaming基本操作.md index a9bd647..09a172f 100644 --- a/notes/Spark_Streaming基本操作.md +++ b/notes/Spark_Streaming基本操作.md @@ -36,7 +36,7 @@ object NetworkWordCount { def main(args: Array[String]) { - /*指定时间间隔为5s*/ + /*指定时间间隔为 5s*/ val sparkConf = new SparkConf().setAppName("NetworkWordCount").setMaster("local[2]") val ssc = new StreamingContext(sparkConf, Seconds(5)) @@ -52,7 +52,7 @@ object NetworkWordCount { } ``` -使用本地模式启动Spark程序,然后使用`nc -lk 9999`打开端口并输入测试数据: +使用本地模式启动 Spark 程序,然后使用 `nc -lk 9999` 打开端口并输入测试数据: ```shell [root@hadoop001 ~]# nc -lk 9999 @@ -70,18 +70,18 @@ storm storm flink azkaban ### 3.1 StreamingContext -Spark Streaming编程的入口类是StreamingContext,在创建时候需要指明`sparkConf`和`batchDuration`(批次时间),Spark流处理本质是将流数据拆分为一个个批次,然后进行微批处理,`batchDuration`就是批次拆分的时间间隔。这个时间可以根据业务需求和服务器性能进行指定,如果业务要求低延迟并且服务器性能也允许,则这个时间可以指定得很短。 +Spark Streaming 编程的入口类是 StreamingContext,在创建时候需要指明 `sparkConf` 和 `batchDuration`(批次时间),Spark 流处理本质是将流数据拆分为一个个批次,然后进行微批处理,`batchDuration` 就是批次拆分的时间间隔。这个时间可以根据业务需求和服务器性能进行指定,如果业务要求低延迟并且服务器性能也允许,则这个时间可以指定得很短。 -这里需要注意的是:示例代码使用的是本地模式,配置为`local[2]`,这里不能配置为`local[1]`。这是因为对于流数据的处理,Spark必须有一个独立的Executor来接收数据,然后再由其他的Executors来处理,所以为了保证数据能够被处理,至少要有2个Executors。这里我们的程序只有一个数据流,在并行读取多个数据流的时候,也需要保证有足够的Executors来接收和处理数据。 +这里需要注意的是:示例代码使用的是本地模式,配置为 `local[2]`,这里不能配置为 `local[1]`。这是因为对于流数据的处理,Spark 必须有一个独立的 Executor 来接收数据,然后再由其他的 Executors 来处理,所以为了保证数据能够被处理,至少要有 2 个 Executors。这里我们的程序只有一个数据流,在并行读取多个数据流的时候,也需要保证有足够的 Executors 来接收和处理数据。 ### 3.2 数据源 -在示例代码中使用的是`socketTextStream`来创建基于Socket的数据流,实际上Spark还支持多种数据源,分为以下两类: +在示例代码中使用的是 `socketTextStream` 来创建基于 Socket 的数据流,实际上 Spark 还支持多种数据源,分为以下两类: -+ **基本数据源**:包括文件系统、Socket连接等; -+ **高级数据源**:包括Kafka,Flume,Kinesis等。 ++ **基本数据源**:包括文件系统、Socket 连接等; ++ **高级数据源**:包括 Kafka,Flume,Kinesis 等。 -在基本数据源中,Spark支持监听HDFS上指定目录,当有新文件加入时,会获取其文件内容作为输入流。创建方式如下: +在基本数据源中,Spark 支持监听 HDFS 上指定目录,当有新文件加入时,会获取其文件内容作为输入流。创建方式如下: ```scala // 对于文本文件,指明监听目录即可 @@ -90,13 +90,13 @@ streamingContext.textFileStream(dataDirectory) streamingContext.fileStream[KeyClass, ValueClass, InputFormatClass](dataDirectory) ``` -被监听的目录可以是具体目录,如`hdfs://host:8040/logs/`;也可以使用通配符,如`hdfs://host:8040/logs/2017/*`。 +被监听的目录可以是具体目录,如 `hdfs://host:8040/logs/`;也可以使用通配符,如 `hdfs://host:8040/logs/2017/*`。 -> 关于高级数据源的整合单独整理至:[Spark Streaming整合Flume](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Spark_Streaming整合Flume.md) 和 [Spark Streaming整合Kafka](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Spark_Streaming整合Kafka.md) +> 关于高级数据源的整合单独整理至:[Spark Streaming 整合 Flume](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Spark_Streaming 整合 Flume.md) 和 [Spark Streaming 整合 Kafka](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Spark_Streaming 整合 Kafka.md) ### 3.3 服务的启动与停止 -在示例代码中,使用`streamingContext.start()`代表启动服务,此时还要使用`streamingContext.awaitTermination()`使服务处于等待和可用的状态,直到发生异常或者手动使用`streamingContext.stop()`进行终止。 +在示例代码中,使用 `streamingContext.start()` 代表启动服务,此时还要使用 `streamingContext.awaitTermination()` 使服务处于等待和可用的状态,直到发生异常或者手动使用 `streamingContext.stop()` 进行终止。 @@ -104,13 +104,13 @@ streamingContext.fileStream[KeyClass, ValueClass, InputFormatClass](dataDirector ### 2.1 DStream与RDDs -DStream是Spark Streaming提供的基本抽象。它表示连续的数据流。在内部,DStream由一系列连续的RDD表示。所以从本质上而言,应用于DStream的任何操作都会转换为底层RDD上的操作。例如,在示例代码中flatMap算子的操作实际上是作用在每个RDDs上(如下图)。因为这个原因,所以DStream能够支持RDD大部分的*transformation*算子。 +DStream 是 Spark Streaming 提供的基本抽象。它表示连续的数据流。在内部,DStream 由一系列连续的 RDD 表示。所以从本质上而言,应用于 DStream 的任何操作都会转换为底层 RDD 上的操作。例如,在示例代码中 flatMap 算子的操作实际上是作用在每个 RDDs 上 (如下图)。因为这个原因,所以 DStream 能够支持 RDD 大部分的*transformation*算子。
### 2.2 updateStateByKey -除了能够支持RDD的算子外,DStream还有部分独有的*transformation*算子,这当中比较常用的是`updateStateByKey`。文章开头的词频统计程序,只能统计每一次输入文本中单词出现的数量,想要统计所有历史输入中单词出现的数量,可以使用`updateStateByKey`算子。代码如下: +除了能够支持 RDD 的算子外,DStream 还有部分独有的*transformation*算子,这当中比较常用的是 `updateStateByKey`。文章开头的词频统计程序,只能统计每一次输入文本中单词出现的数量,想要统计所有历史输入中单词出现的数量,可以使用 `updateStateByKey` 算子。代码如下: ```scala object NetworkWordCountV2 { @@ -119,8 +119,8 @@ object NetworkWordCountV2 { def main(args: Array[String]) { /* - * 本地测试时最好指定hadoop用户名,否则会默认使用本地电脑的用户名, - * 此时在HDFS上创建目录时可能会抛出权限不足的异常 + * 本地测试时最好指定 hadoop 用户名,否则会默认使用本地电脑的用户名, + * 此时在 HDFS 上创建目录时可能会抛出权限不足的异常 */ System.setProperty("HADOOP_USER_NAME", "root") @@ -130,7 +130,7 @@ object NetworkWordCountV2 { ssc.checkpoint("hdfs://hadoop001:8020/spark-streaming") val lines = ssc.socketTextStream("hadoop001", 9999) lines.flatMap(_.split(" ")).map(x => (x, 1)) - .updateStateByKey[Int](updateFunction _) //updateStateByKey算子 + .updateStateByKey[Int](updateFunction _) //updateStateByKey 算子 .print() ssc.start() @@ -152,7 +152,7 @@ object NetworkWordCountV2 { } ``` -使用`updateStateByKey`算子,你必须使用`ssc.checkpoint()`设置检查点,这样当使用`updateStateByKey`算子时,它会去检查点中取出上一次保存的信息,并使用自定义的`updateFunction`函数将上一次的数据和本次数据进行相加,然后返回。 +使用 `updateStateByKey` 算子,你必须使用 `ssc.checkpoint()` 设置检查点,这样当使用 `updateStateByKey` 算子时,它会去检查点中取出上一次保存的信息,并使用自定义的 `updateFunction` 函数将上一次的数据和本次数据进行相加,然后返回。 ### 2.3 启动测试 @@ -186,21 +186,21 @@ Deleting hdfs://hadoop001:8020/spark-streaming/checkpoint-1558945265000 ### 3.1 输出API -Spark Streaming支持以下输出操作: +Spark Streaming 支持以下输出操作: | Output Operation | Meaning | | :------------------------------------------ | :----------------------------------------------------------- | -| **print**() | 在运行流应用程序的driver节点上打印DStream中每个批次的前十个元素。用于开发调试。 | -| **saveAsTextFiles**(*prefix*, [*suffix*]) | 将DStream的内容保存为文本文件。每个批处理间隔的文件名基于前缀和后缀生成:“prefix-TIME_IN_MS [.suffix]”。 | -| **saveAsObjectFiles**(*prefix*, [*suffix*]) | 将DStream的内容序列化为Java对象,并保存到SequenceFiles。每个批处理间隔的文件名基于前缀和后缀生成:“prefix-TIME_IN_MS [.suffix]”。 | -| **saveAsHadoopFiles**(*prefix*, [*suffix*]) | 将DStream的内容保存为Hadoop文件。每个批处理间隔的文件名基于前缀和后缀生成:“prefix-TIME_IN_MS [.suffix]”。 | -| **foreachRDD**(*func*) | 最通用的输出方式,它将函数func应用于从流生成的每个RDD。此函数应将每个RDD中的数据推送到外部系统,例如将RDD保存到文件,或通过网络将其写入数据库。 | +| **print**() | 在运行流应用程序的 driver 节点上打印 DStream 中每个批次的前十个元素。用于开发调试。 | +| **saveAsTextFiles**(*prefix*, [*suffix*]) | 将 DStream 的内容保存为文本文件。每个批处理间隔的文件名基于前缀和后缀生成:“prefix-TIME_IN_MS [.suffix]”。 | +| **saveAsObjectFiles**(*prefix*, [*suffix*]) | 将 DStream 的内容序列化为 Java 对象,并保存到 SequenceFiles。每个批处理间隔的文件名基于前缀和后缀生成:“prefix-TIME_IN_MS [.suffix]”。 | +| **saveAsHadoopFiles**(*prefix*, [*suffix*]) | 将 DStream 的内容保存为 Hadoop 文件。每个批处理间隔的文件名基于前缀和后缀生成:“prefix-TIME_IN_MS [.suffix]”。 | +| **foreachRDD**(*func*) | 最通用的输出方式,它将函数 func 应用于从流生成的每个 RDD。此函数应将每个 RDD 中的数据推送到外部系统,例如将 RDD 保存到文件,或通过网络将其写入数据库。 | -前面的四个API都是直接调用即可,下面主要讲解通用的输出方式`foreachRDD(func)`,通过该API你可以将数据保存到任何你需要的数据源。 +前面的四个 API 都是直接调用即可,下面主要讲解通用的输出方式 `foreachRDD(func)`,通过该 API 你可以将数据保存到任何你需要的数据源。 ### 3.1 foreachRDD -这里我们使用Redis作为客户端,对文章开头示例程序进行改变,把每一次词频统计的结果写入到Redis,并利用Redis的`HINCRBY`命令来进行词频统计。这里需要导入Jedis依赖: +这里我们使用 Redis 作为客户端,对文章开头示例程序进行改变,把每一次词频统计的结果写入到 Redis,并利用 Redis 的 `HINCRBY` 命令来进行词频统计。这里需要导入 Jedis 依赖: ```xml @@ -228,7 +228,7 @@ object NetworkWordCountToRedis { /*创建文本输入流,并进行词频统计*/ val lines = ssc.socketTextStream("hadoop001", 9999) val pairs: DStream[(String, Int)] = lines.flatMap(_.split(" ")).map(x => (x, 1)).reduceByKey(_ + _) - /*保存数据到Redis*/ + /*保存数据到 Redis*/ pairs.foreachRDD { rdd => rdd.foreachPartition { partitionOfRecords => var jedis: Jedis = null @@ -250,7 +250,7 @@ object NetworkWordCountToRedis { ``` -其中`JedisPoolUtil`的代码如下: +其中 `JedisPoolUtil` 的代码如下: ```java import redis.clients.jedis.Jedis; @@ -259,7 +259,7 @@ import redis.clients.jedis.JedisPoolConfig; public class JedisPoolUtil { - /* 声明为volatile防止指令重排序 */ + /* 声明为 volatile 防止指令重排序 */ private static volatile JedisPool jedisPool = null; private static final String HOST = "localhost"; private static final int PORT = 6379; @@ -283,7 +283,7 @@ public class JedisPoolUtil { ### 3.3 代码说明 -这里将上面保存到Redis的代码单独抽取出来,并去除异常判断的部分。精简后的代码如下: +这里将上面保存到 Redis 的代码单独抽取出来,并去除异常判断的部分。精简后的代码如下: ```scala pairs.foreachRDD { rdd => @@ -295,7 +295,7 @@ pairs.foreachRDD { rdd => } ``` -这里可以看到一共使用了三次循环,分别是循环RDD,循环分区,循环每条记录,上面我们的代码是在循环分区的时候获取连接,也就是为每一个分区获取一个连接。但是这里大家可能会有疑问:为什么不在循环RDD的时候,为每一个RDD获取一个连接,这样所需要的连接数会更少。实际上这是不可行的,如果按照这种情况进行改写,如下: +这里可以看到一共使用了三次循环,分别是循环 RDD,循环分区,循环每条记录,上面我们的代码是在循环分区的时候获取连接,也就是为每一个分区获取一个连接。但是这里大家可能会有疑问:为什么不在循环 RDD 的时候,为每一个 RDD 获取一个连接,这样所需要的连接数会更少。实际上这是不可行的,如果按照这种情况进行改写,如下: ```scala pairs.foreachRDD { rdd => @@ -307,9 +307,9 @@ pairs.foreachRDD { rdd => } ``` -此时在执行时候就会抛出`Caused by: java.io.NotSerializableException: redis.clients.jedis.Jedis`,这是因为在实际计算时,Spark会将对RDD操作分解为多个Task,Task运行在具体的Worker Node上。在执行之前,Spark会对任务进行闭包,之后闭包被序列化并发送给每个Executor,而`Jedis`显然是不能被序列化的,所以会抛出异常。 +此时在执行时候就会抛出 `Caused by: java.io.NotSerializableException: redis.clients.jedis.Jedis`,这是因为在实际计算时,Spark 会将对 RDD 操作分解为多个 Task,Task 运行在具体的 Worker Node 上。在执行之前,Spark 会对任务进行闭包,之后闭包被序列化并发送给每个 Executor,而 `Jedis` 显然是不能被序列化的,所以会抛出异常。 -第二个需要注意的是ConnectionPool最好是一个静态,惰性初始化连接池 。这是因为Spark的转换操作本身就是惰性的,且没有数据流时不会触发写出操作,所以出于性能考虑,连接池应该是惰性的,因此上面`JedisPool`在初始化时采用了懒汉式单例进行惰性初始化。 +第二个需要注意的是 ConnectionPool 最好是一个静态,惰性初始化连接池 。这是因为 Spark 的转换操作本身就是惰性的,且没有数据流时不会触发写出操作,所以出于性能考虑,连接池应该是惰性的,因此上面 `JedisPool` 在初始化时采用了懒汉式单例进行惰性初始化。 ### 3.4 启动测试 @@ -323,7 +323,7 @@ hello world hello spark hive hive hadoop storm storm flink azkaban ``` -使用Redis Manager查看写入结果(如下图),可以看到与使用`updateStateByKey`算子得到的计算结果相同。 +使用 Redis Manager 查看写入结果 (如下图),可以看到与使用 `updateStateByKey` 算子得到的计算结果相同。
@@ -335,4 +335,4 @@ storm storm flink azkaban ## 参考资料 -Spark官方文档:http://spark.apache.org/docs/latest/streaming-programming-guide.html \ No newline at end of file +Spark 官方文档:http://spark.apache.org/docs/latest/streaming-programming-guide.html diff --git a/notes/Spark_Streaming整合Flume.md b/notes/Spark_Streaming整合Flume.md index 6f47061..a699d80 100644 --- a/notes/Spark_Streaming整合Flume.md +++ b/notes/Spark_Streaming整合Flume.md @@ -20,15 +20,15 @@ ## 一、简介 -Apache Flume是一个分布式,高可用的数据收集系统,可以从不同的数据源收集数据,经过聚合后发送到分布式计算框架或者存储系统中。Spark Straming提供了以下两种方式用于Flume的整合。 +Apache Flume 是一个分布式,高可用的数据收集系统,可以从不同的数据源收集数据,经过聚合后发送到分布式计算框架或者存储系统中。Spark Straming 提供了以下两种方式用于 Flume 的整合。 ## 二、推送式方法 -在推送式方法(Flume-style Push-based Approach)中,Spark Streaming程序需要对某台服务器的某个端口进行监听,Flume通过`avro Sink`将数据源源不断推送到该端口。这里以监听日志文件为例,具体整合方式如下: +在推送式方法 (Flume-style Push-based Approach) 中,Spark Streaming 程序需要对某台服务器的某个端口进行监听,Flume 通过 `avro Sink` 将数据源源不断推送到该端口。这里以监听日志文件为例,具体整合方式如下: ### 2.1 配置日志收集Flume -新建配置`netcat-memory-avro.properties`,使用`tail`命令监听文件内容变化,然后将新的文件内容通过`avro sink`发送到hadoop001这台服务器的8888端口: +新建配置 `netcat-memory-avro.properties`,使用 `tail` 命令监听文件内容变化,然后将新的文件内容通过 `avro sink` 发送到 hadoop001 这台服务器的 8888 端口: ```properties #指定agent的sources,sinks,channels @@ -57,7 +57,7 @@ a1.channels.c1.transactionCapacity = 100 ### 2.2 项目依赖 -项目采用Maven工程进行构建,主要依赖为`spark-streaming`和`spark-streaming-flume`。 +项目采用 Maven 工程进行构建,主要依赖为 `spark-streaming` 和 `spark-streaming-flume`。 ```xml @@ -72,7 +72,7 @@ a1.channels.c1.transactionCapacity = 100 spark-streaming_${scala.version} ${spark.version}
- + org.apache.spark spark-streaming-flume_${scala.version} @@ -84,7 +84,7 @@ a1.channels.c1.transactionCapacity = 100 ### 2.3 Spark Streaming接收日志数据 -调用 FlumeUtils工具类的`createStream`方法,对hadoop001的8888端口进行监听,获取到流数据并进行打印: +调用 FlumeUtils 工具类的 `createStream` 方法,对 hadoop001 的 8888 端口进行监听,获取到流数据并进行打印: ```scala import org.apache.spark.SparkConf @@ -109,9 +109,9 @@ object PushBasedWordCount { ### 2.4 项目打包 -因为Spark安装目录下是不含有`spark-streaming-flume`依赖包的,所以在提交到集群运行时候必须提供该依赖包,你可以在提交命令中使用`--jar`指定上传到服务器的该依赖包,或者使用`--packages org.apache.spark:spark-streaming-flume_2.12:2.4.3`指定依赖包的完整名称,这样程序在启动时会先去中央仓库进行下载。 +因为 Spark 安装目录下是不含有 `spark-streaming-flume` 依赖包的,所以在提交到集群运行时候必须提供该依赖包,你可以在提交命令中使用 `--jar` 指定上传到服务器的该依赖包,或者使用 `--packages org.apache.spark:spark-streaming-flume_2.12:2.4.3` 指定依赖包的完整名称,这样程序在启动时会先去中央仓库进行下载。 -这里我采用的是第三种方式:使用`maven-shade-plugin`插件进行`ALL IN ONE`打包,把所有依赖的Jar一并打入最终包中。需要注意的是`spark-streaming`包在Spark安装目录的`jars`目录中已经提供,所以不需要打入。插件配置如下: +这里我采用的是第三种方式:使用 `maven-shade-plugin` 插件进行 `ALL IN ONE` 打包,把所有依赖的 Jar 一并打入最终包中。需要注意的是 `spark-streaming` 包在 Spark 安装目录的 `jars` 目录中已经提供,所以不需要打入。插件配置如下: ```xml @@ -125,7 +125,7 @@ object PushBasedWordCount { 8 - + org.apache.maven.plugins maven-shade-plugin @@ -174,7 +174,7 @@ object PushBasedWordCount { - + org.scala-tools maven-scala-plugin @@ -204,13 +204,13 @@ object PushBasedWordCount { ``` > 本项目完整源码见:[spark-streaming-flume](https://github.com/heibaiying/BigData-Notes/tree/master/code/spark/spark-streaming-flume) -使用`mvn clean package`命令打包后会生产以下两个Jar包,提交`非original`开头的Jar即可。 +使用 `mvn clean package` 命令打包后会生产以下两个 Jar 包,提交 ` 非 original` 开头的 Jar 即可。
### 2.5 启动服务和提交作业 - 启动Flume服务: + 启动 Flume 服务: ```shell flume-ng agent \ @@ -219,7 +219,7 @@ flume-ng agent \ --name a1 -Dflume.root.logger=INFO,console ``` -提交Spark Streaming作业: +提交 Spark Streaming 作业: ```shell spark-submit \ @@ -230,11 +230,11 @@ spark-submit \ ### 2.6 测试 -这里使用`echo`命令模拟日志产生的场景,往日志文件中追加数据,然后查看程序的输出: +这里使用 `echo` 命令模拟日志产生的场景,往日志文件中追加数据,然后查看程序的输出:
-Spark Streaming程序成功接收到数据并打印输出: +Spark Streaming 程序成功接收到数据并打印输出:
@@ -242,23 +242,23 @@ Spark Streaming程序成功接收到数据并打印输出: #### 1. 启动顺序 -这里需要注意的,不论你先启动Spark程序还是Flume程序,由于两者的启动都需要一定的时间,此时先启动的程序会短暂地抛出端口拒绝连接的异常,此时不需要进行任何操作,等待两个程序都启动完成即可。 +这里需要注意的,不论你先启动 Spark 程序还是 Flume 程序,由于两者的启动都需要一定的时间,此时先启动的程序会短暂地抛出端口拒绝连接的异常,此时不需要进行任何操作,等待两个程序都启动完成即可。
#### 2. 版本一致 -最好保证用于本地开发和编译的Scala版本和Spark的Scala版本一致,至少保证大版本一致,如都是`2.11`。 +最好保证用于本地开发和编译的 Scala 版本和 Spark 的 Scala 版本一致,至少保证大版本一致,如都是 `2.11`。
## 三、拉取式方法 -拉取式方法(Pull-based Approach using a Custom Sink)是将数据推送到`SparkSink`接收器中,此时数据会保持缓冲状态,Spark Streaming定时从接收器中拉取数据。这种方式是基于事务的,即只有在Spark Streaming接收和复制数据完成后,才会删除缓存的数据。与第一种方式相比,具有更强的可靠性和容错保证。整合步骤如下: +拉取式方法 (Pull-based Approach using a Custom Sink) 是将数据推送到 `SparkSink` 接收器中,此时数据会保持缓冲状态,Spark Streaming 定时从接收器中拉取数据。这种方式是基于事务的,即只有在 Spark Streaming 接收和复制数据完成后,才会删除缓存的数据。与第一种方式相比,具有更强的可靠性和容错保证。整合步骤如下: ### 3.1 配置日志收集Flume -新建Flume配置文件`netcat-memory-sparkSink.properties`,配置和上面基本一致,只是把`a1.sinks.k1.type`的属性修改为`org.apache.spark.streaming.flume.sink.SparkSink`,即采用Spark接收器。 +新建 Flume 配置文件 `netcat-memory-sparkSink.properties`,配置和上面基本一致,只是把 `a1.sinks.k1.type` 的属性修改为 `org.apache.spark.streaming.flume.sink.SparkSink`,即采用 Spark 接收器。 ```properties #指定agent的sources,sinks,channels @@ -302,11 +302,11 @@ a1.channels.c1.transactionCapacity = 100
``` -注意:添加这两个依赖只是为了本地测试,Spark的安装目录下已经提供了这两个依赖,所以在最终打包时需要进行排除。 +注意:添加这两个依赖只是为了本地测试,Spark 的安装目录下已经提供了这两个依赖,所以在最终打包时需要进行排除。 ### 2.3 Spark Streaming接收日志数据 -这里和上面推送式方法的代码基本相同,只是将调用方法改为`createPollingStream`。 +这里和上面推送式方法的代码基本相同,只是将调用方法改为 `createPollingStream`。 ```scala import org.apache.spark.SparkConf @@ -333,7 +333,7 @@ object PullBasedWordCount { 启动和提交作业流程与上面相同,这里给出执行脚本,过程不再赘述。 -启动Flume进行日志收集: +启动 Flume 进行日志收集: ```shell flume-ng agent \ @@ -342,7 +342,7 @@ flume-ng agent \ --name a1 -Dflume.root.logger=INFO,console ``` -提交Spark Streaming作业: +提交 Spark Streaming 作业: ```shel spark-submit \ @@ -356,4 +356,4 @@ spark-submit \ ## 参考资料 - [streaming-flume-integration](https://spark.apache.org/docs/latest/streaming-flume-integration.html) -- 关于大数据应用常用的打包方式可以参见:[大数据应用常用打包方式](https://github.com/heibaiying/BigData-Notes/blob/master/notes/大数据应用常用打包方式.md) \ No newline at end of file +- 关于大数据应用常用的打包方式可以参见:[大数据应用常用打包方式](https://github.com/heibaiying/BigData-Notes/blob/master/notes/大数据应用常用打包方式.md) diff --git a/notes/Spark_Streaming整合Kafka.md b/notes/Spark_Streaming整合Kafka.md index e8e801c..d01c7de 100644 --- a/notes/Spark_Streaming整合Kafka.md +++ b/notes/Spark_Streaming整合Kafka.md @@ -15,12 +15,12 @@ ## 一、版本说明 -Spark针对Kafka的不同版本,提供了两套整合方案:`spark-streaming-kafka-0-8`和`spark-streaming-kafka-0-10`,其主要区别如下: +Spark 针对 Kafka 的不同版本,提供了两套整合方案:`spark-streaming-kafka-0-8` 和 `spark-streaming-kafka-0-10`,其主要区别如下: | | [spark-streaming-kafka-0-8](https://spark.apache.org/docs/latest/streaming-kafka-0-8-integration.html) | [spark-streaming-kafka-0-10](https://spark.apache.org/docs/latest/streaming-kafka-0-10-integration.html) | | :-------------------------------------------- | :----------------------------------------------------------- | :----------------------------------------------------------- | -| Kafka版本 | 0.8.2.1 or higher | 0.10.0 or higher | -| AP状态 | Deprecated
从Spark 2.3.0版本开始,Kafka 0.8支持已被弃用 | Stable(稳定版) | +| Kafka 版本 | 0.8.2.1 or higher | 0.10.0 or higher | +| AP 状态 | Deprecated
从 Spark 2.3.0 版本开始,Kafka 0.8 支持已被弃用 | Stable(稳定版) | | 语言支持 | Scala, Java, Python | Scala, Java | | Receiver DStream | Yes | No | | Direct DStream | Yes | Yes | @@ -28,11 +28,11 @@ Spark针对Kafka的不同版本,提供了两套整合方案:`spark-streaming | Offset Commit API(偏移量提交) | No | Yes | | Dynamic Topic Subscription
(动态主题订阅) | No | Yes | -本文使用的Kafka版本为`kafka_2.12-2.2.0`,故采用第二种方式进行整合。 +本文使用的 Kafka 版本为 `kafka_2.12-2.2.0`,故采用第二种方式进行整合。 ## 二、项目依赖 -项目采用Maven进行构建,主要依赖如下: +项目采用 Maven 进行构建,主要依赖如下: ```xml @@ -46,7 +46,7 @@ Spark针对Kafka的不同版本,提供了两套整合方案:`spark-streaming spark-streaming_${scala.version} ${spark.version}
- + org.apache.spark spark-streaming-kafka-0-10_${scala.version} @@ -59,7 +59,7 @@ Spark针对Kafka的不同版本,提供了两套整合方案:`spark-streaming ## 三、整合Kafka -通过调用`KafkaUtils`对象的`createDirectStream`方法来创建输入流,完整代码如下: +通过调用 `KafkaUtils` 对象的 `createDirectStream` 方法来创建输入流,完整代码如下: ```scala import org.apache.kafka.common.serialization.StringDeserializer @@ -81,15 +81,15 @@ object KafkaDirectStream { val kafkaParams = Map[String, Object]( /* - * 指定broker的地址清单,清单里不需要包含所有的broker地址,生产者会从给定的broker里查找其他broker的信息。 - * 不过建议至少提供两个broker的信息作为容错。 + * 指定 broker 的地址清单,清单里不需要包含所有的 broker 地址,生产者会从给定的 broker 里查找其他 broker 的信息。 + * 不过建议至少提供两个 broker 的信息作为容错。 */ "bootstrap.servers" -> "hadoop001:9092", /*键的序列化器*/ "key.deserializer" -> classOf[StringDeserializer], /*值的序列化器*/ "value.deserializer" -> classOf[StringDeserializer], - /*消费者所在分组的ID*/ + /*消费者所在分组的 ID*/ "group.id" -> "spark-streaming-group", /* * 该属性指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下该作何处理: @@ -122,7 +122,7 @@ object KafkaDirectStream { ### 3.1 ConsumerRecord -这里获得的输入流中每一个Record实际上是`ConsumerRecord `的实例,其包含了Record的所有可用信息,源码如下: +这里获得的输入流中每一个 Record 实际上是 `ConsumerRecord ` 的实例,其包含了 Record 的所有可用信息,源码如下: ```scala public class ConsumerRecord { @@ -157,15 +157,15 @@ public class ConsumerRecord { ### 3.2 生产者属性 -在示例代码中`kafkaParams`封装了Kafka消费者的属性,这些属性和Spark Streaming无关,是Kafka原生API中就有定义的。其中服务器地址、键序列化器和值序列化器是必选的,其他配置是可选的。其余可选的配置项如下: +在示例代码中 `kafkaParams` 封装了 Kafka 消费者的属性,这些属性和 Spark Streaming 无关,是 Kafka 原生 API 中就有定义的。其中服务器地址、键序列化器和值序列化器是必选的,其他配置是可选的。其余可选的配置项如下: #### 1. fetch.min.byte -消费者从服务器获取记录的最小字节数。如果可用的数据量小于设置值,broker会等待有足够的可用数据时才会把它返回给消费者。 +消费者从服务器获取记录的最小字节数。如果可用的数据量小于设置值,broker 会等待有足够的可用数据时才会把它返回给消费者。 #### 2. fetch.max.wait.ms -broker返回给消费者数据的等待时间。 +broker 返回给消费者数据的等待时间。 #### 3. max.partition.fetch.bytes @@ -184,29 +184,29 @@ broker返回给消费者数据的等待时间。 #### 6. enable.auto.commit -是否自动提交偏移量,默认值是true,为了避免出现重复数据和数据丢失,可以把它设置为false。 +是否自动提交偏移量,默认值是 true,为了避免出现重复数据和数据丢失,可以把它设置为 false。 #### 7. client.id -客户端id,服务器用来识别消息的来源。 +客户端 id,服务器用来识别消息的来源。 #### 8. max.poll.records -单次调用`poll()`方法能够返回的记录数量。 +单次调用 `poll()` 方法能够返回的记录数量。 #### 9. receive.buffer.bytes 和 send.buffer.byte -这两个参数分别指定TCP socket 接收和发送数据包缓冲区的大小,-1代表使用操作系统的默认值。 +这两个参数分别指定 TCP socket 接收和发送数据包缓冲区的大小,-1 代表使用操作系统的默认值。 ### 3.3 位置策略 -Spark Streaming中提供了如下三种位置策略,用于指定Kafka主题分区与Spark执行程序Executors之间的分配关系: +Spark Streaming 中提供了如下三种位置策略,用于指定 Kafka 主题分区与 Spark 执行程序 Executors 之间的分配关系: -+ **PreferConsistent** : 它将在所有的Executors上均匀分配分区; ++ **PreferConsistent** : 它将在所有的 Executors 上均匀分配分区; -+ **PreferBrokers** : 当Spark的Executor与Kafka Broker在同一机器上时可以选择该选项,它优先将该Broker上的首领分区分配给该机器上的Executor; ++ **PreferBrokers** : 当 Spark 的 Executor 与 Kafka Broker 在同一机器上时可以选择该选项,它优先将该 Broker 上的首领分区分配给该机器上的 Executor; + **PreferFixed** : 可以指定主题分区与特定主机的映射关系,显示地将分区分配到特定的主机,其构造器如下: ```scala @@ -223,13 +223,13 @@ def PreferFixed(hostMap: ju.Map[TopicPartition, String]): LocationStrategy = ### 3.4 订阅方式 -Spark Streaming提供了两种主题订阅方式,分别为`Subscribe`和`SubscribePattern`。后者可以使用正则匹配订阅主题的名称。其构造器分别如下: +Spark Streaming 提供了两种主题订阅方式,分别为 `Subscribe` 和 `SubscribePattern`。后者可以使用正则匹配订阅主题的名称。其构造器分别如下: ```scala /** * @param 需要订阅的主题的集合 - * @param Kafka消费者参数 - * @param offsets(可选): 在初始启动时开始的偏移量。如果没有,则将使用保存的偏移量或auto.offset.reset属性的值 + * @param Kafka 消费者参数 + * @param offsets(可选): 在初始启动时开始的偏移量。如果没有,则将使用保存的偏移量或 auto.offset.reset 属性的值 */ def Subscribe[K, V]( topics: ju.Collection[jl.String], @@ -238,8 +238,8 @@ def Subscribe[K, V]( /** * @param 需要订阅的正则 - * @param Kafka消费者参数 - * @param offsets(可选): 在初始启动时开始的偏移量。如果没有,则将使用保存的偏移量或auto.offset.reset属性的值 + * @param Kafka 消费者参数 + * @param offsets(可选): 在初始启动时开始的偏移量。如果没有,则将使用保存的偏移量或 auto.offset.reset 属性的值 */ def SubscribePattern[K, V]( pattern: ju.regex.Pattern, @@ -247,16 +247,16 @@ def SubscribePattern[K, V]( offsets: collection.Map[TopicPartition, Long]): ConsumerStrategy[K, V] = { ... } ``` -在示例代码中,我们实际上并没有指定第三个参数`offsets`,所以程序默认采用的是配置的`auto.offset.reset`属性的值latest,即在偏移量无效的情况下,消费者将从其启动之后生成的最新的记录开始读取数据。 +在示例代码中,我们实际上并没有指定第三个参数 `offsets`,所以程序默认采用的是配置的 `auto.offset.reset` 属性的值 latest,即在偏移量无效的情况下,消费者将从其启动之后生成的最新的记录开始读取数据。 ### 3.5 提交偏移量 -在示例代码中,我们将`enable.auto.commit`设置为true,代表自动提交。在某些情况下,你可能需要更高的可靠性,如在业务完全处理完成后再提交偏移量,这时候可以使用手动提交。想要进行手动提交,需要调用Kafka原生的API : +在示例代码中,我们将 `enable.auto.commit` 设置为 true,代表自动提交。在某些情况下,你可能需要更高的可靠性,如在业务完全处理完成后再提交偏移量,这时候可以使用手动提交。想要进行手动提交,需要调用 Kafka 原生的 API : + `commitSync`: 用于异步提交; + `commitAsync`:用于同步提交。 -具体提交方式可以参见:[Kafka消费者详解](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Kafka消费者详解.md) +具体提交方式可以参见:[Kafka 消费者详解](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Kafka 消费者详解.md) @@ -266,7 +266,7 @@ def SubscribePattern[K, V]( #### 1. 启动Kakfa -Kafka的运行依赖于zookeeper,需要预先启动,可以启动Kafka内置的zookeeper,也可以启动自己安装的: +Kafka 的运行依赖于 zookeeper,需要预先启动,可以启动 Kafka 内置的 zookeeper,也可以启动自己安装的: ```shell # zookeeper启动命令 @@ -276,7 +276,7 @@ bin/zkServer.sh start bin/zookeeper-server-start.sh config/zookeeper.properties ``` -启动单节点kafka用于测试: +启动单节点 kafka 用于测试: ```shell # bin/kafka-server-start.sh config/server.properties @@ -298,7 +298,7 @@ bin/kafka-topics.sh --create \ #### 3. 创建生产者 -这里创建一个Kafka生产者,用于发送测试数据: +这里创建一个 Kafka 生产者,用于发送测试数据: ```shell bin/kafka-console-producer.sh --broker-list hadoop001:9092 --topic spark-streaming-topic @@ -306,9 +306,9 @@ bin/kafka-console-producer.sh --broker-list hadoop001:9092 --topic spark-streami ### 4.2 本地模式测试 -这里我直接使用本地模式启动Spark Streaming程序。启动后使用生产者发送数据,从控制台查看结果。 +这里我直接使用本地模式启动 Spark Streaming 程序。启动后使用生产者发送数据,从控制台查看结果。 -从控制台输出中可以看到数据流已经被成功接收,由于采用`kafka-console-producer.sh`发送的数据默认是没有key的,所以key值为null。同时从输出中也可以看到在程序中指定的`groupId`和程序自动分配的`clientId`。 +从控制台输出中可以看到数据流已经被成功接收,由于采用 `kafka-console-producer.sh` 发送的数据默认是没有 key 的,所以 key 值为 null。同时从输出中也可以看到在程序中指定的 `groupId` 和程序自动分配的 `clientId`。
diff --git a/notes/Spark_Structured_API的基本使用.md b/notes/Spark_Structured_API的基本使用.md index 64281aa..089c547 100644 --- a/notes/Spark_Structured_API的基本使用.md +++ b/notes/Spark_Structured_API的基本使用.md @@ -12,18 +12,18 @@ ### 1.1 创建DataFrame -Spark中所有功能的入口点是`SparkSession`,可以使用`SparkSession.builder()`创建。创建后应用程序就可以从现有RDD,Hive表或Spark数据源创建DataFrame。示例如下: +Spark 中所有功能的入口点是 `SparkSession`,可以使用 `SparkSession.builder()` 创建。创建后应用程序就可以从现有 RDD,Hive 表或 Spark 数据源创建 DataFrame。示例如下: ```scala val spark = SparkSession.builder().appName("Spark-SQL").master("local[2]").getOrCreate() val df = spark.read.json("/usr/file/json/emp.json") df.show() -// 建议在进行spark SQL编程前导入下面的隐式转换,因为DataFrames和dataSets中很多操作都依赖了隐式转换 +// 建议在进行 spark SQL 编程前导入下面的隐式转换,因为 DataFrames 和 dataSets 中很多操作都依赖了隐式转换 import spark.implicits._ ``` -可以使用`spark-shell`进行测试,需要注意的是`spark-shell`启动后会自动创建一个名为`spark`的`SparkSession`,在命令行中可以直接引用即可: +可以使用 `spark-shell` 进行测试,需要注意的是 `spark-shell` 启动后会自动创建一个名为 `spark` 的 `SparkSession`,在命令行中可以直接引用即可:
@@ -31,7 +31,7 @@ import spark.implicits._ ### 1.2 创建Dataset -Spark支持由内部数据集和外部数据集来创建DataSet,其创建方式分别如下: +Spark 支持由内部数据集和外部数据集来创建 DataSet,其创建方式分别如下: #### 1. 由外部数据集创建 @@ -39,11 +39,11 @@ Spark支持由内部数据集和外部数据集来创建DataSet,其创建方 // 1.需要导入隐式转换 import spark.implicits._ -// 2.创建case class,等价于Java Bean +// 2.创建 case class,等价于 Java Bean case class Emp(ename: String, comm: Double, deptno: Long, empno: Long, hiredate: String, job: String, mgr: Long, sal: Double) -// 3.由外部数据集创建Datasets +// 3.由外部数据集创建 Datasets val ds = spark.read.json("/usr/file/emp.json").as[Emp] ds.show() ``` @@ -54,11 +54,11 @@ ds.show() // 1.需要导入隐式转换 import spark.implicits._ -// 2.创建case class,等价于Java Bean +// 2.创建 case class,等价于 Java Bean case class Emp(ename: String, comm: Double, deptno: Long, empno: Long, hiredate: String, job: String, mgr: Long, sal: Double) -// 3.由内部数据集创建Datasets +// 3.由内部数据集创建 Datasets val caseClassDS = Seq(Emp("ALLEN", 300.0, 30, 7499, "1981-02-20 00:00:00", "SALESMAN", 7698, 1600.0), Emp("JONES", 300.0, 30, 7499, "1981-02-20 00:00:00", "SALESMAN", 7698, 1600.0)) .toDS() @@ -69,7 +69,7 @@ caseClassDS.show() ### 1.3 由RDD创建DataFrame -Spark支持两种方式把RDD转换为DataFrame,分别是使用反射推断和指定Schema转换: +Spark 支持两种方式把 RDD 转换为 DataFrame,分别是使用反射推断和指定 Schema 转换: #### 1. 使用反射推断 @@ -80,12 +80,12 @@ import spark.implicits._ // 2.创建部门类 case class Dept(deptno: Long, dname: String, loc: String) -// 3.创建RDD并转换为dataSet +// 3.创建 RDD 并转换为 dataSet val rddToDS = spark.sparkContext .textFile("/usr/file/dept.txt") .map(_.split("\t")) .map(line => Dept(line(0).trim.toLong, line(1), line(2))) - .toDS() // 如果调用toDF()则转换为dataFrame + .toDS() // 如果调用 toDF() 则转换为 dataFrame ``` #### 2. 以编程方式指定Schema @@ -100,15 +100,15 @@ val fields = Array(StructField("deptno", LongType, nullable = true), StructField("dname", StringType, nullable = true), StructField("loc", StringType, nullable = true)) -// 2.创建schema +// 2.创建 schema val schema = StructType(fields) -// 3.创建RDD +// 3.创建 RDD val deptRDD = spark.sparkContext.textFile("/usr/file/dept.txt") val rowRDD = deptRDD.map(_.split("\t")).map(line => Row(line(0).toLong, line(1), line(2))) -// 4.将RDD转换为dataFrame +// 4.将 RDD 转换为 dataFrame val deptDF = spark.createDataFrame(rowRDD, schema) deptDF.show() ``` @@ -117,7 +117,7 @@ deptDF.show() ### 1.4 DataFrames与Datasets互相转换 -Spark提供了非常简单的转换方法用于DataFrame与Dataset间的互相转换,示例如下: +Spark 提供了非常简单的转换方法用于 DataFrame 与 Dataset 间的互相转换,示例如下: ```shell # DataFrames转Datasets @@ -135,13 +135,13 @@ res2: org.apache.spark.sql.DataFrame = [COMM: double, DEPTNO: bigint ... 6 more ### 2.1 引用列 -Spark支持多种方法来构造和引用列,最简单的是使用 `col() `或 `column() `函数。 +Spark 支持多种方法来构造和引用列,最简单的是使用 `col() ` 或 `column() ` 函数。 ```scala col("colName") column("colName") -// 对于Scala语言而言,还可以使用$"myColumn"和'myColumn这两种语法糖进行引用。 +// 对于 Scala 语言而言,还可以使用$"myColumn"和'myColumn 这两种语法糖进行引用。 df.select($"ename", $"job").show() df.select('ename, 'job).show() ``` @@ -168,7 +168,7 @@ df.drop("comm","job").show() df.withColumnRenamed("comm", "common").show() ``` -需要说明的是新增,删除,重命名列都会产生新的DataFrame,原来的DataFrame不会被改变。 +需要说明的是新增,删除,重命名列都会产生新的 DataFrame,原来的 DataFrame 不会被改变。
@@ -178,13 +178,13 @@ df.withColumnRenamed("comm", "common").show() // 1.查询员工姓名及工作 df.select($"ename", $"job").show() -// 2.filter 查询工资大于2000的员工信息 +// 2.filter 查询工资大于 2000 的员工信息 df.filter($"sal" > 2000).show() // 3.orderBy 按照部门编号降序,工资升序进行查询 df.orderBy(desc("deptno"), asc("sal")).show() -// 4.limit 查询工资最高的3名员工的信息 +// 4.limit 查询工资最高的 3 名员工的信息 df.orderBy(desc("sal")).limit(3).show() // 5.distinct 查询所有部门编号 @@ -201,19 +201,19 @@ df.groupBy("deptno").count().show() ### 4.1 Spark SQL基本使用 ```scala -// 1.首先需要将DataFrame注册为临时视图 +// 1.首先需要将 DataFrame 注册为临时视图 df.createOrReplaceTempView("emp") // 2.查询员工姓名及工作 spark.sql("SELECT ename,job FROM emp").show() -// 3.查询工资大于2000的员工信息 +// 3.查询工资大于 2000 的员工信息 spark.sql("SELECT * FROM emp where sal > 2000").show() // 4.orderBy 按照部门编号降序,工资升序进行查询 spark.sql("SELECT * FROM emp ORDER BY deptno DESC,sal ASC").show() -// 5.limit 查询工资最高的3名员工的信息 +// 5.limit 查询工资最高的 3 名员工的信息 spark.sql("SELECT * FROM emp ORDER BY sal DESC LIMIT 3").show() // 6.distinct 查询所有部门编号 @@ -225,9 +225,9 @@ spark.sql("SELECT deptno,count(ename) FROM emp group by deptno").show() ### 4.2 全局临时视图 -上面使用`createOrReplaceTempView`创建的是会话临时视图,它的生命周期仅限于会话范围,会随会话的结束而结束。 +上面使用 `createOrReplaceTempView` 创建的是会话临时视图,它的生命周期仅限于会话范围,会随会话的结束而结束。 -你也可以使用`createGlobalTempView`创建全局临时视图,全局临时视图可以在所有会话之间共享,并直到整个Spark应用程序终止后才会消失。全局临时视图被定义在内置的`global_temp`数据库下,需要使用限定名称进行引用,如`SELECT * FROM global_temp.view1`。 +你也可以使用 `createGlobalTempView` 创建全局临时视图,全局临时视图可以在所有会话之间共享,并直到整个 Spark 应用程序终止后才会消失。全局临时视图被定义在内置的 `global_temp` 数据库下,需要使用限定名称进行引用,如 `SELECT * FROM global_temp.view1`。 ```scala // 注册为全局临时视图 diff --git a/notes/Spark_Transformation和Action算子.md b/notes/Spark_Transformation和Action算子.md index a39041d..7c8d76f 100644 --- a/notes/Spark_Transformation和Action算子.md +++ b/notes/Spark_Transformation和Action算子.md @@ -27,28 +27,28 @@ ## 一、Transformation -spark常用的Transformation算子如下表: +spark 常用的 Transformation 算子如下表: -| Transformation算子 | Meaning(含义) | +| Transformation 算子 | Meaning(含义) | | ------------------------------------------------------------ | ------------------------------------------------------------ | -| **map**(*func*) | 对原RDD中每个元素运用 *func* 函数,并生成新的RDD | -| **filter**(*func*) | 对原RDD中每个元素使用*func* 函数进行过滤,并生成新的RDD | +| **map**(*func*) | 对原 RDD 中每个元素运用 *func* 函数,并生成新的 RDD | +| **filter**(*func*) | 对原 RDD 中每个元素使用*func* 函数进行过滤,并生成新的 RDD | | **flatMap**(*func*) | 与 map 类似,但是每一个输入的 item 被映射成 0 个或多个输出的 items( *func* 返回类型需要为 Seq )。 | -| **mapPartitions**(*func*) | 与 map 类似,但函数单独在RDD的每个分区上运行, *func*函数的类型为 Iterator\ => Iterator\ ,其中T是RDD的类型,即RDD[T] | +| **mapPartitions**(*func*) | 与 map 类似,但函数单独在 RDD 的每个分区上运行, *func*函数的类型为 Iterator\ => Iterator\ ,其中 T 是 RDD 的类型,即 RDD[T] | | **mapPartitionsWithIndex**(*func*) | 与 mapPartitions 类似,但 *func* 类型为 (Int, Iterator\) => Iterator\ ,其中第一个参数为分区索引 | | **sample**(*withReplacement*, *fraction*, *seed*) | 数据采样,有三个可选参数:设置是否放回(withReplacement)、采样的百分比(*fraction*)、随机数生成器的种子(seed); | -| **union**(*otherDataset*) | 合并两个RDD | -| **intersection**(*otherDataset*) | 求两个RDD的交集 | +| **union**(*otherDataset*) | 合并两个 RDD | +| **intersection**(*otherDataset*) | 求两个 RDD 的交集 | | **distinct**([*numTasks*])) | 去重 | -| **groupByKey**([*numTasks*]) | 按照key值进行分区,即在一个 (K, V) 对的 dataset 上调用时,返回一个 (K, Iterable\)
**Note:** 如果分组是为了在每一个 key 上执行聚合操作(例如,sum 或 average),此时使用 `reduceByKey` 或 `aggregateByKey` 性能会更好
**Note:** 默认情况下,并行度取决于父 RDD 的分区数。可以传入 `numTasks` 参数进行修改。 | -| **reduceByKey**(*func*, [*numTasks*]) | 按照key值进行分组,并对分组后的数据执行归约操作。 | -| **aggregateByKey**(*zeroValue*,*numPartitions*)(*seqOp*, *combOp*, [*numTasks*]) | 当调用(K,V)对的数据集时,返回(K,U)对的数据集,其中使用给定的组合函数和zeroValue聚合每个键的值。与groupByKey类似,reduce任务的数量可通过第二个参数进行配置。 | -| **sortByKey**([*ascending*], [*numTasks*]) | 按照key进行排序,其中的key需要实现Ordered特质,即可比较 | -| **join**(*otherDataset*, [*numTasks*]) | 在一个 (K, V) 和 (K, W) 类型的 dataset 上调用时,返回一个 (K, (V, W)) pairs 的 dataset,等价于内连接操作。如果想要执行外连接,可以使用`leftOuterJoin`, `rightOuterJoin` 和 `fullOuterJoin` 等算子。 | +| **groupByKey**([*numTasks*]) | 按照 key 值进行分区,即在一个 (K, V) 对的 dataset 上调用时,返回一个 (K, Iterable\)
**Note:** 如果分组是为了在每一个 key 上执行聚合操作(例如,sum 或 average),此时使用 `reduceByKey` 或 `aggregateByKey` 性能会更好
**Note:** 默认情况下,并行度取决于父 RDD 的分区数。可以传入 `numTasks` 参数进行修改。 | +| **reduceByKey**(*func*, [*numTasks*]) | 按照 key 值进行分组,并对分组后的数据执行归约操作。 | +| **aggregateByKey**(*zeroValue*,*numPartitions*)(*seqOp*, *combOp*, [*numTasks*]) | 当调用(K,V)对的数据集时,返回(K,U)对的数据集,其中使用给定的组合函数和 zeroValue 聚合每个键的值。与 groupByKey 类似,reduce 任务的数量可通过第二个参数进行配置。 | +| **sortByKey**([*ascending*], [*numTasks*]) | 按照 key 进行排序,其中的 key 需要实现 Ordered 特质,即可比较 | +| **join**(*otherDataset*, [*numTasks*]) | 在一个 (K, V) 和 (K, W) 类型的 dataset 上调用时,返回一个 (K, (V, W)) pairs 的 dataset,等价于内连接操作。如果想要执行外连接,可以使用 `leftOuterJoin`, `rightOuterJoin` 和 `fullOuterJoin` 等算子。 | | **cogroup**(*otherDataset*, [*numTasks*]) | 在一个 (K, V) 对的 dataset 上调用时,返回一个 (K, (Iterable\, Iterable\)) tuples 的 dataset。 | | **cartesian**(*otherDataset*) | 在一个 T 和 U 类型的 dataset 上调用时,返回一个 (T, U) 类型的 dataset(即笛卡尔积)。 | -| **coalesce**(*numPartitions*) | 将RDD中的分区数减少为numPartitions。 | -| **repartition**(*numPartitions*) | 随机重新调整RDD中的数据以创建更多或更少的分区,并在它们之间进行平衡。 | +| **coalesce**(*numPartitions*) | 将 RDD 中的分区数减少为 numPartitions。 | +| **repartition**(*numPartitions*) | 随机重新调整 RDD 中的数据以创建更多或更少的分区,并在它们之间进行平衡。 | | **repartitionAndSortWithinPartitions**(*partitioner*) | 根据给定的 partitioner(分区器)对 RDD 进行重新分区,并对分区中的数据按照 key 值进行排序。这比调用 `repartition` 然后再 sorting(排序)效率更高,因为它可以将排序过程推送到 shuffle 操作所在的机器。 | 下面分别给出这些算子的基本使用示例: @@ -73,7 +73,7 @@ sc.parallelize(list).filter(_ >= 10).foreach(println) ### 1.3 flatMap -`flatMap(func)`与`map`类似,但每一个输入的item会被映射成 0 个或多个输出的items( *func* 返回类型需要为`Seq`)。 +`flatMap(func)` 与 `map` 类似,但每一个输入的 item 会被映射成 0 个或多个输出的 items( *func* 返回类型需要为 `Seq`)。 ```scala val list = List(List(1, 2), List(3), List(), List(4, 5)) @@ -82,7 +82,7 @@ sc.parallelize(list).flatMap(_.toList).map(_ * 10).foreach(println) // 输出结果 : 10 20 30 40 50 ``` -flatMap 这个算子在日志分析中使用概率非常高,这里进行一下演示:拆分输入的每行数据为单个单词,并赋值为1,代表出现一次,之后按照单词分组并统计其出现总次数,代码如下: +flatMap 这个算子在日志分析中使用概率非常高,这里进行一下演示:拆分输入的每行数据为单个单词,并赋值为 1,代表出现一次,之后按照单词分组并统计其出现总次数,代码如下: ```scala val lines = List("spark flume spark", @@ -99,7 +99,7 @@ map(word=>(word,1)).reduceByKey(_+_).foreach(println) ### 1.4 mapPartitions -与 map 类似,但函数单独在RDD的每个分区上运行, *func*函数的类型为`Iterator => Iterator` (其中T是RDD的类型),即输入和输出都必须是可迭代类型。 +与 map 类似,但函数单独在 RDD 的每个分区上运行, *func*函数的类型为 `Iterator => Iterator` (其中 T 是 RDD 的类型),即输入和输出都必须是可迭代类型。 ```scala val list = List(1, 2, 3, 4, 5, 6) @@ -116,7 +116,7 @@ sc.parallelize(list, 3).mapPartitions(iterator => { ### 1.5 mapPartitionsWithIndex - 与 mapPartitions 类似,但 *func* 类型为`(Int, Iterator) => Iterator` ,其中第一个参数为分区索引。 + 与 mapPartitions 类似,但 *func* 类型为 `(Int, Iterator) => Iterator` ,其中第一个参数为分区索引。 ```scala val list = List(1, 2, 3, 4, 5, 6) @@ -128,17 +128,17 @@ sc.parallelize(list, 3).mapPartitionsWithIndex((index, iterator) => { buffer.toIterator }).foreach(println) //输出 -0分区:100 -0分区:200 -1分区:300 -1分区:400 -2分区:500 -2分区:600 +0 分区:100 +0 分区:200 +1 分区:300 +1 分区:400 +2 分区:500 +2 分区:600 ``` ### 1.6 sample - 数据采样。有三个可选参数:设置是否放回(withReplacement)、采样的百分比(fraction)、随机数生成器的种子(seed) : + 数据采样。有三个可选参数:设置是否放回 (withReplacement)、采样的百分比 (fraction)、随机数生成器的种子 (seed) : ```scala val list = List(1, 2, 3, 4, 5, 6) @@ -147,7 +147,7 @@ sc.parallelize(list).sample(withReplacement = false, fraction = 0.5).foreach(pri ### 1.7 union -合并两个RDD: +合并两个 RDD: ```scala val list1 = List(1, 2, 3) @@ -158,7 +158,7 @@ sc.parallelize(list1).union(sc.parallelize(list2)).foreach(println) ### 1.8 intersection -求两个RDD的交集: +求两个 RDD 的交集: ```scala val list1 = List(1, 2, 3, 4, 5) @@ -231,7 +231,7 @@ sc.parallelize(list02).sortBy(x=>x._2,ascending=false).foreach(println) ### 1.13 join -在一个 (K, V) 和 (K, W) 类型的 Dataset 上调用时,返回一个 (K, (V, W)) 的 Dataset,等价于内连接操作。如果想要执行外连接,可以使用`leftOuterJoin`, `rightOuterJoin` 和 `fullOuterJoin` 等算子。 +在一个 (K, V) 和 (K, W) 类型的 Dataset 上调用时,返回一个 (K, (V, W)) 的 Dataset,等价于内连接操作。如果想要执行外连接,可以使用 `leftOuterJoin`, `rightOuterJoin` 和 `fullOuterJoin` 等算子。 ```scala val list01 = List((1, "student01"), (2, "student02"), (3, "student03")) @@ -246,7 +246,7 @@ sc.parallelize(list01).join(sc.parallelize(list02)).foreach(println) ### 1.14 cogroup -在一个 (K, V) 对的 Dataset 上调用时,返回多个类型为 (K, (Iterable\, Iterable\)) 的元组所组成的Dataset。 +在一个 (K, V) 对的 Dataset 上调用时,返回多个类型为 (K, (Iterable\, Iterable\)) 的元组所组成的 Dataset。 ```scala val list01 = List((1, "a"),(1, "a"), (2, "b"), (3, "e")) @@ -254,7 +254,7 @@ val list02 = List((1, "A"), (2, "B"), (3, "E")) val list03 = List((1, "[ab]"), (2, "[bB]"), (3, "eE"),(3, "eE")) sc.parallelize(list01).cogroup(sc.parallelize(list02),sc.parallelize(list03)).foreach(println) -// 输出: 同一个RDD中的元素先按照key进行分组,然后再对不同RDD中的元素按照key进行分组 +// 输出: 同一个 RDD 中的元素先按照 key 进行分组,然后再对不同 RDD 中的元素按照 key 进行分组 (1,(CompactBuffer(a, a),CompactBuffer(A),CompactBuffer([ab]))) (3,(CompactBuffer(e),CompactBuffer(E),CompactBuffer(eE, eE))) (2,(CompactBuffer(b),CompactBuffer(B),CompactBuffer([bB]))) @@ -284,7 +284,7 @@ sc.parallelize(list1).cartesian(sc.parallelize(list2)).foreach(println) ### 1.16 aggregateByKey -当调用(K,V)对的数据集时,返回(K,U)对的数据集,其中使用给定的组合函数和zeroValue聚合每个键的值。与`groupByKey`类似,reduce任务的数量可通过第二个参数`numPartitions`进行配置。示例如下: +当调用(K,V)对的数据集时,返回(K,U)对的数据集,其中使用给定的组合函数和 zeroValue 聚合每个键的值。与 `groupByKey` 类似,reduce 任务的数量可通过第二个参数 `numPartitions` 进行配置。示例如下: ```scala // 为了清晰,以下所有参数均使用具名传参 @@ -299,11 +299,11 @@ sc.parallelize(list,numSlices = 2).aggregateByKey(zeroValue = 0,numPartitions = (spark,7) ``` -这里使用了`numSlices = 2`指定aggregateByKey父操作parallelize的分区数量为2,其执行流程如下: +这里使用了 `numSlices = 2` 指定 aggregateByKey 父操作 parallelize 的分区数量为 2,其执行流程如下:
-基于同样的执行流程,如果`numSlices = 1`,则意味着只有输入一个分区,则其最后一步combOp相当于是无效的,执行结果为: +基于同样的执行流程,如果 `numSlices = 1`,则意味着只有输入一个分区,则其最后一步 combOp 相当于是无效的,执行结果为: ```properties (hadoop,3) @@ -311,7 +311,7 @@ sc.parallelize(list,numSlices = 2).aggregateByKey(zeroValue = 0,numPartitions = (spark,4) ``` -同样的,如果每个单词对一个分区,即`numSlices = 6`,此时相当于求和操作,执行结果为: +同样的,如果每个单词对一个分区,即 `numSlices = 6`,此时相当于求和操作,执行结果为: ```properties (hadoop,5) @@ -319,7 +319,7 @@ sc.parallelize(list,numSlices = 2).aggregateByKey(zeroValue = 0,numPartitions = (spark,7) ``` -`aggregateByKey(zeroValue = 0,numPartitions = 3)`的第二个参数`numPartitions`决定的是输出RDD的分区数量,想要验证这个问题,可以对上面代码进行改写,使用`getNumPartitions`方法获取分区数量: +`aggregateByKey(zeroValue = 0,numPartitions = 3)` 的第二个参数 `numPartitions` 决定的是输出 RDD 的分区数量,想要验证这个问题,可以对上面代码进行改写,使用 `getNumPartitions` 方法获取分区数量: ```scala sc.parallelize(list,numSlices = 6).aggregateByKey(zeroValue = 0,numPartitions = 3)( @@ -332,7 +332,7 @@ sc.parallelize(list,numSlices = 6).aggregateByKey(zeroValue = 0,numPartitions = ## 二、Action -Spark常用的Action算子如下: +Spark 常用的 Action 算子如下: | Action(动作) | Meaning(含义) | | -------------------------------------------------- | ------------------------------------------------------------ | @@ -344,10 +344,10 @@ Spark常用的Action算子如下: | **takeSample**(*withReplacement*, *num*, [*seed*]) | 对一个 dataset 进行随机抽样 | | **takeOrdered**(*n*, *[ordering]*) | 按自然顺序(natural order)或自定义比较器(custom comparator)排序后返回前 *n* 个元素。只适用于小结果集,因为所有数据都会被加载到驱动程序的内存中进行排序。 | | **saveAsTextFile**(*path*) | 将 dataset 中的元素以文本文件的形式写入本地文件系统、HDFS 或其它 Hadoop 支持的文件系统中。Spark 将对每个元素调用 toString 方法,将元素转换为文本文件中的一行记录。 | -| **saveAsSequenceFile**(*path*) | 将 dataset 中的元素以Hadoop SequenceFile 的形式写入到本地文件系统、HDFS 或其它 Hadoop 支持的文件系统中。该操作要求RDD中的元素需要实现 Hadoop 的 Writable 接口。对于Scala语言而言,它可以将Spark中的基本数据类型自动隐式转换为对应Writable类型。(目前仅支持Java and Scala) | -| **saveAsObjectFile**(*path*) | 使用 Java 序列化后存储,可以使用 `SparkContext.objectFile()` 进行加载。(目前仅支持Java and Scala) | +| **saveAsSequenceFile**(*path*) | 将 dataset 中的元素以 Hadoop SequenceFile 的形式写入到本地文件系统、HDFS 或其它 Hadoop 支持的文件系统中。该操作要求 RDD 中的元素需要实现 Hadoop 的 Writable 接口。对于 Scala 语言而言,它可以将 Spark 中的基本数据类型自动隐式转换为对应 Writable 类型。(目前仅支持 Java and Scala) | +| **saveAsObjectFile**(*path*) | 使用 Java 序列化后存储,可以使用 `SparkContext.objectFile()` 进行加载。(目前仅支持 Java and Scala) | | **countByKey**() | 计算每个键出现的次数。 | -| **foreach**(*func*) | 遍历RDD中每个元素,并对其执行*fun*函数 | +| **foreach**(*func*) | 遍历 RDD 中每个元素,并对其执行*fun*函数 | ### 2.1 reduce @@ -363,7 +363,7 @@ sc.parallelize(list).reduce(_ + _) ### 2.2 takeOrdered -按自然顺序(natural order)或自定义比较器(custom comparator)排序后返回前 *n* 个元素。需要注意的是`takeOrdered`使用隐式参数进行隐式转换,以下为其源码。所以在使用自定义排序时,需要继承`Ordering[T]`实现自定义比较器,然后将其作为隐式参数引入。 +按自然顺序(natural order)或自定义比较器(custom comparator)排序后返回前 *n* 个元素。需要注意的是 `takeOrdered` 使用隐式参数进行隐式转换,以下为其源码。所以在使用自定义排序时,需要继承 `Ordering[T]` 实现自定义比较器,然后将其作为隐式参数引入。 ```scala def takeOrdered(num: Int)(implicit ord: Ordering[T]): Array[T] = withScope { @@ -374,7 +374,7 @@ def takeOrdered(num: Int)(implicit ord: Ordering[T]): Array[T] = withScope { 自定义规则排序: ```scala -// 继承Ordering[T],实现自定义比较器,按照value值的长度进行排序 +// 继承 Ordering[T],实现自定义比较器,按照 value 值的长度进行排序 class CustomOrdering extends Ordering[(Int, String)] { override def compare(x: (Int, String), y: (Int, String)): Int = if (x._2.length > y._2.length) 1 else -1 diff --git a/notes/Spark简介.md b/notes/Spark简介.md index 80a9bc1..9945ddf 100644 --- a/notes/Spark简介.md +++ b/notes/Spark简介.md @@ -1,94 +1,94 @@ -# Spark简介 - - - -## 一、简介 - -Spark于2009年诞生于加州大学伯克利分校AMPLab,2013年被捐赠给Apache软件基金会,2014年2月成为Apache的顶级项目。相对于MapReduce的批处理计算,Spark可以带来上百倍的性能提升,因此它成为继MapReduce之后,最为广泛使用的分布式计算框架。 - -## 二、特点 - -Apache Spark具有以下特点: - -+ 使用先进的DAG调度程序,查询优化器和物理执行引擎,以实现性能上的保证; -+ 多语言支持,目前支持的有Java,Scala,Python和R; -+ 提供了80多个高级API,可以轻松地构建应用程序; -+ 支持批处理,流处理和复杂的业务分析; -+ 丰富的类库支持:包括SQL,MLlib,GraphX和Spark Streaming等库,并且可以将它们无缝地进行组合; -+ 丰富的部署模式:支持本地模式和自带的集群模式,也支持在Hadoop,Mesos,Kubernetes上运行; -+ 多数据源支持:支持访问HDFS,Alluxio,Cassandra,HBase,Hive以及数百个其他数据源中的数据。 - -
- -## 三、集群架构 - -| Term(术语) | Meaning(含义) | -| --------------- | ------------------------------------------------------------ | -| Application | Spark应用程序,由集群上的一个Driver节点和多个Executor节点组成。 | -| Driver program | 主运用程序,该进程运行应用的 main() 方法并且创建 SparkContext | -| Cluster manager | 集群资源管理器(例如,Standlone Manager,Mesos,YARN) | -| Worker node | 执行计算任务的工作节点 | -| Executor | 位于工作节点上的应用进程,负责执行计算任务并且将输出数据保存到内存或者磁盘中 | -| Task | 被发送到Executor中的工作单元 | - -
- -**执行过程**: - -1. 用户程序创建SparkContext后,它会连接到集群资源管理器,集群资源管理器会为用户程序分配计算资源,并启动Executor; -2. Dirver将计算程序划分为不同的执行阶段和多个Task,之后将Task发送给Executor; -3. Executor负责执行Task,并将执行状态汇报给Driver,同时也会将当前节点资源的使用情况汇报给集群资源管理器。 - -## 四、核心组件 - -Spark基于Spark Core扩展了四个核心组件,分别用于满足不同领域的计算需求。 - -
- -### 3.1 Spark SQL - -Spark SQL主要用于结构化数据的处理。其具有以下特点: - -- 能够将SQL查询与Spark程序无缝混合,允许您使用SQL或DataFrame API对结构化数据进行查询; -- 支持多种数据源,包括Hive,Avro,Parquet,ORC,JSON和JDBC; -- 支持HiveQL语法以及用户自定义函数(UDF),允许你访问现有的Hive仓库; -- 支持标准的JDBC和ODBC连接; -- 支持优化器,列式存储和代码生成等特性,以提高查询效率。 - -### 3.2 Spark Streaming - -Spark Streaming主要用于快速构建可扩展,高吞吐量,高容错的流处理程序。支持从HDFS,Flume,Kafka,Twitter和ZeroMQ读取数据,并进行处理。 - -
- - Spark Streaming的本质是微批处理,它将数据流进行极小粒度的拆分,拆分为多个批处理,从而达到接近于流处理的效果。 - -
- - - -### 3.3 MLlib - -MLlib是Spark的机器学习库。其设计目标是使得机器学习变得简单且可扩展。它提供了以下工具: - -+ **常见的机器学习算法**:如分类,回归,聚类和协同过滤; -+ **特征化**:特征提取,转换,降维和选择; -+ **管道**:用于构建,评估和调整ML管道的工具; -+ **持久性**:保存和加载算法,模型,管道数据; -+ **实用工具**:线性代数,统计,数据处理等。 - -### 3.4 Graphx - -GraphX是Spark中用于图形计算和图形并行计算的新组件。在高层次上,GraphX通过引入一个新的图形抽象来扩展 RDD(一种具有附加到每个顶点和边缘的属性的定向多重图形)。为了支持图计算,GraphX提供了一组基本运算符(如: subgraph,joinVertices 和 aggregateMessages)以及优化后的Pregel API。此外,GraphX还包括越来越多的图形算法和构建器,以简化图形分析任务。 - -## +# Spark简介 + + + +## 一、简介 + +Spark 于 2009 年诞生于加州大学伯克利分校 AMPLab,2013 年被捐赠给 Apache 软件基金会,2014 年 2 月成为 Apache 的顶级项目。相对于 MapReduce 的批处理计算,Spark 可以带来上百倍的性能提升,因此它成为继 MapReduce 之后,最为广泛使用的分布式计算框架。 + +## 二、特点 + +Apache Spark 具有以下特点: + ++ 使用先进的 DAG 调度程序,查询优化器和物理执行引擎,以实现性能上的保证; ++ 多语言支持,目前支持的有 Java,Scala,Python 和 R; ++ 提供了 80 多个高级 API,可以轻松地构建应用程序; ++ 支持批处理,流处理和复杂的业务分析; ++ 丰富的类库支持:包括 SQL,MLlib,GraphX 和 Spark Streaming 等库,并且可以将它们无缝地进行组合; ++ 丰富的部署模式:支持本地模式和自带的集群模式,也支持在 Hadoop,Mesos,Kubernetes 上运行; ++ 多数据源支持:支持访问 HDFS,Alluxio,Cassandra,HBase,Hive 以及数百个其他数据源中的数据。 + +
+ +## 三、集群架构 + +| Term(术语) | Meaning(含义) | +| --------------- | ------------------------------------------------------------ | +| Application | Spark 应用程序,由集群上的一个 Driver 节点和多个 Executor 节点组成。 | +| Driver program | 主运用程序,该进程运行应用的 main() 方法并且创建 SparkContext | +| Cluster manager | 集群资源管理器(例如,Standlone Manager,Mesos,YARN) | +| Worker node | 执行计算任务的工作节点 | +| Executor | 位于工作节点上的应用进程,负责执行计算任务并且将输出数据保存到内存或者磁盘中 | +| Task | 被发送到 Executor 中的工作单元 | + +
+ +**执行过程**: + +1. 用户程序创建 SparkContext 后,它会连接到集群资源管理器,集群资源管理器会为用户程序分配计算资源,并启动 Executor; +2. Dirver 将计算程序划分为不同的执行阶段和多个 Task,之后将 Task 发送给 Executor; +3. Executor 负责执行 Task,并将执行状态汇报给 Driver,同时也会将当前节点资源的使用情况汇报给集群资源管理器。 + +## 四、核心组件 + +Spark 基于 Spark Core 扩展了四个核心组件,分别用于满足不同领域的计算需求。 + +
+ +### 3.1 Spark SQL + +Spark SQL 主要用于结构化数据的处理。其具有以下特点: + +- 能够将 SQL 查询与 Spark 程序无缝混合,允许您使用 SQL 或 DataFrame API 对结构化数据进行查询; +- 支持多种数据源,包括 Hive,Avro,Parquet,ORC,JSON 和 JDBC; +- 支持 HiveQL 语法以及用户自定义函数 (UDF),允许你访问现有的 Hive 仓库; +- 支持标准的 JDBC 和 ODBC 连接; +- 支持优化器,列式存储和代码生成等特性,以提高查询效率。 + +### 3.2 Spark Streaming + +Spark Streaming 主要用于快速构建可扩展,高吞吐量,高容错的流处理程序。支持从 HDFS,Flume,Kafka,Twitter 和 ZeroMQ 读取数据,并进行处理。 + +
+ + Spark Streaming 的本质是微批处理,它将数据流进行极小粒度的拆分,拆分为多个批处理,从而达到接近于流处理的效果。 + +
+ + + +### 3.3 MLlib + +MLlib 是 Spark 的机器学习库。其设计目标是使得机器学习变得简单且可扩展。它提供了以下工具: + ++ **常见的机器学习算法**:如分类,回归,聚类和协同过滤; ++ **特征化**:特征提取,转换,降维和选择; ++ **管道**:用于构建,评估和调整 ML 管道的工具; ++ **持久性**:保存和加载算法,模型,管道数据; ++ **实用工具**:线性代数,统计,数据处理等。 + +### 3.4 Graphx + +GraphX 是 Spark 中用于图形计算和图形并行计算的新组件。在高层次上,GraphX 通过引入一个新的图形抽象来扩展 RDD(一种具有附加到每个顶点和边缘的属性的定向多重图形)。为了支持图计算,GraphX 提供了一组基本运算符(如: subgraph,joinVertices 和 aggregateMessages)以及优化后的 Pregel API。此外,GraphX 还包括越来越多的图形算法和构建器,以简化图形分析任务。 + +## diff --git a/notes/Spark累加器与广播变量.md b/notes/Spark累加器与广播变量.md index 14bcaea..0715f4a 100644 --- a/notes/Spark累加器与广播变量.md +++ b/notes/Spark累加器与广播变量.md @@ -1,105 +1,105 @@ -# Spark 累加器与广播变量 - - - -## 一、简介 - -在Spark中,提供了两种类型的共享变量:累加器(accumulator)与广播变量(broadcast variable): - -+ **累加器**:用来对信息进行聚合,主要用于累计计数等场景; -+ **广播变量**:主要用于在节点间高效分发大对象。 - -## 二、累加器 - -这里先看一个具体的场景,对于正常的累计求和,如果在集群模式中使用下面的代码进行计算,会发现执行结果并非预期: - -```scala -var counter = 0 -val data = Array(1, 2, 3, 4, 5) -sc.parallelize(data).foreach(x => counter += x) - println(counter) -``` - -counter最后的结果是0,导致这个问题的主要原因是闭包。 - -
- - - -### 2.1 理解闭包 - -**1. Scala中闭包的概念** - -这里先介绍一下Scala中关于闭包的概念: - -``` -var more = 10 -val addMore = (x: Int) => x + more -``` - -如上函数`addMore`中有两个变量x和more: - -- **x** : 是一个绑定变量(bound variable),因为其是该函数的入参,在函数的上下文中有明确的定义; -- **more** : 是一个自由变量(free variable),因为函数字面量本生并没有给more赋予任何含义。 - -按照定义:在创建函数时,如果需要捕获自由变量,那么包含指向被捕获变量的引用的函数就被称为闭包函数。 - -**2. Spark中的闭包** - -在实际计算时,Spark会将对RDD操作分解为Task,Task运行在Worker Node上。在执行之前,Spark会对任务进行闭包,如果闭包内涉及到自由变量,则程序会进行拷贝,并将副本变量放在闭包中,之后闭包被序列化并发送给每个执行者。因此,当在foreach函数中引用`counter`时,它将不再是Driver节点上的`counter`,而是闭包中的副本`counter`,默认情况下,副本`counter`更新后的值不会回传到Driver,所以`counter`的最终值仍然为零。 - -需要注意的是:在Local模式下,有可能执行`foreach`的Worker Node与Diver处在相同的JVM,并引用相同的原始`counter`,这时候更新可能是正确的,但是在集群模式下一定不正确。所以在遇到此类问题时应优先使用累加器。 - -累加器的原理实际上很简单:就是将每个副本变量的最终值传回Driver,由Driver聚合后得到最终值,并更新原始变量。 - - -
- -### 2.2 使用累加器 - -`SparkContext`中定义了所有创建累加器的方法,需要注意的是:被中横线划掉的累加器方法在Spark 2.0.0之后被标识为废弃。 - -
- -使用示例和执行结果分别如下: - -```scala -val data = Array(1, 2, 3, 4, 5) -// 定义累加器 -val accum = sc.longAccumulator("My Accumulator") -sc.parallelize(data).foreach(x => accum.add(x)) -// 获取累加器的值 -accum.value -``` - -
- - - -## 三、广播变量 - -在上面介绍中闭包的过程中我们说道每个Task任务的闭包都会持有自由变量的副本,如果变量很大且Task任务很多的情况下,这必然会对网络IO造成压力,为了解决这个情况,Spark提供了广播变量。 - -广播变量的做法很简单:就是不把副本变量分发到每个Task中,而是将其分发到每个Executor,Executor中的所有Task共享一个副本变量。 - -```scala -// 把一个数组定义为一个广播变量 -val broadcastVar = sc.broadcast(Array(1, 2, 3, 4, 5)) -// 之后用到该数组时应优先使用广播变量,而不是原值 -sc.parallelize(broadcastVar.value).map(_ * 10).collect() -``` - - - - - -## 参考资料 - -[RDD Programming Guide](http://spark.apache.org/docs/latest/rdd-programming-guide.html#rdd-programming-guide) - +# Spark 累加器与广播变量 + + + +## 一、简介 + +在 Spark 中,提供了两种类型的共享变量:累加器 (accumulator) 与广播变量 (broadcast variable): + ++ **累加器**:用来对信息进行聚合,主要用于累计计数等场景; ++ **广播变量**:主要用于在节点间高效分发大对象。 + +## 二、累加器 + +这里先看一个具体的场景,对于正常的累计求和,如果在集群模式中使用下面的代码进行计算,会发现执行结果并非预期: + +```scala +var counter = 0 +val data = Array(1, 2, 3, 4, 5) +sc.parallelize(data).foreach(x => counter += x) + println(counter) +``` + +counter 最后的结果是 0,导致这个问题的主要原因是闭包。 + +
+ + + +### 2.1 理解闭包 + +**1. Scala 中闭包的概念** + +这里先介绍一下 Scala 中关于闭包的概念: + +``` +var more = 10 +val addMore = (x: Int) => x + more +``` + +如上函数 `addMore` 中有两个变量 x 和 more: + +- **x** : 是一个绑定变量 (bound variable),因为其是该函数的入参,在函数的上下文中有明确的定义; +- **more** : 是一个自由变量 (free variable),因为函数字面量本生并没有给 more 赋予任何含义。 + +按照定义:在创建函数时,如果需要捕获自由变量,那么包含指向被捕获变量的引用的函数就被称为闭包函数。 + +**2. Spark 中的闭包** + +在实际计算时,Spark 会将对 RDD 操作分解为 Task,Task 运行在 Worker Node 上。在执行之前,Spark 会对任务进行闭包,如果闭包内涉及到自由变量,则程序会进行拷贝,并将副本变量放在闭包中,之后闭包被序列化并发送给每个执行者。因此,当在 foreach 函数中引用 `counter` 时,它将不再是 Driver 节点上的 `counter`,而是闭包中的副本 `counter`,默认情况下,副本 `counter` 更新后的值不会回传到 Driver,所以 `counter` 的最终值仍然为零。 + +需要注意的是:在 Local 模式下,有可能执行 `foreach` 的 Worker Node 与 Diver 处在相同的 JVM,并引用相同的原始 `counter`,这时候更新可能是正确的,但是在集群模式下一定不正确。所以在遇到此类问题时应优先使用累加器。 + +累加器的原理实际上很简单:就是将每个副本变量的最终值传回 Driver,由 Driver 聚合后得到最终值,并更新原始变量。 + + +
+ +### 2.2 使用累加器 + +`SparkContext` 中定义了所有创建累加器的方法,需要注意的是:被中横线划掉的累加器方法在 Spark 2.0.0 之后被标识为废弃。 + +
+ +使用示例和执行结果分别如下: + +```scala +val data = Array(1, 2, 3, 4, 5) +// 定义累加器 +val accum = sc.longAccumulator("My Accumulator") +sc.parallelize(data).foreach(x => accum.add(x)) +// 获取累加器的值 +accum.value +``` + +
+ + + +## 三、广播变量 + +在上面介绍中闭包的过程中我们说道每个 Task 任务的闭包都会持有自由变量的副本,如果变量很大且 Task 任务很多的情况下,这必然会对网络 IO 造成压力,为了解决这个情况,Spark 提供了广播变量。 + +广播变量的做法很简单:就是不把副本变量分发到每个 Task 中,而是将其分发到每个 Executor,Executor 中的所有 Task 共享一个副本变量。 + +```scala +// 把一个数组定义为一个广播变量 +val broadcastVar = sc.broadcast(Array(1, 2, 3, 4, 5)) +// 之后用到该数组时应优先使用广播变量,而不是原值 +sc.parallelize(broadcastVar.value).map(_ * 10).collect() +``` + + + + + +## 参考资料 + +[RDD Programming Guide](http://spark.apache.org/docs/latest/rdd-programming-guide.html#rdd-programming-guide) + diff --git a/notes/Spark部署模式与作业提交.md b/notes/Spark部署模式与作业提交.md index 03092fe..ec78f9a 100644 --- a/notes/Spark部署模式与作业提交.md +++ b/notes/Spark部署模式与作业提交.md @@ -12,49 +12,49 @@ ### 1.1 spark-submit -Spark所有模式均使用`spark-submit`命令提交作业,其格式如下: +Spark 所有模式均使用 `spark-submit` 命令提交作业,其格式如下: ```shell ./bin/spark-submit \ --class \ # 应用程序主入口类 - --master \ # 集群的Master Url + --master \ # 集群的 Master Url --deploy-mode \ # 部署模式 --conf = \ # 可选配置 ... # other options - \ # Jar包路径 + \ # Jar 包路径 [application-arguments] #传递给主入口类的参数 ``` -需要注意的是:在集群环境下,`application-jar`必须能被集群中所有节点都能访问,可以是HDFS上的路径;也可以是本地文件系统路径,如果是本地文件系统路径,则要求集群中每一个机器节点上的相同路径都存在该Jar包。 +需要注意的是:在集群环境下,`application-jar` 必须能被集群中所有节点都能访问,可以是 HDFS 上的路径;也可以是本地文件系统路径,如果是本地文件系统路径,则要求集群中每一个机器节点上的相同路径都存在该 Jar 包。 ### 1.2 deploy-mode -deploy-mode有`cluster`和`client`两个可选参数,默认为`client`。这里以Spark On Yarn模式对两者的区别进行说明 : +deploy-mode 有 `cluster` 和 `client` 两个可选参数,默认为 `client`。这里以 Spark On Yarn 模式对两者的区别进行说明 : -+ 在cluster模式下,Spark Drvier在应用程序的Master进程内运行,该进程由群集上的YARN管理,提交作业的客户端可以在启动应用程序后关闭; -+ 在client模式下,Spark Drvier在提交作业的客户端进程中运行,Master进程仅用于从YARN请求资源。 ++ 在 cluster 模式下,Spark Drvier 在应用程序的 Master 进程内运行,该进程由群集上的 YARN 管理,提交作业的客户端可以在启动应用程序后关闭; ++ 在 client 模式下,Spark Drvier 在提交作业的客户端进程中运行,Master 进程仅用于从 YARN 请求资源。 ### 1.3 master-url -master-url的所有可选参数如下表所示: +master-url 的所有可选参数如下表所示: | Master URL | Meaning | | --------------------------------- | ------------------------------------------------------------ | -| `local` | 使用一个线程本地运行Spark | +| `local` | 使用一个线程本地运行 Spark | | `local[K]` | 使用 K 个 worker 线程本地运行 Spark | -| `local[K,F]` | 使用 K 个 worker 线程本地运行 , 第二个参数为Task的失败重试次数 | -| `local[*]` | 使用与CPU核心数一样的线程数在本地运行Spark | -| `local[*,F]` | 使用与CPU核心数一样的线程数在本地运行Spark
第二个参数为Task的失败重试次数 | +| `local[K,F]` | 使用 K 个 worker 线程本地运行 , 第二个参数为 Task 的失败重试次数 | +| `local[*]` | 使用与 CPU 核心数一样的线程数在本地运行 Spark | +| `local[*,F]` | 使用与 CPU 核心数一样的线程数在本地运行 Spark
第二个参数为 Task 的失败重试次数 | | `spark://HOST:PORT` | 连接至指定的 standalone 集群的 master 节点。端口号默认是 7077。 | -| `spark://HOST1:PORT1,HOST2:PORT2` | 如果standalone集群采用Zookeeper实现高可用,则必须包含由zookeeper设置的所有master主机地址。 | -| `mesos://HOST:PORT` | 连接至给定的Mesos集群。端口默认是 5050。对于使用了 ZooKeeper 的 Mesos cluster 来说,使用 `mesos://zk://...`来指定地址,使用 `--deploy-mode cluster`模式来提交。 | -| `yarn` | 连接至一个 YARN 集群,集群由配置的 `HADOOP_CONF_DIR` 或者 `YARN_CONF_DIR` 来决定。使用`--deploy-mode`参数来配置`client` 或`cluster` 模式。 | +| `spark://HOST1:PORT1,HOST2:PORT2` | 如果 standalone 集群采用 Zookeeper 实现高可用,则必须包含由 zookeeper 设置的所有 master 主机地址。 | +| `mesos://HOST:PORT` | 连接至给定的 Mesos 集群。端口默认是 5050。对于使用了 ZooKeeper 的 Mesos cluster 来说,使用 `mesos://zk://...` 来指定地址,使用 `--deploy-mode cluster` 模式来提交。 | +| `yarn` | 连接至一个 YARN 集群,集群由配置的 `HADOOP_CONF_DIR` 或者 `YARN_CONF_DIR` 来决定。使用 `--deploy-mode` 参数来配置 `client` 或 `cluster` 模式。 | 下面主要介绍三种常用部署模式及对应的作业提交方式。 ## 二、Local模式 -Local模式下提交作业最为简单,不需要进行任何配置,提交命令如下: +Local 模式下提交作业最为简单,不需要进行任何配置,提交命令如下: ```shell # 本地模式提交应用 @@ -62,10 +62,10 @@ spark-submit \ --class org.apache.spark.examples.SparkPi \ --master local[2] \ /usr/app/spark-2.4.0-bin-hadoop2.6/examples/jars/spark-examples_2.11-2.4.0.jar \ -100 # 传给SparkPi的参数 +100 # 传给 SparkPi 的参数 ``` -`spark-examples_2.11-2.4.0.jar`是Spark提供的测试用例包,`SparkPi`用于计算Pi值,执行结果如下: +`spark-examples_2.11-2.4.0.jar` 是 Spark 提供的测试用例包,`SparkPi` 用于计算 Pi 值,执行结果如下:
@@ -73,10 +73,10 @@ spark-submit \ ## 三、Standalone模式 -Standalone是Spark提供的一种内置的集群模式,采用内置的资源管理器进行管理。下面按照如图所示演示1个Mater和2个Worker节点的集群配置,这里使用两台主机进行演示: +Standalone 是 Spark 提供的一种内置的集群模式,采用内置的资源管理器进行管理。下面按照如图所示演示 1 个 Mater 和 2 个 Worker 节点的集群配置,这里使用两台主机进行演示: -+ hadoop001: 由于只有两台主机,所以hadoop001既是Master节点,也是Worker节点; -+ hadoop002 : Worker节点。 ++ hadoop001: 由于只有两台主机,所以 hadoop001 既是 Master 节点,也是 Worker 节点; ++ hadoop002 : Worker 节点。 @@ -86,13 +86,13 @@ Standalone是Spark提供的一种内置的集群模式,采用内置的资源 ### 3.1 环境配置 -首先需要保证Spark已经解压在两台主机的相同路径上。然后进入hadoop001的`${SPARK_HOME}/conf/`目录下,拷贝配置样本并进行相关配置: +首先需要保证 Spark 已经解压在两台主机的相同路径上。然后进入 hadoop001 的 `${SPARK_HOME}/conf/` 目录下,拷贝配置样本并进行相关配置: ```shell # cp spark-env.sh.template spark-env.sh ``` -在`spark-env.sh`中配置JDK的目录,完成后将该配置使用scp命令分发到hadoop002上: +在 `spark-env.sh` 中配置 JDK 的目录,完成后将该配置使用 scp 命令分发到 hadoop002 上: ```shell # JDK安装位置 @@ -101,13 +101,13 @@ JAVA_HOME=/usr/java/jdk1.8.0_201 ### 3.2 集群配置 -在`${SPARK_HOME}/conf/`目录下,拷贝集群配置样本并进行相关配置: +在 `${SPARK_HOME}/conf/` 目录下,拷贝集群配置样本并进行相关配置: ``` # cp slaves.template slaves ``` -指定所有Worker节点的主机名: +指定所有 Worker 节点的主机名: ```shell # A Spark Worker will be started on each of the machines listed below. @@ -117,19 +117,19 @@ hadoop002 这里需要注意以下三点: -+ 主机名与IP地址的映射必须在`/etc/hosts`文件中已经配置,否则就直接使用IP地址; ++ 主机名与 IP 地址的映射必须在 `/etc/hosts` 文件中已经配置,否则就直接使用 IP 地址; + 每个主机名必须独占一行; -+ Spark的Master主机是通过SSH访问所有的Worker节点,所以需要预先配置免密登录。 ++ Spark 的 Master 主机是通过 SSH 访问所有的 Worker 节点,所以需要预先配置免密登录。 ### 3.3 启动 -使用`start-all.sh`代表启动Master和所有Worker服务。 +使用 `start-all.sh` 代表启动 Master 和所有 Worker 服务。 ```shell ./sbin/start-master.sh ``` -访问8080端口,查看Spark的Web-UI界面,,此时应该显示有两个有效的工作节点: +访问 8080 端口,查看 Spark 的 Web-UI 界面,,此时应该显示有两个有效的工作节点:
@@ -150,7 +150,7 @@ spark-submit \ --class org.apache.spark.examples.SparkPi \ --master spark://207.184.161.138:7077 \ --deploy-mode cluster \ ---supervise \ # 配置此参数代表开启监督,如果主应用程序异常退出,则自动重启Driver +--supervise \ # 配置此参数代表开启监督,如果主应用程序异常退出,则自动重启 Driver --executor-memory 2G \ --total-executor-cores 10 \ /usr/app/spark-2.4.0-bin-hadoop2.6/examples/jars/spark-examples_2.11-2.4.0.jar \ @@ -170,13 +170,13 @@ check your cluster UI to ensure that workers are registered and have sufficient
-这时候可以查看Web UI,我这里是内存空间不足:提交命令中要求作业的`executor-memory`是2G,但是实际的工作节点的`Memory`只有1G,这时候你可以修改`--executor-memory`,也可以修改 Woker 的`Memory`,其默认值为主机所有可用内存值减去1G。 +这时候可以查看 Web UI,我这里是内存空间不足:提交命令中要求作业的 `executor-memory` 是 2G,但是实际的工作节点的 `Memory` 只有 1G,这时候你可以修改 `--executor-memory`,也可以修改 Woker 的 `Memory`,其默认值为主机所有可用内存值减去 1G。

-关于Master和Woker节点的所有可选配置如下,可以在`spark-env.sh`中进行对应的配置: +关于 Master 和 Woker 节点的所有可选配置如下,可以在 `spark-env.sh` 中进行对应的配置: | Environment Variable(环境变量) | Meaning(含义) | | -------------------------------- | ------------------------------------------------------------ | @@ -184,12 +184,12 @@ check your cluster UI to ensure that workers are registered and have sufficient | `SPARK_MASTER_PORT` | master 节点地址端口(默认:7077) | | `SPARK_MASTER_WEBUI_PORT` | master 的 web UI 的端口(默认:8080) | | `SPARK_MASTER_OPTS` | 仅用于 master 的配置属性,格式是 "-Dx=y"(默认:none),所有属性可以参考官方文档:[spark-standalone-mode](https://spark.apache.org/docs/latest/spark-standalone.html#spark-standalone-mode) | -| `SPARK_LOCAL_DIRS` | spark 的临时存储的目录,用于暂存map的输出和持久化存储RDDs。多个目录用逗号分隔 | -| `SPARK_WORKER_CORES` | spark worker节点可以使用CPU Cores的数量。(默认:全部可用) | -| `SPARK_WORKER_MEMORY` | spark worker节点可以使用的内存数量(默认:全部的内存减去1GB); | -| `SPARK_WORKER_PORT` | spark worker节点的端口(默认: random(随机)) | +| `SPARK_LOCAL_DIRS` | spark 的临时存储的目录,用于暂存 map 的输出和持久化存储 RDDs。多个目录用逗号分隔 | +| `SPARK_WORKER_CORES` | spark worker 节点可以使用 CPU Cores 的数量。(默认:全部可用) | +| `SPARK_WORKER_MEMORY` | spark worker 节点可以使用的内存数量(默认:全部的内存减去 1GB); | +| `SPARK_WORKER_PORT` | spark worker 节点的端口(默认: random(随机)) | | `SPARK_WORKER_WEBUI_PORT` | worker 的 web UI 的 Port(端口)(默认:8081) | -| `SPARK_WORKER_DIR` | worker运行应用程序的目录,这个目录中包含日志和暂存空间(default:SPARK_HOME/work) | +| `SPARK_WORKER_DIR` | worker 运行应用程序的目录,这个目录中包含日志和暂存空间(default:SPARK_HOME/work) | | `SPARK_WORKER_OPTS` | 仅用于 worker 的配置属性,格式是 "-Dx=y"(默认:none)。所有属性可以参考官方文档:[spark-standalone-mode](https://spark.apache.org/docs/latest/spark-standalone.html#spark-standalone-mode) | | `SPARK_DAEMON_MEMORY` | 分配给 spark master 和 worker 守护进程的内存。(默认: 1G) | | `SPARK_DAEMON_JAVA_OPTS` | spark master 和 worker 守护进程的 JVM 选项,格式是 "-Dx=y"(默认:none) | @@ -199,11 +199,11 @@ check your cluster UI to ensure that workers are registered and have sufficient ## 三、Spark on Yarn模式 -Spark支持将作业提交到Yarn上运行,此时不需要启动Master节点,也不需要启动Worker节点。 +Spark 支持将作业提交到 Yarn 上运行,此时不需要启动 Master 节点,也不需要启动 Worker 节点。 ### 3.1 配置 -在`spark-env.sh`中配置hadoop的配置目录的位置,可以使用`YARN_CONF_DIR`或`HADOOP_CONF_DIR`进行指定: +在 `spark-env.sh` 中配置 hadoop 的配置目录的位置,可以使用 `YARN_CONF_DIR` 或 `HADOOP_CONF_DIR` 进行指定: ```properties YARN_CONF_DIR=/usr/app/hadoop-2.6.0-cdh5.15.2/etc/hadoop @@ -213,7 +213,7 @@ JAVA_HOME=/usr/java/jdk1.8.0_201 ### 3.2 启动 -必须要保证Hadoop已经启动,这里包括YARN和HDFS都需要启动,因为在计算过程中Spark会使用HDFS存储临时文件,如果HDFS没有启动,则会抛出异常。 +必须要保证 Hadoop 已经启动,这里包括 YARN 和 HDFS 都需要启动,因为在计算过程中 Spark 会使用 HDFS 存储临时文件,如果 HDFS 没有启动,则会抛出异常。 ```shell # start-yarn.sh diff --git a/notes/Spring+Mybtais+Phoenix整合.md b/notes/Spring+Mybtais+Phoenix整合.md index aa27ba2..505f3c3 100644 --- a/notes/Spring+Mybtais+Phoenix整合.md +++ b/notes/Spring+Mybtais+Phoenix整合.md @@ -1,5 +1,6 @@ -# Spring/Spring Boot 整合 Mybatis + Phoenix -