Hadoop学习教程,从入门到精通, Spark 完整知识点详解(14)

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,压缩率更高