diff --git a/doc/ch-big-data-intro/batch-stream.md b/doc/ch-big-data-intro/batch-stream.md index 124c123..f6f366c 100644 --- a/doc/ch-big-data-intro/batch-stream.md +++ b/doc/ch-big-data-intro/batch-stream.md @@ -17,51 +17,57 @@ name: data-and-data-stream 在本书以及其他官方资料中,也会将单条事件称为一条数据或一个元素(Element)。在本书后文的描述中,事件、数据、元素这 3 个概念均可以用来表示数据流中的某个元素。 -## 1.2.2 批处理与流处理 +## 批处理与流处理 -### 1. 批处理 +### 批处理 批处理(Batch Processing)是指对一批数据进行处理。我们身边的批处理比比皆是,最常见的批处理例子有:微信运动每天晚上有一个批处理任务,把用户好友一天所走的步数统计一遍,生成排序结果后推送给用户;银行信用卡中心每月账单日有一个批处理任务,把一个月的消费总额统计一次,生成用户月度账单;国家统计局每季度对经济数据做一次统计,公布季度国内生产总值(GDP)。可见,批处理任务一般是对一段时间的数据聚合后进行处理的。对于数据量庞大的应用,如微信运动、银行信用卡中心等情景,一段时间内积累的数据总量非常大,计算非常耗时。 -### 2. 流处理 +### 流处理 如前文所述,数据其实是以流(Stream)的方式持续不断地产生着的,流处理(Stream Processing)就是对数据流进行处理。时间就是金钱,对数据流进行分析和处理,获取实时数据价值越发重要。如 “双十一电商大促销”,管理者要以秒级的响应时间查看实时销售业绩、库存信息以及与竞品的对比结果,以争取更多的决策时间;股票交易要以毫秒级的速度来对新信息做出响应;风险控制要对每一份欺诈交易迅速做出处理,以减少不必要的损失;网络运营商要以极快速度发现网络和数据中心的故障;等等。以上这些场景,一旦出现故障,造成服务的延迟,损失都难以估量。因此,响应速度越快,越能减少损失、增加收入。而 IoT 和 5G 的兴起将为数据生成提供更完美的底层技术基础,海量的数据在 IoT 设备上采集,并通过高速的 5G 通道传输到服务器,庞大的实时数据流将汹涌而至,流处理的需求肯定会爆炸式增长。 -## 1.2.3 为什么需要一个优秀的流处理框架 +## 为什么需要一个优秀的流处理框架 处理实时流的系统通常被称为流计算框架、实时计算框架或流处理框架。下面就来解释为何需要一个可靠的流处理框架。 -### 1. 股票交易的业务场景 +### 股票交易的业务场景 我们都知道股票交易非常依赖各类信息,一些有可能影响股票市场价格的信息经常首发于财经网站、微博、微信等社交媒体平台上。作为人类的我们不可能 24 小时一直监控各类媒体,如果有一个自动化的系统来做一些分析和预警,将为决策者争取到更多时间。 假设我们有数只股票的交易数据流,我们可以通过这个数据流来计算以 10 秒为一个时间窗口的股票价格波动,选出那些超过 5% 变化幅度的股票,并将这些股票与媒体的实时文本数据做相关分析,以判断媒体上的哪些实时信息会影响股票价格。当相关分析的结果足够有说服力时,可以将这个系统部署到生产环境,实时处理股票与媒体数据,产生分析报表,并发送给交易人员。那么,如何构建一个可靠的程序来解决上述业务场景问题呢? -### 2. 生产者 - 消费者模型 +### 生产者 - 消费者模型 -处理流数据一般使用 “生产者 - 消费者”(Producer-Consumer)模型来解决问题。如图 1-6 所示,生产者生成数据,将数据发送到一个缓存区域(Buffer),消费者从缓存区域中消费数据。这里我们暂且不关心生产者如何生产数据,以及数据如何缓存,我们只关心如何实现消费者。 +处理流数据一般使用 “生产者 - 消费者”(Producer-Consumer)模型来解决问题。如 {numref}`producer-consumer` 所示,生产者生成数据,将数据发送到一个缓存区域(Buffer),消费者从缓存区域中消费数据。这里我们暂且不关心生产者如何生产数据,以及数据如何缓存,我们只关心如何实现消费者。 -![图 1-6 生产者 - 消费者模型](./img/producer-consumer.png) +```{figure} ./img/producer-consumer.png +--- +width: 60% +name: producer-consumer +--- +生产者 - 消费者模型 +``` 在股票交易的场景中,我们可以启动一个进程来实现消费者,该进程以 10 秒为一个时间窗口,统计时间窗口内的交易情况,找到波动最大的那些股票。同时,该进程也对新流入的媒体文本进行分析。这个逻辑看起来很容易实现,但深挖之后会发现问题繁多。 -### 3. 流处理框架要解决的诸多问题 +### 流处理框架要解决的诸多问题 -#### (1) 可扩展性 +#### 可扩展性 股票交易和媒体文本的数据量都非常大,仅以微博为例,平均每秒有上千条、每天有上亿条微博数据。一般情况下,单个节点无法处理这样规模的数据,这时候需要使用分布式计算。假如我们使用类似 MPI 的框架,需要手动设计分治算法,这对很多程序员来说有一定的挑战性。 随着数据不断增多,我们能否保证我们的程序能够快速扩展到更多的节点上,以应对更多的计算需求?具体而言,当计算需求增多时,计算资源能否线性增加而不是耗费大量的资源,程序的代码逻辑能否保持简单而不会变得极其复杂?一个具有可扩展性的系统必须能够优雅地解决这些问题。 -#### (2) 数据倾斜 +#### 数据倾斜 在分布式计算中,数据需要按照某种规则分布到各个节点上。假如数据路由规则设计得不够完善,当数据本身分布不均匀时,会发生数据倾斜,这很可能导致部分节点数据量远大于其他节点。这样的后果是:轻则负载重的节点延迟过高,重则引发整个系统的崩溃。假如一条突发新闻在网络媒体平台引发激烈的讨论和分析,数据突增,程序很可能会崩溃。数据倾斜是分布式计算中经常面临的一个问题。 -#### (3) 容错性 +#### 容错性 整个系统崩溃重启后,之前的那些计算如何恢复?或者部分节点发生故障,如何将该节点上的计算迁移到其他的节点上?我们需要一个机制来做故障恢复,以增强系统的容错性。 -#### (4) 时序错乱 +#### 时序错乱 限于网络条件和其他各种潜在影响因素,流处理引擎处理某个事件的时间并不是事件本来发生的时间。比如,你想统计上午 11:00:00 到 11:00:10 的交易情况,然而发生在 11:00:05 的某项交易因网络延迟没能抵达,这时候要直接放弃这项交易吗?绝大多数情况下我们会让程序等待,比如我们会假设数据最晚不会延迟超过 10 分钟,因此程序会等待 10 分钟。等待一次也还能接受,但是如果有多个节点在并行处理呢?每个节点等待一段时间,最后做数据聚合时就要等待更长时间。 diff --git a/doc/ch-big-data-intro/bigdata-programming-languages.md b/doc/ch-big-data-intro/bigdata-programming-languages.md index 6980644..58090b3 100644 --- a/doc/ch-big-data-intro/bigdata-programming-languages.md +++ b/doc/ch-big-data-intro/bigdata-programming-languages.md @@ -1,31 +1,33 @@ (bigdata-programming-languages)= # 编程语言的选择 -大数据编程一般会使用Java、Scala和Python等编程语言,Flink目前也支持上述3种语言,本节从大数据编程的角度来分析几种编程语言的优劣。 +大数据编程一般会使用 Java、Scala 和 Python 等编程语言,Flink 目前也支持上述 3 种语言,本节从大数据编程的角度来分析几种编程语言的优劣。 -## 1.6.1 Java和Scala +## Java 和 Scala -Java是“老牌”企业级编程语言。Java相比C/C++更易上手,支持多线程,其生态圈中可用的第三方库众多。Java虚拟机(Java Virtual Machine,JVM)保证了程序的可移植性,可以快速部署到不同计算机上,是很多分布式系统首选的编程语言,比如Hadoop和Flink的绝大多数代码都是用Java编写的,这些框架提供了丰富的文档,网络社区的支持好。因此,进行大数据编程,Java编程是必备的技能。相比一些新型编程语言,Java的缺点是代码有点冗长。 +Java 是“老牌”企业级编程语言。Java 相比 C/C++ 更易上手,支持多线程,其生态圈中可用的第三方库众多。Java 虚拟机(Java Virtual Machine,JVM)保证了程序的可移植性,可以快速部署到不同计算机上,是很多分布式系统首选的编程语言,比如 Hadoop 和 Flink 的绝大多数代码都是用 Java 编写的,这些框架提供了丰富的文档,网络社区的支持好。因此,进行大数据编程,Java 编程是必备的技能。相比一些新型编程语言,Java 的缺点是代码有点冗长。 -Scala是一门基于JVM的编程语言。相比Java,Scala的特色是函数式编程。函数式编程非常适合大数据处理,我们将在第2章进一步介绍函数式编程思想。在并行计算方面,Scala支持Actor模型,Actor模型是一种更为先进的并行计算编程模型,很多大数据框架都基于Actor模型。Spark、Flink和Kafka都是基于Actor模型的大数据框架。Scala可以直接调用Java的代码,相比Java,Scala代码更为简洁和紧凑。凡事都有两面性,Scala虽然灵活简洁,但是不容易掌握,即使是有一定Java基础的开发者,也需要花一定时间系统了解Scala。 +Scala 是一门基于 JVM 的编程语言。相比 Java,Scala 的特色是函数式编程。函数式编程非常适合大数据处理,我们将在第 2 章进一步介绍函数式编程思想。在并行计算方面,Scala 支持 Actor 模型,Actor 模型是一种更为先进的并行计算编程模型,很多大数据框架都基于 Actor 模型。Spark、Flink 和 Kafka 都是基于 Actor 模型的大数据框架。Scala 可以直接调用 Java 的代码,相比 Java,Scala 代码更为简洁和紧凑。凡事都有两面性,Scala 虽然灵活简洁,但是不容易掌握,即使是有一定 Java 基础的开发者,也需要花一定时间系统了解 Scala。 -另外,Java和Scala在互相学习和借鉴。Java 8开始引入了Lambda表达式和链式调用,能够支持函数式编程,部分语法与Scala越来越接近,代码也更加简洁。 +另外,Java 和 Scala 在互相学习和借鉴。Java 8 开始引入了 Lambda 表达式和链式调用,能够支持函数式编程,部分语法与 Scala 越来越接近,代码也更加简洁。 -**注意** +:::{note} -这里的Lambda表达式与1.4.1小节介绍的Lambda架构不同。Lambda表达式基于函数式编程,是一种编写代码的方式。Lambda架构主要指如何同时处理流批数据,是一种大数据架构。 +这里的 Lambda 表达式与 1.4.1 小节介绍的 Lambda 架构不同。Lambda 表达式基于函数式编程,是一种编写代码的方式。Lambda 架构主要指如何同时处理流批数据,是一种大数据架构。 -Flink的核心代码由Java和Scala编写,为这两种语言提供丰富强大的API,程序员可根据自己和团队的习惯从Java和Scala中选择。本书基于以下两点考虑,决定主要以Java来演示Flink的编程。 +::: -- Flink目前绝大多数代码和功能均由Java实现,考虑到本书会展示一些Flink中基于Java的源码和接口,为了避免读者在Java和Scala两种语言间混淆,将主要使用Java展示一些Flink的核心概念。 -- 不同读者的编程语言基础不一样,Scala用户往往有一定的Java编程基础,而Java用户可能对Scala并不熟悉。而且Scala的语法非常灵活,一不小心可能出现莫名其妙的错误,初学者难以自行解决,而Scala相对应的书籍和教程不多。或者说Scala用户一般能够兼容Java,而Java用户学习Scala的成本较高。 +Flink 的核心代码由 Java 和 Scala 编写,为这两种语言提供丰富强大的 API,程序员可根据自己和团队的习惯从 Java 和 Scala 中选择。本书基于以下两点考虑,决定主要以 Java 来演示 Flink 的编程。 -此外,由于大多数Spark作业基于Scala,很多大数据工程师要同时负责Spark和Flink两套业务逻辑,加上Flink的Scala API与Spark比较接近,本书也会在一些地方提示Scala用户在使用Flink时的必要注意事项,并在随书附赠的工程中提供Java和Scala两个版本的代码,方便读者学习。 +- Flink 目前绝大多数代码和功能均由 Java 实现,考虑到本书会展示一些 Flink 中基于 Java 的源码和接口,为了避免读者在 Java 和 Scala 两种语言间混淆,将主要使用 Java 展示一些 Flink 的核心概念。 +- 不同读者的编程语言基础不一样,Scala 用户往往有一定的 Java 编程基础,而 Java 用户可能对 Scala 并不熟悉。而且 Scala 的语法非常灵活,一不小心可能出现莫名其妙的错误,初学者难以自行解决,而 Scala 相对应的书籍和教程不多。或者说 Scala 用户一般能够兼容 Java,而 Java 用户学习 Scala 的成本较高。 -## 1.6.2 Python +此外,由于大多数 Spark 作业基于 Scala,很多大数据工程师要同时负责 Spark 和 Flink 两套业务逻辑,加上 Flink 的 Scala API 与 Spark 比较接近,本书也会在一些地方提示 Scala 用户在使用 Flink 时的必要注意事项,并在随书附赠的工程中提供 Java 和 Scala 两个版本的代码,方便读者学习。 -Python无疑是近几年来编程语言界的“明星”。Python简单易用,有大量第三方库,支持Web、科学计算和机器学习,被广泛应用到人工智能领域。大数据生态圈的各项技术对Python支持力度也很大,Hadoop、Spark、Kafka、HBase等技术都有Python版本的API。鉴于Python在机器学习和大数据领域的流行程度,Flink社区非常重视对Python API的支持,正在积极完善Flink的Python接口。相比Java和Scala,Python API还处于完善阶段,迭代速度非常快。Flink的Python API名为PyFlink,是在1.9版本之后逐渐完善的,但相比Java和Scala还不够完善。考虑到Python和Java/Scala有较大区别,本书的绝大多数内容均基于Java相关知识,且PyFlink也在不断迭代、完善,本书暂时不探讨PyFlink。 +## Python -## 1.6.3 SQL +Python 无疑是近几年来编程语言界的“明星”。Python 简单易用,有大量第三方库,支持 Web、科学计算和机器学习,被广泛应用到人工智能领域。大数据生态圈的各项技术对 Python 支持力度也很大,Hadoop、Spark、Kafka、HBase 等技术都有 Python 版本的 API。鉴于 Python 在机器学习和大数据领域的流行程度,Flink 社区非常重视对 Python API 的支持,正在积极完善 Flink 的 Python 接口。相比 Java 和 Scala,Python API 还处于完善阶段,迭代速度非常快。Flink 的 Python API 名为 PyFlink,是在 1.9 版本之后逐渐完善的,但相比 Java 和 Scala 还不够完善。考虑到 Python 和 Java/Scala 有较大区别,本书的绝大多数内容均基于 Java 相关知识,且 PyFlink 也在不断迭代、完善,本书暂时不探讨 PyFlink。 -严格来说,SQL并不是一种全能的编程语言,而是一种在数据库上对数据进行操作的语言,相比Java、Scala和Python,SQL的上手门槛更低,它在结构化数据的查询上有绝对的优势。一些非计算机相关专业出身的读者可以在短期内掌握SQL,并进行数据分析。随着数据科学的兴起,越来越多的岗位开始要求候选人有SQL技能,包括数据分析师、数据产品经理和数据运营等岗位。Flink把这种面向结构化查询的需求封装成了表(Table),对外提供Table API 和 SQL的调用接口,提供了非常成熟的SQL支持。SQL的学习和编写成本很低,利用它能够处理相对简单的业务逻辑,其非常适合在企业内被大规模推广。本书第8章将重点介绍Table API 和SQL的使用方法。 +## SQL + +严格来说,SQL 并不是一种全能的编程语言,而是一种在数据库上对数据进行操作的语言,相比 Java、Scala 和 Python,SQL 的上手门槛更低,它在结构化数据的查询上有绝对的优势。一些非计算机相关专业出身的读者可以在短期内掌握 SQL,并进行数据分析。随着数据科学的兴起,越来越多的岗位开始要求候选人有 SQL 技能,包括数据分析师、数据产品经理和数据运营等岗位。Flink 把这种面向结构化查询的需求封装成了表(Table),对外提供 Table API 和 SQL 的调用接口,提供了非常成熟的 SQL 支持。SQL 的学习和编写成本很低,利用它能够处理相对简单的业务逻辑,其非常适合在企业内被大规模推广。本书第 8 章将重点介绍 Table API 和 SQL 的使用方法。 diff --git a/doc/ch-big-data-intro/bigdata.md b/doc/ch-big-data-intro/bigdata.md index 9e5c5e8..ca99235 100644 --- a/doc/ch-big-data-intro/bigdata.md +++ b/doc/ch-big-data-intro/bigdata.md @@ -31,17 +31,29 @@ name: 5v 计算机诞生之后,一般是在单台计算机上处理数据。大数据时代到来后,一些传统的数据处理方法无法满足大数据的处理需求。将一组计算机组织到一起形成一个集群,利用集群的力量来处理大数据的工程实践逐渐成为主流。这种使用集群进行计算的方式被称为分布式计算,当前几乎所有的大数据系统都在使用集群进行分布式计算。 -分布式计算的概念听起来很高深,其背后的思想却十分朴素,即分而治之,又称为分治法(Divide and Conquer)。如图 1-2 所示,分治法是指将一个原始问题分解为多个子问题,多个子问题分别在多台计算机上求解,借助必要的数据交换和合并策略,将子结果汇总即可求出最终结果的方法。具体而言,不同的分布式系统使用的算法和策略根据所要解决的问题各有不同,但基本上都是将计算拆分,把子问题放到多台计算机上,分而治之地计算求解。分布式计算的每台计算机(物理机或虚拟机)又被称为一个节点。 +分布式计算的概念听起来很高深,其背后的思想却十分朴素,即分而治之,又称为分治法(Divide and Conquer)。如图 {numref}`divide-conquer` 所示,分治法是指将一个原始问题分解为多个子问题,多个子问题分别在多台计算机上求解,借助必要的数据交换和合并策略,将子结果汇总即可求出最终结果的方法。具体而言,不同的分布式系统使用的算法和策略根据所要解决的问题各有不同,但基本上都是将计算拆分,把子问题放到多台计算机上,分而治之地计算求解。分布式计算的每台计算机(物理机或虚拟机)又被称为一个节点。 -![图 1-2 分治法](./img/divide-conquer.png) +```{figure} ./img/divide-conquer.png +--- +width: 60% +name: divide-conquer +--- +分治法 +``` 分布式计算已经有很多比较成熟的方案,其中比较有名的有消息传递接口(Message Passing Interface,MPI)和映射归约模型(MapReduce)。 -### 1. MPI +### MPI -MPI 是一个 “老牌” 分布式计算框架,从 MPI 这个名字也可以看出,MPI 主要解决节点间数据通信的问题。在前 MapReduce 时代,MPI 是分布式计算的业界标准。MPI 现在依然广泛运用于全球各大超级计算中心、大学、研究机构中,许多物理、生物、化学、能源等基础学科的大规模分布式计算都依赖 MPI。图 1-3 所示为使用 MPI 在 4 台服务器上并行计算的示意图。 +MPI 是一个 “老牌” 分布式计算框架,从 MPI 这个名字也可以看出,MPI 主要解决节点间数据通信的问题。在前 MapReduce 时代,MPI 是分布式计算的业界标准。MPI 现在依然广泛运用于全球各大超级计算中心、大学、研究机构中,许多物理、生物、化学、能源等基础学科的大规模分布式计算都依赖 MPI。{numref}`mpi`所示为使用 MPI 在 4 台服务器上并行计算的示意图。 -![图 1-3 在 4 台服务器上使用 MPI 进行并行计算](./img/mpi.png) +```{figure} ./img/mpi.png +--- +width: 60% +name: mpi +--- +在 4 台服务器上使用 MPI 进行并行计算 +``` 使用 MPI 编程,需要使用分治法将问题分解成子问题,在不同节点上分而治之地求解。MPI 提供了一个在多进程、多节点间进行数据通信的方案,因为绝大多数情况下,在中间求解和最终汇总的过程中,需要对多个节点上的数据进行交换和同步。 @@ -51,17 +63,23 @@ MPI 能够以很细的粒度控制数据的通信,这是它的优势,从某 并非所有的程序员都能熟练掌握 MPI 编程,衡量一个程序的时间成本,不仅要考虑程序运行的时间,也要考虑程序员学习、开发和调试的时间。就像 C 语言运算速度极快,但是 Python 却更受欢迎一样,MPI 虽然能提供极快的分布式计算速度,但不太接地气。 -### 2. MapReduce +### MapReduce 为了解决分布式计算学习和使用成本高的问题,研究人员开发出了更简单易用的 MapReduce 编程模型。MapReduce 是 Google 于 2004 年推出的一种编程模型,与 MPI 将所有事情交给程序员控制不同,MapReduce 编程模型只需要程序员定义两个操作:Map 和 Reduce。 比起 MPI,MapReduce 编程模型将更多的中间过程做了封装,程序员只需要将原始问题转化为更高层次的应用程序接口(Application Programming Interface,API),至于原始问题如何分解为更小的子问题、中间数据如何传输和交换、如何将计算扩展到多个节点等一系列细节问题可以交给大数据编程模型来解决。因此,MapReduce 相对来说学习门槛更低,使用更方便,编程开发速度更快。 -图 1-4 所示为使用 MapReduce 思想制作三明治的过程,读者可以通过这幅图更好的理解 MapReduce。 +{numref}`mapreduce-sandwichs` 所示为使用 MapReduce 思想制作三明治的过程,读者可以通过这幅图更好的理解 MapReduce。 假设我们需要大批量地制作三明治,三明治的每种食材可以分别单独处理,Map 阶段将原材料在不同的节点上分别进行处理,生成一些中间食材,Shuffle/Group 阶段将不同的中间食材进行组合,Reduce 阶段最终将一组中间食材组合成三明治成品。可以看到,这种 Map + Shuffle/Group + Reduce 的方式就是分治法的一种实现。 -![图 1-4 使用 MapReduce 制作三明治的过程](./img/mapreduce-sandwichs.jpeg) +```{figure} ./img/mapreduce-sandwichs.jpeg +--- +width: 60% +name: mapreduce-sandwichs +--- +使用 MapReduce 制作三明治的过程 +``` 基于 MapReduce 编程模型,不同的团队分别实现了自己的大数据框架:Hadoop 是较早的一种开源实现,如今已经成为大数据领域的业界标杆,之后又出现了 Spark 和 Flink。这些框架提供了编程接口,辅助程序员存储、处理和分析大数据。 diff --git a/doc/ch-big-data-intro/evolution.md b/doc/ch-big-data-intro/evolution.md index fe11f16..1b4a19e 100644 --- a/doc/ch-big-data-intro/evolution.md +++ b/doc/ch-big-data-intro/evolution.md @@ -1,16 +1,22 @@ (evolution)= -# 从Lambda到Kappa:大数据处理平台的演进 +# 从 Lambda 到 Kappa:大数据处理平台的演进 -前文已经提到,流处理框架经历了3代的更新迭代,大数据处理也随之经历了从Lambda架构到Kappa架构的演进。本节以电商平台的数据分析为例,来解释大数据处理平台如何支持企业在线服务。电商平台会将用户在App或网页的搜索、点击和购买行为以日志的形式记录下来,用户的各类行为形成了一个实时数据流,我们称之为用户行为日志。 +前文已经提到,流处理框架经历了 3 代的更新迭代,大数据处理也随之经历了从 Lambda 架构到 Kappa 架构的演进。本节以电商平台的数据分析为例,来解释大数据处理平台如何支持企业在线服务。电商平台会将用户在 App 或网页的搜索、点击和购买行为以日志的形式记录下来,用户的各类行为形成了一个实时数据流,我们称之为用户行为日志。 -## 1.4.1 Lambda架构 +## Lambda 架构 -当以 Storm 为代表的第一代流处理框架成熟后,一些互联网公司为了兼顾数据的实时性和准确性,采用图1-12所示的Lambda架构来处理数据并提供在线服务。Lambda架构主要分为3部分:批处理层、流处理层和在线服务层。其中数据流来自Kafka这样的消息队列。 +当以 Storm 为代表的第一代流处理框架成熟后,一些互联网公司为了兼顾数据的实时性和准确性,采用 {numref}`lambda-architecture` 所示的 Lambda 架构来处理数据并提供在线服务。Lambda 架构主要分为 3 部分:批处理层、流处理层和在线服务层。其中数据流来自 Kafka 这样的消息队列。 -![图1-12 Lambda架构](./img/lambda.png) +```{figure} ./img/lambda.png +--- +width: 60% +name: lambda-architecture +--- +Lambda 架构 +``` 1. **批处理层** - 在批处理层,数据流首先会被持久化保存到批处理数据仓库中,积累一段时间后,再使用批处理引擎来进行计算。这个积累时间可以是一小时、一天,也可以是一个月。处理结果最后导入一个可供在线应用系统查询的数据库上。批处理层中的批处理数据仓库可以是HDFS、Amazon S3或其他数据仓库,批处理引擎可以是MapReduce或Spark。 + 在批处理层,数据流首先会被持久化保存到批处理数据仓库中,积累一段时间后,再使用批处理引擎来进行计算。这个积累时间可以是一小时、一天,也可以是一个月。处理结果最后导入一个可供在线应用系统查询的数据库上。批处理层中的批处理数据仓库可以是 HDFS、Amazon S3 或其他数据仓库,批处理引擎可以是 MapReduce 或 Spark。 假如电商平台的数据分析部门想查看全网某天哪些商品购买次数最多,可使用批处理引擎对该天数据进行计算。像淘宝、京东这种级别的电商平台,用户行为日志数据量非常大,在这份日志上进行一个非常简单的计算都可能需要几个小时。批处理引擎一般会定时启动,对前一天或前几个小时的数据进行处理,将结果输出到一个数据库中。与动辄几个小时的批处理的处理时间相比,直接查询一个在线数据库中的数据只需要几毫秒。使用批处理生成一个预处理结果,将结果输出到在线服务层的数据库中,是很多企业仍在采用的办法。 @@ -19,36 +25,42 @@ 批处理层能保证某份数据的结果的准确性,而且即使程序运行失败,直接重启即可。此外,批处理引擎一般扩展性好,即使数据量增多,也可以通过增加节点数量来横向扩展。 2. **流处理层** - 很明显,假如整个系统只有一个批处理层,会导致用户必须等待很久才能获取计算结果,一般有几个小时的延迟。电商数据分析部门只能查看前一天的统计分析结果,无法获取当前的结果,这对实时决策来说是一个巨大的时间鸿沟,很可能导致管理者错过最佳决策时机。因此,在批处理层的基础上,Lambda架构增加了一个流处理层,用户行为日志会实时流入流处理层,流处理引擎生成预处理结果,并导入一个数据库。分析人员可以查看前一小时或前几分钟内的数据结果,这大大增强了整个系统的实时性。但数据流会有事件乱序等问题,使用早期的流处理引擎,只能得到一个近似准确的计算结果,相当于牺牲了一定的准确性来换取实时性。 + 很明显,假如整个系统只有一个批处理层,会导致用户必须等待很久才能获取计算结果,一般有几个小时的延迟。电商数据分析部门只能查看前一天的统计分析结果,无法获取当前的结果,这对实时决策来说是一个巨大的时间鸿沟,很可能导致管理者错过最佳决策时机。因此,在批处理层的基础上,Lambda 架构增加了一个流处理层,用户行为日志会实时流入流处理层,流处理引擎生成预处理结果,并导入一个数据库。分析人员可以查看前一小时或前几分钟内的数据结果,这大大增强了整个系统的实时性。但数据流会有事件乱序等问题,使用早期的流处理引擎,只能得到一个近似准确的计算结果,相当于牺牲了一定的准确性来换取实时性。 - 1.3.4小节曾提到,早期的流处理引擎有一些缺点,由于准确性、扩展性和容错性的不足,流处理层无法直接取代批处理层,只能给用户提供一个近似结果,还不能为用户提供一个一致准确的结果。因此Lambda架构中,出现了批处理和流处理并存的现象。 + 1.3.4 小节曾提到,早期的流处理引擎有一些缺点,由于准确性、扩展性和容错性的不足,流处理层无法直接取代批处理层,只能给用户提供一个近似结果,还不能为用户提供一个一致准确的结果。因此 Lambda 架构中,出现了批处理和流处理并存的现象。 3. **在线服务层** - 在线服务层直接面向用户的特定请求,需要将来自批处理层准确但有延迟的预处理结果和流处理层实时但不够准确的预处理结果做融合。在融合过程中,需要不断将流处理层的实时数据覆盖批处理层的旧数据。很多数据分析工具在数据合并上下了不少功夫,如Apache Druid,它可以融合流处理与批处理结果。当然,我们也可以在应用程序中人为控制预处理结果的融合。存储预处理结果的数据库可能是关系型数据库MySQL,也可能是Key-Value键值数据库Redis或HBase。 + 在线服务层直接面向用户的特定请求,需要将来自批处理层准确但有延迟的预处理结果和流处理层实时但不够准确的预处理结果做融合。在融合过程中,需要不断将流处理层的实时数据覆盖批处理层的旧数据。很多数据分析工具在数据合并上下了不少功夫,如 Apache Druid,它可以融合流处理与批处理结果。当然,我们也可以在应用程序中人为控制预处理结果的融合。存储预处理结果的数据库可能是关系型数据库 MySQL,也可能是 Key-Value 键值数据库 Redis 或 HBase。 -4. **Lambda架构的优缺点** - Lambda架构在实时性和准确性之间做了一个平衡,能够解决很多大数据处理的问题,它的优点如下: +4. **Lambda 架构的优缺点** + Lambda 架构在实时性和准确性之间做了一个平衡,能够解决很多大数据处理的问题,它的优点如下: - 批处理的准确度较高,而且在数据探索阶段可以对某份数据试用不同的方法,反复对数据进行实验。另外,批处理的容错性和扩展性较强。 - 流处理的实时性较强,可以提供一个近似准确的结果。 - Lambda架构的缺点也比较明显,如下: - - 使用两套大数据处理引擎,如果两套大数据处理引擎的API不同,有任何逻辑上的改动,就需要在两边同步更新,维护成本高,后期迭代的时间周期长。 + Lambda 架构的缺点也比较明显,如下: + - 使用两套大数据处理引擎,如果两套大数据处理引擎的 API 不同,有任何逻辑上的改动,就需要在两边同步更新,维护成本高,后期迭代的时间周期长。 - 早期流处理层的结果只是近似准确。 -## 1.4.2 Kappa架构 +## Kappa 架构 -Kafka的创始人杰•克雷普斯认为在很多场景下,维护一套Lambda架构的大数据处理平台耗时耗力,于是提出在某些场景下,没有必要维护一个批处理层,直接使用一个流处理层即可满足需求,即图1-13所示的Kappa架构。 +Kafka 的创始人杰•克雷普斯认为在很多场景下,维护一套 Lambda 架构的大数据处理平台耗时耗力,于是提出在某些场景下,没有必要维护一个批处理层,直接使用一个流处理层即可满足需求,即 {numref}`kappa-architecture` 所示的 Kappa 架构。 -![图1-13 Kappa架构](./img/kappa.png) +```{figure} ./img/kappa.png +--- +width: 60% +name: kappa-architecture +--- +Kappa 架构 +``` -Kappa架构的兴起主要有如下两个原因: -- Kafka可以保存更长时间的历史数据,它不仅起到消息队列的作用,也可以存储数据,替代数据库。 -- Flink流处理引擎解决了事件乱序下计算结果的准确性问题。 +Kappa 架构的兴起主要有如下两个原因: +- Kafka 可以保存更长时间的历史数据,它不仅起到消息队列的作用,也可以存储数据,替代数据库。 +- Flink 流处理引擎解决了事件乱序下计算结果的准确性问题。 -Kappa架构相对更简单,实时性更好,所需的计算资源远小于Lambda架构,随着实时处理的需求的不断增长,更多的企业开始使用Kappa架构。 +Kappa 架构相对更简单,实时性更好,所需的计算资源远小于 Lambda 架构,随着实时处理的需求的不断增长,更多的企业开始使用 Kappa 架构。 -Kappa架构的流行并不意味着不再需要批处理,批处理在一些特定场景上仍然有自己的优势。比如,进行一些数据探索、机器学习实验,需要使用批处理来反复验证不同的算法。Kappa架构适用于一些逻辑固定的数据预处理流程,比如统计一个时间段内商品的曝光和购买次数、某些关键词的搜索次数等,这类数据处理需求已经固定,无须反复试验迭代。 +Kappa 架构的流行并不意味着不再需要批处理,批处理在一些特定场景上仍然有自己的优势。比如,进行一些数据探索、机器学习实验,需要使用批处理来反复验证不同的算法。Kappa 架构适用于一些逻辑固定的数据预处理流程,比如统计一个时间段内商品的曝光和购买次数、某些关键词的搜索次数等,这类数据处理需求已经固定,无须反复试验迭代。 -Flink以流处理见长,但也实现了批处理的API,是一个集流处理与批处理于一体的大数据处理引擎,为Kappa架构提供更可靠的数据处理性能,未来Kappa架构将在更多场景下逐渐替换Lambda架构。 +Flink 以流处理见长,但也实现了批处理的 API,是一个集流处理与批处理于一体的大数据处理引擎,为 Kappa 架构提供更可靠的数据处理性能,未来 Kappa 架构将在更多场景下逐渐替换 Lambda 架构。 diff --git a/doc/ch-big-data-intro/exercise-stream-with-kafka.md b/doc/ch-big-data-intro/exercise-stream-with-kafka.md index 9032738..f5ca86a 100644 --- a/doc/ch-big-data-intro/exercise-stream-with-kafka.md +++ b/doc/ch-big-data-intro/exercise-stream-with-kafka.md @@ -3,16 +3,16 @@ 尽管本书主题是 Flink,但是对数据流的整个生命周期有一个更全面的认识有助于我们理解大数据和流处理。{numref}`technologies` 简单介绍了 Kafka 这项技术,本节将介绍如何使用 Kafka 构建实时文本数据流,读者可以通过本节了解数据流管道的大致结构:数据生产者源源不断地生成数据流,数据流通过消息队列投递,数据消费者异步地对数据流进行处理。 -## 1.7.1 Kafka 和消息队列相关背景知识 +## Kafka 和消息队列相关背景知识 -### 1. 消息队列的功能 +### 消息队列的功能 -消息队列一般使用图 1-6 所示的 “生产者 - 消费者” 模型来解决问题:生产者生成数据,将数据发送到一个缓存区域,消费者从缓存区域中消费数据。消息队列可以解决以下问题: +消息队列一般使用{numref}`producer-consumer` 所示的 “生产者 - 消费者” 模型来解决问题:生产者生成数据,将数据发送到一个缓存区域,消费者从缓存区域中消费数据。消息队列可以解决以下问题: -- ** 系统解耦 **:很多企业内部有众多系统,一个 App 也包含众多模块,如果将所有的系统和模块都放在一起作为一个庞大的系统来开发,未来则会很难维护和扩展。如果将各个模块独立出来,模块之间通过消息队列来通信,未来可以轻松扩展每个独立模块。另外,假设没有消息队列,M 个生产者和 N 个消费者通信,会产生 M×N 个数据管道,消息队列将这个复杂度降到了 M+N。 -- ** 异步处理 **:同步是指如果模块 A 向模块 B 发送消息,必须等待返回结果后才能执行接下来的业务逻辑。异步是消息发送方模块 A 无须等待返回结果即可继续执行,只需要向消息队列中发送消息,至于谁去处理这些消息、消息等待多长时间才能被处理等一系列问题,都由消费者负责。异步处理更像是发布通知,发送方不用关心谁去接收通知、如何对通知做出响应等问题。 -- ** 流量削峰 **:电商促销、抢票等场景会对系统造成巨大的压力,瞬时请求暴涨,消息队列的缓存就像一个蓄水池,以很低的成本将上游的洪峰缓存起来,下游的数据处理模块按照自身处理能力从缓存中拉取数据,避免数据处理模块崩溃。 -- ** 数据冗余 **:很多情况下,下游的数据处理模块可能发生故障,消息队列将数据缓存起来,直到数据被处理,一定程度上避免了数据丢失风险。 +- 系统解耦:很多企业内部有众多系统,一个 App 也包含众多模块,如果将所有的系统和模块都放在一起作为一个庞大的系统来开发,未来则会很难维护和扩展。如果将各个模块独立出来,模块之间通过消息队列来通信,未来可以轻松扩展每个独立模块。另外,假设没有消息队列,M 个生产者和 N 个消费者通信,会产生 M×N 个数据管道,消息队列将这个复杂度降到了 M+N。 +- 异步处理:同步是指如果模块 A 向模块 B 发送消息,必须等待返回结果后才能执行接下来的业务逻辑。异步是消息发送方模块 A 无须等待返回结果即可继续执行,只需要向消息队列中发送消息,至于谁去处理这些消息、消息等待多长时间才能被处理等一系列问题,都由消费者负责。异步处理更像是发布通知,发送方不用关心谁去接收通知、如何对通知做出响应等问题。 +- 流量削峰:电商促销、抢票等场景会对系统造成巨大的压力,瞬时请求暴涨,消息队列的缓存就像一个蓄水池,以很低的成本将上游的洪峰缓存起来,下游的数据处理模块按照自身处理能力从缓存中拉取数据,避免数据处理模块崩溃。 +- 数据冗余:很多情况下,下游的数据处理模块可能发生故障,消息队列将数据缓存起来,直到数据被处理,一定程度上避免了数据丢失风险。 Kafka 作为一个消息队列,主要提供如下 3 种核心能力: @@ -22,7 +22,7 @@ Kafka 作为一个消息队列,主要提供如下 3 种核心能力: 可见 Kafka 不仅是一个消息队列,也有数据存储和流处理的功能,确切地说,Kafka 是一个流处理系统。 -### 2. Kafka 的一些核心概念 +### Kafka 的一些核心概念 Kafka 涉及不少概念,包括 Topic、Producer、Consumer 等,这里从 Flink 流处理的角度出发,只对与流处理关系密切的核心概念做简单介绍。 @@ -30,9 +30,9 @@ Kafka 涉及不少概念,包括 Topic、Producer、Consumer 等,这里从 Fl - **Producer**:多个 Producer 将某份数据发布到某个 Topic 下。比如电商平台的多台线上服务器将买家行为日志发送到名为 user_behavior 的 Topic 下。 - **Consumer**:多个 Consumer 被分为一组,名为 Consumer Group,一组 Consumer Group 订阅一个 Topic 下的数据。通常我们可以使用 Flink 编写的程序作为 Kafka 的 Consumer 来对一个数据流做处理。 -## 1.7.2 使用 Kafka 构建一个文本数据流 +## 使用 Kafka 构建一个文本数据流 -### 1. 下载和安装 +### 下载和安装 如前文所述,绝大多数的大数据框架基于 Java,因此在进行开发之前要先搭建 Java 编程环境,主要是下载和配置 Java 开发工具包(Java Development Kit,JDK)。网络上针对不同操作系统的相关教程已经很多,这里不赘述。 @@ -43,13 +43,15 @@ $ tar -xzf kafka_2.12-2.3.0.tgz $ cd kafka_2.12-2.3.0 ``` -** 注意 **: +:::{note} `$` 符号表示该行命令在类 UNIX 操作系统(macOS 和 Linux)命令行中执行,而不是在 Python 交互命令界面或其他任何交互界面中。Windows 的命令行提示符是大于号 `>`。 +::: + 解压之后的文件中,`bin` 目录默认为 Linux 和 macOS 设计。Windows 用户要进入 `bin\windows\` 来启动相应脚本,且脚本文件扩展名要改为 `.bat`。 -### 2. 启动服务 +### 启动服务 Kafka 使用 ZooKeeper 来管理集群,因此需要先启动 ZooKeeper。刚刚下载的 Kafka 包里已经包含了 ZooKeeper 的启动脚本,可以使用这个脚本快速启动一个 ZooKeeper 服务。 @@ -67,7 +69,7 @@ $ bin/kafka-server-start.sh config/server.properties 以上两个操作均使用 `config` 文件夹下的默认配置文件,需要注意配置文件的路径是否写错。生产环境中的配置文件比默认配置文件复杂得多。 -### 3. 创建 Topic +### 创建 Topic 开启一个命令行会话,创建一个名为 `Shakespeare` 的 Topic: @@ -82,7 +84,7 @@ $ bin/kafka-topics.sh --list --bootstrap-server localhost:9092 Shakespeare ``` -### 4. 发送消息 +### 发送消息 接下来我们模拟 Producer,假设这个 Producer 是莎士比亚(Shakespeare)本人,它不断向 “Shakespeare” 这个 Topic 发送自己的最新作品: @@ -93,7 +95,7 @@ $ bin/kafka-console-producer.sh --broker-list localhost:9092 --topic Shakespeare 每一行作为一条消息事件,被发送到了 Kafka 集群上,虽然这个集群只有本机这一台服务器。 -### 5. 消费数据 +### 消费数据 另外一些人想了解莎士比亚向 Kafka 发送过哪些新作,所以需要使用一个 Consumer 来消费刚刚发送的数据。我们开启一个命令行会话来模拟 Consumer: diff --git a/doc/ch-big-data-intro/img/evolution-stream-frameworks.png b/doc/ch-big-data-intro/img/evolution-stream-frameworks.png new file mode 100644 index 0000000..5eb36bf Binary files /dev/null and b/doc/ch-big-data-intro/img/evolution-stream-frameworks.png differ diff --git a/doc/ch-big-data-intro/stream-processing-basics.md b/doc/ch-big-data-intro/stream-processing-basics.md index c8e4c63..cd5e6fc 100644 --- a/doc/ch-big-data-intro/stream-processing-basics.md +++ b/doc/ch-big-data-intro/stream-processing-basics.md @@ -3,43 +3,49 @@ 前文已经多次提到,在某些场景下,流处理打破了批处理的一些局限。Flink 作为一款以流处理见长的大数据引擎,相比其他流处理引擎具有众多优势。本节将对流处理的一些基本概念进行细化,这些概念是入门流处理的必备基础,至此你将正式进入数据流的世界。 -## 1.5.1 延迟和吞吐 +## 延迟和吞吐 在批处理场景中,我们主要通过一次计算的总耗时来评价性能。在流处理场景,数据源源不断地流入系统,大数据框架对每个数据的处理越快越好,大数据框架能处理的数据量越大越好。例如 1.2.3 小节中提到的股票交易案例,如果系统只能处理一两只股票或处理时间长达一天,那说明这个系统非常不靠谱。衡量流处理的 “快” 和“量”两方面的性能,一般用延迟(Latency)和吞吐(Throughput)这两个指标。 -### 1. 延迟 +### 延迟 延迟表示一个事件被系统处理的总时间,一般以毫秒为单位。根据业务不同,我们一般关心平均延迟(Average Latency)和分位延迟(Percentile Latency)。假设一个食堂的自助取餐流水线是一个流处理系统,每个就餐者前来就餐是它需要处理的事件,从就餐者到达食堂到他拿到所需菜品并付费离开的总耗时,就是这个就餐者的延迟。如果正赶上午餐高峰期,就餐者极有可能排队,这个排队时间也要算在延迟中。例如,99 分位延迟表示对所有就餐者的延迟进行统计和排名,取排名第 99% 位的就餐者延迟。一般商业系统更关注分位延迟,因为分位延迟比平均延迟更能反映这个系统的一些潜在问题。还是以食堂的自助餐流水线为例,该流水线的平均延迟可能不高,但是在就餐高峰期,延迟一般会比较高。如果延迟过高,部分就餐者会因为等待时间过长而放弃排队,用户体验较差。通过检查各模块分位延迟,能够快速定位到哪个模块正在 “拖累” 整个系统的性能。 延迟对于很多流处理系统非常重要,比如欺诈检测系统、告警监控系统等。Flink 可以将延迟降到毫秒级别。如果用 mini-batch 的思想处理同样的数据流,很可能有分钟级到小时级的延迟,因为批处理引擎必须等待一批数据达到才开始进行计算。 -### 2. 吞吐 +### 吞吐 吞吐表示一个系统最多能处理多少事件,一般以单位时间处理的事件数量为标准。需要注意的是,吞吐除了与引擎自身设计有关,也与数据源发送过来的事件数据量有关,有可能流处理引擎的最大吞吐量远小于数据源的数据量。比如,自助取餐流水线可能在午餐时间的需求最高,很可能出现大量排队的情况,但另外的时间几乎不需要排队等待。假设一天能为 1 000 个人提供就餐服务,共计 10 小时,那它的平均吞吐量为 100 人 / 小时;仅午间 2 小时的高峰期就提供了 600 人,它的峰值吞吐量是 300 人 / 小时。比起平均吞吐量,峰值吞吐量更影响用户体验,如果峰值吞吐量低,会导致就餐者等待时间过长而放弃排队。排队的过程被称作缓存(Buffering)。如果排队期间仍然有大量数据进入缓存,很可能超出系统的极限,就会出现反压(Backpressure)问题,这时候就需要一些优雅的策略来处理类似问题,否则会造成系统崩溃,用户体验较差。 -### 3. 延迟与吞吐 +### 延迟与吞吐 延迟与吞吐其实并不是相互孤立的,它们相互影响。如果延迟高,那么很可能吞吐较低,系统处理不了太多数据。为了优化这两个指标,首先提高自助取餐流水线的行进速度,加快取餐各个环节的进程。当用户量大到超过流水线的瓶颈时,需要再增加一个自助取餐流水线。这就是当前大数据系统都在采用的两种加速方式,第一是优化单节点内的计算速度,第二是使用并行策略,分而治之地处理数据。如果一台计算机做不了或做得不够快,那就用更多的计算机一起来做。 综上,延迟和吞吐是衡量流处理引擎的重要指标。如何保证流处理系统保持高吞吐和低延迟是一项非常有挑战性的工作。 -## 1.5.2 窗口与时间 +## 窗口与时间 -### 1. 不同窗口模式 +### 不同窗口模式 比起批处理,流处理对窗口(Window)和时间概念更为敏感。在批处理场景下,数据已经按照某个时间维度被分批次地存储了。一些公司经常将用户行为日志按天存储,一些开放数据集都会说明数据采集的时间始末。因此,对于批处理任务,处理一个数据集,其实就是对该数据集对应的时间窗口内的数据进行处理。在流处理场景下,数据以源源不断的流的形式存在,数据一直在产生,没有始末。我们要对数据进行处理时,往往需要明确一个时间窗口,比如,数据在 “每秒”“每小时”“每天” 的维度下的一些特性。窗口将数据流切分成多个数据块,很多数据分析都是在窗口上进行操作,比如连接、聚合以及其他时间相关的操作。 -图 1-14 展示了 3 种常见的窗口形式:滚动窗口、滑动窗口、会话窗口。 +{numref}`three-type-window` 展示了 3 种常见的窗口形式:滚动窗口、滑动窗口、会话窗口。 -![图 1-14 3 种常见的窗口形式](./img/three-type-window.png) +```{figure} ./img/three-type-window.png +--- +width: 60% +name: three-type-window +--- +3 种常见的窗口形式 +``` -- ** 滚动窗口(Tumbling Window)**:模式一般定义一个固定的窗口长度,长度是一个时间间隔,比如小时级的窗口或分钟级的窗口。窗口像车轮一样,滚动向前,任意两个窗口之间不会包含同样的数据。 -- ** 滑动窗口(Sliding Window)**:模式也设有一个固定的窗口长度。假如我们想每分钟开启一个窗口,统计 10 分钟内的股票价格波动,就使用滑动窗口模式。当窗口的长度大于滑动的间隔,可能会导致两个窗口之间包含同样的事件。其实,滚动窗口模式是滑动窗口模式的一个特例,滚动窗口模式中滑动的间隔正好等于窗口的大小。 -- ** 会话窗口(Session Window)**:模式的窗口长度不固定,而是通过一个间隔来确定窗口,这个间隔被称为会话间隔(Session Gap)。当两个事件之间的间隔大于会话间隔,则两个事件被划分到不同的窗口中;当事件之间的间隔小于会话间隔,则两个事件被划分到同一窗口。 +- **滚动窗口(Tumbling Window)**:模式一般定义一个固定的窗口长度,长度是一个时间间隔,比如小时级的窗口或分钟级的窗口。窗口像车轮一样,滚动向前,任意两个窗口之间不会包含同样的数据。 +- **滑动窗口(Sliding Window)**:模式也设有一个固定的窗口长度。假如我们想每分钟开启一个窗口,统计 10 分钟内的股票价格波动,就使用滑动窗口模式。当窗口的长度大于滑动的间隔,可能会导致两个窗口之间包含同样的事件。其实,滚动窗口模式是滑动窗口模式的一个特例,滚动窗口模式中滑动的间隔正好等于窗口的大小。 +- **会话窗口(Session Window)**:模式的窗口长度不固定,而是通过一个间隔来确定窗口,这个间隔被称为会话间隔(Session Gap)。当两个事件之间的间隔大于会话间隔,则两个事件被划分到不同的窗口中;当事件之间的间隔小于会话间隔,则两个事件被划分到同一窗口。 -### 2. 时间语义 +### 时间语义 -#### (1) Event Time 和 Processing Time +#### Event Time 和 Processing Time “时间” 是平时生活中最常用的概念之一,在流处理中需要额外注意它,因为时间的语义不仅与窗口有关,也与事件乱序、触发计算等各类流处理问题有关。常见的时间语义如下。 @@ -48,13 +54,19 @@ 对于一个事件,自其发生起,Event Time 就已经确定不会改变。因各类延迟、流处理引擎各个模块先后处理顺序等因素,不同节点、系统内不同模块、同一数据不同次处理都会产生不同的 Processing Time。 -#### (2) “一分钟” 真的是一分钟吗? +#### “一分钟” 真的是一分钟吗? -在很多应用场景中,时间有着不同的语义,“一分钟” 真的是一分钟吗?很多手机游戏中多玩家在线实时竞技,假设我们在玩某款手机游戏,该游戏将数据实时发送给游戏服务器,服务器计算一分钟内玩家的一些操作,这些计算影响用户该局游戏的最终得分。当游戏正酣,我们进入了电梯,手机信号丢失,一分钟后才恢复信号;幸好手机在电梯期间缓存了掉线时的数据,并在信号恢复后将缓存数据传回了服务器,图 1-15 展示了这个场景的流处理过程。在丢失信号的这段时间,你的数据没有被计算进去,显然这样的计算不公平。当信号恢复时,数据重传到服务器,再根据 Event Time 重新计算一次,那就非常公平了。我们可以根据 Event Time 复现一个事件序列的实际顺序。因此,使用 Event Time 是最准确的。 +在很多应用场景中,时间有着不同的语义,“一分钟” 真的是一分钟吗?很多手机游戏中多玩家在线实时竞技,假设我们在玩某款手机游戏,该游戏将数据实时发送给游戏服务器,服务器计算一分钟内玩家的一些操作,这些计算影响用户该局游戏的最终得分。当游戏正酣,我们进入了电梯,手机信号丢失,一分钟后才恢复信号;幸好手机在电梯期间缓存了掉线时的数据,并在信号恢复后将缓存数据传回了服务器,{numref}`data-transmission-signal-loss` 展示了这个场景的流处理过程。在丢失信号的这段时间,你的数据没有被计算进去,显然这样的计算不公平。当信号恢复时,数据重传到服务器,再根据 Event Time 重新计算一次,那就非常公平了。我们可以根据 Event Time 复现一个事件序列的实际顺序。因此,使用 Event Time 是最准确的。 -![图 1-15 数据传输过程恰好遇到信号丢失](./img/signal.png) +```{figure} ./img/signal.png +--- +width: 60% +name: data-transmission-signal-loss +--- +数据传输过程恰好遇到信号丢失 +``` -### 3. Watermark +### Watermark 虽然使用 Event Time 更准确,但问题在于,因为各种不可控因素,事件上报会有延迟,那么最多要等待多长时间呢?从服务器的角度来看,在事件到达之前,我们也无法确定是否有事件发生了延迟,如何设置等待时间是一个很难的问题。比如刚才的例子,我们要统计一分钟内的实时数据,考虑到事件的延迟,如何设置合理的等待时间,以等待一分钟内所有事件都到达服务器?也正因为这个问题,流处理与批处理在准确性上有差距,因为批处理一般以更长的一段时间为一个批次,一个批次内延迟上报的数据比一个流处理时间窗口内延迟上报的数据相对更少。比如电商平台上,对于计算一件商品每分钟点击次数,使用一天的总数除以分钟数,比使用一分钟时间窗口实时的点击次数更准确。可以看到,数据的实时性和准确性二者不可得兼,必须取一个平衡。 @@ -62,11 +74,17 @@ Watermark 是一种折中解决方案,它假设某个时间点上,不会有 那既然 Event Time 似乎可以解决一切问题,为什么还要使用 Processing Time?前文也提到了,为了处理延迟上报或事件乱序,需要使用一些机制来等待,这样会导致延迟提高。某些场景可能对准确性要求不高,但是对实时性要求更高,在这些场景下使用 Processing Time 就更合适一些。 -## 1.5.3 状态与检查点 +## 状态与检查点 -状态是流处理区别于批处理的特有概念。如果我们对一个文本数据流进行处理,把英文大写字母都改成英文小写字母,这种处理是无状态的,即系统不需要记录额外的信息。如果我们想统计这个数据流一分钟内的单词出现次数,一方面要处理每一瞬间新流入的数据,另一方面要保存之前一分钟内已经进入系统的数据,额外保存的数据就是状态。图 1-16 展示了无状态和有状态两种不同类型的计算。 +状态是流处理区别于批处理的特有概念。如果我们对一个文本数据流进行处理,把英文大写字母都改成英文小写字母,这种处理是无状态的,即系统不需要记录额外的信息。如果我们想统计这个数据流一分钟内的单词出现次数,一方面要处理每一瞬间新流入的数据,另一方面要保存之前一分钟内已经进入系统的数据,额外保存的数据就是状态。{numref}`state-stateless` 展示了无状态和有状态两种不同类型的计算。 -![图 1-16 无状态计算和有状态计算](./img/state-stateless.png) +```{figure} ./img/state-stateless.png +--- +width: 60% +name: state-stateless +--- +无状态计算和有状态计算 +``` 状态在流处理中经常被用到。再举一个温度报警的例子,当系统在监听到 “高温” 事件后 10 分钟内又监听到 “冒烟” 的事件,系统必须及时报警。在这个场景下,流处理引擎把 “高温” 的事件作为状态记录下来,并判断这个状态接下来十分钟内是否有 “冒烟” 事件。 @@ -78,7 +96,7 @@ Watermark 是一种折中解决方案,它假设某个时间点上,不会有 检查点(Checkpoint)机制其实并不是一个新鲜事物,它广泛存在于各类计算任务上,主要作用是将中间数据保存下来。当计算任务出现问题,重启后可以根据 Checkpoint 中保存的数据重新恢复任务。在流处理中,Checkpoint 主要保存状态数据。 -## 1.5.4 数据一致性保障 +## 数据一致性保障 流处理任务可能因为各种原因出现故障,比如数据量暴涨导致内存溢出、输入数据发生变化而无法解析、网络故障、集群维护等。事件进入流处理引擎,如果遇到故障并重启,该事件是否被成功处理了呢?一般有如下 3 种结果。 diff --git a/doc/ch-big-data-intro/technologies.md b/doc/ch-big-data-intro/technologies.md index d4ea283..bee749b 100644 --- a/doc/ch-big-data-intro/technologies.md +++ b/doc/ch-big-data-intro/technologies.md @@ -3,9 +3,9 @@ MapReduce 编程模型的提出为大数据分析和处理开创了一条先河,其后涌现出一批知名的开源大数据技术,本节主要对一些流行的技术和框架进行简单介绍。 -## 1.3.1 Hadoop +## Hadoop -2004 年,Hadoop 的创始人道格·卡廷(Doug Cutting)和麦克·卡法雷拉(Mike Cafarella)受 MapReduce 编程模型和 Google File System 等技术的启发,对其中提及的思想进行了编程实现,Hadoop 的名字来源于道格 · 卡廷儿子的玩具大象。由于道格 · 卡廷后来加入了雅虎,并在雅虎工作期间做了大量 Hadoop 的研发工作,因此 Hadoop 也经常被认为是雅虎开源的一款大数据框架。时至今日,Hadoop 不仅是整个大数据领域的先行者和领航者,更形成了一套围绕 Hadoop 的生态圈,Hadoop 和它的生态圈是绝大多数企业首选的大数据解决方案。图 1-7 展示了 Hadoop 生态圈一些流行组件。 +2004 年,Hadoop 的创始人道格·卡廷(Doug Cutting)和麦克·卡法雷拉(Mike Cafarella)受 MapReduce 编程模型和 Google File System 等技术的启发,对其中提及的思想进行了编程实现,Hadoop 的名字来源于道格 · 卡廷儿子的玩具大象。由于道格 · 卡廷后来加入了雅虎,并在雅虎工作期间做了大量 Hadoop 的研发工作,因此 Hadoop 也经常被认为是雅虎开源的一款大数据框架。时至今日,Hadoop 不仅是整个大数据领域的先行者和领航者,更形成了一套围绕 Hadoop 的生态圈,Hadoop 和它的生态圈是绝大多数企业首选的大数据解决方案。{numref}`hadoop-ecosystem` 展示了 Hadoop 生态圈一些流行组件。 Hadoop 生态圈的核心组件主要有如下 3 个。 @@ -20,50 +20,80 @@ Hadoop 生态圈的核心组件主要有如下 3 个。 - **Kafka**:Kafka 是一款流处理框架,主要用作消息队列。 - **ZooKeeper**:Hadoop 生态圈中很多组件使用动物来命名,形成了一个大型 “动物园”,ZooKeeper 是这个动物园的管理者,主要负责分布式环境的协调。 -![图 1-7 Hadoop 生态圈](./img/hadoop.png) +```{figure} ./img/hadoop.png +--- +width: 60% +name: hadoop-ecosystem +--- +Hadoop 生态圈 +``` -## 1.3.2 Spark +## Spark 2009 年,Spark 诞生于加州大学伯克利分校,2013 年被捐献给 Apache 基金会。实际上,Spark 的创始团队本来是为了开发集群管理框架 Apache Mesos(以下简称 Mesos)的,其功能类似 YARN,Mesos 开发完成后,需要一个基于 Mesos 的产品运行在上面以验证 Mesos 的各种功能,于是他们接着开发了 Spark。Spark 有火花、鼓舞之意,创始团队希望用 Spark 来证明在 Mesos 上从零开始创造一个项目非常简单。 Spark 是一款大数据处理框架,其开发初衷是改良 Hadoop MapReduce 的编程模型和提高运行速度,尤其是提升大数据在机器学习方向上的性能。与 Hadoop 相比,Spark 的改进主要有如下两点。 -- ** 易用性 **:MapReduce 模型比 MPI 更友好,但仍然不够方便。因为并不是所有计算任务都可以被简单拆分成 Map 和 Reduce,有可能为了解决一个问题,要设计多个 MapReduce 任务,任务之间相互依赖,整个程序非常复杂,导致代码的可读性和可维护性差。Spark 提供更加方便易用的接口,提供 Java、Scala、Python 和 R 语言等的 API,支持 SQL、机器学习和图计算,覆盖了绝大多数计算场景。 -- ** 速度快 **:Hadoop 的 Map 和 Reduce 的中间结果都需要存储到磁盘上,而 Spark 尽量将大部分计算放在内存中。加上 Spark 有向无环图的优化,在官方的基准测试中,Spark 比 Hadoop 快一百倍以上。 +- **易用性**:MapReduce 模型比 MPI 更友好,但仍然不够方便。因为并不是所有计算任务都可以被简单拆分成 Map 和 Reduce,有可能为了解决一个问题,要设计多个 MapReduce 任务,任务之间相互依赖,整个程序非常复杂,导致代码的可读性和可维护性差。Spark 提供更加方便易用的接口,提供 Java、Scala、Python 和 R 语言等的 API,支持 SQL、机器学习和图计算,覆盖了绝大多数计算场景。 +- **速度快**:Hadoop 的 Map 和 Reduce 的中间结果都需要存储到磁盘上,而 Spark 尽量将大部分计算放在内存中。加上 Spark 有向无环图的优化,在官方的基准测试中,Spark 比 Hadoop 快一百倍以上。 Spark 的核心在于计算,主要目的在于优化 Hadoop MapReduce 计算部分,在计算层面提供更细致的服务。 -Spark 并不能完全取代 Hadoop,实际上,从图 1-7 可以看出,Spark 融入了 Hadoop 生态圈,成为其中的重要一员。一个 Spark 任务很可能依赖 HDFS 上的数据,向 YARN 申请计算资源,将结果输出到 HBase 上。当然,Spark 也可以不用依赖这些组件,独立地完成计算。 +Spark 并不能完全取代 Hadoop,实际上,从 {numref}`hadoop-ecosystem` 可以看出,Spark 融入了 Hadoop 生态圈,成为其中的重要一员。一个 Spark 任务很可能依赖 HDFS 上的数据,向 YARN 申请计算资源,将结果输出到 HBase 上。当然,Spark 也可以不用依赖这些组件,独立地完成计算。 -![图 1-8 Spark 生态圈](./img/spark.png) +```{figure} ./img/spark.png +--- +width: 60% +name: spark-ecosystem +--- +Spark 生态圈 +``` -Spark 主要面向批处理需求,因其优异的性能和易用的接口,Spark 已经是批处理界绝对的 “王者”。Spark 的子模块 Spark Streaming 提供了流处理的功能,它的流处理主要基于 mini-batch 的思想。如图 1-9 所示,Spark Streaming 将输入数据流切分成多个批次,每个批次使用批处理的方式进行计算。因此,Spark 是一款集批处理和流处理于一体的处理框架。 +Spark 主要面向批处理需求,因其优异的性能和易用的接口,Spark 已经是批处理界绝对的 “王者”。Spark 的子模块 Spark Streaming 提供了流处理的功能,它的流处理主要基于 mini-batch 的思想。如 {numref}`spark-streaming-mini-batch` 所示,Spark Streaming 将输入数据流切分成多个批次,每个批次使用批处理的方式进行计算。因此,Spark 是一款集批处理和流处理于一体的处理框架。 -![图 1-9 Spark Streaming mini-batch 处理](./img/spark-streaming-mini-batch.png) +```{figure} ./img/spark-streaming-mini-batch.png +--- +width: 60% +name: spark-streaming-mini-batch +--- +Spark Streaming mini-batch 处理 +``` -## 1.3.3 Apache Kafka +## Apache Kafka 2010 年,LinkedIn 开始了其内部流处理框架的开发,2011 年将该框架捐献给了 Apache 基金会,取名 Apache Kafka(以下简称 Kafka)。Kafka 的创始人杰 · 克雷普斯(Jay Kreps)觉得这个框架主要用于优化读写,应该用一个作家的名字来命名,加上他很喜欢作家卡夫卡的文学作品,觉得这个名字对一个开源项目来说很酷,因此取名 Kafka。 Kafka 也是一种面向大数据领域的消息队列框架。在大数据生态圈中,Hadoop 的 HDFS 或 Amazon S3 提供数据存储服务,Hadoop MapReduce、Spark 和 Flink 负责计算,Kafka 常常用来连接不同的应用系统。 -如图 1-10 所示,企业中不同的应用系统作为数据生产者会产生大量数据流,这些数据流还需要进入不同的数据消费者,Kafka 起到数据集成和系统解耦的作用。系统解耦是让某个应用系统专注于一个目标,以降低整个系统的维护难度。在实践上,一个企业经常拆分出很多不同的应用系统,系统之间需要建立数据流管道(Stream Pipeline)。假如没有 Kafka 的消息队列,M 个生产者和 N 个消费者之间要建立 M×N 个点对点的数据流管道,Kafka 就像一个中介,让数据管道的个数变为 M+N,大大减小了数据流管道的复杂程度。 +如 {numref}`kafka-multi-system` 所示,企业中不同的应用系统作为数据生产者会产生大量数据流,这些数据流还需要进入不同的数据消费者,Kafka 起到数据集成和系统解耦的作用。系统解耦是让某个应用系统专注于一个目标,以降低整个系统的维护难度。在实践上,一个企业经常拆分出很多不同的应用系统,系统之间需要建立数据流管道(Stream Pipeline)。假如没有 Kafka 的消息队列,M 个生产者和 N 个消费者之间要建立 M×N 个点对点的数据流管道,Kafka 就像一个中介,让数据管道的个数变为 M+N,大大减小了数据流管道的复杂程度。 -![图 1-10 Kafka 可以连接多个应用系统](./img/kafka.png) +```{figure} ./img/kafka.png +--- +width: 60% +name: kafka-multi-system +--- +Kafka 可以连接多个应用系统 +``` 从批处理和流处理的角度来讲,数据流经 Kafka 后会持续不断地写入 HDFS,积累一段时间后可提供给后续的批处理任务,同时数据流也可以直接流入 Flink,被用于流处理。 随着流处理的兴起,Kafka 不甘心只做一个数据流管道,开始向轻量级流处理方向努力,但相比 Spark 和 Flink 这样的计算框架,Kafka 的主要功能侧重在消息队列上。 -## 1.3.4 Flink +## Flink Flink 是由德国 3 所大学发起的的学术项目,后来不断发展壮大,并于 2014 年年末成为 Apache 顶级项目之一。在德语中,“flink” 表示快速、敏捷,以此来表征这款计算框架的特点。 -Flink 主要面向流处理,如果说 Spark 是批处理界的 “王者”,那么 Flink 就是流处理领域冉冉升起的 “新星”。流处理并不是一项全新的技术,在 Flink 之前,不乏流处理引擎,比较著名的有 Storm、Spark Streaming,图 1-11 展示了流处理框架经历的三代演进。 +Flink 主要面向流处理,如果说 Spark 是批处理界的 “王者”,那么 Flink 就是流处理领域冉冉升起的 “新星”。流处理并不是一项全新的技术,在 Flink 之前,不乏流处理引擎,比较著名的有 Storm、Spark Streaming,{numref}`evolution-stream-frameworks` 展示了流处理框架经历的三代演进。 2011 年成熟的 Apache Strom(以下简称 Storm)是第一代被广泛采用的流处理引擎。它是以数据流中的事件为最小单位来进行计算的。以事件为单位的框架的优势是延迟非常低,可以提供毫秒级的延迟。流处理结果依赖事件到达的时序准确性,Storm 并不能保障处理结果的一致性和准确性。Storm 只支持至少一次(At-Least-Once)和至多一次(At-Most-Once),即数据流里的事件投递只能保证至少一次或至多一次,不能保证只有一次(Exactly-Once)。在多项基准测试中,Storm 的数据吞吐量和延迟都远逊于 Flink。对于很多对数据准确性要求较高的应用,Storm 有一定劣势。此外,Storm 不支持 SQL,不支持中间状态(State)。 -图 1-11 流处理框架演进 +```{figure} ./img/evolution-stream-frameworks.png +--- +width: 60% +name: evolution-stream-frameworks +--- +流处理框架演进 +``` 2013 年成熟的 Spark Streaming 是第二代被广泛采用的流处理框架。1.3.2 小节中提到,Spark 是 “一统江湖” 的大数据处理框架,Spark Streaming 采用微批次(mini-batch)的思想,将数据流切分成一个个小批次,一个小批次里包含多个事件,以接近实时处理的效果。这种做法保证了 “Exactly-Once” 的事件投递效果,因为假如某次计算出现故障,重新进行该次计算即可。Spark Streaming 的 API 相比第一代流处理框架更加方便易用,与 Spark 批处理集成度较高,因此 Spark 可以给用户提供一个流处理与批处理一体的体验。但因为 Spark Streaming 以批次为单位,每次计算一小批数据,比起以事件为单位的框架来说,延迟从毫秒级变为秒级。 diff --git a/doc/ch-programming-basics/exercise-Flink-development-environment.md b/doc/ch-programming-basics/exercise-Flink-development-environment.md index 08cf75c..e609df3 100644 --- a/doc/ch-programming-basics/exercise-Flink-development-environment.md +++ b/doc/ch-programming-basics/exercise-Flink-development-environment.md @@ -1,31 +1,31 @@ (exercise-Flink-development-environment)= -# 案例实战 Flink开发环境搭建 +# 案例实战 Flink 开发环境搭建 -本案例实战主要带领读者完成对Flink开发环境的搭建。 +本案例实战主要带领读者完成对 Flink 开发环境的搭建。 -## 2.4.1 准备所需软件 +## 准备所需软件 -在1.7节中我们简单提到了Kafka的安装部署所需的软件环境,这里我们再次梳理一下Flink开发所需的软件环境。 +在 1.7 节中我们简单提到了 Kafka 的安装部署所需的软件环境,这里我们再次梳理一下 Flink 开发所需的软件环境。 -1. **操作系统** - - 目前,我们可以在Linux、macOS和Windows操作系统上开发和运行Flink。类UNIX操作系统(Linux或macOS)是大数据首选的操作系统,它们对Flink的支持更好,适合进行Flink学习和开发。后文会假设读者已经拥有了一个类UNIX操作系统。Windows用户为了构建一个类UNIX环境,可以使用专门为Linux操作系统打造的子系统(Windows subsystem for Linux,即WSL)或者是Cygwin,又或者创建一个虚拟机,在虚拟机中安装Linux操作系统。 +1. 操作系统 + - 目前,我们可以在 Linux、macOS 和 Windows 操作系统上开发和运行 Flink。类 UNIX 操作系统(Linux 或 macOS)是大数据首选的操作系统,它们对 Flink 的支持更好,适合进行 Flink 学习和开发。后文会假设读者已经拥有了一个类 UNIX 操作系统。Windows 用户为了构建一个类 UNIX 环境,可以使用专门为 Linux 操作系统打造的子系统(Windows subsystem for Linux,即 WSL)或者是 Cygwin,又或者创建一个虚拟机,在虚拟机中安装 Linux 操作系统。 -2. **JDK** - - 和Kafka一样,Flink开发基于JDK,因此也需要提前安装好JDK 1.8+ (Java 8或更高的版本),配置好Java环境变量。 +2. JDK + - 和 Kafka 一样,Flink 开发基于 JDK,因此也需要提前安装好 JDK 1.8+ (Java 8 或更高的版本),配置好 Java 环境变量。 -3. **其他工具** - - 其他的工具因开发者习惯不同来安装,不是Flink开发所必需的,但这里仍然建议提前安装好以下工具。 +3. 其他工具 + - 其他的工具因开发者习惯不同来安装,不是 Flink 开发所必需的,但这里仍然建议提前安装好以下工具。 - **Apache Maven 3.0+** - - Apache Maven是一个项目管理工具,可以对Java或Scala项目进行构建及依赖管理,是进行大数据开发必备的工具。这里推荐使用Maven是因为Flink源码工程和本书的示例代码工程均使用Maven进行管理。 + - Apache Maven 是一个项目管理工具,可以对 Java 或 Scala 项目进行构建及依赖管理,是进行大数据开发必备的工具。这里推荐使用 Maven 是因为 Flink 源码工程和本书的示例代码工程均使用 Maven 进行管理。 - **IntelliJ IDEA** - - IntelliJ IDEA是一个非常强大的编辑器和开发工具,内置了Maven等一系列工具,是大数据开发必不可少的利器。Intellij IDEA本来是一个商业软件,它提供了社区免费版本,免费版本已经基本能满足绝大多数的开发需求。 - - 除IntelliJ IDEA之外,还有Eclipse IDE或NetBeans IDE等开发工具,读者可以根据自己的使用习惯选择。由于IntelliJ IDEA对Scala的支持更好,本书建议读者使用IntelliJ IDEA。 + - IntelliJ IDEA 是一个非常强大的编辑器和开发工具,内置了 Maven 等一系列工具,是大数据开发必不可少的利器。Intellij IDEA 本来是一个商业软件,它提供了社区免费版本,免费版本已经基本能满足绝大多数的开发需求。 + - 除 IntelliJ IDEA 之外,还有 Eclipse IDE 或 NetBeans IDE 等开发工具,读者可以根据自己的使用习惯选择。由于 IntelliJ IDEA 对 Scala 的支持更好,本书建议读者使用 IntelliJ IDEA。 -## 2.4.2 下载并安装Flink +## 下载并安装 Flink -从Flink官网下载编译好的Flink程序,把下载的.tgz压缩包放在你想放置的目录。在下载时,Flink提供了不同的选项,包括Scala 2.11、Scala 2.12、源码版等。其中,前两个版本是Flink官方提供的可执行版,解压后可直接使用,无须从源码开始编译打包。Scala不同版本间兼容性较差,对于Scala开发者来说,需要选择自己常用的版本,对于Java开发者来说,选择哪个Scala版本区别不大。本书写作时,使用的是Flink 1.11和Scala 2.11,读者可以根据自身情况下载相应版本。 +从 Flink 官网下载编译好的 Flink 程序,把下载的.tgz 压缩包放在你想放置的目录。在下载时,Flink 提供了不同的选项,包括 Scala 2.11、Scala 2.12、源码版等。其中,前两个版本是 Flink 官方提供的可执行版,解压后可直接使用,无须从源码开始编译打包。Scala 不同版本间兼容性较差,对于 Scala 开发者来说,需要选择自己常用的版本,对于 Java 开发者来说,选择哪个 Scala 版本区别不大。本书写作时,使用的是 Flink 1.11 和 Scala 2.11,读者可以根据自身情况下载相应版本。 -按照下面的方式,解压该压缩包,进入解压目录,并启动Flink集群。 +按照下面的方式,解压该压缩包,进入解压目录,并启动 Flink 集群。 ```bash $ tar -zxvf flink-1.11.2-bin-scala_2.11.tgz # 解压 @@ -33,13 +33,19 @@ $ cd flink-1.11.2-bin-scala_2.11 # 进入解压目录 $ ./bin/start-cluster.sh # 启动 Flink 集群 ``` -成功启动后,打开浏览器,输入`http://localhost:8081`,可以进入Flink集群的仪表盘(WebUI),如图2-4所示。Flink WebUI可以对Flink集群进行管理和监控。 +成功启动后,打开浏览器,输入 `http://localhost:8081`,可以进入 Flink 集群的仪表盘(WebUI),如 {numref}`flink-WebUI-job` 所示。Flink WebUI 可以对 Flink 集群进行管理和监控。 -![图2-4 Flink WebUI](./img/flink-WebUI.png) +```{figure} ./img/flink-WebUI.png +--- +name: flink-WebUI +width: 60% +--- +Flink WebUI +``` -## 2.4.3 创建Flink工程 +## 创建 Flink 工程 -我们使用Maven从零开始创建一个Flink工程。 +我们使用 Maven 从零开始创建一个 Flink 工程。 ```bash $ mvn archetype:generate \ @@ -53,52 +59,92 @@ $ mvn archetype:generate \ -DinteractiveMode=false ``` -archetype是Maven提供的一种项目模板,是别人提前准备好了的项目的结构框架,用户只需要使用Maven工具下载这个模板,在这个模板的基础上丰富并完善代码逻辑。主流框架一般都准备好了archetype,如Spring、Hadoop等。 +archetype 是 Maven 提供的一种项目模板,是别人提前准备好了的项目的结构框架,用户只需要使用 Maven 工具下载这个模板,在这个模板的基础上丰富并完善代码逻辑。主流框架一般都准备好了 archetype,如 Spring、Hadoop 等。 -不熟悉Maven的读者可以先使用IntelliJ IDEA内置的Maven工具,熟悉Maven的读者可直接跳过这部分。 +不熟悉 Maven 的读者可以先使用 IntelliJ IDEA 内置的 Maven 工具,熟悉 Maven 的读者可直接跳过这部分。 -如图2-5所示,在IntelliJ IDEA里依次单击“File”→“New”→“Project”,创建一个新工程。 +如 {numref}`new-project` 所示,在 IntelliJ IDEA 里依次单击“File”→“New”→“Project”,创建一个新工程。 -![图2-5 在IntelliJ IDEA中创建新工程](./img/new-project.png) +```{figure} ./img/new-project.png +--- +name: new-project +width: 60% +--- +在 IntelliJ IDEA 中创建新工程 +``` -如图2-6所示,选择左侧的“Maven”,并勾选“Create from archetype”,并单击右侧的“Add Archetype”按钮。 +如 {numref}`Maven` 所示,选择左侧的“Maven”,并勾选“Create from archetype”,并单击右侧的“Add Archetype”按钮。 -![图2-6 添加Maven项目](./img/Maven.png) +```{figure} ./img/Maven.png +--- +name: Maven +width: 60% +--- +添加 Maven 项目 +``` -如图2-7所示,在弹出的窗口中填写archetype信息。其中GroupId为org.apache.flink,ArtifactId为flink-quickstart-java,Version为1.11.2,然后单击“OK”。这里主要是告诉Maven去资源库中下载哪个版本的模板。随着Flink的迭代开发,Version也在不断更新,读者可以在Flink的Maven资源库中查看最新的版本。GroupId、ArtifactId、Version可以唯一表示一个发布出来的Java程序包。配置好后,单击Next按钮进入下一步。 +如 {numref}`archetype` 所示,在弹出的窗口中填写 archetype 信息。其中 GroupId 为 org.apache.flink,ArtifactId 为 flink-quickstart-java,Version 为 1.11.2,然后单击“OK”。这里主要是告诉 Maven 去资源库中下载哪个版本的模板。随着 Flink 的迭代开发,Version 也在不断更新,读者可以在 Flink 的 Maven 资源库中查看最新的版本。GroupId、ArtifactId、Version 可以唯一表示一个发布出来的 Java 程序包。配置好后,单击 Next 按钮进入下一步。 -![图2-7 填写archetype信息](./img/archetype.png) +```{figure} ./img/archetype.png +--- +name: archetype +width: 60% +--- +填写 archetype 信息 +``` + +如 {numref}`project-info` 所示,这一步是建立你自己的 Maven 工程,以区别其他 Maven 工程,GroupId 是你的公司或部门名称(可以随意填写),ArtifactId 是工程发布时的 Java 归档(Java Archive,JAR)包名,Version 是工程的版本。这些配置主要用于区别不同公司所发布的不同包,这与 Maven 和版本控制相关,Maven 的教程中都会介绍这些概念,这里不赘述。 + +```{figure} ./img/project-info.png +--- +name: project-info +width: 60% +--- +配置你的工程信息 +``` -如图2-8所示,这一步是建立你自己的Maven工程,以区别其他Maven工程,GroupId是你的公司或部门名称(可以随意填写),ArtifactId是工程发布时的Java归档(Java Archive,JAR)包名,Version是工程的版本。这些配置主要用于区别不同公司所发布的不同包,这与Maven和版本控制相关,Maven的教程中都会介绍这些概念,这里不赘述。 +接下来可以继续单击“Next”按钮,注意最后一步选择你的工程所在的磁盘位置,单击“Finish”按钮,如 {numref}`project-location` 所示。至此,一个 Flink 模板就下载好了。 -![图2-8 配置你的工程信息](./img/project-info.png) +```{figure} ./img/project-location.png +--- +name: project-location +width: 60% +--- +配置本工程的位置 +``` -接下来可以继续单击“Next”按钮,注意最后一步选择你的工程所在的磁盘位置,单击“Finish”按钮,如图2-9所示。至此,一个Flink模板就下载好了。 +工程结构如 {numref}`project-structure` 所示。左侧的“Project”栏是工程结构,其中 src/main/java 文件夹是 Java 代码文件存放位置,src/main/scala 是 Scala 代码文件存放位置。我们可以在 StreamingJob 这个文件上继续修改,也可以重新创建一个新文件。 -![图2-9 配置本工程的位置](./img/project-location.png) +```{figure} ./img/project-structure.png +--- +name: project-structure +width: 60% +--- +工程结构 +``` -工程结构如图2-10所示。左侧的“Project”栏是工程结构,其中src/main/java文件夹是Java代码文件存放位置,src/main/scala是Scala代码文件存放位置。我们可以在StreamingJob这个文件上继续修改,也可以重新创建一个新文件。 +:::{note} -![图2-10 工程结构](./img/project-structure.png) +开发前要单击右下角的“Import Changes”,让 Maven 导入所依赖的包。 -注意,开发前要单击右下角的“Import Changes”,让Maven导入所依赖的包。 +::: -## 2.4.4 调试和运行Flink程序 +## 调试和运行 Flink 程序 -我们创建一个新的文件,名为WordCountKafkaInStdOut.java,开始编写第一个Flink程序—流式词频统计(WordCount)程序。这个程序接收一个Kafka文本数据流,进行词频统计,然后输出到标准输出上。这里先不对程序做深入分析,后文中将会做更详细的解释。 +我们创建一个新的文件,名为 WordCountKafkaInStdOut.java,开始编写第一个 Flink 程序—流式词频统计(WordCount)程序。这个程序接收一个 Kafka 文本数据流,进行词频统计,然后输出到标准输出上。这里先不对程序做深入分析,后文中将会做更详细的解释。 -首先要设置Flink的运行环境。 +首先要设置 Flink 的运行环境。 ```java -// 设置Flink运行环境 +// 设置 Flink 运行环境 StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); ``` -设置Kafka相关参数,连接对应的服务器和端口号,读取名为Shakespeare的Topic中的数据源,将数据源命名为stream。 +设置 Kafka 相关参数,连接对应的服务器和端口号,读取名为 Shakespeare 的 Topic 中的数据源,将数据源命名为 stream。 ```java -// Kafka参数 +// Kafka 参数 Properties properties = new Properties(); properties.setProperty("bootstrap.servers", "localhost:9092"); properties.setProperty("group.id", "flink-group"); @@ -109,11 +155,11 @@ FlinkKafkaConsumer consumer = DataStream stream = env.addSource(consumer); ``` -使用Flink API处理这个数据流。 +使用 Flink API 处理这个数据流。 ```java // Transformation -// 使用Flink API对输入流的文本进行操作 +// 使用 Flink API 对输入流的文本进行操作 // 切词转换、分组、设置时间窗口、聚合 DataStream> wordCount = stream .flatMap((String line, Collector> collector) -> { @@ -145,9 +191,9 @@ wordCount.print(); env.execute("kafka streaming word count"); ``` -env.execute() 是启动Flink作业所必需的,只有在execute()方法被调用时,之前调用的各个操作才会被提交到集群上或本地计算机上运行。 +env.execute()是启动 Flink 作业所必需的,只有在 execute() 方法被调用时,之前调用的各个操作才会被提交到集群上或本地计算机上运行。 -该程序的完整代码如代码清单2-9所示。 +该程序的完整代码如代码清单 2-9 所示。 ```java import org.apache.flink.api.common.serialization.SimpleStringSchema; @@ -165,11 +211,11 @@ public class WordCountKafkaInStdOut { public static void main(String[] args) throws Exception { - // 设置Flink执行环境 + // 设置 Flink 执行环境 StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); - // Kafka参数 + // Kafka 参数 Properties properties = new Properties(); properties.setProperty("bootstrap.servers", "localhost:9092"); properties.setProperty("group.id", "flink-group"); @@ -182,7 +228,7 @@ StreamExecutionEnvironment.getExecutionEnvironment(); DataStream stream = env.addSource(consumer); // Transformation - // 使用Flink API对输入流的文本进行操作 + // 使用 Flink API 对输入流的文本进行操作 // 按空格切词、计数、分区、设置时间窗口、聚合 DataStream> wordCount = stream .flatMap((String line, Collector> collector) -> { @@ -209,7 +255,7 @@ StreamExecutionEnvironment.getExecutionEnvironment(); } ``` -代码写完后,我们还要在Maven的项目对象模型(Project Object Model,POM)文件中引入下面的依赖,让Maven可以引用Kafka。 +代码写完后,我们还要在 Maven 的项目对象模型(Project Object Model,POM)文件中引入下面的依赖,让 Maven 可以引用 Kafka。 ```xml @@ -219,38 +265,53 @@ StreamExecutionEnvironment.getExecutionEnvironment(); ``` -其中,`${scala.binary.version}`是所用的Scala版本号,可以是2.11或2.12,`${flink.version}`是所用的Flink的版本号,比如1.11.2。 +其中,`${scala.binary.version}` 是所用的 Scala 版本号,可以是 2.11 或 2.12,`${flink.version}` 是所用的 Flink 的版本号,比如 1.11.2。 -## 2.4.5 运行程序 +## 运行程序 -我们在1.7节中展示过如何启动一个Kafka集群,并向某个Topic内发送数据流。在本次Flink作业启动之前,我们还要按照1.7节提到的方式启动一个Kafka集群、创建对应的Topic,并向Topic中写入数据。 +我们在 1.7 节中展示过如何启动一个 Kafka 集群,并向某个 Topic 内发送数据流。在本次 Flink 作业启动之前,我们还要按照 1.7 节提到的方式启动一个 Kafka 集群、创建对应的 Topic,并向 Topic 中写入数据。 -1. **在IntelliJ IDEA中运行程序** - - 在IntelliJ IDEA中,单击绿色运行按钮,运行这个程序。图2-11所示的两个绿色运行按钮中的任意一个都可以运行这个程序。 - - IntelliJ IDEA下方的“Run”栏会显示程序的输出,包括本次需要输出的结果,如图2-12所示。 +1. 在 IntelliJ IDEA 中运行程序 + - 在 IntelliJ IDEA 中,单击绿色运行按钮,运行这个程序。{numref}`run` 所示的两个绿色运行按钮中的任意一个都可以运行这个程序。 + - IntelliJ IDEA 下方的“Run”栏会显示程序的输出,包括本次需要输出的结果,{numref}`result` 所示。 -![图2-11 在IntelliJ IDEA中运行Flink程序](./img/run.png) +```{figure} ./img/run.png +--- +name: run +width: 60% +--- +在 IntelliJ IDEA 中运行 Flink 程序 +``` -![图2-12 WordCount程序运行结果](./img/result.png) +```{figure} ./img/result.png +--- +name: result +width: 60% +--- +WordCount 程序运行结果 +``` -恭喜你,你的第一个Flink程序运行成功! -**提示** +恭喜你,你的第一个 Flink 程序运行成功! -如果在Intellij IDEA中运行程序时遇到`java.lang.NoClassDefFoundError` 报错,这是因为没有把依赖的类都加载进来。在Intellij IDEA中单击“Run”->“Edit configurations...”,在“Use classpath of module”选项上选择当前工程,并且勾选“Include dependencies with‘Provided’ Scope” +:::{note} -2. **向集群提交作业** - - 目前,我们学会了先下载并启动本地集群,接着在模板的基础上添加代码,并在IntelliJ IDEA中运行程序。而在生产环境中,我们一般需要将代码编译打包,提交到集群上。我们将在第9章详细介绍如何向Flink集群提交作业。 - - 注意,这里涉及两个目录:一个是我们存放刚刚编写代码的工程目录,简称工程目录;另一个是从Flink官网下载解压的Flink主目录,主目录下的bin目录中有Flink提供的命令行工具。 - - 进入工程目录,使用Maven命令行将代码编译打包。 +如果在 Intellij IDEA 中运行程序时遇到 `java.lang.NoClassDefFoundError` 报错,这是因为没有把依赖的类都加载进来。在 Intellij IDEA 中单击“Run”->“Edit configurations...”,在“Use classpath of module”选项上选择当前工程,并且勾选“Include dependencies with‘Provided’ Scope” + +::: + +2. 向集群提交作业 + - 目前,我们学会了先下载并启动本地集群,接着在模板的基础上添加代码,并在 IntelliJ IDEA 中运行程序。而在生产环境中,我们一般需要将代码编译打包,提交到集群上。我们将在第 9 章详细介绍如何向 Flink 集群提交作业。 + - 注意,这里涉及两个目录:一个是我们存放刚刚编写代码的工程目录,简称工程目录;另一个是从 Flink 官网下载解压的 Flink 主目录,主目录下的 bin 目录中有 Flink 提供的命令行工具。 + - 进入工程目录,使用 Maven 命令行将代码编译打包。 ```bash -# 使用Maven命令行将代码编译打包 -# 打好的包一般放在工程目录的target目录下 +# 使用 Maven 命令行将代码编译打包 +# 打好的包一般放在工程目录的 target 目录下 $ mvn clean package ``` -回到Flink主目录,使用Flink提供的命令行工具flink,将打包好的作业提交到集群上。命令行的参数--class用来指定哪个主类作为入口。我们之后会介绍命令行的具体使用方法。 +回到 Flink 主目录,使用 Flink 提供的命令行工具 flink,将打包好的作业提交到集群上。命令行的参数 --class 用来指定哪个主类作为入口。我们之后会介绍命令行的具体使用方法。 ```bash $ bin/flink run --class @@ -258,11 +319,17 @@ com.flink.tutorials.java.api.projects.wordcount.WordCountKafkaInStdOut /Users/luweizheng/Projects/big-data/flink-tutorials/target/flink-tutorials-0.1.jar ``` -如图2-13所示,这时,Flink WebUI上就多了一个Flink作业。 +如{numref}`flink-WebUI-job`示,这时,Flink WebUI 上就多了一个 Flink 作业。 -![图2-13 Flink WebUI中多了一个Flink作业](./img/flink-WebUI-job.png) +```{figure} ./img/flink-WebUI-job.png +--- +name: flink-WebUI-job +width: 60% +--- +Flink WebUI 中多了一个 Flink 作业 +``` -程序的输出会保存到Flink主目录下面的log目录下的.out文件中,可以使用下面的命令查看结果。 +程序的输出会保存到 Flink 主目录下面的 log 目录下的.out 文件中,可以使用下面的命令查看结果。 ```bash $ tail -f log/flink-*-taskexecutor-*.out @@ -274,12 +341,12 @@ $ tail -f log/flink-*-taskexecutor-*.out $ ./bin/stop-cluster.sh ``` -Flink开发和调试过程中,一般有如下几种方式运行程序。 -- 使用IntelliJ IDEA内置的绿色运行按钮。这种方式主要在本地调试时使用。 -- 使用Flink提供的命令行工具向集群提交作业,包括Java和Scala程序。这种方式更适合生产环境。 -- 使用Flink提供的其他命令行工具,比如针对Scala、Python和SQL的交互式环境。 +Flink 开发和调试过程中,一般有如下几种方式运行程序。 +- 使用 IntelliJ IDEA 内置的绿色运行按钮。这种方式主要在本地调试时使用。 +- 使用 Flink 提供的命令行工具向集群提交作业,包括 Java 和 Scala 程序。这种方式更适合生产环境。 +- 使用 Flink 提供的其他命令行工具,比如针对 Scala、Python 和 SQL 的交互式环境。 -对于新手,可以先使用IntelliJ IDEA提供的内置运行按钮,熟练后再使用命令行工具。 +对于新手,可以先使用 IntelliJ IDEA 提供的内置运行按钮,熟练后再使用命令行工具。 ## 本章小结 -本章中,我们回顾了Flink开发经常用到的继承和多态、泛型和函数式编程等概念,在本地搭建了一个Flink集群,创建了第一个Flink工程,并学会了如何运行Flink程序。 +本章中,我们回顾了 Flink 开发经常用到的继承和多态、泛型和函数式编程等概念,在本地搭建了一个 Flink 集群,创建了第一个 Flink 工程,并学会了如何运行 Flink 程序。 diff --git a/doc/ch-programming-basics/functional-programming.md b/doc/ch-programming-basics/functional-programming.md index 3123302..7f4d943 100644 --- a/doc/ch-programming-basics/functional-programming.md +++ b/doc/ch-programming-basics/functional-programming.md @@ -1,13 +1,13 @@ (functional-programming)= # 函数式编程 -函数式编程(Functional Programming)是一种编程范式,因其更适合做并行计算,近年来开始受到大数据开发者的广泛关注。Python、JavaScript等语言对函数式编程的支持都不错;Scala更是以函数式编程的优势在大数据领域“攻城略地”;即使是Java,也为了适应函数式编程,加强对函数式编程的支持。未来的程序员或多或少都要了解一些函数式编程思想。这里抛开一些数学推理等各类复杂的概念,仅从Flink开发的角度带领读者熟悉函数式编程。 +函数式编程(Functional Programming)是一种编程范式,因其更适合做并行计算,近年来开始受到大数据开发者的广泛关注。Python、JavaScript 等语言对函数式编程的支持都不错;Scala 更是以函数式编程的优势在大数据领域“攻城略地”;即使是 Java,也为了适应函数式编程,加强对函数式编程的支持。未来的程序员或多或少都要了解一些函数式编程思想。这里抛开一些数学推理等各类复杂的概念,仅从 Flink 开发的角度带领读者熟悉函数式编程。 -## 2.3.1 函数式编程思想简介 +## 函数式编程思想简介 在介绍函数式编程前,我们可以先回顾传统的编程范式如何解决一个数学问题。假设我们想求解一个数学表达式: -```plaintext +```text addResult = x + y result = addResult * z ``` @@ -16,29 +16,29 @@ result = addResult * z 中学时代,我们的老师在数学课上曾花费大量时间讲解函数,函数指对于自变量的映射。函数式编程的思想正是基于数学中对函数的定义。其基本思想是,在使用计算机求解问题时,我们可以把整个计算过程定义为不同的函数。比如,将这个问题转化为: -```plaintext +```text result = multiply(add(x, y), z) ``` 我们再对其做进一步的转换: -```plaintext +```text result = add(x, y).multiply(z) ``` 传统思路中要创建中间变量,要分步执行,而函数式编程的形式与数学表达式形式更为相似。人们普遍认为,这种函数式的描述更接近人类自然语言。 如果要实现这样一个函数式程序,主要需要如下两步。 -1. 实现单个函数,将零到多个输入转换成零到多个输出。比如`add()`这种带有映射关系的函数,它将两个输入转化为一个输出。 -2. 将多个函数连接起来,实现所需业务逻辑。比如,将`add()`、`multiply()`连接到一起。 +1. 实现单个函数,将零到多个输入转换成零到多个输出。比如 `add()` 这种带有映射关系的函数,它将两个输入转化为一个输出。 +2. 将多个函数连接起来,实现所需业务逻辑。比如,将 `add()`、`multiply()` 连接到一起。 -接下来我们通过Java代码来展示如何实践函数式编程思想。 +接下来我们通过 Java 代码来展示如何实践函数式编程思想。 -## 2.3.2 Lambda表达式的内部结构 +## Lambda 表达式的内部结构 -数理逻辑领域有一个名为λ演算的形式系统,主要研究如何使用函数来表达计算。一些编程语言将这个概念应用到自己的平台上,期望能实现函数式编程,取名为Lambda表达式(λ的英文拼写为Lambda)。 +数理逻辑领域有一个名为 λ 演算的形式系统,主要研究如何使用函数来表达计算。一些编程语言将这个概念应用到自己的平台上,期望能实现函数式编程,取名为 Lambda 表达式(λ 的英文拼写为 Lambda)。 -我们先看一下Java的Lambda表达式的语法规则。 +我们先看一下 Java 的 Lambda 表达式的语法规则。 ```java (parameters) -> { @@ -46,42 +46,48 @@ result = add(x, y).multiply(z) } ``` -Lambda表达式主要包括一个箭头符号`->`,其两边连接着输入参数和函数体。我们再看看代码清单 2-5中的几个Java Lambda表达式。 +Lambda 表达式主要包括一个箭头符号 `->`,其两边连接着输入参数和函数体。我们再看看代码清单 2-5 中的几个 Java Lambda 表达式。 ```java -// 1. 无参数,返回值为5 +// 1. 无参数,返回值为 5 () -> 5 -// 2. 接收1个int类型参数,将其乘以2,返回一个int类型值 +// 2. 接收 1 个 int 类型参数,将其乘以 2,返回一个 int 类型值 x -> 2 * x -// 3. 接收2个int类型参数,返回它们的差 -(x, y) -> x – y +// 3. 接收 2 个 int 类型参数,返回它们的差 +(x, y) -> x - y -// 4. 接收2个int类型参数,返回它们的和 +// 4. 接收 2 个 int 类型参数,返回它们的和 (int x, int y) -> x + y -// 5. 接收1个String类型参数,将其输出到控制台,不返回任何值 -(String s) -> { System.out.print(s); } +// 5. 接收 1 个 String 类型参数,将其输出到控制台,不返回任何值 +(String s) -> {System.out.print(s); } -// 6. 参数为圆半径,返回圆面积,返回值为double类型 +// 6. 参数为圆半径,返回圆面积,返回值为 double 类型 (double r) -> { double pi = 3.1415; return r * r * pi; } ``` -代码清单 2-5 Java Lambda表达式 +代码清单 2-5 Java Lambda 表达式 -可以看到,这几个例子都有一个`->`,表示这是一个函数式的映射,相对比较灵活的是左侧的输入参数和右侧的函数体。图2-2所示为Java Lambda表达式的拆解,这很符合数学中对一个函数做映射的思维方式。 +可以看到,这几个例子都有一个 `->`,表示这是一个函数式的映射,相对比较灵活的是左侧的输入参数和右侧的函数体。{numref}`lambda` 所示为 Java Lambda 表达式的拆解,这很符合数学中对一个函数做映射的思维方式。 -![图2-2 Java Lambda表达式拆解](./img/lambda.png) +```{figure} ./img/lambda.png +--- +name: lambda +width: 60% +--- +Java Lambda 表达式拆解 +``` -## 2.3.3 函数式接口 +## 函数式接口 -通过前文的几个例子,我们大概知道Lambda表达式的内部结构了,那么Lambda表达式到底是什么类型呢?在Java中,Lambda表达式是有类型的,它是一种接口。确切地说,Lambda表达式实现了一个函数式接口(Functional Interface),或者说,前文提到的一些Lambda表达式都是函数式接口的具体实现。 +通过前文的几个例子,我们大概知道 Lambda 表达式的内部结构了,那么 Lambda 表达式到底是什么类型呢?在 Java 中,Lambda 表达式是有类型的,它是一种接口。确切地说,Lambda 表达式实现了一个函数式接口(Functional Interface),或者说,前文提到的一些 Lambda 表达式都是函数式接口的具体实现。 -函数式接口是一种接口,并且它只有一个虚函数。因为这种接口只有一个虚函数,因此其对应英文为Single Abstract Method(SAM)。SAM表示这个接口对外只提供这一个函数的功能。如果我们想自己设计一个函数式接口,我们应该给这个接口添加`@FunctionalInterface`注解。编译器会根据这个注解确保该接口是函数式接口,当我们尝试往该接口中添加超过一个虚函数时,编译器会报错。在代码清单 2-6中,我们设计一个加法的函数式接口`AddInterface`,然后实现这个接口。 +函数式接口是一种接口,并且它只有一个虚函数。因为这种接口只有一个虚函数,因此其对应英文为 Single Abstract Method(SAM)。SAM 表示这个接口对外只提供这一个函数的功能。如果我们想自己设计一个函数式接口,我们应该给这个接口添加 `@FunctionalInterface` 注解。编译器会根据这个注解确保该接口是函数式接口,当我们尝试往该接口中添加超过一个虚函数时,编译器会报错。在代码清单 2-6 中,我们设计一个加法的函数式接口 `AddInterface`,然后实现这个接口。 ```java @FunctionalInterface @@ -91,7 +97,7 @@ interface AddInterface { public class FunctionalInterfaceExample { - public static void main( String[] args ) { + public static void main(String[] args ) { AddInterface addInt = (Integer a, Integer b) -> a + b; AddInterface addDouble = (Double a, Double b) -> a + b; @@ -107,9 +113,9 @@ public class FunctionalInterfaceExample { 代码清单 2-6 一个能够实现加法功能的函数式接口 -Lambda表达式实际上是在实现函数式接口中的虚函数,Lambda表达式的输入类型和返回类型要与虚函数定义的类型相匹配。 +Lambda 表达式实际上是在实现函数式接口中的虚函数,Lambda 表达式的输入类型和返回类型要与虚函数定义的类型相匹配。 -假如没有Lambda表达式,我们仍然可以实现这个函数式接口,只不过代码比较“臃肿”。首先,我们需要声明一个类来实现这个接口,可以是下面的类。 +假如没有 Lambda 表达式,我们仍然可以实现这个函数式接口,只不过代码比较“臃肿”。首先,我们需要声明一个类来实现这个接口,可以是下面的类。 ```java public static class MyAdd implements AddInterface { @@ -120,7 +126,7 @@ public static class MyAdd implements AddInterface { } ``` -然后,在业务逻辑中这样调用:`doubleResult = new MyAdd().add(1.1d, 2.2d);`。或者是使用匿名类,省去`MyAdd`这个名字,直接实现`AddInterface`并调用: +然后,在业务逻辑中这样调用:`doubleResult = new MyAdd().add(1.1d, 2.2d);`。或者是使用匿名类,省去 `MyAdd` 这个名字,直接实现 `AddInterface` 并调用: ```java doubleResult = new AddInterface(){ @@ -131,37 +137,37 @@ doubleResult = new AddInterface(){ }.add(1d, 2d); ``` -声明类并实现接口和使用匿名类这两种方法是Lambda表达式出现之前,Java开发者经常使用的两种方法。实际上我们想实现的逻辑仅仅是一个`a + b`,其他代码其实都是冗余的,都是为了给编译器看的,并不是为了给程序员看的。有了比较我们就会发现,Lambda表达式的简洁优雅的优势就凸显出来了。 +声明类并实现接口和使用匿名类这两种方法是 Lambda 表达式出现之前,Java 开发者经常使用的两种方法。实际上我们想实现的逻辑仅仅是一个 `a + b`,其他代码其实都是冗余的,都是为了给编译器看的,并不是为了给程序员看的。有了比较我们就会发现,Lambda 表达式的简洁优雅的优势就凸显出来了。 -为了方便大家使用,Java内置了一些的函数式接口,放在`java.util.function`包中,比如`Predicate`、`Function`等,开发者可以根据自己需求实现这些接口。这里简单展示一下这两个接口。 +为了方便大家使用,Java 内置了一些的函数式接口,放在 `java.util.function` 包中,比如 `Predicate`、`Function` 等,开发者可以根据自己需求实现这些接口。这里简单展示一下这两个接口。 -`Predicate`对输入进行判断,符合给定逻辑则返回`true`,否则返回`false`。 +`Predicate` 对输入进行判断,符合给定逻辑则返回 `true`,否则返回 `false`。 ```java @FunctionalInterface public interface Predicate { - // 判断输入的真假,返回boolean类型值 + // 判断输入的真假,返回 boolean 类型值 boolean test(T t); } ``` -`Function`接收一个类型`T`的输入,返回一个类型`R`的输出。 +`Function` 接收一个类型 `T` 的输入,返回一个类型 `R` 的输出。 ```java @FunctionalInterface public interface Function { - // 接收一个类型T的输入,返回一个类型R的输出 + // 接收一个类型 T 的输入,返回一个类型 R 的输出 R apply(T t); } ``` -部分底层代码提供了一些函数式接口供开发者调用,很多框架的API就是类似上面的函数式接口,开发者通过实现接口来完成自己的业务逻辑。Spark和Flink对外提供的Java API其实就是这种函数式接口。 +部分底层代码提供了一些函数式接口供开发者调用,很多框架的 API 就是类似上面的函数式接口,开发者通过实现接口来完成自己的业务逻辑。Spark 和 Flink 对外提供的 Java API 其实就是这种函数式接口。 -## 2.3.4 Java Stream API +## Java Stream API -Stream API是Java 8 的一大亮点,它与`java.io`包里的`InputStream`和`OutputStream`是完全不同的概念,也不是Flink、Kafka等大数据流处理框架中的数据流。它专注于对集合(Collection)对象的操作,是借助Lambda表达式的一种应用。通过Java Stream API,我们可以体验到Lambda表达式带来的编程效率的提升。 +Stream API 是 Java 8 的一大亮点,它与 `java.io` 包里的 `InputStream` 和 `OutputStream` 是完全不同的概念,也不是 Flink、Kafka 等大数据流处理框架中的数据流。它专注于对集合(Collection)对象的操作,是借助 Lambda 表达式的一种应用。通过 Java Stream API,我们可以体验到 Lambda 表达式带来的编程效率的提升。 -我们看一个简单的例子,代码清单 2-7首先过滤出非空字符串,然后求得每个字符串的长度,最终返回为一个`List`类型值。代码使用了Lambda表达式来完成对应的逻辑。 +我们看一个简单的例子,代码清单 2-7 首先过滤出非空字符串,然后求得每个字符串的长度,最终返回为一个 `List` 类型值。代码使用了 Lambda 表达式来完成对应的逻辑。 ```java List strings = Arrays.asList( @@ -177,14 +183,20 @@ List lengths = strings lengths.forEach((s) -> System.out.println(s)); ``` -代码清单 2-7 使用Lambda表达式来完成对String类型列表的操作 +代码清单 2-7 使用 Lambda 表达式来完成对 String 类型列表的操作 -这段代码中,数据先经过`stream()`方法被转换为一个`Stream`类型,后经过`filter()`、`map()`、`collect()`等处理逻辑,生成我们所需的输出。各个操作之间使用英文点号`.`来连接,这种方式被称作方法链(Method Chaining)或者链式调用。链式调用可以被抽象成一个管道(Pipeline),将代码清单2-7进行抽象,可以形成图2-3所示的Stream管道。 +这段代码中,数据先经过 `stream()` 方法被转换为一个 `Stream` 类型,后经过 `filter()`、`map()`、`collect()` 等处理逻辑,生成我们所需的输出。各个操作之间使用英文点号 `.` 来连接,这种方式被称作方法链(Method Chaining)或者链式调用。链式调用可以被抽象成一个管道(Pipeline),将代码清单 2-7 进行抽象,可以形成 {numref}`stream` 所示的 Stream 管道。 -![图2-3 Stream管道](./img/stream.png) +```{figure} ./img/stream.png +--- +name: stream +width: 60% +--- +Stream 管道 +``` -## 2.3.5 函数式编程小结 +## 函数式编程小结 -函数式编程更符合数学上函数映射的思想。具体到编程语言层面,我们可以使用Lambda表达式来快速编写函数映射,函数之间通过链式调用连接到一起,完成所需业务逻辑。Java的Lambda表达式是后来才引入的,而Scala天生就是为函数式编程所设计。由于在并行处理方面的优势,函数式编程正在被大量应用于大数据处理领域。 +函数式编程更符合数学上函数映射的思想。具体到编程语言层面,我们可以使用 Lambda 表达式来快速编写函数映射,函数之间通过链式调用连接到一起,完成所需业务逻辑。Java 的 Lambda 表达式是后来才引入的,而 Scala 天生就是为函数式编程所设计。由于在并行处理方面的优势,函数式编程正在被大量应用于大数据处理领域。 -对Lambda表达式、Java Stream API以及Flink API有了基本了解后,我们也应该注意不要将Java Stream API与Flink API混淆。 +对 Lambda 表达式、Java Stream API 以及 Flink API 有了基本了解后,我们也应该注意不要将 Java Stream API 与 Flink API 混淆。 diff --git a/doc/ch-programming-basics/generics.md b/doc/ch-programming-basics/generics.md index d0851ba..6c6aa5f 100644 --- a/doc/ch-programming-basics/generics.md +++ b/doc/ch-programming-basics/generics.md @@ -1,22 +1,22 @@ (generics)= # 泛型 -泛型(Generic)是强类型编程语言中经常使用的一种技术。很多框架的代码中都会大量使用泛型,比如在Java中我们经常看到如下的代码。 +泛型(Generic)是强类型编程语言中经常使用的一种技术。很多框架的代码中都会大量使用泛型,比如在 Java 中我们经常看到如下的代码。 ```java List strList = new ArrayList(); List doubleList = new LinkedList(); ``` -在这段代码中,`ArrayList`是一个泛型类,`List`是一个泛型接口,它们提供给开发者一个放置不同类型的集合容器,我们可以向这个集合容器中添加`String`、`Double`以及其他各类数据类型。无论内部存储的是什么类型,集合容器提供给开发者的功能都是相同的,比如`add()`,`get()`等方法。有了泛型,我们就没必要创建`StringArrayList`、`DoubleArrayList`等类了,否则代码量太大,维护起来成本极高。 +在这段代码中,`ArrayList` 是一个泛型类,`List` 是一个泛型接口,它们提供给开发者一个放置不同类型的集合容器,我们可以向这个集合容器中添加 `String`、`Double` 以及其他各类数据类型。无论内部存储的是什么类型,集合容器提供给开发者的功能都是相同的,比如 `add()`,`get()` 等方法。有了泛型,我们就没必要创建 `StringArrayList`、`DoubleArrayList` 等类了,否则代码量太大,维护起来成本极高。 -## 2.2.1 Java中的泛型 +## Java 中的泛型 -在Java中,泛型一般有3种使用方式:泛型类、泛型接口和泛型方法。一般使用尖括号`<>`来接收泛型参数。 +在 Java 中,泛型一般有 3 种使用方式:泛型类、泛型接口和泛型方法。一般使用尖括号 `<>` 来接收泛型参数。 -### 1. Java泛型类 +### Java 泛型类 -如代码清单 2-3所示,我们定义一个泛型类`MyArrayList`,这个类可以简单支持初始化和数据写入。只要在类名后面加上``就可以让这个类支持泛型,类内部的一些属性和方法都可以使用泛型`T`。或者说,类的泛型会作用到整个类。 +如代码清单 2-3 所示,我们定义一个泛型类 `MyArrayList`,这个类可以简单支持初始化和数据写入。只要在类名后面加上 `` 就可以让这个类支持泛型,类内部的一些属性和方法都可以使用泛型 `T`。或者说,类的泛型会作用到整个类。 ```java public class MyArrayList { @@ -52,16 +52,16 @@ public class MyArrayList { } ``` -代码清单 2-3 一个名为`MyArrayList`的泛型类,它可以支持简单的数据写入 +代码清单 2-3 一个名为 `MyArrayList` 的泛型类,它可以支持简单的数据写入 -当然我们也可以给这个类添加多个泛型参数,比如``, ``等。泛型一般使用大写字母表示,Java为此提供了一些大写字母使用规范,如下。 +当然我们也可以给这个类添加多个泛型参数,比如 ``, `` 等。泛型一般使用大写字母表示,Java 为此提供了一些大写字母使用规范,如下。 - `T` 代表一般的任何类。 - `E` 代表元素(Element)或异常(Exception)。 -- `K` 或`KEY`代表键(Key)。 -- `V` 代表值(Value),通常与`K`一起配合使用。 +- `K` 或 `KEY` 代表键(Key)。 +- `V` 代表值(Value),通常与 `K` 一起配合使用。 -我们也可以从父类中继承并扩展泛型,比如Flink源码中有这样一个类定义,子类继承了父类的`T`,同时自己增加了泛型`K`: +我们也可以从父类中继承并扩展泛型,比如 Flink 源码中有这样一个类定义,子类继承了父类的 `T`,同时自己增加了泛型 `K`: ```java public class KeyedStream extends DataStream { @@ -69,9 +69,9 @@ public class KeyedStream extends DataStream { } ``` -### 2. Java泛型接口 +### Java 泛型接口 -Java泛型接口的定义和Java泛型类基本相同。下面的代码展示了在`List`接口中定义`subList()`方法,该方法对数据做截取。 +Java 泛型接口的定义和 Java 泛型类基本相同。下面的代码展示了在 `List` 接口中定义 `subList()` 方法,该方法对数据做截取。 ```java public interface List { @@ -87,32 +87,32 @@ public class ArrayList implements List { ... public List subList(int fromIndex, int toIndex) { ... - // 返回一个List类型值 + // 返回一个 List 类型值 } } ``` -这个例子中,要实现的`ArrayList`依然是泛型的。需要注意的是,`class ArrayList implements List`这句声明中,`ArrayList`和`List`后面都要加上``,表明要实现的子类是泛型的。还有另外一种情况,要实现的子类不是泛型的,而是有确定类型的,如下面的代码。 +这个例子中,要实现的 `ArrayList` 依然是泛型的。需要注意的是,`class ArrayList implements List` 这句声明中,`ArrayList` 和 `List` 后面都要加上 ``,表明要实现的子类是泛型的。还有另外一种情况,要实现的子类不是泛型的,而是有确定类型的,如下面的代码。 ```java public class DoubleList implements List { ... public List subList(int fromIndex, int toIndex) { ... - // 返回一个List类型值 + // 返回一个 List 类型值 } } ``` -### 3. Java泛型方法 +### Java 泛型方法 泛型方法可以存在于泛型类中,也可以存在于普通的类中。 ```java public class MyArrayList { ... - // public关键字后的表明该方法是一个泛型方法 - // 泛型方法中的类型E和泛型类中的类型T可以不一样 + // public 关键字后的 表明该方法是一个泛型方法 + // 泛型方法中的类型 E 和泛型类中的类型 T 可以不一样 public E processElement(E element) { ... return E; @@ -120,15 +120,15 @@ public class MyArrayList { } ``` -从上面的代码可以看出,`public`或`private`关键字后的``表示该方法一个泛型方法。泛型方法的类型`E`和泛型类中的类型`T`可以不一样。或者说,如果泛型方法是泛型类的一个成员,泛型方法既可以继续使用类的类型`T`,也可以自己定义新的类型`E`。 +从上面的代码可以看出,`public` 或 `private` 关键字后的 `` 表示该方法一个泛型方法。泛型方法的类型 `E` 和泛型类中的类型 `T` 可以不一样。或者说,如果泛型方法是泛型类的一个成员,泛型方法既可以继续使用类的类型 `T`,也可以自己定义新的类型 `E`。 -### 4. 通配符 +### 通配符 -除了用 ``表示泛型,还可用 ``这种形式。`` 被称为通配符,用来适应各种不同的泛型。此外,一些代码中还会涉及通配符的边界问题,主要是为了对泛型做一些安全性方面的限制。有兴趣的读者可以自行了解泛型的通配符和边界。 +除了用 `` 表示泛型,还可用 `` 这种形式。`` 被称为通配符,用来适应各种不同的泛型。此外,一些代码中还会涉及通配符的边界问题,主要是为了对泛型做一些安全性方面的限制。有兴趣的读者可以自行了解泛型的通配符和边界。 -### 5. 类型擦除 +### 类型擦除 -Java的泛型有一个遗留问题,那就是类型擦除(Type Erasure)。我们先看一下下面的代码。 +Java 的泛型有一个遗留问题,那就是类型擦除(Type Erasure)。我们先看一下下面的代码。 ```java Class strListClass = new ArrayList().getClass(); @@ -141,27 +141,27 @@ System.out.println(intListClass); System.out.println(strListClass.equals(intListClass)); ``` -虽然声明时我们分别使用了`String`和`Integer`,但运行时关于泛型的信息被擦除了,我们无法区别`strListClass`和`intListClass`这两个类型。这是因为,泛型信息只存在于代码编译阶段,当程序运行到JVM上时,与泛型相关的信息会被擦除。类型擦除对于绝大多数应用系统开发者来说影响不太大,但是对于一些框架开发者来说,必须要注意。比如,Spark和Flink的开发者都使用了一些办法来解决类型擦除问题,对于API调用者来说,受到的影响不大。 +虽然声明时我们分别使用了 `String` 和 `Integer`,但运行时关于泛型的信息被擦除了,我们无法区别 `strListClass` 和 `intListClass` 这两个类型。这是因为,泛型信息只存在于代码编译阶段,当程序运行到 JVM 上时,与泛型相关的信息会被擦除。类型擦除对于绝大多数应用系统开发者来说影响不太大,但是对于一些框架开发者来说,必须要注意。比如,Spark 和 Flink 的开发者都使用了一些办法来解决类型擦除问题,对于 API 调用者来说,受到的影响不大。 -## 2.2.2 Scala中的泛型 +## Scala 中的泛型 -对Java的泛型有了基本了解后,我们接着来了解一下Scala中的泛型。相比而言,Scala的类型系统更复杂,这里只介绍一些简单语法,使读者能够读懂一些源码。 +对 Java 的泛型有了基本了解后,我们接着来了解一下 Scala 中的泛型。相比而言,Scala 的类型系统更复杂,这里只介绍一些简单语法,使读者能够读懂一些源码。 -Scala中,泛型放在了方括号`[]`中。或者我们可以简单地理解为,原来Java的泛型类``,现在改为`[T]`即可。 +Scala 中,泛型放在了方括号 `[]` 中。或者我们可以简单地理解为,原来 Java 的泛型类 ``,现在改为 `[T]` 即可。 -在代码清单 2-4中,我们创建了一个名为`Stack`的泛型类,并实现了两个简单的方法,类中各成员和方法都可以使用泛型`T`。我们也定义了一个泛型方法,形如`isStackPeekEquals[T]()`,方法中可以使用泛型`T`。 +在代码清单 2-4 中,我们创建了一个名为 `Stack` 的泛型类,并实现了两个简单的方法,类中各成员和方法都可以使用泛型 `T`。我们也定义了一个泛型方法,形如 `isStackPeekEquals[T]()`,方法中可以使用泛型 `T`。 ```scala object MyStackDemo { - // Stack泛型类 + // Stack 泛型类 class Stack[T] { private var elements: List[T] = Nil - def push(x: T) { elements = x :: elements } + def push(x: T) {elements = x :: elements} def peek: T = elements.head } - // 泛型方法,检查两个Stack顶部是否相同 + // 泛型方法,检查两个 Stack 顶部是否相同 def isStackPeekEquals[T](p: Stack[T], q: Stack[T]): Boolean = { p.peek == q.peek } @@ -182,10 +182,10 @@ object MyStackDemo { } ``` -代码清单 2-4 使用Scala实现一个简易的`Stack`泛型类 +代码清单 2-4 使用 Scala 实现一个简易的 `Stack` 泛型类 -## 2.2.3 泛型小结 +## 泛型小结 -本节简单总结了Java和Scala的泛型知识。对于初学者来说,泛型的语法有时候让人有些眼花缭乱,但其目的是接受不同的数据类型,增强代码的复用性。 +本节简单总结了 Java 和 Scala 的泛型知识。对于初学者来说,泛型的语法有时候让人有些眼花缭乱,但其目的是接受不同的数据类型,增强代码的复用性。 -泛型给开发者提供了不少便利,尤其是保证了底层代码简洁性。因为这些底层代码通常被封装为一个框架,会有各种各样的上层应用调用这些底层代码,进行特定的业务处理,每次调用都可能涉及泛型问题。包括Spark和Flink在内的很多框架都需要开发者基于泛型进行API调用。开发者非常有必要了解泛型的基本用法。 +泛型给开发者提供了不少便利,尤其是保证了底层代码简洁性。因为这些底层代码通常被封装为一个框架,会有各种各样的上层应用调用这些底层代码,进行特定的业务处理,每次调用都可能涉及泛型问题。包括 Spark 和 Flink 在内的很多框架都需要开发者基于泛型进行 API 调用。开发者非常有必要了解泛型的基本用法。 diff --git a/doc/ch-programming-basics/inheritance-and-polymorphism.md b/doc/ch-programming-basics/inheritance-and-polymorphism.md index b8a354a..ba83d86 100644 --- a/doc/ch-programming-basics/inheritance-and-polymorphism.md +++ b/doc/ch-programming-basics/inheritance-and-polymorphism.md @@ -1,15 +1,15 @@ (inheritance-and-polymorphism)= # 继承和多态 -继承和多态是现代编程语言中最为重要的概念。继承和多态允许用户将一些代码进行抽象,以达到复用的目的。Flink开发过程中会涉及大量的继承和多态相关问题。 +继承和多态是现代编程语言中最为重要的概念。继承和多态允许用户将一些代码进行抽象,以达到复用的目的。Flink 开发过程中会涉及大量的继承和多态相关问题。 -## 2.1.1 继承、类和接口 +## 继承、类和接口 继承在现实世界中无处不在。比如我们想描述动物和它们的行为,可以先创建一个动物类别,动物类别又可以分为狗和鱼,这样的一种层次结构其实就是编程语言中的继承关系。动物类涵盖了每种动物都有的属性,比如名字、描述信息等。从动物类衍生出的众多子类,比如鱼类、狗类等都具备动物的基本属性。不同类型的动物又有自己的特点,比如鱼会游泳、狗会吼叫。继承关系保证所有动物都具有动物的基本属性,这样就不必在创建一个新的子类的时候,将它们的基本属性(名字、描述信息)再复制一遍。同时,子类更加关注自己区别于其他类的特点,比如鱼所特有的游泳动作。 -图2-1所示为对动物进行的简单的建模。其中,每个动物都有一些基本属性,即名字(name)和描述(description);有一些基本方法,即getName()和eat(),这些基本功能共同组成了Animal类。在Animal类的基础上,可以衍生出各种各样的子类、子类的子类等。比如,Dog类有自己的dogData属性和bark()方法,同时也可以使用父类的name等属性和eat()方法。 +{numref}`extend` 所示为对动物进行的简单的建模。其中,每个动物都有一些基本属性,即名字(name)和描述(description);有一些基本方法,即 getName()和 eat(),这些基本功能共同组成了 Animal 类。在 Animal 类的基础上,可以衍生出各种各样的子类、子类的子类等。比如,Dog 类有自己的 dogData 属性和 bark()方法,同时也可以使用父类的 name 等属性和 eat() 方法。 -我们将图2-1所示的Animal类继承关系转化为代码,一个Animal公共父类可以抽象如代码清单 2-1所示。 +我们将 {numref}`extend` 所示的 Animal 类继承关系转化为代码,一个 Animal 公共父类可以抽象如代码清单 2-1 所示。 ```java public class Animal { @@ -32,11 +32,17 @@ public class Animal { } ``` -代码清单 2-1 一个简单的Animal类 +代码清单 2-1 一个简单的 Animal 类 -![图2-1 Animal类继承关系](./img/extend.png) +```{figure} ./img/extend.png +--- +name: extend +width: 60% +--- +Animal 类继承关系 +``` -子类可以拥有父类非private的属性和方法,同时可以扩展属于自己的属性和方法。比如Dog类或Fish类可以继承Animal类,可以直接复用Animal类里定义的属性和方法。这样就不存在代码的重复问题,整个工程的可维护性更好。在Java和Scala中,子类继承父类时都要使用extends关键字。代码清单 2-2实现了一个Dog类,并在里面添加了Dog类的一些特有成员。 +子类可以拥有父类非 private 的属性和方法,同时可以扩展属于自己的属性和方法。比如 Dog 类或 Fish 类可以继承 Animal 类,可以直接复用 Animal 类里定义的属性和方法。这样就不存在代码的重复问题,整个工程的可维护性更好。在 Java 和 Scala 中,子类继承父类时都要使用 extends 关键字。代码清单 2-2 实现了一个 Dog 类,并在里面添加了 Dog 类的一些特有成员。 ```java public class Dog extends Animal implements Move { @@ -60,11 +66,11 @@ public class Dog extends Animal implements Move { } ``` -代码清单 2-2 Dog类继承Animal类,并实现了一些特有的成员 +代码清单 2-2 Dog 类继承 Animal 类,并实现了一些特有的成员 -不过,Java只允许子类继承一个父类,或者说Java不支持多继承。`class A extends B, C`这样的语法在Java中是不允许的。另外,有一些方法具有更普遍的意义,比如move()方法,不仅动物会移动,一些机器(比如Machine类和Car类)也会移动。因此让Animal类和Machine类都继承一个Mover类在逻辑上没有太大意义。对于这种场景,Java提供了接口,以关键字`interface`标注,可以将一些方法进一步抽象出来,对外提供一种功能。不同的子类可以继承相同的接口,实现自己的业务逻辑,也解决了Java不允许多继承的问题。代码清单 2-2的Dog类也实现了这样一个名为Move的接口。 +不过,Java 只允许子类继承一个父类,或者说 Java 不支持多继承。`class A extends B, C` 这样的语法在 Java 中是不允许的。另外,有一些方法具有更普遍的意义,比如 move() 方法,不仅动物会移动,一些机器(比如 Machine 类和 Car 类)也会移动。因此让 Animal 类和 Machine 类都继承一个 Mover 类在逻辑上没有太大意义。对于这种场景,Java 提供了接口,以关键字 `interface` 标注,可以将一些方法进一步抽象出来,对外提供一种功能。不同的子类可以继承相同的接口,实现自己的业务逻辑,也解决了 Java 不允许多继承的问题。代码清单 2-2 的 Dog 类也实现了这样一个名为 Move 的接口。 -Move接口的定义如下。 +Move 接口的定义如下。 ```java public interface Move { @@ -72,9 +78,9 @@ public interface Move { } ``` -注意 +:::{note} -在Java中,一个类可以实现多个接口,并使用`implements`关键字。 +在 Java 中,一个类可以实现多个接口,并使用 `implements` 关键字。 ```java class ClassA implements Move, InterfaceA, InterfaceB { @@ -82,7 +88,7 @@ class ClassA implements Move, InterfaceA, InterfaceB { } ``` -在Scala中,一个类实现第一个接口时使用关键字`extends`,后面则使用关键字`with`。 +在 Scala 中,一个类实现第一个接口时使用关键字 `extends`,后面则使用关键字 `with`。 ```scala class ClassA extends Move with InterfaceA, InterfaceB { @@ -90,17 +96,19 @@ class ClassA extends Move with InterfaceA, InterfaceB { } ``` -接口与类的主要区别在于,从功能上来说,接口强调特定功能,类强调所属关系;从技术实现上来说,接口里提供的都是抽象方法,类中只有用`abstract`关键字定义的方法才是抽象方法。抽象方法是指只定义了方法签名,没有定义具体实现的方法。实现一个子类时,遇到抽象方法必须去做自己的实现。继承并实现接口时,要实现里面所有的方法,否则会报错。 +接口与类的主要区别在于,从功能上来说,接口强调特定功能,类强调所属关系;从技术实现上来说,接口里提供的都是抽象方法,类中只有用 `abstract` 关键字定义的方法才是抽象方法。抽象方法是指只定义了方法签名,没有定义具体实现的方法。实现一个子类时,遇到抽象方法必须去做自己的实现。继承并实现接口时,要实现里面所有的方法,否则会报错。 + +::: -在Flink API调用过程中,绝大多数情况下都继承一个父类或接口。对于Java用户来说,如果继承一个接口,就要使用`implements`关键字;如果继承一个类,要使用`extends`关键字。对于Scala用户来说,绝大多数情况使用`extends`关键字就足够了。 +在 Flink API 调用过程中,绝大多数情况下都继承一个父类或接口。对于 Java 用户来说,如果继承一个接口,就要使用 `implements` 关键字;如果继承一个类,要使用 `extends` 关键字。对于 Scala 用户来说,绝大多数情况使用 `extends` 关键字就足够了。 -## 2.1.2 重写与重载 +## 重写与重载 -### 1. 重写 +### 重写 -子类可以用自己的方式实现父类和接口的方法,比如前文提到的move()方法。子类的方法会覆盖父类中已有的方法,实际执行时,Java会调用子类方法,而不是使用父类方法,这个过程被称为重写(Override)。在实现重写时,需要使用`@Override`注解(Annotation)。重写可以概括为,外壳不变,核心重写;或者说方法签名等都不能与父类有变化,只修改花括号内的逻辑。 +子类可以用自己的方式实现父类和接口的方法,比如前文提到的 move() 方法。子类的方法会覆盖父类中已有的方法,实际执行时,Java 会调用子类方法,而不是使用父类方法,这个过程被称为重写(Override)。在实现重写时,需要使用 `@Override` 注解(Annotation)。重写可以概括为,外壳不变,核心重写;或者说方法签名等都不能与父类有变化,只修改花括号内的逻辑。 -虽然Java没有强制开发者使用这个注解,但是`@Override`会检查该方法是否正确重写了父类方法,如果发现其父类或接口中并没有该方法时,会报编译错误。像IntelliJ IDEA之类的集成开发环境也会有相应的提示,帮助我们检查方法是否正确重写。这里强烈建议开发者在继承并实现方法时养成使用`@Override`的习惯。 +虽然 Java 没有强制开发者使用这个注解,但是 `@Override` 会检查该方法是否正确重写了父类方法,如果发现其父类或接口中并没有该方法时,会报编译错误。像 IntelliJ IDEA 之类的集成开发环境也会有相应的提示,帮助我们检查方法是否正确重写。这里强烈建议开发者在继承并实现方法时养成使用 `@Override` 的习惯。 ```java public class ClassA implements Move { @@ -111,7 +119,7 @@ public class ClassA implements Move { } ``` -在Scala中,在方法前添加一个`override`关键字可以起到重写提示的作用。 +在 Scala 中,在方法前添加一个 `override` 关键字可以起到重写提示的作用。 ```scala class ClassA extends Move { @@ -121,14 +129,14 @@ class ClassA extends Move { } ``` -### 2. 重载 +### 重载 一个很容易和重写混淆的概念是重载(Overload)。重载是指,在一个类里有多个同名方法,这些方法名字相同、参数不同、返回类型不同。 ```java public class Overloading { - // 无参数,返回值为int类型 + // 无参数,返回值为 int 类型 public int test(){ System.out.println("test"); return 1; @@ -147,11 +155,11 @@ public class Overloading { } ``` -这段代码演示了名为test()的方法的多种不同的具体实现,每种实现在参数和返回值类型上都有区别。包括Flink在内,很多框架的源码和API应用了大量的重载,目的是给开发者提供多种不同的调用接口。 +这段代码演示了名为 test() 的方法的多种不同的具体实现,每种实现在参数和返回值类型上都有区别。包括 Flink 在内,很多框架的源码和 API 应用了大量的重载,目的是给开发者提供多种不同的调用接口。 -## 2.1.3 继承和多态小结 +## 继承和多态小结 -本节简单总结了Java/Scala的继承和多态基本原理和使用方法,包括数据建模、关键字的使用、方法的重写等。从Flink开发的角度来说,需要注意以下两点。 +本节简单总结了 Java/Scala 的继承和多态基本原理和使用方法,包括数据建模、关键字的使用、方法的重写等。从 Flink 开发的角度来说,需要注意以下两点。 -- 对于Java的一个子类,可以用`extends`关键字继承一个类,用`implements`关键字实现一个接口。如果需要覆盖父类的方法,则需要使用`@Override`注解。 -- 对于Scala的一个子类,可以用`extends`关键字继承一个类或接口。如果需要覆盖父类的方法,则需要在方法前添加一个`override`关键字。 +- 对于 Java 的一个子类,可以用 `extends` 关键字继承一个类,用 `implements` 关键字实现一个接口。如果需要覆盖父类的方法,则需要使用 `@Override` 注解。 +- 对于 Scala 的一个子类,可以用 `extends` 关键字继承一个类或接口。如果需要覆盖父类的方法,则需要在方法前添加一个 `override` 关键字。