Spark - Code 核心教程

一、概述

1、简介

Spark 是一种基于内存的快速、通用、可扩展的大数据分析计算引擎。

官网地址https://spark.apache.org/

2、与Hadoop对比

|--------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------|
| Spark | Hadoop |
| Spark是一种由Scala语言开发的快速、通用、可扩展的大数据分析引擎 | Hadoop是由java语言编写的,在分布式服务器集群上存储海量数据并运行分布式 分析应用的开源框架 |
| Spark Core 中提供了Spark最基础与最核心的功能 | 作为Hadoop 分布式文件系统,HDFS处于Hadoop生态圈的最下层,存储着所有 的数据,支持着 Hadoop 的所有服务。 |
| Spark SQL是Spark用来操作结构化数据的组件。通过Spark SQL,用户可以使用 SQL 或者Apache Hive 版本的SQL方言(HQL)来查询数据。 | MapReduce 是一种编程模型,作为Hadoop 的分布式计算模型,是Hadoop的核心。基于这个框架,分布式并行程序的编写变得异常简单。综合了HDFS的分布式存储和MapReduce的分布式计 算,Hadoop在处理海量数据时,性能横向扩展变得非常容易。 |
| Spark Streaming 是 Spark 平台上针对实时数据进行流式计算的组件,提供了丰富的 处理数据流的API。 | HBase是对Google 的Bigtable 的开源实现,但又和Bigtable 存在许多不同之处。 HBase 是一个基于HDFS的分布式数据库,擅长实时地随机读/写超大规模数据集。 它也是Hadoop非常重要的组件。 |
| Spark多个作业之间数据 通信是基于内存 | Hadoop是基于磁盘 |
| Spark只有在shuffle的时候将数据写入磁盘 | 而Hadoop中多个MR作业之间的数据交 互都要依赖于磁盘交互 |

3、核心模块

1)Spark Core: Spark Core 中提供了 Spark 最基础与最核心的功能,Spark其他的功能如:Spark SQL, Spark Streaming,GraphX, MLlib 都是在 Spark Core 的基础上进行扩展的

2)Spark SQL: Spark SQL 是 Spark 用来操作结构化数据的组件。通过Spark SQL,用户可以使用SQL 或者Apache Hive 版本的SQL方言(HQL)来查询数据。

3)Spark Streaming: Spark Streaming 是 Spark 平台上针对实时数据进行流式计算的组件,提供了丰富的处理数据流的API。

4)Spark MLlib: MLlib 是 Spark 提供的一个机器学习算法库。MLlib不仅提供了模型评估、数据导入等额外的功能,还提供了一些更底层的机器学习原语。

5)Spark GraphX: GraphX 是Spark 面向图计算提供的框架与算法库。

二、快速上手

在Idea中搭建Maven项目

(1)增加 Scala 插件

file --> settings --> plugins --> 搜索scala

(2)创建工程

(3)添加依赖

XML 复制代码
    <dependencies>
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-core_2.12</artifactId>
            <version>3.0.0</version>
        </dependency>
    </dependencies>

(4)WordCount案例

1)方式一

Scala 复制代码
object WordCountDemo1 {
  def main(args: Array[String]): Unit = {
    // 建立与Spark框架的连接
    val sparkConf = new SparkConf().setMaster("local").setAppName("WordCount")
    val context = new SparkContext(sparkConf)

    // 一行一行读取文件
    val lines: RDD[String] = context.textFile("datas")

    // 扁平化:将整体拆分成个体的操作(分词)
    val words: RDD[String] = lines.flatMap(item => item.split(" "))

    // 进行分组统计
    val wordGroup: RDD[(String, Iterable[String])]  = words.groupBy(word => word)
    val wordCount: RDD[(String, Int)] = wordGroup.map { case (word, wordList) => (word, wordList.size) }

    // 将转换结果采集到控制台打印出来
    val array: Array[(String, Int)] = wordCount.collect()
    array.foreach(println)

    // 关闭连接
    context.stop()
  }
}

2)方式二

Scala 复制代码
object WordCountDemo2 {
  def main(args: Array[String]): Unit = {
    // 建立与Spark框架的连接
    val sparkConf = new SparkConf().setMaster("local").setAppName("WordCount")
    val context = new SparkContext(sparkConf)

    // 一行一行读取文件
    val lines: RDD[String] = context.textFile("datas")

    // 扁平化:将整体拆分成个体的操作(分词)
    val words: RDD[String] = lines.flatMap(item => item.split(" "))

    // 将单词进行结构的转换
    val wordToOne: RDD[(String, Int)] = words.map(word => (word, 1))

    // 进行分组统计
    val wordGroup: RDD[(String, Iterable[(String, Int)])] = wordToOne.groupBy(_._1)
    val wordCount: RDD[(String, Int)] = wordGroup.map { case (word, list) => list.reduce((a, b) => (word, a._2 + b._2)) }

    // 将转换结果采集到控制台打印出来
    val array: Array[(String, Int)] = wordCount.collect()
    array.foreach(println)

    // 关闭连接
    context.stop()
  }
}

3)方式三

Scala 复制代码
object WordCountDemo3 {
  def main(args: Array[String]): Unit = {
    // 建立与Spark框架的连接
    val sparkConf = new SparkConf().setMaster("local").setAppName("WordCount")
    val context = new SparkContext(sparkConf)

    // 一行一行读取文件
    val lines: RDD[String] = context.textFile("datas")

    // 扁平化:将整体拆分成个体的操作(分词)
    val words: RDD[String] = lines.flatMap(item => item.split(" "))

    // 将单词进行结构的转换
    val wordToOne = words.map(word => (word, 1))

    // 进行统计
    val wordCount: RDD[(String, Int)] = wordToOne.reduceByKey(_ + _)

    // 将转换结果采集到控制台打印出来
    val array: Array[(String, Int)] = wordCount.collect()
    array.foreach(println)

    // 关闭连接
    context.stop()
  }
}

(5)创建数据

(6)运行

(7)异常处理

运行时出现

java.io.IOException: Could not locate executable null\bin\winutils.exe in the Hadoop binaries.

出现这个问题的原因,并不是程序的错误,而是windows系统用到了hadoop相关的服 务,解决办法是通过配置关联到windows的系统依赖就可以了。

解决办法:在IDEA中配置Run Configuration,添加HADOOP_HOME变量

**例如:**HADOOP_HOME=D:\mysoftware\WindowsDep\hadoop-3.0.0

(8)日志文件

执行过程中,会产生大量的执行日志,如果为了能够更好的查看程序的执行结果,可以在项目的resources目录中创建log4j.properties文件,并添加日志配置信息:

Scala 复制代码
log4j.rootCategory=ERROR, console

log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.target=System.err
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d{yy/MM/dd HH:mm:ss} %p %c{1}: %m%n

# Set the default spark-shell log level to ERROR. When running the spark-shell,the
# log level for this class is used to overwrite the root logger's log level, so that
# the user can have different defaults for the shell and regular Spark apps.
log4j.logger.org.apache.spark.repl.Main=ERROR

# Settings to quiet third party logs that are too verbose
log4j.logger.org.spark_project.jetty=ERROR
log4j.logger.org.spark_project.jetty.util.component.AbstractLifeCycle=ERROR
log4j.logger.org.apache.spark.repl.SparkIMain$exprTyper=ERROR
log4j.logger.org.apache.spark.repl.SparkILoop$SparkILoopInterpreter=ERROR
log4j.logger.org.apache.parquet=ERROR
log4j.logger.parquet=ERROR

# SPARK-9183: Settings to avoid annoying messages when looking up nonexistent
UDFs in SparkSQL with Hive support
log4j.logger.org.apache.hadoop.hive.metastore.RetryingHMSHandler=FATAL
log4j.logger.org.apache.hadoop.hive.ql.exec.FunctionRegistry=ERROR

三、Spark运行环境

1、Local 模式

所谓的Local模式,就是不需要其他任何节点资源就可以在本地执行Spark代码的环境,一般用于调试,演示等, 之前在IDEA中运行代码的环境我们称之为开发环境,不太一样。

(1)上传并解压文件

将spark-3.0.0-bin-hadoop3.2.tgz 文件上传到 Linux 并解压缩,路径中不要包含中文或空格。

tar -zxvf spark-3.0.0-bin-hadoop3.2.tgz -C /opt/module/

cd /opt/module

mv spark-3.0.0-bin-hadoop3.2 spark-local

(2)启动 Local 环境

bin/spark-shell

监控页面访问 http://hd01:4040

(3)命令行工具

在解压缩文件夹下的data目录中,添加word.txt文件。在命令行工具中执行如下代码指令(和IDEA中代码简化版一致)

sc.textFile("data/word.txt").flatMap(.split(" ")).map((,1)).reduceByKey(+).collect

(4)退出本地模式

按键Ctrl+C或输入Scala指令 :quit

(5)提交应用

bin/spark-submit \

--class org.apache.spark.examples.SparkPi \

--master local[2] \

./examples/jars/spark-examples_2.12-3.0.0.jar \

10

说明:

    1. --class: 表示要执行程序的主类,可以更换为自己写的应用程序
    1. --master local[2]: 部署模式,默认为本地模式,数字表示分配的虚拟CPU核数量
    1. spark-examples_2.12-3.0.0.jar: 运行的应用类所在的jar 包,实际使用时,可以设定为自己打的jar包
    1. 数字10:表示程序的入口参数,用于设定当前应用的任务数量

2、Standalone 模式

local 本地模式只是用来进行测试的,真实工作中要将应用提交到对应的集群中去执行,只使用Spark自身节点运行的集群模式,就是所谓的独立部署(Standalone)模式。Spark的Standalone 模式体现了经典的master-slave模式。

(1)集群规划

|-------|---------------|--------|--------|
| | hd01 | hd02 | hd03 |
| Spark | Worker Master | Worker | Worker |

(2)解压缩文件

将spark-3.0.0-bin-hadoop3.2.tgz 文件上传到 Linux 并解压缩在指定位置

tar -zxvf spark-3.0.0-bin-hadoop3.2.tgz -C /opt/module/

cd /opt/module

mv spark-3.0.0-bin-hadoop3.2 spark-standalone

(3)修改配置文件

  1. 进入conf目录,复制slaves.template文件名为slaves

cp slaves.template slaves

vim slaves

2)修改slaves文件,添加work节点

hd01

hd02

hd03

3)复制spark-env.sh.template 文件名为 spark-env.sh

cp spark-env.sh.template spark-env.sh

vim spark-env.sh

4)修改spark-env.sh 文件,添加JAVA_HOME环境变量和集群对应的master节点

export JAVA_HOME=/opt/module/jdk1.8

SPARK_MASTER_HOST=hd01

SPARK_MASTER_PORT=7077

注意:7077端口,相当于hadoop3内部通信的8020端口,此处的端口需要确认自己的Hadoop 配置

5)分发

分发spark-standalone 目录到其他节点

xsync spark-standalone

(4)启动集群

  1. 执行脚本命令

sbin/start-all.sh

2)查看三台服务器运行进程

3)查看Master资源监控Web UI界面: http://hd01:8080

(5)提交应用

bin/spark-submit \

--class org.apache.spark.examples.SparkPi \

--master spark://hd01:7077 \

./examples/jars/spark-examples_2.12-3.0.0.jar \

10

说明:

    1. --class: 表示要执行程序的主类
    1. --master: spark://hd01:7077 独立部署模式,连接到 Spark 集群
    1. spark-examples_2.12-3.0.0.jar: 运行类所在的 jar包
    1. 数字10:表示程序的入口参数,用于设定当前应用的任务数量

(6)提交参数说明

|--------------------------|--------------------------------------------------------------------------------------------|----------------------------------------|
| 参数 | 解释 | 可选值举例 |
| --class | Spark 程序中包含主函数的类 | |
| --master | Spark 程序运行的模式(环境) | 模式:local[*]、spark://hd01:7077、 Yarn |
| --executor-memory 1G | 指定每个executor可用内存为1G | |
| --total-executor-cores 2 | 指定所有executor使用的cpu核数为2个 | |
| --executor-cores | 指定每个executor使用的cpu核数 | |
| application-jar | 打包好的应用jar,包含依赖。这 个 URL 在集群中全局可见。 比 如hdfs:// 共享存储系统,如果是file:// path,那么所有的节点的 path 都包含同样的jar | |
| application-arguments | 传给main()方法的参数 | |

(7)配置历史服务

spark-shell 停止掉后,集群监控hd01:4040页面就看不到历史任务的运行情况,所以要配置历史服务器记录任务运行情况。

  1. 复制spark-defaults.conf.template 文件名为 spark-defaults.conf

cp spark-defaults.conf.template spark-defaults.conf

vim spark-defaults.conf

  1. 修改spark-default.conf 文件,配置日志存储路径

spark.eventLog.enabled true

spark.eventLog.dir hdfs://hd01:9000/directory

3)启动Hadoop

启动hadoop集群,HDFS上创建directory目录。

sbin/start-dfs.sh

hadoop fs -mkdir /directory

  1. 修改spark-env.sh 文件, 添加日志配置

export SPARK_HISTORY_OPTS="

-Dspark.history.ui.port=18080

-Dspark.history.fs.logDirectory=hdfs://hd01:9000/directory

-Dspark.history.retainedApplications=30"

说明:

  • 参数1:WEB UI访问的端口号为18080
  • 参数2:指定历史服务器日志存储路径
  • 参数3:指定保存Application历史记录的个数,如果超过这个值,旧的应用程序信息将被删除,这个是内存中的应用数,而不是页面上显示的应用数。

5)分发配置文件

xsync conf

6)启动集群和历史服务

sbin/start-all.sh

sbin/start-history-server.sh

7)重新执行任务

bin/spark-submit \

--class org.apache.spark.examples.SparkPi \

--master spark://hd01:7077 \

./examples/jars/spark-examples_2.12-3.0.0.jar \

10

8)查看历史服务

访问http://hd01:18080

(8)配置高可用(HA)

所谓的高可用是因为当前集群中的Master节点只有一个,会存在单点故障问题。为了解决单点故障问题,需要在集群中配置多个Master节点,一旦处于活动状态的Master 发生故障时,由备用Master提供服务,保证作业可以继续执行。

1)集群规划

|-------|-------------------------|-------------------------|------------------|
| | hd01 | hd02 | hd03 |
| spark | Zookeeper Master Worker | Zookeeper Master Worker | Zookeeper Worker |

2)启动zookeeper集群

bin/zkServer.sh start

  1. 修改spark-env.sh 文件添加如下配置

注释如下内容:

SPARK_MASTER_HOST=hd01

SPARK_MASTER_PORT=7077

添加如下内容:

#Master 监控页面默认访问端口为 8080,但是可能会和Zookeeper 冲突,所以改成 8989

SPARK_MASTER_WEBUI_PORT=8989

export SPARK_DAEMON_JAVA_OPTS="

-Dspark.deploy.recoveryMode=ZOOKEEPER

-Dspark.deploy.zookeeper.url=hd01,hd02,hd03

-Dspark.deploy.zookeeper.dir=/spark"

  1. 分发配置文件

xsync conf/

  1. 启动集群

sbin/start-all.sh

访问http://hd01:8989

6)单独启动hd02的Master节点

此时hd02节点Master状态处于备用状态

sbin/start-master.sh

访问http://hd02:8989

7)提交应用到高可用集群

bin/spark-submit \

--class org.apache.spark.examples.SparkPi \

--master spark://hd01:7077,hd02:7077 \

./examples/jars/spark-examples_2.12-3.0.0.jar \

10

8)停止hd01的Master资源监控进程

查看hd02的Master 资源监控

在hd01停止

sbin/stop-master.sh

3、Yarn模式

独立部署(Standalone)模式由Spark自身提供计算资源,无需其他框架提供资源。这种方式降低了和其他第三方资源框架的耦合性,独立性非常强。但是Spark主要是计算框架,而不是资源调度框架,所以本身提供的资源调度并不是它的强项,所以还是和其他专业的资源调度框架集成会更靠谱一些。所以Spark和强大的Yarn环境进行结合。

(1)解压缩文件

将spark-3.0.0-bin-hadoop3.2.tgz 文件上传到 linux 并解压缩,放置在指定位置。

tar -zxvf spark-3.0.0-bin-hadoop3.2.tgz -C /opt/module/

cd /opt/module

mv spark-3.0.0-bin-hadoop3.2 spark-yarn

(2)修改配置文件

  1. 修改hadoop配置文件

hadoop/etc/hadoop/yarn-site.xml, 并分发

XML 复制代码
<!--是否启动一个线程检查每个任务正使用的物理内存量,如果任务超出分配值,则直接将其杀掉,默认是true -->
<property> 
    <name>yarn.nodemanager.pmem-check-enabled</name> 
    <value>false</value> 
</property> 
<!--是否启动一个线程检查每个任务正使用的虚拟内存量,如果任务超出分配值,则直接将其杀掉,默认是true --> 
<property> 
    <name>yarn.nodemanager.vmem-check-enabled</name> 
    <value>false</value> 
</property> 

3)复制spark-env.sh.template 文件名为 spark-env.sh

cp spark-env.sh.template spark-env.sh

vim spark-env.sh

4)修改spark-env.sh 文件

添加JAVA_HOME环境变量和集群对应的master节点

export JAVA_HOME=/opt/module/jdk1.8

YARN_CONF_DIR=/opt/module/hadoop-2.7.7/etc/hadoop/

(3)启动Hadoop

启动HDFS 以及YARN集群

start-dfs.sh

start-yarn.sh

(4)提交应用

bin/spark-submit \

--class org.apache.spark.examples.SparkPi \

--master yarn \

--deploy-mode cluster \

./examples/jars/spark-examples_2.12-3.0.0.jar \

10

查看http://hd02:8088 页面,点击 History,查看历史页面

(5)配置历史服务器

1)复制spark-defaults.conf.template文件名为spark-defaults.conf

cp spark-defaults.conf.template spark-defaults.conf

2)修改spark-default.conf文件,配置日志存储路径

spark.eventLog.enabled true

spark.eventLog.dir hdfs://hd01:9000/directory

3)启动Hadoop

启动hadoop集群,HDFS上创建directory目录。

sbin/start-dfs.sh

hadoop fs -mkdir /directory

  1. 修改spark-env.sh 文件, 添加日志配置

export SPARK_HISTORY_OPTS="

-Dspark.history.ui.port=18080

-Dspark.history.fs.logDirectory=hdfs://hd01:9000/directory

-Dspark.history.retainedApplications=30"

说明:

  • 参数1:WEB UI访问的端口号为18080
  • 参数2:指定历史服务器日志存储路径
  • 参数3:指定保存Application历史记录的个数,如果超过这个值,旧的应用程序信息将被删除,这个是内存中的应用数,而不是页面上显示的应用数。
  1. 修改spark-defaults.conf

spark.yarn.historyServer.address=hd01:18080

spark.history.ui.port=18080

  1. 启动历史服务

sbin/start-history-server.sh

  1. 重新提交应用

bin/spark-submit \

--class org.apache.spark.examples.SparkPi \

--master yarn \

--deploy-mode cluster \

./examples/jars/spark-examples_2.12-3.0.0.jar \

10

  1. Web页面查看日志

http://hd02:8088

http://hd01:18080

4、Windows模式

(1)解压缩文件

将文件spark-3.0.0-bin-hadoop3.2.tgz 解压缩到无中文无空格的路径中

(2)启动本地环境

  1. 执行解压缩文件路径下bin目录中的spark-shell.cmd文件,启动Spark本地环境
  1. 在bin目录中创建input目录,并添加word.txt文件, 在命令行中输入脚本代码

sc.textFile("input/word.txt").flatMap(.split(" ")).map((,1)).reduceByKey(+).collect

3)命令行提交应用

在bin目录下执行

spark-submit --class org.apache.spark.examples.SparkPi --master local[2] ../examples/jars/spark-examples_2.12-3.0.0.jar 10

5、K8S & Mesos 模式

容器化部署是目前业界很流行的一项技术,基于Docker镜像运行能够让用户更加方便地对应用进行管理和运维。容器管理工具中最为流行的就是Kubernetes(k8s),而Spark 也在最近的版本中支持了k8s部署模式。https://spark.apache.org/docs/latest/running-on-kubernetes.html

6、部署模式对比

|------------|-------------|-----------------|--------|------|
| 模式 | Spark 安装机器数 | 需启动的进程 | 所属者 | 应用场景 |
| Local | 1 | 无 | Spark | 测试 |
| Standalone | 3 | Master 及 Worker | Spark | 单独部署 |
| Yarn | 1 | Yarn 及HDFS | Hadoop | 混合部署 |

7、 端口号

➢ Spark查看当前Spark-shell运行任务情况端口号:4040(计算)

➢ Spark Master 内部通信服务端口号:7077

➢ Standalone 模式下,Spark Master Web 端口号:8080(资源)

➢ Spark历史服务器端口号:18080

➢ Hadoop YARN任务运行情况查看端口号:8088

四、Spark运行架构

1、运行架构

Spark 框架的核心是一个计算引擎,整体来说,它采用了标准 master-slave 的结构。 如下图所示,它展示了一个 Spark执行时的基本结构。图形中的Driver表示master, 负责管理整个集群中的作业任务调度。图形中的Executor 则是 slave,负责实际执行任务

2、核心组件

(1)Driver

Spark 驱动器节点,用于执行Spark任务中的main方法,负责实际代码的执行工作。

Driver 在 Spark 作业执行时主要负责:

➢ 将用户程序转化为作业(job)

➢ 在Executor之间调度任务(task)

➢ 跟踪Executor的执行情况

➢ 通过UI展示查询运行情况

实际上是无法准确地描述Driver的定义,因为在整个的编程过程中没有看到任何有关 Driver 的字眼。所以简单理解,所谓的Driver就是驱使整个应用运行起来的程序,也称之为 Driver 类。

(2)Executor

Spark Executor 是集群中工作节点(Worker)中的一个JVM进程,负责在 Spark 作业中运行具体任务(Task),任务彼此之间相互独立。Spark 应用启动时,Executor节点被同时启动,并且始终伴随着整个 Spark 应用的生命周期而存在。如果有Executor节点发生了故障或崩溃,Spark 应用也可以继续执行,会将出错节点上的任务调度到其他Executor节点 上继续运行。

Executor 有两个核心功能:

➢ 负责运行组成Spark应用的任务,并将结果返回给驱动器进程

➢ 它们通过自身的块管理器(Block Manager)为用户程序中要求缓存的 RDD 提供内存式存储。RDD 是直接缓存在Executor进程内的,因此任务可以在运行时充分利用缓存 数据加速运算。

(3)Master & Worker

Spark 集群的独立部署环境中,不需要依赖其他的资源调度框架,自身就实现了资源调度的功能,所以环境中还有其他两个核心组件:Master和Worker,这里的Master是一个进程,主要负责资源的调度和分配,并进行集群的监控等职责,类似于Yarn环境中的RM, 而 Worker 呢,也是进程,一个Worker运行在集群中的一台服务器上,由Master分配资源对数据进行并行的处理和计算,类似于Yarn环境中NM。

(4)ApplicationMaster

Hadoop 用户向YARN集群提交应用程序时,提交程序中应该包含ApplicationMaster,用于向资源调度器申请执行任务的资源容器Container,运行用户自己的程序任务job,监控整个任务的执行,跟踪整个任务的状态,处理任务失败等异常情况。

说的简单点就是,ResourceManager(资源)和Driver(计算)之间的解耦合靠的就是 ApplicationMaster。

3、核心概念

(1)Executor 与 Core

Spark Executor 是集群中运行在工作节点(Worker)中的一个JVM进程,是整个集群中的专门用于计算的节点。在提交应用中,可以提供参数指定计算节点的个数,以及对应的资源。这里的资源一般指的是工作节点Executor的内存大小和使用的虚拟CPU核(Core)数量。

|-------------------|-----------------------------|
| 名称 | 说明 |
| --num-executors | 配置Executor 的数量 |
| --executor-memory | 配置每个Executor 的内存大小 |
| --executor-cores | 配置每个Executor 的虚拟CPU core 数量 |

(2)并行度(Parallelism)

在分布式计算框架中多个任务同时执行,由于任务分布在不同的计算节点进行计算,所以能够真正地实现多任务并行执行,这里是并行,而不是并发。将整个集群并行执行任务的数量称之为并行度。

(3)有向无环图(DAG)

所谓的有向无环图,并不是真正意义的图形,而是由Spark程序直接映射成的数据流的高级抽象模型。简单理解就是将整个程序计算的执行过程用图形表示出来,这样更直观,更便于理解,可以用于表示程序的拓扑结构。

DAG(Directed Acyclic Graph)有向无环图是由点和线组成的拓扑图形,该图形具有方向,不会闭环。

(4)提交流程

所谓的提交流程,其实就是写的应用程序通过Spark客户端提交给Spark 运行环境执行计算的流程。

Spark 应用程序提交到Yarn环境中执行的时候,一般会有两种部署执行的方式:Client 和Cluster。两种模式主要区别在于:Driver程序的运行节点位置。

1)Yarn Client 模式

Client 模式将用于监控和调度的Driver模块在客户端执行,而不是在Yarn中,所以一 般用于测试。

➢ Driver在任务提交的本地机器上运行

➢ Driver启动后会和ResourceManager通讯申请启动ApplicationMaster

➢ ResourceManager 分配 container,在合适的NodeManager 上启动ApplicationMaster,负责向ResourceManager 申请 Executor 内存

➢ ResourceManager 接到 ApplicationMaster 的资源申请后会分配container,然后 ApplicationMaster 在资源分配指定的NodeManager上启动Executor 进程

➢ Executor进程启动后会向Driver反向注册,Executor全部注册完成后Driver开始执行 main 函数

➢ 之后执行到Action算子时,触发一个Job,并根据宽依赖开始划分stage,每个stage生成对应的TaskSet,之后将task分发到各个Executor上执行。

2)Yarn Cluster 模式

Cluster 模式将用于监控和调度的Driver模块启动在Yarn集群资源中执行。一般应用于实际生产环境。

➢ 在YARN Cluster模式下,任务提交后会和ResourceManager通讯申请启动 ApplicationMaster

➢ 随后ResourceManager分配container,在合适的NodeManager上启动ApplicationMaster, 此时的ApplicationMaster 就是 Driver。

➢ Driver启动后向ResourceManager申请Executor内存,ResourceManager 接到 ApplicationMaster 的资源申请后会分配container,然后在合适的NodeManager上启动 Executor 进程

➢ Executor进程启动后会向Driver反向注册,Executor全部注册完成后Driver开始执行 main 函数,

➢ 之后执行到Action算子时,触发一个Job,并根据宽依赖开始划分stage,每个stage生 成对应的TaskSet,之后将task分发到各个Executor上执行。

五、Spark核心编程

Spark 计算框架为了能够进行高并发和高吞吐的数据处理,封装了三大数据结构,用于处理不同的应用场景。

三大数据结构分别是:

➢ RDD : 弹性分布式数据集

➢ 累加器:分布式共享只写变量

➢ 广播变量:分布式共享只读变量

1、RDD

(1)介绍

RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是 Spark 中最基本的数据处理模型。代码中是一个抽象类,它代表一个弹性的、不可变、可分区、里面的元素可并行计算的集合。

➢ 弹性

存储的弹性:内存与磁盘的自动切换;

容错的弹性:数据丢失可以自动恢复;

计算的弹性:计算出错重试机制;

分片的弹性:可根据需要重新分片。

➢ 分布式:数据存储在大数据集群不同节点上

➢ 数据集:RDD封装了计算逻辑,并不保存数据

➢ 数据抽象:RDD是一个抽象类,需要子类具体实现

➢ 不可变:RDD封装了计算逻辑,是不可以改变的,想要改变,只能产生新的RDD,在新的RDD里面封装计算逻辑

➢ 可分区、并行计算

(2)核心属性

A list of partitions 分区列表

A function for computing each split 分区计算函数

A list of dependencies on other RDDs RDD依赖关系

Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned) 分区器

Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file 首选位置

1)分区列表

RDD数据结构中存在分区列表,用于执行任务时并行计算,是实现分布式计算的重要属性。

该方法获取分区数

2)分区计算函数

Spark 在计算时,是使用分区函数对每一个分区进行计算

3)RDD之间的依赖关系

RDD是计算模型的封装,当需求中需要将多个计算模型进行组合时,就需要将多个RDD建立依赖关系

4)分区器(可选)

当数据为KV类型数据时,可以通过设定分区器自定义数据的分区

5)首选位置(可选)

计算数据时,可以根据计算节点的状态选择不同的节点位置进行计算

(3)执行原理

从计算的角度来讲,数据处理过程中需要计算资源(内存 & CPU)和计算模型(逻辑)。 执行时,需要将计算资源和计算模型进行协调和整合。 Spark 框架在执行时,先申请资源,然后将应用程序的数据处理逻辑分解成一个一个的计算任务。然后将任务发到已经分配资源的计算节点上, 按照指定的计算模型进行数据计算。最后得到计算结果。

RDD是Spark框架中用于数据处理的核心模型,在Yarn环境中,RDD 的工作原理:

1) 启动Yarn集群环境

2) Spark 通过申请资源创建调度节点和计算节点

3) Spark 框架根据需求将计算逻辑根据分区划分成不同的任务

4) 调度节点将任务根据计算节点状态发送到对应的计算节点进行计算

从以上流程可以看出RDD在整个流程中主要用于将逻辑进行封装,并生成Task发送给 Executor 节点执行计算。

(4)基础编程

1)RDD 创建

a、从集合(内存)中创建RDD

Spark主要提供了两个方法:parallelize和makeRDD

Scala 复制代码
object RDD_Memory {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    // 创建RDD
    var valueList = List(1,2,3,4)

    // 方式一:parallelize
    val rdd: RDD[Int] = sc.parallelize(valueList)

    // 方式二: makeRDD
    // makeRDD方法在底层实现时其实就是调用了rdd对象的parallelize方法。
    val rdd2: RDD[Int] = sc.makeRDD(valueList)

    rdd.collect().foreach(println)

    // 关闭环境
    sc.stop()
  }
}

b、从外部存储(文件)创建RDD

由外部存储系统的数据集创建RDD包括:本地的文件系统,所有Hadoop支持的数据集, 比如HDFS、HBase等。

Scala 复制代码
object RDD_File {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    // 方式一:本地绝对路径
    val rdd_1: RDD[String] = sc.textFile("F:\\study-demo\\sparkDemo\\datas\\test01.txt")

    // 方式二: 本地相对路径
    val rdd_2: RDD[String] = sc.textFile("datas/test02.txt")

    // 方式三: 文件夹
    val rdd_3: RDD[String] = sc.textFile("datas")
    
    // 方式四:通配符 *
    val rdd_4: RDD[String] = sc.textFile("datas/test*.txt")
    
    // 方式五: hdfs
    val rdd_5: RDD[String] = sc.textFile("hdfs://hd01:9000/test.txt")

    rdd_4.collect().foreach(println)

    // 关闭环境
    sc.stop()
  }
}
2)RDD 并行度与分区

默认情况下,Spark可以将一个作业切分多个任务后,发送给Executor节点并行计算,而能够并行计算的任务数量称之为并行度。这个数量可以在构建RDD时指定。记住,这里的并行执行的任务数量,并不是指的切分任务的数量。

a、读取内存数据

读取内存数据时,数据可以按照并行度的设定进行数据的分区操作。

Scala 复制代码
object RDD_Memory {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    // 创建RDD
    var valueList = List(1,2,3,4)

    /**
     * makeRDD方法可以传递第二个参数,这个参数表示分区的数量
     * 第二个参数可以不传递的,那么makeRDD方法会使用默认值 : defaultParallelism(默认并行度)
     * 这个属性取值为当前运行环境的最大可用核数
     */
    val rdd: RDD[Int] = sc.makeRDD(valueList, 3)

    rdd.saveAsTextFile("output")

    // 关闭环境
    sc.stop()
  }
}

数据分区规则的 Spark 核心源码:

numSlices:表示分区的数量,不传递会使用默认值 : defaultParallelism(默认并行度)

spark在默认情况下,从配置对象中获取配置参数:spark.default.parallelism

scheduler.conf.getInt("spark.default.parallelism", totalCores),如果获取不到,那么使用totalCores属性,这个属性取值为当前运行环境的最大可用核数
var valueList = List(1,2,3,4)

sc.makeRdd(valueList, 3)

对分区数3进行计算:numSlices:3 length: 4

0 ==> start: 0/3 = 0 end: 4/3 = 1 文件0数据:[1,1) 1

1 ==> start: 4/3 = 1 end: 8/3 = 2 文件1数据:[1, 2) 2

2 ==> start: 8/3 = 2 end: 12/3 = 4 文件2数据:[2,4) 3,4

b、读取文件数据

读取文件数据时,数据是按照Hadoop文件读取的规则进行切片分区,而切片规则和数据读取的规则有些差异。

Scala 复制代码
object RDD_File {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val rdd_2: RDD[String] = sc.textFile("datas/test02.txt", 3)

    rdd_2.saveAsTextFile("output")

    // 关闭环境
    sc.stop()
  }
}

分区数量是3,但是文件数量却是4

具体Spark核心源码如下

val rdd_2: RDD[String] = sc.textFile("datas/test02.txt", 3)

textFile可以将文件作为数据处理的数据源,默认也可以设定分区。如果不想使用默认的分区数量,可以通过第二个参数指定分区数

minPartitions : 最小分区数量

math.min(defaultParallelism, 2)

Spark读取文件,底层其实使用的就是Hadoop的读取方式

得到test02.txt有22个字节,分成了3个分区

22/3 = 7 也就是每个分区7个字节

22/7 = 3....1 最终得到4个文件

下面分析每个文件里的内容

数据以行为单位进行读取,spark读取文件,采用的是hadoop的方式读取,所以一行一行读取,和字节数没有关系;数据读取时以偏移量为单位,偏移量不会被重复读取

这里是test02.txt的文件内容

hello java => 12字节(换行2个字节)

scala word => 10字节

数据分区的偏移量范围的计算:上面按计算出每个文件7个字节

0文件 ==> [0, 7) 理论上应该读取到 【hello j】 但是spark读取文件是整行读取 【hello, java】

1文件 ==> [7,14) 理论上应该读取到 【ava sc】 但是Java已经被读取,所以不可以二次读取,且读取文件是整行读取,最终为【scala word】

2文件 ==>[14, 21) 理论上应该读取到 【ala wo】 但是scala word已经被读取,所以不可以二次读取.最终为【】

3文件 ==>[21,22] 理论上应该读取到 【rd】 但是scala word已经被读取,所以不可以二次读取.最终为【】

3)其他

a、读取文件时,输出携带文件名

Scala 复制代码
object RDD_File2 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    // wholeTextFiles : 以文件为单位读取数据,读取的结果表示为元组,第一个元素表示文件路径,第二个元素表示文件内容
    val rdd: RDD[(String, String)] = sc.wholeTextFiles("datas")

    rdd.collect().foreach(println)

    // 关闭环境
    sc.stop()
  }
}

a、读取多文件

Scala 复制代码
object RDD_File3 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val rdd: RDD[String] = sc.textFile("datas", 3)

    rdd.saveAsTextFile("output")

    // 关闭环境
    sc.stop()
  }
}

理论分析

Scala 复制代码
文件1内容:
hello word
hello spark
hello scala
一共36字节

文件2内容:
hello java
scala word
一共22字节

val rdd: RDD[String] = sc.textFile("datas", 3)
计算分区
(36+22) / 3 = 19...1
58/19 = 3...1
所以计算分区为4

文件0:[0,19)      理论内容为【hello word  hello s】  最终为【hello word  hello spark】
文件1:[19, 38)    理论内容为【park  hello scala】  最终为【hello scala】 文件test01.txt读取结束
文件2:[38,57)     理论内容为【hello java  scala w】  最终为【hello java  scala word】 文件test02.txt读取结束
文件3:[57,58]     理论内容为【ord】  最终为【】 因为文件读取结束

(5)RDD转换算子

RDD根据数据处理方式的不同将算子整体上分为Value类型、双Value类型和Key-Value 类型

1)Value类型
a、map

1.基本使用

def map[U: ClassTag](f: T => U): RDD[U]

将处理的数据逐条进行映射转换。

Scala 复制代码
object Test01 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val values: List[Int] = List(1,2,3,4)

    // 算子 - map
    val rdd: RDD[Int] = sc.makeRDD(values)
    val newRdd: RDD[Int] = rdd.map(_ * 2)

    newRdd.collect().foreach(println)

    // 关闭环境
    sc.stop()
  }
}

2.执行顺序

rdd计算一个分区内的数据是一个一个执行,只有前面一个数据全部的逻辑执行完毕后,才会执行下一个数据。

  • 分区内数据的执行是有序的。
  • 不同分区数据计算是无序的。
Scala 复制代码
object Test01 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val values: List[Int] = List(1,2,3,4)

    // 算子 - map
    val rdd: RDD[Int] = sc.makeRDD(values,2)
    val newRdd: RDD[Int] = rdd.map(item => {
      println("rdd 1=====" + item)
      item
    })

    val rdd2: RDD[Int] = newRdd.map(item => {
      println("rdd 2===== " + item)
      item
    })

    rdd2.collect()

    // 关闭环境
    sc.stop()
  }
}

上述代码分了2个区

rdd1===3执行后执行 rdd2===3

rdd1===4执行后执行 rdd2===4

rdd1===1执行后执行 rdd2===1

rdd1===2执行后执行 rdd2===2

分区内执行是有序的,不同分区执行是无序的

3.案例

从服务器日志数据apache.log中获取用户请求URL资源路径

日志格式

代码实现

Scala 复制代码
object Test01Demo {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val line: RDD[String] = sc.textFile("datas/apache.log")

    val mapRdd: RDD[String] = line.map(item => {
      val strings: Array[String] = item.split(" ")
      strings(6)
    })


    mapRdd.collect().foreach(println)

    // 关闭环境
    sc.stop()
  }
}
b、mapPartitions

1.基本使用

def mapPartitions[U: ClassTag](

f: Iterator[T] => Iterator[U],

preservesPartitioning: Boolean = false

): RDD[U]

将待处理的数据以分区为单位发送到计算节点进行处理。

会将整个分区的数据加载到内存进行引用,在内存较小数据量较大的场合下,容易出现内存溢出。

Scala 复制代码
object Test02 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val values: List[Int] = List(1,2,3,4)
    val rdd: RDD[Int] = sc.makeRDD(values,2)

    // 算子 - mapPartitions
    val newRdd: RDD[Int] = rdd.mapPartitions(item => {
      println("rdd =====" + item)
      item
    })
    
    newRdd.collect().foreach(println)

    // 关闭环境
    sc.stop()
  }
}

2.案例

获取每个数据分区的最大值

List(1,2,3,4) 分为2个区

【1,2】【3,4】

每个区的最大值为【2】【4】

Scala 复制代码
object Test02_test {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val values: List[Int] = List(1,2,3,4)
    val rdd: RDD[Int] = sc.makeRDD(values,2)

    // 算子 - mapPartitions
    val newRdd: RDD[Int] = rdd.mapPartitions(item => {
      List(item.max).iterator
    })

    newRdd.collect().foreach(println)

    // 关闭环境
    sc.stop()
  }
}
c、map和mapPartitions的区别

➢ 数据处理角度: Map 算子是分区内一个数据一个数据的执行,类似于串行操作。而mapPartitions算子是以分区为单位进行批处理操作。

➢ 功能的角度: Map 算子主要目的将数据源中的数据进行转换和改变。但是不会减少或增多数据。 MapPartitions 算子需要传递一个迭代器,返回一个迭代器,没有要求元素的个数保持不变, 所以可以增加或减少数据

➢ 性能的角度: Map 算子因为类似于串行操作,所以性能比较低,而是mapPartitions算子类似于批处理,所以性能较高。但是mapPartitions算子会长时间占用内存,那么这样会导致出现内存溢出的错误。所以在内存有限的情况下不推荐使用。使用map操作。

d、mapPartitionsWithIndex

1.基本使用

def mapPartitionsWithIndex[U: ClassTag]( f: (

Int,

Iterator[T]) => Iterator[U],

preservesPartitioning: Boolean = false

): RDD[U]

将待处理的数据以分区为单位发送到计算节点进行处理,在处理时同时可以获取当前分区索引。

例如:只要第二个分区的数据

Scala 复制代码
object Test03 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val values: List[Int] = List(1,2,3,4)
    val rdd: RDD[Int] = sc.makeRDD(values,2)  // [1,2] [3,4]

    // 算子 - mapPartitionsWithIndex
    // 只要第二个分区的数据
    val newRdd: RDD[Int] = rdd.mapPartitionsWithIndex((index, item) => {
      if (index == 1) {
        item
      } else {
        Nil.iterator
      }
    })

    newRdd.collect().foreach(println)

    // 关闭环境
    sc.stop()
  }
}

2.案例

将数据和分区以元组的形式返回

Scala 复制代码
object Test03_test {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val values: List[Int] = List(1,2,3,4)
    val rdd: RDD[Int] = sc.makeRDD(values,2)  // [1,2] [3,4]

    // 算子 - mapPartitionsWithIndex
    val newRdd: RDD[(Int, Int)] = rdd.mapPartitionsWithIndex((index, item) => {
      item.map((index, _))
    })

    newRdd.collect().foreach(println)

    // 关闭环境
    sc.stop()
  }
}
e、flatMap

1.基本使用

def flatMap[U: ClassTag](f: T => TraversableOnce[U]): RDD[U]

将处理的数据进行扁平化后再进行映射处理,所以算子也称之为扁平映射

例如:将List(List(1,2),List(3,4))进行扁平化操作

Scala 复制代码
object Test04 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val values: List[List[Int]] = List(List(1,2),List(3,4))
    val rdd: RDD[List[Int]] = sc.makeRDD(values)

    // 算子 - flatMap
    val newRdd: RDD[Int] = rdd.flatMap(item => item)

    newRdd.collect().foreach(println)

    // 关闭环境
    sc.stop()
  }
}

2.案例

将字符串进行扁平化操作

Scala 复制代码
object Test04_test01 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val rdd: RDD[String] = sc.makeRDD(List("hello word", "hello scala", "hello spark"))

    // 算子 - flatMap
    val newRdd: RDD[String] = rdd.flatMap(item => item.split(" "))

    newRdd.collect().foreach(println)

    // 关闭环境
    sc.stop()
  }

将List(List(1,2),3,List(4,5))进行扁平化操作

Scala 复制代码
object Test04_test02 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val value: List[Any] = List(List(1, 2), 3, List(4, 5))
    val rdd: RDD[Any] = sc.makeRDD(value)

    // 算子 - flatMap
    val newRdd: RDD[Any] = rdd.flatMap(item => {
      item match {
        case list: List[_] => list
        case v => List(v)
      }
    })

    newRdd.collect().foreach(println)

    // 关闭环境
    sc.stop()
  }
}
f、glom

1.基本使用

def glom(): RDD[Array[T]]

将同一个分区的数据直接转换为相同类型的内存数组进行处理,分区不变

类似于将1,2,3,4转换为(1,2)(3,4)分区不变

Scala 复制代码
object Test05 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val values: List[Int] = List(1,2,3,4)
    val rdd: RDD[Int] = sc.makeRDD(values, 2)

    // 算子 - glom
    val newRdd: RDD[Array[Int]] = rdd.glom()

    newRdd.collect().foreach(item => println(item.mkString(",")))

    // 关闭环境
    sc.stop()
  }
}

2.案例

计算所有分区最大值求和(分区内取最大值,分区间最大值求和)

Scala 复制代码
object Test05_test {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val values: List[Int] = List(1,2,3,4)
    val rdd: RDD[Int] = sc.makeRDD(values, 2)

    // 算子 - glom
    /*
      [1,2] [3,4]
      最大值:[2] [4]
      求和: 6
     */
    val glomRdd: RDD[Array[Int]] = rdd.glom()
    // 最大值
    val maxRdd: RDD[Int] = glomRdd.map(_.max)
    // 求和
    val sum: Int = maxRdd.collect().sum

    println(sum)
    // 关闭环境
    sc.stop()
  }
}
g、groupBy

1.基本使用

def groupBy[K](f: T => K)(implicit kt: ClassTag[K]): RDD[(K, Iterable[T])]

将数据根据指定的规则进行分组, 分区默认不变,但是数据会被打乱重新组合,将这样的操作称之为shuffle。极限情况下,数据可能被分在同一个分区中

一个组的数据在一个分区中,但是并不是说一个分区中只有一个组

Scala 复制代码
object Test06 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val values: List[Int] = List(1,2,3,4)
    val rdd: RDD[Int] = sc.makeRDD(values, 2)

    // 算子 - groupBy
    val newRdd: RDD[(Boolean, Iterable[Int])] = rdd.groupBy(_ % 2 == 0)

    newRdd.collect().foreach(println)

    // 关闭环境
    sc.stop()
  }
}

2.案例

将List("Hello", "hive", "hbase", "Hadoop")根据单词首写字母进行分组

Scala 复制代码
object Test06_test01 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val values: List[String] = List("Hello", "hive", "hbase", "Hadoop")
    val rdd: RDD[String] = sc.makeRDD(values, 2)

    // 算子 - groupBy
    val newRdd: RDD[(Char, Iterable[String])] = rdd.groupBy(_.charAt(0))

    newRdd.collect().foreach(println)

    // 关闭环境
    sc.stop()
  }
}

从服务器日志数据apache.log中获取每个时间段访问量

Scala 复制代码
object Test06_test01 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val line: RDD[String] = sc.textFile("datas/apache.log")

    val timeRdd: RDD[(String, Int)] = line.map(item => {
      val itemValue: Array[String] = item.split(" ")
      val key: String = itemValue(3).substring(0, 13)
      (key, 1)
    })

    val groupRdd: RDD[(String, Iterable[(String, Int)])] = timeRdd.groupBy(_._1)
    
    val newRdd: RDD[(String, Int)] = groupRdd.map { case (h, item) => (h, item.size) }

    newRdd.collect().foreach(println)
    // 关闭环境
    sc.stop()
  }
}
h、filter

1.基本使用

def filter(f: T => Boolean): RDD[T]

将数据根据指定的规则进行筛选过滤,符合规则的数据保留,不符合规则的数据丢弃。 当数据进行筛选过滤后,分区不变,但是分区内的数据可能不均衡,生产环境下,可能会出 现数据倾斜。

Scala 复制代码
object Test07 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val values: List[Int] = List(1,2,3,4)
    val rdd: RDD[Int] = sc.makeRDD(values, 2)

    // 算子 - filter
    val newRdd: RDD[Int] = rdd.filter(_ % 2 == 0)

    newRdd.collect().foreach(println)

    // 关闭环境
    sc.stop()
  }
}

2.案例

从服务器日志数据apache.log中获取2015年5月17日的请求路径

Scala 复制代码
object Test07_test {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val line: RDD[String] = sc.textFile("datas/apache.log")

    val newRdd: RDD[String] = line.filter(item => {
      val l: Array[String] = item.split(" ")
      l(3).startsWith("17/05/2015")
    })

    newRdd.collect().foreach(println)
    // 关闭环境
    sc.stop()
  }
}
i、sample

1.基本使用

def sample(

withReplacement: Boolean,

fraction: Double,

seed: Long = Utils.random.nextLong

): RDD[T]

根据指定的规则从数据集中抽取数据
参数说明:

参数1:表示抽取数据后是否将数据放回: true(放回),false(丢弃)

参数2:如果抽取不放回,表示抽取的几率,范围在[0,1]之间, 0:全不取;1:全取;

如果抽取放回,表示重复数据的几率,范围大于等于0.表示每一个元素被期望抽取到的次数

参数3:随机数种子,如果不传递,那么使用的是当前系统时间

Scala 复制代码
object Test08 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val values: List[Int] = List(1,2,3,4,5,6,7,8,9)
    val rdd: RDD[Int] = sc.makeRDD(values)

    // 算子 - sample
    // 如果传递了随机种子,那么每次输出都是一样的
    val newRdd: RDD[Int] = rdd.sample(false, 0.4, 1)

    newRdd.collect().foreach(println)

    // 关闭环境
    sc.stop()
  }
}

// 如果传递了随机种子,那么每次输出都是一样的

val newRdd: RDD[Int] = rdd.sample(false, 0.4, 1)

// 如果不传递了随机种子,那么每次输出都是不一样的

val newRdd: RDD[Int] = rdd.sample(false, 0.4)

val newRdd: RDD[Int] = rdd.sample(true, 3)

j、distinct

1.基本使用

def distinct()(implicit ord: Ordering[T] = null): RDD[T]

def distinct(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T]

将数据集中重复的数据去重

Scala 复制代码
object Test09 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val values: List[Int] = List(1,2,3,4,4,6,3,8,1)
    val rdd: RDD[Int] = sc.makeRDD(values)

    // 算子 - distinct
    val newRdd: RDD[Int] = rdd.distinct()

    newRdd.collect().foreach(println)

    // 关闭环境
    sc.stop()
  }
}
k、coalesce

def coalesce(

numPartitions: Int,

shuffle: Boolean = false,

partitionCoalescer: Option[PartitionCoalescer] = Option.empty

) (implicit ord: Ordering[T] = null) : RDD[T]

根据数据量缩减分区,用于大数据集过滤后,提高小数据集的执行效率

当spark 程序中,存在过多的小任务的时候,可以通过coalesce方法,收缩合并分区,减少 分区的个数,减小任务调度成本

Scala 复制代码
object Test10 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val values: List[Int] = List(1,2,3,4,4,6,3,8,1)
    val rdd: RDD[Int] = sc.makeRDD(values, 3)

    // 算子 - coalesce
    /**
     * coalesce方法默认情况下不会将分区的数据打乱重新组合
     * 这种情况下的缩减分区可能会导致数据不均衡,出现数据倾斜
     * 如果想要让数据均衡,可以进行shuffle处理, 将第二个参数设置为true
     */
//    val newRdd: RDD[Int] = rdd.coalesce(2)
    val newRdd: RDD[Int] = rdd.coalesce(2, true)

    newRdd.saveAsTextFile("output")

    // 关闭环境
    sc.stop()
  }
}

val newRdd: RDD[Int] = rdd.coalesce(2) 查看输出

val newRdd: RDD[Int] = rdd.coalesce(2, true) 查看输出

l、repartition

def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T]

该操作内部其实执行的是coalesce操作,参数shuffle的默认值为true。

无论是将分区数多的 RDD转换为分区数少的RDD,还是将分区数少的RDD转换为分区数多的RDD,repartition 操作都可以完成,因为无论如何都会经shuffle过程。

Scala 复制代码
object Test11 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val values: List[Int] = List(1,2,3,4,4,6,3,8,1)
    val rdd: RDD[Int] = sc.makeRDD(values, 3)

    // 算子 - repartition
    /**
     * coalesce算子可以扩大分区的,但是如果不进行shuffle操作,是没有意义,不起作用。
     * 所以如果想要实现扩大分区的效果,需要使用shuffle操作
     * spark提供了一个简化的操作:
     *    缩减分区:coalesce,如果想要数据均衡,可以采用shuffle
     *    扩大分区:repartition, 底层代码调用的就是coalesce,而且肯定采用shuffle
     */
//    val newRdd: RDD[Int] = rdd.coalesce(3, true)
    val newRdd: RDD[Int] = rdd.repartition(3)

    newRdd.saveAsTextFile("output")

    // 关闭环境
    sc.stop()
  }
}
m、sortBy

1.基本使用

def sortBy[K](

f: (T) => K,ascending: Boolean = true,

numPartitions: Int = this.partitions.length

) (implicit ord: Ordering[K], ctag: ClassTag[K]): RDD[T]

该操作用于排序数据。在排序之前,可以将数据通过 f 函数进行处理,之后按照 f 函数处理的结果进行排序,默认为升序排列。排序后新产生的RDD的分区数与原RDD的分区数一 致。中间存在shuffle的过程

Scala 复制代码
object Test12 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val values: List[Int] = List(1,2,3,4,4,6,3,8,1)
    val rdd: RDD[Int] = sc.makeRDD(values, 3)

    // 算子 - sortBy
    val newRdd: RDD[Int] = rdd.sortBy(item => item)

    newRdd.collect().foreach(println)

    // 关闭环境
    sc.stop()
  }
}

2.案例

对元组集合进行排序

Scala 复制代码
object Test12_test {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val values: List[(String, Int)] = List(
      ("a", 1),
      ("b", 10),
      ("e", 2),
      ("c", 5)
    )
    val rdd: RDD[(String, Int)] = sc.makeRDD(values, 2)

    // 算子 - sortBy
    /**
     * sortBy方法可以根据指定的规则对数据源中的数据进行排序,默认为升序,第二个参数可以改变排序的方式
     * sortBy默认情况下,不会改变分区。但是中间存在shuffle操作
     */
    val newRdd: RDD[(String, Int)] = rdd.sortBy(item => item)

    newRdd.collect().foreach(println)

    // 关闭环境
    sc.stop()
  }
}

val newRdd: RDD[(String, Int)] = rdd.sortBy(item => item)

val newRdd: RDD[(String, Int)] = rdd.sortBy(_._2, false)

2)双Value类型
a、intersection

def intersection(other: RDD[T]): RDD[T]

对源RDD和参数RDD求交集后返回一个新的RDD

Scala 复制代码
object Test13 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val rdd_1: RDD[Int] = sc.makeRDD(List(1,2,3,4))
    val rdd_2: RDD[Int] = sc.makeRDD(List(3,4,5,6))

    // 算子 - intersection 交集
    val newRdd: RDD[Int] = rdd_1.intersection(rdd_2)

    newRdd.collect().foreach(println)

    // 关闭环境
    sc.stop()
  }
}
b、union

def union(other: RDD[T]): RDD[T]

对源RDD和参数RDD求并集后返回一个新的RDD

Scala 复制代码
object Test15 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val rdd_1: RDD[Int] = sc.makeRDD(List(1,2,3,4))
    val rdd_2: RDD[Int] = sc.makeRDD(List(3,4,5,6))

    // 算子 - union 并集
    val newRdd: RDD[Int] = rdd_1.union(rdd_2)

    newRdd.collect().foreach(println)

    // 关闭环境
    sc.stop()
  }
}
c、subtract

def subtract(other: RDD[T]): RDD[T]

以一个RDD元素为主,去除两个RDD中重复元素,将其他元素保留下来。求差集

Scala 复制代码
object Test14 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val rdd_1: RDD[Int] = sc.makeRDD(List(1,2,3,4))
    val rdd_2: RDD[Int] = sc.makeRDD(List(3,4,5,6))

    // 算子 - subtract 差集
    val newRdd: RDD[Int] = rdd_1.subtract(rdd_2)

    newRdd.collect().foreach(println)

    // 关闭环境
    sc.stop()
  }
}
d、zip

def zip[U: ClassTag](other: RDD[U]): RDD[(T, U)]

将两个RDD中的元素,以键值对的形式进行合并。其中,键值对中的Key为第1个RDD 中的元素,Value为第2个RDD中的相同位置的元素。

注意:

拉链操作两个数据源的类型可以不一致

两个数据源要求分区数量要保持一致

两个数据源要求分区中数据数量保持一致

Scala 复制代码
object Test16 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val rdd1 = sc.makeRDD(List(1,2,3,4),2)
    val rdd2 = sc.makeRDD(List(3,4,5,6),2)

    // 算子 - zip 拉链
    val newRdd: RDD[(Int, Int)] = rdd1.zip(rdd2)

    newRdd.collect().foreach(println)

    // 关闭环境
    sc.stop()
  }
}

注意:

交集,并集和差集要求两个数据源数据类型保持一致
拉链操作两个数据源的类型可以不一致

3)Key - Value类型
a、partitionBy

def partitionBy(partitioner: Partitioner): RDD[(K, V)]

将数据按照指定Partitioner重新进行分区。Spark默认的分区器是HashPartitioner

Scala 复制代码
object Test17 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val rdd = sc.makeRDD(Array((1,"aaa"),(2,"bbb"),(3,"ccc")),3)

    // 算子 - partitionBy
    val newRdd: RDD[(Int, String)] = rdd.partitionBy(new HashPartitioner(2))

    newRdd.saveAsTextFile("output")

    // 关闭环境
    sc.stop()
  }
}
b、reduceByKey

def reduceByKey(func: (V, V) => V): RDD[(K, V)]

def reduceByKey(func: (V, V) => V, numPartitions: Int): RDD[(K, V)]

可以将数据按照相同的Key对Value进行聚合

Scala 复制代码
object Test17 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val rdd = sc.makeRDD(List(
      ("a", 1),
      ("c", 3),
      ("a", 3),
      ("b", 2),
    ))

    // 算子 - reduceByKey
    val newRdd: RDD[(String, Int)] = rdd.reduceByKey(_ + _)

    newRdd.collect().foreach(println)

    // 关闭环境
    sc.stop()
  }
}
c、groupByKey

def groupByKey(): RDD[(K, Iterable[V])]

def groupByKey(numPartitions: Int): RDD[(K, Iterable[V])]

def groupByKey(partitioner: Partitioner): RDD[(K, Iterable[V])]

将数据源的数据根据key对value进行分组

Scala 复制代码
object Test18 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val rdd = sc.makeRDD(List(
      ("a", 1),
      ("c", 3),
      ("a", 3),
      ("b", 2),
    ))

    // 算子 - groupByKey
    val newRdd: RDD[(String, Iterable[Int])] = rdd.groupByKey()

    newRdd.collect().foreach(println)

    // 关闭环境
    sc.stop()
  }
}

reduceByKey和groupByKey的区别

1.从shuffle 的角度:reduceByKey 和groupByKey 都存在shuffle 的操作,但是reduceByKey 可以在shuffle 前对分区内相同key的数据进行预聚合(combine)功能,这样会减少落盘的 数据量,而groupByKey只是进行分组,不存在数据量减少的问题,reduceByKey性能比较高。

2.从功能的角度:reduceByKey其实包含分组和聚合的功能。GroupByKey只能分组,不能聚合,所以在分组聚合的场合下,推荐使用reduceByKey,如果仅仅是分组而不需要聚合。那么还是只能使用groupByKey

d、aggregateByKey

def aggregateByKey[U: ClassTag](zeroValue: U)(

seqOp: (U, V) => U,

combOp: (U, U) => U

): RDD[(K, U)]

将数据根据不同的规则进行分区内计算和分区间计算

参数列表1:zeroValue 初始值

参数列表2:seqOp 分区内的计算函数 combOp 分区间的计算函数

案例:

1、取出每个分区内相同key的最大值然后分区间相加

Scala 复制代码
object Test19 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val rdd = sc.makeRDD(List(
      ("a", 1), ("a", 4), ("c", 3),
      ("a", 3), ("c", 2), ("c", 5)
    ),2)

    // 算子 - aggregateByKey
    val newRdd: RDD[(String, Int)] = rdd.aggregateByKey(0)(
      math.max(_, _),
      _ + _
    )

    newRdd.collect().foreach(println)

    // 关闭环境
    sc.stop()
  }
}

2、分析如果初始值为4

Scala 复制代码
object Test19 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val rdd = sc.makeRDD(List(
      ("a", 1), ("a", 4), ("c", 3),
      ("a", 3), ("c", 2), ("c", 5)
    ),2)

    // 算子 - aggregateByKey
    val newRdd: RDD[(String, Int)] = rdd.aggregateByKey(4)(
      math.max(_, _),
      _ + _
    )

    newRdd.collect().foreach(println)

    // 关闭环境
    sc.stop()
  }
}

3、如果分区内计算和分区间计算是一样的

Scala 复制代码
object Test20 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val rdd = sc.makeRDD(List(
      ("a", 1), ("a", 4), ("c", 3),
      ("a", 3), ("c", 2), ("c", 5)
    ),2)

    // 算子 - aggregateByKey
    // 如果聚合计算时,分区内和分区间计算规则相同,spark提供了简化的方法 foldByKey
    val newRdd: RDD[(String, Int)] = rdd.aggregateByKey(0)(
      _ + _,
      _ + _
    )

    newRdd.collect().foreach(println)

    // 关闭环境
    sc.stop()
  }
}

4、如果初始值为2个参数

Scala 复制代码
object Test22 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val rdd = sc.makeRDD(List(
      ("a", 2), ("a", 4), ("c", 3),
      ("a", 3), ("c", 2), ("c", 7)
    ),2)

    // 算子 - aggregateByKey
    // 获取相同key的数据的平均值
    val aggregateRdd: RDD[(String, (Int, Int))] = rdd.aggregateByKey((0, 0))(
      (x, y) => (x._1 + y, x._2 + 1),
      (x, y) => (x._1 + y._1, x._2 + y._2)
    )
    val newRdd: RDD[(String, Int)] = aggregateRdd.mapValues { case (num, count) => num / count }
    newRdd.collect().foreach(println)

    // 关闭环境
    sc.stop()
  }
}

5、分析

val aggregateRdd: RDD[(String, (Int, Int))] = rdd.aggregateByKey((4, 2))(

(x, y) => (x._1 + y, x._2 + 1),

(x, y) => (x._1 + y._1, x._2 + y._2)

)

e、foldByKey

def foldByKey(zeroValue: V)(func: (V, V) => V): RDD[(K, V)]

当分区内计算规则和分区间计算规则相同时,aggregateByKey就可以简化为foldByKey

Scala 复制代码
object Test21 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val rdd = sc.makeRDD(List(
      ("a", 1), ("a", 4), ("c", 3),
      ("a", 3), ("c", 2), ("c", 5)
    ),2)

    // 算子 - foldByKey
    // 如果聚合计算时,分区内和分区间计算规则相同,spark提供了简化的方法 foldByKey
    val newRdd: RDD[(String, Int)] = rdd.foldByKey(0)(_ + _)

    newRdd.collect().foreach(println)

    // 关闭环境
    sc.stop()
  }
}
f、combineByKey

def combineByKey[C](

createCombiner: V => C,

mergeValue: (C, V) => C,

mergeCombiners: (C, C) => C

): RDD[(K, C)]

最通用的对key-value型rdd进行聚集操作的聚集函数(aggregation function)。类似于 aggregate(),combineByKey()允许返回值的类型与输入不一致

参数1:将相同key的第一个数据进行结构的转换

参数2:分区内的计算规则

参数3:分区间的计算规则

将数据List(("a", 88), ("b", 95), ("a", 91), ("b", 93), ("a", 95), ("b", 98))求每个 key 的平均值

Scala 复制代码
object Test23 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val rdd = sc.makeRDD(List(
        ("a", 88), ("b", 95), ("a", 91),
        ("b", 93), ("a", 95), ("b", 98)
    ))

    // 算子 - combineByKey
    // 获取相同key的数据的平均值
    val combineRdd: RDD[(String, (Int, Int))] = rdd.combineByKey(
      v => (v, 1),
      (x: (Int, Int), y) => (x._1 + y, x._2 + 1),
      (x: (Int, Int), y: (Int, Int)) => (x._1 + y._1, x._2 + y._2)
    )

    val newRdd: RDD[(String, Int)] = combineRdd.mapValues { case (num, cnt) => num / cnt }

    newRdd.collect().foreach(println)
    // 关闭环境
    sc.stop()
  }
}

注意:reduceByKey、foldByKey、aggregateByKey、combineByKey 的区别

  • reduceByKey: 相同 key 的第一个数据不进行任何计算,分区内和分区间计算规则相同
  • FoldByKey: 相同 key的第一个数据和初始值进行分区内计算,分区内和分区间计算规则相 同
  • AggregateByKey:相同 key 的第一个数据和初始值进行分区内计算,分区内和分区间计算规则可以不相同
  • CombineByKey:当计算时,发现数据结构不满足要求时,可以让第一个数据转换结构。分区内和分区间计算规则不相同。
g、sortByKey

def sortByKey(

ascending: Boolean = true,

numPartitions: Int = self.partitions.length

) : RDD[(K, V)]

在一个(K,V)的RDD上调用,K必须实现Ordered接口(特质),按照key进行排序

Scala 复制代码
object Test24 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val rdd = sc.makeRDD(List(
      ("a", 1), ("a", 2), ("c", 3)
    ))

    // 算子 - sortByKey
    val newRdd: RDD[(String, Int)] = rdd.sortByKey(false)

    newRdd.collect().foreach(println)
    // 关闭环境
    sc.stop()
  }
}
h、join

def join[W](other: RDD[(K, W)]): RDD[(K, (V, W))]

在类型为(K,V)和(K,W)的RDD上调用,返回一个相同key对应的所有元素连接在一起的 (K,(V,W))的 RDD

  • 两个不同数据源的数据,相同的key的value会连接在一起,形成元组
  • 如果两个数据源中key没有匹配上,那么数据不会出现在结果中
  • 如果两个数据源中key有多个相同的,会依次匹配,可能会出现笛卡尔乘积
Scala 复制代码
object Test25 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val rdd1 = sc.makeRDD(List(
      ("a", 1), ("a", 2), ("c", 3)
    ))
    val rdd2 = sc.makeRDD(List(
      ("a", 3), ("c", 5), ("c", 3)
    ))

    // 算子 - join
    val newRdd: RDD[(String, (Int, Int))] = rdd1.join(rdd2)

    newRdd.collect().foreach(println)
    // 关闭环境
    sc.stop()
  }
}
i、leftOuterJoin

def leftOuterJoin[W](other: RDD[(K, W)]): RDD[(K, (V, Option[W]))]

类似于SQL语句的左外连接

Scala 复制代码
object Test26 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val rdd1 = sc.makeRDD(List(
      ("a", 1), ("b", 2)
    ))
    val rdd2 = sc.makeRDD(List(
      ("a", 3), ("b", 5), ("c", 3)
    ))

    // 算子 - leftOuterJoin
    val newRdd: RDD[(String, (Int, Option[Int]))] = rdd1.leftOuterJoin(rdd2)

    newRdd.collect().foreach(println)
    // 关闭环境
    sc.stop()
  }
}
j、cogroup

def cogroup[W](other: RDD[(K, W)]): RDD[(K, (Iterable[V], Iterable[W]))]

在类型为(K,V)和(K,W)的RDD上调用,返回一个(K,(Iterable,Iterable))类型的RDD

Scala 复制代码
object Test27 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    val rdd1 = sc.makeRDD(List(
      ("a", 1), ("b", 2)
    ))
    val rdd2 = sc.makeRDD(List(
      ("a", 3), ("b", 5), ("c", 3)
    ))

    // 算子 - cogroup
    val newRdd: RDD[(String, (Iterable[Int], Iterable[Int]))] = rdd1.cogroup(rdd2)

    newRdd.collect().foreach(println)
    // 关闭环境
    sc.stop()
  }
}
4)案例

agent.log:时间戳,省份,城市,用户,广告,中间字段使用空格分隔。

统计出每一个省份每个广告被点击数量排行的Top3

Scala 复制代码
object Test28 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val sc = new SparkContext(sparkConf)

    // 获取原始数据:时间戳,省份,城市,用户,广告
    val lineData: RDD[String] = sc.textFile("datas/agent.log")

    // 转换数据格式为  ( ( 省份,广告 ), 1 )
    val mapRdd: RDD[((String, String), Int)] = lineData.map(item => {
      val strings: Array[String] = item.split(" ")
      ((strings(1), strings(4)), 1)
    })

    // 进行分组聚合 ( ( 省份,广告 ), 1 ) => ( ( 省份,广告 ), sum )
    val reduceRdd: RDD[((String, String), Int)] = mapRdd.reduceByKey(_ + _)

    // 进行结构的转换  ( ( 省份,广告 ), sum ) => ( 省份, ( 广告, sum ) )
    val newRdd: RDD[(String, (String, Int))] = reduceRdd.map { case ((x, y), z) => (x, (y, z)) }

    // 进行分组
    val groupRdd: RDD[(String, Iterable[(String, Int)])] = newRdd.groupByKey()

    // 排序取前3名
    val newData: RDD[(String, List[(String, Int)])] = groupRdd.mapValues(item => {
      item.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
    })

    // 输出
    newData.collect().foreach(println)

    // 关闭环境
    sc.stop()
  }
}

(6)RDD 行动算子

1)reduce

def reduce(f: (T, T) => T): T

聚集RDD中的所有元素,先聚合分区内数据,再聚合分区间数据

Scala 复制代码
object Test01 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    val rdd = sc.makeRDD(List(1,2,3,4))

    // 行动算子 -- reduce
    val i: Int = rdd.reduce(_ + _)
    println("i = " + i)

    sc.stop()
  }
}
2)collect

def collect(): Array[T]

在驱动程序中,以数组Array的形式返回数据集的所有元素

Scala 复制代码
object Test01 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    val rdd = sc.makeRDD(List(1,2,3,4))

    // 行动算子 -- collect
    val ints: Array[Int] = rdd.collect()
    println(ints.mkString(","))

    sc.stop()
  }
}
3)count

def count(): Long

返回RDD中元素的个数

Scala 复制代码
object Test01 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    val rdd = sc.makeRDD(List(1,2,3,4))

    // 行动算子 -- count
    val l: Long = rdd.count()
    println("count: " + l)

    sc.stop()
  }
}
4)first

def first(): T

返回RDD中的第一个元素

Scala 复制代码
object Test01 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    val rdd = sc.makeRDD(List(1,2,3,4))

    // 行动算子 -- first
    val i: Int = rdd.first()
    println(i)

    sc.stop()
  }
}
5)take

def take(num: Int): Array[T]

返回一个由RDD的前n个元素组成的数组

Scala 复制代码
object Test01 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    val rdd = sc.makeRDD(List(1,2,3,4))

    // 行动算子 -- take
    val ints: Array[Int] = rdd.take(3)
    println(ints.mkString(","))

    sc.stop()
  }
}
6)takeOrdered

def takeOrdered(num: Int)(implicit ord: Ordering[T]): Array[T]

返回该RDD排序后的前n个元素组成的数组

Scala 复制代码
object Test01 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    val rdd = sc.makeRDD(List(1,2,3,4))

    // 行动算子 -- takeOrdered
    val ints: Array[Int] = rdd.takeOrdered(3)
    println(ints.mkString(","))
    println("==========")

    val ints1: Array[Int] = rdd.takeOrdered(3)(Ordering.Int.reverse)
    println(ints1.mkString(","))

    sc.stop()
  }
}
7)aggregate

def aggregate[U: ClassTag](zeroValue: U)(

seqOp: (U, T) => U,

combOp: (U, U) => U

): U

分区的数据通过初始值和分区内的数据进行聚合,然后再和初始值进行分区间的数据聚合

Scala 复制代码
object Test02 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    val rdd = sc.makeRDD(List(1,2,3,4),2)

    // 行动算子 -- aggregate
    /**
     * [1,2] [3,4]
     * 分区内计算  初始值1 + 1 + 2 =4
     *            初始值1 + 3 + 4 = 8
     * 分区间计算  初始值1 + 4 + 8 = 13
     * 
     * 最终结果13
     */
    val i: Int = rdd.aggregate(1)(
      _ + _,
      _ + _
    )
    println(i)

    sc.stop()
  }
}
8)fold

def fold(zeroValue: T)(op: (T, T) => T): T

折叠操作,aggregate的简化版操作

Scala 复制代码
object Test02 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    val rdd = sc.makeRDD(List(1,2,3,4),2)

    // 行动算子 -- fold
    /**
     * [1,2] [3,4]
     * 分区内计算  初始值1 + 1 + 2 =4
     *            初始值1 + 3 + 4 = 8
     * 分区间计算  初始值1 + 4 + 8 = 13
     *
     * 最终结果13
     */
    val i: Int = rdd.fold(1)( _ + _ )
    println(i)

    sc.stop()
  }
}
9)countByKey

def countByKey(): Map[K, Long]

统计每种key的个数

Scala 复制代码
object Test02 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    val rdd = sc.makeRDD( List(
        (1, "a"), (1, "a"), (1, "a"),
        (2,"b"), (3, "c"), (3, "c")
    ),2)

    // 行动算子 -- countByKey
    val intToLong: collection.Map[Int, Long] = rdd.countByKey()
    println(intToLong)
    println("============")

    // 行动算子 -- countByValue
    val tupleToLong: collection.Map[(Int, String), Long] = rdd.countByValue()
    println(tupleToLong)

    sc.stop()
  }
}
10)save 相关算子

def saveAsTextFile(path: String): Unit

def saveAsObjectFile(path: String): Unit

def saveAsSequenceFile(

path: String,

codec: Option[Class[_ <: CompressionCodec]] = None

): Unit

将数据保存到不同格式的文件中

Scala 复制代码
object Test03 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    val rdd = sc.makeRDD( List(
      ("a", 1),("a", 2),("a", 3)
    ))

    // 行动算子 -- save
    rdd.saveAsTextFile("output")
    rdd.saveAsObjectFile("output1")
    rdd.saveAsSequenceFile("output2")

    sc.stop()
  }
}
11)foreach

def foreach(f: T => Unit): Unit = withScope {

val cleanF = sc.clean(f)

sc.runJob(this, (iter: Iterator[T]) => iter.foreach(cleanF))

}

分布式遍历RDD中的每一个元素,调用指定函数

Scala 复制代码
object Test04 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    val rdd = sc.makeRDD( List(
      ("a", 1),("a", 2),("a", 3)
    ))

    // 行动算子 -- foreach
    rdd.foreach(println)

    sc.stop()
  }
}

并行打印输出是无序的

(7)RDD序列化

1)闭包检查

从计算的角度, 算子以外的代码都是在Driver端执行, 算子里面的代码都是在Executor 端执行。那么在scala的函数式编程中,就会导致算子内经常会用到算子外的数据,这样就形成了闭包的效果,如果使用算子外数据没有序列化,就意味着无法传值给Executor 端执行,就会发生错误,所以需要在执行任务计算前,检测闭包内的对象是否可以进行序列 化,这个操作称之为闭包检测。Scala2.12版本后闭包编译方式发生了改变

2) 序列化方法和属性

从计算的角度, 算子以外的代码都是在Driver端执行, 算子里面的代码都是在Executor 端执行,看如下代码:

Scala 复制代码
object Test05 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    //3.创建一个RDD
    val rdd: RDD[String] = sc.makeRDD(Array("hello world", "hello spark", "hive", "java"))

    // 函数传递,打印:ERROR Task not serializable
    // 情况一:将类进行序列化
    val search = new Search("hello")
    search.getMatch1(rdd).collect().foreach(println)
    println("=====================")

    // 情况二:使用case关键字
    val search2 = Search2("hello")
    search2.getMatch1(rdd).collect().foreach(println)
    println("=====================")

    // 属性传递,打印:ERROR Task not serializable
    // 情况三:将参数重新赋值
    val search3 = new Search3("hello")
    search3.getMatch1(rdd).collect().foreach(println)
    println("=====================")

    //4.关闭连接
    sc.stop()
  }
}

class Search(query:String) extends Serializable {

  def isMatch(s: String): Boolean = {
    s.contains(query)
  }

  // 函数序列化案例
  def getMatch1 (rdd: RDD[String]): RDD[String] = {
    //rdd.filter(this.isMatch)
    rdd.filter(isMatch)
  }

}

case class Search2(query:String) {

  def isMatch(s: String): Boolean = {
    s.contains(query)
  }

  // 函数序列化案例
  def getMatch1 (rdd: RDD[String]): RDD[String] = {
    //rdd.filter(this.isMatch)
    rdd.filter(isMatch)
  }

}

class Search3(query:String) {
  // 属性序列化案例
  def getMatch1(rdd: RDD[String]): RDD[String] = {
//    rdd.filter(x => x.contains(query)) // 这里的query其实是this.query

    val q = query
    rdd.filter(x => x.contains(q))
  }
}
3) Kryo序列化框架

参考地址: https://github.com/EsotericSoftware/kryo

Java的序列化能够序列化任何的类。但是比较重(字节多),序列化后,对象的提交也 比较大。Spark出于性能的考虑,Spark2.0开始支持另外一种Kryo序列化机制。Kryo速度是Serializable的10倍。当RDD在Shuffle数据的时候,简单数据类型、数组和字符串类型已经在Spark内部使用Kryo来序列化。 注意:即使使用Kryo序列化,也要继承Serializable接口。

Scala 复制代码
 val conf: SparkConf = new SparkConf() 
                .setAppName("SerDemo") 
                .setMaster("local[*]") 
                // 替换默认的序列化机制 
                .set("spark.serializer", "org.apache.spark.serializer.KryoSerializer") 
                // 注册需要使用 kryo 序列化的自定义类 
                .registerKryoClasses(Array(classOf[Searcher])) 

(8)RDD依赖关系

1)RDD 血缘关系

RDD只支持粗粒度转换,即在大量记录上执行的单个操作。将创建RDD的一系列Lineage (血统)记录下来,以便恢复丢失的分区。RDD的Lineage会记录RDD的元数据信息和转换行为,当该RDD的部分分区数据丢失时,它可以根据这些信息来重新运算和恢复丢失的数据分区。

Scala 复制代码
object Test06 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    // 读取文件
    val line: RDD[String] = sc.textFile("datas/test01.txt")
    println(line.toDebugString)
    println("======================")

    val lineMap: RDD[String] = line.flatMap(_.split(" "))
    println(lineMap.toDebugString)
    println("======================")

    val valueMap: RDD[(String, Int)] = lineMap.map((_, 1))
    println(valueMap.toDebugString)
    println("======================")

    val newRdd: RDD[(String, Int)] = valueMap.reduceByKey(_ + _)
    println(newRdd.toDebugString)
    println("======================")

    newRdd.collect().foreach(println)

    sc.stop()
  }
}
2)RDD 依赖关系

所谓的依赖关系,其实就是两个相邻RDD之间的关系

Scala 复制代码
object Test07 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    // 读取文件
    val line: RDD[String] = sc.textFile("datas/test01.txt")
    println(line.dependencies)
    println("======================")

    val lineMap: RDD[String] = line.flatMap(_.split(" "))
    println(lineMap.dependencies)
    println("======================")

    val valueMap: RDD[(String, Int)] = lineMap.map((_, 1))
    println(valueMap.dependencies)
    println("======================")

    val newRdd: RDD[(String, Int)] = valueMap.reduceByKey(_ + _)
    println(newRdd.dependencies)
    println("======================")

    newRdd.collect().foreach(println)

    sc.stop()
  }
}
3)RDD 窄依赖

窄依赖表示每一个父(上游)RDD的Partition最多被子(下游)RDD的一个Partition使用, 窄依赖形象的比喻为独生子女。

class OneToOneDependency[T](rdd: RDD[T]) extends NarrowDependency[T](rdd) 4)

4)RDD 宽依赖

宽依赖表示同一个父(上游)RDD的Partition被多个子(下游)RDD的Partition依赖,会引起Shuffle,总结:宽依赖形象的比喻为多生。

class ShuffleDependency [K: ClassTag, V: ClassTag, C: ClassTag](
@transient private val rdd: RDD[ <: Product2[K, V]],
val partitioner: Partitioner,
val serializer: Serializer = SparkEnv.get.serializer,
val keyOrdering: Option[Ordering[K]] = None,
val aggregator: Option[Aggregator[K, V, C]] = None,
val mapSideCombine: Boolean = false)
extends Dependency[Product2[K, V]]

(9)RDD 持久化

1)RDD Cache缓存

RDD通过Cache或者Persist方法将前面的计算结果缓存,默认情况下会把数据以缓存在JVM的堆内存中。但是并不是这两个方法被调用时立即缓存,而是触发后面的action算子时,该RDD将会被缓存在计算节点的内存中,并供后面重用。

不进行持久化结果:

Scala 复制代码
object Test08 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    val list = List("Hello Scala", "Hello Spark")
    val rdd = sc.makeRDD(list)

    val flatRDD = rdd.flatMap(_.split(" "))

    val mapRDD = flatRDD.map(word=>{
      println("@@@@@@@@@@@@")
      (word,1)
    })
    val reduceRDD: RDD[(String, Int)] = mapRDD.reduceByKey(_+_)
    // 第一次输出
    reduceRDD.collect().foreach(println)
    println("**************************************")

    // 第二次输出
    val groupRDD = mapRDD.groupByKey()
    groupRDD.collect().foreach(println)

    sc.stop()
  }
}

因为Rdd不存储数据,所以当需要使用rdd数据时spark需要重新计算得到数据,效率低

进行持久化结果:

Scala 复制代码
object Test08 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    val list = List("Hello Scala", "Hello Spark")
    val rdd = sc.makeRDD(list)

    val flatRDD = rdd.flatMap(_.split(" "))

    val mapRDD = flatRDD.map(word=>{
      println("@@@@@@@@@@@@")
      (word,1)
    })

    // 将mapRDD得到的数据进行持久化
    // cache默认持久化的操作,只能将数据保存到内存中,如果想要保存到磁盘文件,需要更改存储级别
    mapRDD.cache()
    // 持久化操作必须在行动算子执行时完成的。
//    mapRDD.persist(StorageLevel.DISK_ONLY)

    val reduceRDD: RDD[(String, Int)] = mapRDD.reduceByKey(_+_)
    // 第一次输出
    reduceRDD.collect().foreach(println)
    println("**************************************")

    // 第二次输出
    val groupRDD = mapRDD.groupByKey()
    groupRDD.collect().foreach(println)

    sc.stop()
  }
}

缓存有可能丢失,或者存储于内存的数据由于内存不足而被删除,RDD的缓存容错机制保证了即使缓存丢失也能保证计算的正确执行。通过基于RDD的一系列转换,丢失的数据会被重算,由于RDD的各个Partition是相对独立的,因此只需要计算丢失的部分即可, 并不需要重算全部Partition。 Spark 会自动对一些Shuffle操作的中间数据做持久化操作(比如:reduceByKey)。这样 做的目的是为了当一个节点Shuffle失败了避免重新计算整个输入。但是,在实际使用的时候,如果想重用数据,仍然建议调用persist或cache。

2)RDD CheckPoint 检查点

所谓的检查点其实就是通过将RDD中间结果写入磁盘, 由于血缘依赖过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,如果检查点之后有节点出现问题,可以从检查点开始重做血缘,减少了开销。 对RDD进行checkpoint操作并不会马上被执行,必须执行Action操作才能触发。

观察:如果不设置缓存只设置检查点

Scala 复制代码
object Test09 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    // checkpoint 需要落盘,需要指定检查点保存路径
    // 检查点路径保存的文件,当作业执行完毕后,不会被删除
    // 一般保存路径都是在分布式存储系统:HDFS
    sc.setCheckpointDir("./checkpoint1")

    val list = List("Hello Scala", "Hello Spark")
    val rdd = sc.makeRDD(list)

    val flatRDD = rdd.flatMap(_.split(" "))

    val mapRDD = flatRDD.map(word=>{
      println("@@@@@@@@@@@@")
      (word,1)
    })

    //  数据检查点:针对wordToOneRdd做检查点计算
    mapRDD.checkpoint()

    val reduceRDD: RDD[(String, Int)] = mapRDD.reduceByKey(_+_)
    // 输出
    reduceRDD.collect().foreach(println)

    sc.stop()
  }
}

发现输出了2次,这是因为执行检查点操作时数据会重新计算,所以一般在设置检查点之前设置缓存

观察:设置缓存和检查点

Scala 复制代码
object Test09 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    // checkpoint 需要落盘,需要指定检查点保存路径
    // 检查点路径保存的文件,当作业执行完毕后,不会被删除
    // 一般保存路径都是在分布式存储系统:HDFS
    sc.setCheckpointDir("./checkpoint1")

    val list = List("Hello Scala", "Hello Spark")
    val rdd = sc.makeRDD(list)

    val flatRDD = rdd.flatMap(_.split(" "))

    val mapRDD = flatRDD.map(word=>{
      println("@@@@@@@@@@@@")
      (word,1)
    })

    // 增加缓存,避免再重新跑一个job做checkpoint
    mapRDD.cache()
    //  数据检查点:针对wordToOneRdd做检查点计算
    mapRDD.checkpoint()

    val reduceRDD: RDD[(String, Int)] = mapRDD.reduceByKey(_+_)
    // 输出
    reduceRDD.collect().foreach(println)

    sc.stop()
  }
}
3)缓存和检查点区别

1、Cache 缓存只是将数据保存起来,不切断血缘依赖。Checkpoint检查点切断血缘依赖。

2)Cache 缓存的数据通常存储在磁盘、内存等地方,可靠性低。Checkpoint的数据通常存储在HDFS等容错、高可用的文件系统,可靠性高。

3)建议对checkpoint()的RDD使用Cache缓存,这样checkpoint的job只需从Cache缓存 中读取数据即可,否则需要再从头计算一次RDD。

观察:Checkpoint血缘依赖

Scala 复制代码
object Test09 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    // checkpoint 需要落盘,需要指定检查点保存路径
    // 检查点路径保存的文件,当作业执行完毕后,不会被删除
    // 一般保存路径都是在分布式存储系统:HDFS
    sc.setCheckpointDir("./checkpoint1")

    val list = List("Hello Scala", "Hello Spark")
    val rdd = sc.makeRDD(list)

    val flatRDD = rdd.flatMap(_.split(" "))

    val mapRDD = flatRDD.map(word=>{
      println("@@@@@@@@@@@@")
      (word,1)
    })

    // 增加缓存,避免再重新跑一个job做checkpoint
    mapRDD.cache()
    //  数据检查点:针对wordToOneRdd做检查点计算
    mapRDD.checkpoint()
    println(mapRDD.toDebugString)
    println("==========================")
    val reduceRDD: RDD[(String, Int)] = mapRDD.reduceByKey(_+_)
    // 输出
    reduceRDD.collect().foreach(println)
    println("==========================")
    println(mapRDD.toDebugString)
    sc.stop()
  }
}

(10)RDD分区器

Spark目前支持Hash分区和Range分区,和用户自定义分区。Hash分区为当前的默认分区。分区器直接决定了RDD中分区的个数、RDD中每条数据经过Shuffle后进入哪个分区,进而决定了Reduce的个数。

➢ 只有Key-Value类型的RDD才有分区器,非Key-Value类型的RDD分区的值是None

➢ 每个RDD的分区ID范围:0 ~ (numPartitions - 1),决定这个值是属于那个分区的。

1)Hash分区

对于给定的key,计算其hashCode,并除以分区个数取余

Scala 复制代码
object Test10 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    val rdd = sc.makeRDD(List(
      ("a", 1),
      ("c", 1),
      ("a", 1),
      ("d", 1)
    ))

    val value: RDD[(String, Int)] = rdd.partitionBy(new HashPartitioner(2))
    value.saveAsTextFile("output")

    sc.stop()
  }
}

2)Range分区

将一定范围内的数据映射到一个分区中,尽量保证每个分区数据均匀,而 且分区间有序

Scala 复制代码
object Test11 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    val rdd = sc.makeRDD(List(
      (1, "a"),
      (5, "b"),
      (3, "c"),
      (9, "d"),
      (7, "e"),
      (2, "f")
    ))

    val value: RDD[(Int, String)] = rdd.partitionBy(new RangePartitioner(3, rdd))
    value.saveAsTextFile("output")

    sc.stop()
  }
}

3)自定义分区

当默认分区不满足使用时,可以自定义分区,自定义分区实现Partitioner

Scala 复制代码
object Test12 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    val rdd = sc.makeRDD(List(
      (1, "a"),
      (5, "b"),
      (3, "c"),
      (9, "d"),
      (7, "e"),
      (2, "f")
    ))

    val value: RDD[(Int, String)] = rdd.partitionBy(new MyPartitioner(2))
    value.saveAsTextFile("output")

    sc.stop()
  }
}

class MyPartitioner(partitions: Int) extends Partitioner() {
  // 分区数量
  override def numPartitions: Int = partitions


  override def getPartition(key: Any): Int = {
    // 根据数据的key值返回数据所在的分区索引(从0开始)
    key match {
      case i: Int =>
        i match {
          case k if k % 2 == 0 => 0
          case k if k % 2 == 1 => 1
        }
      case _ =>
        1
    }

  }
}

(11)RDD 文件读取与保存

Spark 的数据读取及数据保存可以从两个维度来作区分:文件格式以及文件系统。

文件格式分为:text文件、csv文件、sequence文件以及Object文件;

文件系统分为:本地文件系统、HDFS、HBASE以及数据库。

1)text文件
Scala 复制代码
object Test13 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    val rdd = sc.makeRDD(List(
      (1, "a"),
      (5, "b"),
      (3, "c"),
      (9, "d"),
      (7, "e"),
      (2, "f")
    ))

    // 保存文件
    rdd.saveAsTextFile("output1")

    // 读取文件
    val line: RDD[String] = sc.textFile("output1")
    line.collect().foreach(println)

    sc.stop()
  }
}
2)sequence文件

SequenceFile 文件是Hadoop 用来存储二进制形式的key-value对而设计的一种平面文件(Flat File)。在 SparkContext 中,可以调用sequenceFile[keyClass, valueClass](path)。

Scala 复制代码
object Test14 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    val rdd = sc.makeRDD(List(
      (1, "a"),
      (5, "b"),
      (3, "c"),
      (9, "d"),
      (7, "e"),
      (2, "f")
    ))

    // 保存文件
    rdd.saveAsSequenceFile("output2")

    // 读取文件
    val value: RDD[(Int, String)] = sc.sequenceFile[Int, String]("output2")
    value.collect().foreach(println)

    sc.stop()
  }
}
3)object对象文件

对象文件是将对象序列化后保存的文件,采用Java的序列化机制。可以通过objectFile[T: ClassTag](path)函数接收一个路径,读取对象文件,返回对应的RDD,也可以通过调用 saveAsObjectFile()实现对对象文件的输出。因为是序列化所以要指定类型。

Scala 复制代码
object Test15 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    val rdd = sc.makeRDD(List(
      (1, "a"),
      (5, "b"),
      (3, "c"),
      (9, "d"),
      (7, "e"),
      (2, "f")
    ))

    // 保存文件
    rdd.saveAsObjectFile("output3")

    // 读取文件
    val value: RDD[(String, Int)] = sc.objectFile[(String, Int)]("output3")
    value.collect().foreach(println)

    sc.stop()
  }
}

(12)综合案例wordcount

Scala 复制代码
object Test05 {
  def main(args: Array[String]): Unit = {
    val sparConf = new SparkConf().setMaster("local").setAppName("Acc")
    val sc = new SparkContext(sparConf)

    wordcount1(sc)

    sc.stop()
  }

  // groupBy
  def wordcount1(sc : SparkContext): Unit = {
    val rdd = sc.makeRDD(List("Hello Scala", "Hello Spark"))
    val words = rdd.flatMap(_.split(" "))
    val group: RDD[(String, Iterable[String])] = words.groupBy(word=>word)
    val wordCount: RDD[(String, Int)] = group.mapValues(iter=>iter.size)
  }

  // groupByKey
  def wordcount2(sc : SparkContext): Unit = {
    val rdd = sc.makeRDD(List("Hello Scala", "Hello Spark"))
    val words = rdd.flatMap(_.split(" "))
    val wordOne = words.map((_,1))
    val group: RDD[(String, Iterable[Int])] = wordOne.groupByKey()
    val wordCount: RDD[(String, Int)] = group.mapValues(iter=>iter.size)
  }

  // reduceByKey
  def wordcount3(sc : SparkContext): Unit = {
    val rdd = sc.makeRDD(List("Hello Scala", "Hello Spark"))
    val words = rdd.flatMap(_.split(" "))
    val wordOne = words.map((_,1))
    val wordCount: RDD[(String, Int)] = wordOne.reduceByKey(_+_)
  }

  // aggregateByKey
  def wordcount4(sc : SparkContext): Unit = {
    val rdd = sc.makeRDD(List("Hello Scala", "Hello Spark"))
    val words = rdd.flatMap(_.split(" "))
    val wordOne = words.map((_,1))
    val wordCount: RDD[(String, Int)] = wordOne.aggregateByKey(0)(_+_, _+_)
  }

  // foldByKey
  def wordcount5(sc : SparkContext): Unit = {
    val rdd = sc.makeRDD(List("Hello Scala", "Hello Spark"))
    val words = rdd.flatMap(_.split(" "))
    val wordOne = words.map((_,1))
    val wordCount: RDD[(String, Int)] = wordOne.foldByKey(0)(_+_)
  }

  // combineByKey
  def wordcount6(sc : SparkContext): Unit = {
    val rdd = sc.makeRDD(List("Hello Scala", "Hello Spark"))
    val words = rdd.flatMap(_.split(" "))
    val wordOne = words.map((_,1))
    val wordCount: RDD[(String, Int)] = wordOne.combineByKey(
      v=>v,
      (x:Int, y) => x + y,
      (x:Int, y:Int) => x + y
    )
  }

  // countByKey
  def wordcount7(sc : SparkContext): Unit = {
    val rdd = sc.makeRDD(List("Hello Scala", "Hello Spark"))
    val words = rdd.flatMap(_.split(" "))
    val wordOne = words.map((_,1))
    val wordCount: collection.Map[String, Long] = wordOne.countByKey()
  }

  // countByValue
  def wordcount8(sc : SparkContext): Unit = {
    val rdd = sc.makeRDD(List("Hello Scala", "Hello Spark"))
    val words = rdd.flatMap(_.split(" "))
    val wordCount: collection.Map[String, Long] = words.countByValue()
  }

  // reduce, aggregate, fold
  def wordcount91011(sc : SparkContext): Unit = {
    val rdd = sc.makeRDD(List("Hello Scala", "Hello Spark"))
    val words = rdd.flatMap(_.split(" "))

    // 【(word, count),(word, count)】
    // word => Map[(word,1)]
    val mapWord = words.map(
      word => {
        mutable.Map[String, Long]((word,1))
      }
    )

    val wordCount = mapWord.reduce(
      (map1, map2) => {
        map2.foreach{
          case (word, count) => {
            val newCount = map1.getOrElse(word, 0L) + count
            map1.update(word, newCount)
          }
        }
        map1
      }
    )

    println(wordCount)
  }
}

2、累加器

(1)实现原理

累加器用来把Executor端变量信息聚合到Driver端。在Driver程序中定义的变量,在 Executor端的每个Task都会得到这个变量的一份新的副本,每个task更新这些副本的值后, 传回Driver端进行merge。

Scala 复制代码
object Test01 {
  def main(args: Array[String]): Unit = {
    val sparConf = new SparkConf().setMaster("local").setAppName("Acc")
    val sc = new SparkContext(sparConf)

    val rdd = sc.makeRDD(List(1,2,3,4))

    var sum = 0
    rdd.foreach(
      num => {
        sum += num
      }
    )
    println("sum = " + sum)

    sc.stop()
  }
}

为什么上述代码的结果是0而不是10:

RDD内部使用到了外部的变量,所以Driver端的sum值会传递给Executor,但是Executor执行完成后没有将sum的值传回给Driver,所以Driver端的sum还是初始值0

(2)基础编程

1)系统累加器

Spark默认就提供了简单数据聚合的累加器:longAccumulator、doubleAccumulator、collectionAccumulator

Scala 复制代码
object Test02 {
  def main(args: Array[String]): Unit = {
    val sparConf = new SparkConf().setMaster("local").setAppName("Acc")
    val sc = new SparkContext(sparConf)

    val rdd = sc.makeRDD(List(1,2,3,4))

    // 获取系统累加器
    val sumAcc = sc.longAccumulator("sum")

    rdd.foreach(
      num => {
        sumAcc.add(num)
      }
    )

    // 获取累加器的值
    println("sum = " + sumAcc.value)

    sc.stop()
  }
}
2)自定义累加器
Scala 复制代码
object Test03 {
  def main(args: Array[String]): Unit = {
    val sparConf = new SparkConf().setMaster("local").setAppName("Acc")
    val sc = new SparkContext(sparConf)

    val line: RDD[String] = sc.textFile("datas/test01.txt")

    // 创建累加器对象
    val acc = new MyAccumulator()

    // 向Spark进行注册
    sc.register(acc, "wordCountAcc")

    val newRdd: RDD[String] = line.flatMap(_.split(" "))
    newRdd.foreach(acc.add(_))

    // 获取累加器累加的结果
    println(acc.value)

    sc.stop()
  }
}

class MyAccumulator extends AccumulatorV2[String, mutable.Map[String, Int]] {
  private var wcMap = mutable.Map[String, Int]()

  // 初始状态
  override def isZero: Boolean = {
    wcMap.isEmpty
  }

  // 复制
  override def copy(): AccumulatorV2[String, mutable.Map[String, Int]] ={
    new MyAccumulator()
  }

  // 重置
  override def reset(): Unit = {
    wcMap.clear()
  }

  // 累加值
  override def add(v: String): Unit = {
    val newCount: Int = wcMap.getOrElse(v, 0) + 1
    wcMap.update(v, newCount)
  }

  // Driver合并多个累加器
  override def merge(other: AccumulatorV2[String, mutable.Map[String, Int]]): Unit = {
    val m1 = this.wcMap
    val m2 = other.value

    m2.foreach{
      case (word, count) => {
        val newCount = m1.getOrElse(word, 0) + count
        m1.update(word, newCount)
      }
    }
  }

  // 累加器结果
  override def value: mutable.Map[String, Int] = wcMap
}

3、广播变量

广播变量用来高效分发较大的对象。向所有工作节点发送一个较大的只读值,以供一个或多个Spark操作使用。比如,如果应用需要向所有节点发送一个较大的只读查询表, 广播变量用起来都很顺手。在多个并行操作中使用同一个变量,但是 Spark会为每个任务分别发送。

Scala 复制代码
object Test04 {
  def main(args: Array[String]): Unit = {
    val sparConf = new SparkConf().setMaster("local").setAppName("Acc")
    val sc = new SparkContext(sparConf)

    // 需要广播的数据
    val list = Map( ("a",4), ("b", 5), ("c", 6), ("d", 7) )
    // 声明广播变量
    val bc: Broadcast[Map[String, Int]] = sc.broadcast(list)

    val rdd: RDD[(String, Int)] = sc.makeRDD(List(("a", 1), ("b", 2), ("c", 3), ("d", 4)), 4)

    rdd.map{
      case (k, v) => {
        val l: Int = bc.value.getOrElse(k, 0)
        (k, (v, l))
      }
    }.collect().foreach(println)

    sc.stop()
  }
}

六、综合案例

上面的数据图是从数据文件中截取的一部分内容,表示为电商网站的用户行为数据,主要包含用户的4种行为:搜索,点击,下单,支付。

数据规则如下:

➢ 数据文件中每行数据采用下划线分隔数据

➢ 每一行数据表示用户的一次行为,这个行为只能是4种行为的一种

➢ 如果搜索关键字为null,表示数据不是搜索数据

➢ 如果点击的品类ID和产品ID为-1,表示数据不是点击数据

➢ 针对于下单行为,一次可以下单多个商品,所以品类ID和产品ID可以是多个,id之间采用逗号分隔,如果本次不是下单行为,则数据采用null表示

➢ 支付行为和下单行为类似

2019-07-17_38_6502cdc9-cf95-4b08-8854-f03a25baa917_24_2019-07-17 00:00:38_null_-1_-1_15,13,5,11,8_99,2_null_null_10

|----|------------------------------------------|--------------------|--------|----------------|
| 编号 | 数据 | 字段名称 | 字段类型 | 字段含义 |
| 1 | 2019-07-17 | date | String | 用户点击行为的日期 |
| 2 | 38 | user_id | Long | 用户的ID |
| 3 | 6502cdc9-cf95-4b08-8854-f03a25baa917 | session_id | String | Session 的 ID |
| 4 | 24 | page_id | Long | 某个页面的ID |
| 5 | 2019-07-17 00:00:38 | action_time | String | 动作的时间点 |
| 6 | null | search_keyword | String | 用户搜索的关键词 |
| 7 | -1 | click_category_id | Long | 某一个商品品类的ID |
| 8 | -1 | click_product_id | Long | 某一个商品的ID |
| 9 | 15,13,5,11,8 | order_category_ids | String | 一次订单中所有品类的ID集合 |
| 10 | 99,2 | order_product_ids | String | 一次订单中所有商品的ID集合 |
| 11 | null | pay_category_ids | String | 一次支付中所有品类的ID集合 |
| 12 | null | pay_product_ids | String | 一次支付中所有商品的ID集合 |
| 13 | 10 | city_id | Long | 城市 id |

1、需求1:Top10热门品类

鞋 点击数 下单数 支付数

衣服 点击数 下单数 支付数

先按照点击数排名,靠前的就排名高;如果点击数相同,再比较下 单数;下单数再相同,就比较支付数。

(1)实现1

Scala 复制代码
object Demo01 {
  def main(args: Array[String]): Unit = {
    val sparConf = new SparkConf().setMaster("local[*]").setAppName("Top10")
    val sc = new SparkContext(sparConf)

    // 读取文件数据
    val sourceData: RDD[String] = sc.textFile("datas/user_visit_action.txt")
    
    val splitRdd: RDD[Array[String]] = sourceData.map(item => {
      item.split("_")
    })

    // 统计品类的点击数量:(品类ID,点击数量)
    val clickActionRDD: RDD[Array[String]] = splitRdd.filter(_(6) != "-1")
    val clickMapRDD: RDD[(String, Int)] = clickActionRDD.map(item => (item(6), 1))
    val clickCountRDD: RDD[(String, Int)] = clickMapRDD.reduceByKey(_ + _)

    // 统计品类的下单数量:(品类ID,下单数量)
    val orderActionRDD: RDD[Array[String]] = splitRdd.filter(_(8) != "null")
    val orderMapRDD: RDD[(String, Int)] = orderActionRDD.flatMap(item => {
      val cids: Array[String] = item(8).split(",")
      cids.map((_, 1))
    })
    val orderCountRDD: RDD[(String, Int)] = orderMapRDD.reduceByKey(_ + _)

    // 统计品类的支付数量:(品类ID,支付数量)
    val payActionRDD: RDD[Array[String]] = splitRdd.filter(_(10) != "-1")
    val payMapRDD: RDD[(String, Int)] = payActionRDD.flatMap(item => {
      val cids: Array[String] = item(10).split(",")
      cids.map((_, 1))
    })
    val payCountRDD: RDD[(String, Int)] = payMapRDD.reduceByKey(_ + _)

    // 将品类进行排序,并且取前10名, 转换数据格式为 ( 品类ID, ( 点击数量, 下单数量, 支付数量 ) )
    val cogroupRDD: RDD[(String, (Iterable[Int], Iterable[Int], Iterable[Int]))] = clickCountRDD.cogroup(orderCountRDD, payCountRDD)
    val analysisRDD: RDD[(String, (Int, Int, Int))] = cogroupRDD.mapValues {
      case (clickIter, orderIter, payIter) => {
        val clickCnt = if (clickIter.iterator.hasNext) clickIter.iterator.next() else 0
        val orderCnt = if (orderIter.iterator.hasNext) orderIter.iterator.next() else 0
        val payCnt = if (payIter.iterator.hasNext) payIter.iterator.next() else 0
        (clickCnt, orderCnt, payCnt)
      }
    }

    val resultRDD: Array[(String, (Int, Int, Int))] = analysisRDD.sortBy(_._2, false).take(10)

    // 输出
    resultRDD.foreach(println)

    sc.stop()
  }
}

问题:

1、splitRdd重复使用,每次都需要从原数据获取

2、cogroup性能可能较低,可能存在shuffle

(2)实现2

Scala 复制代码
object Demo02 {
  def main(args: Array[String]): Unit = {
    val sparConf = new SparkConf().setMaster("local[*]").setAppName("Top10")
    val sc = new SparkContext(sparConf)

    // 读取文件数据
    val sourceData: RDD[String] = sc.textFile("datas/user_visit_action.txt")
    
    val splitRdd: RDD[Array[String]] = sourceData.map(item => {
      item.split("_")
    })

    // 数据存入缓存
    splitRdd.cache()

    // 统计品类的点击数量:(品类ID,点击数量)
    val clickActionRDD: RDD[Array[String]] = splitRdd.filter(_(6) != "-1")
    val clickMapRDD: RDD[(String, Int)] = clickActionRDD.map(item => (item(6), 1))
    val clickCountRDD: RDD[(String, Int)] = clickMapRDD.reduceByKey(_ + _)

    // 统计品类的下单数量:(品类ID,下单数量)
    val orderActionRDD: RDD[Array[String]] = splitRdd.filter(_(8) != "null")
    val orderMapRDD: RDD[(String, Int)] = orderActionRDD.flatMap(item => {
      val cids: Array[String] = item(8).split(",")
      cids.map((_, 1))
    })
    val orderCountRDD: RDD[(String, Int)] = orderMapRDD.reduceByKey(_ + _)

    // 统计品类的支付数量:(品类ID,支付数量)
    val payActionRDD: RDD[Array[String]] = splitRdd.filter(_(10) != "-1")
    val payMapRDD: RDD[(String, Int)] = payActionRDD.flatMap(item => {
      val cids: Array[String] = item(10).split(",")
      cids.map((_, 1))
    })
    val payCountRDD: RDD[(String, Int)] = payMapRDD.reduceByKey(_ + _)

    // 将品类进行排序,并且取前10名, 转换数据格式为 ( 品类ID, ( 点击数量, 下单数量, 支付数量 ) )
    /**
     *  (品类ID, 点击数量) => (品类ID, (点击数量, 0, 0))
     *  (品类ID, 下单数量) => (品类ID, (0, 下单数量, 0))
     *  (品类ID, 支付数量) => (品类ID, (0, 0, 支付数量))
     *
     *  转为:(品类ID, (点击数量, 0, 0)) ==>> (品类ID, (点击数量, 下单数量, 0)) ==>> (品类ID, (点击数量, 下单数量, 支付数量))
     */
    val rdd1: RDD[(String, (Int, Int, Int))] = clickCountRDD.map {
      case (cid, count) => (cid, (count, 0, 0))
    }
    val rdd2: RDD[(String, (Int, Int, Int))] = orderCountRDD.map {
      case (cid, count) => (cid, (0, count, 0))
    }
    val rdd3: RDD[(String, (Int, Int, Int))] = payCountRDD.map {
      case (cid, count) => (cid, (0, 0, count))
    }
    // 将三个数据源合并在一起,统一进行聚合计算
    val soruceRDD: RDD[(String, (Int, Int, Int))] = rdd1.union(rdd2).union(rdd3)
    val analysisRDD: RDD[(String, (Int, Int, Int))] = soruceRDD.reduceByKey(
      (t1, t2) => {
        (t1._1 + t2._1, t1._2 + t2._2, t1._3 + t2._3)
      }
    )

    val resultRDD: Array[(String, (Int, Int, Int))] = analysisRDD.sortBy(_._2, false).take(10)

    // 输出
    resultRDD.foreach(println)

    sc.stop()
  }
}

问题:

存在大量的shuffle操作(reduceByKey)

(3)实现3

Scala 复制代码
object Demo03 {
  def main(args: Array[String]): Unit = {
    val sparConf = new SparkConf().setMaster("local[*]").setAppName("Top10")
    val sc = new SparkContext(sparConf)

    // 读取文件数据
    val sourceData: RDD[String] = sc.textFile("datas/user_visit_action.txt")
    
    val splitRdd: RDD[Array[String]] = sourceData.map(item => {
      item.split("_")
    })

    // 数据存入缓存
    splitRdd.cache()

    /**
     * 将数据转换结构
     * 点击: ( 品类ID,( 1, 0, 0 ) )
     * 下单: ( 品类ID,( 0, 1, 0 ) )
     * 支付: ( 品类ID,( 0, 0, 1 ) )
     */
    val flatRDD: RDD[(String, (Int, Int, Int))] = splitRdd.flatMap(item => {
      if (item(6) != "-1") {
        List((item(6), (1, 0, 0)))
      }
      else if (item(8) != "null") {
        item(8).split(",").map((_, (0, 1, 0)))
      }
      else if (item(10) != "null") {
        item(10).split(",").map((_, (0, 0, 1)))
      }
      else {
        Nil
      }
    })

    // 将相同的品类ID的数据进行分组聚合
    val analysisRDD: RDD[(String, (Int, Int, Int))] = flatRDD.reduceByKey(
      (t1, t2) => {
        (t1._1 + t2._1, t1._2 + t2._2, t1._3 + t2._3)
      }
    )

    val resultRDD: Array[(String, (Int, Int, Int))] = analysisRDD.sortBy(_._2, false).take(10)

    // 输出
    resultRDD.foreach(println)

    sc.stop()
  }
}

那能不能不用reduceByKey?

(4)实现4

Scala 复制代码
object Demo04 {
  def main(args: Array[String]): Unit = {
    val sparConf = new SparkConf().setMaster("local[*]").setAppName("Top10")
    val sc = new SparkContext(sparConf)

    // 读取文件数据
    val sourceData: RDD[String] = sc.textFile("datas/user_visit_action.txt")

    val accumulator = new MyAccumulator
    sc.register(accumulator, "hotCategory")

    sourceData.foreach(item => {
      val datas: Array[String] = item.split("_")

      if (datas(6) != "-1") {
        accumulator.add((datas(6), "click"))
      }
      else if (datas(8) != "null") {
        datas(8).split(",").foreach(cid => accumulator.add((cid, "order")))
      }
      else if (datas(10) != "null") {
        datas(10).split(",").foreach(cid => accumulator.add((cid, "pay")))
      }
    })

    // 取出数据
    val accVal: mutable.Map[String, HotCategory] = accumulator.value
    val categories: mutable.Iterable[HotCategory] = accVal.map(_._2)

    val sortData: List[HotCategory] = categories.toList.sortWith(
      (x, y) => {
        if (x.clickCnt > y.clickCnt) {
          true
        }
        else if (x.clickCnt == y.clickCnt) {
          if (x.orderCnt > y.orderCnt) {
            true
          }
          else if (x.orderCnt == y.orderCnt) {
            x.payCnt > y.payCnt
          }
          else {
            false
          }
        }
        else {
          false
        }
      }
    )

    // 输出
    sortData.take(10).foreach(println)

    sc.stop()
  }
}

case class HotCategory( cid:String, var clickCnt : Int, var orderCnt : Int, var payCnt : Int )

/**
 * 自定义累加器
 * 1. 继承AccumulatorV2,定义泛型
 *    IN : ( 品类ID, 行为类型 )
 *    OUT : mutable.Map[String, HotCategory]
 * 2. 重写方法(6)
 */
class MyAccumulator extends AccumulatorV2[(String, String), mutable.Map[String, HotCategory]] {

  private var hcMap = mutable.Map[String, HotCategory]()

  override def isZero: Boolean = hcMap.isEmpty

  override def copy(): AccumulatorV2[(String, String), mutable.Map[String, HotCategory]] = new MyAccumulator()

  override def reset(): Unit = hcMap.clear()

  override def add(v: (String, String)): Unit = {
    val cid: String = v._1
    val actionType: String = v._2

    val category: HotCategory = hcMap.getOrElse(cid, HotCategory(cid, 0, 0, 0))
    if (actionType == "click") {
      category.clickCnt += 1
    }
    else if (actionType == "order") {
      category.orderCnt += 1
    }
    else if (actionType == "pay") {
      category.payCnt += 1
    }
    hcMap.update(cid, category)
  }

  override def merge(other: AccumulatorV2[(String, String), mutable.Map[String, HotCategory]]): Unit = {
    val m1 = this.hcMap
    val m2 = other.value

    m2.foreach{
      case (cid, hc) => {
        val category: HotCategory = m1.getOrElse(cid, HotCategory(cid, 0, 0, 0))

        category.clickCnt += hc.clickCnt
        category.orderCnt += hc.orderCnt
        category.payCnt += hc.payCnt

        m1.update(cid, category)
      }
    }

  }

  override def value: mutable.Map[String, HotCategory] = hcMap
}

2、Top10热门品类中每个品类的Top10活跃Session统计

Scala 复制代码
object Demo05 {
  def main(args: Array[String]): Unit = {
    val sparConf = new SparkConf().setMaster("local[*]").setAppName("Top10")
    val sc = new SparkContext(sparConf)

    // 读取文件数据
    val sourceData: RDD[String] = sc.textFile("datas/user_visit_action.txt")
    
    val splitRdd: RDD[Array[String]] = sourceData.map(item => {
      item.split("_")
    })

    // 数据存入缓存
    splitRdd.cache()

    // Top10热门品类
    val resultRDD: Array[String] = top10Category(splitRdd)

    // 保留点击前10品类ID数据
    val filterActionRDD: RDD[Array[String]] = splitRdd.filter(item => {
      if (item(6) != "-1") {
        resultRDD.contains(item(6))
      } else {
        false
      }
    })

    // 根据品类ID和sessionid进行点击量的统计
    val mapRdd: RDD[((String, String), Int)] = filterActionRDD.map(item => ((item(6), item(2)), 1))
    val reduceRDD: RDD[((String, String), Int)] = mapRdd.reduceByKey(_ + _)

    // 将统计的结果进行结构的转换
    val valueMap: RDD[(String, (String, Int))] = reduceRDD.map { case ((cid, sid), sum) => (cid, (sid, sum)) }

    // 相同的品类进行分组
    val groupRdd: RDD[(String, Iterable[(String, Int)])] = valueMap.groupByKey()

    // 将分组后的数据进行点击量的排序,取前10名
    val resultRdd: RDD[(String, List[(String, Int)])] = groupRdd.mapValues(
      item => {
        item.toList.sortBy(_._2)(Ordering.Int.reverse).take(10)
      }
    )

    // 输出
    resultRdd.foreach(println)

    sc.stop()
  }

  private def top10Category(splitRdd: RDD[Array[String]]) = {
    /**
     * 将数据转换结构
     * 点击: ( 品类ID,( 1, 0, 0 ) )
     * 下单: ( 品类ID,( 0, 1, 0 ) )
     * 支付: ( 品类ID,( 0, 0, 1 ) )
     */
    val flatRDD: RDD[(String, (Int, Int, Int))] = splitRdd.flatMap(item => {
      if (item(6) != "-1") {
        List((item(6), (1, 0, 0)))
      }
      else if (item(8) != "null") {
        item(8).split(",").map((_, (0, 1, 0)))
      }
      else if (item(10) != "null") {
        item(10).split(",").map((_, (0, 0, 1)))
      }
      else {
        Nil
      }
    })

    // 将相同的品类ID的数据进行分组聚合
    val analysisRDD: RDD[(String, (Int, Int, Int))] = flatRDD.reduceByKey(
      (t1, t2) => {
        (t1._1 + t2._1, t1._2 + t2._2, t1._3 + t2._3)
      }
    )

    val resultRDD: Array[(String, (Int, Int, Int))] = analysisRDD.sortBy(_._2, false).take(10)
    resultRDD.map(_._1)
  }
}

3、页面单跳转换率统计

计算页面单跳转化率,比如一个用户在一次 Session 过程中访问的页面路径 3,5,7,9,10,21,那么页面 3 跳到页面 5 叫一次单跳,7-9 也叫一次单跳, 那么单跳转化率就是要统计页面点击的概率。 比如:计算 3-5 的单跳转化率,先获取符合条件的 Session 对于页面 3 的访问次数(PV) 为 A,然后获取符合条件的 Session 中访问了页面 3 又紧接着访问了页面 5 的次数为 B, 那么 B/A 就是 3-5 的页面单跳转化率。

Scala 复制代码
object Demo06 {
  def main(args: Array[String]): Unit = {
    val sparConf = new SparkConf().setMaster("local[*]").setAppName("Top10")
    val sc = new SparkContext(sparConf)

    // 读取文件数据
    val sourceData: RDD[String] = sc.textFile("datas/user_visit_action.txt")

    val actionDataRDD: RDD[UserVisitAction] = sourceData.map(item => {
      val datas: Array[String] = item.split("_")
      UserVisitAction(
        datas(0),
        datas(1).toLong,
        datas(2),
        datas(3).toLong,
        datas(4),
        datas(5),
        datas(6).toLong,
        datas(7).toLong,
        datas(8),
        datas(9),
        datas(10),
        datas(11),
        datas(12).toLong
      )
    })

    // 缓存
    actionDataRDD.cache()

    /**
     * 对指定的页面连续跳转进行统计
     * 例如:页面1跳转页面2  1-2
     * 1-2,2-3,3-4,4-5,5-6,6-7
     */
    val ids = List[Long](1,2,3,4,5,6,7)
    val flowIds: List[(Long, Long)] = ids.zip(ids.tail)

    // 计算页面点击数
    val pageidToCountMap: Map[Long, Long] = actionDataRDD.filter(item => ids.init.contains(item.page_id))
      .map(item => (item.page_id, 1L))
      .reduceByKey(_ + _)
      .collect().toMap

    // 计算对应页面的跳转数
    val sessionRDD: RDD[(String, Iterable[UserVisitAction])] = actionDataRDD.groupBy(_.session_id)
    // 分组后,根据访问时间进行排序(升序)
    val mvRDD: RDD[(String, List[((Long, Long), Int)])] = sessionRDD.mapValues(
      item => {
        val sortList: List[UserVisitAction] = item.toList.sortBy(_.action_time)

        /*
          例如:页面1跳转页面2  1-2
          1-2,2-3,3-4,4-5,5-6,6-7
          使用拉链
         */
        val pageIds: List[Long] = sortList.map(_.page_id)
        val pageFlowIds: List[(Long, Long)] = pageIds.zip(pageIds.tail)

        // 将不合法的页面跳转进行过滤
        pageFlowIds.filter(flowIds.contains(_)).map((_, 1))
      }
    )

    // 数据转换 ((1,2), 1)
    val flatRDD: RDD[((Long, Long), Int)] = mvRDD.map(_._2).flatMap(item => item)
    // ((1,2), sum)
    val reduceRdd: RDD[((Long, Long), Int)] = flatRDD.reduceByKey(_ + _)

    // 计算单跳转换率
    reduceRdd.foreach{
      case ((p1, p2), sum) => {
        val l: Long = pageidToCountMap.getOrElse(p1, 0L)
        println(s"页面${p1}跳转到页面${p2}单跳转换率为:  " + ( sum.toDouble/l ))
      }
    }

    sc.stop()
  }


}
//用户访问动作表
case class UserVisitAction(
  date: String,//用户点击行为的日期
  user_id: Long,//用户的ID
  session_id: String,//Session的ID
  page_id: Long,//某个页面的ID
  action_time: String,//动作的时间点
  search_keyword: String,//用户搜索的关键词
  click_category_id: Long,//某一个商品品类的ID
  click_product_id: Long,//某一个商品的ID
  order_category_ids: String,//一次订单中所有品类的ID集合
  order_product_ids: String,//一次订单中所有商品的ID集合
  pay_category_ids: String,//一次支付中所有品类的ID集合
  pay_product_ids: String,//一次支付中所有商品的ID集合
  city_id: Long //城市 id
)

4、将案例进行工程化修改

(1)util工具类

1)获取spark上下文对象 EnvUtil

Scala 复制代码
object EnvUtil {
  private val scLocal = new ThreadLocal[SparkContext]()

  // 添加全局上下文
  def put( sc : SparkContext ): Unit = {
    scLocal.set(sc)
  }

  // 获取上下文
  def take(): SparkContext = {
    scLocal.get()
  }

  // 移除上下文
  def clear(): Unit = {
    scLocal.remove()
  }
}

2)自定义累加器

Scala 复制代码
/**
 * 自定义累加器
 * 1. 继承AccumulatorV2,定义泛型
 *    IN : ( 品类ID, 行为类型 )
 *    OUT : mutable.Map[String, HotCategory]
 * 2. 重写方法(6)
 */
class MyAccumulator extends AccumulatorV2[(Long, String), mutable.Map[Long, HotCategory]] {

  private var hcMap = mutable.Map[Long, HotCategory]()

  override def isZero: Boolean = hcMap.isEmpty

  override def copy(): AccumulatorV2[(Long, String), mutable.Map[Long, HotCategory]] = new MyAccumulator()

  override def reset(): Unit = hcMap.clear()

  override def add(v: (Long, String)): Unit = {
    val cid: Long = v._1
    val actionType: String = v._2

    val category: HotCategory = hcMap.getOrElse(cid, HotCategory(cid, 0, 0, 0))
    if (actionType == "click") {
      category.clickCnt += 1
    }
    else if (actionType == "order") {
      category.orderCnt += 1
    }
    else if (actionType == "pay") {
      category.payCnt += 1
    }
    hcMap.update(cid, category)
  }

  override def merge(other: AccumulatorV2[(Long, String), mutable.Map[Long, HotCategory]]): Unit = {
    val m1 = this.hcMap
    val m2 = other.value

    m2.foreach{
      case (cid, hc) => {
        val category: HotCategory = m1.getOrElse(cid, HotCategory(cid, 0, 0, 0))

        category.clickCnt += hc.clickCnt
        category.orderCnt += hc.orderCnt
        category.payCnt += hc.payCnt

        m1.update(cid, category)
      }
    }

  }

  override def value: mutable.Map[Long, HotCategory] = hcMap
}

(2)实体类

1)文件映射类

Scala 复制代码
case class UserVisitAction (
  date: String,//用户点击行为的日期
  user_id: Long,//用户的ID
  session_id: String,//Session的ID
  page_id: Long,//某个页面的ID
  action_time: String,//动作的时间点
  search_keyword: String,//用户搜索的关键词
  click_category_id: Long,//某一个商品品类的ID
  click_product_id: Long,//某一个商品的ID
  order_category_ids: String,//一次订单中所有品类的ID集合
  order_product_ids: String,//一次订单中所有商品的ID集合
  pay_category_ids: String,//一次支付中所有品类的ID集合
  pay_product_ids: String,//一次支付中所有商品的ID集合
  city_id: Long //城市 id
)

2)自定义结果类

Scala 复制代码
case class HotCategory( cid:Long, var clickCnt : Int, var orderCnt : Int, var payCnt : Int )

(3)公共特质

1)TApplication

Scala 复制代码
trait TApplication {
  def start(master: String = "local[*]", appName: String = "Application")(op: => Unit): Unit = {
    // 建立连接
    val sparConf = new SparkConf().setMaster("local").setAppName("Acc")
    val sc = new SparkContext(sparConf)
    EnvUtil.put(sc)
    try {
      // 执行函数
      op
    } catch {
      case ex => println(ex.getMessage)
    }

    // 关闭连接
    sc.stop()
    EnvUtil.clear()
  }
}

2)TProjectController

Scala 复制代码
trait TProjectController {
  def dispatch(): Unit
}

3)TProjectService

Scala 复制代码
trait TProjectService {
  def top10Category(): (List[HotCategory], RDD[UserVisitAction])

  def top10Session(): RDD[(Long, List[(String, Int)])]

  def pageConversionRate(): Unit
}

4)TProjectDao

Scala 复制代码
trait TProjectDao {
  def dataAnalysis(path: String):RDD[String] = {
    EnvUtil.take().textFile(path)
  }
}

(4)启动对象

Scala 复制代码
object Application extends App with TApplication{
  // 启动程序
  start(){
    val controller = new ProjectController()
    controller.dispatch()
  }
}

(5)控制层

Scala 复制代码
// 控制层
class ProjectController extends TProjectController{

  private val projectService = new ProjectService()

  override def dispatch(): Unit = {
//    val tuples: (List[HotCategory], RDD[UserVisitAction]) = projectService.top10Category()
//    tuples._1.foreach(println)
//    println("==========")
//    val value: RDD[(Long, List[(String, Int)])] = projectService.top10Session()
//    value.foreach(println)

    projectService.pageConversionRate()
  }
}

(6)服务层

Scala 复制代码
// 服务层
class ProjectService extends TProjectService{

  private val dao = new ProjectDao()

  // Top10热门品类
  override def top10Category(): (List[HotCategory], RDD[UserVisitAction]) = {
    val sourceData: RDD[UserVisitAction] = getData()

    // 注册累加器
    val accumulator = new MyAccumulator
    EnvUtil.take().register(accumulator, "hotCategory")

    sourceData.foreach(datas => {
      if (datas.click_category_id != -1) {
        accumulator.add((datas.click_category_id, "click"))
      }
      else if (datas.order_category_ids != "null") {
        datas.order_category_ids.split(",").foreach(cid => accumulator.add((cid.toLong, "order")))
      }
      else if (datas.pay_category_ids != "null") {
        datas.pay_category_ids.split(",").foreach(cid => accumulator.add((cid.toLong, "pay")))
      }
    })

    // 取出数据
    val accVal: mutable.Map[Long, HotCategory] = accumulator.value
    val categories: mutable.Iterable[HotCategory] = accVal.map(_._2)

    val sortData: List[HotCategory] = categories.toList.sortWith(
      (x, y) => {
        if (x.clickCnt > y.clickCnt) {
          true
        }
        else if (x.clickCnt == y.clickCnt) {
          if (x.orderCnt > y.orderCnt) {
            true
          }
          else if (x.orderCnt == y.orderCnt) {
            x.payCnt > y.payCnt
          }
          else {
            false
          }
        }
        else {
          false
        }
      }
    )

    // 输出
    (sortData.take(10), sourceData)
  }

  // Top10热门品类中每个品类的Top10活跃Session统计
  override def top10Session(): RDD[(Long, List[(String, Int)])] = {
    val sourceData: (List[HotCategory], RDD[UserVisitAction]) = top10Category()
    val resultRDD: List[HotCategory] = sourceData._1
    val splitRdd: RDD[UserVisitAction] = sourceData._2

    val cidList: List[Long] = resultRDD.map((_.cid))

    // 保留点击前10品类ID数据
    val filterActionRDD: RDD[UserVisitAction] = splitRdd.filter(item => {
      if (item.click_category_id != -1) {
        cidList.contains(item.click_category_id)
      } else {
        false
      }
    })

    // 根据品类ID和sessionid进行点击量的统计
    val mapRdd: RDD[((Long, String), Int)] = filterActionRDD.map(item => ((item.click_category_id, item.session_id), 1))
    val reduceRDD: RDD[((Long, String), Int)] = mapRdd.reduceByKey(_ + _)

    // 将统计的结果进行结构的转换
    val valueMap: RDD[(Long, (String, Int))] = reduceRDD.map { case ((cid, sid), sum) => (cid, (sid, sum)) }

    // 相同的品类进行分组
    val groupRdd: RDD[(Long, Iterable[(String, Int)])] = valueMap.groupByKey()

    // 将分组后的数据进行点击量的排序,取前10名
    groupRdd.mapValues(
      item => {
        item.toList.sortBy(_._2)(Ordering.Int.reverse).take(10)
      }
    )
  }

  // 页面单跳转换率统计
  override def pageConversionRate(): Unit = {
    val actionDataRDD: RDD[UserVisitAction] = getData()
    /**
     * 对指定的页面连续跳转进行统计
     * 例如:页面1跳转页面2  1-2
     * 1-2,2-3,3-4,4-5,5-6,6-7
     */
    val ids = List[Long](1,2,3,4,5,6,7)
    val flowIds: List[(Long, Long)] = ids.zip(ids.tail)

    // 计算页面点击数
    val pageidToCountMap: Map[Long, Long] = actionDataRDD.filter(item => ids.init.contains(item.page_id))
      .map(item => (item.page_id, 1L))
      .reduceByKey(_ + _)
      .collect().toMap

    // 计算对应页面的跳转数
    val sessionRDD: RDD[(String, Iterable[UserVisitAction])] = actionDataRDD.groupBy(_.session_id)
    // 分组后,根据访问时间进行排序(升序)
    val mvRDD: RDD[(String, List[((Long, Long), Int)])] = sessionRDD.mapValues(
      item => {
        val sortList: List[UserVisitAction] = item.toList.sortBy(_.action_time)

        /*
          例如:页面1跳转页面2  1-2
          1-2,2-3,3-4,4-5,5-6,6-7
          使用拉链
         */
        val pageIds: List[Long] = sortList.map(_.page_id)
        val pageFlowIds: List[(Long, Long)] = pageIds.zip(pageIds.tail)

        // 将不合法的页面跳转进行过滤
        pageFlowIds.filter(flowIds.contains(_)).map((_, 1))
      }
    )

    // 数据转换 ((1,2), 1)
    val flatRDD: RDD[((Long, Long), Int)] = mvRDD.map(_._2).flatMap(item => item)
    // ((1,2), sum)
    val reduceRdd: RDD[((Long, Long), Int)] = flatRDD.reduceByKey(_ + _)

    // 计算单跳转换率
    reduceRdd.foreach{
      case ((p1, p2), sum) => {
        val l: Long = pageidToCountMap.getOrElse(p1, 0L)
        println(s"页面${p1}跳转到页面${p2}单跳转换率为:  " + ( sum.toDouble/l ))
      }
    }
  }

  // 数据封装
  def getData(): RDD[UserVisitAction] = {
    val sourceData: RDD[String] = dao.dataAnalysis("datas/user_visit_action.txt")
    val splitRdd: RDD[UserVisitAction] = sourceData.map(item => {
      val datas: Array[String] = item.split("_")
      UserVisitAction(
        datas(0),
        datas(1).toLong,
        datas(2),
        datas(3).toLong,
        datas(4),
        datas(5),
        datas(6).toLong,
        datas(7).toLong,
        datas(8),
        datas(9),
        datas(10),
        datas(11),
        datas(12).toLong
      )
    })
    // 数据存入缓存
    splitRdd.cache()
  }

}

(7)数据层

Scala 复制代码
class ProjectDao extends TProjectDao{

}

(8)测试

Scala 复制代码
  override def dispatch(): Unit = {
//    val tuples: (List[HotCategory], RDD[UserVisitAction]) = projectService.top10Category()
//    tuples._1.foreach(println)
//    println("==========")
//    val value: RDD[(Long, List[(String, Int)])] = projectService.top10Session()
//    value.foreach(println)

    projectService.pageConversionRate()
  }

​​​​​​​

相关推荐
敖正炀1 小时前
CAP 定理、BASE 理论与一致性模型深度
分布式
逸Y 仙X1 小时前
文章二十九:ElasticSearch分桶聚合
android·大数据·elasticsearch·搜索引擎·全文检索
财经资讯数据_灵砚智能2 小时前
基于全球经济类多源新闻的NLP情感分析与数据可视化(日间)2026年5月16日
大数据·人工智能·python·信息可视化·自然语言处理
AI周红伟2 小时前
All in Token,移动,电信和联通,华为,阿里,百度,字节,卖Token Plan,卖算力时代结束,卖智力时代来了:Token经济万亿赛道全景解码
大数据·人工智能·机器学习·百度·华为·copilot·openclaw
Volunteer Technology2 小时前
MapReduce 介绍
大数据·mapreduce
workflower2 小时前
AI能源智慧生产与绿色开发核心场景
大数据·人工智能·设计模式·机器人·软件工程·能源
幻奏岚音2 小时前
AI时代生产力变革与高效使用
大数据·人工智能·深度学习
hahdbk2 小时前
口碑好的医疗设备外观设计选哪家
大数据·人工智能·python
团象科技2 小时前
别盲目布局全球化,先理清海外云服务器能覆盖的业务边界
大数据·服务器·人工智能