🔎大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎
📝个人主页-Sonhhxg_柒的博客_CSDN博客 📃
🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝
📣系列专栏 - 机器学习【ML】 自然语言处理【NLP】 深度学习【DL】
🖍foreword
✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。
如果你对这个系列感兴趣的话,可以关注订阅哟👋
文章目录
在第4章和第5章中,我们介绍了 Spark SQL 和 DataFrame API。我们研究了如何连接到内置和外部数据源,了解了 Spark SQL 引擎,并探讨了诸如 SQL 和 DataFrame 之间的互操作性、创建和管理视图和表以及高级 DataFrame 和 SQL 转换等主题。
尽管我们在第 3 章中简要介绍了 Dataset API ,但我们略读了 Datasets(强类型分布式集合)在 Spark 中的创建、存储、序列化和反序列化的主要方面。
在本章中,我们将深入了解数据集:我们将探索在 Java 和 Scala 中使用数据集,Spark 如何管理内存以适应作为高级 API 的一部分的数据集结构,以及与使用数据集相关的成本。
Java 和 Scala 的单一 API
您可能还记得第 3 章(图 3-1和表 3-6),数据集为强类型对象提供了统一且单一的 API。在 Spark 支持的语言中,只有 Scala 和 Java 是强类型的;因此,Python 和 R 仅支持无类型的 DataFrame API。
数据集是特定领域的类型化对象,可以使用函数式编程或您熟悉的 DataFrame API 中的 DSL 运算符并行操作。
由于这个单一的 API,Java 开发人员不再冒险落后。例如,将来对 Scala 的groupBy()
、flatMap()
、map()
或filter()
API 的任何接口或行为更改对于 Java 也将是相同的,因为它是两个实现通用的单一接口。
用于数据集的 Scala 案例类和 JavaBean
如果您还记得第 3 章(表 3-2),Spark 具有内部数据类型,例如StringType
、BinaryType
、IntegerType
、BooleanType
和MapType
,它用于在 Spark 操作期间无缝映射到 Scala 和 Java 中的语言特定数据类型。这种映射是通过编码器完成的,我们将在本章后面讨论。
为了在 Scala 中创建类型化对象Dataset[T]
,T
您需要一个定义该对象的案例类。使用第 3 章(表 3-1)中的示例数据,假设我们有一个 JSON 文件,其中包含数百万关于博客作者以以下格式撰写的关于 Apache Spark 的条目:
{id: 1, first: "Jules", last: "Damji", url: "https://tinyurl.1", date:
"1/4/2016", hits: 4535, campaigns: {"twitter", "LinkedIn"}},
...
{id: 87, first: "Brooke", last: "Wenig", url: "https://tinyurl.2", date:
"5/5/2018", hits: 8908, campaigns: {"twitter", "LinkedIn"}}
要创建分布式Dataset[Bloggers]
,我们必须首先定义一个 Scala 案例类,该类定义包含 Scala 对象的每个单独字段。此案例类用作类型化对象的蓝图或模式Bloggers
:
// In Scala
case class Bloggers(id:Int, first:String, last:String, url:String, date:String,
hits: Int, campaigns:Array[String])
我们现在可以从数据源中读取文件:
val bloggers = "../data/bloggers.json"
val bloggersDS = spark
.read
.format("json")
.option("path", bloggers)
.load()
.as[Bloggers]
生成的分布式数据集合中的每一行都是类型Bloggers
。
同样,您可以在 Java 中创建一个 JavaBean 类型的类Bloggers
,然后使用编码器创建一个Dataset<Bloggers>
:
// In Java
import org.apache.spark.sql.Encoders;
import java.io.Serializable;
public class Bloggers implements Serializable {
private int id;
private String first;
private String last;
private String url;
private String date;
private int hits;
private Array[String] campaigns;
// JavaBean getters and setters
int getID() { return id; }
void setID(int i) { id = i; }
String getFirst() { return first; }
void setFirst(String f) { first = f; }
String getLast() { return last; }
void setLast(String l) { last = l; }
String getURL() { return url; }
void setURL (String u) { url = u; }
String getDate() { return date; }
Void setDate(String d) { date = d; }
int getHits() { return hits; }
void setHits(int h) { hits = h; }
Array[String] getCampaigns() { return campaigns; }
void setCampaigns(Array[String] c) { campaigns = c; }
}
// Create Encoder
Encoder<Bloggers> BloggerEncoder = Encoders.bean(Bloggers.class);
String bloggers = "../bloggers.json"
Dataset<Bloggers>bloggersDS = spark
.read
.format("json")
.option("path", bloggers)
.load()
.as(BloggerEncoder);
如您所见,在 Scala 和 Java 中创建数据集需要一些先见之明,因为您必须知道正在读取的行的所有单个列名称和类型。与 DataFrame 不同,您可以选择让 Spark 推断架构,Dataset API 要求您提前定义数据类型,并且您的案例类或 JavaBean 类与您的架构匹配。
笔记
Scala 案例类或 Java 类定义中的字段名称必须与数据源中的顺序匹配。数据中每一行的列名会自动映射到类中对应的名称,并自动保留类型。
如果字段名称与您的输入数据匹配,您可以使用现有的 Scala 案例类或 JavaBean 类。使用 Dataset API与使用 DataFrame一样简单、简洁和声明性。对于大多数数据集的转换,您可以使用在前几章中学习过的相同的关系运算符。
使用数据集
创建示例数据集的一种简单而动态的方法是使用实SparkSession
例。在这个场景中,为了说明的目的,我们动态地创建一个包含三个字段的 Scala 对象:(uid
用户的唯一 ID)、uname
(随机生成的用户名字符串)和usage
(服务器或服务使用的分钟数)。
创建示例数据
// In Scala
import scala.util.Random._
// Our case class for the Dataset
case class Usage(uid:Int, uname:String, usage: Int)
val r = new scala.util.Random(42)
// Create 1000 instances of scala Usage class
// This generates data on the fly
val data = for (i <- 0 to 1000)
yield (Usage(i, "user-" + r.alphanumeric.take(5).mkString(""),
r.nextInt(1000)))
// Create a Dataset of Usage typed data
val dsUsage = spark.createDataset(data)
dsUsage.show(10)
+---+----------+-----+
|uid| uname|usage|
+---+----------+-----+
| 0|user-Gpi2C| 525|
| 1|user-DgXDi| 502|
| 2|user-M66yO| 170|
| 3|user-xTOn6| 913|
| 4|user-3xGSz| 246|
| 5|user-2aWRN| 727|
| 6|user-EzZY1| 65|
| 7|user-ZlZMZ| 935|
| 8|user-VjxeG| 756|
| 9|user-iqf1P| 3|
+---+----------+-----+
only showing top 10 rows
在 Java 中这个想法是相似的,但我们必须使用显式Encoder
s(在 Scala 中,Spark 隐式处理):
// In Java
import org.apache.spark.sql.Encoders;
import org.apache.commons.lang3.RandomStringUtils;
import java.io.Serializable;
import java.util.Random;
import java.util.ArrayList;
import java.util.List;
// Create a Java class as a Bean
public class Usage implements Serializable {
int uid; // user id
String uname; // username
int usage; // usage
public Usage(int uid, String uname, int usage) {
this.uid = uid;
this.uname = uname;
this.usage = usage;
}
// JavaBean getters and setters
public int getUid() { return this.uid; }
public void setUid(int uid) { this.uid = uid; }
public String getUname() { return this.uname; }
public void setUname(String uname) { this.uname = uname; }
public int getUsage() { return this.usage; }
public void setUsage(int usage) { this.usage = usage; }
public Usage() {
}
public String toString() {
return "uid: '" + this.uid + "', uame: '" + this.uname + "',
usage: '" + this.usage + "'";
}
}
// Create an explicit Encoder
Encoder<Usage> usageEncoder = Encoders.bean(Usage.class);
Random rand = new Random();
rand.setSeed(42);
List<Usage> data = new ArrayList<Usage>()
// Create 1000 instances of Java Usage class
for (int i = 0; i < 1000; i++) {
data.add(new Usage(i, "user" +
RandomStringUtils.randomAlphanumeric(5),
rand.nextInt(1000));
// Create a Dataset of Usage typed data
Dataset<Usage> dsUsage = spark.createDataset(data, usageEncoder);
笔记
Scala 和 Java 生成的 Dataset 会有所不同,因为随机种子算法可能不同。因此,您的 Scala 和 Java 的查询结果会有所不同。
现在我们已经生成了数据集,dsUsage
让我们执行一些我们在前几章中完成的常见转换。
转换样本数据
回想一下,数据集是特定领域对象的强类型集合。这些对象可以使用函数或关系操作并行转换。这些转换的示例包括map()
、reduce()
、filter()
、select()
和aggregate()
。作为高阶函数的示例,这些方法可以将 lambda、闭包或函数作为参数并返回结果。因此,它们非常适合函数式编程。
Scala 是一种函数式编程语言,最近 lambda、函数式参数和闭包也被添加到 Java 中。让我们在 Spark 中尝试几个高阶函数,并将函数式编程结构与我们之前创建的示例数据一起使用。
高阶函数和函数式编程
举个简单的例子,让我们使用filter()
返回我们dsUsage
数据集中所有使用时间超过 900 分钟的用户。一种方法是使用函数表达式作为filter()
方法的参数:
// In Scala
import org.apache.spark.sql.functions._
dsUsage
.filter(d => d.usage > 900)
.orderBy(desc("usage"))
.show(5, false)
另一种方法是定义一个函数并将该函数作为参数提供给filter()
:
def filterWithUsage(u: Usage) = u.usage > 900
dsUsage.filter(filterWithUsage(_)).orderBy(desc("usage")).show(5)
+---+----------+-----+
|uid| uname|usage|
+---+----------+-----+
|561|user-5n2xY| 999|
|113|user-nnAXr| 999|
|605|user-NL6c4| 999|
|634|user-L0wci| 999|
|805|user-LX27o| 996|
+---+----------+-----+
only showing top 5 rows
在第一种情况下,我们使用 lambda 表达式,{d.usage > 900}
作为filter()
方法的参数,而在第二种情况下,我们定义了一个 Scala 函数,def filterWithUsage(u: Usage) = u.usage > 900
。在这两种情况下,该filter()
方法都会遍历Usage
分布式数据集中对象的每一行,并应用表达式或执行函数,Usage
为表达式或函数的值为 的行返回类型为的新数据集true
。(有关方法签名的详细信息,请参阅Scala 文档。)
在 Java 中, to 的参数filter()
类型为FilterFunction<T>。这可以匿名内联或使用命名函数定义。在本例中,我们将按名称定义函数并将其分配给变量f
。应用此函数filter()
将返回一个新数据集,其中包含我们过滤条件为的所有行true
:
// In Java
// Define a Java filter function
FilterFunction<Usage> f = new FilterFunction<Usage>() {
public boolean call(Usage u) {
return (u.usage > 900);
}
};
// Use filter with our function and order the results in descending order
dsUsage.filter(f).orderBy(col("usage").desc()).show(5);
+---+----------+-----+
|uid|uname |usage|
+---+----------+-----+
|67 |user-qCGvZ|997 |
|878|user-J2HUU|994 |
|668|user-pz2Lk|992 |
|750|user-0zWqR|991 |
|242|user-g0kF6|989 |
+---+----------+-----+
only showing top 5 rows
并非所有 lambda 或函数参数都必须计算为Boolean
值;他们也可以返回计算值。考虑这个使用高阶函数的例子map()
,我们的目标是找出每个用户的使用成本,其usage
价值超过某个阈值,这样我们就可以为这些用户提供每分钟的特价。
// In Scala
// Use an if-then-else lambda expression and compute a value
dsUsage.map(u => {if (u.usage > 750) u.usage * .15 else u.usage * .50 })
.show(5, false)
// Define a function to compute the usage
def computeCostUsage(usage: Int): Double = {
if (usage > 750) usage * 0.15 else usage * 0.50
}
// Use the function as an argument to map()
dsUsage.map(u => {computeCostUsage(u.usage)}).show(5, false)
+------+
|value |
+------+
|262.5 |
|251.0 |
|85.0 |
|136.95|
|123.0 |
+------+
only showing top 5 rows
要map()
在 Java 中使用,您必须定义一个MapFunction<T>. 这可以是匿名类或扩展的已定义类MapFunction<T>
。对于这个例子,我们内联使用它——也就是说,在方法调用本身中:
// In Java
// Define an inline MapFunction
dsUsage.map((MapFunction<Usage, Double>) u -> {
if (u.usage > 750)
return u.usage * 0.15;
else
return u.usage * 0.50;
}, Encoders.DOUBLE()).show(5); // We need to explicitly specify the Encoder
+------+
|value |
+------+
|65.0 |
|114.45|
|124.0 |
|132.6 |
|145.5 |
+------+
only showing top 5 rows
尽管我们已经计算了使用成本的值,但我们不知道计算值与哪些用户相关联。我们如何获得这些信息?
步骤很简单:
-
创建一个 Scala 案例类或 JavaBean 类
UsageCost
,带有一个名为 的附加字段或列cost
。 -
定义一个函数来计算并在方法
cost
中使用它。map()
这是 Scala 中的样子:
// In Scala
// Create a new case class with an additional field, cost
case class UsageCost(uid: Int, uname:String, usage: Int, cost: Double)
// Compute the usage cost with Usage as a parameter
// Return a new object, UsageCost
def computeUserCostUsage(u: Usage): UsageCost = {
val v = if (u.usage > 750) u.usage * 0.15 else u.usage * 0.50
UsageCost(u.uid, u.uname, u.usage, v)
}
// Use map() on our original Dataset
dsUsage.map(u => {computeUserCostUsage(u)}).show(5)
+---+----------+-----+------+
|uid| uname|usage| cost|
+---+----------+-----+------+
| 0|user-Gpi2C| 525| 262.5|
| 1|user-DgXDi| 502| 251.0|
| 2|user-M66yO| 170| 85.0|
| 3|user-xTOn6| 913|136.95|
| 4|user-3xGSz| 246| 123.0|
+---+----------+-----+------+
only showing top 5 rows
现在我们有了一个转换后的数据集,其中包含一个由转换cost
中的函数计算的新列,map()
以及所有其他列。
同样,在 Java 中,如果我们想要与每个用户关联的成本,我们需要定义一个 JavaBean 类UsageCost
和MapFunction<T>
. 有关完整的 JavaBean 示例,请参阅本书的GitHub 存储库;为简洁起见,我们将仅在MapFunction<T>
此处显示内联:
// In Java
// Get the Encoder for the JavaBean class
Encoder<UsageCost> usageCostEncoder = Encoders.bean(UsageCost.class);
// Apply map() function to our data
dsUsage.map( (MapFunction<Usage, UsageCost>) u -> {
double v = 0.0;
if (u.usage > 750) v = u.usage * 0.15; else v = u.usage * 0.50;
return new UsageCost(u.uid, u.uname,u.usage, v); },
usageCostEncoder).show(5);
+------+---+----------+-----+
| cost|uid| uname|usage|
+------+---+----------+-----+
| 65.0| 0|user-xSyzf| 130|
|114.45| 1|user-iOI72| 763|
| 124.0| 2|user-QHRUk| 248|
| 132.6| 3|user-8GTjo| 884|
| 145.5| 4|user-U4cU1| 970|
+------+---+----------+-----+
only showing top 5 rows
关于使用高阶函数和数据集,有几点需要注意:
-
我们使用类型化的 JVM 对象作为函数的参数。
-
我们使用点表示法(来自面向对象的编程)来访问类型化 JVM 对象中的各个字段,使其更易于阅读。
-
我们的一些函数和 lambda 签名可以是类型安全的,确保编译时错误检测并指示 Spark 处理哪些数据类型、执行哪些操作等。
-
我们的代码具有可读性、表达性和简洁性,在 lambda 表达式中使用 Java 或 Scala 语言特性。
-
Spark 在 Java 和 Scala 中都提供了与高阶函数构造等效的
map()
和filter()
没有的高阶函数构造,因此您不必将函数式编程与 Datasets 或 DataFrames 一起使用。相反,您可以简单地使用条件 DSL 运算符或 SQL 表达式:例如,dsUsage.filter("usage > 900")
或dsUsage($"usage" > 900)
. (有关这方面的更多信息,请参阅“使用数据集的成本”。) -
对于数据集,我们使用编码器,这是一种在 JVM 和 Spark 的数据类型内部二进制格式之间有效转换数据的机制(更多信息请参见“数据集编码器”)。
笔记
高阶函数和函数式编程并不是 Spark 数据集独有的;您也可以将它们与 DataFrame 一起使用。回想一下,DataFrame 是一个
Dataset[Row]
,其中Row
是一个通用的无类型 JVM 对象,可以保存不同类型的字段。方法签名采用对 进行操作的表达式或函数Row
,这意味着每个Row
的数据类型都可以作为表达式或函数的输入值。
将 DataFrame 转换为数据集
对于查询和构造的强类型检查,您可以将 DataFrames 转换为 Datasets。要将现有 DataFrame 转换df
为 Dataset 类型SomeCaseClass
,只需使用df.as[SomeCaseClass]
符号。我们之前看到了一个这样的例子:
// In Scala
val bloggersDS = spark
.read
.format("json")
.option("path", "/data/bloggers/bloggers.json")
.load()
.as[Bloggers]
spark.read.format("json")
返回 a DataFrame<Row>
,它在 Scala 中是Dataset[Row]
. Using.as[Bloggers]
指示 Spark 使用本章后面讨论的编码器,将对象从 Spark 的内部内存表示序列化/反序列化为 JVMBloggers
对象.
数据集和数据帧的内存管理
Spark 是一种密集型内存分布式大数据引擎,因此其内存的有效利用对其执行速度至关重要。1纵观其发布历史,Spark 对内存的使用发生了显着变化:
-
Spark 1.0 使用基于 RDD 的 Java 对象进行内存存储、序列化和反序列化,这在资源方面很昂贵且速度很慢。此外,存储是在 Java 堆上分配的,因此对于大型数据集,您只能受 JVM 垃圾收集 (GC) 的支配。
-
Spark 1.x 引入了Project Tungsten。它的一个突出特点是一种新的内部基于行的格式,使用偏移量和指针在堆外内存中布局数据集和数据帧。Spark 使用一种称为编码器的高效机制在 JVM 与其内部 Tungsten 格式之间进行序列化和反序列化。在堆外分配内存意味着 Spark 较少受到 GC 的阻碍。
-
Spark 2.x 引入了第二代 Tungsten 引擎,具有全阶段代码生成和向量化的基于列的内存布局。基于现代编译器的思想和技术,这个新版本还利用现代 CPU 和缓存架构,通过“单指令多数据”(SIMD) 方法实现快速并行数据访问。
数据集编码器
编码器将堆外内存中的数据从 Spark 的内部 Tungsten 格式转换为 JVM Java 对象。换句话说,它们将数据集对象从 Spark 的内部格式序列化和反序列化为 JVM 对象,包括原始数据类型。例如,anEncoder[T]
将从 Spark 的内部 Tungsten 格式转换为Dataset[T]
.
Spark 内置支持为基本类型(例如,字符串、整数、长整数)、Scala 案例类和 JavaBeans 自动生成编码器。与 Java 和 Kryo 的序列化和反序列化相比,Spark 编码器的速度要快得多。
在我们之前的 Java 示例中,我们显式地创建了一个编码器:
Encoder<UsageCost> usageCostEncoder = Encoders.bean(UsageCost.class);
然而,对于 Scala,Spark 会自动为这些高效的转换器生成字节码。让我们来看看 Spark 内部基于 Tungsten 行的格式。
Spark 的内部格式与 Java 对象格式
Java 对象有很大的开销——标头信息、哈希码、Unicode 信息等。即使是简单的 Java 字符串(例如“abcd”)也需要 48 个字节的存储空间,而不是您可能期望的 4 个字节。例如,想象一下创建MyClass(Int, String, String)
对象的开销。
Spark 不是为 Datasets 或 DataFrames 创建基于 JVM 的对象,而是分配堆外 Java 内存来布置它们的数据,并使用编码器将数据从内存表示转换为 JVM 对象。例如,图 6-1显示了 JVM 对象如何在MyClass(Int, String, String)
内部存储。
![](https://img-blog.csdnimg.cn/1daea8e5c03247708737d300abe502c9.png)
图 6-1。JVM 对象存储在由 Spark 管理的连续堆外 Java 内存中
当数据以这种连续方式存储并通过指针算法和偏移量访问时,编码器可以快速序列化或反序列化该数据。这意味着什么?
序列化和反序列化 (SerDe)
分布式计算中的一个并不新鲜的概念,其中数据经常通过网络在集群中的计算机节点之间传输,序列化和反序列化是发送方将类型化对象编码(序列化)为二进制表示或格式并解码的过程(反序列化)从二进制格式到接收器各自的数据类型对象。
例如,如果图 6-1MyClass
中的 JVM 对象必须在Spark 集群中的节点之间共享,则发送方会将其序列化为字节数组,而接收方会将其反序列化回类型为 的 JVM 对象。MyClass
JVM 有自己的内置 Java 序列化器和反序列化器,但效率低下,因为(正如我们在上一节中看到的)JVM 在堆内存中创建的 Java 对象是臃肿的。因此,该过程是缓慢的。
这就是数据集编码器来救援的地方,原因如下:
-
Spark 的内部 Tungsten 二进制格式(参见图6-1和6-2)将对象存储在 Java 堆内存之外,而且它很紧凑,因此这些对象占用的空间更少。
-
编码器可以通过使用带有内存地址和偏移量的简单指针算法遍历内存来快速序列化(图 6-2)。
-
在接收端,编码器可以快速将二进制表示反序列化为 Spark 的内部表示。编码器不受 JVM 垃圾收集暂停的阻碍。
图 6-2。Spark 内部基于 Tungsten 行的格式
然而,正如我们接下来要讨论的那样,生活中大多数美好的事物都是有代价的。
使用数据集的成本
在第 3 章的“DataFrames 与 Datasets”中,我们概述了使用 Datasets 的一些好处——但这些好处是有代价的。如上一节所述,当数据集被传递给高阶函数时,例如,或接受 lambdas 和函数参数的函数,从 Spark 的内部 Tungsten 格式反序列化到 JVM 对象会产生相关成本。filter()
map()
flatMap()
与在 Spark 中引入编码器之前使用的其他序列化器相比,此成本很小且可以忍受。但是,在更大的数据集和许多查询中,此成本会累积并可能影响性能。
降低成本的策略
减轻过度序列化和反序列化的一种策略是在查询中使用DSL 表达式,并避免过度使用 lambda 作为匿名函数作为高阶函数的参数。因为 lambda 在运行时之前对 Catalyst 优化器是匿名且不透明的,所以当您使用它们时,它无法有效地识别您在做什么(您没有告诉 Spark要做什么),因此无法优化您的查询(请参阅“Catalyst Optimizer”在第 3 章中)。
第二种策略是以最小化序列化和反序列化的方式将查询链接在一起。将查询链接在一起是 Spark 中的常见做法。
让我们用一个简单的例子来说明。假设我们有一个类型为 的数据集Person
,其中Person
定义为 Scala 案例类:
// In Scala
Person(id: Integer, firstName: String, middleName: String, lastName: String,
gender: String, birthDate: String, ssn: String, salary: String)
我们想使用函数式编程向这个数据集发出一组查询。
让我们来看看我们编写查询效率低下的情况,以这种方式我们在不知不觉中产生了重复序列化和反序列化的成本:
import java.util.Calendar
val earliestYear = Calendar.getInstance.get(Calendar.YEAR) - 40
personDS
// Everyone above 40: lambda-1
.filter(x => x.birthDate.split("-")(0).toInt > earliestYear)
// Everyone earning more than 80K
.filter($"salary" > 80000)
// Last name starts with J: lambda-2
.filter(x => x.lastName.startsWith("J"))
// First name starts with D
.filter($"firstName".startsWith("D"))
.count()
正如您在图 6-3中所看到的,每次我们从 lambda 移动到 DSL( ) 时,都会产生序列化和反序列化JVM 对象filter($"salary" > 8000)
的成本。Person
图 6-3。使用 lambdas 和 DSL 链接查询的低效方式
相比之下,以下查询仅使用 DSL,不使用 lambda。因此,它的效率要高得多——整个组合和链式查询不需要序列化/反序列化:
personDS
.filter(year($"birthDate") > earliestYear) // Everyone above 40
.filter($"salary" > 80000) // Everyone earning more than 80K
.filter($"lastName".startsWith("J")) // Last name starts with J
.filter($"firstName".startsWith("D")) // First name starts with D
.count()
概括
在本章中,我们详细介绍了如何在 Java 和 Scala 中使用数据集。我们探索了 Spark 如何管理内存以将 Dataset 构造作为其统一和高级 API 的一部分,并且我们考虑了与使用 Datasets 相关的一些成本以及如何降低这些成本。我们还向您展示了如何在 Spark 中使用 Java 和 Scala 的函数式编程结构。
最后,我们深入了解了编码器如何从 Spark 的内部 Tungsten 二进制格式序列化和反序列化为 JVM 对象。