Spark 完整知识点详解
一、Spark概述
1.1 什么是Spark
Apache Spark是一个快速、通用、可扩展的大数据分析计算引擎。由加州大学伯克利分校AMP实验室于2009年开发,2013年捐赠给Apache基金会。
1.2 Spark核心特点
(1) 速度快:基于内存计算,比MapReduce快10~100倍
(2) 易用性:支持Java、Scala、Python、R、SQL多种语言API
(3) 通用性:一站式解决方案(SQL、流处理、机器学习、图计算)
(4) 兼容性:可运行在Standalone、YARN、Mesos、Kubernetes上
1.3 Spark与MapReduce对比
┌──────────────┬──────────────────┬──────────────────┐
│ 对比项 │ MapReduce │ Spark │
├──────────────┼──────────────────┼──────────────────┤
│ 中间结果 │ 写磁盘(HDFS) │ 内存(RDD) │
│ 计算模型 │ Map+Reduce两阶段 │ DAG有向无环图 │
│ 任务调度 │ TaskTracker │ Executor线程池 │
│ 实时性 │ 批处理为主 │ 批处理+流处理 │
│ 迭代计算 │ 每轮写磁盘,慢 │ 内存缓存,快 │
└──────────────┴──────────────────┴──────────────────┘
二、Spark主要组件
┌─────────────────────────────────────────────────────┐
│ Spark生态体系 │
├───────────┬───────────┬──────────┬──────────────────┤
│ Spark SQL │ Spark │ MLlib │ GraphX │
│ 结构化查询 │ Streaming │ 机器学习 │ 图计算 │
│ │ 流处理 │ │ │
├───────────┴───────────┴──────────┴──────────────────┤
│ Spark Core (RDD核心引擎) │
├─────────────────────────────────────────────────────┤
│ 资源管理(Standalone / YARN / Mesos / K8s) │
└─────────────────────────────────────────────────────┘
各组件说明:
(1) Spark Core:核心引擎,提供RDD抽象、任务调度、内存管理、故障恢复等
(2) Spark SQL:处理结构化数据,支持SQL查询和DataFrame/Dataset API
(3) Spark Streaming:实时流处理,基于微批次(Micro-Batch)模型
(4) MLlib:机器学习库,包含分类、回归、聚类、协同过滤等算法
(5) GraphX:图计算框架,提供图的创建、操作和常用图算法
三、Spark运行时架构
3.1 核心概念
(1) Driver Program(驱动程序):用户编写的main函数,创建SparkContext
(2) Cluster Manager(集群管理器):分配资源(Standalone/YARN/Mesos/K8s)
(3) Worker Node(工作节点):运行Executor的节点
(4) Executor(执行器):Worker上的进程,负责执行Task
(5) Task(任务):最小的计算单元,在Executor上执行
(6) Job(作业):由一个Action算子触发的一组Task集合
(7) Stage(阶段):Job被DAGScheduler划分为多个Stage
3.2 架构图
┌──────────────────────┐
│ Driver Program │
│ ┌────────────────┐ │
│ │ SparkContext │ │
│ │ DAGScheduler │ │
│ │ TaskScheduler │ │
│ └────────────────┘ │
└──────────┬───────────┘
│
┌──────────▼───────────┐
│ Cluster Manager │
│ (资源分配与调度) │
└──┬───────────────┬───┘
│ │
┌─────────▼───┐ ┌──────▼─────────┐
│ Worker Node │ │ Worker Node │
│ ┌──────────┐ │ │ ┌────────────┐ │
│ │ Executor │ │ │ │ Executor │ │
│ │ Task Task│ │ │ │ Task Task │ │
│ └──────────┘ │ │ └────────────┘ │
└──────────────┘ └────────────────┘
3.3 任务提交执行流程
步骤1:Driver向Cluster Manager申请资源
步骤2:Cluster Manager在Worker上启动Executor
步骤3:Driver构建DAG(有向无环图)
步骤4:DAGScheduler将DAG划分为Stage
步骤5:TaskScheduler将Task发送到Executor执行
步骤6:Executor执行Task并返回结果给Driver
四、Spark集群环境搭建
4.1 环境准备(所有节点执行)
bash
# ===== (1) 准备3台虚拟机 =====
# hadoop101 (Master/Worker)
# hadoop102 (Worker)
# hadoop103 (Worker)
# ===== (2) 配置主机名和hosts映射 =====
# 编辑/etc/hosts文件,添加以下内容
sudo vim /etc/hosts
192.168.1.101 hadoop101
192.168.1.102 hadoop102
192.168.1.103 hadoop103
bash
# ===== (3) 配置SSH免密登录 =====
# 在hadoop101上生成密钥对
ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa
# 将公钥分发到所有节点(包括自己)
ssh-copy-id hadoop101
ssh-copy-id hadoop102
ssh-copy-id hadoop103
# ===== (4) 安装JDK =====
# 解压JDK
tar -zxvf jdk-8u212-linux-x64.tar.gz -C /opt/module/
# 配置环境变量
sudo vim /etc/profile.d/my_env.sh
bash
# /etc/profile.d/my_env.sh 中添加:
# JAVA_HOME环境变量,指向JDK安装目录
export JAVA_HOME=/opt/module/jdk1.8.0_212
# 将JDK的bin目录加入PATH环境变量
export PATH=$PATH:$JAVA_HOME/bin
bash
# 使环境变量生效
source /etc/profile.d/my_env.sh
# 验证Java安装
java -version
4.2 Spark Standalone模式安装
4.2.1 解压安装
bash
# ===== 在hadoop101上解压Spark =====
# 解压spark-3.3.1到/opt/module目录
tar -zxvf spark-3.3.1-bin-hadoop3.tgz -C /opt/module/
# 重命名目录,方便管理
mv /opt/module/spark-3.3.1-bin-hadoop3 /opt/module/spark-3.3.1
4.2.2 配置环境变量
bash
# 编辑环境变量文件
sudo vim /etc/profile.d/my_env.sh
bash
# 添加以下内容:
# SPARK_HOME环境变量,指向Spark安装目录
export SPARK_HOME=/opt/module/spark-3.3.1
# 将Spark的bin和sbin目录加入PATH
export PATH=$PATH:$SPARK_HOME/bin:$SPARK_HOME/sbin
bash
# 使环境变量生效
source /etc/profile.d/my_env.sh
4.2.3 配置spark-env.sh
bash
# 进入Spark配置目录
cd /opt/module/spark-3.3.1/conf/
# 从模板文件复制配置文件
cp spark-env.sh.template spark-env.sh
# 编辑配置文件
vim spark-env.sh
bash
# 在spark-env.sh末尾添加以下配置:
# 配置Java环境变量路径
export JAVA_HOME=/opt/module/jdk1.8.0_212
# 配置Spark Master的主机名
export SPARK_MASTER_HOST=hadoop101
# 配置Spark Master的端口号(默认7077)
export SPARK_MASTER_PORT=7077
# 配置Master的Web UI端口(默认8080)
export SPARK_MASTER_WEBUI_PORT=8080
# 配置每个Worker使用的核心数(设为总核心数)
export SPARK_WORKER_CORES=4
# 配置每个Worker使用的内存
export SPARK_WORKER_MEMORY=4g
# 配置Worker的Web UI端口
export SPARK_WORKER_WEBUI_PORT=8081
4.2.4 配置workers文件
bash
# 从模板文件复制workers配置文件
cp workers.template workers
# 编辑workers文件(Spark 3.x使用workers,2.x使用slaves)
vim workers
# workers文件内容(每行一个Worker节点主机名):
hadoop101
hadoop102
hadoop103
4.2.5 分发到其他节点
bash
# 将Spark安装目录分发到hadoop102和hadoop103
scp -r /opt/module/spark-3.3.1/ hadoop102:/opt/module/
scp -r /opt/module/spark-3.3.1/ hadoop103:/opt/module/
# 分发环境变量配置文件(所有节点执行source)
sudo scp /etc/profile.d/my_env.sh hadoop102:/etc/profile.d/
sudo scp /etc/profile.d/my_env.sh hadoop103:/etc/profile.d/
4.2.6 启动Standalone集群
bash
# ===== 方式一:一键启动整个集群 =====
# 使用Spark自带的启动脚本(在Master节点执行)
/opt/module/spark-3.3.1/sbin/start-all.sh
# ===== 方式二:分别启动Master和Worker =====
# 启动Master(在hadoop101上执行)
/opt/module/spark-3.3.1/sbin/start-master.sh
# 启动所有Worker(在hadoop101上执行,会ssh到workers文件中的各节点)
/opt/module/spark-3.3.1/sbin/start-workers.sh
# ===== 验证启动 =====
# 查看Java进程(各节点上分别执行)
jps
# hadoop101应该有:Master、Worker
# hadoop102应该有:Worker
# hadoop103应该有:Worker
# 访问Web UI:http://hadoop101:8080
4.2.7 停止集群
bash
# 一键停止整个集群
/opt/module/spark-3.3.1/sbin/stop-all.sh
4.3 Spark On YARN模式安装
4.3.1 前提条件
已安装并启动Hadoop集群(HDFS + YARN)
已按照上述步骤解压Spark并配置环境变量
4.3.2 配置spark-env.sh
bash
vim /opt/module/spark-3.3.1/conf/spark-env.sh
bash
# 添加/修改以下内容:
# Java环境变量
export JAVA_HOME=/opt/module/jdk1.8.0_212
# Hadoop配置文件目录(Spark on YARN需要读取yarn-site.xml等配置)
export HADOOP_CONF_DIR=/opt/module/hadoop-3.3.4/etc/hadoop
# YARN配置文件目录
export YARN_CONF_DIR=/opt/module/hadoop-3.3.4/etc/hadoop
4.3.3 配置spark-defaults.conf
bash
# 从模板复制配置文件
cp /opt/module/spark-3.3.1/conf/spark-defaults.conf.template \
/opt/module/spark-3.3.1/conf/spark-defaults.conf
# 编辑配置文件
vim /opt/module/spark-3.3.1/conf/spark-defaults.conf
properties
# spark-defaults.conf 添加以下内容:
# 设置默认的Master为YARN
spark.master yarn
# 设置Spark运行模式为YARN客户端模式(默认)
spark.submit.deployMode client
# 设置Spark History Server地址(可选)
spark.eventLog.enabled true
spark.eventLog.dir hdfs:///spark-logs
spark.history.fs.logDirectory hdfs:///spark-logs
bash
# 在HDFS上创建日志目录
hadoop fs -mkdir -p /spark-logs
4.3.4 分发到其他节点
bash
# 分发Spark安装目录
scp -r /opt/module/spark-3.3.1/ hadoop102:/opt/module/
scp -r /opt/module/spark-3.3.1/ hadoop103:/opt/module/
4.3.5 YARN两种运行模式
Client模式(用于调试):
- Driver运行在提交任务的客户端机器上
- 客户机可以看到Driver的输出日志
- 命令:spark-submit --deploy-mode client ...
Cluster模式(用于生产):
- Driver运行在YARN集群的某个NodeManager上
- 客户机断开连接不影响任务执行
- 命令:spark-submit --deploy-mode cluster ...
bash
# 测试Spark On YARN(Client模式)
# 使用spark-submit运行示例程序
/opt/module/spark-3.3.1/bin/spark-submit \
--master yarn \
--deploy-mode client \
--class org.apache.spark.examples.SparkPi \
/opt/module/spark-3.3.1/examples/jars/spark-examples_2.12-3.3.1.jar \
100
4.4 Spark HA搭建
4.4.1 基于ZooKeeper的HA
原理:多个Master节点通过ZooKeeper进行选主,一个为Active,其余为Standby
当Active Master挂掉后,ZooKeeper重新选举一个Standby为Active
bash
# 编辑spark-env.sh(所有节点)
vim /opt/module/spark-3.3.1/conf/spark-env.sh
bash
# 修改spark-env.sh,注释掉原来的SPARK_MASTER_HOST
# export SPARK_MASTER_HOST=hadoop101 # 注释掉此行
# 添加ZooKeeper相关配置:
# ZooKeeper集群地址
export SPARK_DAEMON_JAVA_OPTS="-Dspark.deploy.recoveryMode=ZOOKEEPER \
-Dspark.deploy.zookeeper.url=hadoop101:2181,hadoop102:2181,hadoop103:2181 \
-Dspark.deploy.zookeeper.dir=/spark"
# 保留其他配置
export JAVA_HOME=/opt/module/jdk1.8.0_212
export SPARK_MASTER_PORT=7077
export SPARK_WORKER_CORES=4
export SPARK_WORKER_MEMORY=4g
bash
# 分发修改后的配置文件到所有节点
scp /opt/module/spark-3.3.1/conf/spark-env.sh hadoop102:/opt/module/spark-3.3.1/conf/
scp /opt/module/spark-3.3.1/conf/spark-env.sh hadoop103:/opt/module/spark-3.3.1/conf/
bash
# 在hadoop101上启动Master和所有Worker
/opt/module/spark-3.3.1/sbin/start-all.sh
# 在hadoop102上单独启动一个备用Master
ssh hadoop102 /opt/module/spark-3.3.1/sbin/start-master.sh
# 验证:
# hadoop101的Master状态:ALIVE (http://hadoop101:8080)
# hadoop102的Master状态:STANDBY (http://hadoop102:8080)
# 当hadoop101的Master挂掉后,hadoop102自动变为ALIVE
4.4.2 基于文件系统的HA(单机模拟)
bash
# 编辑spark-env.sh
export SPARK_DAEMON_JAVA_OPTS="-Dspark.deploy.recoveryMode=FILESYSTEM \
-Dspark.deploy.recoveryDirectory=/opt/module/spark-3.3.1/recovery"
五、Spark应用程序的提交
5.1 spark-submit 命令详解
bash
# 基本语法格式
spark-submit \
--master <master-url> \
--deploy-mode <client|cluster> \
--class <主类名> \
--executor-memory <内存> \
--executor-cores <核数> \
--num-executors <执行器数量> \
--driver-memory <Driver内存> \
--conf <key>=<value> \
<应用程序jar包路径> \
[应用程序参数]
5.2 常用参数说明
--master 指定Master URL
local 本地单线程
local[K] 本地K线程
local[*] 本地使用所有可用线程
spark://host:7077 Standalone模式
yarn YARN模式
mesos://host:5050 Mesos模式
--deploy-mode 部署模式:client(默认)或 cluster
--class 应用程序的主类(含包名)
--executor-memory 每个Executor的内存,如 2g
--executor-cores 每个Executor的核心数
--num-executors Executor总数量(YARN模式)
--driver-memory Driver的内存
--jars 额外依赖的jar包
--files 分发到集群的文件
--conf Spark配置属性
5.3 各模式提交示例
bash
# ===== (1) 本地模式 =====
# 在本地使用4个线程运行,用于开发调试
/opt/module/spark-3.3.1/bin/spark-submit \
--master local[4] \
--class org.apache.spark.examples.SparkPi \
/opt/module/spark-3.3.1/examples/jars/spark-examples_2.12-3.3.1.jar \
100
# ===== (2) Standalone Client模式 =====
# Driver在客户端,日志可在终端查看
/opt/module/spark-3.3.1/bin/spark-submit \
--master spark://hadoop101:7077 \
--deploy-mode client \
--executor-memory 2g \
--executor-cores 2 \
--num-executors 4 \
--class org.apache.spark.examples.SparkPi \
/opt/module/spark-3.3.1/examples/jars/spark-examples_2.12-3.3.1.jar \
100
# ===== (3) Standalone Cluster模式 =====
# Driver在集群中运行,客户端可断开
/opt/module/spark-3.3.1/bin/spark-submit \
--master spark://hadoop101:7077 \
--deploy-mode cluster \
--executor-memory 2g \
--executor-cores 2 \
--class org.apache.spark.examples.SparkPi \
/opt/module/spark-3.3.1/examples/jars/spark-examples_2.12-3.3.1.jar \
100
# ===== (4) YARN Client模式 =====
/opt/module/spark-3.3.1/bin/spark-submit \
--master yarn \
--deploy-mode client \
--executor-memory 2g \
--executor-cores 2 \
--num-executors 4 \
--class org.apache.spark.examples.SparkPi \
/opt/module/spark-3.3.1/examples/jars/spark-examples_2.12-3.3.1.jar \
100
# ===== (5) YARN Cluster模式 =====
/opt/module/spark-3.3.1/bin/spark-submit \
--master yarn \
--deploy-mode cluster \
--executor-memory 2g \
--executor-cores 2 \
--num-executors 4 \
--class org.apache.spark.examples.SparkPi \
/opt/module/spark-3.3.1/examples/jars/spark-examples_2.12-3.3.1.jar \
100
六、Spark Shell的使用
6.1 启动Spark Shell
bash
# ===== (1) 本地模式启动 =====
# 使用Scala版本的Spark Shell,本地4线程
spark-shell --master local[4]
# ===== (2) Standalone模式启动 =====
# 连接到Standalone集群
spark-shell --master spark://hadoop101:7077
# ===== (3) YARN模式启动 =====
# 连接到YARN集群(Client模式)
spark-shell --master yarn --deploy-mode client
# ===== (4) pyspark(Python版本)=====
pyspark --master local[4]
6.2 Spark Shell中基本操作
scala
// ===== 进入spark-shell后,系统自动创建了sc(SparkContext)和spark(SparkSession) =====
// 查看sc的类型
sc // org.apache.spark.SparkContext
// 从本地文件创建RDD
val rdd = sc.textFile("file:///opt/module/spark-3.3.1/README.md")
// 从HDFS文件创建RDD
val rdd = sc.textFile("hdfs:///input/word.txt")
// 查看RDD的前5行
rdd.take(5).foreach(println)
// 统计行数
rdd.count()
// 查看RDD第一个元素
rdd.first()
// 单词计数示例
val wordCounts = rdd
.flatMap(_.split(" ")) // 按空格切分单词
.map((_, 1)) // 每个单词映射为(word, 1)
.reduceByKey(_ + _) // 按key聚合求和
.collect() // 收集结果到Driver
// 打印结果
wordCounts.foreach(println)
// 退出spark-shell
:quit
// 或按 Ctrl + D
七、Spark RDD
7.1 RDD概念
RDD(Resilient Distributed Dataset)弹性分布式数据集,是Spark最基本的数据抽象。
五大特性:
(1) 一个分区列表(A list of partitions)
(2) 一个计算函数,用于每个分区(A function for computing each split)
(3) 对其他RDD的依赖列表(A list of dependencies on other RDDs)
(4) 可选:对键值对RDD的分区器Partitioner(可选)
(5) 可选:每个分区的首选位置(可选)
7.2 RDD特点
(1) 不可变:RDD一旦创建不能修改,只能通过转换操作生成新的RDD
(2) 分区:RDD的数据被划分为多个分区,分布在集群不同节点上
(3) 惰性求值:转换操作不会立即执行,只有遇到Action操作才会触发计算
(4) 缓存:可以将RDD缓存到内存中,避免重复计算
(5) 容错:通过血缘(Lineage)信息可以从失败中恢复
7.3 创建RDD
7.3.1 从集合创建
scala
// 方式一:parallelize方法------从内存集合创建RDD
// 创建一个包含1到10的RDD,分为3个分区
val rdd1 = sc.parallelize(1 to 10, 3)
// 打印所有元素
rdd1.collect().foreach(println)
// 方式二:makeRDD方法(底层调用parallelize)
// 从List创建RDD,指定2个分区
val rdd2 = sc.makeRDD(List("hello", "spark", "hadoop"), 2)
// 打印所有元素
rdd2.collect().foreach(println)
// 查看分区数量
rdd2.getNumPartitions // 返回 2
// 方式三:指定分区的数据分布
// 指定第一个分区包含"hello",第二个分区包含"spark"和"hadoop"
val rdd3 = sc.makeRDD(
Seq(
Seq("hello"), // 第0个分区的数据
Seq("spark", "hadoop") // 第1个分区的数据
)
)
rdd3.collect().foreach(println)
7.3.2 从外部存储创建
scala
// ===== 从本地文件系统创建RDD =====
// 读取本地文件的每一行作为一个元素
val localRDD = sc.textFile("file:///home/hadoop/data/word.txt")
// ===== 从HDFS创建RDD =====
// 读取HDFS上的文本文件,每一行作为RDD的一个元素
val hdfsRDD = sc.textFile("hdfs:///input/word.txt")
// ===== 通配符读取多个文件 =====
// 读取/input/目录下所有.txt文件
val multiRDD = sc.textFile("hdfs:///input/*.txt")
// ===== 读取整个目录 =====
// 读取/input/目录下所有文件
val dirRDD = sc.textFile("hdfs:///input/")
// ===== 读取SequenceFile格式 =====
// val seqRDD = sc.sequenceFile[String, Int]("hdfs:///input/seq/")
// ===== 读取ObjectFile格式 =====
// val objRDD = sc.objectFile[String]("hdfs:///input/obj/")
// ===== 从其他RDD转换创建 =====
// 通过map转换创建新的RDD
val mappedRDD = hdfsRDD.map(_.toUpperCase())
7.4 RDD算子
7.4.1 Transformation算子(转换算子,惰性执行)
scala
// ============================
// (1) map:对每个元素应用函数,返回新RDD
// ============================
val rdd = sc.parallelize(1 to 5, 2)
// 每个元素乘以2
val mapped = rdd.map(_ * 2)
mapped.collect() // Array(2, 4, 6, 8, 10)
// ============================
// (2) filter:过滤满足条件的元素
// ============================
val rdd = sc.parallelize(1 to 10)
// 过滤出偶数
val filtered = rdd.filter(_ % 2 == 0)
filtered.collect() // Array(2, 4, 6, 8, 10)
// ============================
// (3) flatMap:先映射再扁平化
// ============================
val rdd = sc.parallelize(List("hello spark", "hello hadoop"))
// 按空格切分并展平
val flatMapped = rdd.flatMap(_.split(" "))
flatMapped.collect() // Array(hello, spark, hello, hadoop)
// ============================
// (4) mapPartitions:以分区为单位应用函数
// ============================
val rdd = sc.parallelize(1 to 10, 3)
// 对每个分区的所有元素求和,返回一个新元素
val result = rdd.mapPartitions { iter =>
// iter是当前分区的迭代器
// 对分区中的所有元素求和
Iterator(iter.sum)
}
result.collect() // 各分区之和
// ============================
// (5) mapPartitionsWithIndex:带分区索引的mapPartitions
// ============================
val rdd = sc.parallelize(1 to 6, 3)
// 返回(分区索引, 分区中的元素列表)
val result = rdd.mapPartitionsWithIndex { (index, iter) =>
// index是分区索引号
// iter是当前分区的迭代器
Iterator(s"分区$index: ${iter.toList.mkString(",")}")
}
result.collect().foreach(println)
// ============================
// (6) sample:抽样
// ============================
val rdd = sc.parallelize(1 to 100)
// withReplacement: 是否放回抽样(true=有放回,false=无放回)
// fraction: 抽样比例(0.0~1.0)
// seed: 随机种子(可选)
val sampled = rdd.sample(false, 0.1, 42)
sampled.collect()
// ============================
// (7) distinct:去重
// ============================
val rdd = sc.parallelize(List(1, 2, 2, 3, 3, 3))
val distinctRDD = rdd.distinct()
distinctRDD.collect() // Array(1, 2, 3) 顺序可能不同
// ============================
// (8) groupBy:按条件分组
// ============================
val rdd = sc.parallelize(1 to 10)
// 按奇偶分组,返回(K, Iterable[V])的RDD
val grouped = rdd.groupBy(_ % 2)
grouped.collect()
// Array((0, CompactBuffer(2,4,6,8,10)), (1, CompactBuffer(1,3,5,7,9)))
// ============================
// (9) sortBy:排序
// ============================
val rdd = sc.parallelize(List(3, 1, 4, 1, 5, 9, 2, 6))
// 按元素值升序排列
val sorted = rdd.sortBy(x => x, ascending = true)
sorted.collect() // Array(1, 1, 2, 3, 4, 5, 6, 9)
// 按元素值降序排列
val sortedDesc = rdd.sortBy(x => x, ascending = false)
sortedDesc.collect() // Array(9, 6, 5, 4, 3, 2, 1, 1)
// ============================
// (10) union:并集(不去重)
// ============================
val rdd1 = sc.parallelize(1 to 5)
val rdd2 = sc.parallelize(4 to 8)
val unionRDD = rdd1.union(rdd2)
unionRDD.collect() // Array(1,2,3,4,5,4,5,6,7,8)
// ============================
// (11) intersection:交集
// ============================
val interRDD = rdd1.intersection(rdd2)
interRDD.collect() // Array(4, 5)
// ============================
// (12) subtract:差集(rdd1中有但rdd2中没有的)
// ============================
val subRDD = rdd1.subtract(rdd2)
subRDD.collect() // Array(1, 2, 3)
// ============================
// (13) cartesian:笛卡尔积
// ============================
val rdd1 = sc.parallelize(List("A", "B"))
val rdd2 = sc.parallelize(List(1, 2, 3))
val cartRDD = rdd1.cartesian(rdd2)
cartRDD.collect()
// Array((A,1),(A,2),(A,3),(B,1),(B,2),(B,3))
// ============================
// (14) zip:拉链(两个RDD元素一一配对)
// 要求两个RDD的元素数量和分区数相同
// ============================
val rdd1 = sc.parallelize(List(1, 2, 3))
val rdd2 = sc.parallelize(List("a", "b", "c"))
val zipped = rdd1.zip(rdd2)
zipped.collect() // Array((1,a), (2,b), (3,c))
// ============================
// (15) coalesce:减少分区数(不触发shuffle)
// ============================
val rdd = sc.parallelize(1 to 10, 4)
// 将4个分区合并为2个
val coalesced = rdd.coalesce(2)
coalesced.getNumPartitions // 2
// ============================
// (16) repartition:重新分区(触发shuffle,可增可减)
// ============================
val rdd = sc.parallelize(1 to 10, 2)
// 增加到5个分区
val repartitioned = rdd.repartition(5)
repartitioned.getNumPartitions // 5
// ============================
// (17) groupByKey:按Key分组(适用于PairRDD)
// ============================
val rdd = sc.parallelize(List(
("math", 80), ("english", 90), ("math", 85),
("english", 75), ("math", 90)
))
// 按key(科目)分组
val grouped = rdd.groupByKey()
// 转换为可读格式
grouped.mapValues(_.toList).collect()
// Array((math, List(80,85,90)), (english, List(90,75)))
// ============================
// (18) reduceByKey:按Key聚合(推荐使用,比groupByKey高效)
// ============================
val rdd = sc.parallelize(List(
("math", 80), ("english", 90), ("math", 85),
("english", 75), ("math", 90)
))
// 按key求成绩总和
val reduced = rdd.reduceByKey(_ + _)
reduced.collect()
// Array((math, 255), (english, 165))
// ============================
// (19) aggregateByKey:按Key聚合(带初始值和两个函数)
// ============================
val rdd = sc.parallelize(List(
("math", 80), ("english", 90), ("math", 85),
("english", 75), ("math", 90)
))
// 初始值为0
// seqOp: 分区内操作,求最大值
// combOp: 分区间操作,求最大值
val result = rdd.aggregateByKey(0)(
(acc, value) => math.max(acc, value), // 分区内取最大值
(acc1, acc2) => math.max(acc1, acc2) // 分区间取最大值
)
result.collect()
// Array((math, 90), (english, 90))
// ============================
// (20) sortByKey:按Key排序(适用于PairRDD)
// ============================
val rdd = sc.parallelize(List(
(3, "c"), (1, "a"), (2, "b"), (5, "e"), (4, "d")
))
// 按key升序排列
val sortedByKey = rdd.sortByKey(ascending = true)
sortedByKey.collect()
// Array((1,a), (2,b), (3,c), (4,d), (5,e))
// ============================
// (21) join:内连接(只保留两边都有的Key)
// ============================
val rdd1 = sc.parallelize(List(
(1, "Alice"), (2, "Bob"), (3, "Charlie")
))
val rdd2 = sc.parallelize(List(
(1, 80), (2, 90), (4, 70)
))
// 内连接
val joined = rdd1.join(rdd2)
joined.collect()
// Array((1,(Alice,80)), (2,(Bob,90)))
// ============================
// (22) leftOuterJoin:左外连接
// ============================
val leftJoin = rdd1.leftOuterJoin(rdd2)
leftJoin.collect()
// Array((1,(Alice,Some(80))), (2,(Bob,Some(90))), (3,(Charlie,None)))
// ============================
// (23) rightOuterJoin:右外连接
// ============================
val rightJoin = rdd1.rightOuterJoin(rdd2)
rightJoin.collect()
// Array((1,(Some(Alice),80)), (2,(Some(Bob),90)), (4,(None,70)))
// ============================
// (24) fullOuterJoin:全外连接
// ============================
val fullJoin = rdd1.fullOuterJoin(rdd2)
fullJoin.collect()
// Array(
// (1,(Some(Alice),Some(80))),
// (2,(Some(Bob),Some(90))),
// (3,(Some(Charlie),None)),
// (4,(None,Some(70)))
// )
// ============================
// (25) mapValues:只对Value应用函数
// ============================
val rdd = sc.parallelize(List(
("a", 1), ("b", 2), ("c", 3)
))
// 对value加10
val result = rdd.mapValues(_ + 10)
result.collect()
// Array((a,11), (b,12), (c,13))
// ============================
// (26) combineByKey:最通用的按Key聚合操作
// ============================
val rdd = sc.parallelize(List(
("math", 80), ("english", 90), ("math", 85),
("english", 75), ("math", 90)
))
// 计算每个科目平均分
// createCombiner: 将第一个值转换为组合器的初始形式 (sum, count)
// mergeValue: 分区内合并
// mergeCombiners: 分区间合并
val result = rdd.combineByKey(
(v: Int) => (v, 1), // 第一个值转为(值,1)
(acc: (Int, Int), v: Int) => (acc._1 + v, acc._2 + 1), // 分区内累加
(acc1: (Int, Int), acc2: (Int, Int)) =>
(acc1._1 + acc2._1, acc1._2 + acc2._2) // 分区间合并
).mapValues { case (sum, count) => sum.toDouble / count } // 计算平均值
result.collect()
// Array((math, 85.0), (english, 82.5))
// ============================
// (27) flatMapValues:对Value先映射再展平
// ============================
val rdd = sc.parallelize(List(
("a", "1 2 3"), ("b", "4 5")
))
// 对value按空格切分并展平
val result = rdd.flatMapValues(_.split(" "))
result.collect()
// Array((a,1), (a,2), (a,3), (b,4), (b,5))
// ============================
// (28) partitionBy:按指定分区器重新分区
// ============================
import org.apache.spark.HashPartitioner
val rdd = sc.parallelize(List(
("a", 1), ("b", 2), ("c", 3), ("d", 4)
))
// 使用HashPartitioner将数据分为2个分区
val partitioned = rdd.partitionBy(new HashPartitioner(2))
partitioned.getNumPartitions // 2
// ============================
// (29) cogroup:协同分组
// ============================
val rdd1 = sc.parallelize(List(
(1, "Alice"), (1, "Bob"), (2, "Charlie")
))
val rdd2 = sc.parallelize(List(
(1, 80), (1, 90), (3, 70)
))
// 对两个RDD按Key协同分组
val cogrouped = rdd1.cogroup(rdd2)
cogrouped.mapValues { case (names, scores) =>
(names.toList, scores.toList)
}.collect()
// Array(
// (1, (List(Alice, Bob), List(80, 90))),
// (2, (List(Charlie), List())),
// (3, (List(), List(70)))
// )
7.4.2 Action算子(行动算子,触发计算)
scala
// ============================
// (1) collect:收集所有元素到Driver端
// ============================
val rdd = sc.parallelize(1 to 10, 3)
val arr = rdd.collect() // Array(1,2,3,4,5,6,7,8,9,10)
// 注意:数据量大时不要使用collect,会导致Driver内存溢出
// ============================
// (2) count:统计元素个数
// ============================
rdd.count() // 返回 10
// ============================
// (3) first:获取第一个元素
// ============================
rdd.first() // 返回 1
// ============================
// (4) take(n):获取前n个元素
// ============================
rdd.take(3) // Array(1, 2, 3)
// ============================
// (5) takeOrdered(n):获取排序后的前n个元素
// ============================
val rdd = sc.parallelize(List(3, 1, 4, 1, 5, 9, 2, 6))
// 默认升序取前3个最小值
rdd.takeOrdered(3) // Array(1, 1, 2)
// 指定降序取前3个最大值
rdd.takeOrdered(3)(Ordering[Int].reverse) // Array(9, 6, 5)
// ============================
// (6) reduce:聚合操作
// ============================
val rdd = sc.parallelize(1 to 100)
// 求1到100的和
rdd.reduce(_ + _) // 返回 5050
// ============================
// (7) fold:带初始值的聚合
// ============================
val rdd = sc.parallelize(1 to 10)
// 初始值为100,然后累加
rdd.fold(100)(_ + _) // 返回 100 + 55 = 155
// 注意:每个分区都会加一次初始值,所以实际结果可能比预期大
// 更精确的方式使用aggregate
// ============================
// (8) aggregate:带初始值的自定义聚合
// ============================
val rdd = sc.parallelize(List(1, 2, 3, 4, 5), 2)
// 初始值为0
// seqOp: 分区内操作(累加)
// combOp: 分区间操作(累加)
val result = rdd.aggregate(0)(
(acc, value) => acc + value, // 分区内累加
(acc1, acc2) => acc1 + acc2 // 分区间累加
)
result // 返回 15
// ============================
// (9) countByKey:按Key统计个数
// ============================
val rdd = sc.parallelize(List(
("a", 1), ("b", 2), ("a", 3), ("c", 4), ("b", 5)
))
rdd.countByKey()
// Map(a -> 2, b -> 2, c -> 1)
// ============================
// (10) saveAsTextFile:保存为文本文件
// ============================
val rdd = sc.parallelize(1 to 10, 3)
// 保存到HDFS,每个分区生成一个文件
rdd.saveAsTextFile("hdfs:///output/rdd_text")
// 保存到本地文件系统
rdd.saveAsTextFile("file:///home/hadoop/output/rdd_text")
// ============================
// (11) foreach:遍历每个元素执行函数
// ============================
val rdd = sc.parallelize(1 to 5)
// 注意:foreach在Executor端执行,输出不会显示在Driver端
rdd.foreach(println) // 输出可能在不同Executor的日志中
// 如果想在Driver端打印,应该先collect
rdd.collect().foreach(println)
// ============================
// (12) countByValue:统计每个值出现的次数
// ============================
val rdd = sc.parallelize(List(1, 2, 2, 3, 3, 3))
rdd.countByValue()
// Map(1 -> 1, 2 -> 2, 3 -> 3)
// ============================
// (13) takeSample:随机抽样
// ============================
val rdd = sc.parallelize(1 to 100)
// 随机抽取5个元素(有放回)
rdd.takeSample(withReplacement = true, num = 5, seed = 42)
// ============================
// (14) toDebugString:查看RDD的血缘关系
// ============================
val rdd1 = sc.textFile("hdfs:///input/word.txt")
val rdd2 = rdd1.flatMap(_.split(" "))
val rdd3 = rdd2.map((_, 1))
val rdd4 = rdd3.reduceByKey(_ + _)
// 打印RDD的依赖链(血缘关系)
println(rdd4.toDebugString)
7.4.3 RDD持久化操作
scala
// ============================
// cache():缓存到内存(等同于persist(StorageLevel.MEMORY_ONLY))
// ============================
val rdd = sc.textFile("hdfs:///input/bigdata.txt")
val words = rdd.flatMap(_.split(" "))
// 缓存到内存
words.cache()
// 第一次行动操作,触发计算并缓存
println(words.count())
// 第二次行动操作,直接从缓存读取,速度快
println(words.distinct().count())
// 取消缓存
words.unpersist()
// ============================
// persist():可指定存储级别
// ============================
import org.apache.spark.storage.StorageLevel
val rdd = sc.parallelize(1 to 100)
// 存储级别说明:
// MEMORY_ONLY 只存内存(默认),空间不够时不缓存多余的分区
// MEMORY_AND_DISK 内存+磁盘,空间不够时溢写到磁盘
// MEMORY_ONLY_SER 内存中序列化存储,更省内存但读取时需要反序列化
// MEMORY_AND_DISK_SER 内存序列化+磁盘
// DISK_ONLY 只存磁盘
// MEMORY_ONLY_2 内存存储,2副本
// MEMORY_AND_DISK_2 内存+磁盘,2副本
// 使用MEMORY_AND_DISK级别缓存
rdd.persist(StorageLevel.MEMORY_AND_DISK)
// 触发计算
rdd.count()
// 取消持久化
rdd.unpersist()
// ============================
// checkpoint:检查点,将RDD保存到可靠存储(如HDFS)
// ============================
// 设置检查点目录(必须在行动操作前设置)
sc.setCheckpointDir("hdfs:///checkpoint")
val rdd = sc.textFile("hdfs:///input/bigdata.txt")
val words = rdd.flatMap(_.split(" ")).map((_, 1))
val result = words.reduceByKey(_ + _)
// 标记为需要做检查点(惰性操作)
result.checkpoint()
// 触发行动操作后才真正执行检查点
result.collect()
// 查看检查点文件
// 在HDFS /checkpoint目录下会生成分区文件
7.5 案例分析:使用Spark RDD实现单词计数
scala
import org.apache.spark.{SparkConf, SparkContext}
/**
* 使用Spark RDD实现单词计数
* 功能:读取文本文件,统计每个单词出现的次数
*/
object WordCountRDD {
def main(args: Array[String]): Unit = {
// ===== 第一步:创建SparkConf配置对象 =====
// setAppName:设置应用程序名称(在Web UI上显示)
// setMaster:设置运行模式(local[*]表示本地使用所有CPU核)
val conf = new SparkConf()
.setAppName("WordCountRDD") // 设置应用名称
.setMaster("local[*]") // 本地模式,使用所有可用核心
// ===== 第二步:创建SparkContext(Spark入口)=====
// SparkContext是Spark功能的主要入口点
val sc = new SparkContext(conf)
// ===== 第三步:读取输入文件,创建RDD =====
// textFile读取文本文件,每行作为RDD的一个元素
// "input/word.txt"是输入文件路径
val linesRDD = sc.textFile("input/word.txt")
// ===== 第四步:将每行文本按空格切分为单词 =====
// flatMap将每行切分成单词数组后展平为一个扁平的RDD
val wordsRDD = linesRDD.flatMap(_.split(" "))
// ===== 第五步:过滤空字符串 =====
// filter过滤掉空字符串(多个空格可能导致空元素)
val filteredRDD = wordsRDD.filter(_.nonEmpty)
// ===== 第六步:将每个单词映射为(单词, 1)的元组 =====
// map将每个单词转换为键值对格式(word, 1)
val wordPairRDD = filteredRDD.map(word => (word, 1))
// ===== 第七步:按单词聚合求和 =====
// reduceByKey按照相同的key进行聚合,将value相加
// 相同的单词会被分到一起,1相加得到总次数
val wordCountRDD = wordPairRDD.reduceByKey(_ + _)
// ===== 第八步:按次数降序排列 =====
// sortBy按照元组的第二个元素(次数)降序排列
val sortedRDD = wordCountRDD.sortBy(_._2, ascending = false)
// ===== 第九步:输出结果 =====
// collect将RDD的所有元素收集到Driver端的数组中
// foreach遍历数组并打印每个元素
sortedRDD.collect().foreach(println)
// ===== 第十步:保存结果到文件 =====
// saveAsTextFile将结果保存为文本文件
sortedRDD.saveAsTextFile("output/wordcount")
// ===== 第十一步:关闭SparkContext =====
// 释放资源,关闭与集群的连接
sc.stop()
}
}
7.6 案例扩展:带分区的单词计数
scala
import org.apache.spark.{SparkConf, SparkContext}
/**
* 带分区的单词计数,使用mapPartitions优化
* mapPartitions比map效率更高,因为减少了函数调用次数
*/
object WordCountWithPartition {
def main(args: Array[String]): Unit = {
// 创建Spark配置,指定本地模式运行
val conf = new SparkConf()
.setAppName("WordCountWithPartition")
.setMaster("local[*]")
// 创建SparkContext
val sc = new SparkContext(conf)
// 读取输入文件
val lines = sc.textFile("input/word.txt")
// 使用mapPartitions以分区为单位处理数据
// mapPartitions内部接收一个迭代器,返回一个迭代器
val wordCounts = lines
.mapPartitions { iter => // iter是每个分区的行迭代器
iter.flatMap(_.split(" ")) // 将每行切分成单词
.filter(_.nonEmpty) // 过滤空字符串
.map((_, 1)) // 转为(单词, 1)格式
}
.reduceByKey(_ + _) // 按单词聚合求和
// 使用foreachPartition以分区为单位输出结果
// 比foreach更高效
wordCounts.foreachPartition { iter =>
iter.foreach(println)
}
// 关闭SparkContext
sc.stop()
}
}
八、Spark SQL
8.1 Spark SQL概述
Spark SQL是Spark用于处理结构化数据的模块。
它提供了:
(1) DataFrame和Dataset API
(2) SQL查询接口
(3) 与Hive整合的能力
(4) 读写多种数据源(JSON、Parquet、CSV、JDBC等)
核心入口:SparkSession(Spark 2.x+)
8.2 DataFrame和Dataset
DataFrame:
- 分布式的数据集合,类似于关系型数据库中的表
- 每列有名称和类型(Schema信息)
- 底层基于RDD,增加了Schema优化
- 支持SQL查询和DataFrame API操作
Dataset(Spark 1.6+):
- DataFrame的类型安全版本
- DataFrame = Dataset[Row]
- 支持编译时类型检查
- 在Scala和Java中可用,Python不支持Dataset
对比:
┌──────────────┬─────────────────┬─────────────────┬─────────────────┐
│ 对比项 │ RDD │ DataFrame │ Dataset │
├──────────────┼─────────────────┼─────────────────┼─────────────────┤
│ 类型安全 │ 编译时类型检查 │ 运行时类型检查 │ 编译时类型检查 │
│ 序列化 │ Java序列化 │ Tungsten优化 │ Tungsten优化 │
│ 优化 │ 无Catalyst优化 │ Catalyst优化器 │ Catalyst优化器 │
│ SQL支持 │ 不支持 │ 支持 │ 支持 │
│ 索引 │ 无 │ 有 │ 有 │
└──────────────┴─────────────────┴─────────────────┴─────────────────┘
8.3 Spark SQL基本使用
8.3.1 创建SparkSession
scala
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.types._
/**
* SparkSession是Spark SQL的统一入口点
* 它封装了SparkContext、SQLContext、HiveContext
*/
object SparkSQLDemo {
def main(args: Array[String]): Unit = {
// 创建SparkSession
val spark = SparkSession.builder()
.appName("SparkSQLDemo") // 设置应用名称
.master("local[*]") // 本地模式运行
.config("spark.sql.warehouse.dir", "hdfs:///user/hive/warehouse")
// .enableHiveSupport() // 启用Hive支持(需要Hive配置)
.getOrCreate() // 获取或创建SparkSession实例
// 从SparkSession获取SparkContext
val sc = spark.sparkContext
// 导入隐式转换(必须导入才能使用$符号操作列)
import spark.implicits._
// ... 业务代码 ...
// 关闭SparkSession
spark.stop()
}
}
8.3.2 创建DataFrame
scala
// ===== 方式一:从集合创建DataFrame =====
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.types._
val spark = SparkSession.builder()
.appName("CreateDataFrame")
.master("local[*]")
.getOrCreate()
// 导入隐式转换
import spark.implicits._
// 1. 使用toDF()方法------从元组列表创建
val df1 = Seq(
("Alice", 25, "F"), // 每个元组代表一行数据
("Bob", 30, "M"),
("Charlie", 35, "M")
).toDF("name", "age", "gender") // 指定列名
// 查看DataFrame的Schema(结构信息)
df1.printSchema()
// root
// |-- name: string (nullable = true)
// |-- age: integer (nullable = true)
// |-- gender: string (nullable = true)
// 查�示数据(默认显示前20行,参数可指定行数)
df1.show()
// +-------+---+------+
// | name|age|gender|
// +-------+---+------+
// | Alice| 25| F|
// | Bob| 30| M|
// |Charlie| 35| M|
// +-------+---+------+
// 2. 使用toDF()------从case class创建(自动推断类型)
case class Person(name: String, age: Int, gender: String)
val df2 = Seq(
Person("Alice", 25, "F"),
Person("Bob", 30, "M")
).toDF()
df2.printSchema() // 自动识别字段名和类型
// ===== 方式二:从JSON文件创建DataFrame =====
// 读取JSON文件,自动推断Schema
val df3 = spark.read
.option("inferSchema", "true") // 自动推断列的数据类型
.option("multiLine", "true") // JSON是否跨行
.json("data/people.json")
df3.printSchema()
df3.show()
// ===== 方式三:从CSV文件创建DataFrame =====
val df4 = spark.read
.option("header", "true") // 第一行是否为表头
.option("inferSchema", "true") // 自动推断数据类型
.option("sep", ",") // 指定分隔符(默认逗号)
.csv("data/people.csv")
df4.printSchema()
df4.show()
// ===== 方式四:从Parquet文件创建DataFrame =====
// Parquet是Spark SQL默认的文件格式
val df5 = spark.read.parquet("data/users.parquet")
df5.printSchema()
df5.show()
// ===== 方式五:使用StructType编程式指定Schema =====
// 当文件没有表头或需要自定义Schema时使用
val schema = StructType(Array(
StructField("name", StringType, nullable = true), // 列名、类型、是否允许为空
StructField("age", IntegerType, nullable = true),
StructField("gender", StringType, nullable = true)
))
val df6 = spark.read
.schema(schema) // 手动指定Schema
.option("header", "false") // 文件没有表头
.csv("data/people_no_header.csv")
df6.printSchema()
df6.show()
// ===== 方式六:从RDD转换创建DataFrame =====
val rdd = sc.textFile("data/people.txt")
// 先将RDD转为元组RDD
val tupleRDD = rdd.map { line =>
val fields = line.split(",")
(fields(0), fields(1).trim.toInt, fields(2).trim)
}
// 使用toDF转换为DataFrame
val df7 = tupleRDD.toDF("name", "age", "gender")
df7.show()
// ===== 方式七:使用createDataFrame方法 =====
val rdd = sc.parallelize(Seq(
Row("Alice", 25, "F"), // Row表示一行数据
Row("Bob", 30, "M")
))
val schema = StructType(Array(
StructField("name", StringType, true),
StructField("age", IntegerType, true),
StructField("gender", StringType, true)
))
val df8 = spark.createDataFrame(rdd, schema)
df8.show()
8.3.3 DataFrame操作(DSL风格)
scala
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.functions._
val spark = SparkSession.builder()
.appName("DataFrameOperations")
.master("local[*]")
.getOrCreate()
import spark.implicits._
// 准备测试数据
val df = Seq(
("Alice", 25, "F", 8000.0, "IT"),
("Bob", 30, "M", 12000.0, "HR"),
("Charlie", 35, "M", 15000.0, "IT"),
("Diana", 28, "F", 9000.0, "Finance"),
("Eve", 32, "F", 11000.0, "HR"),
("Frank", 40, "M", 20000.0, "IT")
).toDF("name", "age", "gender", "salary", "dept")
// ===== (1) select:选择特定列 =====
// 选择单列
df.select("name").show()
// +-------+
// | name|
// +-------+
// | Alice|
// | Bob|
// |Charlie|
// +-------+
// 选择多列
df.select("name", "salary").show()
// 使用$符号操作列(需要import spark.implicits._)
df.select($"name", $"salary" * 12 as "annual_salary").show()
// name列和salary列乘以12得到年薪,并重命名为annual_salary
// 使用col函数操作列
df.select(col("name"), col("salary").as("income")).show()
// ===== (2) filter/where:过滤行 =====
// filter和where功能完全相同
// 过滤salary大于10000的记录
df.filter($"salary" > 10000).show()
// 等价写法
df.where(col("salary") > 10000).show()
// 使用SQL表达式过滤
df.filter("salary > 10000").show()
// 组合条件:AND
df.filter($"salary" > 10000 && $"gender" === "M").show()
// 组合条件:OR
df.filter($"dept" === "IT" || $"dept" === "HR").show()
// IN查询
df.filter($"dept".isin("IT", "HR")).show()
// BETWEEN
df.filter($"age".between(25, 35)).show()
// LIKE
df.filter($"name".like("A%")).show() // 名字以A开头
// ===== (3) groupBy:分组聚合 =====
// 按部门分组,计算每个部门的平均工资
df.groupBy("dept")
.agg(
avg("salary").as("avg_salary"), // 平均工资
sum("salary").as("total_salary"), // 工资总和
count("*").as("emp_count"), // 员工数量
max("salary").as("max_salary"), // 最高工资
min("salary").as("min_salary") // 最低工资
)
.show()
// 按性别和部门分组
df.groupBy("gender", "dept")
.count()
.show()
// ===== (4) orderBy/sort:排序 =====
// 按工资降序排列
df.orderBy($"salary".desc).show()
// 等价写法
df.sort(col("salary").desc).show()
// 多列排序:先按部门升序,再按工资降序
df.orderBy($"dept".asc, $"salary".desc).show()
// ===== (5) withColumn:添加或替换列 =====
// 添加新列:年薪
val dfWithAnnual = df.withColumn("annual_salary", $"salary" * 12)
dfWithAnnual.show()
// 替换现有列:工资上调10%
val dfRaise = df.withColumn("salary", $"salary" * 1.1)
dfRaise.show()
// 重命名列
val dfRenamed = df.withColumnRenamed("salary", "income")
dfRenamed.show()
// ===== (6) drop:删除列 =====
// 删除指定列
val dfDropped = df.drop("gender")
dfDropped.show()
// 删除多列
df.drop("gender", "age").show()
// ===== (7) distinct:去重 =====
// 获取唯一的部门列表
df.select("dept").distinct().show()
// ===== (8) dropDuplicates:按指定列去重 =====
// 按dept列去重(保留第一个出现的记录)
df.dropDuplicates("dept").show()
// 按多列去重
df.dropDuplicates("gender", "dept").show()
// ===== (9) join:连接操作 =====
// 准备部门信息DataFrame
val deptDF = Seq(
("IT", "Building A"),
("HR", "Building B"),
("Finance", "Building C"),
("Marketing", "Building D")
).toDF("dept_name", "location")
// 内连接(默认)
df.join(deptDF, df("dept") === deptDF("dept_name"), "inner").show()
// 左外连接
df.join(deptDF, df("dept") === deptDF("dept_name"), "left").show()
// 右外连接
df.join(deptDF, df("dept") === deptDF("dept_name"), "right").show()
// 全外连接
df.join(deptDF, df("dept") === deptDF("dept_name"), "full").show()
// 左半连接(只返回左表中能匹配到的行)
df.join(deptDF, df("dept") === deptDF("dept_name"), "leftsemi").show()
// 左反连接(只返回左表中匹配不到的行)
df.join(deptDF, df("dept") === deptDF("dept_name"), "leftanti").show()
// ===== (10) 聚合函数 =====
import org.apache.spark.sql.functions._
// 收集某列的所有值为列表
df.agg(collect_list("name").as("all_names")).show(false)
// 收集某列的去重值为列表
df.agg(collect_set("dept").as("all_depts")).show(false)
// 条件聚合
df.agg(
sum(when($"gender" === "M", 1).otherwise(0)).as("male_count"), // 男性数量
sum(when($"gender" === "F", 1).otherwise(0)).as("female_count") // 女性数量
).show()
// ===== (11) 缺失值处理 =====
val dfWithNull = Seq(
("Alice", 25, "F", 8000.0),
("Bob", 30, null, 12000.0),
(null, 35, "M", null),
("Diana", 28, "F", 9000.0)
).toDF("name", "age", "gender", "salary")
// 删除包含null的行
dfWithNull.na.drop().show()
// 只删除指定列有null的行
dfWithNull.na.drop(Seq("name", "gender")).show()
// 删除所有列都为null的行
dfWithNull.na.drop("all").show()
// 填充null值
// 用指定值替换null
dfWithNull.na.fill("unknown").show() // 字符串列填充"unknown"
dfWithNull.na.fill(0).show() // 数值列填充0
// 按列指定填充值
dfWithNull.na.fill(Map(
"name" -> "unknown", // name列的null替换为"unknown"
"gender" -> "other", // gender列的null替换为"other"
"salary" -> 0.0 // salary列的null替换为0.0
)).show()
// ===== (12) 窗口函数 =====
import org.apache.spark.sql.expressions.Window
// 按部门分区,按工资排序
val windowSpec = Window.partitionBy("dept").orderBy($"salary".desc)
// rank:排名(有并列会跳号)
df.withColumn("rank", rank().over(windowSpec)).show()
// 例如:两个并列第1,下一个为第3
// dense_rank:排名(有并列不跳号)
df.withColumn("dense_rank", dense_rank().over(windowSpec)).show()
// 例如:两个并列第1,下一个为第2
// row_number:行号(无并列,唯一编号)
df.withColumn("row_num", row_number().over(windowSpec)).show()
// 每个部门工资最高的员工
val topSalary = df.withColumn("row_num", row_number().over(windowSpec))
.filter($"row_num" === 1)
.drop("row_num")
topSalary.show()
// lag/lead:前一行/后一行的值
val windowByAge = Window.orderBy($"age")
df.withColumn("prev_salary", lag($"salary", 1).over(windowByAge)) // 前一个人的工资
.withColumn("next_salary", lead($"salary", 1).over(windowByAge)) // 后一个人的工资
.show()
8.3.4 DataFrame操作(SQL风格)
scala
import org.apache.spark.sql.SparkSession
val spark = SparkSession.builder()
.appName("SparkSQLDemo")
.master("local[*]")
.getOrCreate()
import spark.implicits._
// 准备数据
val df = Seq(
("Alice", 25, "F", 8000.0, "IT"),
("Bob", 30, "M", 12000.0, "HR"),
("Charlie", 35, "M", 15000.0, "IT"),
("Diana", 28, "F", 9000.0, "Finance"),
("Eve", 32, "F", 11000.0, "HR"),
("Frank", 40, "M", 20000.0, "IT")
).toDF("name", "age", "gender", "salary", "dept")
// 注册为临时视图(Session级别,会话结束自动删除)
df.createOrReplaceTempView("employees")
// 注册为全局临时视图(跨Session共享,Spark应用结束才删除)
// df.createGlobalTempView("global_employees")
// ===== (1) 基本查询 =====
// 查询所有数据
spark.sql("SELECT * FROM employees").show()
// 查询特定列
spark.sql("SELECT name, salary FROM employees").show()
// 带条件查询
spark.sql("SELECT name, salary FROM employees WHERE salary > 10000").show()
// 使用AND/OR组合条件
spark.sql("""
SELECT name, salary, dept
FROM employees
WHERE salary > 10000 AND gender = 'M'
""").show()
// LIKE模糊查询
spark.sql("SELECT * FROM employees WHERE name LIKE 'A%'").show()
// IN查询
spark.sql("SELECT * FROM employees WHERE dept IN ('IT', 'HR')").show()
// BETWEEN范围查询
spark.sql("SELECT * FROM employees WHERE age BETWEEN 25 AND 35").show()
// IS NULL / IS NOT NULL
spark.sql("SELECT * FROM employees WHERE name IS NOT NULL").show()
// ===== (2) 排序 =====
// ORDER BY
spark.sql("""
SELECT name, salary, dept
FROM employees
ORDER BY salary DESC
""").show()
// 多列排序
spark.sql("""
SELECT name, salary, dept
FROM employees
ORDER BY dept ASC, salary DESC
""").show()
// LIMIT限制返回行数
spark.sql("SELECT * FROM employees ORDER BY salary DESC LIMIT 3").show()
// ===== (3) 聚合查询 =====
// GROUP BY + 聚合函数
spark.sql("""
SELECT dept,
COUNT(*) AS emp_count,
AVG(salary) AS avg_salary,
SUM(salary) AS total_salary,
MAX(salary) AS max_salary,
MIN(salary) AS min_salary
FROM employees
GROUP BY dept
""").show()
// HAVING子句(对聚合结果过滤)
spark.sql("""
SELECT dept, AVG(salary) AS avg_salary
FROM employees
GROUP BY dept
HAVING AVG(salary) > 10000
""").show()
// ===== (4) 多表连接 =====
// 创建部门信息表
val deptDF = Seq(
("IT", "Building A"),
("HR", "Building B"),
("Finance", "Building C"),
("Marketing", "Building D")
).toDF("dept_name", "location")
// 注册为临时视图
deptDF.createOrReplaceTempView("departments")
// 内连接
spark.sql("""
SELECT e.name, e.salary, d.location
FROM employees e
INNER JOIN departments d ON e.dept = d.dept_name
""").show()
// 左外连接
spark.sql("""
SELECT e.name, e.salary, d.location
FROM employees e
LEFT JOIN departments d ON e.dept = d.dept_name
""").show()
// 全外连接
spark.sql("""
SELECT e.name, e.salary, d.location
FROM employees e
FULL OUTER JOIN departments d ON e.dept = d.dept_name
""").show()
// ===== (5) 子查询 =====
// 标量子查询:工资高于平均工资的员工
spark.sql("""
SELECT name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees)
""").show()
// IN子查询:IT部门的员工
spark.sql("""
SELECT name, salary
FROM employees
WHERE dept IN (SELECT dept_name FROM departments WHERE location = 'Building A')
""").show()
// EXISTS子查询
spark.sql("""
SELECT name, salary, dept
FROM employees e
WHERE EXISTS (
SELECT 1 FROM departments d WHERE e.dept = d.dept_name
)
""").show()
// ===== (6) CASE WHEN条件表达式 =====
spark.sql("""
SELECT name, salary,
CASE
WHEN salary >= 15000 THEN '高薪'
WHEN salary >= 10000 THEN '中等'
ELSE '低薪'
END AS salary_level
FROM employees
""").show()
// ===== (7) 窗口函数 =====
spark.sql("""
SELECT name, dept, salary,
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS row_num,
RANK() OVER (PARTITION BY dept ORDER BY salary DESC) AS rank,
DENSE_RANK() OVER (PARTITION BY dept ORDER BY salary DESC) AS dense_rank
FROM employees
""").show()
// 每个部门工资最高的员工
spark.sql("""
SELECT name, dept, salary
FROM (
SELECT name, dept, salary,
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn
FROM employees
) tmp
WHERE rn = 1
""").show()
// 累计求和
spark.sql("""
SELECT name, dept, salary,
SUM(salary) OVER (PARTITION BY dept ORDER BY salary) AS running_total
FROM employees
""").show()
// ===== (8) 自定义SQL函数 =====
// 注册UDF(User Defined Function)
// 无类型UDF
spark.udf.register("salary_level", (salary: Double) => {
if (salary >= 15000) "高薪"
else if (salary >= 10000) "中等"
else "低薪"
})
// 在SQL中使用自定义函数
spark.sql("""
SELECT name, salary, salary_level(salary) AS level
FROM employees
""").show()
// ===== (9) 创建临时表 =====
// 使用CREATE TEMPORARY VIEW
spark.sql("""
CREATE OR REPLACE TEMPORARY VIEW high_salary AS
SELECT name, salary, dept
FROM employees
WHERE salary > 10000
""")
spark.sql("SELECT * FROM high_salary").show()
// ===== (10) 保存结果 =====
// 保存为Parquet格式(默认)
df.write.mode("overwrite").parquet("output/employees.parquet")
// 保存为JSON格式
df.write.mode("overwrite").json("output/employees.json")
// 保存为CSV格式
df.write.mode("overwrite")
.option("header", "true")
.csv("output/employees.csv")
// 保存为单个文件(先合并为1个分区)
df.coalesce(1).write.mode("overwrite").csv("output/employees_single")
// 按列分区存储(类似Hive的分区表)
df.write.mode("overwrite")
.partitionBy("dept")
.parquet("output/employees_by_dept")
8.3.5 RDD与DataFrame互转
scala
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.types._
import org.apache.spark.sql.Row
val spark = SparkSession.builder()
.appName("RDDDataFrameConversion")
.master("local[*]")
.getOrCreate()
val sc = spark.sparkContext
import spark.implicits._
// ===== RDD → DataFrame =====
// 方式一:使用toDF(需要导入隐式转换)
val rdd1 = sc.parallelize(Seq(("Alice", 25), ("Bob", 30)))
val df1 = rdd1.toDF("name", "age")
df1.show()
// 方式二:使用反射(case class)
case class Person(name: String, age: Int)
val rdd2 = sc.parallelize(Seq(Person("Alice", 25), Person("Bob", 30)))
val df2 = rdd2.toDF()
df2.show()
// 方式三:编程式指定Schema
val rdd3 = sc.parallelize(Seq(Row("Alice", 25), Row("Bob", 30)))
val schema = StructType(Seq(
StructField("name", StringType, true),
StructField("age", IntegerType, true)
))
val df3 = spark.createDataFrame(rdd3, schema)
df3.show()
// ===== DataFrame → RDD =====
// 使用.rdd方法将DataFrame转为RDD[Row]
val rdd = df1.rdd
// 访问Row中的元素(按索引或按列名)
rdd.foreach(row => {
val name = row.getString(0) // 按索引获取(第一个字段)
val age = row.getInt(1) // 按索引获取(第二个字段)
println(s"$name, $age")
})
// 使用getAs方法按列名获取值
rdd.foreach(row => {
val name = row.getAs[String]("name") // 按列名获取
val age = row.getAs[Int]("age")
println(s"$name, $age")
})
8.4 案例分析:使用Spark SQL实现单词计数
scala
import org.apache.spark.sql.SparkSession
/**
* 使用Spark SQL实现单词计数
* 将文本数据转为DataFrame,使用SQL进行单词统计
*/
object WordCountSQL {
def main(args: Array[String]): Unit = {
// ===== 第一步:创建SparkSession =====
val spark = SparkSession.builder()
.appName("WordCountSQL")
.master("local[*]")
.getOrCreate()
// 导入隐式转换
import spark.implicits._
// ===== 第二步:读取文本文件为DataFrame =====
// textFile返回的是Dataset[String],每行是一个元素
val linesDF = spark.read.textFile("input/word.txt")
.toDF("line") // 将Dataset转为DataFrame,列名为"line"
// ===== 第三步:使用SQL进行单词计数 =====
// 方法一:使用explode函数
// explode将数组中的每个元素展开为单独的一行
linesDF.createOrReplaceTempView("lines")
val wordCountDF = spark.sql(
"""
|SELECT
| word, -- 单词
| COUNT(*) AS count -- 统计出现次数
|FROM (
| SELECT
| explode( -- 将数组展开为多行
| split(line, ' ') -- 按空格切分为单词数组
| ) AS word
| FROM lines -- 从lines表中读取
|) tmp
|WHERE word != '' -- 过滤空字符串
|GROUP BY word -- 按单词分组
|ORDER BY count DESC -- 按次数降序排列
""".stripMargin
)
// ===== 第四步:输出结果 =====
wordCountDF.show()
// ===== 第五步:保存结果 =====
wordCountDF.write.mode("overwrite")
.option("header", "true")
.csv("output/wordcount_sql")
// 关闭SparkSession
spark.stop()
}
}
使用DataFrame API实现单词计数
scala
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.functions._
/**
* 使用DataFrame API实现单词计数(不使用SQL语句)
*/
object WordCountDataFrame {
def main(args: Array[String]): Unit = {
val spark = SparkSession.builder()
.appName("WordCountDataFrame")
.master("local[*]")
.getOrCreate()
import spark.implicits._
// 读取文本文件
val linesDF = spark.read.textFile("input/word.txt").toDF("line")
// 使用DataFrame API进行单词计数
val wordCountDF = linesDF
// 1. 使用split函数按空格切分,得到单词数组列
.select(explode(split($"line", " ")).as("word"))
// 2. 使用explode将数组展开为多行
// 3. 过滤空字符串
.filter($"word" =!= "")
// 4. 按单词分组并计数
.groupBy("word")
.count()
// 5. 按次数降序排列
.orderBy($"count".desc)
// 显示结果
wordCountDF.show()
spark.stop()
}
}
8.5 案例分析:Spark SQL与Hive整合
8.5.1 配置准备
bash
# ===== (1) 确保Hive已安装并配置好 =====
# Hive的hive-site.xml需要配置好metastore连接信息
# ===== (2) 复制Hive配置文件到Spark的conf目录 =====
# 将hive-site.xml复制到Spark的配置目录
cp $HIVE_HOME/conf/hive-site.xml $SPARK_HOME/conf/
# ===== (3) 复制MySQL驱动(如果Hive使用MySQL作为元数据库)=====
cp mysql-connector-java-8.0.26.jar $SPARK_HOME/jars/
# ===== (4) 确保HDFS上有Hive的warehouse目录 =====
hadoop fs -mkdir -p /user/hive/warehouse
hadoop fs -chmod 777 /user/hive/warehouse
# ===== (5) 启动Hive Metastore服务(如果使用远程模式)=====
hive --service metastore &
8.5.2 代码实现
scala
import org.apache.spark.sql.SparkSession
/**
* Spark SQL整合Hive
* 使用Spark SQL直接查询Hive中的表
* 前提:已将hive-site.xml复制到Spark的conf目录
*/
object SparkSQLHiveDemo {
def main(args: Array[String]): Unit = {
// 创建SparkSession并启用Hive支持
// enableHiveSupport()会读取hive-site.xml配置,连接Hive Metastore
val spark = SparkSession.builder()
.appName("SparkSQLHiveDemo")
.master("local[*]")
.config("spark.sql.warehouse.dir", "/user/hive/warehouse") // Hive warehouse路径
.enableHiveSupport() // 启用Hive支持(必须)
.getOrCreate()
// 导入隐式转换
import spark.implicits._
// ===== (1) 查看所有数据库 =====
spark.sql("SHOW DATABASES").show()
// ===== (2) 创建数据库 =====
spark.sql("CREATE DATABASE IF NOT EXISTS spark_hive_db")
spark.sql("USE spark_hive_db") // 切换到该数据库
// ===== (3) 创建Hive表 =====
spark.sql("""
CREATE TABLE IF NOT EXISTS employees (
name STRING,
age INT,
salary DOUBLE,
dept STRING
)
ROW FORMAT DELIMITED
FIELDS TERMINATED BY ','
STORED AS TEXTFILE
""")
// ===== (4) 向表中插入数据 =====
// 使用DataFrame写入Hive表
val empDF = Seq(
("Alice", 25, 8000.0, "IT"),
("Bob", 30, 12000.0, "HR"),
("Charlie", 35, 15000.0, "IT"),
("Diana", 28, 9000.0, "Finance")
).toDF("name", "age", "salary", "dept")
// 将DataFrame数据写入Hive表
// mode("overwrite"):覆盖已有数据
// mode("append"):追加数据
empDF.write.mode("overwrite").saveAsTable("spark_hive_db.employees")
// ===== (5) 使用SQL查询Hive表 =====
// 查询所有数据
spark.sql("SELECT * FROM employees").show()
// 条件查询
spark.sql("""
SELECT name, salary, dept
FROM employees
WHERE salary > 10000
ORDER BY salary DESC
""").show()
// 分组聚合查询
spark.sql("""
SELECT dept,
COUNT(*) AS count,
AVG(salary) AS avg_salary
FROM employees
GROUP BY dept
ORDER BY avg_salary DESC
""").show()
// ===== (6) 使用CTAS创建新表 =====
spark.sql("""
CREATE TABLE high_salary_employees AS
SELECT * FROM employees WHERE salary > 10000
""")
spark.sql("SELECT * FROM high_salary_employees").show()
// ===== (7) 创建分区表 =====
spark.sql("""
CREATE TABLE IF NOT EXISTS employees_partitioned (
name STRING,
age INT,
salary DOUBLE
)
PARTITIONED BY (dept STRING)
ROW FORMAT DELIMITED
FIELDS TERMINATED BY ','
STORED AS PARQUET
""")
// 写入分区表
empDF.write
.mode("overwrite")
.partitionBy("dept") // 按dept列分区
.saveAsTable("employees_partitioned")
// 查询分区表
spark.sql("SELECT * FROM employees_partitioned").show()
// 分区裁剪(只扫描指定分区)
spark.sql("""
SELECT * FROM employees_partitioned
WHERE dept = 'IT'
""").show()
// ===== (8) 查看表信息 =====
spark.sql("DESCRIBE FORMATTED employees").show(50, truncate = false)
// ===== (9) 查看表的创建语句 =====
spark.sql("SHOW CREATE TABLE employees").show(truncate = false)
// ===== (10) 删除表和数据库 =====
spark.sql("DROP TABLE IF EXISTS high_salary_employees")
spark.sql("DROP TABLE IF EXISTS employees")
spark.sql("DROP TABLE IF EXISTS employees_partitioned")
spark.sql("DROP DATABASE IF EXISTS spark_hive_db CASCADE")
// 关闭SparkSession
spark.stop()
}
}
8.6 案例分析:Spark SQL读写MySQL
8.6.1 添加MySQL驱动
bash
# 将MySQL JDBC驱动jar包添加到Spark的jars目录
cp mysql-connector-java-8.0.26.jar $SPARK_HOME/jars/
# 或者在提交任务时指定
spark-submit --jars /path/to/mysql-connector-java-8.0.26.jar ...
8.6.2 代码实现
scala
import org.apache.spark.sql.SparkSession
import java.util.Properties
/**
* Spark SQL读写MySQL
* 功能:从MySQL读取数据,在Spark中处理后写回MySQL
*/
object SparkSQLMySQLDemo {
def main(args: Array[String]): Unit = {
// 创建SparkSession
val spark = SparkSession.builder()
.appName("SparkSQLMySQLDemo")
.master("local[*]")
.getOrCreate()
import spark.implicits._
// ===== 配置MySQL连接参数 =====
// 创建Properties对象,存放MySQL连接配置
val mysqlProps = new Properties()
mysqlProps.put("user", "root") // MySQL用户名
mysqlProps.put("password", "123456") // MySQL密码
mysqlProps.put("driver", "com.mysql.cj.jdbc.Driver") // JDBC驱动类名
// MySQL JDBC URL
val jdbcUrl = "jdbc:mysql://hadoop101:3306/spark_db?useSSL=false&serverTimezone=UTC"
// ===== (1) 方式一:使用spark.read.jdbc读取MySQL =====
// 读取MySQL中的users表
val usersDF = spark.read
.jdbc(jdbcUrl, "users", mysqlProps) // 参数:JDBC URL、表名、连接属性
println("=== 从MySQL读取的users表 ===")
usersDF.printSchema()
usersDF.show()
// ===== (2) 方式二:使用SQL方式读取MySQL =====
// 使用CREATE TEMPORARY VIEW方式读取
spark.read
.format("jdbc") // 指定数据源格式为jdbc
.option("url", jdbcUrl) // JDBC连接URL
.option("dbtable", "users") // 要读取的表名
.option("user", "root") // 用户名
.option("password", "123456") // 密码
.load()
.createOrReplaceTempView("mysql_users") // 注册为临时视图
// 使用SQL查询
spark.sql("SELECT * FROM mysql_users WHERE age > 25").show()
// ===== (3) 读取时指定分区(大数据量推荐)=====
// 使用predicates分区读取,提高并行度
val predicates = Array(
"age < 25",
"age >= 25 AND age < 35",
"age >= 35"
)
val partitionedDF = spark.read
.jdbc(jdbcUrl, "users", predicates, mysqlProps) // 使用predicates分区
println("=== 分区读取结果 ===")
partitionedDF.show()
// ===== (4) 读取时指定分区列(更灵活的分区方式)=====
val dfPartitionedByColumn = spark.read
.format("jdbc")
.option("url", jdbcUrl)
.option("dbtable", "users")
.option("user", "root")
.option("password", "123456")
.option("partitionColumn", "id") // 分区列(必须是数值类型)
.option("lowerBound", "1") // 分区列下界
.option("upperBound", "1000") // 分区列上界
.option("numPartitions", "4") // 分区数量
.load()
println("=== 按id列分区读取 ===")
dfPartitionedByColumn.show()
// ===== (5) 写入MySQL =====
// 准备要写入的数据
val newUsersDF = Seq(
(101, "张三", 28, "IT", 15000.0),
(102, "李四", 32, "HR", 12000.0),
(103, "王五", 26, "Finance", 10000.0),
(104, "赵六", 35, "IT", 18000.0),
(105, "钱七", 29, "HR", 13000.0)
).toDF("id", "name", "age", "dept", "salary")
// 写入MySQL
// mode选项:
// "overwrite" - 删除旧表重新创建(慎用!会删除表结构和数据)
// "append" - 追加到已有表(推荐)
// "ignore" - 如果表已存在则忽略
// "error" - 如果表已存在则报错(默认)
// append模式追加数据
newUsersDF.write
.mode("append")
.jdbc(jdbcUrl, "users", mysqlProps)
println("=== 数据写入MySQL成功 ===")
// ===== (6) 写入时先建表(overwrite模式)=====
newUsersDF.write
.mode("overwrite")
.jdbc(jdbcUrl, "new_users", mysqlProps) // 自动创建new_users表
// 验证写入结果
spark.read.jdbc(jdbcUrl, "new_users", mysqlProps).show()
// ===== (7) 使用SQL写入MySQL =====
// 先创建临时视图
newUsersDF.createOrReplaceTempView("temp_users")
// 使用DataFrame API写入
spark.sql("SELECT * FROM temp_users WHERE salary > 12000")
.write
.mode("append")
.format("jdbc")
.option("url", jdbcUrl)
.option("dbtable", "high_salary_users")
.option("user", "root")
.option("password", "123456")
.save()
println("=== 高薪用户已写入MySQL ===")
// ===== (8) 从MySQL读取数据 → Spark处理 → 写回MySQL =====
// 读取MySQL数据
val allUsers = spark.read.jdbc(jdbcUrl, "users", mysqlProps)
// Spark SQL处理:按部门统计平均工资
val deptAvgSalary = allUsers
.groupBy("dept")
.agg(
org.apache.spark.sql.functions.count("*").as("emp_count"),
org.apache.spark.sql.functions.avg("salary").as("avg_salary"),
org.apache.spark.sql.functions.sum("salary").as("total_salary")
)
// 将统计结果写回MySQL的dept_statistics表
deptAvgSalary.write
.mode("overwrite")
.jdbc(jdbcUrl, "dept_statistics", mysqlProps)
println("=== 部门统计结果已写入MySQL ===")
// 验证统计结果
spark.read.jdbc(jdbcUrl, "dept_statistics", mysqlProps).show()
// ===== (9) 使用mapreduce分区写入(控制并行度)=====
newUsersDF.write
.mode("append")
.option("batchsize", "1000") // 每批写入的行数
.option("isolationLevel", "NONE") // 事务隔离级别
.jdbc(jdbcUrl, "users_batch", mysqlProps)
// 关闭SparkSession
spark.stop()
}
}
8.6.3 MySQL中提前建表语句
sql
-- 创建数据库
CREATE DATABASE IF NOT EXISTS spark_db DEFAULT CHARSET utf8mb4;
USE spark_db;
-- 创建users表
CREATE TABLE IF NOT EXISTS users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
age INT,
dept VARCHAR(50),
salary DOUBLE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 插入一些初始数据
INSERT INTO users (name, age, dept, salary) VALUES
('Alice', 25, 'IT', 8000.00),
('Bob', 30, 'HR', 12000.00),
('Charlie', 35, 'IT', 15000.00),
('Diana', 28, 'Finance', 9000.00),
('Eve', 32, 'HR', 11000.00);
九、综合案例汇总
9.1 案例:TopN分析
scala
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.functions._
import org.apache.spark.sql.expressions.Window
/**
* TopN案例:找出每个类别中销售额前N的商品
*/
object TopNAnalysis {
def main(args: Array[String]): Unit = {
val spark = SparkSession.builder()
.appName("TopNAnalysis")
.master("local[*]")
.getOrCreate()
import spark.implicits._
// 准备数据
val salesDF = Seq(
("手机", "Apple", 8999.0),
("手机", "Huawei", 6999.0),
("手机", "Xiaomi", 3999.0),
("手机", "OPPO", 4999.0),
("电脑", "Apple", 12999.0),
("电脑", "Dell", 8999.0),
("电脑", "Lenovo", 7999.0),
("电脑", "HP", 9999.0),
("平板", "Apple", 5999.0),
("平板", "Huawei", 3999.0),
("平板", "Samsung", 4999.0)
).toDF("category", "brand", "price")
val N = 2 // 取Top2
// 方法一:使用窗口函数
val windowSpec = Window
.partitionBy("category") // 按类别分区
.orderBy($"price".desc) // 按价格降序
val topN = salesDF
.withColumn("rank", row_number().over(windowSpec)) // 添加排名列
.filter($"rank" <= N) // 取前N名
.drop("rank") // 删除排名列
println(s"=== 每个类别Top $N ===")
topN.show()
// 方法二:使用SQL
salesDF.createOrReplaceTempView("sales")
val topNSQL = spark.sql(s"""
SELECT category, brand, price
FROM (
SELECT category, brand, price,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY price DESC) AS rn
FROM sales
) tmp
WHERE rn <= $N
""")
println(s"=== SQL方式 Top $N ===")
topNSQL.show()
spark.stop()
}
}
9.2 案例:日志分析
scala
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.functions._
import java.text.SimpleDateFormat
/**
* Web日志分析案例
* 分析Web服务器访问日志,统计PV、UV、热门页面等
*/
object LogAnalysis {
def main(args: Array[String]): Unit = {
val spark = SparkSession.builder()
.appName("LogAnalysis")
.master("local[*]")
.getOrCreate()
import spark.implicits._
// 模拟Web日志数据
val logData = Seq(
"192.168.1.1 - - [01/Jan/2024:10:00:00 +0800] \"GET /index.html HTTP/1.1\" 200 1024",
"192.168.1.2 - - [01/Jan/2024:10:01:00 +0800] \"GET /about.html HTTP/1.1\" 200 2048",
"192.168.1.1 - - [01/Jan/2024:10:02:00 +0800] \"GET /index.html HTTP/1.1\" 200 1024",
"192.168.1.3 - - [01/Jan/2024:10:03:00 +0800] \"GET /contact.html HTTP/1.1\" 404 512",
"192.168.1.2 - - [01/Jan/2024:10:04:00 +0800] \"GET /index.html HTTP/1.1\" 200 1024",
"192.168.1.4 - - [01/Jan/2024:10:05:00 +0800] \"GET /product.html HTTP/1.1\" 200 4096",
"192.168.1.1 - - [01/Jan/2024:10:06:00 +0800] \"GET /product.html HTTP/1.1\" 200 4096"
)
val logRDD = spark.sparkContext.parallelize(logData)
// 使用正则表达式解析日志
val logPattern = """^(\S+) \S+ \S+ \[(.+?)\] "(\S+) (\S+) \S+" (\d+) (\d+)$""".r
// 将RDD转为结构化的DataFrame
val parsedDF = logRDD.flatMap { line =>
line match {
case logPattern(ip, time, method, url, status, size) =>
Some((ip, time, method, url, status.toInt, size.toLong))
case _ => None
}
}.toDF("ip", "time", "method", "url", "status", "size")
// 注册临时视图
parsedDF.createOrReplaceTempView("access_log")
// ===== (1) PV(页面浏览量)=====
val pv = parsedDF.count()
println(s"总PV: $pv")
// ===== (2) UV(独立访客数)=====
val uv = parsedDF.select("ip").distinct().count()
println(s"总UV: $uv")
// ===== (3) 热门页面Top5 =====
println("=== 热门页面 ===")
spark.sql("""
SELECT url, COUNT(*) AS pv
FROM access_log
GROUP BY url
ORDER BY pv DESC
LIMIT 5
""").show()
// ===== (4) HTTP状态码分布 =====
println("=== HTTP状态码分布 ===")
spark.sql("""
SELECT status, COUNT(*) AS count
FROM access_log
GROUP BY status
ORDER BY count DESC
""").show()
// ===== (5) 每个IP的访问次数 =====
println("=== IP访问次数 ===")
spark.sql("""
SELECT ip, COUNT(*) AS visits,
COUNT(DISTINCT url) AS unique_pages
FROM access_log
GROUP BY ip
ORDER BY visits DESC
""").show()
// ===== (6) 404错误页面 =====
println("=== 404错误页面 ===")
spark.sql("""
SELECT url, ip, time
FROM access_log
WHERE status = 404
""").show()
spark.stop()
}
}
十、Spark核心配置参数汇总
properties
# ==================== Application配置 ====================
spark.app.name # 应用名称
spark.master # Master URL
spark.submit.deployMode # 部署模式(client/cluster)
# ==================== Driver配置 ====================
spark.driver.memory # Driver内存,如2g
spark.driver.cores # Driver核心数
spark.driver.maxResultSize # Action算子结果大小限制
# ==================== Executor配置 ====================
spark.executor.memory # 每个Executor内存,如4g
spark.executor.cores # 每个Executor核心数
spark.executor.instances # Executor数量(等同于num-executors)
# ==================== Shuffle配置 ====================
spark.shuffle.manager # Shuffle管理器(sort/hash/tungsten-sort)
spark.shuffle.compress # 是否压缩Shuffle数据(true)
spark.shuffle.spill.compress # 是否压缩溢写数据(true)
spark.sql.shuffle.partitions # SQL Shuffle后分区数(200)
# ==================== 序列化配置 ====================
spark.serializer # 序列化器(org.apache.spark.serializer.KryoSerializer)
spark.kryoserializer.buffer.max # Kryo序列化缓冲区最大值
# ==================== 内存管理 ====================
spark.memory.fraction # 统一内存管理占用总内存比例(0.6)
spark.memory.storageFraction # 存储内存占统一内存的比例(0.5)
# ==================== Spark SQL配置 ====================
spark.sql.warehouse.dir # Hive warehouse目录
spark.sql.autoBroadcastJoinThreshold # 广播表大小阈值(10MB)
spark.sql.parquet.compression.codec # Parquet压缩方式(snappy)
十一、RDD vs DataFrame vs Dataset 性能对比
┌──────────────────────┬──────────────────────┬──────────────────────┐
│ 操作 │ RDD耗时 │ DataFrame/DS耗时 │
├──────────────────────┼──────────────────────┼──────────────────────┤
│ 读取10GB文本文件 │ 约30s │ 约15s │
│ 统计单词频率 │ 约25s │ 约8s │
│ 两表JOIN(1亿行) │ 约120s │ 约40s │
│ 写Parquet文件 │ 约20s │ 约5s │
└──────────────────────┴──────────────────────┴──────────────────────┘
性能提升原因:
(1) Catalyst优化器:逻辑计划→物理计划优化
(2) Tungsten引擎:内存管理优化、代码生成
(3) 列式存储格式:减少IO,压缩率更高