This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,188 @@
<audio id="audio" title="导读 | 构建Kafka工程和源码阅读环境、Scala语言热身" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f5/8e/f5dd9821615c4c3dd4e62391b7a94a8e.mp3"></audio>
你好,我是胡夕。
从今天开始我们就要正式走入Kafka源码的世界了。既然咱们这个课程是教你阅读Kafka源码的那么你首先就得掌握如何在自己的电脑上搭建Kafka的源码环境甚至是知道怎么对它们进行调试。在这节课我展示了很多实操步骤建议你都跟着操作一遍否则很难会有特别深刻的认识。
话不多说,现在,我们就先来搭建源码环境吧。
## 环境准备
在阅读Kafka源码之前我们要先做一些必要的准备工作。这涉及到一些工具软件的安装比如Java、Gradle、Scala、IDE、Git等等。
如果你是在Linux或Mac系统下搭建环境你需要安装Java、IDE和Git如果你使用的是Windows那么你需要全部安装它们。
咱们这个课程统一使用下面的版本进行源码讲解。
- Oracle Java 8我们使用的是Oracle的JDK及Hotspot JVM。如果你青睐于其他厂商或开源的Java版本比如OpenJDK你可以选择安装不同厂商的JVM版本。
- Gradle 6.3我在这门课里带你阅读的Kafka源码是社区的Trunk分支。Trunk分支目前演进到了2.5版本已经支持Gradle 6.x版本。你最好安装Gradle 6.3或更高版本。
- Scala 2.13社区Trunk分支编译当前支持两个Scala版本分别是2.12和2.13。默认使用2.13进行编译因此我推荐你安装Scala 2.13版本。
- IDEA + Scala插件这门课使用IDEA作为IDE来阅读和配置源码。我对Eclipse充满敬意只是我个人比较习惯使用IDEA。另外你需要为IDEA安装Scala插件这样可以方便你阅读Scala源码。
- Git安装Git主要是为了管理Kafka源码版本。如果你要成为一名社区代码贡献者Git管理工具是必不可少的。
## 构建Kafka工程
等你准备好以上这些之后我们就可以来构建Kafka工程了。
首先我们下载Kafka源代码。方法很简单找一个干净的源码路径然后执行下列命令去下载社区的Trunk代码即可
```
$ git clone https://github.com/apache/kafka.git
```
在漫长的等待之后你的路径上会新增一个名为kafka的子目录它就是Kafka项目的根目录。如果在你的环境中上面这条命令无法执行的话你可以在浏览器中输入[https://codeload.github.com/apache/kafka/zip/trunk](https://codeload.github.com/apache/kafka/zip/trunk)下载源码ZIP包并解压只是这样你就失去了Git管理你要手动链接到远程仓库具体方法可以参考这篇[Git文档](https://help.github.com/articles/fork-a-repo/)。
下载完成后你要进入工程所在的路径下也就是进入到名为kafka的路径下然后执行相应的命令来构建Kafka工程。
如果你是在Mac或Linux平台上搭建环境那么直接运行下列命令构建即可
```
$ ./gradlew build
```
该命令首先下载Gradle Wrapper所需的jar文件然后对Kafka工程进行构建。需要注意的是在执行这条命令时你很可能会遇到下面的这个异常
```
Failed to connect to raw.githubusercontent.com port 443: Connection refused
```
如果碰到了这个异常,你也不用惊慌,你可以去这个[官网链接](https://raw.githubusercontent.com/gradle/gradle/v6.3.0/gradle/wrapper/gradle-wrapper.jar)或者是我提供的[链接](https://pan.baidu.com/s/1tuVHunoTwHfbtoqMvoTNoQ)提取码ntvd直接下载Wrapper所需的Jar包手动把这个Jar文件拷贝到kafka路径下的gradle/wrapper子目录下然后重新执行gradlew build命令去构建工程。
我想提醒你的是官网链接包含的版本号是v6.3.0但是该版本后续可能会变化因此你最好先打开gradlew文件去看一下社区使用的是哪个版本的Gradle。**一旦你发现版本不再是v6.3.0了那就不要再使用我提供的链接了。这个时候你需要直接去官网下载对应版本的Jar包**。
举个例子假设gradlew文件中使用的Gradle版本变更为v6.4.0那么你需要把官网链接URL中的版本号修改为v6.4.0然后去下载这个版本的Wrapper Jar包。
如果你是在Windows平台上构建那你就不能使用Gradle Wrapper了因为Kafka没有提供Windows平台上可运行的Wrapper Bat文件。这个时候你只能使用你自己的环境中自行安装的Gradle。具体命令是
```
kafka&gt; gradle.bat build
```
无论是gradle.bat build命令还是gradlew build命令首次运行时都要花费相当长的时间去下载必要的Jar包你要耐心地等待。
下面我用一张图给你展示下Kafka工程的各个目录以及文件
<img src="https://static001.geekbang.org/resource/image/a2/f7/a2ef664cd8d5494f55919643df1305f7.png" alt="">
这里我再简单介绍一些主要的组件路径。
- **bin目录**保存Kafka工具行脚本我们熟知的kafka-server-start和kafka-console-producer等脚本都存放在这里。
- **clients目录**保存Kafka客户端代码比如生产者和消费者的代码都在该目录下。
- **config目录**保存Kafka的配置文件其中比较重要的配置文件是server.properties。
- **connect目录**保存Connect组件的源代码。我在开篇词里提到过Kafka Connect组件是用来实现Kafka与外部系统之间的实时数据传输的。
- **core目录**保存Broker端代码。Kafka服务器端代码全部保存在该目录下。
- **streams目录**保存Streams组件的源代码。Kafka Streams是实现Kafka实时流处理的组件。
其他的目录要么不太重要,要么和配置相关,这里我就不展开讲了。
除了上面的gradlew build命令之外我再介绍一些常用的构建命令帮助你调试Kafka工程。
我们先看一下测试相关的命令。Kafka源代码分为4大部分Broker端代码、Clients端代码、Connect端代码和Streams端代码。如果你想要测试这4个部分的代码可以分别运行以下4条命令
```
$ ./gradlew core:test
$ ./gradlew clients:test
$ ./gradlew connect:[submodule]:test
$ ./gradlew streams:test
```
你可能注意到了在这4条命令中Connect组件的测试方法不太一样。这是因为Connect工程下细分了多个子模块比如api、runtime等所以你需要显式地指定要测试的子模块名才能进行测试。
如果你要单独对某一个具体的测试用例进行测试比如单独测试Broker端core包的LogTest类可以用下面的命令
```
$ ./gradlew core:test --tests kafka.log.LogTest
```
另外如果你要构建整个Kafka工程并打包出一个可运行的二进制环境就需要运行下面的命令
```
$ ./gradlew clean releaseTarGz
```
成功运行后core、clients和streams目录下就会分别生成对应的二进制发布包它们分别是
- **kafka-2.12-2.5.0-SNAPSHOT.tgz**。它是Kafka的Broker端发布包把该文件解压之后就是标准的Kafka运行环境。该文件位于core路径的/build/distributions目录。
- **kafka-clients-2.5.0-SNAPSHOT.jar**。该Jar包是Clients端代码编译打包之后的二进制发布包。该文件位于clients目录下的/build/libs目录。
- **kafka-streams-2.5.0-SNAPSHOT.jar**。该Jar包是Streams端代码编译打包之后的二进制发布包。该文件位于streams目录下的/build/libs目录。
## 搭建源码阅读环境
刚刚我介绍了如何使用Gradle工具来构建Kafka项目工程现在我来带你看一下如何利用IDEA搭建Kafka源码阅读环境。实际上整个过程非常简单。我们打开IDEA点击“文件”随后点击“打开”选择上一步中的Kafka文件路径即可。
项目工程被导入之后IDEA会对项目进行自动构建等构建完成之后你可以找到core目录源码下的Kafka.scala文件。打开它然后右键点击Kafka你应该就能看到这样的输出结果了
<img src="https://static001.geekbang.org/resource/image/ce/d2/ce0a63e7627c641da471b48a62860ad2.png" alt="">
这就是无参执行Kafka主文件的运行结果。通过这段输出我们能够学会启动Broker所必需的参数即指定server.properties文件的地址。这也是启动Kafka Broker的标准命令。
在开篇词中我也说了这个课程会聚焦于讲解Kafka Broker端源代码。因此在正式学习这部分源码之前我先给你简单介绍一下Broker端源码的组织架构。下图展示了Kafka core包的代码架构
<img src="https://static001.geekbang.org/resource/image/df/b2/dfdd73cc95ecc5390ebeb73c324437b2.png" alt="">
我来给你解释几个比较关键的代码包。
- controller包保存了Kafka控制器Controller代码而控制器组件是Kafka的核心组件后面我们会针对这个包的代码进行详细分析。
- coordinator包保存了**消费者端的GroupCoordinator代码**和**用于事务的TransactionCoordinator代码**。对coordinator包进行分析特别是对消费者端的GroupCoordinator代码进行分析是我们弄明白Broker端协调者组件设计原理的关键。
- log包保存了Kafka最核心的日志结构代码包括日志、日志段、索引文件等后面会有详细介绍。另外该包下还封装了Log Compaction的实现机制是非常重要的源码包。
- network包封装了Kafka服务器端网络层的代码特别是SocketServer.scala这个文件是Kafka实现Reactor模式的具体操作类非常值得一读。
- server包顾名思义它是Kafka的服务器端主代码里面的类非常多很多关键的Kafka组件都存放在这里比如后面要讲到的状态机、Purgatory延时机制等。
在后续的课程中我会挑选Kafka最主要的代码类进行详细分析帮助你深入了解Kafka Broker端重要组件的实现原理。
另外,虽然这门课不会涵盖测试用例的代码分析,但在我看来,**弄懂测试用例是帮助你快速了解Kafka组件的最有效的捷径之一**。如果时间允许的话我建议你多读一读Kafka各个组件下的测试用例它们通常都位于代码包的src/test目录下。拿Kafka日志源码Log来说它对应的LogTest.scala测试文件就写得非常完备里面多达几十个测试用例涵盖了Log的方方面面你一定要读一下。
## Scala 语言热身
因为Broker端的源码完全是基于Scala的所以在开始阅读这部分源码之前我还想花一点时间快速介绍一下 Scala 语言的语法特点。我先拿几个真实的 Kafka 源码片段来帮你热热身。
先来看第一个:
```
def sizeInBytes(segments: Iterable[LogSegment]): Long =
segments.map(_.size.toLong).sum
```
这是一个典型的 Scala 方法,方法名是 sizeInBytes。它接收一组 LogSegment 对象返回一个长整型。LogSegment 对象就是我们后面要谈到的日志段。你在 Kafka 分区目录下看到的每一个.log 文件本质上就是一个 LogSegment。从名字上来看这个方法计算的是这组 LogSegment 的总字节数。具体方法是遍历每个输入 LogSegment调用其 size 方法并将其累加求和之后返回。
再来看一个:
```
val firstOffset: Option[Long] = ......
def numMessages: Long = {
firstOffset match {
case Some(firstOffsetVal) if (firstOffsetVal &gt;= 0 &amp;&amp; lastOffset &gt;= 0) =&gt; (lastOffset - firstOffsetVal + 1)
case _ =&gt; 0
}
}
```
该方法是 LogAppendInfo 对象的一个方法,统计的是 Broker 端一次性批量写入的消息数。这里你需要重点关注 **match****case** 这两个关键字,你可以近似地认为它们等同于 Java 中的 switch但它们的功能要强大得多。该方法统计写入消息数的逻辑是如果 firstOffsetVal 和 lastOffset 值都大于 0则写入消息数等于两者的差值+1如果不存在 firstOffsetVal则无法统计写入消息数简单返回 0 即可。
倘若对你而言弄懂上面这两段代码有些吃力我建议你去快速地学习一下Scala语言。重点学什么呢我建议你重点学习下Scala中对于**集合的遍历语法**,以及**基于match的模式匹配用法**。
另外由于Scala具有的函数式编程风格你至少**要理解Java中Lambda表达式的含义**,这会在很大程度上帮你扫清阅读障碍。
相反地如果上面的代码对你来讲很容易理解那么读懂Broker端80%的源码应该没有什么问题。你可能还会关心剩下的那晦涩难懂的20%源码怎么办呢其实没关系你可以等慢慢精通了Scala语言之后再进行阅读它们不会对你熟练掌握核心源码造成影响的。另外后面涉及到比较难的Scala语法特性时我还会再具体给你解释的所以还是那句话你完全不用担心语言的问题
## 总结
今天是我们开启Kafka源码分析的“热身课”我给出了构建Kafka工程以及搭建Kafka源码阅读环境的具体方法。我建议你对照上面的内容完整地走一遍流程亲身体会一下Kafka工程的构建与源码工程的导入。毕竟这些都是后面阅读具体Kafka代码的前提条件。
最后我想再强调一下,阅读任何一个大型项目的源码都不是一件容易的事情,我希望你在任何时候都不要轻言放弃。很多时候,碰到读不懂的代码你就多读几遍,也许稍后就会有醍醐灌顶的感觉。
## 课后讨论
熟悉Kafka的话你一定听说过kafka-console-producer.sh脚本。我前面提到过该脚本位于工程的bin目录下你能找到它对应的Java类是哪个文件吗这个搜索过程能够给你一些寻找Kafka所需类文件的好思路你不妨去试试看。
欢迎你在留言区畅所欲言,跟我交流讨论,也欢迎你把文章分享给你的朋友。

View File

@@ -0,0 +1,133 @@
<audio id="audio" title="开篇词 | 阅读源码,逐渐成了职业进阶道路上的“必选项”" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d4/ac/d46dd95cf9595b346f148b113c99a7ac.mp3"></audio>
你好我是胡夕Apache Kafka Committer老虎证券用户增长团队负责人也是《Apache Kafka实战》这本书的作者。
2019年我在极客时间开设了我的第一个专栏《Kafka核心技术与实战》想要帮助Kafka用户掌握Kafka核心设计原理及实战应用技术。时隔一年我又带来了一个源码专栏。在这个专栏中我会带你深入到Kafka核心源码详细分析和讲述那些源码背后的架构思想和编程理念。同时我还会针对一些棘手问题给出源码级的解决思路。
## 为什么要读源码?
谈到源码分析特别是Apache Kafka这类消息引擎的源码你可能会说“我都已经在使用它了也算是也比较熟练了何必还要再花费时间去阅读源码呢
当然了一些非Kafka使用者也会说“我不用Kafka读源码对我有什么用呢
其实,在没有阅读源码之前,我也是这么想的。但是,后来在生产环境上碰到的一件事,彻底改变了我的想法。
我们知道Kafka Broker端有个log.retention.bytes参数官网的描述是它指定了留存日志的最大值。有了这个参数的“帮忙”我们信誓旦旦地向领导保证不会过多占用公司原本就很紧张的物理磁盘资源。但是最终实际占用的磁盘空间却远远超出了这个最大值。
我们查遍了各种资料,却始终找不到问题的根因,当时,我就想,只能读源码碰碰运气了。结果,源码非常清楚地说明了,这个参数能不能起作用和日志段大小息息相关。知道了这一点,问题就迎刃而解了。
这时,我才意识到,很多棘手的问题都要借助源码才能解决。
除此之外,我还发现,在很多互联网公司资深技术岗位的招聘要求上,“读过至少一种开源框架的源码”赫然在列。这也就意味着,**阅读源码正在从“加分项”向“必选项”转变掌握优秀的框架代码实现从NICE-TO-DO变成了MUST-DO**。
那,为什么读源码逐渐成为了必选项?它究竟有什么作用呢?下面我结合我自己的经历,和你说说读源码的几点收获。
**1.可以帮助你更深刻地理解内部设计原理,提升你的系统架构能力和代码功力。**
作为一款优秀的消息引擎Kafka的架构设计有很多为人称道的地方掌握了这些原理将极大地提升我们自身的系统架构能力和代码功力。
当然了即使你不使用Kafka也可以借鉴其优秀的设计理念提升你在其他框架上的系统架构能力。
你可能会问,官网文档也有相应的阐述啊,我单纯阅读文档不就够了吗?
实际上,我一直认为社区官方文档的内容有很大的提升空间,**Kafka有许多很棒的设计理念和特性在文档中并未得到充分的阐述。**
我简单举个例子。Kafka中有个非常重要的概念**当前日志段Active Segment**。Kafka的很多组件比如LogCleaner是区别对待当前日志段和非当前日志段的。但是Kafka官网上几乎完全没有提过它。
所以你看单纯依赖官网文档的话肯定是无法深入了解Kafka的。
**2.可以帮你快速定位问题并制定调优方案,减少解决问题的时间成本。**
很多人认为,阅读源码需要花费很多时间,不值得。这是一个非常大的误区。
实际上,你掌握的源码知识可以很好地指导你日后的实践,帮助你快速地定位问题的原因,迅速找到相应的解决方案。最重要的是,**如果你对源码了然于心,你会很清楚线上环境的潜在问题,提前避“坑”。在解决问题时,阅读源码其实是事半功倍的“捷径”**。
如果用时间成本来考量的话,你可以把阅读源码的时间分摊到后续解决各种问题的时间上,你会发现,这本质上是一件划算的事情。
**3.你还能参加Kafka开源社区成为一名代码贡献者Contributor。**
在社区中你能够和全世界的Kafka源码贡献者协同工作彼此分享交流想想就是一件很有意思的事情。特别是当你的代码被社区采纳之后全世界的Kafka使用者都会使用你写的代码。这简直太让人兴奋了不是吗
总而言之,**阅读源码的好处真的很多,既能精进代码功力,又能锤炼架构技巧,还能高效地解决实际问题,有百利而无一害。**
## 如何用最短的时间掌握最核心的源码?
Kafka代码有50多万行如果我们直接冲下场开始读一定会“丈二和尚摸不着头脑”。
毕竟,面对这么多代码,通读一遍的效率显然并不高。为了避免从入门到放弃,我们要用最高效的方式阅读最核心的源码。
通常来说,阅读大型项目的源码无外乎两种方法。
- **自上而下Top-Down**:从最顶层或最外层的代码一步步深入。通俗地说,就是从 main 函数开始阅读,逐渐向下层层深入,直到抵达最底层代码。这个方法的好处在于,你遍历的是完整的顶层功能路径,这对于你了解各个功能的整体流程极有帮助。
- **自下而上Bottom-Up**:跟自上而下相反,是指先独立地阅读和搞懂每个组件的代码和实现机制,然后不断向上延展,并最终把它们组装起来。该方法不是沿着功能的维度向上溯源的,相反地,它更有助于你掌握底层的基础组件代码。
这两种方法各有千秋不过在学习Kafka源码的过程中我发现将两者结合的方法其实是最高效的即先弄明白最细小单位组件的用途然后再把它们拼接组合起来掌握组件组合之后的功能。
具体怎么做呢首先你要确认最小单位的组件。我主要是看Kafka源码中的包结构package structure比如controller、log、server等这些包基本上就是按照组件来划分的。我给这些组件确定的优先级顺序是“log&gt;network&gt;controller&gt;server&gt;coordinator&gt;……”,毕竟,后面的组件会频繁地调用前面的组件。
等你清楚地了解了单个组件的源码结构,就可以试着切换成自上而下的方法,即从一个大的功能点入手,再逐步深入到各个底层组件的源码。得益于前面的积累,你会对下沉过程中碰到的各层基础代码非常熟悉,这会带给你很大的成就感。比起单纯使用自上而下或自下而上的方法,这套混合方法兼具了两者的优点。
关于如何选择大的功能点我建议你从Kafka的命令行工具开始这种串联学习搞明白这个工具的每一步都是怎么实现的并且在向下钻取的过程中不断复习单个组件的原理同时把这些组件结合在一起。
随着一遍遍地重复这个过程,你会更清楚各个组件间的交互逻辑,成为一个掌握源码的高手!
知道了方法以后我们就可以开始Kafka源码的学习了。在深入细节之前我们先来看下Kafka的源码全景图找到核心的源码。
<img src="https://static001.geekbang.org/resource/image/97/bd/971dee49c13fd501ceecaa9c573e79bd.jpg" alt="">
从功能上讲Kafka源码分为四大模块。
- 服务器端源码实现Kafka架构和各类优秀特性的基础。
- Java客户端源码定义了与Broker端的交互机制以及通用的Broker端组件支撑代码。
- Connect源码用于实现Kafka与外部系统的高性能数据传输。
- Streams源码用于实现实时的流处理功能。
可以看到服务器端源码是理解Kafka底层架构特别是系统运行原理的基础其他三个模块的源码都对它有着强烈的依赖。因此**Kafka最最精华的代码当属服务器端代码无疑**我们学习这部分代码的性价比是最高的。
## 专栏是如何设计的?
那,我们就抓紧开始吧。在这个专栏里,我基于自己对服务器端源码结构的理解,特意为你精选了下面这些源码。
**这些源码全都是极具价值的组件,也是很多实际线上问题的“高发重灾区”**。比如Kafka日志段的代码逻辑就是很多线上异常的“始作俑者”。掌握这些源码能够大大地缩短你定位问题花费的时间。
我把服务器端源码按照功能划分了7个模块每个模块会进一步划开多个子部分详细地给出各个组件级的源码分析。你可以看下这张思维导图的重点介绍。
<img src="https://static001.geekbang.org/resource/image/d0/21/d0b557ff04864adafc4cdc7572cf0a21.jpg" alt="">
### 丰富的流程图+细粒度讲解
在读源码时,我们最常犯两种错误,一种是直接深入最底层的一行行源码之中,陷入细枝末节;另一种是过于粗粒度地学习,学了跟没学没有什么区别。
为了帮助你高效地学习,我摒弃了贪多求全的源码讲解方式,而是采用“流程图+代码注释”相结合的方式,对重点内容进行细粒度讲解,还会结合我的实战经验,给你划重点。
在阅读源码之前,你可以借助图片对各个方法的实现逻辑有个大致的了解。对于重点内容,我会用详细的注释形式帮助你理解。同时,我还绘制了思维导图,帮你总结回顾。
### 真实的案例讲解,解决你的实战问题
很多人虽然也读源码,却不懂源码可以应用到什么场景、解决什么问题。事实上,我在生产环境中碰到的很多问题,都是没办法单纯依赖官方文档或搜索引擎顺利解决的。只有阅读源码,切实掌握了实现原理,才能找到解决方案。
为了帮你学以致用,我会在专栏里给你分享大量的真实案例,既帮助你提前规避陷阱,也帮你积累一些常见问题的解决方案,有一些甚至是不见诸于文档的“武林秘籍”。
### 传递社区的最新发展动向
这是专栏最有意思的一部分。我们学习的Kafka源码每天都在不断地演进着要想玩转Kafka就必须要知道社区未来的更新计划以及重大功能改进。
我会针对一些具体的主题,给你分享最新的动态资讯。我希望展现在你面前的不再是一行行冰冷的代码,而是一个生动活泼的社区形象,让你真正有参与到社区的感觉。不要小看这种感觉,有的时候,它甚至是支撑你走完源码学习之路的最强大动力。
### 课外拓展
除此之外我还会跟你分享一些延伸内容。比如成为Apache Kafka社区的代码贡献者的具体方法、实用的学习资料、经典的面试题讲解等希望你也不要错过这部分的内容。
<img src="https://static001.geekbang.org/resource/image/69/3c/6961bc3841b09586cfayyf97f1fc803c.jpg" alt="">
最后,**我还想再和你说说Scala语言的问题**。毕竟我们将要一起学习的Broker端源码是完全基于Scala的。
不过这部分源码并没有用到Scala多少高大上的语法特性。如果你有Java语言基础就更不用担心语言的问题了因为它们有很多特性非常相似。
即使你不熟悉Scala语言也没关系。你不需要完整、系统地学习这门语言只要能简单了解基本的函数式编程风格以及它的几个关键特性比如集合遍历、模式匹配等就足够了。
当然了为了不影响你理解专栏内涉及的源码我会在“导读”这节课里带你深入了解下Scala语言。同时在专栏里遇到Scala比较难的语言特性时我也会和你具体解释。所以你完全不用担心语言的问题。
好了现在我们就正式开启Apache Kafka源码分析学习之旅吧。正所谓“日拱一卒无有尽功不唐捐终入海。”阅读源码是个“苦差事”希望你别轻易放弃。毕竟**掌握了源码,你就走在了很多人的前面**。
最后我很荣幸能够和你在这里相遇一起学习交流也欢迎你给我留言说说你对Kafka源码分析的看法和疑问。

View File

@@ -0,0 +1,324 @@
<audio id="audio" title="重磅加餐 | 带你快速入门Scala语言" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fd/4f/fdb9ffacc7651e4d2c40d135b7e5644f.mp3"></audio>
你好我是胡夕。最近我在留言区看到一些同学反馈说“Scala语言不太容易理解”于是我决定临时加一节课给你讲一讲Scala语言的基础语法包括变量和函数的定义、元组的写法、函数式编程风格的循环语句的写法、它独有的case类和强大的match模式匹配功能以及Option对象的用法。
学完这节课以后相信你能够在较短的时间里掌握这些实用的Scala语法特别是Kafka源码中用到的Scala语法特性彻底扫清源码阅读路上的编程语言障碍。
## Java函数式编程
就像我在开篇词里面说的你不熟悉Scala语言其实并没有关系但你至少要对Java 8的函数式编程有一定的了解特别是要熟悉Java 8 Stream的用法。
倘若你之前没有怎么接触过Lambda表达式和Java 8 Stream我给你推荐一本好书**《Java 8实战》**。这本书通过大量的实例深入浅出地讲解了Lambda表达式、Stream以及函数式编程方面的内容你可以去读一读。
现在,我就给你分享一个实际的例子,借着它开始我们今天的所有讨论。
TopicPartition是Kafka定义的主题分区类它建模的是Kafka主题的分区对象其关键代码如下
```
public final class TopicPartition implements Serializable {
private final int partition;
private final String topic;
// 其他字段和方法......
}
```
对于任何一个分区而言一个TopicPartition实例最重要的就是**topic和partition字段**,即**Kafka的主题和分区号**。假设给定了一组分区对象List &lt; TopicPartition &gt; 我想要找出分区数大于3且以“test”开头的所有主题列表我应该怎么写这段Java代码呢你可以先思考一下然后再看下面的答案。
我先给出Java 8 Stream风格的答案
```
// 假设分区对象列表变量名是list
Set&lt;String&gt; topics = list.stream()
.filter(tp -&gt; tp.topic().startsWith(&quot;test-&quot;))
.collect(Collectors.groupingBy(TopicPartition::topic, Collectors.counting()))
.entrySet().stream()
.filter(entry -&gt; entry.getValue() &gt; 3)
.map(entry -&gt; entry.getKey()).collect(Collectors.toSet());
```
这是典型的Java 8 Stream代码里面大量使用了诸如filter、map等操作算子,以及Lambda表达式这让代码看上去一气呵成而且具有很好的可读性。
我从第3行开始解释下每一行的作用第3行的filter方法调用实现了筛选以“test”开头主题的功能第4行是运行collect方法同时指定使用groupingBy统计分区数并按照主题进行分组进而生成一个Map对象第5~7行是提取出这个Map对象的所有&lt;K, V&gt;然后再次调用filter方法将分区数大于3的主题提取出来最后是将这些主题做成一个集合返回。
其实,给出这个例子,我只是想说明,**Scala语言的编写风格和Java 8 Stream有很多相似之处**一方面代码中有大量的filter、map甚至是flatMap等操作算子另一方面代码的风格也和Java中的Lambda表达式写法类似。
如果你不信的话我们来看下Kafka中计算消费者Lag的getLag方法代码
```
private def getLag(offset: Option[Long], logEndOffset: Option[Long]): Option[Long] =
offset.filter(_ != -1).flatMap(offset =&gt; logEndOffset.map(_ - offset))
```
你看这里面也有filter和map。是不是和上面的Java代码有异曲同工之妙
如果你现在还看不懂这个方法的代码是什么意思也不用着急接下来我会带着你一步一步来学习。我相信学完了这节课以后你一定能自主搞懂getLag方法的源码含义。getLag代码是非常典型的Kafka源码一旦你熟悉了这种编码风格后面一定可以举一反三一举攻克其他的源码阅读难题。
我们先从Scala语言中的变量Variable开始说起。毕竟不管是学习任何编程语言最基础的就是先搞明白变量是如何定义的。
## 定义变量和函数
Scala有两类变量**val和var**。**val等同于Java中的final变量一旦被初始化就不能再被重新赋值了**。相反地,**var是非final变量可以重复被赋值**。我们看下这段代码:
```
scala&gt; val msg = &quot;hello, world&quot;
msg: String = hello, world
scala&gt; msg = &quot;another string&quot;
&lt;console&gt;:12: error: reassignment to val
msg = &quot;another string&quot;
scala&gt; var a:Long = 1L
a: Long = 1
scala&gt; a = 2
a: Long = 2
```
很直观对吧msg是一个vala是一个var所以msg不允许被重复赋值而a可以。我想提醒你的是**变量后面可以跟“冒号+类型”,以显式标注变量的类型**。比如这段代码第6行的“Long”就是告诉我们变量a是一个Long型。当然如果你不写“Long”也是可以的因为Scala可以通过后面的值“1L”自动判断出a的类型。
不过,很多时候,显式标注上变量类型,可以让代码有更好的可读性和可维护性。
下面我们来看下Scala中的函数如何定义。我以获取两个整数最大值的Max函数为例进行说明代码如下
```
def max(x: Int, y: Int): Int = {
if (x &gt; y) x
else y
}
```
首先def关键字表示这是一个函数。max是函数名括号中的x和y是函数输入参数它们都是Int类型的值。结尾的“Int =”组合表示max函数返回一个整数。
其次max代码使用if语句比较x和y的大小并返回两者中较大的值但是它没有使用所谓的return关键字而是直接写了x或y。**在Scala中函数体具体代码块最后一行的值将被作为函数结果返回**。在这个例子中if分支代码块的最后一行是x因此此路分支返回x。同理else分支返回y。
讲完了max函数我再用Kafka源码中的一个真实函数来帮你进一步地理解Scala函数
```
def deleteIndicesIfExist(
// 这里参数suffix的默认值是&quot;&quot;,即空字符串
// 函数结尾处的Unit类似于Java中的void关键字表示该函数不返回任何结果
baseFile: File, suffix: String = &quot;&quot;): Unit = {
info(s&quot;Deleting index files with suffix $suffix for baseFile $baseFile&quot;)
val offset = offsetFromFile(baseFile)
Files.deleteIfExists(Log.offsetIndexFile(dir, offset, suffix).toPath)
Files.deleteIfExists(Log.timeIndexFile(dir, offset, suffix).toPath)
Files.deleteIfExists(Log.transactionIndexFile(dir, offset, suffix).toPath)
}
```
和上面的max函数相比这个函数有两个额外的语法特性需要你了解。
第一个特性是**参数默认值**这是Java不支持的。这个函数的参数suffix默认值是空字符串因此以下两种调用方式都是合法的
```
deleteIndicesIfExist(baseFile) // OK
deleteIndicesIfExist(baseFile, &quot;.swap&quot;) // OK
```
第二个特性是**该函数的返回值Unit**。Scala的Unit类似于Java的void因此deleteIndicesIfExist函数的返回值是Unit类型表明它仅仅是执行一段逻辑代码不需要返回任何结果。
## 定义元组Tuple
接下来我们来看下Scala中的元组概念。**元组是承载数据的容器,一旦被创建,就不能再被更改了**。元组中的数据可以是不同数据类型的。定义和访问元组的方法很简单,请看下面的代码:
```
scala&gt; val a = (1, 2.3, &quot;hello&quot;, List(1,2,3)) // 定义一个由4个元素构成的元组每个元素允许是不同的类型
a: (Int, Double, String, List[Int]) = (1,2.3,hello,List(1, 2, 3))
scala&gt; a._1 // 访问元组的第一个元素
res0: Int = 1
scala&gt; a._2 // 访问元组的第二个元素
res1: Double = 2.3
scala&gt; a._3 // 访问元组的第三个元素
res2: String = hello
scala&gt; a._4 // 访问元组的第四个元素
res3: List[Int] = List(1, 2, 3)
```
总体上而言元组的用法简单而优雅。Kafka源码中也有很多使用元组的例子比如
```
def checkEnoughReplicasReachOffset(requiredOffset: Long): (Boolean, Errors) = { // 返回(BooleanErrors)类型的元组
......
if (minIsr &lt;= curInSyncReplicaIds.size) {
......
(true, Errors.NONE)
} else
(false, Errors.NOT_ENOUGH_REPLICAS_AFTER_APPEND)
}
```
checkEnoughReplicasReachOffset方法返回一个(Boolean, Errors)类型的元组即元组的第一个元素或字段是Boolean类型第二个元素是Kafka自定义的Errors类型。
该方法会判断某分区ISR中副本的数量是否大于等于所需的最小ISR副本数如果是就返回true, Errors.NONE元组否则返回falseErrors.NOT_ENOUGH_REPLICAS_AFTER_APPEND。目前你不必理会代码中minIsr或curInSyncReplicaIds的含义仅仅掌握Kafka源码中的元组用法就够了。
## 循环写法
下面我们来看下Scala中循环的写法。我们常见的循环有两种写法**命令式编程方式**和**函数式编程方式**。我们熟悉的是第一种比如下面的for循环代码
```
scala&gt; val list = List(1, 2, 3, 4, 5)
list: List[Int] = List(1, 2, 3, 4, 5)
scala&gt; for (element &lt;- list) println(element)
1
2
3
4
5
```
Scala支持的函数式编程风格的循环类似于下面的这种代码
```
scala&gt; list.foreach(e =&gt; println(e))
// 省略输出......
scala&gt; list.foreach(println)
// 省略输出......
```
特别是代码中的第二种写法会让代码写得异常简洁。我用一段真实的Kafka源码再帮你加强下记忆。它取自SocketServer组件中stopProcessingRequests方法主要目的是让Broker停止请求和新入站TCP连接的处理。SocketServer组件是实现Kafka网络通信的重要组件后面我会花3节课的时间专门讨论它。这里咱们先来学习下这段明显具有函数式风格的代码
```
// dataPlaneAcceptors:ConcurrentHashMap&lt;Endpoint, Acceptor&gt;对象
dataPlaneAcceptors.asScala.values.foreach(_.initiateShutdown())
```
这一行代码首先调用asScala方法将Java的ConcurrentHashMap转换成Scala语言中的concurrent.Map对象然后获取它保存的所有Acceptor线程通过foreach循环调用每个Acceptor对象的initiateShutdown方法。如果这个逻辑用命令式编程来实现至少要几行甚至是十几行才能完成。
## case类
在Scala中case类与普通类是类似的只是它具有一些非常重要的不同点。Case类非常适合用来表示不可变数据。同时它最有用的一个特点是case类自动地为所有类字段定义Getter方法这样能省去很多样本代码。我举个例子说明一下。
如果我们要编写一个类表示平面上的一个点Java代码大概长这个样子
```
public final class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
// setter methods......
// getter methods......
}
```
我就不列出完整的Getter和Setter方法了写过Java的你一定知道这些样本代码。但如果用Scala的case类只需要写一行代码就可以了
```
case class Point(x:Int, y: Int) // 默认写法。不能修改x和y
case class Point(var x: Int, var y: Int) // 支持修改x和y
```
Scala会自动地帮你创建出x和y的Getter方法。默认情况下x和y不能被修改如果要支持修改你要采用上面代码中第二行的写法。
## 模式匹配
有了case类的基础接下来我们就可以学习下Scala中强大的模式匹配功能了。
和Java中switch仅仅只能比较数值和字符串相比Scala中的match要强大得多。我先来举个例子
```
def describe(x: Any) = x match {
case 1 =&gt; &quot;one&quot;
case false =&gt; &quot;False&quot;
case &quot;hi&quot; =&gt; &quot;hello, world!&quot;
case Nil =&gt; &quot;the empty list&quot;
case e: IOException =&gt; &quot;this is an IOException&quot;
case s: String if s.length &gt; 10 =&gt; &quot;a long string&quot;
case _ =&gt; &quot;something else&quot;
}
```
这个函数的x是Any类型这相当于Java中的Object类型即所有类的父类。注意倒数第二行的“case _”的写法它是用来兜底的。如果上面的所有case分支都不匹配那就进入到这个分支。另外它还支持一些复杂的表达式比如倒数第三行的case分支表示x是字符串类型而且x的长度超过10的话就进入到这个分支。
要知道Java在JDK 14才刚刚引入这个相同的功能足见Scala语法的强大和便捷。
## Option对象
最后,我再介绍一个小的语法特性或语言特点:**Option对象**。
实际上Java也引入了类似的类Optional。根据我的理解不论是Scala中的Option还是Java中的Optional都是用来帮助我们更好地规避NullPointerException异常的。
Option表示一个容器对象里面可能装了值也可能没有装任何值。由于是容器因此一般都是这样的写法Option[Any]。中括号里面的Any就是上面说到的Any类型它能是任何类型。如果值存在的话就可以使用Some(x)来获取值或给值赋值否则就使用None来表示。我用一段代码帮助你理解
```
scala&gt; val keywords = Map(&quot;scala&quot; -&gt; &quot;option&quot;, &quot;java&quot; -&gt; &quot;optional&quot;) // 创建一个Map对象
keywords: scala.collection.immutable.Map[String,String] = Map(scala -&gt; option, java -&gt; optional)
scala&gt; keywords.get(&quot;java&quot;) // 获取key值为java的value值。由于值存在故返回Some(optional)
res24: Option[String] = Some(optional)
scala&gt; keywords.get(&quot;C&quot;) // 获取key值为C的value值。由于不存在故返回None
res23: Option[String] = None
```
Option对象还经常与模式匹配语法一起使用以实现不同情况下的处理逻辑。比如Option对象有值和没有值时分别执行什么代码。具体写法你可以参考下面这段代码
```
def display(game: Option[String]) = game match {
case Some(s) =&gt; s
case None =&gt; &quot;unknown&quot;
}
scala&gt; display(Some(&quot;Heroes 3&quot;))
res26: String = Heroes 3
scala&gt; display(Some(&quot;StarCraft&quot;))
res27: String = StarCraft
scala&gt; display(None)
res28: String = unknown
```
## 总结
今天我们专门花了些时间快速地学习了一下Scala语言的语法这些语法能够帮助你更快速地上手Kafka源码的学习。现在让我们再来看下这节课刚开始时我提到的getLag方法源码你看看现在是否能够说出它的含义。我再次把它贴出来
```
private def getLag(offset: Option[Long], logEndOffset: Option[Long]): Option[Long] =
offset.filter(_ != -1).flatMap(offset =&gt; logEndOffset.map(_ - offset))
```
现在你应该知道了它是一个函数接收两个类型为Option[Long]的参数同时返回一个Option[Long]的结果。代码逻辑很简单首先判断offset是否有值且不能是-1。这些都是在filter函数中完成的之后调用flatMap方法计算logEndOffset值与offset的差值最后返回这个差值作为Lag。
这节课结束以后,语言问题应该不再是你学习源码的障碍了,接下来,我们就可以继续专心地学习源码了。借着这个机会,我还想跟你多说几句。
很多时候,我们都以为,要有足够强大的毅力才能把源码学习坚持下去,但实际上,毅力是在你读源码的过程中培养起来的。
考虑到源码并不像具体技术本身那样容易掌握我力争用最清晰易懂的方式来讲这门课。所以我希望你每天都能花一点点时间跟着我一起学习我相信到结课的时候你不仅可以搞懂Kafka Broker端源码还能提升自己的毅力。而毅力和执行力的提升可能比技术本身的提升还要弥足珍贵。
另外我还想给你分享一个小技巧想要养成每天阅读源码的习惯你最好把目标拆解得足够小。人的大脑都是有惰性的比起“我每天要读1000行源码”它更愿意接受“每天只读20行”。你可能会说每天读20行这也太少了吧其实不是的。只要你读了20行源码你就一定能再多读一些“20行”这个小目标只是为了促使你愿意开始去做这件事情。而且即使你真的只读了20行那又怎样读20行总好过1行都没有读对吧
当然了,阅读源码经常会遇到一种情况,那就是读不懂某部分的代码。没关系,读不懂的代码,你可以选择先跳过。
如果你是个追求完美的人,那么对于读不懂的代码,我给出几点建议:
1. **多读几遍**。不要小看这个朴素的建议。有的时候,我们的大脑是很任性的,只让它看一遍代码,它可能“傲娇地表示不理解”,但你多给它看几遍,也许就恍然大悟了。
1. **结合各种资料来学习**。比如,社区或网上关于这部分代码的设计文档、源码注释或源码测试用例等。尤其是搞懂测试用例,往往是让我们领悟代码精神最快捷的办法了。
总之,阅读源码是一项长期的工程,不要幻想有捷径或一蹴而就,微小积累会引发巨大改变,我们一起加油。