一:算子统计
flatmap
map
mapValues
一:Spark简介
-
park和Hadoop的根本差异是多个作业之间的数据通信问题 : Spark多个作业之间数据通信是基于内存,而Hadoop是基于磁盘。
Spark的缓存机制比HDFS的缓存机制高效。
二:wordCount()分析 (flatmap() 与 map())
flatmap与map我的理解:
读取数据是一行一行读的,(如果每一行的数据源是 (Hello World Hello Spark)
)
补充:
用任何一个框架都需要一个环境对象 sc就是SparkContext的环境对象(环境配置对象参数)
val spakConf = new SparkConf().setMaster("local[*]").setAppName("WordCount").set("spark.testing.memory", "2147480000")
val sc = new SparkContext(spakConf)
flatMap()将整体拆分成为个体。
val wordGroup: RDD[(String, Iterable[(String, Int)])] = fileDataRDD.flatMap(_.split(" "))
.map((_, 1))
.groupBy(_._1)
wordGroup.mapValues(
list => {
list.map(_._2)
list.map(_._2).sum
list.map(_._2).reduce(_+_)
list.map(_._2).size
}
)
val wordCount: RDD[(String, Int)] = wordGroup.mapValues(_.map(_._2).reduce(_ + _))
wordGroup.collect().foreach(println)
// flatMap, map, groupBy, mapValues, collect方法都是Spark框架提供的方法,而不是Scala集合的方法
// 为了降低学习的成本,使用的难度,让Spark的API和Scala方法变得类似。
val words = fileDatas.flatMap(_.split(" "))
val wordToOne = words.map((_, 1))
// reduceByKey : 相同的key,对value进行两两聚合
// (word, 1), (word, 1), (word, 1)
// (word, (1,1,1,1,1))
val wordCount = wordToOne.reduceByKey(_+_)
wordCount.collect().foreach(println)
三:spark环境
四:Spark运行架构
YARN中ApplicationMaster能做到资源和计算的解耦合,Driver和Executor使用的计算框架是Spark,因此也可以使用其它的框架,而YARN只做资源的调度使用
Spark应用程序提交到Yarn环境中执行的时候,一般会有两种部署执行的方式:Client和Cluster。两种模式主要区别在于:Driver程序的运行节点位置。
五:Spark核心编程
首先,装饰者模式只会进行功能组合,不会执行,另外RDD的装饰者模式与IO的装饰者模式的区别在于,RDD不保留数据,只对数据进行处理,而IO中有缓冲区可以保留数据。
5.1:执行原理
六:内存磁盘如何分区及存储原理
6.1:算子的创建:
因为简明知意,makeRDD用的比较多
分区数量是如何计算的(只要牵涉的到分区就会出现数据倾斜现象)
七:算子来啦(转换与行为算子)
转换算子就是将旧的RDD转换成为新的RDD,因为要组合功能。
flatMap()保留,map()不保留
不在一个分区是无法进行比较的
map() VS mapPartitions()
mapPartitions()是批量处理的 因此入参为迭代器 出参也为迭代器,map处理一个返回一个,mapPartitions处理一批返回一批
迭代器Iterator也是集合
7.1:groupBy()
groupBy()引出shuffle
shuffle一定会落盘,因为RDD不保留数据,因此,在shuffle阶段,一定会存在Write(shuffle左边)和read(shuffle右边)
所有含有shuffle的算子都有改变分区的能力
因为隐式转换所以字符串可以直接()写下标
7.2:补充零拷贝(NIO实现)页缓存
7.3:flatMap()
7.4:glom
7.5:filter
val fileRDD = sc.textFile("data/apache.log")
// filter算子返回的结果为按照规则保留的数据本身
fileRDD.filter(
line => {
val datas = line.split(" ")
val time = datas(3)
time.startsWith("17/05/2015")
}
).map(
line => {
val datas = line.split(" ")
datas(6)
}
).collect.foreach(println)
7.6: sample
7.7: distinct
scala使用的是单点集合(缺点,单点的资源有限),而RDD使用的是集群去重
7.8:coalesce(缩小分区) 与 repartition(扩大分区)
7.9:sortBy()
八:双值类型数据集的算子(交集并集差集拉链)
单Value(单一数据集)数据操作,双Value(多个数据集)数据操作
九:Key - Value类型算子(隐式转换rddToPairRDDFunction)
9.1:partitionBy()
repartition : 重分区(分区数量)
partitionBy : 重分区(数据的位置 数据进入到哪个分区), 数据路由(Hash定位),分区器 如果想要让数据重新进行分区,那么需要传递分区规则对象(分区器)
Spark中默认常用的分区器:RangePartitioner(要求数据必须能排序) & HashPartitioner(默认)
思考一个问题:如果重分区的分区器和当前RDD的分区器一样怎么办?
此时不做任何处理,还是当前分区,不需要重新shuffle
9.2:reduceByKey groupByKey
TODO reduceByKey算子用于将数据集中相同的key的value数据聚合在一起实现两两聚合
spark的算子有很多是以byKey结尾的,就是说将K-V独立使用
TODO reduceByKey算子可以实现 WordCount
9.3:aggregateByKey foldByKey
需求 数据集中相同的Key的分区内取最大值,分区间求和
reduceByKey groupByKey只考虑key不考虑分区而reduceByKey 在逻辑上,
分区内和分区间是一样的都是聚合 因此reduceByKey groupByKey都不能处理该需求。
需求 如果分区内和分区间的计算规则不一定相同时,Spark提供了一个算子来实现。aggregateByKey
aggregateByKey算子存在函数柯里化,有多个参数列表
第一个参数列表中有一个参数
第一个参数表示计算初始值
第二个参数列表中有二个参数
第一个参数表示 : 分区内计算规则
第二个参数表示 : 分区间计算规则
9.4:combineByKey
reduceByKey,aggregateByKey ,foldByKey, combineByKey本质区别
9.5:sortByKey
9.6:Join leftOuterJoin rightOuterJoin fullOuterJoin
9.7 : cogroup
join算子体现的是数据的关系
join算子将两个数据集中相同的key的数据,连接在一起
zip是将两个数据集位置相同的数据,连接起来与key无关
join 可能存在笛卡尔积 也 存在shuffle()
笛卡尔积 在落盘的时候 如果存在大量相同的key的时候数据量会激增
如果能够使用其它算子实现的功能,那么不推荐使用join
rdd1.join(rdd2).collect().foreach(println)
rdd1.leftOuterJoin(rdd2).collect().foreach(println) rdd1.rightOuterJoin(rdd2).collect().foreach(println) rdd1.fullOuterJoin(rdd2).collect().foreach(println)
cogroup = connect(多个数据集) + group(单一数据集)
9.6:案例实操
方式一:
// 统计出每一个省份每个广告被点击数量排行的Top3
val fileDatas: RDD[String] = sc.textFile("data/agent.log")
val groupDatas: RDD[(String, Iterable[String])] = fileDatas.groupBy(
line => {
val datas: Array[String] = line.split(" ")
datas(1)
}
)
val top3: RDD[(String, List[(String, Int)])] = groupDatas.mapValues(
//数据集是List(line, line, line)
list => {
//数据集是line
val adToCount: Map[String, Int] = list.map(
line => {
val datas: Array[String] = line.split(" ")
(datas(4), 1)
}
//数据集是List((省份,广告)) 按照省份进行分组统计
).groupBy(_._1).mapValues(_.size)
//Map[String, Int] [广告,点击次数]
// 在 groupDatas.mapValues( 的架子中都是对value进行处理的
//元组天生的排序 数据类型[广告,点击次数] 前三条
adToCount.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
}
)
top3.collect().foreach(println)
sc.stop()
}
方式二:
val fileDatas: RDD[String] = sc.textFile("data/agent.log")
// 1. 将广告进行统计分析 (word, cnt)
// ((省份1,广告1), 1),((省份2,广告2), 1)((省份1,广告1), 1)
// ((省份1,广告1), sum),((省份1,广告2), sum)
val reduceDatas: RDD[((String, String), Int)] = fileDatas.map(
line => {
val datas: Array[String] = line.split(" ")
((datas(1), datas(4)), 1)
}
).reduceByKey(_ + _)
// 1.5 将统计结果进行格式转换
// (省份1,(广告1, sum)),(省份1,(广告2, sum))
val mapDatas: RDD[(String, (String, Int))] = reduceDatas.map {
case ((prv, ad), sum) => {
(prv, (ad, sum))
}
}
// 2. 将统计结果按照省份进行分组
// (省份1, (广告1, sum),(广告2, sum))
val prvGroupDatas: RDD[(String, Iterable[(String, Int)])] = mapDatas.groupByKey()
// 3. 将分组后的数据按照点击数量进行排序(降序),取前3名
val top3: RDD[(String, List[(String, Int)])] = prvGroupDatas.mapValues(
iter => {
iter.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
}
)
top3.collect().foreach(println)
十:action行动算子(是对结果进行处理的)
TODO : 所谓的行动(Action)算子,其实就是RDD用于触发作业执行的方法,类似于IO中的read方法
转换算子的返回值为RDD 行动算子的返回值为具体的结果
行动算子和作业的关系 :1 对 1
10.2:reduce
reduce算子可以触发作业的执行,分区内先两两计算,分区间再两两计算
分区内计算都是在Executor。
分区间计算都是在Driver
10.3:count first take takeOrdered
10.4: aggregate fold
10.5: countByKey countByValue
10.5: save相关的算子
rdd.saveAsTextFile("output1")
rdd.saveAsObjectFile("output2")
rdd.saveAsSequenceFile("output3")
10.5: collect foreach
10.6: 序列化闭包检测
一个函数使用了外部的数据,并且改变了这个数据的生命周期 将数据包含到函数内部,形成了一个闭合的环境,这个环境称之为闭包环境,简称闭包
Spark在运行作业之前,会判断程序逻辑中是否包含闭包
如果存在闭包操作,那么需要检测闭包中使用的数据是否能够序列化,如果不能,那么发生错误
这个操作称之为闭包检测
十一:RDD阶段的划分
前一个阶段不执行完,后面的阶段是无法执行的。
十二:依赖关系和血缘关系(宽依赖窄依赖)
十三:持久化
(持久化只针对于当前应用程序,资源使用完之后释放资源,不会给其它节点使用。因此诞生了检查点)
将计算结果缓存起来。
数据的持久化默认将数据保存到内存中,重复使用
wordToOne.cache()
持久化操作也可以保存到磁盘中
wordToOne.persist()
保存级别设置
十三:RDD CheckPoint检查点
java中的finalize方法是Object类中提供的一个方法,在GC准备释放对象所占用的内存空间之前,它将首先调用finalize()方法。
持久化只针对于当前应用程序,资源使用完之后释放资源,不会给其它节点使用。
持久化只针对于当前应用程序
跨应用程序的数据共享需要采用 检查点机制
设置的方式
① :sc.setCheckpointDir("cp") 该检查点的目录一般为分布式目录以便各个节点使用
②:wordToOne.checkpoint()
持久化操作会在血缘关系中增加依赖
检查点一般会将数据保存到分布式文件系统中(HDFS)(新的数据源)
检查点会切断血缘即减少血缘依赖,因为可以把HDFS中相对可靠的数据作为新的数据源。
十四:自定义分区 RDD文件读取与保存
Hash分区:对于给定的key,计算其hashCode,并除以分区个数取余
Range分区:将一定范围内的数据映射到一个分区中,尽量保证每个分区数据均匀,而且分区间有序
十四:累加器(数据模型)(功能比较单一没有shuffle性能相对高)
累加器的现象
分析:
如果数据和RDD有关系,那么可以从Driver端发送到Executor端执行
如果执行完毕,那么可以将结果拉回到Driver端
如果数据和RDD没有关系,因为计算中存在闭包操作,所以RDD会将数据发送到Executor
但是无法将数据从Executor端拉回到Driver端。
如果想要将和RDD无关的数据拉回到Driver端,RDD模型处理不了,需要采用新的数据模型——累加器
doubleAccumulator
collectionAccumulator 累加器
def main(args: Array[String]): Unit = {
// TODO Spark 环境
val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(conf)
// TODO 1. 创建累加器
val wordCountAcc = new MyAcculumator
// TODO 2. 将累加器注册到Spark中
sc.register(wordCountAcc, "WordCount")
val rdd = sc.makeRDD(
List(
"Hello",
"Hello",
"Hello",
"World",
"Hello",
),2
)
rdd.foreach(
word => {
// TODO 3. 使用累加器
wordCountAcc.add(word)
}
)
// TODO 4. 获取累加器的结果
println(wordCountAcc.value)
sc.stop()
}
// TODO 自定义数据累加器(WordCount)
// 1. 继承AccumulatorV2
// 2. 定义数据的泛型
// IN : String
// OUT : mutable.Map[String, Int]
// 3. 重写方法(3(计算) + 3(状态) = 6)
class MyAcculumator extends AccumulatorV2[String, mutable.Map[String, Int]]{
private val wordCountMap = mutable.Map[String, Int]()
// TODO 判断当前累加器是否为初始状态
override def isZero: Boolean = {
wordCountMap.isEmpty
}
// TODO 复制累加器
override def copy(): AccumulatorV2[String, mutable.Map[String, Int]] = {
new MyAcculumator()
}
// TODO 重置累加器
override def reset(): Unit = {
wordCountMap.clear()
}
// TODO 将外部的数据增加到累加器中
override def add(word: String): Unit = {
val oldCnt: Int = wordCountMap.getOrElse(word, 0)
wordCountMap.update(word, oldCnt + 1)
}
// TODO 将多个累加器进行合并
override def merge(other: AccumulatorV2[String, mutable.Map[String, Int]]): Unit = {
other.value.foreach {
case (word, cnt) => {
val oldCnt: Int = wordCountMap.getOrElse(word, 0)
wordCountMap.update(word, oldCnt + cnt)
}
}
}
// TODO 获取累加器的结果
override def value: mutable.Map[String, Int] = {
wordCountMap
}
}
十五:Join现象引出广播变量
def main(args: Array[String]): Unit = {
// TODO Spark 环境
val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(conf)
val rdd1 = sc.makeRDD(
List(
("a", 1),
("b", 2),
)
)
val rdd2 = sc.makeRDD(
List(
("a", 3),
("b", 4),
)
)
// ("a", 1)
// ( a,(1,3))
//rdd1.join(rdd2).collect().foreach(println)
// 计算的对象是以Task为单位进行数据发送的,所以可能会导致大量的数据冗余
// 所以如果计算对象占用比较大的资源,性能会急剧下降
// 为了提供数据访问效率,可以将数据发送给Executor,而不是发给Task。
// RDD数据模型只能给Task发。只能采用其他的数据模型:TODO 广播变量
val map = mutable.Map[String, Int](
("a", 3),
("b", 4)
)
// TODO 使用广播变量
val bc: Broadcast[mutable.Map[String, Int]] = sc.broadcast(map)
rdd1.map {
case (k, v1) => {
// TODO 获取广播变量的值
var v2 = bc.value.getOrElse(k, 0)
(k, (v1, v2))
}
}.collect.foreach(println)
sc.stop()
}
计算的对象是以Task为单位进行数据发送的,所以可能会导致大量的数据冗余 且 如果计算对象占用比较大的资源,性能会急剧下降
为了提供数据访问效率,可以将数据发送给Executor,而不是发给Task。
但是RDD数据模型只能给Task发。只能采用其他的数据模型: 广播变量(可以将数据发送给Executor 使得同一个节点上的Task可以共享一份资源,并且如果有Executor节点与Driver通信比较慢的时候 可以从就近的Executor上获得需要的资源)
十五:SparkSQL(Dataframe DataSet RDD 转换)
Spark + Hive => Shark
Spark SQL是Spark用于结构化数据(structured data)处理的Spark模块。
1.2 SparkSQL特点:
无缝的整合了 SQL 查询和 Spark 编程
SparkSQL可以简化RDD的开发,提高开发效率
15.1:Dataframe 与 DataSet 与 RDD概述
SQL语言本身没有类型
工厂模式生产的对象是有相同的功能的,因为从同一条生产线下来的。
Builder模式要生产某个对象需要经过复杂的过程,但是builder模式会把这些过程顺序确定下来。
Scala中的构造方法有两种,主构造方法和辅助构造方法,辅助构造方法的函数名一定为this。
15.2:Dataframe 与 DataSet 与 RDD
Dataframe : 是SparkSQL中的核心数据模型,是弱类型的模型,不关心数据的类型操作。
因为SQL的局限性,因此在处理数据的时候,Dataframe提供了一种DSL语法(面向对象的语法)
SparkSQL 与 RDD的关系与转换
SparkSQL模块等同于是对SparkCore一个封装,专门用于结构化数据处理的场景
Dataset : 就是使用面向对象的方式操作数据, 类,属性 => 结构 就是
在Dataframe的基础上,增加了类型信息
SparkSQL中存在大量的隐式转换操作,但是一般会导入使用 import时,使用的spark并不是包名,是环境对象的名称。
Dataframe 与 DataSet的区别?
Dataframe是特定类型的DataSet[Row]
说明DataSet兼容Dataframe()是将来用的比较多的
注意:如果从内存中获取数据,spark可以知道数据类型具体是什么。如果是数字,默认作为Int处理;但是从文件中读取的数字,不能确定是什么类型,所以用bigint接收,可以和Long类型转换,但是和Int不能进行转换
SparkSQL技术选型:
15.2:UDF 与 UDAF
package com.atguigu.bigdata.spark.sql01
import org.apache.spark.sql.expressions.Aggregator
import org.apache.spark.sql.{Dataframe, Encoder, Encoders, SparkSession, functions}
object SparkSQL09_Dataframe_UDAF {
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
df.createOrReplaceTempView("user")
// TODO UDAF使用时,需要采用特殊的方式实现自定义操作
// TODO 强类型操作不能应用于弱类型操作,如果想用,那么需要转换functions.udaf
// Spark3.0以后,可以通过转换,让强类型应用在弱类型操作
spark.udf.register("avgAge",functions.udaf(new AvgAgeUDF()))
// TODO : UDF
// 自定义的函数在SQL文使用
spark.sql("select avgAge(age) from user").show()
spark.stop()
}
case class CalcBuffer(var total : Long , var cnt : Long)
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 1. 继承org.apache.spark.sql.expressions.Aggregator
// 2. 定义泛型
// IN : Long
// BUFF : CalcBuffer
// OUT : Long
// 3. 重写方法 (4 + 2(固定) = 6)
class AvgAgeUDF extends Aggregator[Long,CalcBuffer,Long]{
// TODO 缓冲区的初始化
override def zero: CalcBuffer = {
CalcBuffer(0l,0l)
}
// TODO 将输入的数据和缓冲区的数据进行聚合操作
override def reduce(buff: CalcBuffer, a: Long): CalcBuffer ={
buff.total += a
buff.cnt += 1
buff
}
// TODO 多个缓冲区的合并操作
override def merge(b1: CalcBuffer, b2: CalcBuffer): CalcBuffer = {
b1.total += b2.total
b1.cnt += b2.cnt
b1
}
// TODO 计算数据
override def finish(reduction: CalcBuffer): Long = {
reduction.total / reduction.cnt
}
override def bufferEncoder: Encoder[CalcBuffer] = Encoders.product
override def outputEncoder: Encoder[Long] = Encoders.scalaLong
}
}
UDAF老版本:
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
df.createOrReplaceTempView("user")
// TODO UDAF使用时,需要采用特殊的方式实现自定义操作
// TODO 强类型操作不能应用于弱类型操作,如果想用,那么需要转换functions.udaf
// Spark3.0以后,可以通过转换,让强类型应用在弱类型操作
spark.udf.register("avgAge",new AvgAgeUDAF())
// TODO : UDF
// 自定义的函数在SQL文使用
spark.sql("select avgAge(age) from user").show()
spark.stop()
}
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 使用早期Spark版本的UDAF(弱类型)
// 1. 继承UserDefinedAggregateFunction
// 2. 重写方法(8)
class AvgAgeUDAF extends UserDefinedAggregateFunction{
// TODO 输入数据的结构
override def inputSchema: StructType = {
StructType(
Array(
StructField("age",LongType)
)
)
}
// TODO 缓冲区数据的结构
override def bufferSchema: StructType = {
StructType(
Array(
StructField("total",LongType),
StructField("cnt",LongType)
)
)
}
// TODO 输出数据的类型
override def dataType: DataType = LongType
// TODO 计算稳定性
override def deterministic: Boolean = true
// TODO 缓冲区的初始化
override def initialize(buffer: MutableAggregationBuffer): Unit = {
buffer.update(0,0l)
buffer.update(1,0l)
}
// TODO 将输入的数据和缓冲区的数据进行聚合操作
override def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
buffer.update(0,buffer.getLong(0) + input.getLong(0))
buffer.update(1,buffer.getLong(1)+1)
}
// TODO 多个缓冲区的合并操作
override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
buffer1.update(0,buffer1.getLong(0) + buffer2.getLong(0))
buffer1.update(1,buffer1.getLong(1) + buffer2.getLong(1))
}
override def evaluate(buffer: Row): Any = {
buffer.getLong(0) / buffer.getLong(1)
}
}
UDAF_Class老版本:
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
// TODO Spark3.0版本前,强类型的UDAF不能应用于弱类型的SQL中
import spark.implicits._
val udaf: AvgAgeUDAF = new AvgAgeUDAF
val ds: Dataset[User] = df.as[User]
ds.select(udaf.toColumn).show()
spark.stop()
}
case class User(id : Long , name : String , age : Long)
case class CalacBuffer(var total : Long , var cnt : Long)
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 使用早期Spark版本的UDAF(强类型)
// 1. 继承Aggregator
// 2. 定义泛型
// IN : User
// BUFF : CalcBuffer
// OUT : Long
// 3. 重写方法(6)
class AvgAgeUDAF extends Aggregator[User,CalacBuffer,Long]{
override def zero: CalacBuffer = {
CalacBuffer(0l,0L)
}
override def reduce(b: CalacBuffer, user: User): CalacBuffer = {
b.total += user.age
b.cnt += 1
b
}
override def merge(b1: CalacBuffer, b2: CalacBuffer): CalacBuffer = {
b1.total += b2.total
b1.cnt += b2.cnt
b1
}
override def finish(buffer: CalacBuffer): Long = {
buffer.total / buffer.cnt
}
override def bufferEncoder: Encoder[CalacBuffer] = Encoders.product
override def outputEncoder: Encoder[Long] = Encoders.scalaLong
}
15.3:通用读取方式和默认读取方式
user.json is not a Parquet file. expected magic number at tail [80, 65, 82, 49] but found [32, 53, 48, 125]
SparkSQL通用读取的方法,读取的文件为Parquet格式(列式存储格式)
val df = spark.read.load("data/users.parquet")
如果想要读取其他格式的文件也可以,需要额外增加参数进行解析
df.write.mode("append").format("json").save("output")
sparkSQL基于SparkCore
SparkCore读取数据是依赖于Hadoop
Hadoop读取数据是按行读取
SparkSQL读取JSON格式的文件,要求每一行数据必须符合JSON格式
读入数据:
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
//方式1:通用的load方法读取
spark.read.format("jdbc")
.option("url", "jdbc:mysql://linux1:3306/spark-sql")
.option("driver", "com.mysql.jdbc.Driver")
.option("user", "root")
.option("password", "123123")
.option("dbtable", "user")
.load().show
//方式2:通用的load方法读取 参数另一种形式
spark.read.format("jdbc")
.options(Map("url"->"jdbc:mysql://linux1:3306/spark-sql?user=root&password=123123",
"dbtable"->"user","driver"->"com.mysql.jdbc.Driver")).load().show
//方式3:使用jdbc方法读取
val props: Properties = new Properties()
props.setProperty("user", "root")
props.setProperty("password", "123123")
val df: Dataframe = spark.read.jdbc("jdbc:mysql://linux1:3306/spark-sql", "user", props)
df.show
//释放资源
spark.stop()
写入数据:
case class User2(name: String, age: Long)
。。。
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
val rdd: RDD[User2] = spark.sparkContext.makeRDD(List(User2("lisi", 20), User2("zs", 30)))
val ds: Dataset[User2] = rdd.toDS
//方式1:通用的方式 format指定写出类型
//方式2:通过jdbc方法
val props: Properties = new Properties()
props.setProperty("user", "root")
props.setProperty("password", "123123")
ds.write.mode(SaveMode.Append).jdbc("jdbc:mysql://linux1:3306/spark-sql", "user", props)
//释放资源
spark.stop()
十六:SparkStreaming(数据模型是离散流DStrem)
实时 离线 批量 流式
Spark是一个离线的批量数据处理的框架
SparkStreaming是一个准实时,微批次的处理框架
16.1:SparkStreaming知识点
Discretized Stream(离散流)是Spark Streaming的基础抽象,代表持续性的数据流和经过各种Spark原语操作后的结果数据流。在内部实现上,DStream是一系列连续的RDD来表示。每个RDD含有一段时间间隔内的数据。
16.2:DStream
blockingQueue队列:阻塞式队列,有反压机制,可以避免数据挤压。
采集数据的方式:DStream监控目录变化,将RDD放入到离散化流DStream中,自定义数据源。
object SparkStreaming05_DIY {
def main(args: Array[String]): Unit = {
//TODO SparkStreaming 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[*]")
.setAppName("Streaming")
.set("spark.testing.memory", "471859200")
//StreamingContext独享的第二个构造参数表示数据采集周期
// val ssc: StreamingContext = new StreamingContext(conf,new Duration(3 * 1000))
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
val uuidDS: ReceiverInputDStream[String] = ssc.receiverStream(new MyReceiver)
uuidDS.print()
//启动采集器
ssc.start()
// 等待采集器结束
ssc.awaitTermination()
}
class MyReceiver extends Receiver[String](StorageLevel.MEMORY_ONLY){
private var flg = true
override def onStart(): Unit = {
//采集数据
while (flg) {
//采集数据
val uuid: String = UUID.randomUUID().toString
//存储数据
store(uuid)
Thread.sleep(1000)
}
}
override def onStop(): Unit = {
//释放资源
flg = false
}
}
}
十七:DStream转换(有无状态转换)
离散化流无法完成排序操作 ,离散化流其实就是每个时间段RDD的封装 ,离散化流可以转换为RDD实现操作
transform用于将离散化流实现不了的功能进行调用,transform方法是DStream的方法,称之为原语,RDD的方法称之为算子 如果想要在Driver端周期性执行一些逻辑的场合,需要使用transform。
将两个窗口key相同的连接起来
使用updateStateByKey需要对检查点目录进行配置,会使用检查点来保存状态。
reduceByKey原语不会保留中间计算结果,一旦采集周期结束,数据丢弃 ,如果想要保留中间计算结果,需要有状态操作
从检查点中恢复数据
def main(args: Array[String]): Unit = {
//从检查点中恢复数据
val outerSSC = StreamingContext.getOrCreate("cp",() => {
//TODO SparkStreaming 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[*]")
.setAppName("Streaming")
.set("spark.testing.memory", "471859200")
//StreamingContext独享的第二个构造参数表示数据采集周期
// val ssc: StreamingContext = new StreamingContext(conf,new Duration(3 * 1000))
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
ssc.checkpoint("cp")
val socketDS: ReceiverInputDStream[String] = ssc.socketTextStream("localhost", 8888)
val wordDS: DStream[String] = socketDS.flatMap(_.split(" "))
val wordToOneDS: DStream[(String, Int)] = wordDS.map((_, 1))
wordToOneDS.updateStateByKey(
(seq : Seq[Int] , buffer : Option[Int]) => {
Option(seq.sum + buffer.getOrElse(0))
}
).print()
ssc
})
//SparkStreaming (reduceByKey) + Redis内存数据库 = 有状态
outerSSC.start()
outerSSC.awaitTermination()
}
十八:DStream窗口
窗口划过的规律是:数据由无到有再到无(先多后少后变没)
DStream离散流有window方法:wordToOneDS.window(Seconds(9),Seconds(3))可以设置窗口的大小。
window操作中,窗口范围和滑动的幅度应该为采集周期的整数倍
如果窗口范围和滑动的幅度相同, 那么就是滚动窗口,没有重复数据 默认情况下,不使用窗口,也会有窗口操作,窗口范围取值为1个采集周期,滑动幅度也是一个采集周期。
如果窗口范围和滑动的幅度不相同, 那么就是滑动窗口
1>窗口范围大于滑动的幅度, 存在重复数据
2>窗口范围小于滑动的幅度, 存在丢失数据的可能
二十:优雅的关闭
对于流式数据,线程的启动和关闭应该是分开的,属于两个线程。
优雅的关闭,不要强制关闭,关闭前,不再接受数据,而是把手头上的数据处理完成之后再关闭。
二十一:DStream输出
如果想要将DStream进行特殊处理,那么可以转换为底层RDD实现
flatmap
map
mapValues
mapValues
park和Hadoop的根本差异是多个作业之间的数据通信问题 : Spark多个作业之间数据通信是基于内存,而Hadoop是基于磁盘。
Spark的缓存机制比HDFS的缓存机制高效。
二:wordCount()分析 (flatmap() 与 map())
flatmap与map我的理解:
读取数据是一行一行读的,(如果每一行的数据源是 (Hello World Hello Spark)
)
补充:
用任何一个框架都需要一个环境对象 sc就是SparkContext的环境对象(环境配置对象参数)
val spakConf = new SparkConf().setMaster("local[*]").setAppName("WordCount").set("spark.testing.memory", "2147480000")
val sc = new SparkContext(spakConf)
flatMap()将整体拆分成为个体。
val wordGroup: RDD[(String, Iterable[(String, Int)])] = fileDataRDD.flatMap(_.split(" "))
.map((_, 1))
.groupBy(_._1)
wordGroup.mapValues(
list => {
list.map(_._2)
list.map(_._2).sum
list.map(_._2).reduce(_+_)
list.map(_._2).size
}
)
val wordCount: RDD[(String, Int)] = wordGroup.mapValues(_.map(_._2).reduce(_ + _))
wordGroup.collect().foreach(println)
// flatMap, map, groupBy, mapValues, collect方法都是Spark框架提供的方法,而不是Scala集合的方法
// 为了降低学习的成本,使用的难度,让Spark的API和Scala方法变得类似。
val words = fileDatas.flatMap(_.split(" "))
val wordToOne = words.map((_, 1))
// reduceByKey : 相同的key,对value进行两两聚合
// (word, 1), (word, 1), (word, 1)
// (word, (1,1,1,1,1))
val wordCount = wordToOne.reduceByKey(_+_)
wordCount.collect().foreach(println)
三:spark环境
四:Spark运行架构
YARN中ApplicationMaster能做到资源和计算的解耦合,Driver和Executor使用的计算框架是Spark,因此也可以使用其它的框架,而YARN只做资源的调度使用
Spark应用程序提交到Yarn环境中执行的时候,一般会有两种部署执行的方式:Client和Cluster。两种模式主要区别在于:Driver程序的运行节点位置。
五:Spark核心编程
首先,装饰者模式只会进行功能组合,不会执行,另外RDD的装饰者模式与IO的装饰者模式的区别在于,RDD不保留数据,只对数据进行处理,而IO中有缓冲区可以保留数据。
5.1:执行原理
六:内存磁盘如何分区及存储原理
6.1:算子的创建:
因为简明知意,makeRDD用的比较多
分区数量是如何计算的(只要牵涉的到分区就会出现数据倾斜现象)
七:算子来啦(转换与行为算子)
转换算子就是将旧的RDD转换成为新的RDD,因为要组合功能。
flatMap()保留,map()不保留
不在一个分区是无法进行比较的
map() VS mapPartitions()
mapPartitions()是批量处理的 因此入参为迭代器 出参也为迭代器,map处理一个返回一个,mapPartitions处理一批返回一批
迭代器Iterator也是集合
7.1:groupBy()
groupBy()引出shuffle
shuffle一定会落盘,因为RDD不保留数据,因此,在shuffle阶段,一定会存在Write(shuffle左边)和read(shuffle右边)
所有含有shuffle的算子都有改变分区的能力
因为隐式转换所以字符串可以直接()写下标
7.2:补充零拷贝(NIO实现)页缓存
7.3:flatMap()
7.4:glom
7.5:filter
val fileRDD = sc.textFile("data/apache.log")
// filter算子返回的结果为按照规则保留的数据本身
fileRDD.filter(
line => {
val datas = line.split(" ")
val time = datas(3)
time.startsWith("17/05/2015")
}
).map(
line => {
val datas = line.split(" ")
datas(6)
}
).collect.foreach(println)
7.6: sample
7.7: distinct
scala使用的是单点集合(缺点,单点的资源有限),而RDD使用的是集群去重
7.8:coalesce(缩小分区) 与 repartition(扩大分区)
7.9:sortBy()
八:双值类型数据集的算子(交集并集差集拉链)
单Value(单一数据集)数据操作,双Value(多个数据集)数据操作
九:Key - Value类型算子(隐式转换rddToPairRDDFunction)
9.1:partitionBy()
repartition : 重分区(分区数量)
partitionBy : 重分区(数据的位置 数据进入到哪个分区), 数据路由(Hash定位),分区器 如果想要让数据重新进行分区,那么需要传递分区规则对象(分区器)
Spark中默认常用的分区器:RangePartitioner(要求数据必须能排序) & HashPartitioner(默认)
思考一个问题:如果重分区的分区器和当前RDD的分区器一样怎么办?
此时不做任何处理,还是当前分区,不需要重新shuffle
9.2:reduceByKey groupByKey
TODO reduceByKey算子用于将数据集中相同的key的value数据聚合在一起实现两两聚合
spark的算子有很多是以byKey结尾的,就是说将K-V独立使用
TODO reduceByKey算子可以实现 WordCount
9.3:aggregateByKey foldByKey
需求 数据集中相同的Key的分区内取最大值,分区间求和
reduceByKey groupByKey只考虑key不考虑分区而reduceByKey 在逻辑上,
分区内和分区间是一样的都是聚合 因此reduceByKey groupByKey都不能处理该需求。
需求 如果分区内和分区间的计算规则不一定相同时,Spark提供了一个算子来实现。aggregateByKey
aggregateByKey算子存在函数柯里化,有多个参数列表
第一个参数列表中有一个参数
第一个参数表示计算初始值
第二个参数列表中有二个参数
第一个参数表示 : 分区内计算规则
第二个参数表示 : 分区间计算规则
9.4:combineByKey
reduceByKey,aggregateByKey ,foldByKey, combineByKey本质区别
9.5:sortByKey
9.6:Join leftOuterJoin rightOuterJoin fullOuterJoin
9.7 : cogroup
join算子体现的是数据的关系
join算子将两个数据集中相同的key的数据,连接在一起
zip是将两个数据集位置相同的数据,连接起来与key无关
join 可能存在笛卡尔积 也 存在shuffle()
笛卡尔积 在落盘的时候 如果存在大量相同的key的时候数据量会激增
如果能够使用其它算子实现的功能,那么不推荐使用join
rdd1.join(rdd2).collect().foreach(println)
rdd1.leftOuterJoin(rdd2).collect().foreach(println) rdd1.rightOuterJoin(rdd2).collect().foreach(println) rdd1.fullOuterJoin(rdd2).collect().foreach(println)
cogroup = connect(多个数据集) + group(单一数据集)
9.6:案例实操
方式一:
// 统计出每一个省份每个广告被点击数量排行的Top3
val fileDatas: RDD[String] = sc.textFile("data/agent.log")
val groupDatas: RDD[(String, Iterable[String])] = fileDatas.groupBy(
line => {
val datas: Array[String] = line.split(" ")
datas(1)
}
)
val top3: RDD[(String, List[(String, Int)])] = groupDatas.mapValues(
//数据集是List(line, line, line)
list => {
//数据集是line
val adToCount: Map[String, Int] = list.map(
line => {
val datas: Array[String] = line.split(" ")
(datas(4), 1)
}
//数据集是List((省份,广告)) 按照省份进行分组统计
).groupBy(_._1).mapValues(_.size)
//Map[String, Int] [广告,点击次数]
// 在 groupDatas.mapValues( 的架子中都是对value进行处理的
//元组天生的排序 数据类型[广告,点击次数] 前三条
adToCount.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
}
)
top3.collect().foreach(println)
sc.stop()
}
方式二:
val fileDatas: RDD[String] = sc.textFile("data/agent.log")
// 1. 将广告进行统计分析 (word, cnt)
// ((省份1,广告1), 1),((省份2,广告2), 1)((省份1,广告1), 1)
// ((省份1,广告1), sum),((省份1,广告2), sum)
val reduceDatas: RDD[((String, String), Int)] = fileDatas.map(
line => {
val datas: Array[String] = line.split(" ")
((datas(1), datas(4)), 1)
}
).reduceByKey(_ + _)
// 1.5 将统计结果进行格式转换
// (省份1,(广告1, sum)),(省份1,(广告2, sum))
val mapDatas: RDD[(String, (String, Int))] = reduceDatas.map {
case ((prv, ad), sum) => {
(prv, (ad, sum))
}
}
// 2. 将统计结果按照省份进行分组
// (省份1, (广告1, sum),(广告2, sum))
val prvGroupDatas: RDD[(String, Iterable[(String, Int)])] = mapDatas.groupByKey()
// 3. 将分组后的数据按照点击数量进行排序(降序),取前3名
val top3: RDD[(String, List[(String, Int)])] = prvGroupDatas.mapValues(
iter => {
iter.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
}
)
top3.collect().foreach(println)
十:action行动算子(是对结果进行处理的)
TODO : 所谓的行动(Action)算子,其实就是RDD用于触发作业执行的方法,类似于IO中的read方法
转换算子的返回值为RDD 行动算子的返回值为具体的结果
行动算子和作业的关系 :1 对 1
10.2:reduce
reduce算子可以触发作业的执行,分区内先两两计算,分区间再两两计算
分区内计算都是在Executor。
分区间计算都是在Driver
10.3:count first take takeOrdered
10.4: aggregate fold
10.5: countByKey countByValue
10.5: save相关的算子
rdd.saveAsTextFile("output1")
rdd.saveAsObjectFile("output2")
rdd.saveAsSequenceFile("output3")
10.5: collect foreach
10.6: 序列化闭包检测
一个函数使用了外部的数据,并且改变了这个数据的生命周期 将数据包含到函数内部,形成了一个闭合的环境,这个环境称之为闭包环境,简称闭包
Spark在运行作业之前,会判断程序逻辑中是否包含闭包
如果存在闭包操作,那么需要检测闭包中使用的数据是否能够序列化,如果不能,那么发生错误
这个操作称之为闭包检测
十一:RDD阶段的划分
前一个阶段不执行完,后面的阶段是无法执行的。
十二:依赖关系和血缘关系(宽依赖窄依赖)
十三:持久化
(持久化只针对于当前应用程序,资源使用完之后释放资源,不会给其它节点使用。因此诞生了检查点)
将计算结果缓存起来。
数据的持久化默认将数据保存到内存中,重复使用
wordToOne.cache()
持久化操作也可以保存到磁盘中
wordToOne.persist()
保存级别设置
十三:RDD CheckPoint检查点
java中的finalize方法是Object类中提供的一个方法,在GC准备释放对象所占用的内存空间之前,它将首先调用finalize()方法。
持久化只针对于当前应用程序,资源使用完之后释放资源,不会给其它节点使用。
持久化只针对于当前应用程序
跨应用程序的数据共享需要采用 检查点机制
设置的方式
① :sc.setCheckpointDir("cp") 该检查点的目录一般为分布式目录以便各个节点使用
②:wordToOne.checkpoint()
持久化操作会在血缘关系中增加依赖
检查点一般会将数据保存到分布式文件系统中(HDFS)(新的数据源)
检查点会切断血缘即减少血缘依赖,因为可以把HDFS中相对可靠的数据作为新的数据源。
十四:自定义分区 RDD文件读取与保存
Hash分区:对于给定的key,计算其hashCode,并除以分区个数取余
Range分区:将一定范围内的数据映射到一个分区中,尽量保证每个分区数据均匀,而且分区间有序
十四:累加器(数据模型)(功能比较单一没有shuffle性能相对高)
累加器的现象
分析:
如果数据和RDD有关系,那么可以从Driver端发送到Executor端执行
如果执行完毕,那么可以将结果拉回到Driver端
如果数据和RDD没有关系,因为计算中存在闭包操作,所以RDD会将数据发送到Executor
但是无法将数据从Executor端拉回到Driver端。
如果想要将和RDD无关的数据拉回到Driver端,RDD模型处理不了,需要采用新的数据模型——累加器
doubleAccumulator
collectionAccumulator 累加器
def main(args: Array[String]): Unit = {
// TODO Spark 环境
val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(conf)
// TODO 1. 创建累加器
val wordCountAcc = new MyAcculumator
// TODO 2. 将累加器注册到Spark中
sc.register(wordCountAcc, "WordCount")
val rdd = sc.makeRDD(
List(
"Hello",
"Hello",
"Hello",
"World",
"Hello",
),2
)
rdd.foreach(
word => {
// TODO 3. 使用累加器
wordCountAcc.add(word)
}
)
// TODO 4. 获取累加器的结果
println(wordCountAcc.value)
sc.stop()
}
// TODO 自定义数据累加器(WordCount)
// 1. 继承AccumulatorV2
// 2. 定义数据的泛型
// IN : String
// OUT : mutable.Map[String, Int]
// 3. 重写方法(3(计算) + 3(状态) = 6)
class MyAcculumator extends AccumulatorV2[String, mutable.Map[String, Int]]{
private val wordCountMap = mutable.Map[String, Int]()
// TODO 判断当前累加器是否为初始状态
override def isZero: Boolean = {
wordCountMap.isEmpty
}
// TODO 复制累加器
override def copy(): AccumulatorV2[String, mutable.Map[String, Int]] = {
new MyAcculumator()
}
// TODO 重置累加器
override def reset(): Unit = {
wordCountMap.clear()
}
// TODO 将外部的数据增加到累加器中
override def add(word: String): Unit = {
val oldCnt: Int = wordCountMap.getOrElse(word, 0)
wordCountMap.update(word, oldCnt + 1)
}
// TODO 将多个累加器进行合并
override def merge(other: AccumulatorV2[String, mutable.Map[String, Int]]): Unit = {
other.value.foreach {
case (word, cnt) => {
val oldCnt: Int = wordCountMap.getOrElse(word, 0)
wordCountMap.update(word, oldCnt + cnt)
}
}
}
// TODO 获取累加器的结果
override def value: mutable.Map[String, Int] = {
wordCountMap
}
}
十五:Join现象引出广播变量
def main(args: Array[String]): Unit = {
// TODO Spark 环境
val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(conf)
val rdd1 = sc.makeRDD(
List(
("a", 1),
("b", 2),
)
)
val rdd2 = sc.makeRDD(
List(
("a", 3),
("b", 4),
)
)
// ("a", 1)
// ( a,(1,3))
//rdd1.join(rdd2).collect().foreach(println)
// 计算的对象是以Task为单位进行数据发送的,所以可能会导致大量的数据冗余
// 所以如果计算对象占用比较大的资源,性能会急剧下降
// 为了提供数据访问效率,可以将数据发送给Executor,而不是发给Task。
// RDD数据模型只能给Task发。只能采用其他的数据模型:TODO 广播变量
val map = mutable.Map[String, Int](
("a", 3),
("b", 4)
)
// TODO 使用广播变量
val bc: Broadcast[mutable.Map[String, Int]] = sc.broadcast(map)
rdd1.map {
case (k, v1) => {
// TODO 获取广播变量的值
var v2 = bc.value.getOrElse(k, 0)
(k, (v1, v2))
}
}.collect.foreach(println)
sc.stop()
}
计算的对象是以Task为单位进行数据发送的,所以可能会导致大量的数据冗余 且 如果计算对象占用比较大的资源,性能会急剧下降
为了提供数据访问效率,可以将数据发送给Executor,而不是发给Task。
但是RDD数据模型只能给Task发。只能采用其他的数据模型: 广播变量(可以将数据发送给Executor 使得同一个节点上的Task可以共享一份资源,并且如果有Executor节点与Driver通信比较慢的时候 可以从就近的Executor上获得需要的资源)
十五:SparkSQL(Dataframe DataSet RDD 转换)
Spark + Hive => Shark
Spark SQL是Spark用于结构化数据(structured data)处理的Spark模块。
1.2 SparkSQL特点:
无缝的整合了 SQL 查询和 Spark 编程
SparkSQL可以简化RDD的开发,提高开发效率
15.1:Dataframe 与 DataSet 与 RDD概述
SQL语言本身没有类型
工厂模式生产的对象是有相同的功能的,因为从同一条生产线下来的。
Builder模式要生产某个对象需要经过复杂的过程,但是builder模式会把这些过程顺序确定下来。
Scala中的构造方法有两种,主构造方法和辅助构造方法,辅助构造方法的函数名一定为this。
15.2:Dataframe 与 DataSet 与 RDD
Dataframe : 是SparkSQL中的核心数据模型,是弱类型的模型,不关心数据的类型操作。
因为SQL的局限性,因此在处理数据的时候,Dataframe提供了一种DSL语法(面向对象的语法)
SparkSQL 与 RDD的关系与转换
SparkSQL模块等同于是对SparkCore一个封装,专门用于结构化数据处理的场景
Dataset : 就是使用面向对象的方式操作数据, 类,属性 => 结构 就是
在Dataframe的基础上,增加了类型信息
SparkSQL中存在大量的隐式转换操作,但是一般会导入使用 import时,使用的spark并不是包名,是环境对象的名称。
Dataframe 与 DataSet的区别?
Dataframe是特定类型的DataSet[Row]
说明DataSet兼容Dataframe()是将来用的比较多的
注意:如果从内存中获取数据,spark可以知道数据类型具体是什么。如果是数字,默认作为Int处理;但是从文件中读取的数字,不能确定是什么类型,所以用bigint接收,可以和Long类型转换,但是和Int不能进行转换
SparkSQL技术选型:
15.2:UDF 与 UDAF
package com.atguigu.bigdata.spark.sql01
import org.apache.spark.sql.expressions.Aggregator
import org.apache.spark.sql.{Dataframe, Encoder, Encoders, SparkSession, functions}
object SparkSQL09_Dataframe_UDAF {
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
df.createOrReplaceTempView("user")
// TODO UDAF使用时,需要采用特殊的方式实现自定义操作
// TODO 强类型操作不能应用于弱类型操作,如果想用,那么需要转换functions.udaf
// Spark3.0以后,可以通过转换,让强类型应用在弱类型操作
spark.udf.register("avgAge",functions.udaf(new AvgAgeUDF()))
// TODO : UDF
// 自定义的函数在SQL文使用
spark.sql("select avgAge(age) from user").show()
spark.stop()
}
case class CalcBuffer(var total : Long , var cnt : Long)
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 1. 继承org.apache.spark.sql.expressions.Aggregator
// 2. 定义泛型
// IN : Long
// BUFF : CalcBuffer
// OUT : Long
// 3. 重写方法 (4 + 2(固定) = 6)
class AvgAgeUDF extends Aggregator[Long,CalcBuffer,Long]{
// TODO 缓冲区的初始化
override def zero: CalcBuffer = {
CalcBuffer(0l,0l)
}
// TODO 将输入的数据和缓冲区的数据进行聚合操作
override def reduce(buff: CalcBuffer, a: Long): CalcBuffer ={
buff.total += a
buff.cnt += 1
buff
}
// TODO 多个缓冲区的合并操作
override def merge(b1: CalcBuffer, b2: CalcBuffer): CalcBuffer = {
b1.total += b2.total
b1.cnt += b2.cnt
b1
}
// TODO 计算数据
override def finish(reduction: CalcBuffer): Long = {
reduction.total / reduction.cnt
}
override def bufferEncoder: Encoder[CalcBuffer] = Encoders.product
override def outputEncoder: Encoder[Long] = Encoders.scalaLong
}
}
UDAF老版本:
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
df.createOrReplaceTempView("user")
// TODO UDAF使用时,需要采用特殊的方式实现自定义操作
// TODO 强类型操作不能应用于弱类型操作,如果想用,那么需要转换functions.udaf
// Spark3.0以后,可以通过转换,让强类型应用在弱类型操作
spark.udf.register("avgAge",new AvgAgeUDAF())
// TODO : UDF
// 自定义的函数在SQL文使用
spark.sql("select avgAge(age) from user").show()
spark.stop()
}
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 使用早期Spark版本的UDAF(弱类型)
// 1. 继承UserDefinedAggregateFunction
// 2. 重写方法(8)
class AvgAgeUDAF extends UserDefinedAggregateFunction{
// TODO 输入数据的结构
override def inputSchema: StructType = {
StructType(
Array(
StructField("age",LongType)
)
)
}
// TODO 缓冲区数据的结构
override def bufferSchema: StructType = {
StructType(
Array(
StructField("total",LongType),
StructField("cnt",LongType)
)
)
}
// TODO 输出数据的类型
override def dataType: DataType = LongType
// TODO 计算稳定性
override def deterministic: Boolean = true
// TODO 缓冲区的初始化
override def initialize(buffer: MutableAggregationBuffer): Unit = {
buffer.update(0,0l)
buffer.update(1,0l)
}
// TODO 将输入的数据和缓冲区的数据进行聚合操作
override def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
buffer.update(0,buffer.getLong(0) + input.getLong(0))
buffer.update(1,buffer.getLong(1)+1)
}
// TODO 多个缓冲区的合并操作
override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
buffer1.update(0,buffer1.getLong(0) + buffer2.getLong(0))
buffer1.update(1,buffer1.getLong(1) + buffer2.getLong(1))
}
override def evaluate(buffer: Row): Any = {
buffer.getLong(0) / buffer.getLong(1)
}
}
UDAF_Class老版本:
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
// TODO Spark3.0版本前,强类型的UDAF不能应用于弱类型的SQL中
import spark.implicits._
val udaf: AvgAgeUDAF = new AvgAgeUDAF
val ds: Dataset[User] = df.as[User]
ds.select(udaf.toColumn).show()
spark.stop()
}
case class User(id : Long , name : String , age : Long)
case class CalacBuffer(var total : Long , var cnt : Long)
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 使用早期Spark版本的UDAF(强类型)
// 1. 继承Aggregator
// 2. 定义泛型
// IN : User
// BUFF : CalcBuffer
// OUT : Long
// 3. 重写方法(6)
class AvgAgeUDAF extends Aggregator[User,CalacBuffer,Long]{
override def zero: CalacBuffer = {
CalacBuffer(0l,0L)
}
override def reduce(b: CalacBuffer, user: User): CalacBuffer = {
b.total += user.age
b.cnt += 1
b
}
override def merge(b1: CalacBuffer, b2: CalacBuffer): CalacBuffer = {
b1.total += b2.total
b1.cnt += b2.cnt
b1
}
override def finish(buffer: CalacBuffer): Long = {
buffer.total / buffer.cnt
}
override def bufferEncoder: Encoder[CalacBuffer] = Encoders.product
override def outputEncoder: Encoder[Long] = Encoders.scalaLong
}
15.3:通用读取方式和默认读取方式
user.json is not a Parquet file. expected magic number at tail [80, 65, 82, 49] but found [32, 53, 48, 125]
SparkSQL通用读取的方法,读取的文件为Parquet格式(列式存储格式)
val df = spark.read.load("data/users.parquet")
如果想要读取其他格式的文件也可以,需要额外增加参数进行解析
df.write.mode("append").format("json").save("output")
sparkSQL基于SparkCore
SparkCore读取数据是依赖于Hadoop
Hadoop读取数据是按行读取
SparkSQL读取JSON格式的文件,要求每一行数据必须符合JSON格式
读入数据:
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
//方式1:通用的load方法读取
spark.read.format("jdbc")
.option("url", "jdbc:mysql://linux1:3306/spark-sql")
.option("driver", "com.mysql.jdbc.Driver")
.option("user", "root")
.option("password", "123123")
.option("dbtable", "user")
.load().show
//方式2:通用的load方法读取 参数另一种形式
spark.read.format("jdbc")
.options(Map("url"->"jdbc:mysql://linux1:3306/spark-sql?user=root&password=123123",
"dbtable"->"user","driver"->"com.mysql.jdbc.Driver")).load().show
//方式3:使用jdbc方法读取
val props: Properties = new Properties()
props.setProperty("user", "root")
props.setProperty("password", "123123")
val df: Dataframe = spark.read.jdbc("jdbc:mysql://linux1:3306/spark-sql", "user", props)
df.show
//释放资源
spark.stop()
写入数据:
case class User2(name: String, age: Long)
。。。
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
val rdd: RDD[User2] = spark.sparkContext.makeRDD(List(User2("lisi", 20), User2("zs", 30)))
val ds: Dataset[User2] = rdd.toDS
//方式1:通用的方式 format指定写出类型
//方式2:通过jdbc方法
val props: Properties = new Properties()
props.setProperty("user", "root")
props.setProperty("password", "123123")
ds.write.mode(SaveMode.Append).jdbc("jdbc:mysql://linux1:3306/spark-sql", "user", props)
//释放资源
spark.stop()
十六:SparkStreaming(数据模型是离散流DStrem)
实时 离线 批量 流式
Spark是一个离线的批量数据处理的框架
SparkStreaming是一个准实时,微批次的处理框架
16.1:SparkStreaming知识点
Discretized Stream(离散流)是Spark Streaming的基础抽象,代表持续性的数据流和经过各种Spark原语操作后的结果数据流。在内部实现上,DStream是一系列连续的RDD来表示。每个RDD含有一段时间间隔内的数据。
16.2:DStream
blockingQueue队列:阻塞式队列,有反压机制,可以避免数据挤压。
采集数据的方式:DStream监控目录变化,将RDD放入到离散化流DStream中,自定义数据源。
object SparkStreaming05_DIY {
def main(args: Array[String]): Unit = {
//TODO SparkStreaming 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[*]")
.setAppName("Streaming")
.set("spark.testing.memory", "471859200")
//StreamingContext独享的第二个构造参数表示数据采集周期
// val ssc: StreamingContext = new StreamingContext(conf,new Duration(3 * 1000))
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
val uuidDS: ReceiverInputDStream[String] = ssc.receiverStream(new MyReceiver)
uuidDS.print()
//启动采集器
ssc.start()
// 等待采集器结束
ssc.awaitTermination()
}
class MyReceiver extends Receiver[String](StorageLevel.MEMORY_ONLY){
private var flg = true
override def onStart(): Unit = {
//采集数据
while (flg) {
//采集数据
val uuid: String = UUID.randomUUID().toString
//存储数据
store(uuid)
Thread.sleep(1000)
}
}
override def onStop(): Unit = {
//释放资源
flg = false
}
}
}
十七:DStream转换(有无状态转换)
离散化流无法完成排序操作 ,离散化流其实就是每个时间段RDD的封装 ,离散化流可以转换为RDD实现操作
transform用于将离散化流实现不了的功能进行调用,transform方法是DStream的方法,称之为原语,RDD的方法称之为算子 如果想要在Driver端周期性执行一些逻辑的场合,需要使用transform。
将两个窗口key相同的连接起来
使用updateStateByKey需要对检查点目录进行配置,会使用检查点来保存状态。
reduceByKey原语不会保留中间计算结果,一旦采集周期结束,数据丢弃 ,如果想要保留中间计算结果,需要有状态操作
从检查点中恢复数据
def main(args: Array[String]): Unit = {
//从检查点中恢复数据
val outerSSC = StreamingContext.getOrCreate("cp",() => {
//TODO SparkStreaming 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[*]")
.setAppName("Streaming")
.set("spark.testing.memory", "471859200")
//StreamingContext独享的第二个构造参数表示数据采集周期
// val ssc: StreamingContext = new StreamingContext(conf,new Duration(3 * 1000))
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
ssc.checkpoint("cp")
val socketDS: ReceiverInputDStream[String] = ssc.socketTextStream("localhost", 8888)
val wordDS: DStream[String] = socketDS.flatMap(_.split(" "))
val wordToOneDS: DStream[(String, Int)] = wordDS.map((_, 1))
wordToOneDS.updateStateByKey(
(seq : Seq[Int] , buffer : Option[Int]) => {
Option(seq.sum + buffer.getOrElse(0))
}
).print()
ssc
})
//SparkStreaming (reduceByKey) + Redis内存数据库 = 有状态
outerSSC.start()
outerSSC.awaitTermination()
}
十八:DStream窗口
窗口划过的规律是:数据由无到有再到无(先多后少后变没)
DStream离散流有window方法:wordToOneDS.window(Seconds(9),Seconds(3))可以设置窗口的大小。
window操作中,窗口范围和滑动的幅度应该为采集周期的整数倍
如果窗口范围和滑动的幅度相同, 那么就是滚动窗口,没有重复数据 默认情况下,不使用窗口,也会有窗口操作,窗口范围取值为1个采集周期,滑动幅度也是一个采集周期。
如果窗口范围和滑动的幅度不相同, 那么就是滑动窗口
1>窗口范围大于滑动的幅度, 存在重复数据
2>窗口范围小于滑动的幅度, 存在丢失数据的可能
二十:优雅的关闭
对于流式数据,线程的启动和关闭应该是分开的,属于两个线程。
优雅的关闭,不要强制关闭,关闭前,不再接受数据,而是把手头上的数据处理完成之后再关闭。
二十一:DStream输出
如果想要将DStream进行特殊处理,那么可以转换为底层RDD实现
flatmap与map我的理解:
读取数据是一行一行读的,(如果每一行的数据源是 (Hello World Hello Spark)
)
补充:
用任何一个框架都需要一个环境对象 sc就是SparkContext的环境对象(环境配置对象参数)
val spakConf = new SparkConf().setMaster("local[*]").setAppName("WordCount").set("spark.testing.memory", "2147480000")
val sc = new SparkContext(spakConf)
flatMap()将整体拆分成为个体。
val wordGroup: RDD[(String, Iterable[(String, Int)])] = fileDataRDD.flatMap(_.split(" "))
.map((_, 1))
.groupBy(_._1)
wordGroup.mapValues(
list => {
list.map(_._2)
list.map(_._2).sum
list.map(_._2).reduce(_+_)
list.map(_._2).size
}
)
val wordCount: RDD[(String, Int)] = wordGroup.mapValues(_.map(_._2).reduce(_ + _))
wordGroup.collect().foreach(println)
// flatMap, map, groupBy, mapValues, collect方法都是Spark框架提供的方法,而不是Scala集合的方法
// 为了降低学习的成本,使用的难度,让Spark的API和Scala方法变得类似。
val words = fileDatas.flatMap(_.split(" "))
val wordToOne = words.map((_, 1))
// reduceByKey : 相同的key,对value进行两两聚合
// (word, 1), (word, 1), (word, 1)
// (word, (1,1,1,1,1))
val wordCount = wordToOne.reduceByKey(_+_)
wordCount.collect().foreach(println)
四:Spark运行架构
YARN中ApplicationMaster能做到资源和计算的解耦合,Driver和Executor使用的计算框架是Spark,因此也可以使用其它的框架,而YARN只做资源的调度使用
Spark应用程序提交到Yarn环境中执行的时候,一般会有两种部署执行的方式:Client和Cluster。两种模式主要区别在于:Driver程序的运行节点位置。
五:Spark核心编程
首先,装饰者模式只会进行功能组合,不会执行,另外RDD的装饰者模式与IO的装饰者模式的区别在于,RDD不保留数据,只对数据进行处理,而IO中有缓冲区可以保留数据。
5.1:执行原理
六:内存磁盘如何分区及存储原理
6.1:算子的创建:
因为简明知意,makeRDD用的比较多
分区数量是如何计算的(只要牵涉的到分区就会出现数据倾斜现象)
七:算子来啦(转换与行为算子)
转换算子就是将旧的RDD转换成为新的RDD,因为要组合功能。
flatMap()保留,map()不保留
不在一个分区是无法进行比较的
map() VS mapPartitions()
mapPartitions()是批量处理的 因此入参为迭代器 出参也为迭代器,map处理一个返回一个,mapPartitions处理一批返回一批
迭代器Iterator也是集合
7.1:groupBy()
groupBy()引出shuffle
shuffle一定会落盘,因为RDD不保留数据,因此,在shuffle阶段,一定会存在Write(shuffle左边)和read(shuffle右边)
所有含有shuffle的算子都有改变分区的能力
因为隐式转换所以字符串可以直接()写下标
7.2:补充零拷贝(NIO实现)页缓存
7.3:flatMap()
7.4:glom
7.5:filter
val fileRDD = sc.textFile("data/apache.log")
// filter算子返回的结果为按照规则保留的数据本身
fileRDD.filter(
line => {
val datas = line.split(" ")
val time = datas(3)
time.startsWith("17/05/2015")
}
).map(
line => {
val datas = line.split(" ")
datas(6)
}
).collect.foreach(println)
7.6: sample
7.7: distinct
scala使用的是单点集合(缺点,单点的资源有限),而RDD使用的是集群去重
7.8:coalesce(缩小分区) 与 repartition(扩大分区)
7.9:sortBy()
八:双值类型数据集的算子(交集并集差集拉链)
单Value(单一数据集)数据操作,双Value(多个数据集)数据操作
九:Key - Value类型算子(隐式转换rddToPairRDDFunction)
9.1:partitionBy()
repartition : 重分区(分区数量)
partitionBy : 重分区(数据的位置 数据进入到哪个分区), 数据路由(Hash定位),分区器 如果想要让数据重新进行分区,那么需要传递分区规则对象(分区器)
Spark中默认常用的分区器:RangePartitioner(要求数据必须能排序) & HashPartitioner(默认)
思考一个问题:如果重分区的分区器和当前RDD的分区器一样怎么办?
此时不做任何处理,还是当前分区,不需要重新shuffle
9.2:reduceByKey groupByKey
TODO reduceByKey算子用于将数据集中相同的key的value数据聚合在一起实现两两聚合
spark的算子有很多是以byKey结尾的,就是说将K-V独立使用
TODO reduceByKey算子可以实现 WordCount
9.3:aggregateByKey foldByKey
需求 数据集中相同的Key的分区内取最大值,分区间求和
reduceByKey groupByKey只考虑key不考虑分区而reduceByKey 在逻辑上,
分区内和分区间是一样的都是聚合 因此reduceByKey groupByKey都不能处理该需求。
需求 如果分区内和分区间的计算规则不一定相同时,Spark提供了一个算子来实现。aggregateByKey
aggregateByKey算子存在函数柯里化,有多个参数列表
第一个参数列表中有一个参数
第一个参数表示计算初始值
第二个参数列表中有二个参数
第一个参数表示 : 分区内计算规则
第二个参数表示 : 分区间计算规则
9.4:combineByKey
reduceByKey,aggregateByKey ,foldByKey, combineByKey本质区别
9.5:sortByKey
9.6:Join leftOuterJoin rightOuterJoin fullOuterJoin
9.7 : cogroup
join算子体现的是数据的关系
join算子将两个数据集中相同的key的数据,连接在一起
zip是将两个数据集位置相同的数据,连接起来与key无关
join 可能存在笛卡尔积 也 存在shuffle()
笛卡尔积 在落盘的时候 如果存在大量相同的key的时候数据量会激增
如果能够使用其它算子实现的功能,那么不推荐使用join
rdd1.join(rdd2).collect().foreach(println)
rdd1.leftOuterJoin(rdd2).collect().foreach(println) rdd1.rightOuterJoin(rdd2).collect().foreach(println) rdd1.fullOuterJoin(rdd2).collect().foreach(println)
cogroup = connect(多个数据集) + group(单一数据集)
9.6:案例实操
方式一:
// 统计出每一个省份每个广告被点击数量排行的Top3
val fileDatas: RDD[String] = sc.textFile("data/agent.log")
val groupDatas: RDD[(String, Iterable[String])] = fileDatas.groupBy(
line => {
val datas: Array[String] = line.split(" ")
datas(1)
}
)
val top3: RDD[(String, List[(String, Int)])] = groupDatas.mapValues(
//数据集是List(line, line, line)
list => {
//数据集是line
val adToCount: Map[String, Int] = list.map(
line => {
val datas: Array[String] = line.split(" ")
(datas(4), 1)
}
//数据集是List((省份,广告)) 按照省份进行分组统计
).groupBy(_._1).mapValues(_.size)
//Map[String, Int] [广告,点击次数]
// 在 groupDatas.mapValues( 的架子中都是对value进行处理的
//元组天生的排序 数据类型[广告,点击次数] 前三条
adToCount.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
}
)
top3.collect().foreach(println)
sc.stop()
}
方式二:
val fileDatas: RDD[String] = sc.textFile("data/agent.log")
// 1. 将广告进行统计分析 (word, cnt)
// ((省份1,广告1), 1),((省份2,广告2), 1)((省份1,广告1), 1)
// ((省份1,广告1), sum),((省份1,广告2), sum)
val reduceDatas: RDD[((String, String), Int)] = fileDatas.map(
line => {
val datas: Array[String] = line.split(" ")
((datas(1), datas(4)), 1)
}
).reduceByKey(_ + _)
// 1.5 将统计结果进行格式转换
// (省份1,(广告1, sum)),(省份1,(广告2, sum))
val mapDatas: RDD[(String, (String, Int))] = reduceDatas.map {
case ((prv, ad), sum) => {
(prv, (ad, sum))
}
}
// 2. 将统计结果按照省份进行分组
// (省份1, (广告1, sum),(广告2, sum))
val prvGroupDatas: RDD[(String, Iterable[(String, Int)])] = mapDatas.groupByKey()
// 3. 将分组后的数据按照点击数量进行排序(降序),取前3名
val top3: RDD[(String, List[(String, Int)])] = prvGroupDatas.mapValues(
iter => {
iter.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
}
)
top3.collect().foreach(println)
十:action行动算子(是对结果进行处理的)
TODO : 所谓的行动(Action)算子,其实就是RDD用于触发作业执行的方法,类似于IO中的read方法
转换算子的返回值为RDD 行动算子的返回值为具体的结果
行动算子和作业的关系 :1 对 1
10.2:reduce
reduce算子可以触发作业的执行,分区内先两两计算,分区间再两两计算
分区内计算都是在Executor。
分区间计算都是在Driver
10.3:count first take takeOrdered
10.4: aggregate fold
10.5: countByKey countByValue
10.5: save相关的算子
rdd.saveAsTextFile("output1")
rdd.saveAsObjectFile("output2")
rdd.saveAsSequenceFile("output3")
10.5: collect foreach
10.6: 序列化闭包检测
一个函数使用了外部的数据,并且改变了这个数据的生命周期 将数据包含到函数内部,形成了一个闭合的环境,这个环境称之为闭包环境,简称闭包
Spark在运行作业之前,会判断程序逻辑中是否包含闭包
如果存在闭包操作,那么需要检测闭包中使用的数据是否能够序列化,如果不能,那么发生错误
这个操作称之为闭包检测
十一:RDD阶段的划分
前一个阶段不执行完,后面的阶段是无法执行的。
十二:依赖关系和血缘关系(宽依赖窄依赖)
十三:持久化
(持久化只针对于当前应用程序,资源使用完之后释放资源,不会给其它节点使用。因此诞生了检查点)
将计算结果缓存起来。
数据的持久化默认将数据保存到内存中,重复使用
wordToOne.cache()
持久化操作也可以保存到磁盘中
wordToOne.persist()
保存级别设置
十三:RDD CheckPoint检查点
java中的finalize方法是Object类中提供的一个方法,在GC准备释放对象所占用的内存空间之前,它将首先调用finalize()方法。
持久化只针对于当前应用程序,资源使用完之后释放资源,不会给其它节点使用。
持久化只针对于当前应用程序
跨应用程序的数据共享需要采用 检查点机制
设置的方式
① :sc.setCheckpointDir("cp") 该检查点的目录一般为分布式目录以便各个节点使用
②:wordToOne.checkpoint()
持久化操作会在血缘关系中增加依赖
检查点一般会将数据保存到分布式文件系统中(HDFS)(新的数据源)
检查点会切断血缘即减少血缘依赖,因为可以把HDFS中相对可靠的数据作为新的数据源。
十四:自定义分区 RDD文件读取与保存
Hash分区:对于给定的key,计算其hashCode,并除以分区个数取余
Range分区:将一定范围内的数据映射到一个分区中,尽量保证每个分区数据均匀,而且分区间有序
十四:累加器(数据模型)(功能比较单一没有shuffle性能相对高)
累加器的现象
分析:
如果数据和RDD有关系,那么可以从Driver端发送到Executor端执行
如果执行完毕,那么可以将结果拉回到Driver端
如果数据和RDD没有关系,因为计算中存在闭包操作,所以RDD会将数据发送到Executor
但是无法将数据从Executor端拉回到Driver端。
如果想要将和RDD无关的数据拉回到Driver端,RDD模型处理不了,需要采用新的数据模型——累加器
doubleAccumulator
collectionAccumulator 累加器
def main(args: Array[String]): Unit = {
// TODO Spark 环境
val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(conf)
// TODO 1. 创建累加器
val wordCountAcc = new MyAcculumator
// TODO 2. 将累加器注册到Spark中
sc.register(wordCountAcc, "WordCount")
val rdd = sc.makeRDD(
List(
"Hello",
"Hello",
"Hello",
"World",
"Hello",
),2
)
rdd.foreach(
word => {
// TODO 3. 使用累加器
wordCountAcc.add(word)
}
)
// TODO 4. 获取累加器的结果
println(wordCountAcc.value)
sc.stop()
}
// TODO 自定义数据累加器(WordCount)
// 1. 继承AccumulatorV2
// 2. 定义数据的泛型
// IN : String
// OUT : mutable.Map[String, Int]
// 3. 重写方法(3(计算) + 3(状态) = 6)
class MyAcculumator extends AccumulatorV2[String, mutable.Map[String, Int]]{
private val wordCountMap = mutable.Map[String, Int]()
// TODO 判断当前累加器是否为初始状态
override def isZero: Boolean = {
wordCountMap.isEmpty
}
// TODO 复制累加器
override def copy(): AccumulatorV2[String, mutable.Map[String, Int]] = {
new MyAcculumator()
}
// TODO 重置累加器
override def reset(): Unit = {
wordCountMap.clear()
}
// TODO 将外部的数据增加到累加器中
override def add(word: String): Unit = {
val oldCnt: Int = wordCountMap.getOrElse(word, 0)
wordCountMap.update(word, oldCnt + 1)
}
// TODO 将多个累加器进行合并
override def merge(other: AccumulatorV2[String, mutable.Map[String, Int]]): Unit = {
other.value.foreach {
case (word, cnt) => {
val oldCnt: Int = wordCountMap.getOrElse(word, 0)
wordCountMap.update(word, oldCnt + cnt)
}
}
}
// TODO 获取累加器的结果
override def value: mutable.Map[String, Int] = {
wordCountMap
}
}
十五:Join现象引出广播变量
def main(args: Array[String]): Unit = {
// TODO Spark 环境
val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(conf)
val rdd1 = sc.makeRDD(
List(
("a", 1),
("b", 2),
)
)
val rdd2 = sc.makeRDD(
List(
("a", 3),
("b", 4),
)
)
// ("a", 1)
// ( a,(1,3))
//rdd1.join(rdd2).collect().foreach(println)
// 计算的对象是以Task为单位进行数据发送的,所以可能会导致大量的数据冗余
// 所以如果计算对象占用比较大的资源,性能会急剧下降
// 为了提供数据访问效率,可以将数据发送给Executor,而不是发给Task。
// RDD数据模型只能给Task发。只能采用其他的数据模型:TODO 广播变量
val map = mutable.Map[String, Int](
("a", 3),
("b", 4)
)
// TODO 使用广播变量
val bc: Broadcast[mutable.Map[String, Int]] = sc.broadcast(map)
rdd1.map {
case (k, v1) => {
// TODO 获取广播变量的值
var v2 = bc.value.getOrElse(k, 0)
(k, (v1, v2))
}
}.collect.foreach(println)
sc.stop()
}
计算的对象是以Task为单位进行数据发送的,所以可能会导致大量的数据冗余 且 如果计算对象占用比较大的资源,性能会急剧下降
为了提供数据访问效率,可以将数据发送给Executor,而不是发给Task。
但是RDD数据模型只能给Task发。只能采用其他的数据模型: 广播变量(可以将数据发送给Executor 使得同一个节点上的Task可以共享一份资源,并且如果有Executor节点与Driver通信比较慢的时候 可以从就近的Executor上获得需要的资源)
十五:SparkSQL(Dataframe DataSet RDD 转换)
Spark + Hive => Shark
Spark SQL是Spark用于结构化数据(structured data)处理的Spark模块。
1.2 SparkSQL特点:
无缝的整合了 SQL 查询和 Spark 编程
SparkSQL可以简化RDD的开发,提高开发效率
15.1:Dataframe 与 DataSet 与 RDD概述
SQL语言本身没有类型
工厂模式生产的对象是有相同的功能的,因为从同一条生产线下来的。
Builder模式要生产某个对象需要经过复杂的过程,但是builder模式会把这些过程顺序确定下来。
Scala中的构造方法有两种,主构造方法和辅助构造方法,辅助构造方法的函数名一定为this。
15.2:Dataframe 与 DataSet 与 RDD
Dataframe : 是SparkSQL中的核心数据模型,是弱类型的模型,不关心数据的类型操作。
因为SQL的局限性,因此在处理数据的时候,Dataframe提供了一种DSL语法(面向对象的语法)
SparkSQL 与 RDD的关系与转换
SparkSQL模块等同于是对SparkCore一个封装,专门用于结构化数据处理的场景
Dataset : 就是使用面向对象的方式操作数据, 类,属性 => 结构 就是
在Dataframe的基础上,增加了类型信息
SparkSQL中存在大量的隐式转换操作,但是一般会导入使用 import时,使用的spark并不是包名,是环境对象的名称。
Dataframe 与 DataSet的区别?
Dataframe是特定类型的DataSet[Row]
说明DataSet兼容Dataframe()是将来用的比较多的
注意:如果从内存中获取数据,spark可以知道数据类型具体是什么。如果是数字,默认作为Int处理;但是从文件中读取的数字,不能确定是什么类型,所以用bigint接收,可以和Long类型转换,但是和Int不能进行转换
SparkSQL技术选型:
15.2:UDF 与 UDAF
package com.atguigu.bigdata.spark.sql01
import org.apache.spark.sql.expressions.Aggregator
import org.apache.spark.sql.{Dataframe, Encoder, Encoders, SparkSession, functions}
object SparkSQL09_Dataframe_UDAF {
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
df.createOrReplaceTempView("user")
// TODO UDAF使用时,需要采用特殊的方式实现自定义操作
// TODO 强类型操作不能应用于弱类型操作,如果想用,那么需要转换functions.udaf
// Spark3.0以后,可以通过转换,让强类型应用在弱类型操作
spark.udf.register("avgAge",functions.udaf(new AvgAgeUDF()))
// TODO : UDF
// 自定义的函数在SQL文使用
spark.sql("select avgAge(age) from user").show()
spark.stop()
}
case class CalcBuffer(var total : Long , var cnt : Long)
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 1. 继承org.apache.spark.sql.expressions.Aggregator
// 2. 定义泛型
// IN : Long
// BUFF : CalcBuffer
// OUT : Long
// 3. 重写方法 (4 + 2(固定) = 6)
class AvgAgeUDF extends Aggregator[Long,CalcBuffer,Long]{
// TODO 缓冲区的初始化
override def zero: CalcBuffer = {
CalcBuffer(0l,0l)
}
// TODO 将输入的数据和缓冲区的数据进行聚合操作
override def reduce(buff: CalcBuffer, a: Long): CalcBuffer ={
buff.total += a
buff.cnt += 1
buff
}
// TODO 多个缓冲区的合并操作
override def merge(b1: CalcBuffer, b2: CalcBuffer): CalcBuffer = {
b1.total += b2.total
b1.cnt += b2.cnt
b1
}
// TODO 计算数据
override def finish(reduction: CalcBuffer): Long = {
reduction.total / reduction.cnt
}
override def bufferEncoder: Encoder[CalcBuffer] = Encoders.product
override def outputEncoder: Encoder[Long] = Encoders.scalaLong
}
}
UDAF老版本:
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
df.createOrReplaceTempView("user")
// TODO UDAF使用时,需要采用特殊的方式实现自定义操作
// TODO 强类型操作不能应用于弱类型操作,如果想用,那么需要转换functions.udaf
// Spark3.0以后,可以通过转换,让强类型应用在弱类型操作
spark.udf.register("avgAge",new AvgAgeUDAF())
// TODO : UDF
// 自定义的函数在SQL文使用
spark.sql("select avgAge(age) from user").show()
spark.stop()
}
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 使用早期Spark版本的UDAF(弱类型)
// 1. 继承UserDefinedAggregateFunction
// 2. 重写方法(8)
class AvgAgeUDAF extends UserDefinedAggregateFunction{
// TODO 输入数据的结构
override def inputSchema: StructType = {
StructType(
Array(
StructField("age",LongType)
)
)
}
// TODO 缓冲区数据的结构
override def bufferSchema: StructType = {
StructType(
Array(
StructField("total",LongType),
StructField("cnt",LongType)
)
)
}
// TODO 输出数据的类型
override def dataType: DataType = LongType
// TODO 计算稳定性
override def deterministic: Boolean = true
// TODO 缓冲区的初始化
override def initialize(buffer: MutableAggregationBuffer): Unit = {
buffer.update(0,0l)
buffer.update(1,0l)
}
// TODO 将输入的数据和缓冲区的数据进行聚合操作
override def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
buffer.update(0,buffer.getLong(0) + input.getLong(0))
buffer.update(1,buffer.getLong(1)+1)
}
// TODO 多个缓冲区的合并操作
override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
buffer1.update(0,buffer1.getLong(0) + buffer2.getLong(0))
buffer1.update(1,buffer1.getLong(1) + buffer2.getLong(1))
}
override def evaluate(buffer: Row): Any = {
buffer.getLong(0) / buffer.getLong(1)
}
}
UDAF_Class老版本:
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
// TODO Spark3.0版本前,强类型的UDAF不能应用于弱类型的SQL中
import spark.implicits._
val udaf: AvgAgeUDAF = new AvgAgeUDAF
val ds: Dataset[User] = df.as[User]
ds.select(udaf.toColumn).show()
spark.stop()
}
case class User(id : Long , name : String , age : Long)
case class CalacBuffer(var total : Long , var cnt : Long)
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 使用早期Spark版本的UDAF(强类型)
// 1. 继承Aggregator
// 2. 定义泛型
// IN : User
// BUFF : CalcBuffer
// OUT : Long
// 3. 重写方法(6)
class AvgAgeUDAF extends Aggregator[User,CalacBuffer,Long]{
override def zero: CalacBuffer = {
CalacBuffer(0l,0L)
}
override def reduce(b: CalacBuffer, user: User): CalacBuffer = {
b.total += user.age
b.cnt += 1
b
}
override def merge(b1: CalacBuffer, b2: CalacBuffer): CalacBuffer = {
b1.total += b2.total
b1.cnt += b2.cnt
b1
}
override def finish(buffer: CalacBuffer): Long = {
buffer.total / buffer.cnt
}
override def bufferEncoder: Encoder[CalacBuffer] = Encoders.product
override def outputEncoder: Encoder[Long] = Encoders.scalaLong
}
15.3:通用读取方式和默认读取方式
user.json is not a Parquet file. expected magic number at tail [80, 65, 82, 49] but found [32, 53, 48, 125]
SparkSQL通用读取的方法,读取的文件为Parquet格式(列式存储格式)
val df = spark.read.load("data/users.parquet")
如果想要读取其他格式的文件也可以,需要额外增加参数进行解析
df.write.mode("append").format("json").save("output")
sparkSQL基于SparkCore
SparkCore读取数据是依赖于Hadoop
Hadoop读取数据是按行读取
SparkSQL读取JSON格式的文件,要求每一行数据必须符合JSON格式
读入数据:
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
//方式1:通用的load方法读取
spark.read.format("jdbc")
.option("url", "jdbc:mysql://linux1:3306/spark-sql")
.option("driver", "com.mysql.jdbc.Driver")
.option("user", "root")
.option("password", "123123")
.option("dbtable", "user")
.load().show
//方式2:通用的load方法读取 参数另一种形式
spark.read.format("jdbc")
.options(Map("url"->"jdbc:mysql://linux1:3306/spark-sql?user=root&password=123123",
"dbtable"->"user","driver"->"com.mysql.jdbc.Driver")).load().show
//方式3:使用jdbc方法读取
val props: Properties = new Properties()
props.setProperty("user", "root")
props.setProperty("password", "123123")
val df: Dataframe = spark.read.jdbc("jdbc:mysql://linux1:3306/spark-sql", "user", props)
df.show
//释放资源
spark.stop()
写入数据:
case class User2(name: String, age: Long)
。。。
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
val rdd: RDD[User2] = spark.sparkContext.makeRDD(List(User2("lisi", 20), User2("zs", 30)))
val ds: Dataset[User2] = rdd.toDS
//方式1:通用的方式 format指定写出类型
//方式2:通过jdbc方法
val props: Properties = new Properties()
props.setProperty("user", "root")
props.setProperty("password", "123123")
ds.write.mode(SaveMode.Append).jdbc("jdbc:mysql://linux1:3306/spark-sql", "user", props)
//释放资源
spark.stop()
十六:SparkStreaming(数据模型是离散流DStrem)
实时 离线 批量 流式
Spark是一个离线的批量数据处理的框架
SparkStreaming是一个准实时,微批次的处理框架
16.1:SparkStreaming知识点
Discretized Stream(离散流)是Spark Streaming的基础抽象,代表持续性的数据流和经过各种Spark原语操作后的结果数据流。在内部实现上,DStream是一系列连续的RDD来表示。每个RDD含有一段时间间隔内的数据。
16.2:DStream
blockingQueue队列:阻塞式队列,有反压机制,可以避免数据挤压。
采集数据的方式:DStream监控目录变化,将RDD放入到离散化流DStream中,自定义数据源。
object SparkStreaming05_DIY {
def main(args: Array[String]): Unit = {
//TODO SparkStreaming 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[*]")
.setAppName("Streaming")
.set("spark.testing.memory", "471859200")
//StreamingContext独享的第二个构造参数表示数据采集周期
// val ssc: StreamingContext = new StreamingContext(conf,new Duration(3 * 1000))
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
val uuidDS: ReceiverInputDStream[String] = ssc.receiverStream(new MyReceiver)
uuidDS.print()
//启动采集器
ssc.start()
// 等待采集器结束
ssc.awaitTermination()
}
class MyReceiver extends Receiver[String](StorageLevel.MEMORY_ONLY){
private var flg = true
override def onStart(): Unit = {
//采集数据
while (flg) {
//采集数据
val uuid: String = UUID.randomUUID().toString
//存储数据
store(uuid)
Thread.sleep(1000)
}
}
override def onStop(): Unit = {
//释放资源
flg = false
}
}
}
十七:DStream转换(有无状态转换)
离散化流无法完成排序操作 ,离散化流其实就是每个时间段RDD的封装 ,离散化流可以转换为RDD实现操作
transform用于将离散化流实现不了的功能进行调用,transform方法是DStream的方法,称之为原语,RDD的方法称之为算子 如果想要在Driver端周期性执行一些逻辑的场合,需要使用transform。
将两个窗口key相同的连接起来
使用updateStateByKey需要对检查点目录进行配置,会使用检查点来保存状态。
reduceByKey原语不会保留中间计算结果,一旦采集周期结束,数据丢弃 ,如果想要保留中间计算结果,需要有状态操作
从检查点中恢复数据
def main(args: Array[String]): Unit = {
//从检查点中恢复数据
val outerSSC = StreamingContext.getOrCreate("cp",() => {
//TODO SparkStreaming 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[*]")
.setAppName("Streaming")
.set("spark.testing.memory", "471859200")
//StreamingContext独享的第二个构造参数表示数据采集周期
// val ssc: StreamingContext = new StreamingContext(conf,new Duration(3 * 1000))
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
ssc.checkpoint("cp")
val socketDS: ReceiverInputDStream[String] = ssc.socketTextStream("localhost", 8888)
val wordDS: DStream[String] = socketDS.flatMap(_.split(" "))
val wordToOneDS: DStream[(String, Int)] = wordDS.map((_, 1))
wordToOneDS.updateStateByKey(
(seq : Seq[Int] , buffer : Option[Int]) => {
Option(seq.sum + buffer.getOrElse(0))
}
).print()
ssc
})
//SparkStreaming (reduceByKey) + Redis内存数据库 = 有状态
outerSSC.start()
outerSSC.awaitTermination()
}
十八:DStream窗口
窗口划过的规律是:数据由无到有再到无(先多后少后变没)
DStream离散流有window方法:wordToOneDS.window(Seconds(9),Seconds(3))可以设置窗口的大小。
window操作中,窗口范围和滑动的幅度应该为采集周期的整数倍
如果窗口范围和滑动的幅度相同, 那么就是滚动窗口,没有重复数据 默认情况下,不使用窗口,也会有窗口操作,窗口范围取值为1个采集周期,滑动幅度也是一个采集周期。
如果窗口范围和滑动的幅度不相同, 那么就是滑动窗口
1>窗口范围大于滑动的幅度, 存在重复数据
2>窗口范围小于滑动的幅度, 存在丢失数据的可能
二十:优雅的关闭
对于流式数据,线程的启动和关闭应该是分开的,属于两个线程。
优雅的关闭,不要强制关闭,关闭前,不再接受数据,而是把手头上的数据处理完成之后再关闭。
二十一:DStream输出
如果想要将DStream进行特殊处理,那么可以转换为底层RDD实现
YARN中ApplicationMaster能做到资源和计算的解耦合,Driver和Executor使用的计算框架是Spark,因此也可以使用其它的框架,而YARN只做资源的调度使用
Spark应用程序提交到Yarn环境中执行的时候,一般会有两种部署执行的方式:Client和Cluster。两种模式主要区别在于:Driver程序的运行节点位置。
首先,装饰者模式只会进行功能组合,不会执行,另外RDD的装饰者模式与IO的装饰者模式的区别在于,RDD不保留数据,只对数据进行处理,而IO中有缓冲区可以保留数据。
5.1:执行原理
六:内存磁盘如何分区及存储原理
6.1:算子的创建:
因为简明知意,makeRDD用的比较多
分区数量是如何计算的(只要牵涉的到分区就会出现数据倾斜现象)
七:算子来啦(转换与行为算子)
转换算子就是将旧的RDD转换成为新的RDD,因为要组合功能。
flatMap()保留,map()不保留
不在一个分区是无法进行比较的
map() VS mapPartitions()
mapPartitions()是批量处理的 因此入参为迭代器 出参也为迭代器,map处理一个返回一个,mapPartitions处理一批返回一批
迭代器Iterator也是集合
7.1:groupBy()
groupBy()引出shuffle
shuffle一定会落盘,因为RDD不保留数据,因此,在shuffle阶段,一定会存在Write(shuffle左边)和read(shuffle右边)
所有含有shuffle的算子都有改变分区的能力
因为隐式转换所以字符串可以直接()写下标
7.2:补充零拷贝(NIO实现)页缓存
7.3:flatMap()
7.4:glom
7.5:filter
val fileRDD = sc.textFile("data/apache.log")
// filter算子返回的结果为按照规则保留的数据本身
fileRDD.filter(
line => {
val datas = line.split(" ")
val time = datas(3)
time.startsWith("17/05/2015")
}
).map(
line => {
val datas = line.split(" ")
datas(6)
}
).collect.foreach(println)
7.6: sample
7.7: distinct
scala使用的是单点集合(缺点,单点的资源有限),而RDD使用的是集群去重
7.8:coalesce(缩小分区) 与 repartition(扩大分区)
7.9:sortBy()
八:双值类型数据集的算子(交集并集差集拉链)
单Value(单一数据集)数据操作,双Value(多个数据集)数据操作
九:Key - Value类型算子(隐式转换rddToPairRDDFunction)
9.1:partitionBy()
repartition : 重分区(分区数量)
partitionBy : 重分区(数据的位置 数据进入到哪个分区), 数据路由(Hash定位),分区器 如果想要让数据重新进行分区,那么需要传递分区规则对象(分区器)
Spark中默认常用的分区器:RangePartitioner(要求数据必须能排序) & HashPartitioner(默认)
思考一个问题:如果重分区的分区器和当前RDD的分区器一样怎么办?
此时不做任何处理,还是当前分区,不需要重新shuffle
9.2:reduceByKey groupByKey
TODO reduceByKey算子用于将数据集中相同的key的value数据聚合在一起实现两两聚合
spark的算子有很多是以byKey结尾的,就是说将K-V独立使用
TODO reduceByKey算子可以实现 WordCount
9.3:aggregateByKey foldByKey
需求 数据集中相同的Key的分区内取最大值,分区间求和
reduceByKey groupByKey只考虑key不考虑分区而reduceByKey 在逻辑上,
分区内和分区间是一样的都是聚合 因此reduceByKey groupByKey都不能处理该需求。
需求 如果分区内和分区间的计算规则不一定相同时,Spark提供了一个算子来实现。aggregateByKey
aggregateByKey算子存在函数柯里化,有多个参数列表
第一个参数列表中有一个参数
第一个参数表示计算初始值
第二个参数列表中有二个参数
第一个参数表示 : 分区内计算规则
第二个参数表示 : 分区间计算规则
9.4:combineByKey
reduceByKey,aggregateByKey ,foldByKey, combineByKey本质区别
9.5:sortByKey
9.6:Join leftOuterJoin rightOuterJoin fullOuterJoin
9.7 : cogroup
join算子体现的是数据的关系
join算子将两个数据集中相同的key的数据,连接在一起
zip是将两个数据集位置相同的数据,连接起来与key无关
join 可能存在笛卡尔积 也 存在shuffle()
笛卡尔积 在落盘的时候 如果存在大量相同的key的时候数据量会激增
如果能够使用其它算子实现的功能,那么不推荐使用join
rdd1.join(rdd2).collect().foreach(println)
rdd1.leftOuterJoin(rdd2).collect().foreach(println) rdd1.rightOuterJoin(rdd2).collect().foreach(println) rdd1.fullOuterJoin(rdd2).collect().foreach(println)
cogroup = connect(多个数据集) + group(单一数据集)
9.6:案例实操
方式一:
// 统计出每一个省份每个广告被点击数量排行的Top3
val fileDatas: RDD[String] = sc.textFile("data/agent.log")
val groupDatas: RDD[(String, Iterable[String])] = fileDatas.groupBy(
line => {
val datas: Array[String] = line.split(" ")
datas(1)
}
)
val top3: RDD[(String, List[(String, Int)])] = groupDatas.mapValues(
//数据集是List(line, line, line)
list => {
//数据集是line
val adToCount: Map[String, Int] = list.map(
line => {
val datas: Array[String] = line.split(" ")
(datas(4), 1)
}
//数据集是List((省份,广告)) 按照省份进行分组统计
).groupBy(_._1).mapValues(_.size)
//Map[String, Int] [广告,点击次数]
// 在 groupDatas.mapValues( 的架子中都是对value进行处理的
//元组天生的排序 数据类型[广告,点击次数] 前三条
adToCount.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
}
)
top3.collect().foreach(println)
sc.stop()
}
方式二:
val fileDatas: RDD[String] = sc.textFile("data/agent.log")
// 1. 将广告进行统计分析 (word, cnt)
// ((省份1,广告1), 1),((省份2,广告2), 1)((省份1,广告1), 1)
// ((省份1,广告1), sum),((省份1,广告2), sum)
val reduceDatas: RDD[((String, String), Int)] = fileDatas.map(
line => {
val datas: Array[String] = line.split(" ")
((datas(1), datas(4)), 1)
}
).reduceByKey(_ + _)
// 1.5 将统计结果进行格式转换
// (省份1,(广告1, sum)),(省份1,(广告2, sum))
val mapDatas: RDD[(String, (String, Int))] = reduceDatas.map {
case ((prv, ad), sum) => {
(prv, (ad, sum))
}
}
// 2. 将统计结果按照省份进行分组
// (省份1, (广告1, sum),(广告2, sum))
val prvGroupDatas: RDD[(String, Iterable[(String, Int)])] = mapDatas.groupByKey()
// 3. 将分组后的数据按照点击数量进行排序(降序),取前3名
val top3: RDD[(String, List[(String, Int)])] = prvGroupDatas.mapValues(
iter => {
iter.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
}
)
top3.collect().foreach(println)
十:action行动算子(是对结果进行处理的)
TODO : 所谓的行动(Action)算子,其实就是RDD用于触发作业执行的方法,类似于IO中的read方法
转换算子的返回值为RDD 行动算子的返回值为具体的结果
行动算子和作业的关系 :1 对 1
10.2:reduce
reduce算子可以触发作业的执行,分区内先两两计算,分区间再两两计算
分区内计算都是在Executor。
分区间计算都是在Driver
10.3:count first take takeOrdered
10.4: aggregate fold
10.5: countByKey countByValue
10.5: save相关的算子
rdd.saveAsTextFile("output1")
rdd.saveAsObjectFile("output2")
rdd.saveAsSequenceFile("output3")
10.5: collect foreach
10.6: 序列化闭包检测
一个函数使用了外部的数据,并且改变了这个数据的生命周期 将数据包含到函数内部,形成了一个闭合的环境,这个环境称之为闭包环境,简称闭包
Spark在运行作业之前,会判断程序逻辑中是否包含闭包
如果存在闭包操作,那么需要检测闭包中使用的数据是否能够序列化,如果不能,那么发生错误
这个操作称之为闭包检测
十一:RDD阶段的划分
前一个阶段不执行完,后面的阶段是无法执行的。
十二:依赖关系和血缘关系(宽依赖窄依赖)
十三:持久化
(持久化只针对于当前应用程序,资源使用完之后释放资源,不会给其它节点使用。因此诞生了检查点)
将计算结果缓存起来。
数据的持久化默认将数据保存到内存中,重复使用
wordToOne.cache()
持久化操作也可以保存到磁盘中
wordToOne.persist()
保存级别设置
十三:RDD CheckPoint检查点
java中的finalize方法是Object类中提供的一个方法,在GC准备释放对象所占用的内存空间之前,它将首先调用finalize()方法。
持久化只针对于当前应用程序,资源使用完之后释放资源,不会给其它节点使用。
持久化只针对于当前应用程序
跨应用程序的数据共享需要采用 检查点机制
设置的方式
① :sc.setCheckpointDir("cp") 该检查点的目录一般为分布式目录以便各个节点使用
②:wordToOne.checkpoint()
持久化操作会在血缘关系中增加依赖
检查点一般会将数据保存到分布式文件系统中(HDFS)(新的数据源)
检查点会切断血缘即减少血缘依赖,因为可以把HDFS中相对可靠的数据作为新的数据源。
十四:自定义分区 RDD文件读取与保存
Hash分区:对于给定的key,计算其hashCode,并除以分区个数取余
Range分区:将一定范围内的数据映射到一个分区中,尽量保证每个分区数据均匀,而且分区间有序
十四:累加器(数据模型)(功能比较单一没有shuffle性能相对高)
累加器的现象
分析:
如果数据和RDD有关系,那么可以从Driver端发送到Executor端执行
如果执行完毕,那么可以将结果拉回到Driver端
如果数据和RDD没有关系,因为计算中存在闭包操作,所以RDD会将数据发送到Executor
但是无法将数据从Executor端拉回到Driver端。
如果想要将和RDD无关的数据拉回到Driver端,RDD模型处理不了,需要采用新的数据模型——累加器
doubleAccumulator
collectionAccumulator 累加器
def main(args: Array[String]): Unit = {
// TODO Spark 环境
val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(conf)
// TODO 1. 创建累加器
val wordCountAcc = new MyAcculumator
// TODO 2. 将累加器注册到Spark中
sc.register(wordCountAcc, "WordCount")
val rdd = sc.makeRDD(
List(
"Hello",
"Hello",
"Hello",
"World",
"Hello",
),2
)
rdd.foreach(
word => {
// TODO 3. 使用累加器
wordCountAcc.add(word)
}
)
// TODO 4. 获取累加器的结果
println(wordCountAcc.value)
sc.stop()
}
// TODO 自定义数据累加器(WordCount)
// 1. 继承AccumulatorV2
// 2. 定义数据的泛型
// IN : String
// OUT : mutable.Map[String, Int]
// 3. 重写方法(3(计算) + 3(状态) = 6)
class MyAcculumator extends AccumulatorV2[String, mutable.Map[String, Int]]{
private val wordCountMap = mutable.Map[String, Int]()
// TODO 判断当前累加器是否为初始状态
override def isZero: Boolean = {
wordCountMap.isEmpty
}
// TODO 复制累加器
override def copy(): AccumulatorV2[String, mutable.Map[String, Int]] = {
new MyAcculumator()
}
// TODO 重置累加器
override def reset(): Unit = {
wordCountMap.clear()
}
// TODO 将外部的数据增加到累加器中
override def add(word: String): Unit = {
val oldCnt: Int = wordCountMap.getOrElse(word, 0)
wordCountMap.update(word, oldCnt + 1)
}
// TODO 将多个累加器进行合并
override def merge(other: AccumulatorV2[String, mutable.Map[String, Int]]): Unit = {
other.value.foreach {
case (word, cnt) => {
val oldCnt: Int = wordCountMap.getOrElse(word, 0)
wordCountMap.update(word, oldCnt + cnt)
}
}
}
// TODO 获取累加器的结果
override def value: mutable.Map[String, Int] = {
wordCountMap
}
}
十五:Join现象引出广播变量
def main(args: Array[String]): Unit = {
// TODO Spark 环境
val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(conf)
val rdd1 = sc.makeRDD(
List(
("a", 1),
("b", 2),
)
)
val rdd2 = sc.makeRDD(
List(
("a", 3),
("b", 4),
)
)
// ("a", 1)
// ( a,(1,3))
//rdd1.join(rdd2).collect().foreach(println)
// 计算的对象是以Task为单位进行数据发送的,所以可能会导致大量的数据冗余
// 所以如果计算对象占用比较大的资源,性能会急剧下降
// 为了提供数据访问效率,可以将数据发送给Executor,而不是发给Task。
// RDD数据模型只能给Task发。只能采用其他的数据模型:TODO 广播变量
val map = mutable.Map[String, Int](
("a", 3),
("b", 4)
)
// TODO 使用广播变量
val bc: Broadcast[mutable.Map[String, Int]] = sc.broadcast(map)
rdd1.map {
case (k, v1) => {
// TODO 获取广播变量的值
var v2 = bc.value.getOrElse(k, 0)
(k, (v1, v2))
}
}.collect.foreach(println)
sc.stop()
}
计算的对象是以Task为单位进行数据发送的,所以可能会导致大量的数据冗余 且 如果计算对象占用比较大的资源,性能会急剧下降
为了提供数据访问效率,可以将数据发送给Executor,而不是发给Task。
但是RDD数据模型只能给Task发。只能采用其他的数据模型: 广播变量(可以将数据发送给Executor 使得同一个节点上的Task可以共享一份资源,并且如果有Executor节点与Driver通信比较慢的时候 可以从就近的Executor上获得需要的资源)
十五:SparkSQL(Dataframe DataSet RDD 转换)
Spark + Hive => Shark
Spark SQL是Spark用于结构化数据(structured data)处理的Spark模块。
1.2 SparkSQL特点:
无缝的整合了 SQL 查询和 Spark 编程
SparkSQL可以简化RDD的开发,提高开发效率
15.1:Dataframe 与 DataSet 与 RDD概述
SQL语言本身没有类型
工厂模式生产的对象是有相同的功能的,因为从同一条生产线下来的。
Builder模式要生产某个对象需要经过复杂的过程,但是builder模式会把这些过程顺序确定下来。
Scala中的构造方法有两种,主构造方法和辅助构造方法,辅助构造方法的函数名一定为this。
15.2:Dataframe 与 DataSet 与 RDD
Dataframe : 是SparkSQL中的核心数据模型,是弱类型的模型,不关心数据的类型操作。
因为SQL的局限性,因此在处理数据的时候,Dataframe提供了一种DSL语法(面向对象的语法)
SparkSQL 与 RDD的关系与转换
SparkSQL模块等同于是对SparkCore一个封装,专门用于结构化数据处理的场景
Dataset : 就是使用面向对象的方式操作数据, 类,属性 => 结构 就是
在Dataframe的基础上,增加了类型信息
SparkSQL中存在大量的隐式转换操作,但是一般会导入使用 import时,使用的spark并不是包名,是环境对象的名称。
Dataframe 与 DataSet的区别?
Dataframe是特定类型的DataSet[Row]
说明DataSet兼容Dataframe()是将来用的比较多的
注意:如果从内存中获取数据,spark可以知道数据类型具体是什么。如果是数字,默认作为Int处理;但是从文件中读取的数字,不能确定是什么类型,所以用bigint接收,可以和Long类型转换,但是和Int不能进行转换
SparkSQL技术选型:
15.2:UDF 与 UDAF
package com.atguigu.bigdata.spark.sql01
import org.apache.spark.sql.expressions.Aggregator
import org.apache.spark.sql.{Dataframe, Encoder, Encoders, SparkSession, functions}
object SparkSQL09_Dataframe_UDAF {
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
df.createOrReplaceTempView("user")
// TODO UDAF使用时,需要采用特殊的方式实现自定义操作
// TODO 强类型操作不能应用于弱类型操作,如果想用,那么需要转换functions.udaf
// Spark3.0以后,可以通过转换,让强类型应用在弱类型操作
spark.udf.register("avgAge",functions.udaf(new AvgAgeUDF()))
// TODO : UDF
// 自定义的函数在SQL文使用
spark.sql("select avgAge(age) from user").show()
spark.stop()
}
case class CalcBuffer(var total : Long , var cnt : Long)
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 1. 继承org.apache.spark.sql.expressions.Aggregator
// 2. 定义泛型
// IN : Long
// BUFF : CalcBuffer
// OUT : Long
// 3. 重写方法 (4 + 2(固定) = 6)
class AvgAgeUDF extends Aggregator[Long,CalcBuffer,Long]{
// TODO 缓冲区的初始化
override def zero: CalcBuffer = {
CalcBuffer(0l,0l)
}
// TODO 将输入的数据和缓冲区的数据进行聚合操作
override def reduce(buff: CalcBuffer, a: Long): CalcBuffer ={
buff.total += a
buff.cnt += 1
buff
}
// TODO 多个缓冲区的合并操作
override def merge(b1: CalcBuffer, b2: CalcBuffer): CalcBuffer = {
b1.total += b2.total
b1.cnt += b2.cnt
b1
}
// TODO 计算数据
override def finish(reduction: CalcBuffer): Long = {
reduction.total / reduction.cnt
}
override def bufferEncoder: Encoder[CalcBuffer] = Encoders.product
override def outputEncoder: Encoder[Long] = Encoders.scalaLong
}
}
UDAF老版本:
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
df.createOrReplaceTempView("user")
// TODO UDAF使用时,需要采用特殊的方式实现自定义操作
// TODO 强类型操作不能应用于弱类型操作,如果想用,那么需要转换functions.udaf
// Spark3.0以后,可以通过转换,让强类型应用在弱类型操作
spark.udf.register("avgAge",new AvgAgeUDAF())
// TODO : UDF
// 自定义的函数在SQL文使用
spark.sql("select avgAge(age) from user").show()
spark.stop()
}
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 使用早期Spark版本的UDAF(弱类型)
// 1. 继承UserDefinedAggregateFunction
// 2. 重写方法(8)
class AvgAgeUDAF extends UserDefinedAggregateFunction{
// TODO 输入数据的结构
override def inputSchema: StructType = {
StructType(
Array(
StructField("age",LongType)
)
)
}
// TODO 缓冲区数据的结构
override def bufferSchema: StructType = {
StructType(
Array(
StructField("total",LongType),
StructField("cnt",LongType)
)
)
}
// TODO 输出数据的类型
override def dataType: DataType = LongType
// TODO 计算稳定性
override def deterministic: Boolean = true
// TODO 缓冲区的初始化
override def initialize(buffer: MutableAggregationBuffer): Unit = {
buffer.update(0,0l)
buffer.update(1,0l)
}
// TODO 将输入的数据和缓冲区的数据进行聚合操作
override def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
buffer.update(0,buffer.getLong(0) + input.getLong(0))
buffer.update(1,buffer.getLong(1)+1)
}
// TODO 多个缓冲区的合并操作
override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
buffer1.update(0,buffer1.getLong(0) + buffer2.getLong(0))
buffer1.update(1,buffer1.getLong(1) + buffer2.getLong(1))
}
override def evaluate(buffer: Row): Any = {
buffer.getLong(0) / buffer.getLong(1)
}
}
UDAF_Class老版本:
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
// TODO Spark3.0版本前,强类型的UDAF不能应用于弱类型的SQL中
import spark.implicits._
val udaf: AvgAgeUDAF = new AvgAgeUDAF
val ds: Dataset[User] = df.as[User]
ds.select(udaf.toColumn).show()
spark.stop()
}
case class User(id : Long , name : String , age : Long)
case class CalacBuffer(var total : Long , var cnt : Long)
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 使用早期Spark版本的UDAF(强类型)
// 1. 继承Aggregator
// 2. 定义泛型
// IN : User
// BUFF : CalcBuffer
// OUT : Long
// 3. 重写方法(6)
class AvgAgeUDAF extends Aggregator[User,CalacBuffer,Long]{
override def zero: CalacBuffer = {
CalacBuffer(0l,0L)
}
override def reduce(b: CalacBuffer, user: User): CalacBuffer = {
b.total += user.age
b.cnt += 1
b
}
override def merge(b1: CalacBuffer, b2: CalacBuffer): CalacBuffer = {
b1.total += b2.total
b1.cnt += b2.cnt
b1
}
override def finish(buffer: CalacBuffer): Long = {
buffer.total / buffer.cnt
}
override def bufferEncoder: Encoder[CalacBuffer] = Encoders.product
override def outputEncoder: Encoder[Long] = Encoders.scalaLong
}
15.3:通用读取方式和默认读取方式
user.json is not a Parquet file. expected magic number at tail [80, 65, 82, 49] but found [32, 53, 48, 125]
SparkSQL通用读取的方法,读取的文件为Parquet格式(列式存储格式)
val df = spark.read.load("data/users.parquet")
如果想要读取其他格式的文件也可以,需要额外增加参数进行解析
df.write.mode("append").format("json").save("output")
sparkSQL基于SparkCore
SparkCore读取数据是依赖于Hadoop
Hadoop读取数据是按行读取
SparkSQL读取JSON格式的文件,要求每一行数据必须符合JSON格式
读入数据:
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
//方式1:通用的load方法读取
spark.read.format("jdbc")
.option("url", "jdbc:mysql://linux1:3306/spark-sql")
.option("driver", "com.mysql.jdbc.Driver")
.option("user", "root")
.option("password", "123123")
.option("dbtable", "user")
.load().show
//方式2:通用的load方法读取 参数另一种形式
spark.read.format("jdbc")
.options(Map("url"->"jdbc:mysql://linux1:3306/spark-sql?user=root&password=123123",
"dbtable"->"user","driver"->"com.mysql.jdbc.Driver")).load().show
//方式3:使用jdbc方法读取
val props: Properties = new Properties()
props.setProperty("user", "root")
props.setProperty("password", "123123")
val df: Dataframe = spark.read.jdbc("jdbc:mysql://linux1:3306/spark-sql", "user", props)
df.show
//释放资源
spark.stop()
写入数据:
case class User2(name: String, age: Long)
。。。
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
val rdd: RDD[User2] = spark.sparkContext.makeRDD(List(User2("lisi", 20), User2("zs", 30)))
val ds: Dataset[User2] = rdd.toDS
//方式1:通用的方式 format指定写出类型
//方式2:通过jdbc方法
val props: Properties = new Properties()
props.setProperty("user", "root")
props.setProperty("password", "123123")
ds.write.mode(SaveMode.Append).jdbc("jdbc:mysql://linux1:3306/spark-sql", "user", props)
//释放资源
spark.stop()
十六:SparkStreaming(数据模型是离散流DStrem)
实时 离线 批量 流式
Spark是一个离线的批量数据处理的框架
SparkStreaming是一个准实时,微批次的处理框架
16.1:SparkStreaming知识点
Discretized Stream(离散流)是Spark Streaming的基础抽象,代表持续性的数据流和经过各种Spark原语操作后的结果数据流。在内部实现上,DStream是一系列连续的RDD来表示。每个RDD含有一段时间间隔内的数据。
16.2:DStream
blockingQueue队列:阻塞式队列,有反压机制,可以避免数据挤压。
采集数据的方式:DStream监控目录变化,将RDD放入到离散化流DStream中,自定义数据源。
object SparkStreaming05_DIY {
def main(args: Array[String]): Unit = {
//TODO SparkStreaming 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[*]")
.setAppName("Streaming")
.set("spark.testing.memory", "471859200")
//StreamingContext独享的第二个构造参数表示数据采集周期
// val ssc: StreamingContext = new StreamingContext(conf,new Duration(3 * 1000))
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
val uuidDS: ReceiverInputDStream[String] = ssc.receiverStream(new MyReceiver)
uuidDS.print()
//启动采集器
ssc.start()
// 等待采集器结束
ssc.awaitTermination()
}
class MyReceiver extends Receiver[String](StorageLevel.MEMORY_ONLY){
private var flg = true
override def onStart(): Unit = {
//采集数据
while (flg) {
//采集数据
val uuid: String = UUID.randomUUID().toString
//存储数据
store(uuid)
Thread.sleep(1000)
}
}
override def onStop(): Unit = {
//释放资源
flg = false
}
}
}
十七:DStream转换(有无状态转换)
离散化流无法完成排序操作 ,离散化流其实就是每个时间段RDD的封装 ,离散化流可以转换为RDD实现操作
transform用于将离散化流实现不了的功能进行调用,transform方法是DStream的方法,称之为原语,RDD的方法称之为算子 如果想要在Driver端周期性执行一些逻辑的场合,需要使用transform。
将两个窗口key相同的连接起来
使用updateStateByKey需要对检查点目录进行配置,会使用检查点来保存状态。
reduceByKey原语不会保留中间计算结果,一旦采集周期结束,数据丢弃 ,如果想要保留中间计算结果,需要有状态操作
从检查点中恢复数据
def main(args: Array[String]): Unit = {
//从检查点中恢复数据
val outerSSC = StreamingContext.getOrCreate("cp",() => {
//TODO SparkStreaming 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[*]")
.setAppName("Streaming")
.set("spark.testing.memory", "471859200")
//StreamingContext独享的第二个构造参数表示数据采集周期
// val ssc: StreamingContext = new StreamingContext(conf,new Duration(3 * 1000))
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
ssc.checkpoint("cp")
val socketDS: ReceiverInputDStream[String] = ssc.socketTextStream("localhost", 8888)
val wordDS: DStream[String] = socketDS.flatMap(_.split(" "))
val wordToOneDS: DStream[(String, Int)] = wordDS.map((_, 1))
wordToOneDS.updateStateByKey(
(seq : Seq[Int] , buffer : Option[Int]) => {
Option(seq.sum + buffer.getOrElse(0))
}
).print()
ssc
})
//SparkStreaming (reduceByKey) + Redis内存数据库 = 有状态
outerSSC.start()
outerSSC.awaitTermination()
}
十八:DStream窗口
窗口划过的规律是:数据由无到有再到无(先多后少后变没)
DStream离散流有window方法:wordToOneDS.window(Seconds(9),Seconds(3))可以设置窗口的大小。
window操作中,窗口范围和滑动的幅度应该为采集周期的整数倍
如果窗口范围和滑动的幅度相同, 那么就是滚动窗口,没有重复数据 默认情况下,不使用窗口,也会有窗口操作,窗口范围取值为1个采集周期,滑动幅度也是一个采集周期。
如果窗口范围和滑动的幅度不相同, 那么就是滑动窗口
1>窗口范围大于滑动的幅度, 存在重复数据
2>窗口范围小于滑动的幅度, 存在丢失数据的可能
二十:优雅的关闭
对于流式数据,线程的启动和关闭应该是分开的,属于两个线程。
优雅的关闭,不要强制关闭,关闭前,不再接受数据,而是把手头上的数据处理完成之后再关闭。
二十一:DStream输出
如果想要将DStream进行特殊处理,那么可以转换为底层RDD实现
6.1:算子的创建:
因为简明知意,makeRDD用的比较多
分区数量是如何计算的(只要牵涉的到分区就会出现数据倾斜现象)
七:算子来啦(转换与行为算子)
转换算子就是将旧的RDD转换成为新的RDD,因为要组合功能。
flatMap()保留,map()不保留
不在一个分区是无法进行比较的
map() VS mapPartitions()
mapPartitions()是批量处理的 因此入参为迭代器 出参也为迭代器,map处理一个返回一个,mapPartitions处理一批返回一批
迭代器Iterator也是集合
7.1:groupBy()
groupBy()引出shuffle
shuffle一定会落盘,因为RDD不保留数据,因此,在shuffle阶段,一定会存在Write(shuffle左边)和read(shuffle右边)
所有含有shuffle的算子都有改变分区的能力
因为隐式转换所以字符串可以直接()写下标
7.2:补充零拷贝(NIO实现)页缓存
7.3:flatMap()
7.4:glom
7.5:filter
val fileRDD = sc.textFile("data/apache.log")
// filter算子返回的结果为按照规则保留的数据本身
fileRDD.filter(
line => {
val datas = line.split(" ")
val time = datas(3)
time.startsWith("17/05/2015")
}
).map(
line => {
val datas = line.split(" ")
datas(6)
}
).collect.foreach(println)
7.6: sample
7.7: distinct
scala使用的是单点集合(缺点,单点的资源有限),而RDD使用的是集群去重
7.8:coalesce(缩小分区) 与 repartition(扩大分区)
7.9:sortBy()
八:双值类型数据集的算子(交集并集差集拉链)
单Value(单一数据集)数据操作,双Value(多个数据集)数据操作
九:Key - Value类型算子(隐式转换rddToPairRDDFunction)
9.1:partitionBy()
repartition : 重分区(分区数量)
partitionBy : 重分区(数据的位置 数据进入到哪个分区), 数据路由(Hash定位),分区器 如果想要让数据重新进行分区,那么需要传递分区规则对象(分区器)
Spark中默认常用的分区器:RangePartitioner(要求数据必须能排序) & HashPartitioner(默认)
思考一个问题:如果重分区的分区器和当前RDD的分区器一样怎么办?
此时不做任何处理,还是当前分区,不需要重新shuffle
9.2:reduceByKey groupByKey
TODO reduceByKey算子用于将数据集中相同的key的value数据聚合在一起实现两两聚合
spark的算子有很多是以byKey结尾的,就是说将K-V独立使用
TODO reduceByKey算子可以实现 WordCount
9.3:aggregateByKey foldByKey
需求 数据集中相同的Key的分区内取最大值,分区间求和
reduceByKey groupByKey只考虑key不考虑分区而reduceByKey 在逻辑上,
分区内和分区间是一样的都是聚合 因此reduceByKey groupByKey都不能处理该需求。
需求 如果分区内和分区间的计算规则不一定相同时,Spark提供了一个算子来实现。aggregateByKey
aggregateByKey算子存在函数柯里化,有多个参数列表
第一个参数列表中有一个参数
第一个参数表示计算初始值
第二个参数列表中有二个参数
第一个参数表示 : 分区内计算规则
第二个参数表示 : 分区间计算规则
9.4:combineByKey
reduceByKey,aggregateByKey ,foldByKey, combineByKey本质区别
9.5:sortByKey
9.6:Join leftOuterJoin rightOuterJoin fullOuterJoin
9.7 : cogroup
join算子体现的是数据的关系
join算子将两个数据集中相同的key的数据,连接在一起
zip是将两个数据集位置相同的数据,连接起来与key无关
join 可能存在笛卡尔积 也 存在shuffle()
笛卡尔积 在落盘的时候 如果存在大量相同的key的时候数据量会激增
如果能够使用其它算子实现的功能,那么不推荐使用join
rdd1.join(rdd2).collect().foreach(println)
rdd1.leftOuterJoin(rdd2).collect().foreach(println) rdd1.rightOuterJoin(rdd2).collect().foreach(println) rdd1.fullOuterJoin(rdd2).collect().foreach(println)
cogroup = connect(多个数据集) + group(单一数据集)
9.6:案例实操
方式一:
// 统计出每一个省份每个广告被点击数量排行的Top3
val fileDatas: RDD[String] = sc.textFile("data/agent.log")
val groupDatas: RDD[(String, Iterable[String])] = fileDatas.groupBy(
line => {
val datas: Array[String] = line.split(" ")
datas(1)
}
)
val top3: RDD[(String, List[(String, Int)])] = groupDatas.mapValues(
//数据集是List(line, line, line)
list => {
//数据集是line
val adToCount: Map[String, Int] = list.map(
line => {
val datas: Array[String] = line.split(" ")
(datas(4), 1)
}
//数据集是List((省份,广告)) 按照省份进行分组统计
).groupBy(_._1).mapValues(_.size)
//Map[String, Int] [广告,点击次数]
// 在 groupDatas.mapValues( 的架子中都是对value进行处理的
//元组天生的排序 数据类型[广告,点击次数] 前三条
adToCount.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
}
)
top3.collect().foreach(println)
sc.stop()
}
方式二:
val fileDatas: RDD[String] = sc.textFile("data/agent.log")
// 1. 将广告进行统计分析 (word, cnt)
// ((省份1,广告1), 1),((省份2,广告2), 1)((省份1,广告1), 1)
// ((省份1,广告1), sum),((省份1,广告2), sum)
val reduceDatas: RDD[((String, String), Int)] = fileDatas.map(
line => {
val datas: Array[String] = line.split(" ")
((datas(1), datas(4)), 1)
}
).reduceByKey(_ + _)
// 1.5 将统计结果进行格式转换
// (省份1,(广告1, sum)),(省份1,(广告2, sum))
val mapDatas: RDD[(String, (String, Int))] = reduceDatas.map {
case ((prv, ad), sum) => {
(prv, (ad, sum))
}
}
// 2. 将统计结果按照省份进行分组
// (省份1, (广告1, sum),(广告2, sum))
val prvGroupDatas: RDD[(String, Iterable[(String, Int)])] = mapDatas.groupByKey()
// 3. 将分组后的数据按照点击数量进行排序(降序),取前3名
val top3: RDD[(String, List[(String, Int)])] = prvGroupDatas.mapValues(
iter => {
iter.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
}
)
top3.collect().foreach(println)
十:action行动算子(是对结果进行处理的)
TODO : 所谓的行动(Action)算子,其实就是RDD用于触发作业执行的方法,类似于IO中的read方法
转换算子的返回值为RDD 行动算子的返回值为具体的结果
行动算子和作业的关系 :1 对 1
10.2:reduce
reduce算子可以触发作业的执行,分区内先两两计算,分区间再两两计算
分区内计算都是在Executor。
分区间计算都是在Driver
10.3:count first take takeOrdered
10.4: aggregate fold
10.5: countByKey countByValue
10.5: save相关的算子
rdd.saveAsTextFile("output1")
rdd.saveAsObjectFile("output2")
rdd.saveAsSequenceFile("output3")
10.5: collect foreach
10.6: 序列化闭包检测
一个函数使用了外部的数据,并且改变了这个数据的生命周期 将数据包含到函数内部,形成了一个闭合的环境,这个环境称之为闭包环境,简称闭包
Spark在运行作业之前,会判断程序逻辑中是否包含闭包
如果存在闭包操作,那么需要检测闭包中使用的数据是否能够序列化,如果不能,那么发生错误
这个操作称之为闭包检测
十一:RDD阶段的划分
前一个阶段不执行完,后面的阶段是无法执行的。
十二:依赖关系和血缘关系(宽依赖窄依赖)
十三:持久化
(持久化只针对于当前应用程序,资源使用完之后释放资源,不会给其它节点使用。因此诞生了检查点)
将计算结果缓存起来。
数据的持久化默认将数据保存到内存中,重复使用
wordToOne.cache()
持久化操作也可以保存到磁盘中
wordToOne.persist()
保存级别设置
十三:RDD CheckPoint检查点
java中的finalize方法是Object类中提供的一个方法,在GC准备释放对象所占用的内存空间之前,它将首先调用finalize()方法。
持久化只针对于当前应用程序,资源使用完之后释放资源,不会给其它节点使用。
持久化只针对于当前应用程序
跨应用程序的数据共享需要采用 检查点机制
设置的方式
① :sc.setCheckpointDir("cp") 该检查点的目录一般为分布式目录以便各个节点使用
②:wordToOne.checkpoint()
持久化操作会在血缘关系中增加依赖
检查点一般会将数据保存到分布式文件系统中(HDFS)(新的数据源)
检查点会切断血缘即减少血缘依赖,因为可以把HDFS中相对可靠的数据作为新的数据源。
十四:自定义分区 RDD文件读取与保存
Hash分区:对于给定的key,计算其hashCode,并除以分区个数取余
Range分区:将一定范围内的数据映射到一个分区中,尽量保证每个分区数据均匀,而且分区间有序
十四:累加器(数据模型)(功能比较单一没有shuffle性能相对高)
累加器的现象
分析:
如果数据和RDD有关系,那么可以从Driver端发送到Executor端执行
如果执行完毕,那么可以将结果拉回到Driver端
如果数据和RDD没有关系,因为计算中存在闭包操作,所以RDD会将数据发送到Executor
但是无法将数据从Executor端拉回到Driver端。
如果想要将和RDD无关的数据拉回到Driver端,RDD模型处理不了,需要采用新的数据模型——累加器
doubleAccumulator
collectionAccumulator 累加器
def main(args: Array[String]): Unit = {
// TODO Spark 环境
val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(conf)
// TODO 1. 创建累加器
val wordCountAcc = new MyAcculumator
// TODO 2. 将累加器注册到Spark中
sc.register(wordCountAcc, "WordCount")
val rdd = sc.makeRDD(
List(
"Hello",
"Hello",
"Hello",
"World",
"Hello",
),2
)
rdd.foreach(
word => {
// TODO 3. 使用累加器
wordCountAcc.add(word)
}
)
// TODO 4. 获取累加器的结果
println(wordCountAcc.value)
sc.stop()
}
// TODO 自定义数据累加器(WordCount)
// 1. 继承AccumulatorV2
// 2. 定义数据的泛型
// IN : String
// OUT : mutable.Map[String, Int]
// 3. 重写方法(3(计算) + 3(状态) = 6)
class MyAcculumator extends AccumulatorV2[String, mutable.Map[String, Int]]{
private val wordCountMap = mutable.Map[String, Int]()
// TODO 判断当前累加器是否为初始状态
override def isZero: Boolean = {
wordCountMap.isEmpty
}
// TODO 复制累加器
override def copy(): AccumulatorV2[String, mutable.Map[String, Int]] = {
new MyAcculumator()
}
// TODO 重置累加器
override def reset(): Unit = {
wordCountMap.clear()
}
// TODO 将外部的数据增加到累加器中
override def add(word: String): Unit = {
val oldCnt: Int = wordCountMap.getOrElse(word, 0)
wordCountMap.update(word, oldCnt + 1)
}
// TODO 将多个累加器进行合并
override def merge(other: AccumulatorV2[String, mutable.Map[String, Int]]): Unit = {
other.value.foreach {
case (word, cnt) => {
val oldCnt: Int = wordCountMap.getOrElse(word, 0)
wordCountMap.update(word, oldCnt + cnt)
}
}
}
// TODO 获取累加器的结果
override def value: mutable.Map[String, Int] = {
wordCountMap
}
}
十五:Join现象引出广播变量
def main(args: Array[String]): Unit = {
// TODO Spark 环境
val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(conf)
val rdd1 = sc.makeRDD(
List(
("a", 1),
("b", 2),
)
)
val rdd2 = sc.makeRDD(
List(
("a", 3),
("b", 4),
)
)
// ("a", 1)
// ( a,(1,3))
//rdd1.join(rdd2).collect().foreach(println)
// 计算的对象是以Task为单位进行数据发送的,所以可能会导致大量的数据冗余
// 所以如果计算对象占用比较大的资源,性能会急剧下降
// 为了提供数据访问效率,可以将数据发送给Executor,而不是发给Task。
// RDD数据模型只能给Task发。只能采用其他的数据模型:TODO 广播变量
val map = mutable.Map[String, Int](
("a", 3),
("b", 4)
)
// TODO 使用广播变量
val bc: Broadcast[mutable.Map[String, Int]] = sc.broadcast(map)
rdd1.map {
case (k, v1) => {
// TODO 获取广播变量的值
var v2 = bc.value.getOrElse(k, 0)
(k, (v1, v2))
}
}.collect.foreach(println)
sc.stop()
}
计算的对象是以Task为单位进行数据发送的,所以可能会导致大量的数据冗余 且 如果计算对象占用比较大的资源,性能会急剧下降
为了提供数据访问效率,可以将数据发送给Executor,而不是发给Task。
但是RDD数据模型只能给Task发。只能采用其他的数据模型: 广播变量(可以将数据发送给Executor 使得同一个节点上的Task可以共享一份资源,并且如果有Executor节点与Driver通信比较慢的时候 可以从就近的Executor上获得需要的资源)
十五:SparkSQL(Dataframe DataSet RDD 转换)
Spark + Hive => Shark
Spark SQL是Spark用于结构化数据(structured data)处理的Spark模块。
1.2 SparkSQL特点:
无缝的整合了 SQL 查询和 Spark 编程
SparkSQL可以简化RDD的开发,提高开发效率
15.1:Dataframe 与 DataSet 与 RDD概述
SQL语言本身没有类型
工厂模式生产的对象是有相同的功能的,因为从同一条生产线下来的。
Builder模式要生产某个对象需要经过复杂的过程,但是builder模式会把这些过程顺序确定下来。
Scala中的构造方法有两种,主构造方法和辅助构造方法,辅助构造方法的函数名一定为this。
15.2:Dataframe 与 DataSet 与 RDD
Dataframe : 是SparkSQL中的核心数据模型,是弱类型的模型,不关心数据的类型操作。
因为SQL的局限性,因此在处理数据的时候,Dataframe提供了一种DSL语法(面向对象的语法)
SparkSQL 与 RDD的关系与转换
SparkSQL模块等同于是对SparkCore一个封装,专门用于结构化数据处理的场景
Dataset : 就是使用面向对象的方式操作数据, 类,属性 => 结构 就是
在Dataframe的基础上,增加了类型信息
SparkSQL中存在大量的隐式转换操作,但是一般会导入使用 import时,使用的spark并不是包名,是环境对象的名称。
Dataframe 与 DataSet的区别?
Dataframe是特定类型的DataSet[Row]
说明DataSet兼容Dataframe()是将来用的比较多的
注意:如果从内存中获取数据,spark可以知道数据类型具体是什么。如果是数字,默认作为Int处理;但是从文件中读取的数字,不能确定是什么类型,所以用bigint接收,可以和Long类型转换,但是和Int不能进行转换
SparkSQL技术选型:
15.2:UDF 与 UDAF
package com.atguigu.bigdata.spark.sql01
import org.apache.spark.sql.expressions.Aggregator
import org.apache.spark.sql.{Dataframe, Encoder, Encoders, SparkSession, functions}
object SparkSQL09_Dataframe_UDAF {
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
df.createOrReplaceTempView("user")
// TODO UDAF使用时,需要采用特殊的方式实现自定义操作
// TODO 强类型操作不能应用于弱类型操作,如果想用,那么需要转换functions.udaf
// Spark3.0以后,可以通过转换,让强类型应用在弱类型操作
spark.udf.register("avgAge",functions.udaf(new AvgAgeUDF()))
// TODO : UDF
// 自定义的函数在SQL文使用
spark.sql("select avgAge(age) from user").show()
spark.stop()
}
case class CalcBuffer(var total : Long , var cnt : Long)
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 1. 继承org.apache.spark.sql.expressions.Aggregator
// 2. 定义泛型
// IN : Long
// BUFF : CalcBuffer
// OUT : Long
// 3. 重写方法 (4 + 2(固定) = 6)
class AvgAgeUDF extends Aggregator[Long,CalcBuffer,Long]{
// TODO 缓冲区的初始化
override def zero: CalcBuffer = {
CalcBuffer(0l,0l)
}
// TODO 将输入的数据和缓冲区的数据进行聚合操作
override def reduce(buff: CalcBuffer, a: Long): CalcBuffer ={
buff.total += a
buff.cnt += 1
buff
}
// TODO 多个缓冲区的合并操作
override def merge(b1: CalcBuffer, b2: CalcBuffer): CalcBuffer = {
b1.total += b2.total
b1.cnt += b2.cnt
b1
}
// TODO 计算数据
override def finish(reduction: CalcBuffer): Long = {
reduction.total / reduction.cnt
}
override def bufferEncoder: Encoder[CalcBuffer] = Encoders.product
override def outputEncoder: Encoder[Long] = Encoders.scalaLong
}
}
UDAF老版本:
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
df.createOrReplaceTempView("user")
// TODO UDAF使用时,需要采用特殊的方式实现自定义操作
// TODO 强类型操作不能应用于弱类型操作,如果想用,那么需要转换functions.udaf
// Spark3.0以后,可以通过转换,让强类型应用在弱类型操作
spark.udf.register("avgAge",new AvgAgeUDAF())
// TODO : UDF
// 自定义的函数在SQL文使用
spark.sql("select avgAge(age) from user").show()
spark.stop()
}
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 使用早期Spark版本的UDAF(弱类型)
// 1. 继承UserDefinedAggregateFunction
// 2. 重写方法(8)
class AvgAgeUDAF extends UserDefinedAggregateFunction{
// TODO 输入数据的结构
override def inputSchema: StructType = {
StructType(
Array(
StructField("age",LongType)
)
)
}
// TODO 缓冲区数据的结构
override def bufferSchema: StructType = {
StructType(
Array(
StructField("total",LongType),
StructField("cnt",LongType)
)
)
}
// TODO 输出数据的类型
override def dataType: DataType = LongType
// TODO 计算稳定性
override def deterministic: Boolean = true
// TODO 缓冲区的初始化
override def initialize(buffer: MutableAggregationBuffer): Unit = {
buffer.update(0,0l)
buffer.update(1,0l)
}
// TODO 将输入的数据和缓冲区的数据进行聚合操作
override def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
buffer.update(0,buffer.getLong(0) + input.getLong(0))
buffer.update(1,buffer.getLong(1)+1)
}
// TODO 多个缓冲区的合并操作
override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
buffer1.update(0,buffer1.getLong(0) + buffer2.getLong(0))
buffer1.update(1,buffer1.getLong(1) + buffer2.getLong(1))
}
override def evaluate(buffer: Row): Any = {
buffer.getLong(0) / buffer.getLong(1)
}
}
UDAF_Class老版本:
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
// TODO Spark3.0版本前,强类型的UDAF不能应用于弱类型的SQL中
import spark.implicits._
val udaf: AvgAgeUDAF = new AvgAgeUDAF
val ds: Dataset[User] = df.as[User]
ds.select(udaf.toColumn).show()
spark.stop()
}
case class User(id : Long , name : String , age : Long)
case class CalacBuffer(var total : Long , var cnt : Long)
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 使用早期Spark版本的UDAF(强类型)
// 1. 继承Aggregator
// 2. 定义泛型
// IN : User
// BUFF : CalcBuffer
// OUT : Long
// 3. 重写方法(6)
class AvgAgeUDAF extends Aggregator[User,CalacBuffer,Long]{
override def zero: CalacBuffer = {
CalacBuffer(0l,0L)
}
override def reduce(b: CalacBuffer, user: User): CalacBuffer = {
b.total += user.age
b.cnt += 1
b
}
override def merge(b1: CalacBuffer, b2: CalacBuffer): CalacBuffer = {
b1.total += b2.total
b1.cnt += b2.cnt
b1
}
override def finish(buffer: CalacBuffer): Long = {
buffer.total / buffer.cnt
}
override def bufferEncoder: Encoder[CalacBuffer] = Encoders.product
override def outputEncoder: Encoder[Long] = Encoders.scalaLong
}
15.3:通用读取方式和默认读取方式
user.json is not a Parquet file. expected magic number at tail [80, 65, 82, 49] but found [32, 53, 48, 125]
SparkSQL通用读取的方法,读取的文件为Parquet格式(列式存储格式)
val df = spark.read.load("data/users.parquet")
如果想要读取其他格式的文件也可以,需要额外增加参数进行解析
df.write.mode("append").format("json").save("output")
sparkSQL基于SparkCore
SparkCore读取数据是依赖于Hadoop
Hadoop读取数据是按行读取
SparkSQL读取JSON格式的文件,要求每一行数据必须符合JSON格式
读入数据:
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
//方式1:通用的load方法读取
spark.read.format("jdbc")
.option("url", "jdbc:mysql://linux1:3306/spark-sql")
.option("driver", "com.mysql.jdbc.Driver")
.option("user", "root")
.option("password", "123123")
.option("dbtable", "user")
.load().show
//方式2:通用的load方法读取 参数另一种形式
spark.read.format("jdbc")
.options(Map("url"->"jdbc:mysql://linux1:3306/spark-sql?user=root&password=123123",
"dbtable"->"user","driver"->"com.mysql.jdbc.Driver")).load().show
//方式3:使用jdbc方法读取
val props: Properties = new Properties()
props.setProperty("user", "root")
props.setProperty("password", "123123")
val df: Dataframe = spark.read.jdbc("jdbc:mysql://linux1:3306/spark-sql", "user", props)
df.show
//释放资源
spark.stop()
写入数据:
case class User2(name: String, age: Long)
。。。
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
val rdd: RDD[User2] = spark.sparkContext.makeRDD(List(User2("lisi", 20), User2("zs", 30)))
val ds: Dataset[User2] = rdd.toDS
//方式1:通用的方式 format指定写出类型
//方式2:通过jdbc方法
val props: Properties = new Properties()
props.setProperty("user", "root")
props.setProperty("password", "123123")
ds.write.mode(SaveMode.Append).jdbc("jdbc:mysql://linux1:3306/spark-sql", "user", props)
//释放资源
spark.stop()
十六:SparkStreaming(数据模型是离散流DStrem)
实时 离线 批量 流式
Spark是一个离线的批量数据处理的框架
SparkStreaming是一个准实时,微批次的处理框架
16.1:SparkStreaming知识点
Discretized Stream(离散流)是Spark Streaming的基础抽象,代表持续性的数据流和经过各种Spark原语操作后的结果数据流。在内部实现上,DStream是一系列连续的RDD来表示。每个RDD含有一段时间间隔内的数据。
16.2:DStream
blockingQueue队列:阻塞式队列,有反压机制,可以避免数据挤压。
采集数据的方式:DStream监控目录变化,将RDD放入到离散化流DStream中,自定义数据源。
object SparkStreaming05_DIY {
def main(args: Array[String]): Unit = {
//TODO SparkStreaming 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[*]")
.setAppName("Streaming")
.set("spark.testing.memory", "471859200")
//StreamingContext独享的第二个构造参数表示数据采集周期
// val ssc: StreamingContext = new StreamingContext(conf,new Duration(3 * 1000))
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
val uuidDS: ReceiverInputDStream[String] = ssc.receiverStream(new MyReceiver)
uuidDS.print()
//启动采集器
ssc.start()
// 等待采集器结束
ssc.awaitTermination()
}
class MyReceiver extends Receiver[String](StorageLevel.MEMORY_ONLY){
private var flg = true
override def onStart(): Unit = {
//采集数据
while (flg) {
//采集数据
val uuid: String = UUID.randomUUID().toString
//存储数据
store(uuid)
Thread.sleep(1000)
}
}
override def onStop(): Unit = {
//释放资源
flg = false
}
}
}
十七:DStream转换(有无状态转换)
离散化流无法完成排序操作 ,离散化流其实就是每个时间段RDD的封装 ,离散化流可以转换为RDD实现操作
transform用于将离散化流实现不了的功能进行调用,transform方法是DStream的方法,称之为原语,RDD的方法称之为算子 如果想要在Driver端周期性执行一些逻辑的场合,需要使用transform。
将两个窗口key相同的连接起来
使用updateStateByKey需要对检查点目录进行配置,会使用检查点来保存状态。
reduceByKey原语不会保留中间计算结果,一旦采集周期结束,数据丢弃 ,如果想要保留中间计算结果,需要有状态操作
从检查点中恢复数据
def main(args: Array[String]): Unit = {
//从检查点中恢复数据
val outerSSC = StreamingContext.getOrCreate("cp",() => {
//TODO SparkStreaming 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[*]")
.setAppName("Streaming")
.set("spark.testing.memory", "471859200")
//StreamingContext独享的第二个构造参数表示数据采集周期
// val ssc: StreamingContext = new StreamingContext(conf,new Duration(3 * 1000))
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
ssc.checkpoint("cp")
val socketDS: ReceiverInputDStream[String] = ssc.socketTextStream("localhost", 8888)
val wordDS: DStream[String] = socketDS.flatMap(_.split(" "))
val wordToOneDS: DStream[(String, Int)] = wordDS.map((_, 1))
wordToOneDS.updateStateByKey(
(seq : Seq[Int] , buffer : Option[Int]) => {
Option(seq.sum + buffer.getOrElse(0))
}
).print()
ssc
})
//SparkStreaming (reduceByKey) + Redis内存数据库 = 有状态
outerSSC.start()
outerSSC.awaitTermination()
}
十八:DStream窗口
窗口划过的规律是:数据由无到有再到无(先多后少后变没)
DStream离散流有window方法:wordToOneDS.window(Seconds(9),Seconds(3))可以设置窗口的大小。
window操作中,窗口范围和滑动的幅度应该为采集周期的整数倍
如果窗口范围和滑动的幅度相同, 那么就是滚动窗口,没有重复数据 默认情况下,不使用窗口,也会有窗口操作,窗口范围取值为1个采集周期,滑动幅度也是一个采集周期。
如果窗口范围和滑动的幅度不相同, 那么就是滑动窗口
1>窗口范围大于滑动的幅度, 存在重复数据
2>窗口范围小于滑动的幅度, 存在丢失数据的可能
二十:优雅的关闭
对于流式数据,线程的启动和关闭应该是分开的,属于两个线程。
优雅的关闭,不要强制关闭,关闭前,不再接受数据,而是把手头上的数据处理完成之后再关闭。
二十一:DStream输出
如果想要将DStream进行特殊处理,那么可以转换为底层RDD实现
转换算子就是将旧的RDD转换成为新的RDD,因为要组合功能。
flatMap()保留,map()不保留不在一个分区是无法进行比较的
map() VS mapPartitions()
mapPartitions()是批量处理的 因此入参为迭代器 出参也为迭代器,map处理一个返回一个,mapPartitions处理一批返回一批
迭代器Iterator也是集合
7.1:groupBy()
groupBy()引出shuffle
shuffle一定会落盘,因为RDD不保留数据,因此,在shuffle阶段,一定会存在Write(shuffle左边)和read(shuffle右边)
所有含有shuffle的算子都有改变分区的能力
因为隐式转换所以字符串可以直接()写下标
7.3:flatMap()
7.4:glom
7.5:filter
val fileRDD = sc.textFile("data/apache.log")
// filter算子返回的结果为按照规则保留的数据本身
fileRDD.filter(
line => {
val datas = line.split(" ")
val time = datas(3)
time.startsWith("17/05/2015")
}
).map(
line => {
val datas = line.split(" ")
datas(6)
}
).collect.foreach(println)
7.6: sample
7.7: distinct
scala使用的是单点集合(缺点,单点的资源有限),而RDD使用的是集群去重
7.8:coalesce(缩小分区) 与 repartition(扩大分区)
7.9:sortBy()
八:双值类型数据集的算子(交集并集差集拉链)
单Value(单一数据集)数据操作,双Value(多个数据集)数据操作
九:Key - Value类型算子(隐式转换rddToPairRDDFunction)
9.1:partitionBy()
repartition : 重分区(分区数量)
partitionBy : 重分区(数据的位置 数据进入到哪个分区), 数据路由(Hash定位),分区器 如果想要让数据重新进行分区,那么需要传递分区规则对象(分区器)
Spark中默认常用的分区器:RangePartitioner(要求数据必须能排序) & HashPartitioner(默认)
思考一个问题:如果重分区的分区器和当前RDD的分区器一样怎么办?
此时不做任何处理,还是当前分区,不需要重新shuffle
9.2:reduceByKey groupByKey
TODO reduceByKey算子用于将数据集中相同的key的value数据聚合在一起实现两两聚合
spark的算子有很多是以byKey结尾的,就是说将K-V独立使用
TODO reduceByKey算子可以实现 WordCount
9.3:aggregateByKey foldByKey
需求 数据集中相同的Key的分区内取最大值,分区间求和
reduceByKey groupByKey只考虑key不考虑分区而reduceByKey 在逻辑上,
分区内和分区间是一样的都是聚合 因此reduceByKey groupByKey都不能处理该需求。
需求 如果分区内和分区间的计算规则不一定相同时,Spark提供了一个算子来实现。aggregateByKey
aggregateByKey算子存在函数柯里化,有多个参数列表
第一个参数列表中有一个参数
第一个参数表示计算初始值
第二个参数列表中有二个参数
第一个参数表示 : 分区内计算规则
第二个参数表示 : 分区间计算规则
9.4:combineByKey
reduceByKey,aggregateByKey ,foldByKey, combineByKey本质区别
9.5:sortByKey
9.6:Join leftOuterJoin rightOuterJoin fullOuterJoin
9.7 : cogroup
join算子体现的是数据的关系
join算子将两个数据集中相同的key的数据,连接在一起
zip是将两个数据集位置相同的数据,连接起来与key无关
join 可能存在笛卡尔积 也 存在shuffle()
笛卡尔积 在落盘的时候 如果存在大量相同的key的时候数据量会激增
如果能够使用其它算子实现的功能,那么不推荐使用join
rdd1.join(rdd2).collect().foreach(println)
rdd1.leftOuterJoin(rdd2).collect().foreach(println) rdd1.rightOuterJoin(rdd2).collect().foreach(println) rdd1.fullOuterJoin(rdd2).collect().foreach(println)
cogroup = connect(多个数据集) + group(单一数据集)
9.6:案例实操
方式一:
// 统计出每一个省份每个广告被点击数量排行的Top3
val fileDatas: RDD[String] = sc.textFile("data/agent.log")
val groupDatas: RDD[(String, Iterable[String])] = fileDatas.groupBy(
line => {
val datas: Array[String] = line.split(" ")
datas(1)
}
)
val top3: RDD[(String, List[(String, Int)])] = groupDatas.mapValues(
//数据集是List(line, line, line)
list => {
//数据集是line
val adToCount: Map[String, Int] = list.map(
line => {
val datas: Array[String] = line.split(" ")
(datas(4), 1)
}
//数据集是List((省份,广告)) 按照省份进行分组统计
).groupBy(_._1).mapValues(_.size)
//Map[String, Int] [广告,点击次数]
// 在 groupDatas.mapValues( 的架子中都是对value进行处理的
//元组天生的排序 数据类型[广告,点击次数] 前三条
adToCount.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
}
)
top3.collect().foreach(println)
sc.stop()
}
方式二:
val fileDatas: RDD[String] = sc.textFile("data/agent.log")
// 1. 将广告进行统计分析 (word, cnt)
// ((省份1,广告1), 1),((省份2,广告2), 1)((省份1,广告1), 1)
// ((省份1,广告1), sum),((省份1,广告2), sum)
val reduceDatas: RDD[((String, String), Int)] = fileDatas.map(
line => {
val datas: Array[String] = line.split(" ")
((datas(1), datas(4)), 1)
}
).reduceByKey(_ + _)
// 1.5 将统计结果进行格式转换
// (省份1,(广告1, sum)),(省份1,(广告2, sum))
val mapDatas: RDD[(String, (String, Int))] = reduceDatas.map {
case ((prv, ad), sum) => {
(prv, (ad, sum))
}
}
// 2. 将统计结果按照省份进行分组
// (省份1, (广告1, sum),(广告2, sum))
val prvGroupDatas: RDD[(String, Iterable[(String, Int)])] = mapDatas.groupByKey()
// 3. 将分组后的数据按照点击数量进行排序(降序),取前3名
val top3: RDD[(String, List[(String, Int)])] = prvGroupDatas.mapValues(
iter => {
iter.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
}
)
top3.collect().foreach(println)
十:action行动算子(是对结果进行处理的)
TODO : 所谓的行动(Action)算子,其实就是RDD用于触发作业执行的方法,类似于IO中的read方法
转换算子的返回值为RDD 行动算子的返回值为具体的结果
行动算子和作业的关系 :1 对 1
10.2:reduce
reduce算子可以触发作业的执行,分区内先两两计算,分区间再两两计算
分区内计算都是在Executor。
分区间计算都是在Driver
10.3:count first take takeOrdered
10.4: aggregate fold
10.5: countByKey countByValue
10.5: save相关的算子
rdd.saveAsTextFile("output1")
rdd.saveAsObjectFile("output2")
rdd.saveAsSequenceFile("output3")
10.5: collect foreach
10.6: 序列化闭包检测
一个函数使用了外部的数据,并且改变了这个数据的生命周期 将数据包含到函数内部,形成了一个闭合的环境,这个环境称之为闭包环境,简称闭包
Spark在运行作业之前,会判断程序逻辑中是否包含闭包
如果存在闭包操作,那么需要检测闭包中使用的数据是否能够序列化,如果不能,那么发生错误
这个操作称之为闭包检测
十一:RDD阶段的划分
前一个阶段不执行完,后面的阶段是无法执行的。
十二:依赖关系和血缘关系(宽依赖窄依赖)
十三:持久化
(持久化只针对于当前应用程序,资源使用完之后释放资源,不会给其它节点使用。因此诞生了检查点)
将计算结果缓存起来。
数据的持久化默认将数据保存到内存中,重复使用
wordToOne.cache()
持久化操作也可以保存到磁盘中
wordToOne.persist()
保存级别设置
十三:RDD CheckPoint检查点
java中的finalize方法是Object类中提供的一个方法,在GC准备释放对象所占用的内存空间之前,它将首先调用finalize()方法。
持久化只针对于当前应用程序,资源使用完之后释放资源,不会给其它节点使用。
持久化只针对于当前应用程序
跨应用程序的数据共享需要采用 检查点机制
设置的方式
① :sc.setCheckpointDir("cp") 该检查点的目录一般为分布式目录以便各个节点使用
②:wordToOne.checkpoint()
持久化操作会在血缘关系中增加依赖
检查点一般会将数据保存到分布式文件系统中(HDFS)(新的数据源)
检查点会切断血缘即减少血缘依赖,因为可以把HDFS中相对可靠的数据作为新的数据源。
十四:自定义分区 RDD文件读取与保存
Hash分区:对于给定的key,计算其hashCode,并除以分区个数取余
Range分区:将一定范围内的数据映射到一个分区中,尽量保证每个分区数据均匀,而且分区间有序
十四:累加器(数据模型)(功能比较单一没有shuffle性能相对高)
累加器的现象
分析:
如果数据和RDD有关系,那么可以从Driver端发送到Executor端执行
如果执行完毕,那么可以将结果拉回到Driver端
如果数据和RDD没有关系,因为计算中存在闭包操作,所以RDD会将数据发送到Executor
但是无法将数据从Executor端拉回到Driver端。
如果想要将和RDD无关的数据拉回到Driver端,RDD模型处理不了,需要采用新的数据模型——累加器
doubleAccumulator
collectionAccumulator 累加器
def main(args: Array[String]): Unit = {
// TODO Spark 环境
val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(conf)
// TODO 1. 创建累加器
val wordCountAcc = new MyAcculumator
// TODO 2. 将累加器注册到Spark中
sc.register(wordCountAcc, "WordCount")
val rdd = sc.makeRDD(
List(
"Hello",
"Hello",
"Hello",
"World",
"Hello",
),2
)
rdd.foreach(
word => {
// TODO 3. 使用累加器
wordCountAcc.add(word)
}
)
// TODO 4. 获取累加器的结果
println(wordCountAcc.value)
sc.stop()
}
// TODO 自定义数据累加器(WordCount)
// 1. 继承AccumulatorV2
// 2. 定义数据的泛型
// IN : String
// OUT : mutable.Map[String, Int]
// 3. 重写方法(3(计算) + 3(状态) = 6)
class MyAcculumator extends AccumulatorV2[String, mutable.Map[String, Int]]{
private val wordCountMap = mutable.Map[String, Int]()
// TODO 判断当前累加器是否为初始状态
override def isZero: Boolean = {
wordCountMap.isEmpty
}
// TODO 复制累加器
override def copy(): AccumulatorV2[String, mutable.Map[String, Int]] = {
new MyAcculumator()
}
// TODO 重置累加器
override def reset(): Unit = {
wordCountMap.clear()
}
// TODO 将外部的数据增加到累加器中
override def add(word: String): Unit = {
val oldCnt: Int = wordCountMap.getOrElse(word, 0)
wordCountMap.update(word, oldCnt + 1)
}
// TODO 将多个累加器进行合并
override def merge(other: AccumulatorV2[String, mutable.Map[String, Int]]): Unit = {
other.value.foreach {
case (word, cnt) => {
val oldCnt: Int = wordCountMap.getOrElse(word, 0)
wordCountMap.update(word, oldCnt + cnt)
}
}
}
// TODO 获取累加器的结果
override def value: mutable.Map[String, Int] = {
wordCountMap
}
}
十五:Join现象引出广播变量
def main(args: Array[String]): Unit = {
// TODO Spark 环境
val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(conf)
val rdd1 = sc.makeRDD(
List(
("a", 1),
("b", 2),
)
)
val rdd2 = sc.makeRDD(
List(
("a", 3),
("b", 4),
)
)
// ("a", 1)
// ( a,(1,3))
//rdd1.join(rdd2).collect().foreach(println)
// 计算的对象是以Task为单位进行数据发送的,所以可能会导致大量的数据冗余
// 所以如果计算对象占用比较大的资源,性能会急剧下降
// 为了提供数据访问效率,可以将数据发送给Executor,而不是发给Task。
// RDD数据模型只能给Task发。只能采用其他的数据模型:TODO 广播变量
val map = mutable.Map[String, Int](
("a", 3),
("b", 4)
)
// TODO 使用广播变量
val bc: Broadcast[mutable.Map[String, Int]] = sc.broadcast(map)
rdd1.map {
case (k, v1) => {
// TODO 获取广播变量的值
var v2 = bc.value.getOrElse(k, 0)
(k, (v1, v2))
}
}.collect.foreach(println)
sc.stop()
}
计算的对象是以Task为单位进行数据发送的,所以可能会导致大量的数据冗余 且 如果计算对象占用比较大的资源,性能会急剧下降
为了提供数据访问效率,可以将数据发送给Executor,而不是发给Task。
但是RDD数据模型只能给Task发。只能采用其他的数据模型: 广播变量(可以将数据发送给Executor 使得同一个节点上的Task可以共享一份资源,并且如果有Executor节点与Driver通信比较慢的时候 可以从就近的Executor上获得需要的资源)
十五:SparkSQL(Dataframe DataSet RDD 转换)
Spark + Hive => Shark
Spark SQL是Spark用于结构化数据(structured data)处理的Spark模块。
1.2 SparkSQL特点:
无缝的整合了 SQL 查询和 Spark 编程
SparkSQL可以简化RDD的开发,提高开发效率
15.1:Dataframe 与 DataSet 与 RDD概述
SQL语言本身没有类型
工厂模式生产的对象是有相同的功能的,因为从同一条生产线下来的。
Builder模式要生产某个对象需要经过复杂的过程,但是builder模式会把这些过程顺序确定下来。
Scala中的构造方法有两种,主构造方法和辅助构造方法,辅助构造方法的函数名一定为this。
15.2:Dataframe 与 DataSet 与 RDD
Dataframe : 是SparkSQL中的核心数据模型,是弱类型的模型,不关心数据的类型操作。
因为SQL的局限性,因此在处理数据的时候,Dataframe提供了一种DSL语法(面向对象的语法)
SparkSQL 与 RDD的关系与转换
SparkSQL模块等同于是对SparkCore一个封装,专门用于结构化数据处理的场景
Dataset : 就是使用面向对象的方式操作数据, 类,属性 => 结构 就是
在Dataframe的基础上,增加了类型信息
SparkSQL中存在大量的隐式转换操作,但是一般会导入使用 import时,使用的spark并不是包名,是环境对象的名称。
Dataframe 与 DataSet的区别?
Dataframe是特定类型的DataSet[Row]
说明DataSet兼容Dataframe()是将来用的比较多的
注意:如果从内存中获取数据,spark可以知道数据类型具体是什么。如果是数字,默认作为Int处理;但是从文件中读取的数字,不能确定是什么类型,所以用bigint接收,可以和Long类型转换,但是和Int不能进行转换
SparkSQL技术选型:
15.2:UDF 与 UDAF
package com.atguigu.bigdata.spark.sql01
import org.apache.spark.sql.expressions.Aggregator
import org.apache.spark.sql.{Dataframe, Encoder, Encoders, SparkSession, functions}
object SparkSQL09_Dataframe_UDAF {
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
df.createOrReplaceTempView("user")
// TODO UDAF使用时,需要采用特殊的方式实现自定义操作
// TODO 强类型操作不能应用于弱类型操作,如果想用,那么需要转换functions.udaf
// Spark3.0以后,可以通过转换,让强类型应用在弱类型操作
spark.udf.register("avgAge",functions.udaf(new AvgAgeUDF()))
// TODO : UDF
// 自定义的函数在SQL文使用
spark.sql("select avgAge(age) from user").show()
spark.stop()
}
case class CalcBuffer(var total : Long , var cnt : Long)
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 1. 继承org.apache.spark.sql.expressions.Aggregator
// 2. 定义泛型
// IN : Long
// BUFF : CalcBuffer
// OUT : Long
// 3. 重写方法 (4 + 2(固定) = 6)
class AvgAgeUDF extends Aggregator[Long,CalcBuffer,Long]{
// TODO 缓冲区的初始化
override def zero: CalcBuffer = {
CalcBuffer(0l,0l)
}
// TODO 将输入的数据和缓冲区的数据进行聚合操作
override def reduce(buff: CalcBuffer, a: Long): CalcBuffer ={
buff.total += a
buff.cnt += 1
buff
}
// TODO 多个缓冲区的合并操作
override def merge(b1: CalcBuffer, b2: CalcBuffer): CalcBuffer = {
b1.total += b2.total
b1.cnt += b2.cnt
b1
}
// TODO 计算数据
override def finish(reduction: CalcBuffer): Long = {
reduction.total / reduction.cnt
}
override def bufferEncoder: Encoder[CalcBuffer] = Encoders.product
override def outputEncoder: Encoder[Long] = Encoders.scalaLong
}
}
UDAF老版本:
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
df.createOrReplaceTempView("user")
// TODO UDAF使用时,需要采用特殊的方式实现自定义操作
// TODO 强类型操作不能应用于弱类型操作,如果想用,那么需要转换functions.udaf
// Spark3.0以后,可以通过转换,让强类型应用在弱类型操作
spark.udf.register("avgAge",new AvgAgeUDAF())
// TODO : UDF
// 自定义的函数在SQL文使用
spark.sql("select avgAge(age) from user").show()
spark.stop()
}
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 使用早期Spark版本的UDAF(弱类型)
// 1. 继承UserDefinedAggregateFunction
// 2. 重写方法(8)
class AvgAgeUDAF extends UserDefinedAggregateFunction{
// TODO 输入数据的结构
override def inputSchema: StructType = {
StructType(
Array(
StructField("age",LongType)
)
)
}
// TODO 缓冲区数据的结构
override def bufferSchema: StructType = {
StructType(
Array(
StructField("total",LongType),
StructField("cnt",LongType)
)
)
}
// TODO 输出数据的类型
override def dataType: DataType = LongType
// TODO 计算稳定性
override def deterministic: Boolean = true
// TODO 缓冲区的初始化
override def initialize(buffer: MutableAggregationBuffer): Unit = {
buffer.update(0,0l)
buffer.update(1,0l)
}
// TODO 将输入的数据和缓冲区的数据进行聚合操作
override def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
buffer.update(0,buffer.getLong(0) + input.getLong(0))
buffer.update(1,buffer.getLong(1)+1)
}
// TODO 多个缓冲区的合并操作
override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
buffer1.update(0,buffer1.getLong(0) + buffer2.getLong(0))
buffer1.update(1,buffer1.getLong(1) + buffer2.getLong(1))
}
override def evaluate(buffer: Row): Any = {
buffer.getLong(0) / buffer.getLong(1)
}
}
UDAF_Class老版本:
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
// TODO Spark3.0版本前,强类型的UDAF不能应用于弱类型的SQL中
import spark.implicits._
val udaf: AvgAgeUDAF = new AvgAgeUDAF
val ds: Dataset[User] = df.as[User]
ds.select(udaf.toColumn).show()
spark.stop()
}
case class User(id : Long , name : String , age : Long)
case class CalacBuffer(var total : Long , var cnt : Long)
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 使用早期Spark版本的UDAF(强类型)
// 1. 继承Aggregator
// 2. 定义泛型
// IN : User
// BUFF : CalcBuffer
// OUT : Long
// 3. 重写方法(6)
class AvgAgeUDAF extends Aggregator[User,CalacBuffer,Long]{
override def zero: CalacBuffer = {
CalacBuffer(0l,0L)
}
override def reduce(b: CalacBuffer, user: User): CalacBuffer = {
b.total += user.age
b.cnt += 1
b
}
override def merge(b1: CalacBuffer, b2: CalacBuffer): CalacBuffer = {
b1.total += b2.total
b1.cnt += b2.cnt
b1
}
override def finish(buffer: CalacBuffer): Long = {
buffer.total / buffer.cnt
}
override def bufferEncoder: Encoder[CalacBuffer] = Encoders.product
override def outputEncoder: Encoder[Long] = Encoders.scalaLong
}
15.3:通用读取方式和默认读取方式
user.json is not a Parquet file. expected magic number at tail [80, 65, 82, 49] but found [32, 53, 48, 125]
SparkSQL通用读取的方法,读取的文件为Parquet格式(列式存储格式)
val df = spark.read.load("data/users.parquet")
如果想要读取其他格式的文件也可以,需要额外增加参数进行解析
df.write.mode("append").format("json").save("output")
sparkSQL基于SparkCore
SparkCore读取数据是依赖于Hadoop
Hadoop读取数据是按行读取
SparkSQL读取JSON格式的文件,要求每一行数据必须符合JSON格式
读入数据:
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
//方式1:通用的load方法读取
spark.read.format("jdbc")
.option("url", "jdbc:mysql://linux1:3306/spark-sql")
.option("driver", "com.mysql.jdbc.Driver")
.option("user", "root")
.option("password", "123123")
.option("dbtable", "user")
.load().show
//方式2:通用的load方法读取 参数另一种形式
spark.read.format("jdbc")
.options(Map("url"->"jdbc:mysql://linux1:3306/spark-sql?user=root&password=123123",
"dbtable"->"user","driver"->"com.mysql.jdbc.Driver")).load().show
//方式3:使用jdbc方法读取
val props: Properties = new Properties()
props.setProperty("user", "root")
props.setProperty("password", "123123")
val df: Dataframe = spark.read.jdbc("jdbc:mysql://linux1:3306/spark-sql", "user", props)
df.show
//释放资源
spark.stop()
写入数据:
case class User2(name: String, age: Long)
。。。
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
val rdd: RDD[User2] = spark.sparkContext.makeRDD(List(User2("lisi", 20), User2("zs", 30)))
val ds: Dataset[User2] = rdd.toDS
//方式1:通用的方式 format指定写出类型
//方式2:通过jdbc方法
val props: Properties = new Properties()
props.setProperty("user", "root")
props.setProperty("password", "123123")
ds.write.mode(SaveMode.Append).jdbc("jdbc:mysql://linux1:3306/spark-sql", "user", props)
//释放资源
spark.stop()
十六:SparkStreaming(数据模型是离散流DStrem)
实时 离线 批量 流式
Spark是一个离线的批量数据处理的框架
SparkStreaming是一个准实时,微批次的处理框架
16.1:SparkStreaming知识点
Discretized Stream(离散流)是Spark Streaming的基础抽象,代表持续性的数据流和经过各种Spark原语操作后的结果数据流。在内部实现上,DStream是一系列连续的RDD来表示。每个RDD含有一段时间间隔内的数据。
16.2:DStream
blockingQueue队列:阻塞式队列,有反压机制,可以避免数据挤压。
采集数据的方式:DStream监控目录变化,将RDD放入到离散化流DStream中,自定义数据源。
object SparkStreaming05_DIY {
def main(args: Array[String]): Unit = {
//TODO SparkStreaming 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[*]")
.setAppName("Streaming")
.set("spark.testing.memory", "471859200")
//StreamingContext独享的第二个构造参数表示数据采集周期
// val ssc: StreamingContext = new StreamingContext(conf,new Duration(3 * 1000))
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
val uuidDS: ReceiverInputDStream[String] = ssc.receiverStream(new MyReceiver)
uuidDS.print()
//启动采集器
ssc.start()
// 等待采集器结束
ssc.awaitTermination()
}
class MyReceiver extends Receiver[String](StorageLevel.MEMORY_ONLY){
private var flg = true
override def onStart(): Unit = {
//采集数据
while (flg) {
//采集数据
val uuid: String = UUID.randomUUID().toString
//存储数据
store(uuid)
Thread.sleep(1000)
}
}
override def onStop(): Unit = {
//释放资源
flg = false
}
}
}
十七:DStream转换(有无状态转换)
离散化流无法完成排序操作 ,离散化流其实就是每个时间段RDD的封装 ,离散化流可以转换为RDD实现操作
transform用于将离散化流实现不了的功能进行调用,transform方法是DStream的方法,称之为原语,RDD的方法称之为算子 如果想要在Driver端周期性执行一些逻辑的场合,需要使用transform。
将两个窗口key相同的连接起来
使用updateStateByKey需要对检查点目录进行配置,会使用检查点来保存状态。
reduceByKey原语不会保留中间计算结果,一旦采集周期结束,数据丢弃 ,如果想要保留中间计算结果,需要有状态操作
从检查点中恢复数据
def main(args: Array[String]): Unit = {
//从检查点中恢复数据
val outerSSC = StreamingContext.getOrCreate("cp",() => {
//TODO SparkStreaming 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[*]")
.setAppName("Streaming")
.set("spark.testing.memory", "471859200")
//StreamingContext独享的第二个构造参数表示数据采集周期
// val ssc: StreamingContext = new StreamingContext(conf,new Duration(3 * 1000))
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
ssc.checkpoint("cp")
val socketDS: ReceiverInputDStream[String] = ssc.socketTextStream("localhost", 8888)
val wordDS: DStream[String] = socketDS.flatMap(_.split(" "))
val wordToOneDS: DStream[(String, Int)] = wordDS.map((_, 1))
wordToOneDS.updateStateByKey(
(seq : Seq[Int] , buffer : Option[Int]) => {
Option(seq.sum + buffer.getOrElse(0))
}
).print()
ssc
})
//SparkStreaming (reduceByKey) + Redis内存数据库 = 有状态
outerSSC.start()
outerSSC.awaitTermination()
}
十八:DStream窗口
窗口划过的规律是:数据由无到有再到无(先多后少后变没)
DStream离散流有window方法:wordToOneDS.window(Seconds(9),Seconds(3))可以设置窗口的大小。
window操作中,窗口范围和滑动的幅度应该为采集周期的整数倍
如果窗口范围和滑动的幅度相同, 那么就是滚动窗口,没有重复数据 默认情况下,不使用窗口,也会有窗口操作,窗口范围取值为1个采集周期,滑动幅度也是一个采集周期。
如果窗口范围和滑动的幅度不相同, 那么就是滑动窗口
1>窗口范围大于滑动的幅度, 存在重复数据
2>窗口范围小于滑动的幅度, 存在丢失数据的可能
二十:优雅的关闭
对于流式数据,线程的启动和关闭应该是分开的,属于两个线程。
优雅的关闭,不要强制关闭,关闭前,不再接受数据,而是把手头上的数据处理完成之后再关闭。
二十一:DStream输出
如果想要将DStream进行特殊处理,那么可以转换为底层RDD实现
val fileRDD = sc.textFile("data/apache.log")
// filter算子返回的结果为按照规则保留的数据本身
fileRDD.filter(
line => {
val datas = line.split(" ")
val time = datas(3)
time.startsWith("17/05/2015")
}
).map(
line => {
val datas = line.split(" ")
datas(6)
}
).collect.foreach(println)
7.6: sample
7.7: distinct
scala使用的是单点集合(缺点,单点的资源有限),而RDD使用的是集群去重
7.8:coalesce(缩小分区) 与 repartition(扩大分区)
7.9:sortBy()
八:双值类型数据集的算子(交集并集差集拉链)
单Value(单一数据集)数据操作,双Value(多个数据集)数据操作
九:Key - Value类型算子(隐式转换rddToPairRDDFunction)
9.1:partitionBy()
repartition : 重分区(分区数量)
partitionBy : 重分区(数据的位置 数据进入到哪个分区), 数据路由(Hash定位),分区器 如果想要让数据重新进行分区,那么需要传递分区规则对象(分区器)
Spark中默认常用的分区器:RangePartitioner(要求数据必须能排序) & HashPartitioner(默认)
思考一个问题:如果重分区的分区器和当前RDD的分区器一样怎么办?
此时不做任何处理,还是当前分区,不需要重新shuffle
9.2:reduceByKey groupByKey
TODO reduceByKey算子用于将数据集中相同的key的value数据聚合在一起实现两两聚合
spark的算子有很多是以byKey结尾的,就是说将K-V独立使用
TODO reduceByKey算子可以实现 WordCount
9.3:aggregateByKey foldByKey
需求 数据集中相同的Key的分区内取最大值,分区间求和
reduceByKey groupByKey只考虑key不考虑分区而reduceByKey 在逻辑上,
分区内和分区间是一样的都是聚合 因此reduceByKey groupByKey都不能处理该需求。
需求 如果分区内和分区间的计算规则不一定相同时,Spark提供了一个算子来实现。aggregateByKey
aggregateByKey算子存在函数柯里化,有多个参数列表
第一个参数列表中有一个参数
第一个参数表示计算初始值
第二个参数列表中有二个参数
第一个参数表示 : 分区内计算规则
第二个参数表示 : 分区间计算规则
9.4:combineByKey
reduceByKey,aggregateByKey ,foldByKey, combineByKey本质区别
9.5:sortByKey
9.6:Join leftOuterJoin rightOuterJoin fullOuterJoin
9.7 : cogroup
join算子体现的是数据的关系
join算子将两个数据集中相同的key的数据,连接在一起
zip是将两个数据集位置相同的数据,连接起来与key无关
join 可能存在笛卡尔积 也 存在shuffle()
笛卡尔积 在落盘的时候 如果存在大量相同的key的时候数据量会激增
如果能够使用其它算子实现的功能,那么不推荐使用join
rdd1.join(rdd2).collect().foreach(println)
rdd1.leftOuterJoin(rdd2).collect().foreach(println) rdd1.rightOuterJoin(rdd2).collect().foreach(println) rdd1.fullOuterJoin(rdd2).collect().foreach(println)
cogroup = connect(多个数据集) + group(单一数据集)
9.6:案例实操
方式一:
// 统计出每一个省份每个广告被点击数量排行的Top3
val fileDatas: RDD[String] = sc.textFile("data/agent.log")
val groupDatas: RDD[(String, Iterable[String])] = fileDatas.groupBy(
line => {
val datas: Array[String] = line.split(" ")
datas(1)
}
)
val top3: RDD[(String, List[(String, Int)])] = groupDatas.mapValues(
//数据集是List(line, line, line)
list => {
//数据集是line
val adToCount: Map[String, Int] = list.map(
line => {
val datas: Array[String] = line.split(" ")
(datas(4), 1)
}
//数据集是List((省份,广告)) 按照省份进行分组统计
).groupBy(_._1).mapValues(_.size)
//Map[String, Int] [广告,点击次数]
// 在 groupDatas.mapValues( 的架子中都是对value进行处理的
//元组天生的排序 数据类型[广告,点击次数] 前三条
adToCount.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
}
)
top3.collect().foreach(println)
sc.stop()
}
方式二:
val fileDatas: RDD[String] = sc.textFile("data/agent.log")
// 1. 将广告进行统计分析 (word, cnt)
// ((省份1,广告1), 1),((省份2,广告2), 1)((省份1,广告1), 1)
// ((省份1,广告1), sum),((省份1,广告2), sum)
val reduceDatas: RDD[((String, String), Int)] = fileDatas.map(
line => {
val datas: Array[String] = line.split(" ")
((datas(1), datas(4)), 1)
}
).reduceByKey(_ + _)
// 1.5 将统计结果进行格式转换
// (省份1,(广告1, sum)),(省份1,(广告2, sum))
val mapDatas: RDD[(String, (String, Int))] = reduceDatas.map {
case ((prv, ad), sum) => {
(prv, (ad, sum))
}
}
// 2. 将统计结果按照省份进行分组
// (省份1, (广告1, sum),(广告2, sum))
val prvGroupDatas: RDD[(String, Iterable[(String, Int)])] = mapDatas.groupByKey()
// 3. 将分组后的数据按照点击数量进行排序(降序),取前3名
val top3: RDD[(String, List[(String, Int)])] = prvGroupDatas.mapValues(
iter => {
iter.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
}
)
top3.collect().foreach(println)
十:action行动算子(是对结果进行处理的)
TODO : 所谓的行动(Action)算子,其实就是RDD用于触发作业执行的方法,类似于IO中的read方法
转换算子的返回值为RDD 行动算子的返回值为具体的结果
行动算子和作业的关系 :1 对 1
10.2:reduce
reduce算子可以触发作业的执行,分区内先两两计算,分区间再两两计算
分区内计算都是在Executor。
分区间计算都是在Driver
10.3:count first take takeOrdered
10.4: aggregate fold
10.5: countByKey countByValue
10.5: save相关的算子
rdd.saveAsTextFile("output1")
rdd.saveAsObjectFile("output2")
rdd.saveAsSequenceFile("output3")
10.5: collect foreach
10.6: 序列化闭包检测
一个函数使用了外部的数据,并且改变了这个数据的生命周期 将数据包含到函数内部,形成了一个闭合的环境,这个环境称之为闭包环境,简称闭包
Spark在运行作业之前,会判断程序逻辑中是否包含闭包
如果存在闭包操作,那么需要检测闭包中使用的数据是否能够序列化,如果不能,那么发生错误
这个操作称之为闭包检测
十一:RDD阶段的划分
前一个阶段不执行完,后面的阶段是无法执行的。
十二:依赖关系和血缘关系(宽依赖窄依赖)
十三:持久化
(持久化只针对于当前应用程序,资源使用完之后释放资源,不会给其它节点使用。因此诞生了检查点)
将计算结果缓存起来。
数据的持久化默认将数据保存到内存中,重复使用
wordToOne.cache()
持久化操作也可以保存到磁盘中
wordToOne.persist()
保存级别设置
十三:RDD CheckPoint检查点
java中的finalize方法是Object类中提供的一个方法,在GC准备释放对象所占用的内存空间之前,它将首先调用finalize()方法。
持久化只针对于当前应用程序,资源使用完之后释放资源,不会给其它节点使用。
持久化只针对于当前应用程序
跨应用程序的数据共享需要采用 检查点机制
设置的方式
① :sc.setCheckpointDir("cp") 该检查点的目录一般为分布式目录以便各个节点使用
②:wordToOne.checkpoint()
持久化操作会在血缘关系中增加依赖
检查点一般会将数据保存到分布式文件系统中(HDFS)(新的数据源)
检查点会切断血缘即减少血缘依赖,因为可以把HDFS中相对可靠的数据作为新的数据源。
十四:自定义分区 RDD文件读取与保存
Hash分区:对于给定的key,计算其hashCode,并除以分区个数取余
Range分区:将一定范围内的数据映射到一个分区中,尽量保证每个分区数据均匀,而且分区间有序
十四:累加器(数据模型)(功能比较单一没有shuffle性能相对高)
累加器的现象
分析:
如果数据和RDD有关系,那么可以从Driver端发送到Executor端执行
如果执行完毕,那么可以将结果拉回到Driver端
如果数据和RDD没有关系,因为计算中存在闭包操作,所以RDD会将数据发送到Executor
但是无法将数据从Executor端拉回到Driver端。
如果想要将和RDD无关的数据拉回到Driver端,RDD模型处理不了,需要采用新的数据模型——累加器
doubleAccumulator
collectionAccumulator 累加器
def main(args: Array[String]): Unit = {
// TODO Spark 环境
val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(conf)
// TODO 1. 创建累加器
val wordCountAcc = new MyAcculumator
// TODO 2. 将累加器注册到Spark中
sc.register(wordCountAcc, "WordCount")
val rdd = sc.makeRDD(
List(
"Hello",
"Hello",
"Hello",
"World",
"Hello",
),2
)
rdd.foreach(
word => {
// TODO 3. 使用累加器
wordCountAcc.add(word)
}
)
// TODO 4. 获取累加器的结果
println(wordCountAcc.value)
sc.stop()
}
// TODO 自定义数据累加器(WordCount)
// 1. 继承AccumulatorV2
// 2. 定义数据的泛型
// IN : String
// OUT : mutable.Map[String, Int]
// 3. 重写方法(3(计算) + 3(状态) = 6)
class MyAcculumator extends AccumulatorV2[String, mutable.Map[String, Int]]{
private val wordCountMap = mutable.Map[String, Int]()
// TODO 判断当前累加器是否为初始状态
override def isZero: Boolean = {
wordCountMap.isEmpty
}
// TODO 复制累加器
override def copy(): AccumulatorV2[String, mutable.Map[String, Int]] = {
new MyAcculumator()
}
// TODO 重置累加器
override def reset(): Unit = {
wordCountMap.clear()
}
// TODO 将外部的数据增加到累加器中
override def add(word: String): Unit = {
val oldCnt: Int = wordCountMap.getOrElse(word, 0)
wordCountMap.update(word, oldCnt + 1)
}
// TODO 将多个累加器进行合并
override def merge(other: AccumulatorV2[String, mutable.Map[String, Int]]): Unit = {
other.value.foreach {
case (word, cnt) => {
val oldCnt: Int = wordCountMap.getOrElse(word, 0)
wordCountMap.update(word, oldCnt + cnt)
}
}
}
// TODO 获取累加器的结果
override def value: mutable.Map[String, Int] = {
wordCountMap
}
}
十五:Join现象引出广播变量
def main(args: Array[String]): Unit = {
// TODO Spark 环境
val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(conf)
val rdd1 = sc.makeRDD(
List(
("a", 1),
("b", 2),
)
)
val rdd2 = sc.makeRDD(
List(
("a", 3),
("b", 4),
)
)
// ("a", 1)
// ( a,(1,3))
//rdd1.join(rdd2).collect().foreach(println)
// 计算的对象是以Task为单位进行数据发送的,所以可能会导致大量的数据冗余
// 所以如果计算对象占用比较大的资源,性能会急剧下降
// 为了提供数据访问效率,可以将数据发送给Executor,而不是发给Task。
// RDD数据模型只能给Task发。只能采用其他的数据模型:TODO 广播变量
val map = mutable.Map[String, Int](
("a", 3),
("b", 4)
)
// TODO 使用广播变量
val bc: Broadcast[mutable.Map[String, Int]] = sc.broadcast(map)
rdd1.map {
case (k, v1) => {
// TODO 获取广播变量的值
var v2 = bc.value.getOrElse(k, 0)
(k, (v1, v2))
}
}.collect.foreach(println)
sc.stop()
}
计算的对象是以Task为单位进行数据发送的,所以可能会导致大量的数据冗余 且 如果计算对象占用比较大的资源,性能会急剧下降
为了提供数据访问效率,可以将数据发送给Executor,而不是发给Task。
但是RDD数据模型只能给Task发。只能采用其他的数据模型: 广播变量(可以将数据发送给Executor 使得同一个节点上的Task可以共享一份资源,并且如果有Executor节点与Driver通信比较慢的时候 可以从就近的Executor上获得需要的资源)
十五:SparkSQL(Dataframe DataSet RDD 转换)
Spark + Hive => Shark
Spark SQL是Spark用于结构化数据(structured data)处理的Spark模块。
1.2 SparkSQL特点:
无缝的整合了 SQL 查询和 Spark 编程
SparkSQL可以简化RDD的开发,提高开发效率
15.1:Dataframe 与 DataSet 与 RDD概述
SQL语言本身没有类型
工厂模式生产的对象是有相同的功能的,因为从同一条生产线下来的。
Builder模式要生产某个对象需要经过复杂的过程,但是builder模式会把这些过程顺序确定下来。
Scala中的构造方法有两种,主构造方法和辅助构造方法,辅助构造方法的函数名一定为this。
15.2:Dataframe 与 DataSet 与 RDD
Dataframe : 是SparkSQL中的核心数据模型,是弱类型的模型,不关心数据的类型操作。
因为SQL的局限性,因此在处理数据的时候,Dataframe提供了一种DSL语法(面向对象的语法)
SparkSQL 与 RDD的关系与转换
SparkSQL模块等同于是对SparkCore一个封装,专门用于结构化数据处理的场景
Dataset : 就是使用面向对象的方式操作数据, 类,属性 => 结构 就是
在Dataframe的基础上,增加了类型信息
SparkSQL中存在大量的隐式转换操作,但是一般会导入使用 import时,使用的spark并不是包名,是环境对象的名称。
Dataframe 与 DataSet的区别?
Dataframe是特定类型的DataSet[Row]
说明DataSet兼容Dataframe()是将来用的比较多的
注意:如果从内存中获取数据,spark可以知道数据类型具体是什么。如果是数字,默认作为Int处理;但是从文件中读取的数字,不能确定是什么类型,所以用bigint接收,可以和Long类型转换,但是和Int不能进行转换
SparkSQL技术选型:
15.2:UDF 与 UDAF
package com.atguigu.bigdata.spark.sql01
import org.apache.spark.sql.expressions.Aggregator
import org.apache.spark.sql.{Dataframe, Encoder, Encoders, SparkSession, functions}
object SparkSQL09_Dataframe_UDAF {
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
df.createOrReplaceTempView("user")
// TODO UDAF使用时,需要采用特殊的方式实现自定义操作
// TODO 强类型操作不能应用于弱类型操作,如果想用,那么需要转换functions.udaf
// Spark3.0以后,可以通过转换,让强类型应用在弱类型操作
spark.udf.register("avgAge",functions.udaf(new AvgAgeUDF()))
// TODO : UDF
// 自定义的函数在SQL文使用
spark.sql("select avgAge(age) from user").show()
spark.stop()
}
case class CalcBuffer(var total : Long , var cnt : Long)
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 1. 继承org.apache.spark.sql.expressions.Aggregator
// 2. 定义泛型
// IN : Long
// BUFF : CalcBuffer
// OUT : Long
// 3. 重写方法 (4 + 2(固定) = 6)
class AvgAgeUDF extends Aggregator[Long,CalcBuffer,Long]{
// TODO 缓冲区的初始化
override def zero: CalcBuffer = {
CalcBuffer(0l,0l)
}
// TODO 将输入的数据和缓冲区的数据进行聚合操作
override def reduce(buff: CalcBuffer, a: Long): CalcBuffer ={
buff.total += a
buff.cnt += 1
buff
}
// TODO 多个缓冲区的合并操作
override def merge(b1: CalcBuffer, b2: CalcBuffer): CalcBuffer = {
b1.total += b2.total
b1.cnt += b2.cnt
b1
}
// TODO 计算数据
override def finish(reduction: CalcBuffer): Long = {
reduction.total / reduction.cnt
}
override def bufferEncoder: Encoder[CalcBuffer] = Encoders.product
override def outputEncoder: Encoder[Long] = Encoders.scalaLong
}
}
UDAF老版本:
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
df.createOrReplaceTempView("user")
// TODO UDAF使用时,需要采用特殊的方式实现自定义操作
// TODO 强类型操作不能应用于弱类型操作,如果想用,那么需要转换functions.udaf
// Spark3.0以后,可以通过转换,让强类型应用在弱类型操作
spark.udf.register("avgAge",new AvgAgeUDAF())
// TODO : UDF
// 自定义的函数在SQL文使用
spark.sql("select avgAge(age) from user").show()
spark.stop()
}
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 使用早期Spark版本的UDAF(弱类型)
// 1. 继承UserDefinedAggregateFunction
// 2. 重写方法(8)
class AvgAgeUDAF extends UserDefinedAggregateFunction{
// TODO 输入数据的结构
override def inputSchema: StructType = {
StructType(
Array(
StructField("age",LongType)
)
)
}
// TODO 缓冲区数据的结构
override def bufferSchema: StructType = {
StructType(
Array(
StructField("total",LongType),
StructField("cnt",LongType)
)
)
}
// TODO 输出数据的类型
override def dataType: DataType = LongType
// TODO 计算稳定性
override def deterministic: Boolean = true
// TODO 缓冲区的初始化
override def initialize(buffer: MutableAggregationBuffer): Unit = {
buffer.update(0,0l)
buffer.update(1,0l)
}
// TODO 将输入的数据和缓冲区的数据进行聚合操作
override def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
buffer.update(0,buffer.getLong(0) + input.getLong(0))
buffer.update(1,buffer.getLong(1)+1)
}
// TODO 多个缓冲区的合并操作
override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
buffer1.update(0,buffer1.getLong(0) + buffer2.getLong(0))
buffer1.update(1,buffer1.getLong(1) + buffer2.getLong(1))
}
override def evaluate(buffer: Row): Any = {
buffer.getLong(0) / buffer.getLong(1)
}
}
UDAF_Class老版本:
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
// TODO Spark3.0版本前,强类型的UDAF不能应用于弱类型的SQL中
import spark.implicits._
val udaf: AvgAgeUDAF = new AvgAgeUDAF
val ds: Dataset[User] = df.as[User]
ds.select(udaf.toColumn).show()
spark.stop()
}
case class User(id : Long , name : String , age : Long)
case class CalacBuffer(var total : Long , var cnt : Long)
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 使用早期Spark版本的UDAF(强类型)
// 1. 继承Aggregator
// 2. 定义泛型
// IN : User
// BUFF : CalcBuffer
// OUT : Long
// 3. 重写方法(6)
class AvgAgeUDAF extends Aggregator[User,CalacBuffer,Long]{
override def zero: CalacBuffer = {
CalacBuffer(0l,0L)
}
override def reduce(b: CalacBuffer, user: User): CalacBuffer = {
b.total += user.age
b.cnt += 1
b
}
override def merge(b1: CalacBuffer, b2: CalacBuffer): CalacBuffer = {
b1.total += b2.total
b1.cnt += b2.cnt
b1
}
override def finish(buffer: CalacBuffer): Long = {
buffer.total / buffer.cnt
}
override def bufferEncoder: Encoder[CalacBuffer] = Encoders.product
override def outputEncoder: Encoder[Long] = Encoders.scalaLong
}
15.3:通用读取方式和默认读取方式
user.json is not a Parquet file. expected magic number at tail [80, 65, 82, 49] but found [32, 53, 48, 125]
SparkSQL通用读取的方法,读取的文件为Parquet格式(列式存储格式)
val df = spark.read.load("data/users.parquet")
如果想要读取其他格式的文件也可以,需要额外增加参数进行解析
df.write.mode("append").format("json").save("output")
sparkSQL基于SparkCore
SparkCore读取数据是依赖于Hadoop
Hadoop读取数据是按行读取
SparkSQL读取JSON格式的文件,要求每一行数据必须符合JSON格式
读入数据:
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
//方式1:通用的load方法读取
spark.read.format("jdbc")
.option("url", "jdbc:mysql://linux1:3306/spark-sql")
.option("driver", "com.mysql.jdbc.Driver")
.option("user", "root")
.option("password", "123123")
.option("dbtable", "user")
.load().show
//方式2:通用的load方法读取 参数另一种形式
spark.read.format("jdbc")
.options(Map("url"->"jdbc:mysql://linux1:3306/spark-sql?user=root&password=123123",
"dbtable"->"user","driver"->"com.mysql.jdbc.Driver")).load().show
//方式3:使用jdbc方法读取
val props: Properties = new Properties()
props.setProperty("user", "root")
props.setProperty("password", "123123")
val df: Dataframe = spark.read.jdbc("jdbc:mysql://linux1:3306/spark-sql", "user", props)
df.show
//释放资源
spark.stop()
写入数据:
case class User2(name: String, age: Long)
。。。
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
val rdd: RDD[User2] = spark.sparkContext.makeRDD(List(User2("lisi", 20), User2("zs", 30)))
val ds: Dataset[User2] = rdd.toDS
//方式1:通用的方式 format指定写出类型
//方式2:通过jdbc方法
val props: Properties = new Properties()
props.setProperty("user", "root")
props.setProperty("password", "123123")
ds.write.mode(SaveMode.Append).jdbc("jdbc:mysql://linux1:3306/spark-sql", "user", props)
//释放资源
spark.stop()
十六:SparkStreaming(数据模型是离散流DStrem)
实时 离线 批量 流式
Spark是一个离线的批量数据处理的框架
SparkStreaming是一个准实时,微批次的处理框架
16.1:SparkStreaming知识点
Discretized Stream(离散流)是Spark Streaming的基础抽象,代表持续性的数据流和经过各种Spark原语操作后的结果数据流。在内部实现上,DStream是一系列连续的RDD来表示。每个RDD含有一段时间间隔内的数据。
16.2:DStream
blockingQueue队列:阻塞式队列,有反压机制,可以避免数据挤压。
采集数据的方式:DStream监控目录变化,将RDD放入到离散化流DStream中,自定义数据源。
object SparkStreaming05_DIY {
def main(args: Array[String]): Unit = {
//TODO SparkStreaming 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[*]")
.setAppName("Streaming")
.set("spark.testing.memory", "471859200")
//StreamingContext独享的第二个构造参数表示数据采集周期
// val ssc: StreamingContext = new StreamingContext(conf,new Duration(3 * 1000))
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
val uuidDS: ReceiverInputDStream[String] = ssc.receiverStream(new MyReceiver)
uuidDS.print()
//启动采集器
ssc.start()
// 等待采集器结束
ssc.awaitTermination()
}
class MyReceiver extends Receiver[String](StorageLevel.MEMORY_ONLY){
private var flg = true
override def onStart(): Unit = {
//采集数据
while (flg) {
//采集数据
val uuid: String = UUID.randomUUID().toString
//存储数据
store(uuid)
Thread.sleep(1000)
}
}
override def onStop(): Unit = {
//释放资源
flg = false
}
}
}
十七:DStream转换(有无状态转换)
离散化流无法完成排序操作 ,离散化流其实就是每个时间段RDD的封装 ,离散化流可以转换为RDD实现操作
transform用于将离散化流实现不了的功能进行调用,transform方法是DStream的方法,称之为原语,RDD的方法称之为算子 如果想要在Driver端周期性执行一些逻辑的场合,需要使用transform。
将两个窗口key相同的连接起来
使用updateStateByKey需要对检查点目录进行配置,会使用检查点来保存状态。
reduceByKey原语不会保留中间计算结果,一旦采集周期结束,数据丢弃 ,如果想要保留中间计算结果,需要有状态操作
从检查点中恢复数据
def main(args: Array[String]): Unit = {
//从检查点中恢复数据
val outerSSC = StreamingContext.getOrCreate("cp",() => {
//TODO SparkStreaming 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[*]")
.setAppName("Streaming")
.set("spark.testing.memory", "471859200")
//StreamingContext独享的第二个构造参数表示数据采集周期
// val ssc: StreamingContext = new StreamingContext(conf,new Duration(3 * 1000))
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
ssc.checkpoint("cp")
val socketDS: ReceiverInputDStream[String] = ssc.socketTextStream("localhost", 8888)
val wordDS: DStream[String] = socketDS.flatMap(_.split(" "))
val wordToOneDS: DStream[(String, Int)] = wordDS.map((_, 1))
wordToOneDS.updateStateByKey(
(seq : Seq[Int] , buffer : Option[Int]) => {
Option(seq.sum + buffer.getOrElse(0))
}
).print()
ssc
})
//SparkStreaming (reduceByKey) + Redis内存数据库 = 有状态
outerSSC.start()
outerSSC.awaitTermination()
}
十八:DStream窗口
窗口划过的规律是:数据由无到有再到无(先多后少后变没)
DStream离散流有window方法:wordToOneDS.window(Seconds(9),Seconds(3))可以设置窗口的大小。
window操作中,窗口范围和滑动的幅度应该为采集周期的整数倍
如果窗口范围和滑动的幅度相同, 那么就是滚动窗口,没有重复数据 默认情况下,不使用窗口,也会有窗口操作,窗口范围取值为1个采集周期,滑动幅度也是一个采集周期。
如果窗口范围和滑动的幅度不相同, 那么就是滑动窗口
1>窗口范围大于滑动的幅度, 存在重复数据
2>窗口范围小于滑动的幅度, 存在丢失数据的可能
二十:优雅的关闭
对于流式数据,线程的启动和关闭应该是分开的,属于两个线程。
优雅的关闭,不要强制关闭,关闭前,不再接受数据,而是把手头上的数据处理完成之后再关闭。
二十一:DStream输出
如果想要将DStream进行特殊处理,那么可以转换为底层RDD实现
scala使用的是单点集合(缺点,单点的资源有限),而RDD使用的是集群去重
7.8:coalesce(缩小分区) 与 repartition(扩大分区)
7.9:sortBy()
八:双值类型数据集的算子(交集并集差集拉链)
单Value(单一数据集)数据操作,双Value(多个数据集)数据操作
九:Key - Value类型算子(隐式转换rddToPairRDDFunction)
9.1:partitionBy()
repartition : 重分区(分区数量)
partitionBy : 重分区(数据的位置 数据进入到哪个分区), 数据路由(Hash定位),分区器 如果想要让数据重新进行分区,那么需要传递分区规则对象(分区器)
Spark中默认常用的分区器:RangePartitioner(要求数据必须能排序) & HashPartitioner(默认)
思考一个问题:如果重分区的分区器和当前RDD的分区器一样怎么办?
此时不做任何处理,还是当前分区,不需要重新shuffle
9.2:reduceByKey groupByKey
TODO reduceByKey算子用于将数据集中相同的key的value数据聚合在一起实现两两聚合
spark的算子有很多是以byKey结尾的,就是说将K-V独立使用
TODO reduceByKey算子可以实现 WordCount
9.3:aggregateByKey foldByKey
需求 数据集中相同的Key的分区内取最大值,分区间求和
reduceByKey groupByKey只考虑key不考虑分区而reduceByKey 在逻辑上,
分区内和分区间是一样的都是聚合 因此reduceByKey groupByKey都不能处理该需求。
需求 如果分区内和分区间的计算规则不一定相同时,Spark提供了一个算子来实现。aggregateByKey
aggregateByKey算子存在函数柯里化,有多个参数列表
第一个参数列表中有一个参数
第一个参数表示计算初始值
第二个参数列表中有二个参数
第一个参数表示 : 分区内计算规则
第二个参数表示 : 分区间计算规则
9.4:combineByKey
reduceByKey,aggregateByKey ,foldByKey, combineByKey本质区别
9.5:sortByKey
9.6:Join leftOuterJoin rightOuterJoin fullOuterJoin
9.7 : cogroup
join算子体现的是数据的关系
join算子将两个数据集中相同的key的数据,连接在一起
zip是将两个数据集位置相同的数据,连接起来与key无关
join 可能存在笛卡尔积 也 存在shuffle()
笛卡尔积 在落盘的时候 如果存在大量相同的key的时候数据量会激增
如果能够使用其它算子实现的功能,那么不推荐使用join
rdd1.join(rdd2).collect().foreach(println)
rdd1.leftOuterJoin(rdd2).collect().foreach(println) rdd1.rightOuterJoin(rdd2).collect().foreach(println) rdd1.fullOuterJoin(rdd2).collect().foreach(println)
cogroup = connect(多个数据集) + group(单一数据集)
9.6:案例实操
方式一:
// 统计出每一个省份每个广告被点击数量排行的Top3
val fileDatas: RDD[String] = sc.textFile("data/agent.log")
val groupDatas: RDD[(String, Iterable[String])] = fileDatas.groupBy(
line => {
val datas: Array[String] = line.split(" ")
datas(1)
}
)
val top3: RDD[(String, List[(String, Int)])] = groupDatas.mapValues(
//数据集是List(line, line, line)
list => {
//数据集是line
val adToCount: Map[String, Int] = list.map(
line => {
val datas: Array[String] = line.split(" ")
(datas(4), 1)
}
//数据集是List((省份,广告)) 按照省份进行分组统计
).groupBy(_._1).mapValues(_.size)
//Map[String, Int] [广告,点击次数]
// 在 groupDatas.mapValues( 的架子中都是对value进行处理的
//元组天生的排序 数据类型[广告,点击次数] 前三条
adToCount.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
}
)
top3.collect().foreach(println)
sc.stop()
}
方式二:
val fileDatas: RDD[String] = sc.textFile("data/agent.log")
// 1. 将广告进行统计分析 (word, cnt)
// ((省份1,广告1), 1),((省份2,广告2), 1)((省份1,广告1), 1)
// ((省份1,广告1), sum),((省份1,广告2), sum)
val reduceDatas: RDD[((String, String), Int)] = fileDatas.map(
line => {
val datas: Array[String] = line.split(" ")
((datas(1), datas(4)), 1)
}
).reduceByKey(_ + _)
// 1.5 将统计结果进行格式转换
// (省份1,(广告1, sum)),(省份1,(广告2, sum))
val mapDatas: RDD[(String, (String, Int))] = reduceDatas.map {
case ((prv, ad), sum) => {
(prv, (ad, sum))
}
}
// 2. 将统计结果按照省份进行分组
// (省份1, (广告1, sum),(广告2, sum))
val prvGroupDatas: RDD[(String, Iterable[(String, Int)])] = mapDatas.groupByKey()
// 3. 将分组后的数据按照点击数量进行排序(降序),取前3名
val top3: RDD[(String, List[(String, Int)])] = prvGroupDatas.mapValues(
iter => {
iter.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
}
)
top3.collect().foreach(println)
十:action行动算子(是对结果进行处理的)
TODO : 所谓的行动(Action)算子,其实就是RDD用于触发作业执行的方法,类似于IO中的read方法
转换算子的返回值为RDD 行动算子的返回值为具体的结果
行动算子和作业的关系 :1 对 1
10.2:reduce
reduce算子可以触发作业的执行,分区内先两两计算,分区间再两两计算
分区内计算都是在Executor。
分区间计算都是在Driver
10.3:count first take takeOrdered
10.4: aggregate fold
10.5: countByKey countByValue
10.5: save相关的算子
rdd.saveAsTextFile("output1")
rdd.saveAsObjectFile("output2")
rdd.saveAsSequenceFile("output3")
10.5: collect foreach
10.6: 序列化闭包检测
一个函数使用了外部的数据,并且改变了这个数据的生命周期 将数据包含到函数内部,形成了一个闭合的环境,这个环境称之为闭包环境,简称闭包
Spark在运行作业之前,会判断程序逻辑中是否包含闭包
如果存在闭包操作,那么需要检测闭包中使用的数据是否能够序列化,如果不能,那么发生错误
这个操作称之为闭包检测
十一:RDD阶段的划分
前一个阶段不执行完,后面的阶段是无法执行的。
十二:依赖关系和血缘关系(宽依赖窄依赖)
十三:持久化
(持久化只针对于当前应用程序,资源使用完之后释放资源,不会给其它节点使用。因此诞生了检查点)
将计算结果缓存起来。
数据的持久化默认将数据保存到内存中,重复使用
wordToOne.cache()
持久化操作也可以保存到磁盘中
wordToOne.persist()
保存级别设置
十三:RDD CheckPoint检查点
java中的finalize方法是Object类中提供的一个方法,在GC准备释放对象所占用的内存空间之前,它将首先调用finalize()方法。
持久化只针对于当前应用程序,资源使用完之后释放资源,不会给其它节点使用。
持久化只针对于当前应用程序
跨应用程序的数据共享需要采用 检查点机制
设置的方式
① :sc.setCheckpointDir("cp") 该检查点的目录一般为分布式目录以便各个节点使用
②:wordToOne.checkpoint()
持久化操作会在血缘关系中增加依赖
检查点一般会将数据保存到分布式文件系统中(HDFS)(新的数据源)
检查点会切断血缘即减少血缘依赖,因为可以把HDFS中相对可靠的数据作为新的数据源。
十四:自定义分区 RDD文件读取与保存
Hash分区:对于给定的key,计算其hashCode,并除以分区个数取余
Range分区:将一定范围内的数据映射到一个分区中,尽量保证每个分区数据均匀,而且分区间有序
十四:累加器(数据模型)(功能比较单一没有shuffle性能相对高)
累加器的现象
分析:
如果数据和RDD有关系,那么可以从Driver端发送到Executor端执行
如果执行完毕,那么可以将结果拉回到Driver端
如果数据和RDD没有关系,因为计算中存在闭包操作,所以RDD会将数据发送到Executor
但是无法将数据从Executor端拉回到Driver端。
如果想要将和RDD无关的数据拉回到Driver端,RDD模型处理不了,需要采用新的数据模型——累加器
doubleAccumulator
collectionAccumulator 累加器
def main(args: Array[String]): Unit = {
// TODO Spark 环境
val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(conf)
// TODO 1. 创建累加器
val wordCountAcc = new MyAcculumator
// TODO 2. 将累加器注册到Spark中
sc.register(wordCountAcc, "WordCount")
val rdd = sc.makeRDD(
List(
"Hello",
"Hello",
"Hello",
"World",
"Hello",
),2
)
rdd.foreach(
word => {
// TODO 3. 使用累加器
wordCountAcc.add(word)
}
)
// TODO 4. 获取累加器的结果
println(wordCountAcc.value)
sc.stop()
}
// TODO 自定义数据累加器(WordCount)
// 1. 继承AccumulatorV2
// 2. 定义数据的泛型
// IN : String
// OUT : mutable.Map[String, Int]
// 3. 重写方法(3(计算) + 3(状态) = 6)
class MyAcculumator extends AccumulatorV2[String, mutable.Map[String, Int]]{
private val wordCountMap = mutable.Map[String, Int]()
// TODO 判断当前累加器是否为初始状态
override def isZero: Boolean = {
wordCountMap.isEmpty
}
// TODO 复制累加器
override def copy(): AccumulatorV2[String, mutable.Map[String, Int]] = {
new MyAcculumator()
}
// TODO 重置累加器
override def reset(): Unit = {
wordCountMap.clear()
}
// TODO 将外部的数据增加到累加器中
override def add(word: String): Unit = {
val oldCnt: Int = wordCountMap.getOrElse(word, 0)
wordCountMap.update(word, oldCnt + 1)
}
// TODO 将多个累加器进行合并
override def merge(other: AccumulatorV2[String, mutable.Map[String, Int]]): Unit = {
other.value.foreach {
case (word, cnt) => {
val oldCnt: Int = wordCountMap.getOrElse(word, 0)
wordCountMap.update(word, oldCnt + cnt)
}
}
}
// TODO 获取累加器的结果
override def value: mutable.Map[String, Int] = {
wordCountMap
}
}
十五:Join现象引出广播变量
def main(args: Array[String]): Unit = {
// TODO Spark 环境
val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(conf)
val rdd1 = sc.makeRDD(
List(
("a", 1),
("b", 2),
)
)
val rdd2 = sc.makeRDD(
List(
("a", 3),
("b", 4),
)
)
// ("a", 1)
// ( a,(1,3))
//rdd1.join(rdd2).collect().foreach(println)
// 计算的对象是以Task为单位进行数据发送的,所以可能会导致大量的数据冗余
// 所以如果计算对象占用比较大的资源,性能会急剧下降
// 为了提供数据访问效率,可以将数据发送给Executor,而不是发给Task。
// RDD数据模型只能给Task发。只能采用其他的数据模型:TODO 广播变量
val map = mutable.Map[String, Int](
("a", 3),
("b", 4)
)
// TODO 使用广播变量
val bc: Broadcast[mutable.Map[String, Int]] = sc.broadcast(map)
rdd1.map {
case (k, v1) => {
// TODO 获取广播变量的值
var v2 = bc.value.getOrElse(k, 0)
(k, (v1, v2))
}
}.collect.foreach(println)
sc.stop()
}
计算的对象是以Task为单位进行数据发送的,所以可能会导致大量的数据冗余 且 如果计算对象占用比较大的资源,性能会急剧下降
为了提供数据访问效率,可以将数据发送给Executor,而不是发给Task。
但是RDD数据模型只能给Task发。只能采用其他的数据模型: 广播变量(可以将数据发送给Executor 使得同一个节点上的Task可以共享一份资源,并且如果有Executor节点与Driver通信比较慢的时候 可以从就近的Executor上获得需要的资源)
十五:SparkSQL(Dataframe DataSet RDD 转换)
Spark + Hive => Shark
Spark SQL是Spark用于结构化数据(structured data)处理的Spark模块。
1.2 SparkSQL特点:
无缝的整合了 SQL 查询和 Spark 编程
SparkSQL可以简化RDD的开发,提高开发效率
15.1:Dataframe 与 DataSet 与 RDD概述
SQL语言本身没有类型
工厂模式生产的对象是有相同的功能的,因为从同一条生产线下来的。
Builder模式要生产某个对象需要经过复杂的过程,但是builder模式会把这些过程顺序确定下来。
Scala中的构造方法有两种,主构造方法和辅助构造方法,辅助构造方法的函数名一定为this。
15.2:Dataframe 与 DataSet 与 RDD
Dataframe : 是SparkSQL中的核心数据模型,是弱类型的模型,不关心数据的类型操作。
因为SQL的局限性,因此在处理数据的时候,Dataframe提供了一种DSL语法(面向对象的语法)
SparkSQL 与 RDD的关系与转换
SparkSQL模块等同于是对SparkCore一个封装,专门用于结构化数据处理的场景
Dataset : 就是使用面向对象的方式操作数据, 类,属性 => 结构 就是
在Dataframe的基础上,增加了类型信息
SparkSQL中存在大量的隐式转换操作,但是一般会导入使用 import时,使用的spark并不是包名,是环境对象的名称。
Dataframe 与 DataSet的区别?
Dataframe是特定类型的DataSet[Row]
说明DataSet兼容Dataframe()是将来用的比较多的
注意:如果从内存中获取数据,spark可以知道数据类型具体是什么。如果是数字,默认作为Int处理;但是从文件中读取的数字,不能确定是什么类型,所以用bigint接收,可以和Long类型转换,但是和Int不能进行转换
SparkSQL技术选型:
15.2:UDF 与 UDAF
package com.atguigu.bigdata.spark.sql01
import org.apache.spark.sql.expressions.Aggregator
import org.apache.spark.sql.{Dataframe, Encoder, Encoders, SparkSession, functions}
object SparkSQL09_Dataframe_UDAF {
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
df.createOrReplaceTempView("user")
// TODO UDAF使用时,需要采用特殊的方式实现自定义操作
// TODO 强类型操作不能应用于弱类型操作,如果想用,那么需要转换functions.udaf
// Spark3.0以后,可以通过转换,让强类型应用在弱类型操作
spark.udf.register("avgAge",functions.udaf(new AvgAgeUDF()))
// TODO : UDF
// 自定义的函数在SQL文使用
spark.sql("select avgAge(age) from user").show()
spark.stop()
}
case class CalcBuffer(var total : Long , var cnt : Long)
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 1. 继承org.apache.spark.sql.expressions.Aggregator
// 2. 定义泛型
// IN : Long
// BUFF : CalcBuffer
// OUT : Long
// 3. 重写方法 (4 + 2(固定) = 6)
class AvgAgeUDF extends Aggregator[Long,CalcBuffer,Long]{
// TODO 缓冲区的初始化
override def zero: CalcBuffer = {
CalcBuffer(0l,0l)
}
// TODO 将输入的数据和缓冲区的数据进行聚合操作
override def reduce(buff: CalcBuffer, a: Long): CalcBuffer ={
buff.total += a
buff.cnt += 1
buff
}
// TODO 多个缓冲区的合并操作
override def merge(b1: CalcBuffer, b2: CalcBuffer): CalcBuffer = {
b1.total += b2.total
b1.cnt += b2.cnt
b1
}
// TODO 计算数据
override def finish(reduction: CalcBuffer): Long = {
reduction.total / reduction.cnt
}
override def bufferEncoder: Encoder[CalcBuffer] = Encoders.product
override def outputEncoder: Encoder[Long] = Encoders.scalaLong
}
}
UDAF老版本:
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
df.createOrReplaceTempView("user")
// TODO UDAF使用时,需要采用特殊的方式实现自定义操作
// TODO 强类型操作不能应用于弱类型操作,如果想用,那么需要转换functions.udaf
// Spark3.0以后,可以通过转换,让强类型应用在弱类型操作
spark.udf.register("avgAge",new AvgAgeUDAF())
// TODO : UDF
// 自定义的函数在SQL文使用
spark.sql("select avgAge(age) from user").show()
spark.stop()
}
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 使用早期Spark版本的UDAF(弱类型)
// 1. 继承UserDefinedAggregateFunction
// 2. 重写方法(8)
class AvgAgeUDAF extends UserDefinedAggregateFunction{
// TODO 输入数据的结构
override def inputSchema: StructType = {
StructType(
Array(
StructField("age",LongType)
)
)
}
// TODO 缓冲区数据的结构
override def bufferSchema: StructType = {
StructType(
Array(
StructField("total",LongType),
StructField("cnt",LongType)
)
)
}
// TODO 输出数据的类型
override def dataType: DataType = LongType
// TODO 计算稳定性
override def deterministic: Boolean = true
// TODO 缓冲区的初始化
override def initialize(buffer: MutableAggregationBuffer): Unit = {
buffer.update(0,0l)
buffer.update(1,0l)
}
// TODO 将输入的数据和缓冲区的数据进行聚合操作
override def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
buffer.update(0,buffer.getLong(0) + input.getLong(0))
buffer.update(1,buffer.getLong(1)+1)
}
// TODO 多个缓冲区的合并操作
override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
buffer1.update(0,buffer1.getLong(0) + buffer2.getLong(0))
buffer1.update(1,buffer1.getLong(1) + buffer2.getLong(1))
}
override def evaluate(buffer: Row): Any = {
buffer.getLong(0) / buffer.getLong(1)
}
}
UDAF_Class老版本:
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
// TODO Spark3.0版本前,强类型的UDAF不能应用于弱类型的SQL中
import spark.implicits._
val udaf: AvgAgeUDAF = new AvgAgeUDAF
val ds: Dataset[User] = df.as[User]
ds.select(udaf.toColumn).show()
spark.stop()
}
case class User(id : Long , name : String , age : Long)
case class CalacBuffer(var total : Long , var cnt : Long)
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 使用早期Spark版本的UDAF(强类型)
// 1. 继承Aggregator
// 2. 定义泛型
// IN : User
// BUFF : CalcBuffer
// OUT : Long
// 3. 重写方法(6)
class AvgAgeUDAF extends Aggregator[User,CalacBuffer,Long]{
override def zero: CalacBuffer = {
CalacBuffer(0l,0L)
}
override def reduce(b: CalacBuffer, user: User): CalacBuffer = {
b.total += user.age
b.cnt += 1
b
}
override def merge(b1: CalacBuffer, b2: CalacBuffer): CalacBuffer = {
b1.total += b2.total
b1.cnt += b2.cnt
b1
}
override def finish(buffer: CalacBuffer): Long = {
buffer.total / buffer.cnt
}
override def bufferEncoder: Encoder[CalacBuffer] = Encoders.product
override def outputEncoder: Encoder[Long] = Encoders.scalaLong
}
15.3:通用读取方式和默认读取方式
user.json is not a Parquet file. expected magic number at tail [80, 65, 82, 49] but found [32, 53, 48, 125]
SparkSQL通用读取的方法,读取的文件为Parquet格式(列式存储格式)
val df = spark.read.load("data/users.parquet")
如果想要读取其他格式的文件也可以,需要额外增加参数进行解析
df.write.mode("append").format("json").save("output")
sparkSQL基于SparkCore
SparkCore读取数据是依赖于Hadoop
Hadoop读取数据是按行读取
SparkSQL读取JSON格式的文件,要求每一行数据必须符合JSON格式
读入数据:
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
//方式1:通用的load方法读取
spark.read.format("jdbc")
.option("url", "jdbc:mysql://linux1:3306/spark-sql")
.option("driver", "com.mysql.jdbc.Driver")
.option("user", "root")
.option("password", "123123")
.option("dbtable", "user")
.load().show
//方式2:通用的load方法读取 参数另一种形式
spark.read.format("jdbc")
.options(Map("url"->"jdbc:mysql://linux1:3306/spark-sql?user=root&password=123123",
"dbtable"->"user","driver"->"com.mysql.jdbc.Driver")).load().show
//方式3:使用jdbc方法读取
val props: Properties = new Properties()
props.setProperty("user", "root")
props.setProperty("password", "123123")
val df: Dataframe = spark.read.jdbc("jdbc:mysql://linux1:3306/spark-sql", "user", props)
df.show
//释放资源
spark.stop()
写入数据:
case class User2(name: String, age: Long)
。。。
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
val rdd: RDD[User2] = spark.sparkContext.makeRDD(List(User2("lisi", 20), User2("zs", 30)))
val ds: Dataset[User2] = rdd.toDS
//方式1:通用的方式 format指定写出类型
//方式2:通过jdbc方法
val props: Properties = new Properties()
props.setProperty("user", "root")
props.setProperty("password", "123123")
ds.write.mode(SaveMode.Append).jdbc("jdbc:mysql://linux1:3306/spark-sql", "user", props)
//释放资源
spark.stop()
十六:SparkStreaming(数据模型是离散流DStrem)
实时 离线 批量 流式
Spark是一个离线的批量数据处理的框架
SparkStreaming是一个准实时,微批次的处理框架
16.1:SparkStreaming知识点
Discretized Stream(离散流)是Spark Streaming的基础抽象,代表持续性的数据流和经过各种Spark原语操作后的结果数据流。在内部实现上,DStream是一系列连续的RDD来表示。每个RDD含有一段时间间隔内的数据。
16.2:DStream
blockingQueue队列:阻塞式队列,有反压机制,可以避免数据挤压。
采集数据的方式:DStream监控目录变化,将RDD放入到离散化流DStream中,自定义数据源。
object SparkStreaming05_DIY {
def main(args: Array[String]): Unit = {
//TODO SparkStreaming 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[*]")
.setAppName("Streaming")
.set("spark.testing.memory", "471859200")
//StreamingContext独享的第二个构造参数表示数据采集周期
// val ssc: StreamingContext = new StreamingContext(conf,new Duration(3 * 1000))
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
val uuidDS: ReceiverInputDStream[String] = ssc.receiverStream(new MyReceiver)
uuidDS.print()
//启动采集器
ssc.start()
// 等待采集器结束
ssc.awaitTermination()
}
class MyReceiver extends Receiver[String](StorageLevel.MEMORY_ONLY){
private var flg = true
override def onStart(): Unit = {
//采集数据
while (flg) {
//采集数据
val uuid: String = UUID.randomUUID().toString
//存储数据
store(uuid)
Thread.sleep(1000)
}
}
override def onStop(): Unit = {
//释放资源
flg = false
}
}
}
十七:DStream转换(有无状态转换)
离散化流无法完成排序操作 ,离散化流其实就是每个时间段RDD的封装 ,离散化流可以转换为RDD实现操作
transform用于将离散化流实现不了的功能进行调用,transform方法是DStream的方法,称之为原语,RDD的方法称之为算子 如果想要在Driver端周期性执行一些逻辑的场合,需要使用transform。
将两个窗口key相同的连接起来
使用updateStateByKey需要对检查点目录进行配置,会使用检查点来保存状态。
reduceByKey原语不会保留中间计算结果,一旦采集周期结束,数据丢弃 ,如果想要保留中间计算结果,需要有状态操作
从检查点中恢复数据
def main(args: Array[String]): Unit = {
//从检查点中恢复数据
val outerSSC = StreamingContext.getOrCreate("cp",() => {
//TODO SparkStreaming 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[*]")
.setAppName("Streaming")
.set("spark.testing.memory", "471859200")
//StreamingContext独享的第二个构造参数表示数据采集周期
// val ssc: StreamingContext = new StreamingContext(conf,new Duration(3 * 1000))
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
ssc.checkpoint("cp")
val socketDS: ReceiverInputDStream[String] = ssc.socketTextStream("localhost", 8888)
val wordDS: DStream[String] = socketDS.flatMap(_.split(" "))
val wordToOneDS: DStream[(String, Int)] = wordDS.map((_, 1))
wordToOneDS.updateStateByKey(
(seq : Seq[Int] , buffer : Option[Int]) => {
Option(seq.sum + buffer.getOrElse(0))
}
).print()
ssc
})
//SparkStreaming (reduceByKey) + Redis内存数据库 = 有状态
outerSSC.start()
outerSSC.awaitTermination()
}
十八:DStream窗口
窗口划过的规律是:数据由无到有再到无(先多后少后变没)
DStream离散流有window方法:wordToOneDS.window(Seconds(9),Seconds(3))可以设置窗口的大小。
window操作中,窗口范围和滑动的幅度应该为采集周期的整数倍
如果窗口范围和滑动的幅度相同, 那么就是滚动窗口,没有重复数据 默认情况下,不使用窗口,也会有窗口操作,窗口范围取值为1个采集周期,滑动幅度也是一个采集周期。
如果窗口范围和滑动的幅度不相同, 那么就是滑动窗口
1>窗口范围大于滑动的幅度, 存在重复数据
2>窗口范围小于滑动的幅度, 存在丢失数据的可能
二十:优雅的关闭
对于流式数据,线程的启动和关闭应该是分开的,属于两个线程。
优雅的关闭,不要强制关闭,关闭前,不再接受数据,而是把手头上的数据处理完成之后再关闭。
二十一:DStream输出
如果想要将DStream进行特殊处理,那么可以转换为底层RDD实现
八:双值类型数据集的算子(交集并集差集拉链)
单Value(单一数据集)数据操作,双Value(多个数据集)数据操作
九:Key - Value类型算子(隐式转换rddToPairRDDFunction)
9.1:partitionBy()
repartition : 重分区(分区数量)
partitionBy : 重分区(数据的位置 数据进入到哪个分区), 数据路由(Hash定位),分区器 如果想要让数据重新进行分区,那么需要传递分区规则对象(分区器)
Spark中默认常用的分区器:RangePartitioner(要求数据必须能排序) & HashPartitioner(默认)
思考一个问题:如果重分区的分区器和当前RDD的分区器一样怎么办?
此时不做任何处理,还是当前分区,不需要重新shuffle
9.2:reduceByKey groupByKey
TODO reduceByKey算子用于将数据集中相同的key的value数据聚合在一起实现两两聚合
spark的算子有很多是以byKey结尾的,就是说将K-V独立使用
TODO reduceByKey算子可以实现 WordCount
9.3:aggregateByKey foldByKey
需求 数据集中相同的Key的分区内取最大值,分区间求和
reduceByKey groupByKey只考虑key不考虑分区而reduceByKey 在逻辑上,
分区内和分区间是一样的都是聚合 因此reduceByKey groupByKey都不能处理该需求。
需求 如果分区内和分区间的计算规则不一定相同时,Spark提供了一个算子来实现。aggregateByKey
aggregateByKey算子存在函数柯里化,有多个参数列表
第一个参数列表中有一个参数
第一个参数表示计算初始值
第二个参数列表中有二个参数
第一个参数表示 : 分区内计算规则
第二个参数表示 : 分区间计算规则
9.4:combineByKey
reduceByKey,aggregateByKey ,foldByKey, combineByKey本质区别
9.5:sortByKey
9.6:Join leftOuterJoin rightOuterJoin fullOuterJoin
9.7 : cogroup
join算子体现的是数据的关系
join算子将两个数据集中相同的key的数据,连接在一起
zip是将两个数据集位置相同的数据,连接起来与key无关
join 可能存在笛卡尔积 也 存在shuffle()
笛卡尔积 在落盘的时候 如果存在大量相同的key的时候数据量会激增
如果能够使用其它算子实现的功能,那么不推荐使用join
rdd1.join(rdd2).collect().foreach(println)
rdd1.leftOuterJoin(rdd2).collect().foreach(println) rdd1.rightOuterJoin(rdd2).collect().foreach(println) rdd1.fullOuterJoin(rdd2).collect().foreach(println)
cogroup = connect(多个数据集) + group(单一数据集)
9.6:案例实操
方式一:
// 统计出每一个省份每个广告被点击数量排行的Top3
val fileDatas: RDD[String] = sc.textFile("data/agent.log")
val groupDatas: RDD[(String, Iterable[String])] = fileDatas.groupBy(
line => {
val datas: Array[String] = line.split(" ")
datas(1)
}
)
val top3: RDD[(String, List[(String, Int)])] = groupDatas.mapValues(
//数据集是List(line, line, line)
list => {
//数据集是line
val adToCount: Map[String, Int] = list.map(
line => {
val datas: Array[String] = line.split(" ")
(datas(4), 1)
}
//数据集是List((省份,广告)) 按照省份进行分组统计
).groupBy(_._1).mapValues(_.size)
//Map[String, Int] [广告,点击次数]
// 在 groupDatas.mapValues( 的架子中都是对value进行处理的
//元组天生的排序 数据类型[广告,点击次数] 前三条
adToCount.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
}
)
top3.collect().foreach(println)
sc.stop()
}
方式二:
val fileDatas: RDD[String] = sc.textFile("data/agent.log")
// 1. 将广告进行统计分析 (word, cnt)
// ((省份1,广告1), 1),((省份2,广告2), 1)((省份1,广告1), 1)
// ((省份1,广告1), sum),((省份1,广告2), sum)
val reduceDatas: RDD[((String, String), Int)] = fileDatas.map(
line => {
val datas: Array[String] = line.split(" ")
((datas(1), datas(4)), 1)
}
).reduceByKey(_ + _)
// 1.5 将统计结果进行格式转换
// (省份1,(广告1, sum)),(省份1,(广告2, sum))
val mapDatas: RDD[(String, (String, Int))] = reduceDatas.map {
case ((prv, ad), sum) => {
(prv, (ad, sum))
}
}
// 2. 将统计结果按照省份进行分组
// (省份1, (广告1, sum),(广告2, sum))
val prvGroupDatas: RDD[(String, Iterable[(String, Int)])] = mapDatas.groupByKey()
// 3. 将分组后的数据按照点击数量进行排序(降序),取前3名
val top3: RDD[(String, List[(String, Int)])] = prvGroupDatas.mapValues(
iter => {
iter.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
}
)
top3.collect().foreach(println)
十:action行动算子(是对结果进行处理的)
TODO : 所谓的行动(Action)算子,其实就是RDD用于触发作业执行的方法,类似于IO中的read方法
转换算子的返回值为RDD 行动算子的返回值为具体的结果
行动算子和作业的关系 :1 对 1
10.2:reduce
reduce算子可以触发作业的执行,分区内先两两计算,分区间再两两计算
分区内计算都是在Executor。
分区间计算都是在Driver
10.3:count first take takeOrdered
10.4: aggregate fold
10.5: countByKey countByValue
10.5: save相关的算子
rdd.saveAsTextFile("output1")
rdd.saveAsObjectFile("output2")
rdd.saveAsSequenceFile("output3")
10.5: collect foreach
10.6: 序列化闭包检测
一个函数使用了外部的数据,并且改变了这个数据的生命周期 将数据包含到函数内部,形成了一个闭合的环境,这个环境称之为闭包环境,简称闭包
Spark在运行作业之前,会判断程序逻辑中是否包含闭包
如果存在闭包操作,那么需要检测闭包中使用的数据是否能够序列化,如果不能,那么发生错误
这个操作称之为闭包检测
十一:RDD阶段的划分
前一个阶段不执行完,后面的阶段是无法执行的。
十二:依赖关系和血缘关系(宽依赖窄依赖)
十三:持久化
(持久化只针对于当前应用程序,资源使用完之后释放资源,不会给其它节点使用。因此诞生了检查点)
将计算结果缓存起来。
数据的持久化默认将数据保存到内存中,重复使用
wordToOne.cache()
持久化操作也可以保存到磁盘中
wordToOne.persist()
保存级别设置
十三:RDD CheckPoint检查点
java中的finalize方法是Object类中提供的一个方法,在GC准备释放对象所占用的内存空间之前,它将首先调用finalize()方法。
持久化只针对于当前应用程序,资源使用完之后释放资源,不会给其它节点使用。
持久化只针对于当前应用程序
跨应用程序的数据共享需要采用 检查点机制
设置的方式
① :sc.setCheckpointDir("cp") 该检查点的目录一般为分布式目录以便各个节点使用
②:wordToOne.checkpoint()
持久化操作会在血缘关系中增加依赖
检查点一般会将数据保存到分布式文件系统中(HDFS)(新的数据源)
检查点会切断血缘即减少血缘依赖,因为可以把HDFS中相对可靠的数据作为新的数据源。
十四:自定义分区 RDD文件读取与保存
Hash分区:对于给定的key,计算其hashCode,并除以分区个数取余
Range分区:将一定范围内的数据映射到一个分区中,尽量保证每个分区数据均匀,而且分区间有序
十四:累加器(数据模型)(功能比较单一没有shuffle性能相对高)
累加器的现象
分析:
如果数据和RDD有关系,那么可以从Driver端发送到Executor端执行
如果执行完毕,那么可以将结果拉回到Driver端
如果数据和RDD没有关系,因为计算中存在闭包操作,所以RDD会将数据发送到Executor
但是无法将数据从Executor端拉回到Driver端。
如果想要将和RDD无关的数据拉回到Driver端,RDD模型处理不了,需要采用新的数据模型——累加器
doubleAccumulator
collectionAccumulator 累加器
def main(args: Array[String]): Unit = {
// TODO Spark 环境
val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(conf)
// TODO 1. 创建累加器
val wordCountAcc = new MyAcculumator
// TODO 2. 将累加器注册到Spark中
sc.register(wordCountAcc, "WordCount")
val rdd = sc.makeRDD(
List(
"Hello",
"Hello",
"Hello",
"World",
"Hello",
),2
)
rdd.foreach(
word => {
// TODO 3. 使用累加器
wordCountAcc.add(word)
}
)
// TODO 4. 获取累加器的结果
println(wordCountAcc.value)
sc.stop()
}
// TODO 自定义数据累加器(WordCount)
// 1. 继承AccumulatorV2
// 2. 定义数据的泛型
// IN : String
// OUT : mutable.Map[String, Int]
// 3. 重写方法(3(计算) + 3(状态) = 6)
class MyAcculumator extends AccumulatorV2[String, mutable.Map[String, Int]]{
private val wordCountMap = mutable.Map[String, Int]()
// TODO 判断当前累加器是否为初始状态
override def isZero: Boolean = {
wordCountMap.isEmpty
}
// TODO 复制累加器
override def copy(): AccumulatorV2[String, mutable.Map[String, Int]] = {
new MyAcculumator()
}
// TODO 重置累加器
override def reset(): Unit = {
wordCountMap.clear()
}
// TODO 将外部的数据增加到累加器中
override def add(word: String): Unit = {
val oldCnt: Int = wordCountMap.getOrElse(word, 0)
wordCountMap.update(word, oldCnt + 1)
}
// TODO 将多个累加器进行合并
override def merge(other: AccumulatorV2[String, mutable.Map[String, Int]]): Unit = {
other.value.foreach {
case (word, cnt) => {
val oldCnt: Int = wordCountMap.getOrElse(word, 0)
wordCountMap.update(word, oldCnt + cnt)
}
}
}
// TODO 获取累加器的结果
override def value: mutable.Map[String, Int] = {
wordCountMap
}
}
十五:Join现象引出广播变量
def main(args: Array[String]): Unit = {
// TODO Spark 环境
val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(conf)
val rdd1 = sc.makeRDD(
List(
("a", 1),
("b", 2),
)
)
val rdd2 = sc.makeRDD(
List(
("a", 3),
("b", 4),
)
)
// ("a", 1)
// ( a,(1,3))
//rdd1.join(rdd2).collect().foreach(println)
// 计算的对象是以Task为单位进行数据发送的,所以可能会导致大量的数据冗余
// 所以如果计算对象占用比较大的资源,性能会急剧下降
// 为了提供数据访问效率,可以将数据发送给Executor,而不是发给Task。
// RDD数据模型只能给Task发。只能采用其他的数据模型:TODO 广播变量
val map = mutable.Map[String, Int](
("a", 3),
("b", 4)
)
// TODO 使用广播变量
val bc: Broadcast[mutable.Map[String, Int]] = sc.broadcast(map)
rdd1.map {
case (k, v1) => {
// TODO 获取广播变量的值
var v2 = bc.value.getOrElse(k, 0)
(k, (v1, v2))
}
}.collect.foreach(println)
sc.stop()
}
计算的对象是以Task为单位进行数据发送的,所以可能会导致大量的数据冗余 且 如果计算对象占用比较大的资源,性能会急剧下降
为了提供数据访问效率,可以将数据发送给Executor,而不是发给Task。
但是RDD数据模型只能给Task发。只能采用其他的数据模型: 广播变量(可以将数据发送给Executor 使得同一个节点上的Task可以共享一份资源,并且如果有Executor节点与Driver通信比较慢的时候 可以从就近的Executor上获得需要的资源)
十五:SparkSQL(Dataframe DataSet RDD 转换)
Spark + Hive => Shark
Spark SQL是Spark用于结构化数据(structured data)处理的Spark模块。
1.2 SparkSQL特点:
无缝的整合了 SQL 查询和 Spark 编程
SparkSQL可以简化RDD的开发,提高开发效率
15.1:Dataframe 与 DataSet 与 RDD概述
SQL语言本身没有类型
工厂模式生产的对象是有相同的功能的,因为从同一条生产线下来的。
Builder模式要生产某个对象需要经过复杂的过程,但是builder模式会把这些过程顺序确定下来。
Scala中的构造方法有两种,主构造方法和辅助构造方法,辅助构造方法的函数名一定为this。
15.2:Dataframe 与 DataSet 与 RDD
Dataframe : 是SparkSQL中的核心数据模型,是弱类型的模型,不关心数据的类型操作。
因为SQL的局限性,因此在处理数据的时候,Dataframe提供了一种DSL语法(面向对象的语法)
SparkSQL 与 RDD的关系与转换
SparkSQL模块等同于是对SparkCore一个封装,专门用于结构化数据处理的场景
Dataset : 就是使用面向对象的方式操作数据, 类,属性 => 结构 就是
在Dataframe的基础上,增加了类型信息
SparkSQL中存在大量的隐式转换操作,但是一般会导入使用 import时,使用的spark并不是包名,是环境对象的名称。
Dataframe 与 DataSet的区别?
Dataframe是特定类型的DataSet[Row]
说明DataSet兼容Dataframe()是将来用的比较多的
注意:如果从内存中获取数据,spark可以知道数据类型具体是什么。如果是数字,默认作为Int处理;但是从文件中读取的数字,不能确定是什么类型,所以用bigint接收,可以和Long类型转换,但是和Int不能进行转换
SparkSQL技术选型:
15.2:UDF 与 UDAF
package com.atguigu.bigdata.spark.sql01
import org.apache.spark.sql.expressions.Aggregator
import org.apache.spark.sql.{Dataframe, Encoder, Encoders, SparkSession, functions}
object SparkSQL09_Dataframe_UDAF {
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
df.createOrReplaceTempView("user")
// TODO UDAF使用时,需要采用特殊的方式实现自定义操作
// TODO 强类型操作不能应用于弱类型操作,如果想用,那么需要转换functions.udaf
// Spark3.0以后,可以通过转换,让强类型应用在弱类型操作
spark.udf.register("avgAge",functions.udaf(new AvgAgeUDF()))
// TODO : UDF
// 自定义的函数在SQL文使用
spark.sql("select avgAge(age) from user").show()
spark.stop()
}
case class CalcBuffer(var total : Long , var cnt : Long)
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 1. 继承org.apache.spark.sql.expressions.Aggregator
// 2. 定义泛型
// IN : Long
// BUFF : CalcBuffer
// OUT : Long
// 3. 重写方法 (4 + 2(固定) = 6)
class AvgAgeUDF extends Aggregator[Long,CalcBuffer,Long]{
// TODO 缓冲区的初始化
override def zero: CalcBuffer = {
CalcBuffer(0l,0l)
}
// TODO 将输入的数据和缓冲区的数据进行聚合操作
override def reduce(buff: CalcBuffer, a: Long): CalcBuffer ={
buff.total += a
buff.cnt += 1
buff
}
// TODO 多个缓冲区的合并操作
override def merge(b1: CalcBuffer, b2: CalcBuffer): CalcBuffer = {
b1.total += b2.total
b1.cnt += b2.cnt
b1
}
// TODO 计算数据
override def finish(reduction: CalcBuffer): Long = {
reduction.total / reduction.cnt
}
override def bufferEncoder: Encoder[CalcBuffer] = Encoders.product
override def outputEncoder: Encoder[Long] = Encoders.scalaLong
}
}
UDAF老版本:
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
df.createOrReplaceTempView("user")
// TODO UDAF使用时,需要采用特殊的方式实现自定义操作
// TODO 强类型操作不能应用于弱类型操作,如果想用,那么需要转换functions.udaf
// Spark3.0以后,可以通过转换,让强类型应用在弱类型操作
spark.udf.register("avgAge",new AvgAgeUDAF())
// TODO : UDF
// 自定义的函数在SQL文使用
spark.sql("select avgAge(age) from user").show()
spark.stop()
}
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 使用早期Spark版本的UDAF(弱类型)
// 1. 继承UserDefinedAggregateFunction
// 2. 重写方法(8)
class AvgAgeUDAF extends UserDefinedAggregateFunction{
// TODO 输入数据的结构
override def inputSchema: StructType = {
StructType(
Array(
StructField("age",LongType)
)
)
}
// TODO 缓冲区数据的结构
override def bufferSchema: StructType = {
StructType(
Array(
StructField("total",LongType),
StructField("cnt",LongType)
)
)
}
// TODO 输出数据的类型
override def dataType: DataType = LongType
// TODO 计算稳定性
override def deterministic: Boolean = true
// TODO 缓冲区的初始化
override def initialize(buffer: MutableAggregationBuffer): Unit = {
buffer.update(0,0l)
buffer.update(1,0l)
}
// TODO 将输入的数据和缓冲区的数据进行聚合操作
override def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
buffer.update(0,buffer.getLong(0) + input.getLong(0))
buffer.update(1,buffer.getLong(1)+1)
}
// TODO 多个缓冲区的合并操作
override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
buffer1.update(0,buffer1.getLong(0) + buffer2.getLong(0))
buffer1.update(1,buffer1.getLong(1) + buffer2.getLong(1))
}
override def evaluate(buffer: Row): Any = {
buffer.getLong(0) / buffer.getLong(1)
}
}
UDAF_Class老版本:
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
// TODO Spark3.0版本前,强类型的UDAF不能应用于弱类型的SQL中
import spark.implicits._
val udaf: AvgAgeUDAF = new AvgAgeUDAF
val ds: Dataset[User] = df.as[User]
ds.select(udaf.toColumn).show()
spark.stop()
}
case class User(id : Long , name : String , age : Long)
case class CalacBuffer(var total : Long , var cnt : Long)
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 使用早期Spark版本的UDAF(强类型)
// 1. 继承Aggregator
// 2. 定义泛型
// IN : User
// BUFF : CalcBuffer
// OUT : Long
// 3. 重写方法(6)
class AvgAgeUDAF extends Aggregator[User,CalacBuffer,Long]{
override def zero: CalacBuffer = {
CalacBuffer(0l,0L)
}
override def reduce(b: CalacBuffer, user: User): CalacBuffer = {
b.total += user.age
b.cnt += 1
b
}
override def merge(b1: CalacBuffer, b2: CalacBuffer): CalacBuffer = {
b1.total += b2.total
b1.cnt += b2.cnt
b1
}
override def finish(buffer: CalacBuffer): Long = {
buffer.total / buffer.cnt
}
override def bufferEncoder: Encoder[CalacBuffer] = Encoders.product
override def outputEncoder: Encoder[Long] = Encoders.scalaLong
}
15.3:通用读取方式和默认读取方式
user.json is not a Parquet file. expected magic number at tail [80, 65, 82, 49] but found [32, 53, 48, 125]
SparkSQL通用读取的方法,读取的文件为Parquet格式(列式存储格式)
val df = spark.read.load("data/users.parquet")
如果想要读取其他格式的文件也可以,需要额外增加参数进行解析
df.write.mode("append").format("json").save("output")
sparkSQL基于SparkCore
SparkCore读取数据是依赖于Hadoop
Hadoop读取数据是按行读取
SparkSQL读取JSON格式的文件,要求每一行数据必须符合JSON格式
读入数据:
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
//方式1:通用的load方法读取
spark.read.format("jdbc")
.option("url", "jdbc:mysql://linux1:3306/spark-sql")
.option("driver", "com.mysql.jdbc.Driver")
.option("user", "root")
.option("password", "123123")
.option("dbtable", "user")
.load().show
//方式2:通用的load方法读取 参数另一种形式
spark.read.format("jdbc")
.options(Map("url"->"jdbc:mysql://linux1:3306/spark-sql?user=root&password=123123",
"dbtable"->"user","driver"->"com.mysql.jdbc.Driver")).load().show
//方式3:使用jdbc方法读取
val props: Properties = new Properties()
props.setProperty("user", "root")
props.setProperty("password", "123123")
val df: Dataframe = spark.read.jdbc("jdbc:mysql://linux1:3306/spark-sql", "user", props)
df.show
//释放资源
spark.stop()
写入数据:
case class User2(name: String, age: Long)
。。。
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
val rdd: RDD[User2] = spark.sparkContext.makeRDD(List(User2("lisi", 20), User2("zs", 30)))
val ds: Dataset[User2] = rdd.toDS
//方式1:通用的方式 format指定写出类型
//方式2:通过jdbc方法
val props: Properties = new Properties()
props.setProperty("user", "root")
props.setProperty("password", "123123")
ds.write.mode(SaveMode.Append).jdbc("jdbc:mysql://linux1:3306/spark-sql", "user", props)
//释放资源
spark.stop()
十六:SparkStreaming(数据模型是离散流DStrem)
实时 离线 批量 流式
Spark是一个离线的批量数据处理的框架
SparkStreaming是一个准实时,微批次的处理框架
16.1:SparkStreaming知识点
Discretized Stream(离散流)是Spark Streaming的基础抽象,代表持续性的数据流和经过各种Spark原语操作后的结果数据流。在内部实现上,DStream是一系列连续的RDD来表示。每个RDD含有一段时间间隔内的数据。
16.2:DStream
blockingQueue队列:阻塞式队列,有反压机制,可以避免数据挤压。
采集数据的方式:DStream监控目录变化,将RDD放入到离散化流DStream中,自定义数据源。
object SparkStreaming05_DIY {
def main(args: Array[String]): Unit = {
//TODO SparkStreaming 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[*]")
.setAppName("Streaming")
.set("spark.testing.memory", "471859200")
//StreamingContext独享的第二个构造参数表示数据采集周期
// val ssc: StreamingContext = new StreamingContext(conf,new Duration(3 * 1000))
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
val uuidDS: ReceiverInputDStream[String] = ssc.receiverStream(new MyReceiver)
uuidDS.print()
//启动采集器
ssc.start()
// 等待采集器结束
ssc.awaitTermination()
}
class MyReceiver extends Receiver[String](StorageLevel.MEMORY_ONLY){
private var flg = true
override def onStart(): Unit = {
//采集数据
while (flg) {
//采集数据
val uuid: String = UUID.randomUUID().toString
//存储数据
store(uuid)
Thread.sleep(1000)
}
}
override def onStop(): Unit = {
//释放资源
flg = false
}
}
}
十七:DStream转换(有无状态转换)
离散化流无法完成排序操作 ,离散化流其实就是每个时间段RDD的封装 ,离散化流可以转换为RDD实现操作
transform用于将离散化流实现不了的功能进行调用,transform方法是DStream的方法,称之为原语,RDD的方法称之为算子 如果想要在Driver端周期性执行一些逻辑的场合,需要使用transform。
将两个窗口key相同的连接起来
使用updateStateByKey需要对检查点目录进行配置,会使用检查点来保存状态。
reduceByKey原语不会保留中间计算结果,一旦采集周期结束,数据丢弃 ,如果想要保留中间计算结果,需要有状态操作
从检查点中恢复数据
def main(args: Array[String]): Unit = {
//从检查点中恢复数据
val outerSSC = StreamingContext.getOrCreate("cp",() => {
//TODO SparkStreaming 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[*]")
.setAppName("Streaming")
.set("spark.testing.memory", "471859200")
//StreamingContext独享的第二个构造参数表示数据采集周期
// val ssc: StreamingContext = new StreamingContext(conf,new Duration(3 * 1000))
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
ssc.checkpoint("cp")
val socketDS: ReceiverInputDStream[String] = ssc.socketTextStream("localhost", 8888)
val wordDS: DStream[String] = socketDS.flatMap(_.split(" "))
val wordToOneDS: DStream[(String, Int)] = wordDS.map((_, 1))
wordToOneDS.updateStateByKey(
(seq : Seq[Int] , buffer : Option[Int]) => {
Option(seq.sum + buffer.getOrElse(0))
}
).print()
ssc
})
//SparkStreaming (reduceByKey) + Redis内存数据库 = 有状态
outerSSC.start()
outerSSC.awaitTermination()
}
十八:DStream窗口
窗口划过的规律是:数据由无到有再到无(先多后少后变没)
DStream离散流有window方法:wordToOneDS.window(Seconds(9),Seconds(3))可以设置窗口的大小。
window操作中,窗口范围和滑动的幅度应该为采集周期的整数倍
如果窗口范围和滑动的幅度相同, 那么就是滚动窗口,没有重复数据 默认情况下,不使用窗口,也会有窗口操作,窗口范围取值为1个采集周期,滑动幅度也是一个采集周期。
如果窗口范围和滑动的幅度不相同, 那么就是滑动窗口
1>窗口范围大于滑动的幅度, 存在重复数据
2>窗口范围小于滑动的幅度, 存在丢失数据的可能
二十:优雅的关闭
对于流式数据,线程的启动和关闭应该是分开的,属于两个线程。
优雅的关闭,不要强制关闭,关闭前,不再接受数据,而是把手头上的数据处理完成之后再关闭。
二十一:DStream输出
如果想要将DStream进行特殊处理,那么可以转换为底层RDD实现
9.1:partitionBy()
repartition : 重分区(分区数量)
partitionBy : 重分区(数据的位置 数据进入到哪个分区), 数据路由(Hash定位),分区器 如果想要让数据重新进行分区,那么需要传递分区规则对象(分区器)
Spark中默认常用的分区器:RangePartitioner(要求数据必须能排序) & HashPartitioner(默认)
思考一个问题:如果重分区的分区器和当前RDD的分区器一样怎么办?
此时不做任何处理,还是当前分区,不需要重新shuffle
9.2:reduceByKey groupByKey
TODO reduceByKey算子用于将数据集中相同的key的value数据聚合在一起实现两两聚合
spark的算子有很多是以byKey结尾的,就是说将K-V独立使用
TODO reduceByKey算子可以实现 WordCount
9.3:aggregateByKey foldByKey
需求 数据集中相同的Key的分区内取最大值,分区间求和
reduceByKey groupByKey只考虑key不考虑分区而reduceByKey 在逻辑上,
分区内和分区间是一样的都是聚合 因此reduceByKey groupByKey都不能处理该需求。
需求 如果分区内和分区间的计算规则不一定相同时,Spark提供了一个算子来实现。aggregateByKey
aggregateByKey算子存在函数柯里化,有多个参数列表
第一个参数列表中有一个参数
第一个参数表示计算初始值
第二个参数列表中有二个参数
第一个参数表示 : 分区内计算规则
第二个参数表示 : 分区间计算规则
9.4:combineByKey
reduceByKey,aggregateByKey ,foldByKey, combineByKey本质区别
9.5:sortByKey
9.6:Join leftOuterJoin rightOuterJoin fullOuterJoin
9.7 : cogroup
join算子体现的是数据的关系
join算子将两个数据集中相同的key的数据,连接在一起
zip是将两个数据集位置相同的数据,连接起来与key无关
join 可能存在笛卡尔积 也 存在shuffle()
笛卡尔积 在落盘的时候 如果存在大量相同的key的时候数据量会激增
如果能够使用其它算子实现的功能,那么不推荐使用join
rdd1.join(rdd2).collect().foreach(println)
rdd1.leftOuterJoin(rdd2).collect().foreach(println) rdd1.rightOuterJoin(rdd2).collect().foreach(println) rdd1.fullOuterJoin(rdd2).collect().foreach(println)
cogroup = connect(多个数据集) + group(单一数据集)
9.6:案例实操
方式一:
// 统计出每一个省份每个广告被点击数量排行的Top3
val fileDatas: RDD[String] = sc.textFile("data/agent.log")
val groupDatas: RDD[(String, Iterable[String])] = fileDatas.groupBy(
line => {
val datas: Array[String] = line.split(" ")
datas(1)
}
)
val top3: RDD[(String, List[(String, Int)])] = groupDatas.mapValues(
//数据集是List(line, line, line)
list => {
//数据集是line
val adToCount: Map[String, Int] = list.map(
line => {
val datas: Array[String] = line.split(" ")
(datas(4), 1)
}
//数据集是List((省份,广告)) 按照省份进行分组统计
).groupBy(_._1).mapValues(_.size)
//Map[String, Int] [广告,点击次数]
// 在 groupDatas.mapValues( 的架子中都是对value进行处理的
//元组天生的排序 数据类型[广告,点击次数] 前三条
adToCount.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
}
)
top3.collect().foreach(println)
sc.stop()
}
方式二:
val fileDatas: RDD[String] = sc.textFile("data/agent.log")
// 1. 将广告进行统计分析 (word, cnt)
// ((省份1,广告1), 1),((省份2,广告2), 1)((省份1,广告1), 1)
// ((省份1,广告1), sum),((省份1,广告2), sum)
val reduceDatas: RDD[((String, String), Int)] = fileDatas.map(
line => {
val datas: Array[String] = line.split(" ")
((datas(1), datas(4)), 1)
}
).reduceByKey(_ + _)
// 1.5 将统计结果进行格式转换
// (省份1,(广告1, sum)),(省份1,(广告2, sum))
val mapDatas: RDD[(String, (String, Int))] = reduceDatas.map {
case ((prv, ad), sum) => {
(prv, (ad, sum))
}
}
// 2. 将统计结果按照省份进行分组
// (省份1, (广告1, sum),(广告2, sum))
val prvGroupDatas: RDD[(String, Iterable[(String, Int)])] = mapDatas.groupByKey()
// 3. 将分组后的数据按照点击数量进行排序(降序),取前3名
val top3: RDD[(String, List[(String, Int)])] = prvGroupDatas.mapValues(
iter => {
iter.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
}
)
top3.collect().foreach(println)
十:action行动算子(是对结果进行处理的)
TODO : 所谓的行动(Action)算子,其实就是RDD用于触发作业执行的方法,类似于IO中的read方法
转换算子的返回值为RDD 行动算子的返回值为具体的结果
行动算子和作业的关系 :1 对 1
10.2:reduce
reduce算子可以触发作业的执行,分区内先两两计算,分区间再两两计算
分区内计算都是在Executor。
分区间计算都是在Driver
10.3:count first take takeOrdered
10.4: aggregate fold
10.5: countByKey countByValue
10.5: save相关的算子
rdd.saveAsTextFile("output1")
rdd.saveAsObjectFile("output2")
rdd.saveAsSequenceFile("output3")
10.5: collect foreach
10.6: 序列化闭包检测
一个函数使用了外部的数据,并且改变了这个数据的生命周期 将数据包含到函数内部,形成了一个闭合的环境,这个环境称之为闭包环境,简称闭包
Spark在运行作业之前,会判断程序逻辑中是否包含闭包
如果存在闭包操作,那么需要检测闭包中使用的数据是否能够序列化,如果不能,那么发生错误
这个操作称之为闭包检测
十一:RDD阶段的划分
前一个阶段不执行完,后面的阶段是无法执行的。
十二:依赖关系和血缘关系(宽依赖窄依赖)
十三:持久化
(持久化只针对于当前应用程序,资源使用完之后释放资源,不会给其它节点使用。因此诞生了检查点)
将计算结果缓存起来。
数据的持久化默认将数据保存到内存中,重复使用
wordToOne.cache()
持久化操作也可以保存到磁盘中
wordToOne.persist()
保存级别设置
十三:RDD CheckPoint检查点
java中的finalize方法是Object类中提供的一个方法,在GC准备释放对象所占用的内存空间之前,它将首先调用finalize()方法。
持久化只针对于当前应用程序,资源使用完之后释放资源,不会给其它节点使用。
持久化只针对于当前应用程序
跨应用程序的数据共享需要采用 检查点机制
设置的方式
① :sc.setCheckpointDir("cp") 该检查点的目录一般为分布式目录以便各个节点使用
②:wordToOne.checkpoint()
持久化操作会在血缘关系中增加依赖
检查点一般会将数据保存到分布式文件系统中(HDFS)(新的数据源)
检查点会切断血缘即减少血缘依赖,因为可以把HDFS中相对可靠的数据作为新的数据源。
十四:自定义分区 RDD文件读取与保存
Hash分区:对于给定的key,计算其hashCode,并除以分区个数取余
Range分区:将一定范围内的数据映射到一个分区中,尽量保证每个分区数据均匀,而且分区间有序
十四:累加器(数据模型)(功能比较单一没有shuffle性能相对高)
累加器的现象
分析:
如果数据和RDD有关系,那么可以从Driver端发送到Executor端执行
如果执行完毕,那么可以将结果拉回到Driver端
如果数据和RDD没有关系,因为计算中存在闭包操作,所以RDD会将数据发送到Executor
但是无法将数据从Executor端拉回到Driver端。
如果想要将和RDD无关的数据拉回到Driver端,RDD模型处理不了,需要采用新的数据模型——累加器
doubleAccumulator
collectionAccumulator 累加器
def main(args: Array[String]): Unit = {
// TODO Spark 环境
val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(conf)
// TODO 1. 创建累加器
val wordCountAcc = new MyAcculumator
// TODO 2. 将累加器注册到Spark中
sc.register(wordCountAcc, "WordCount")
val rdd = sc.makeRDD(
List(
"Hello",
"Hello",
"Hello",
"World",
"Hello",
),2
)
rdd.foreach(
word => {
// TODO 3. 使用累加器
wordCountAcc.add(word)
}
)
// TODO 4. 获取累加器的结果
println(wordCountAcc.value)
sc.stop()
}
// TODO 自定义数据累加器(WordCount)
// 1. 继承AccumulatorV2
// 2. 定义数据的泛型
// IN : String
// OUT : mutable.Map[String, Int]
// 3. 重写方法(3(计算) + 3(状态) = 6)
class MyAcculumator extends AccumulatorV2[String, mutable.Map[String, Int]]{
private val wordCountMap = mutable.Map[String, Int]()
// TODO 判断当前累加器是否为初始状态
override def isZero: Boolean = {
wordCountMap.isEmpty
}
// TODO 复制累加器
override def copy(): AccumulatorV2[String, mutable.Map[String, Int]] = {
new MyAcculumator()
}
// TODO 重置累加器
override def reset(): Unit = {
wordCountMap.clear()
}
// TODO 将外部的数据增加到累加器中
override def add(word: String): Unit = {
val oldCnt: Int = wordCountMap.getOrElse(word, 0)
wordCountMap.update(word, oldCnt + 1)
}
// TODO 将多个累加器进行合并
override def merge(other: AccumulatorV2[String, mutable.Map[String, Int]]): Unit = {
other.value.foreach {
case (word, cnt) => {
val oldCnt: Int = wordCountMap.getOrElse(word, 0)
wordCountMap.update(word, oldCnt + cnt)
}
}
}
// TODO 获取累加器的结果
override def value: mutable.Map[String, Int] = {
wordCountMap
}
}
十五:Join现象引出广播变量
def main(args: Array[String]): Unit = {
// TODO Spark 环境
val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(conf)
val rdd1 = sc.makeRDD(
List(
("a", 1),
("b", 2),
)
)
val rdd2 = sc.makeRDD(
List(
("a", 3),
("b", 4),
)
)
// ("a", 1)
// ( a,(1,3))
//rdd1.join(rdd2).collect().foreach(println)
// 计算的对象是以Task为单位进行数据发送的,所以可能会导致大量的数据冗余
// 所以如果计算对象占用比较大的资源,性能会急剧下降
// 为了提供数据访问效率,可以将数据发送给Executor,而不是发给Task。
// RDD数据模型只能给Task发。只能采用其他的数据模型:TODO 广播变量
val map = mutable.Map[String, Int](
("a", 3),
("b", 4)
)
// TODO 使用广播变量
val bc: Broadcast[mutable.Map[String, Int]] = sc.broadcast(map)
rdd1.map {
case (k, v1) => {
// TODO 获取广播变量的值
var v2 = bc.value.getOrElse(k, 0)
(k, (v1, v2))
}
}.collect.foreach(println)
sc.stop()
}
计算的对象是以Task为单位进行数据发送的,所以可能会导致大量的数据冗余 且 如果计算对象占用比较大的资源,性能会急剧下降
为了提供数据访问效率,可以将数据发送给Executor,而不是发给Task。
但是RDD数据模型只能给Task发。只能采用其他的数据模型: 广播变量(可以将数据发送给Executor 使得同一个节点上的Task可以共享一份资源,并且如果有Executor节点与Driver通信比较慢的时候 可以从就近的Executor上获得需要的资源)
十五:SparkSQL(Dataframe DataSet RDD 转换)
Spark + Hive => Shark
Spark SQL是Spark用于结构化数据(structured data)处理的Spark模块。
1.2 SparkSQL特点:
无缝的整合了 SQL 查询和 Spark 编程
SparkSQL可以简化RDD的开发,提高开发效率
15.1:Dataframe 与 DataSet 与 RDD概述
SQL语言本身没有类型
工厂模式生产的对象是有相同的功能的,因为从同一条生产线下来的。
Builder模式要生产某个对象需要经过复杂的过程,但是builder模式会把这些过程顺序确定下来。
Scala中的构造方法有两种,主构造方法和辅助构造方法,辅助构造方法的函数名一定为this。
15.2:Dataframe 与 DataSet 与 RDD
Dataframe : 是SparkSQL中的核心数据模型,是弱类型的模型,不关心数据的类型操作。
因为SQL的局限性,因此在处理数据的时候,Dataframe提供了一种DSL语法(面向对象的语法)
SparkSQL 与 RDD的关系与转换
SparkSQL模块等同于是对SparkCore一个封装,专门用于结构化数据处理的场景
Dataset : 就是使用面向对象的方式操作数据, 类,属性 => 结构 就是
在Dataframe的基础上,增加了类型信息
SparkSQL中存在大量的隐式转换操作,但是一般会导入使用 import时,使用的spark并不是包名,是环境对象的名称。
Dataframe 与 DataSet的区别?
Dataframe是特定类型的DataSet[Row]
说明DataSet兼容Dataframe()是将来用的比较多的
注意:如果从内存中获取数据,spark可以知道数据类型具体是什么。如果是数字,默认作为Int处理;但是从文件中读取的数字,不能确定是什么类型,所以用bigint接收,可以和Long类型转换,但是和Int不能进行转换
SparkSQL技术选型:
15.2:UDF 与 UDAF
package com.atguigu.bigdata.spark.sql01
import org.apache.spark.sql.expressions.Aggregator
import org.apache.spark.sql.{Dataframe, Encoder, Encoders, SparkSession, functions}
object SparkSQL09_Dataframe_UDAF {
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
df.createOrReplaceTempView("user")
// TODO UDAF使用时,需要采用特殊的方式实现自定义操作
// TODO 强类型操作不能应用于弱类型操作,如果想用,那么需要转换functions.udaf
// Spark3.0以后,可以通过转换,让强类型应用在弱类型操作
spark.udf.register("avgAge",functions.udaf(new AvgAgeUDF()))
// TODO : UDF
// 自定义的函数在SQL文使用
spark.sql("select avgAge(age) from user").show()
spark.stop()
}
case class CalcBuffer(var total : Long , var cnt : Long)
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 1. 继承org.apache.spark.sql.expressions.Aggregator
// 2. 定义泛型
// IN : Long
// BUFF : CalcBuffer
// OUT : Long
// 3. 重写方法 (4 + 2(固定) = 6)
class AvgAgeUDF extends Aggregator[Long,CalcBuffer,Long]{
// TODO 缓冲区的初始化
override def zero: CalcBuffer = {
CalcBuffer(0l,0l)
}
// TODO 将输入的数据和缓冲区的数据进行聚合操作
override def reduce(buff: CalcBuffer, a: Long): CalcBuffer ={
buff.total += a
buff.cnt += 1
buff
}
// TODO 多个缓冲区的合并操作
override def merge(b1: CalcBuffer, b2: CalcBuffer): CalcBuffer = {
b1.total += b2.total
b1.cnt += b2.cnt
b1
}
// TODO 计算数据
override def finish(reduction: CalcBuffer): Long = {
reduction.total / reduction.cnt
}
override def bufferEncoder: Encoder[CalcBuffer] = Encoders.product
override def outputEncoder: Encoder[Long] = Encoders.scalaLong
}
}
UDAF老版本:
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
df.createOrReplaceTempView("user")
// TODO UDAF使用时,需要采用特殊的方式实现自定义操作
// TODO 强类型操作不能应用于弱类型操作,如果想用,那么需要转换functions.udaf
// Spark3.0以后,可以通过转换,让强类型应用在弱类型操作
spark.udf.register("avgAge",new AvgAgeUDAF())
// TODO : UDF
// 自定义的函数在SQL文使用
spark.sql("select avgAge(age) from user").show()
spark.stop()
}
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 使用早期Spark版本的UDAF(弱类型)
// 1. 继承UserDefinedAggregateFunction
// 2. 重写方法(8)
class AvgAgeUDAF extends UserDefinedAggregateFunction{
// TODO 输入数据的结构
override def inputSchema: StructType = {
StructType(
Array(
StructField("age",LongType)
)
)
}
// TODO 缓冲区数据的结构
override def bufferSchema: StructType = {
StructType(
Array(
StructField("total",LongType),
StructField("cnt",LongType)
)
)
}
// TODO 输出数据的类型
override def dataType: DataType = LongType
// TODO 计算稳定性
override def deterministic: Boolean = true
// TODO 缓冲区的初始化
override def initialize(buffer: MutableAggregationBuffer): Unit = {
buffer.update(0,0l)
buffer.update(1,0l)
}
// TODO 将输入的数据和缓冲区的数据进行聚合操作
override def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
buffer.update(0,buffer.getLong(0) + input.getLong(0))
buffer.update(1,buffer.getLong(1)+1)
}
// TODO 多个缓冲区的合并操作
override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
buffer1.update(0,buffer1.getLong(0) + buffer2.getLong(0))
buffer1.update(1,buffer1.getLong(1) + buffer2.getLong(1))
}
override def evaluate(buffer: Row): Any = {
buffer.getLong(0) / buffer.getLong(1)
}
}
UDAF_Class老版本:
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
// TODO Spark3.0版本前,强类型的UDAF不能应用于弱类型的SQL中
import spark.implicits._
val udaf: AvgAgeUDAF = new AvgAgeUDAF
val ds: Dataset[User] = df.as[User]
ds.select(udaf.toColumn).show()
spark.stop()
}
case class User(id : Long , name : String , age : Long)
case class CalacBuffer(var total : Long , var cnt : Long)
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 使用早期Spark版本的UDAF(强类型)
// 1. 继承Aggregator
// 2. 定义泛型
// IN : User
// BUFF : CalcBuffer
// OUT : Long
// 3. 重写方法(6)
class AvgAgeUDAF extends Aggregator[User,CalacBuffer,Long]{
override def zero: CalacBuffer = {
CalacBuffer(0l,0L)
}
override def reduce(b: CalacBuffer, user: User): CalacBuffer = {
b.total += user.age
b.cnt += 1
b
}
override def merge(b1: CalacBuffer, b2: CalacBuffer): CalacBuffer = {
b1.total += b2.total
b1.cnt += b2.cnt
b1
}
override def finish(buffer: CalacBuffer): Long = {
buffer.total / buffer.cnt
}
override def bufferEncoder: Encoder[CalacBuffer] = Encoders.product
override def outputEncoder: Encoder[Long] = Encoders.scalaLong
}
15.3:通用读取方式和默认读取方式
user.json is not a Parquet file. expected magic number at tail [80, 65, 82, 49] but found [32, 53, 48, 125]
SparkSQL通用读取的方法,读取的文件为Parquet格式(列式存储格式)
val df = spark.read.load("data/users.parquet")
如果想要读取其他格式的文件也可以,需要额外增加参数进行解析
df.write.mode("append").format("json").save("output")
sparkSQL基于SparkCore
SparkCore读取数据是依赖于Hadoop
Hadoop读取数据是按行读取
SparkSQL读取JSON格式的文件,要求每一行数据必须符合JSON格式
读入数据:
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
//方式1:通用的load方法读取
spark.read.format("jdbc")
.option("url", "jdbc:mysql://linux1:3306/spark-sql")
.option("driver", "com.mysql.jdbc.Driver")
.option("user", "root")
.option("password", "123123")
.option("dbtable", "user")
.load().show
//方式2:通用的load方法读取 参数另一种形式
spark.read.format("jdbc")
.options(Map("url"->"jdbc:mysql://linux1:3306/spark-sql?user=root&password=123123",
"dbtable"->"user","driver"->"com.mysql.jdbc.Driver")).load().show
//方式3:使用jdbc方法读取
val props: Properties = new Properties()
props.setProperty("user", "root")
props.setProperty("password", "123123")
val df: Dataframe = spark.read.jdbc("jdbc:mysql://linux1:3306/spark-sql", "user", props)
df.show
//释放资源
spark.stop()
写入数据:
case class User2(name: String, age: Long)
。。。
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
val rdd: RDD[User2] = spark.sparkContext.makeRDD(List(User2("lisi", 20), User2("zs", 30)))
val ds: Dataset[User2] = rdd.toDS
//方式1:通用的方式 format指定写出类型
//方式2:通过jdbc方法
val props: Properties = new Properties()
props.setProperty("user", "root")
props.setProperty("password", "123123")
ds.write.mode(SaveMode.Append).jdbc("jdbc:mysql://linux1:3306/spark-sql", "user", props)
//释放资源
spark.stop()
十六:SparkStreaming(数据模型是离散流DStrem)
实时 离线 批量 流式
Spark是一个离线的批量数据处理的框架
SparkStreaming是一个准实时,微批次的处理框架
16.1:SparkStreaming知识点
Discretized Stream(离散流)是Spark Streaming的基础抽象,代表持续性的数据流和经过各种Spark原语操作后的结果数据流。在内部实现上,DStream是一系列连续的RDD来表示。每个RDD含有一段时间间隔内的数据。
16.2:DStream
blockingQueue队列:阻塞式队列,有反压机制,可以避免数据挤压。
采集数据的方式:DStream监控目录变化,将RDD放入到离散化流DStream中,自定义数据源。
object SparkStreaming05_DIY {
def main(args: Array[String]): Unit = {
//TODO SparkStreaming 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[*]")
.setAppName("Streaming")
.set("spark.testing.memory", "471859200")
//StreamingContext独享的第二个构造参数表示数据采集周期
// val ssc: StreamingContext = new StreamingContext(conf,new Duration(3 * 1000))
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
val uuidDS: ReceiverInputDStream[String] = ssc.receiverStream(new MyReceiver)
uuidDS.print()
//启动采集器
ssc.start()
// 等待采集器结束
ssc.awaitTermination()
}
class MyReceiver extends Receiver[String](StorageLevel.MEMORY_ONLY){
private var flg = true
override def onStart(): Unit = {
//采集数据
while (flg) {
//采集数据
val uuid: String = UUID.randomUUID().toString
//存储数据
store(uuid)
Thread.sleep(1000)
}
}
override def onStop(): Unit = {
//释放资源
flg = false
}
}
}
十七:DStream转换(有无状态转换)
离散化流无法完成排序操作 ,离散化流其实就是每个时间段RDD的封装 ,离散化流可以转换为RDD实现操作
transform用于将离散化流实现不了的功能进行调用,transform方法是DStream的方法,称之为原语,RDD的方法称之为算子 如果想要在Driver端周期性执行一些逻辑的场合,需要使用transform。
将两个窗口key相同的连接起来
使用updateStateByKey需要对检查点目录进行配置,会使用检查点来保存状态。
reduceByKey原语不会保留中间计算结果,一旦采集周期结束,数据丢弃 ,如果想要保留中间计算结果,需要有状态操作
从检查点中恢复数据
def main(args: Array[String]): Unit = {
//从检查点中恢复数据
val outerSSC = StreamingContext.getOrCreate("cp",() => {
//TODO SparkStreaming 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[*]")
.setAppName("Streaming")
.set("spark.testing.memory", "471859200")
//StreamingContext独享的第二个构造参数表示数据采集周期
// val ssc: StreamingContext = new StreamingContext(conf,new Duration(3 * 1000))
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
ssc.checkpoint("cp")
val socketDS: ReceiverInputDStream[String] = ssc.socketTextStream("localhost", 8888)
val wordDS: DStream[String] = socketDS.flatMap(_.split(" "))
val wordToOneDS: DStream[(String, Int)] = wordDS.map((_, 1))
wordToOneDS.updateStateByKey(
(seq : Seq[Int] , buffer : Option[Int]) => {
Option(seq.sum + buffer.getOrElse(0))
}
).print()
ssc
})
//SparkStreaming (reduceByKey) + Redis内存数据库 = 有状态
outerSSC.start()
outerSSC.awaitTermination()
}
十八:DStream窗口
窗口划过的规律是:数据由无到有再到无(先多后少后变没)
DStream离散流有window方法:wordToOneDS.window(Seconds(9),Seconds(3))可以设置窗口的大小。
window操作中,窗口范围和滑动的幅度应该为采集周期的整数倍
如果窗口范围和滑动的幅度相同, 那么就是滚动窗口,没有重复数据 默认情况下,不使用窗口,也会有窗口操作,窗口范围取值为1个采集周期,滑动幅度也是一个采集周期。
如果窗口范围和滑动的幅度不相同, 那么就是滑动窗口
1>窗口范围大于滑动的幅度, 存在重复数据
2>窗口范围小于滑动的幅度, 存在丢失数据的可能
二十:优雅的关闭
对于流式数据,线程的启动和关闭应该是分开的,属于两个线程。
优雅的关闭,不要强制关闭,关闭前,不再接受数据,而是把手头上的数据处理完成之后再关闭。
二十一:DStream输出
如果想要将DStream进行特殊处理,那么可以转换为底层RDD实现
repartition : 重分区(分区数量)
partitionBy : 重分区(数据的位置 数据进入到哪个分区), 数据路由(Hash定位),分区器 如果想要让数据重新进行分区,那么需要传递分区规则对象(分区器)
Spark中默认常用的分区器:RangePartitioner(要求数据必须能排序) & HashPartitioner(默认)
思考一个问题:如果重分区的分区器和当前RDD的分区器一样怎么办?
此时不做任何处理,还是当前分区,不需要重新shuffle
Spark中默认常用的分区器:RangePartitioner(要求数据必须能排序) & HashPartitioner(默认)
思考一个问题:如果重分区的分区器和当前RDD的分区器一样怎么办?
此时不做任何处理,还是当前分区,不需要重新shuffle
此时不做任何处理,还是当前分区,不需要重新shuffle
TODO reduceByKey算子用于将数据集中相同的key的value数据聚合在一起实现两两聚合
spark的算子有很多是以byKey结尾的,就是说将K-V独立使用
TODO reduceByKey算子可以实现 WordCount
9.3:aggregateByKey foldByKey
需求 数据集中相同的Key的分区内取最大值,分区间求和
reduceByKey groupByKey只考虑key不考虑分区而reduceByKey 在逻辑上,
分区内和分区间是一样的都是聚合 因此reduceByKey groupByKey都不能处理该需求。
需求 如果分区内和分区间的计算规则不一定相同时,Spark提供了一个算子来实现。aggregateByKey
aggregateByKey算子存在函数柯里化,有多个参数列表
第一个参数列表中有一个参数
第一个参数表示计算初始值
第二个参数列表中有二个参数
第一个参数表示 : 分区内计算规则
第二个参数表示 : 分区间计算规则
9.4:combineByKey
reduceByKey,aggregateByKey ,foldByKey, combineByKey本质区别
9.5:sortByKey
9.6:Join leftOuterJoin rightOuterJoin fullOuterJoin
9.7 : cogroup
join算子体现的是数据的关系
join算子将两个数据集中相同的key的数据,连接在一起
zip是将两个数据集位置相同的数据,连接起来与key无关
join 可能存在笛卡尔积 也 存在shuffle()
笛卡尔积 在落盘的时候 如果存在大量相同的key的时候数据量会激增
如果能够使用其它算子实现的功能,那么不推荐使用join
rdd1.join(rdd2).collect().foreach(println)
rdd1.leftOuterJoin(rdd2).collect().foreach(println) rdd1.rightOuterJoin(rdd2).collect().foreach(println) rdd1.fullOuterJoin(rdd2).collect().foreach(println)
cogroup = connect(多个数据集) + group(单一数据集)
9.6:案例实操
方式一:
// 统计出每一个省份每个广告被点击数量排行的Top3
val fileDatas: RDD[String] = sc.textFile("data/agent.log")
val groupDatas: RDD[(String, Iterable[String])] = fileDatas.groupBy(
line => {
val datas: Array[String] = line.split(" ")
datas(1)
}
)
val top3: RDD[(String, List[(String, Int)])] = groupDatas.mapValues(
//数据集是List(line, line, line)
list => {
//数据集是line
val adToCount: Map[String, Int] = list.map(
line => {
val datas: Array[String] = line.split(" ")
(datas(4), 1)
}
//数据集是List((省份,广告)) 按照省份进行分组统计
).groupBy(_._1).mapValues(_.size)
//Map[String, Int] [广告,点击次数]
// 在 groupDatas.mapValues( 的架子中都是对value进行处理的
//元组天生的排序 数据类型[广告,点击次数] 前三条
adToCount.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
}
)
top3.collect().foreach(println)
sc.stop()
}
方式二:
val fileDatas: RDD[String] = sc.textFile("data/agent.log")
// 1. 将广告进行统计分析 (word, cnt)
// ((省份1,广告1), 1),((省份2,广告2), 1)((省份1,广告1), 1)
// ((省份1,广告1), sum),((省份1,广告2), sum)
val reduceDatas: RDD[((String, String), Int)] = fileDatas.map(
line => {
val datas: Array[String] = line.split(" ")
((datas(1), datas(4)), 1)
}
).reduceByKey(_ + _)
// 1.5 将统计结果进行格式转换
// (省份1,(广告1, sum)),(省份1,(广告2, sum))
val mapDatas: RDD[(String, (String, Int))] = reduceDatas.map {
case ((prv, ad), sum) => {
(prv, (ad, sum))
}
}
// 2. 将统计结果按照省份进行分组
// (省份1, (广告1, sum),(广告2, sum))
val prvGroupDatas: RDD[(String, Iterable[(String, Int)])] = mapDatas.groupByKey()
// 3. 将分组后的数据按照点击数量进行排序(降序),取前3名
val top3: RDD[(String, List[(String, Int)])] = prvGroupDatas.mapValues(
iter => {
iter.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
}
)
top3.collect().foreach(println)
十:action行动算子(是对结果进行处理的)
TODO : 所谓的行动(Action)算子,其实就是RDD用于触发作业执行的方法,类似于IO中的read方法
转换算子的返回值为RDD 行动算子的返回值为具体的结果
行动算子和作业的关系 :1 对 1
10.2:reduce
reduce算子可以触发作业的执行,分区内先两两计算,分区间再两两计算
分区内计算都是在Executor。
分区间计算都是在Driver
10.3:count first take takeOrdered
10.4: aggregate fold
10.5: countByKey countByValue
10.5: save相关的算子
rdd.saveAsTextFile("output1")
rdd.saveAsObjectFile("output2")
rdd.saveAsSequenceFile("output3")
10.5: collect foreach
10.6: 序列化闭包检测
一个函数使用了外部的数据,并且改变了这个数据的生命周期 将数据包含到函数内部,形成了一个闭合的环境,这个环境称之为闭包环境,简称闭包
Spark在运行作业之前,会判断程序逻辑中是否包含闭包
如果存在闭包操作,那么需要检测闭包中使用的数据是否能够序列化,如果不能,那么发生错误
这个操作称之为闭包检测
十一:RDD阶段的划分
前一个阶段不执行完,后面的阶段是无法执行的。
十二:依赖关系和血缘关系(宽依赖窄依赖)
十三:持久化
(持久化只针对于当前应用程序,资源使用完之后释放资源,不会给其它节点使用。因此诞生了检查点)
将计算结果缓存起来。
数据的持久化默认将数据保存到内存中,重复使用
wordToOne.cache()
持久化操作也可以保存到磁盘中
wordToOne.persist()
保存级别设置
十三:RDD CheckPoint检查点
java中的finalize方法是Object类中提供的一个方法,在GC准备释放对象所占用的内存空间之前,它将首先调用finalize()方法。
持久化只针对于当前应用程序,资源使用完之后释放资源,不会给其它节点使用。
持久化只针对于当前应用程序
跨应用程序的数据共享需要采用 检查点机制
设置的方式
① :sc.setCheckpointDir("cp") 该检查点的目录一般为分布式目录以便各个节点使用
②:wordToOne.checkpoint()
持久化操作会在血缘关系中增加依赖
检查点一般会将数据保存到分布式文件系统中(HDFS)(新的数据源)
检查点会切断血缘即减少血缘依赖,因为可以把HDFS中相对可靠的数据作为新的数据源。
十四:自定义分区 RDD文件读取与保存
Hash分区:对于给定的key,计算其hashCode,并除以分区个数取余
Range分区:将一定范围内的数据映射到一个分区中,尽量保证每个分区数据均匀,而且分区间有序
十四:累加器(数据模型)(功能比较单一没有shuffle性能相对高)
累加器的现象
分析:
如果数据和RDD有关系,那么可以从Driver端发送到Executor端执行
如果执行完毕,那么可以将结果拉回到Driver端
如果数据和RDD没有关系,因为计算中存在闭包操作,所以RDD会将数据发送到Executor
但是无法将数据从Executor端拉回到Driver端。
如果想要将和RDD无关的数据拉回到Driver端,RDD模型处理不了,需要采用新的数据模型——累加器
doubleAccumulator
collectionAccumulator 累加器
def main(args: Array[String]): Unit = {
// TODO Spark 环境
val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(conf)
// TODO 1. 创建累加器
val wordCountAcc = new MyAcculumator
// TODO 2. 将累加器注册到Spark中
sc.register(wordCountAcc, "WordCount")
val rdd = sc.makeRDD(
List(
"Hello",
"Hello",
"Hello",
"World",
"Hello",
),2
)
rdd.foreach(
word => {
// TODO 3. 使用累加器
wordCountAcc.add(word)
}
)
// TODO 4. 获取累加器的结果
println(wordCountAcc.value)
sc.stop()
}
// TODO 自定义数据累加器(WordCount)
// 1. 继承AccumulatorV2
// 2. 定义数据的泛型
// IN : String
// OUT : mutable.Map[String, Int]
// 3. 重写方法(3(计算) + 3(状态) = 6)
class MyAcculumator extends AccumulatorV2[String, mutable.Map[String, Int]]{
private val wordCountMap = mutable.Map[String, Int]()
// TODO 判断当前累加器是否为初始状态
override def isZero: Boolean = {
wordCountMap.isEmpty
}
// TODO 复制累加器
override def copy(): AccumulatorV2[String, mutable.Map[String, Int]] = {
new MyAcculumator()
}
// TODO 重置累加器
override def reset(): Unit = {
wordCountMap.clear()
}
// TODO 将外部的数据增加到累加器中
override def add(word: String): Unit = {
val oldCnt: Int = wordCountMap.getOrElse(word, 0)
wordCountMap.update(word, oldCnt + 1)
}
// TODO 将多个累加器进行合并
override def merge(other: AccumulatorV2[String, mutable.Map[String, Int]]): Unit = {
other.value.foreach {
case (word, cnt) => {
val oldCnt: Int = wordCountMap.getOrElse(word, 0)
wordCountMap.update(word, oldCnt + cnt)
}
}
}
// TODO 获取累加器的结果
override def value: mutable.Map[String, Int] = {
wordCountMap
}
}
十五:Join现象引出广播变量
def main(args: Array[String]): Unit = {
// TODO Spark 环境
val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(conf)
val rdd1 = sc.makeRDD(
List(
("a", 1),
("b", 2),
)
)
val rdd2 = sc.makeRDD(
List(
("a", 3),
("b", 4),
)
)
// ("a", 1)
// ( a,(1,3))
//rdd1.join(rdd2).collect().foreach(println)
// 计算的对象是以Task为单位进行数据发送的,所以可能会导致大量的数据冗余
// 所以如果计算对象占用比较大的资源,性能会急剧下降
// 为了提供数据访问效率,可以将数据发送给Executor,而不是发给Task。
// RDD数据模型只能给Task发。只能采用其他的数据模型:TODO 广播变量
val map = mutable.Map[String, Int](
("a", 3),
("b", 4)
)
// TODO 使用广播变量
val bc: Broadcast[mutable.Map[String, Int]] = sc.broadcast(map)
rdd1.map {
case (k, v1) => {
// TODO 获取广播变量的值
var v2 = bc.value.getOrElse(k, 0)
(k, (v1, v2))
}
}.collect.foreach(println)
sc.stop()
}
计算的对象是以Task为单位进行数据发送的,所以可能会导致大量的数据冗余 且 如果计算对象占用比较大的资源,性能会急剧下降
为了提供数据访问效率,可以将数据发送给Executor,而不是发给Task。
但是RDD数据模型只能给Task发。只能采用其他的数据模型: 广播变量(可以将数据发送给Executor 使得同一个节点上的Task可以共享一份资源,并且如果有Executor节点与Driver通信比较慢的时候 可以从就近的Executor上获得需要的资源)
十五:SparkSQL(Dataframe DataSet RDD 转换)
Spark + Hive => Shark
Spark SQL是Spark用于结构化数据(structured data)处理的Spark模块。
1.2 SparkSQL特点:
无缝的整合了 SQL 查询和 Spark 编程
SparkSQL可以简化RDD的开发,提高开发效率
15.1:Dataframe 与 DataSet 与 RDD概述
SQL语言本身没有类型
工厂模式生产的对象是有相同的功能的,因为从同一条生产线下来的。
Builder模式要生产某个对象需要经过复杂的过程,但是builder模式会把这些过程顺序确定下来。
Scala中的构造方法有两种,主构造方法和辅助构造方法,辅助构造方法的函数名一定为this。
15.2:Dataframe 与 DataSet 与 RDD
Dataframe : 是SparkSQL中的核心数据模型,是弱类型的模型,不关心数据的类型操作。
因为SQL的局限性,因此在处理数据的时候,Dataframe提供了一种DSL语法(面向对象的语法)
SparkSQL 与 RDD的关系与转换
SparkSQL模块等同于是对SparkCore一个封装,专门用于结构化数据处理的场景
Dataset : 就是使用面向对象的方式操作数据, 类,属性 => 结构 就是
在Dataframe的基础上,增加了类型信息
SparkSQL中存在大量的隐式转换操作,但是一般会导入使用 import时,使用的spark并不是包名,是环境对象的名称。
Dataframe 与 DataSet的区别?
Dataframe是特定类型的DataSet[Row]
说明DataSet兼容Dataframe()是将来用的比较多的
注意:如果从内存中获取数据,spark可以知道数据类型具体是什么。如果是数字,默认作为Int处理;但是从文件中读取的数字,不能确定是什么类型,所以用bigint接收,可以和Long类型转换,但是和Int不能进行转换
SparkSQL技术选型:
15.2:UDF 与 UDAF
package com.atguigu.bigdata.spark.sql01
import org.apache.spark.sql.expressions.Aggregator
import org.apache.spark.sql.{Dataframe, Encoder, Encoders, SparkSession, functions}
object SparkSQL09_Dataframe_UDAF {
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
df.createOrReplaceTempView("user")
// TODO UDAF使用时,需要采用特殊的方式实现自定义操作
// TODO 强类型操作不能应用于弱类型操作,如果想用,那么需要转换functions.udaf
// Spark3.0以后,可以通过转换,让强类型应用在弱类型操作
spark.udf.register("avgAge",functions.udaf(new AvgAgeUDF()))
// TODO : UDF
// 自定义的函数在SQL文使用
spark.sql("select avgAge(age) from user").show()
spark.stop()
}
case class CalcBuffer(var total : Long , var cnt : Long)
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 1. 继承org.apache.spark.sql.expressions.Aggregator
// 2. 定义泛型
// IN : Long
// BUFF : CalcBuffer
// OUT : Long
// 3. 重写方法 (4 + 2(固定) = 6)
class AvgAgeUDF extends Aggregator[Long,CalcBuffer,Long]{
// TODO 缓冲区的初始化
override def zero: CalcBuffer = {
CalcBuffer(0l,0l)
}
// TODO 将输入的数据和缓冲区的数据进行聚合操作
override def reduce(buff: CalcBuffer, a: Long): CalcBuffer ={
buff.total += a
buff.cnt += 1
buff
}
// TODO 多个缓冲区的合并操作
override def merge(b1: CalcBuffer, b2: CalcBuffer): CalcBuffer = {
b1.total += b2.total
b1.cnt += b2.cnt
b1
}
// TODO 计算数据
override def finish(reduction: CalcBuffer): Long = {
reduction.total / reduction.cnt
}
override def bufferEncoder: Encoder[CalcBuffer] = Encoders.product
override def outputEncoder: Encoder[Long] = Encoders.scalaLong
}
}
UDAF老版本:
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
df.createOrReplaceTempView("user")
// TODO UDAF使用时,需要采用特殊的方式实现自定义操作
// TODO 强类型操作不能应用于弱类型操作,如果想用,那么需要转换functions.udaf
// Spark3.0以后,可以通过转换,让强类型应用在弱类型操作
spark.udf.register("avgAge",new AvgAgeUDAF())
// TODO : UDF
// 自定义的函数在SQL文使用
spark.sql("select avgAge(age) from user").show()
spark.stop()
}
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 使用早期Spark版本的UDAF(弱类型)
// 1. 继承UserDefinedAggregateFunction
// 2. 重写方法(8)
class AvgAgeUDAF extends UserDefinedAggregateFunction{
// TODO 输入数据的结构
override def inputSchema: StructType = {
StructType(
Array(
StructField("age",LongType)
)
)
}
// TODO 缓冲区数据的结构
override def bufferSchema: StructType = {
StructType(
Array(
StructField("total",LongType),
StructField("cnt",LongType)
)
)
}
// TODO 输出数据的类型
override def dataType: DataType = LongType
// TODO 计算稳定性
override def deterministic: Boolean = true
// TODO 缓冲区的初始化
override def initialize(buffer: MutableAggregationBuffer): Unit = {
buffer.update(0,0l)
buffer.update(1,0l)
}
// TODO 将输入的数据和缓冲区的数据进行聚合操作
override def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
buffer.update(0,buffer.getLong(0) + input.getLong(0))
buffer.update(1,buffer.getLong(1)+1)
}
// TODO 多个缓冲区的合并操作
override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
buffer1.update(0,buffer1.getLong(0) + buffer2.getLong(0))
buffer1.update(1,buffer1.getLong(1) + buffer2.getLong(1))
}
override def evaluate(buffer: Row): Any = {
buffer.getLong(0) / buffer.getLong(1)
}
}
UDAF_Class老版本:
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
// TODO Spark3.0版本前,强类型的UDAF不能应用于弱类型的SQL中
import spark.implicits._
val udaf: AvgAgeUDAF = new AvgAgeUDAF
val ds: Dataset[User] = df.as[User]
ds.select(udaf.toColumn).show()
spark.stop()
}
case class User(id : Long , name : String , age : Long)
case class CalacBuffer(var total : Long , var cnt : Long)
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 使用早期Spark版本的UDAF(强类型)
// 1. 继承Aggregator
// 2. 定义泛型
// IN : User
// BUFF : CalcBuffer
// OUT : Long
// 3. 重写方法(6)
class AvgAgeUDAF extends Aggregator[User,CalacBuffer,Long]{
override def zero: CalacBuffer = {
CalacBuffer(0l,0L)
}
override def reduce(b: CalacBuffer, user: User): CalacBuffer = {
b.total += user.age
b.cnt += 1
b
}
override def merge(b1: CalacBuffer, b2: CalacBuffer): CalacBuffer = {
b1.total += b2.total
b1.cnt += b2.cnt
b1
}
override def finish(buffer: CalacBuffer): Long = {
buffer.total / buffer.cnt
}
override def bufferEncoder: Encoder[CalacBuffer] = Encoders.product
override def outputEncoder: Encoder[Long] = Encoders.scalaLong
}
15.3:通用读取方式和默认读取方式
user.json is not a Parquet file. expected magic number at tail [80, 65, 82, 49] but found [32, 53, 48, 125]
SparkSQL通用读取的方法,读取的文件为Parquet格式(列式存储格式)
val df = spark.read.load("data/users.parquet")
如果想要读取其他格式的文件也可以,需要额外增加参数进行解析
df.write.mode("append").format("json").save("output")
sparkSQL基于SparkCore
SparkCore读取数据是依赖于Hadoop
Hadoop读取数据是按行读取
SparkSQL读取JSON格式的文件,要求每一行数据必须符合JSON格式
读入数据:
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
//方式1:通用的load方法读取
spark.read.format("jdbc")
.option("url", "jdbc:mysql://linux1:3306/spark-sql")
.option("driver", "com.mysql.jdbc.Driver")
.option("user", "root")
.option("password", "123123")
.option("dbtable", "user")
.load().show
//方式2:通用的load方法读取 参数另一种形式
spark.read.format("jdbc")
.options(Map("url"->"jdbc:mysql://linux1:3306/spark-sql?user=root&password=123123",
"dbtable"->"user","driver"->"com.mysql.jdbc.Driver")).load().show
//方式3:使用jdbc方法读取
val props: Properties = new Properties()
props.setProperty("user", "root")
props.setProperty("password", "123123")
val df: Dataframe = spark.read.jdbc("jdbc:mysql://linux1:3306/spark-sql", "user", props)
df.show
//释放资源
spark.stop()
写入数据:
case class User2(name: String, age: Long)
。。。
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
val rdd: RDD[User2] = spark.sparkContext.makeRDD(List(User2("lisi", 20), User2("zs", 30)))
val ds: Dataset[User2] = rdd.toDS
//方式1:通用的方式 format指定写出类型
//方式2:通过jdbc方法
val props: Properties = new Properties()
props.setProperty("user", "root")
props.setProperty("password", "123123")
ds.write.mode(SaveMode.Append).jdbc("jdbc:mysql://linux1:3306/spark-sql", "user", props)
//释放资源
spark.stop()
十六:SparkStreaming(数据模型是离散流DStrem)
实时 离线 批量 流式
Spark是一个离线的批量数据处理的框架
SparkStreaming是一个准实时,微批次的处理框架
16.1:SparkStreaming知识点
Discretized Stream(离散流)是Spark Streaming的基础抽象,代表持续性的数据流和经过各种Spark原语操作后的结果数据流。在内部实现上,DStream是一系列连续的RDD来表示。每个RDD含有一段时间间隔内的数据。
16.2:DStream
blockingQueue队列:阻塞式队列,有反压机制,可以避免数据挤压。
采集数据的方式:DStream监控目录变化,将RDD放入到离散化流DStream中,自定义数据源。
object SparkStreaming05_DIY {
def main(args: Array[String]): Unit = {
//TODO SparkStreaming 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[*]")
.setAppName("Streaming")
.set("spark.testing.memory", "471859200")
//StreamingContext独享的第二个构造参数表示数据采集周期
// val ssc: StreamingContext = new StreamingContext(conf,new Duration(3 * 1000))
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
val uuidDS: ReceiverInputDStream[String] = ssc.receiverStream(new MyReceiver)
uuidDS.print()
//启动采集器
ssc.start()
// 等待采集器结束
ssc.awaitTermination()
}
class MyReceiver extends Receiver[String](StorageLevel.MEMORY_ONLY){
private var flg = true
override def onStart(): Unit = {
//采集数据
while (flg) {
//采集数据
val uuid: String = UUID.randomUUID().toString
//存储数据
store(uuid)
Thread.sleep(1000)
}
}
override def onStop(): Unit = {
//释放资源
flg = false
}
}
}
十七:DStream转换(有无状态转换)
离散化流无法完成排序操作 ,离散化流其实就是每个时间段RDD的封装 ,离散化流可以转换为RDD实现操作
transform用于将离散化流实现不了的功能进行调用,transform方法是DStream的方法,称之为原语,RDD的方法称之为算子 如果想要在Driver端周期性执行一些逻辑的场合,需要使用transform。
将两个窗口key相同的连接起来
使用updateStateByKey需要对检查点目录进行配置,会使用检查点来保存状态。
reduceByKey原语不会保留中间计算结果,一旦采集周期结束,数据丢弃 ,如果想要保留中间计算结果,需要有状态操作
从检查点中恢复数据
def main(args: Array[String]): Unit = {
//从检查点中恢复数据
val outerSSC = StreamingContext.getOrCreate("cp",() => {
//TODO SparkStreaming 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[*]")
.setAppName("Streaming")
.set("spark.testing.memory", "471859200")
//StreamingContext独享的第二个构造参数表示数据采集周期
// val ssc: StreamingContext = new StreamingContext(conf,new Duration(3 * 1000))
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
ssc.checkpoint("cp")
val socketDS: ReceiverInputDStream[String] = ssc.socketTextStream("localhost", 8888)
val wordDS: DStream[String] = socketDS.flatMap(_.split(" "))
val wordToOneDS: DStream[(String, Int)] = wordDS.map((_, 1))
wordToOneDS.updateStateByKey(
(seq : Seq[Int] , buffer : Option[Int]) => {
Option(seq.sum + buffer.getOrElse(0))
}
).print()
ssc
})
//SparkStreaming (reduceByKey) + Redis内存数据库 = 有状态
outerSSC.start()
outerSSC.awaitTermination()
}
十八:DStream窗口
窗口划过的规律是:数据由无到有再到无(先多后少后变没)
DStream离散流有window方法:wordToOneDS.window(Seconds(9),Seconds(3))可以设置窗口的大小。
window操作中,窗口范围和滑动的幅度应该为采集周期的整数倍
如果窗口范围和滑动的幅度相同, 那么就是滚动窗口,没有重复数据 默认情况下,不使用窗口,也会有窗口操作,窗口范围取值为1个采集周期,滑动幅度也是一个采集周期。
如果窗口范围和滑动的幅度不相同, 那么就是滑动窗口
1>窗口范围大于滑动的幅度, 存在重复数据
2>窗口范围小于滑动的幅度, 存在丢失数据的可能
二十:优雅的关闭
对于流式数据,线程的启动和关闭应该是分开的,属于两个线程。
优雅的关闭,不要强制关闭,关闭前,不再接受数据,而是把手头上的数据处理完成之后再关闭。
二十一:DStream输出
如果想要将DStream进行特殊处理,那么可以转换为底层RDD实现
需求 数据集中相同的Key的分区内取最大值,分区间求和
reduceByKey groupByKey只考虑key不考虑分区而reduceByKey 在逻辑上,
分区内和分区间是一样的都是聚合 因此reduceByKey groupByKey都不能处理该需求。
需求 如果分区内和分区间的计算规则不一定相同时,Spark提供了一个算子来实现。aggregateByKey
aggregateByKey算子存在函数柯里化,有多个参数列表
第一个参数列表中有一个参数
第一个参数表示计算初始值
第二个参数列表中有二个参数
第一个参数表示 : 分区内计算规则
第二个参数表示 : 分区间计算规则
分区内和分区间是一样的都是聚合 因此reduceByKey groupByKey都不能处理该需求。
需求 如果分区内和分区间的计算规则不一定相同时,Spark提供了一个算子来实现。aggregateByKey
aggregateByKey算子存在函数柯里化,有多个参数列表
第一个参数列表中有一个参数
第一个参数表示计算初始值
第二个参数列表中有二个参数
第一个参数表示 : 分区内计算规则
第二个参数表示 : 分区间计算规则
aggregateByKey算子存在函数柯里化,有多个参数列表
第一个参数列表中有一个参数
第一个参数表示计算初始值
第二个参数列表中有二个参数
第一个参数表示 : 分区内计算规则
第二个参数表示 : 分区间计算规则
reduceByKey,aggregateByKey ,foldByKey, combineByKey本质区别
9.5:sortByKey
9.6:Join leftOuterJoin rightOuterJoin fullOuterJoin
9.7 : cogroup
join算子体现的是数据的关系
join算子将两个数据集中相同的key的数据,连接在一起
zip是将两个数据集位置相同的数据,连接起来与key无关
join 可能存在笛卡尔积 也 存在shuffle()
笛卡尔积 在落盘的时候 如果存在大量相同的key的时候数据量会激增
如果能够使用其它算子实现的功能,那么不推荐使用join
rdd1.join(rdd2).collect().foreach(println)
rdd1.leftOuterJoin(rdd2).collect().foreach(println) rdd1.rightOuterJoin(rdd2).collect().foreach(println) rdd1.fullOuterJoin(rdd2).collect().foreach(println)
cogroup = connect(多个数据集) + group(单一数据集)
9.6:案例实操
方式一:
// 统计出每一个省份每个广告被点击数量排行的Top3
val fileDatas: RDD[String] = sc.textFile("data/agent.log")
val groupDatas: RDD[(String, Iterable[String])] = fileDatas.groupBy(
line => {
val datas: Array[String] = line.split(" ")
datas(1)
}
)
val top3: RDD[(String, List[(String, Int)])] = groupDatas.mapValues(
//数据集是List(line, line, line)
list => {
//数据集是line
val adToCount: Map[String, Int] = list.map(
line => {
val datas: Array[String] = line.split(" ")
(datas(4), 1)
}
//数据集是List((省份,广告)) 按照省份进行分组统计
).groupBy(_._1).mapValues(_.size)
//Map[String, Int] [广告,点击次数]
// 在 groupDatas.mapValues( 的架子中都是对value进行处理的
//元组天生的排序 数据类型[广告,点击次数] 前三条
adToCount.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
}
)
top3.collect().foreach(println)
sc.stop()
}
方式二:
val fileDatas: RDD[String] = sc.textFile("data/agent.log")
// 1. 将广告进行统计分析 (word, cnt)
// ((省份1,广告1), 1),((省份2,广告2), 1)((省份1,广告1), 1)
// ((省份1,广告1), sum),((省份1,广告2), sum)
val reduceDatas: RDD[((String, String), Int)] = fileDatas.map(
line => {
val datas: Array[String] = line.split(" ")
((datas(1), datas(4)), 1)
}
).reduceByKey(_ + _)
// 1.5 将统计结果进行格式转换
// (省份1,(广告1, sum)),(省份1,(广告2, sum))
val mapDatas: RDD[(String, (String, Int))] = reduceDatas.map {
case ((prv, ad), sum) => {
(prv, (ad, sum))
}
}
// 2. 将统计结果按照省份进行分组
// (省份1, (广告1, sum),(广告2, sum))
val prvGroupDatas: RDD[(String, Iterable[(String, Int)])] = mapDatas.groupByKey()
// 3. 将分组后的数据按照点击数量进行排序(降序),取前3名
val top3: RDD[(String, List[(String, Int)])] = prvGroupDatas.mapValues(
iter => {
iter.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
}
)
top3.collect().foreach(println)
十:action行动算子(是对结果进行处理的)
TODO : 所谓的行动(Action)算子,其实就是RDD用于触发作业执行的方法,类似于IO中的read方法
转换算子的返回值为RDD 行动算子的返回值为具体的结果
行动算子和作业的关系 :1 对 1
10.2:reduce
reduce算子可以触发作业的执行,分区内先两两计算,分区间再两两计算
分区内计算都是在Executor。
分区间计算都是在Driver
10.3:count first take takeOrdered
10.4: aggregate fold
10.5: countByKey countByValue
10.5: save相关的算子
rdd.saveAsTextFile("output1")
rdd.saveAsObjectFile("output2")
rdd.saveAsSequenceFile("output3")
10.5: collect foreach
10.6: 序列化闭包检测
一个函数使用了外部的数据,并且改变了这个数据的生命周期 将数据包含到函数内部,形成了一个闭合的环境,这个环境称之为闭包环境,简称闭包
Spark在运行作业之前,会判断程序逻辑中是否包含闭包
如果存在闭包操作,那么需要检测闭包中使用的数据是否能够序列化,如果不能,那么发生错误
这个操作称之为闭包检测
十一:RDD阶段的划分
前一个阶段不执行完,后面的阶段是无法执行的。
十二:依赖关系和血缘关系(宽依赖窄依赖)
十三:持久化
(持久化只针对于当前应用程序,资源使用完之后释放资源,不会给其它节点使用。因此诞生了检查点)
将计算结果缓存起来。
数据的持久化默认将数据保存到内存中,重复使用
wordToOne.cache()
持久化操作也可以保存到磁盘中
wordToOne.persist()
保存级别设置
十三:RDD CheckPoint检查点
java中的finalize方法是Object类中提供的一个方法,在GC准备释放对象所占用的内存空间之前,它将首先调用finalize()方法。
持久化只针对于当前应用程序,资源使用完之后释放资源,不会给其它节点使用。
持久化只针对于当前应用程序
跨应用程序的数据共享需要采用 检查点机制
设置的方式
① :sc.setCheckpointDir("cp") 该检查点的目录一般为分布式目录以便各个节点使用
②:wordToOne.checkpoint()
持久化操作会在血缘关系中增加依赖
检查点一般会将数据保存到分布式文件系统中(HDFS)(新的数据源)
检查点会切断血缘即减少血缘依赖,因为可以把HDFS中相对可靠的数据作为新的数据源。
十四:自定义分区 RDD文件读取与保存
Hash分区:对于给定的key,计算其hashCode,并除以分区个数取余
Range分区:将一定范围内的数据映射到一个分区中,尽量保证每个分区数据均匀,而且分区间有序
十四:累加器(数据模型)(功能比较单一没有shuffle性能相对高)
累加器的现象
分析:
如果数据和RDD有关系,那么可以从Driver端发送到Executor端执行
如果执行完毕,那么可以将结果拉回到Driver端
如果数据和RDD没有关系,因为计算中存在闭包操作,所以RDD会将数据发送到Executor
但是无法将数据从Executor端拉回到Driver端。
如果想要将和RDD无关的数据拉回到Driver端,RDD模型处理不了,需要采用新的数据模型——累加器
doubleAccumulator
collectionAccumulator 累加器
def main(args: Array[String]): Unit = {
// TODO Spark 环境
val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(conf)
// TODO 1. 创建累加器
val wordCountAcc = new MyAcculumator
// TODO 2. 将累加器注册到Spark中
sc.register(wordCountAcc, "WordCount")
val rdd = sc.makeRDD(
List(
"Hello",
"Hello",
"Hello",
"World",
"Hello",
),2
)
rdd.foreach(
word => {
// TODO 3. 使用累加器
wordCountAcc.add(word)
}
)
// TODO 4. 获取累加器的结果
println(wordCountAcc.value)
sc.stop()
}
// TODO 自定义数据累加器(WordCount)
// 1. 继承AccumulatorV2
// 2. 定义数据的泛型
// IN : String
// OUT : mutable.Map[String, Int]
// 3. 重写方法(3(计算) + 3(状态) = 6)
class MyAcculumator extends AccumulatorV2[String, mutable.Map[String, Int]]{
private val wordCountMap = mutable.Map[String, Int]()
// TODO 判断当前累加器是否为初始状态
override def isZero: Boolean = {
wordCountMap.isEmpty
}
// TODO 复制累加器
override def copy(): AccumulatorV2[String, mutable.Map[String, Int]] = {
new MyAcculumator()
}
// TODO 重置累加器
override def reset(): Unit = {
wordCountMap.clear()
}
// TODO 将外部的数据增加到累加器中
override def add(word: String): Unit = {
val oldCnt: Int = wordCountMap.getOrElse(word, 0)
wordCountMap.update(word, oldCnt + 1)
}
// TODO 将多个累加器进行合并
override def merge(other: AccumulatorV2[String, mutable.Map[String, Int]]): Unit = {
other.value.foreach {
case (word, cnt) => {
val oldCnt: Int = wordCountMap.getOrElse(word, 0)
wordCountMap.update(word, oldCnt + cnt)
}
}
}
// TODO 获取累加器的结果
override def value: mutable.Map[String, Int] = {
wordCountMap
}
}
十五:Join现象引出广播变量
def main(args: Array[String]): Unit = {
// TODO Spark 环境
val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(conf)
val rdd1 = sc.makeRDD(
List(
("a", 1),
("b", 2),
)
)
val rdd2 = sc.makeRDD(
List(
("a", 3),
("b", 4),
)
)
// ("a", 1)
// ( a,(1,3))
//rdd1.join(rdd2).collect().foreach(println)
// 计算的对象是以Task为单位进行数据发送的,所以可能会导致大量的数据冗余
// 所以如果计算对象占用比较大的资源,性能会急剧下降
// 为了提供数据访问效率,可以将数据发送给Executor,而不是发给Task。
// RDD数据模型只能给Task发。只能采用其他的数据模型:TODO 广播变量
val map = mutable.Map[String, Int](
("a", 3),
("b", 4)
)
// TODO 使用广播变量
val bc: Broadcast[mutable.Map[String, Int]] = sc.broadcast(map)
rdd1.map {
case (k, v1) => {
// TODO 获取广播变量的值
var v2 = bc.value.getOrElse(k, 0)
(k, (v1, v2))
}
}.collect.foreach(println)
sc.stop()
}
计算的对象是以Task为单位进行数据发送的,所以可能会导致大量的数据冗余 且 如果计算对象占用比较大的资源,性能会急剧下降
为了提供数据访问效率,可以将数据发送给Executor,而不是发给Task。
但是RDD数据模型只能给Task发。只能采用其他的数据模型: 广播变量(可以将数据发送给Executor 使得同一个节点上的Task可以共享一份资源,并且如果有Executor节点与Driver通信比较慢的时候 可以从就近的Executor上获得需要的资源)
十五:SparkSQL(Dataframe DataSet RDD 转换)
Spark + Hive => Shark
Spark SQL是Spark用于结构化数据(structured data)处理的Spark模块。
1.2 SparkSQL特点:
无缝的整合了 SQL 查询和 Spark 编程
SparkSQL可以简化RDD的开发,提高开发效率
15.1:Dataframe 与 DataSet 与 RDD概述
SQL语言本身没有类型
工厂模式生产的对象是有相同的功能的,因为从同一条生产线下来的。
Builder模式要生产某个对象需要经过复杂的过程,但是builder模式会把这些过程顺序确定下来。
Scala中的构造方法有两种,主构造方法和辅助构造方法,辅助构造方法的函数名一定为this。
15.2:Dataframe 与 DataSet 与 RDD
Dataframe : 是SparkSQL中的核心数据模型,是弱类型的模型,不关心数据的类型操作。
因为SQL的局限性,因此在处理数据的时候,Dataframe提供了一种DSL语法(面向对象的语法)
SparkSQL 与 RDD的关系与转换
SparkSQL模块等同于是对SparkCore一个封装,专门用于结构化数据处理的场景
Dataset : 就是使用面向对象的方式操作数据, 类,属性 => 结构 就是
在Dataframe的基础上,增加了类型信息
SparkSQL中存在大量的隐式转换操作,但是一般会导入使用 import时,使用的spark并不是包名,是环境对象的名称。
Dataframe 与 DataSet的区别?
Dataframe是特定类型的DataSet[Row]
说明DataSet兼容Dataframe()是将来用的比较多的
注意:如果从内存中获取数据,spark可以知道数据类型具体是什么。如果是数字,默认作为Int处理;但是从文件中读取的数字,不能确定是什么类型,所以用bigint接收,可以和Long类型转换,但是和Int不能进行转换
SparkSQL技术选型:
15.2:UDF 与 UDAF
package com.atguigu.bigdata.spark.sql01
import org.apache.spark.sql.expressions.Aggregator
import org.apache.spark.sql.{Dataframe, Encoder, Encoders, SparkSession, functions}
object SparkSQL09_Dataframe_UDAF {
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
df.createOrReplaceTempView("user")
// TODO UDAF使用时,需要采用特殊的方式实现自定义操作
// TODO 强类型操作不能应用于弱类型操作,如果想用,那么需要转换functions.udaf
// Spark3.0以后,可以通过转换,让强类型应用在弱类型操作
spark.udf.register("avgAge",functions.udaf(new AvgAgeUDF()))
// TODO : UDF
// 自定义的函数在SQL文使用
spark.sql("select avgAge(age) from user").show()
spark.stop()
}
case class CalcBuffer(var total : Long , var cnt : Long)
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 1. 继承org.apache.spark.sql.expressions.Aggregator
// 2. 定义泛型
// IN : Long
// BUFF : CalcBuffer
// OUT : Long
// 3. 重写方法 (4 + 2(固定) = 6)
class AvgAgeUDF extends Aggregator[Long,CalcBuffer,Long]{
// TODO 缓冲区的初始化
override def zero: CalcBuffer = {
CalcBuffer(0l,0l)
}
// TODO 将输入的数据和缓冲区的数据进行聚合操作
override def reduce(buff: CalcBuffer, a: Long): CalcBuffer ={
buff.total += a
buff.cnt += 1
buff
}
// TODO 多个缓冲区的合并操作
override def merge(b1: CalcBuffer, b2: CalcBuffer): CalcBuffer = {
b1.total += b2.total
b1.cnt += b2.cnt
b1
}
// TODO 计算数据
override def finish(reduction: CalcBuffer): Long = {
reduction.total / reduction.cnt
}
override def bufferEncoder: Encoder[CalcBuffer] = Encoders.product
override def outputEncoder: Encoder[Long] = Encoders.scalaLong
}
}
UDAF老版本:
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
df.createOrReplaceTempView("user")
// TODO UDAF使用时,需要采用特殊的方式实现自定义操作
// TODO 强类型操作不能应用于弱类型操作,如果想用,那么需要转换functions.udaf
// Spark3.0以后,可以通过转换,让强类型应用在弱类型操作
spark.udf.register("avgAge",new AvgAgeUDAF())
// TODO : UDF
// 自定义的函数在SQL文使用
spark.sql("select avgAge(age) from user").show()
spark.stop()
}
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 使用早期Spark版本的UDAF(弱类型)
// 1. 继承UserDefinedAggregateFunction
// 2. 重写方法(8)
class AvgAgeUDAF extends UserDefinedAggregateFunction{
// TODO 输入数据的结构
override def inputSchema: StructType = {
StructType(
Array(
StructField("age",LongType)
)
)
}
// TODO 缓冲区数据的结构
override def bufferSchema: StructType = {
StructType(
Array(
StructField("total",LongType),
StructField("cnt",LongType)
)
)
}
// TODO 输出数据的类型
override def dataType: DataType = LongType
// TODO 计算稳定性
override def deterministic: Boolean = true
// TODO 缓冲区的初始化
override def initialize(buffer: MutableAggregationBuffer): Unit = {
buffer.update(0,0l)
buffer.update(1,0l)
}
// TODO 将输入的数据和缓冲区的数据进行聚合操作
override def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
buffer.update(0,buffer.getLong(0) + input.getLong(0))
buffer.update(1,buffer.getLong(1)+1)
}
// TODO 多个缓冲区的合并操作
override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
buffer1.update(0,buffer1.getLong(0) + buffer2.getLong(0))
buffer1.update(1,buffer1.getLong(1) + buffer2.getLong(1))
}
override def evaluate(buffer: Row): Any = {
buffer.getLong(0) / buffer.getLong(1)
}
}
UDAF_Class老版本:
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
// TODO Spark3.0版本前,强类型的UDAF不能应用于弱类型的SQL中
import spark.implicits._
val udaf: AvgAgeUDAF = new AvgAgeUDAF
val ds: Dataset[User] = df.as[User]
ds.select(udaf.toColumn).show()
spark.stop()
}
case class User(id : Long , name : String , age : Long)
case class CalacBuffer(var total : Long , var cnt : Long)
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 使用早期Spark版本的UDAF(强类型)
// 1. 继承Aggregator
// 2. 定义泛型
// IN : User
// BUFF : CalcBuffer
// OUT : Long
// 3. 重写方法(6)
class AvgAgeUDAF extends Aggregator[User,CalacBuffer,Long]{
override def zero: CalacBuffer = {
CalacBuffer(0l,0L)
}
override def reduce(b: CalacBuffer, user: User): CalacBuffer = {
b.total += user.age
b.cnt += 1
b
}
override def merge(b1: CalacBuffer, b2: CalacBuffer): CalacBuffer = {
b1.total += b2.total
b1.cnt += b2.cnt
b1
}
override def finish(buffer: CalacBuffer): Long = {
buffer.total / buffer.cnt
}
override def bufferEncoder: Encoder[CalacBuffer] = Encoders.product
override def outputEncoder: Encoder[Long] = Encoders.scalaLong
}
15.3:通用读取方式和默认读取方式
user.json is not a Parquet file. expected magic number at tail [80, 65, 82, 49] but found [32, 53, 48, 125]
SparkSQL通用读取的方法,读取的文件为Parquet格式(列式存储格式)
val df = spark.read.load("data/users.parquet")
如果想要读取其他格式的文件也可以,需要额外增加参数进行解析
df.write.mode("append").format("json").save("output")
sparkSQL基于SparkCore
SparkCore读取数据是依赖于Hadoop
Hadoop读取数据是按行读取
SparkSQL读取JSON格式的文件,要求每一行数据必须符合JSON格式
读入数据:
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
//方式1:通用的load方法读取
spark.read.format("jdbc")
.option("url", "jdbc:mysql://linux1:3306/spark-sql")
.option("driver", "com.mysql.jdbc.Driver")
.option("user", "root")
.option("password", "123123")
.option("dbtable", "user")
.load().show
//方式2:通用的load方法读取 参数另一种形式
spark.read.format("jdbc")
.options(Map("url"->"jdbc:mysql://linux1:3306/spark-sql?user=root&password=123123",
"dbtable"->"user","driver"->"com.mysql.jdbc.Driver")).load().show
//方式3:使用jdbc方法读取
val props: Properties = new Properties()
props.setProperty("user", "root")
props.setProperty("password", "123123")
val df: Dataframe = spark.read.jdbc("jdbc:mysql://linux1:3306/spark-sql", "user", props)
df.show
//释放资源
spark.stop()
写入数据:
case class User2(name: String, age: Long)
。。。
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
val rdd: RDD[User2] = spark.sparkContext.makeRDD(List(User2("lisi", 20), User2("zs", 30)))
val ds: Dataset[User2] = rdd.toDS
//方式1:通用的方式 format指定写出类型
//方式2:通过jdbc方法
val props: Properties = new Properties()
props.setProperty("user", "root")
props.setProperty("password", "123123")
ds.write.mode(SaveMode.Append).jdbc("jdbc:mysql://linux1:3306/spark-sql", "user", props)
//释放资源
spark.stop()
十六:SparkStreaming(数据模型是离散流DStrem)
实时 离线 批量 流式
Spark是一个离线的批量数据处理的框架
SparkStreaming是一个准实时,微批次的处理框架
16.1:SparkStreaming知识点
Discretized Stream(离散流)是Spark Streaming的基础抽象,代表持续性的数据流和经过各种Spark原语操作后的结果数据流。在内部实现上,DStream是一系列连续的RDD来表示。每个RDD含有一段时间间隔内的数据。
16.2:DStream
blockingQueue队列:阻塞式队列,有反压机制,可以避免数据挤压。
采集数据的方式:DStream监控目录变化,将RDD放入到离散化流DStream中,自定义数据源。
object SparkStreaming05_DIY {
def main(args: Array[String]): Unit = {
//TODO SparkStreaming 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[*]")
.setAppName("Streaming")
.set("spark.testing.memory", "471859200")
//StreamingContext独享的第二个构造参数表示数据采集周期
// val ssc: StreamingContext = new StreamingContext(conf,new Duration(3 * 1000))
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
val uuidDS: ReceiverInputDStream[String] = ssc.receiverStream(new MyReceiver)
uuidDS.print()
//启动采集器
ssc.start()
// 等待采集器结束
ssc.awaitTermination()
}
class MyReceiver extends Receiver[String](StorageLevel.MEMORY_ONLY){
private var flg = true
override def onStart(): Unit = {
//采集数据
while (flg) {
//采集数据
val uuid: String = UUID.randomUUID().toString
//存储数据
store(uuid)
Thread.sleep(1000)
}
}
override def onStop(): Unit = {
//释放资源
flg = false
}
}
}
十七:DStream转换(有无状态转换)
离散化流无法完成排序操作 ,离散化流其实就是每个时间段RDD的封装 ,离散化流可以转换为RDD实现操作
transform用于将离散化流实现不了的功能进行调用,transform方法是DStream的方法,称之为原语,RDD的方法称之为算子 如果想要在Driver端周期性执行一些逻辑的场合,需要使用transform。
将两个窗口key相同的连接起来
使用updateStateByKey需要对检查点目录进行配置,会使用检查点来保存状态。
reduceByKey原语不会保留中间计算结果,一旦采集周期结束,数据丢弃 ,如果想要保留中间计算结果,需要有状态操作
从检查点中恢复数据
def main(args: Array[String]): Unit = {
//从检查点中恢复数据
val outerSSC = StreamingContext.getOrCreate("cp",() => {
//TODO SparkStreaming 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[*]")
.setAppName("Streaming")
.set("spark.testing.memory", "471859200")
//StreamingContext独享的第二个构造参数表示数据采集周期
// val ssc: StreamingContext = new StreamingContext(conf,new Duration(3 * 1000))
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
ssc.checkpoint("cp")
val socketDS: ReceiverInputDStream[String] = ssc.socketTextStream("localhost", 8888)
val wordDS: DStream[String] = socketDS.flatMap(_.split(" "))
val wordToOneDS: DStream[(String, Int)] = wordDS.map((_, 1))
wordToOneDS.updateStateByKey(
(seq : Seq[Int] , buffer : Option[Int]) => {
Option(seq.sum + buffer.getOrElse(0))
}
).print()
ssc
})
//SparkStreaming (reduceByKey) + Redis内存数据库 = 有状态
outerSSC.start()
outerSSC.awaitTermination()
}
十八:DStream窗口
窗口划过的规律是:数据由无到有再到无(先多后少后变没)
DStream离散流有window方法:wordToOneDS.window(Seconds(9),Seconds(3))可以设置窗口的大小。
window操作中,窗口范围和滑动的幅度应该为采集周期的整数倍
如果窗口范围和滑动的幅度相同, 那么就是滚动窗口,没有重复数据 默认情况下,不使用窗口,也会有窗口操作,窗口范围取值为1个采集周期,滑动幅度也是一个采集周期。
如果窗口范围和滑动的幅度不相同, 那么就是滑动窗口
1>窗口范围大于滑动的幅度, 存在重复数据
2>窗口范围小于滑动的幅度, 存在丢失数据的可能
二十:优雅的关闭
对于流式数据,线程的启动和关闭应该是分开的,属于两个线程。
优雅的关闭,不要强制关闭,关闭前,不再接受数据,而是把手头上的数据处理完成之后再关闭。
二十一:DStream输出
如果想要将DStream进行特殊处理,那么可以转换为底层RDD实现
9.7 : cogroup
join算子体现的是数据的关系
join算子将两个数据集中相同的key的数据,连接在一起
zip是将两个数据集位置相同的数据,连接起来与key无关
join 可能存在笛卡尔积 也 存在shuffle()
笛卡尔积 在落盘的时候 如果存在大量相同的key的时候数据量会激增
如果能够使用其它算子实现的功能,那么不推荐使用join
rdd1.join(rdd2).collect().foreach(println)
rdd1.leftOuterJoin(rdd2).collect().foreach(println) rdd1.rightOuterJoin(rdd2).collect().foreach(println) rdd1.fullOuterJoin(rdd2).collect().foreach(println)
cogroup = connect(多个数据集) + group(单一数据集)
9.6:案例实操
方式一:
// 统计出每一个省份每个广告被点击数量排行的Top3
val fileDatas: RDD[String] = sc.textFile("data/agent.log")
val groupDatas: RDD[(String, Iterable[String])] = fileDatas.groupBy(
line => {
val datas: Array[String] = line.split(" ")
datas(1)
}
)
val top3: RDD[(String, List[(String, Int)])] = groupDatas.mapValues(
//数据集是List(line, line, line)
list => {
//数据集是line
val adToCount: Map[String, Int] = list.map(
line => {
val datas: Array[String] = line.split(" ")
(datas(4), 1)
}
//数据集是List((省份,广告)) 按照省份进行分组统计
).groupBy(_._1).mapValues(_.size)
//Map[String, Int] [广告,点击次数]
// 在 groupDatas.mapValues( 的架子中都是对value进行处理的
//元组天生的排序 数据类型[广告,点击次数] 前三条
adToCount.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
}
)
top3.collect().foreach(println)
sc.stop()
}
方式二:
val fileDatas: RDD[String] = sc.textFile("data/agent.log")
// 1. 将广告进行统计分析 (word, cnt)
// ((省份1,广告1), 1),((省份2,广告2), 1)((省份1,广告1), 1)
// ((省份1,广告1), sum),((省份1,广告2), sum)
val reduceDatas: RDD[((String, String), Int)] = fileDatas.map(
line => {
val datas: Array[String] = line.split(" ")
((datas(1), datas(4)), 1)
}
).reduceByKey(_ + _)
// 1.5 将统计结果进行格式转换
// (省份1,(广告1, sum)),(省份1,(广告2, sum))
val mapDatas: RDD[(String, (String, Int))] = reduceDatas.map {
case ((prv, ad), sum) => {
(prv, (ad, sum))
}
}
// 2. 将统计结果按照省份进行分组
// (省份1, (广告1, sum),(广告2, sum))
val prvGroupDatas: RDD[(String, Iterable[(String, Int)])] = mapDatas.groupByKey()
// 3. 将分组后的数据按照点击数量进行排序(降序),取前3名
val top3: RDD[(String, List[(String, Int)])] = prvGroupDatas.mapValues(
iter => {
iter.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
}
)
top3.collect().foreach(println)
十:action行动算子(是对结果进行处理的)
TODO : 所谓的行动(Action)算子,其实就是RDD用于触发作业执行的方法,类似于IO中的read方法
转换算子的返回值为RDD 行动算子的返回值为具体的结果
行动算子和作业的关系 :1 对 1
10.2:reduce
reduce算子可以触发作业的执行,分区内先两两计算,分区间再两两计算
分区内计算都是在Executor。
分区间计算都是在Driver
10.3:count first take takeOrdered
10.4: aggregate fold
10.5: countByKey countByValue
10.5: save相关的算子
rdd.saveAsTextFile("output1")
rdd.saveAsObjectFile("output2")
rdd.saveAsSequenceFile("output3")
10.5: collect foreach
10.6: 序列化闭包检测
一个函数使用了外部的数据,并且改变了这个数据的生命周期 将数据包含到函数内部,形成了一个闭合的环境,这个环境称之为闭包环境,简称闭包
Spark在运行作业之前,会判断程序逻辑中是否包含闭包
如果存在闭包操作,那么需要检测闭包中使用的数据是否能够序列化,如果不能,那么发生错误
这个操作称之为闭包检测
十一:RDD阶段的划分
前一个阶段不执行完,后面的阶段是无法执行的。
十二:依赖关系和血缘关系(宽依赖窄依赖)
十三:持久化
(持久化只针对于当前应用程序,资源使用完之后释放资源,不会给其它节点使用。因此诞生了检查点)
将计算结果缓存起来。
数据的持久化默认将数据保存到内存中,重复使用
wordToOne.cache()
持久化操作也可以保存到磁盘中
wordToOne.persist()
保存级别设置
十三:RDD CheckPoint检查点
java中的finalize方法是Object类中提供的一个方法,在GC准备释放对象所占用的内存空间之前,它将首先调用finalize()方法。
持久化只针对于当前应用程序,资源使用完之后释放资源,不会给其它节点使用。
持久化只针对于当前应用程序
跨应用程序的数据共享需要采用 检查点机制
设置的方式
① :sc.setCheckpointDir("cp") 该检查点的目录一般为分布式目录以便各个节点使用
②:wordToOne.checkpoint()
持久化操作会在血缘关系中增加依赖
检查点一般会将数据保存到分布式文件系统中(HDFS)(新的数据源)
检查点会切断血缘即减少血缘依赖,因为可以把HDFS中相对可靠的数据作为新的数据源。
十四:自定义分区 RDD文件读取与保存
Hash分区:对于给定的key,计算其hashCode,并除以分区个数取余
Range分区:将一定范围内的数据映射到一个分区中,尽量保证每个分区数据均匀,而且分区间有序
十四:累加器(数据模型)(功能比较单一没有shuffle性能相对高)
累加器的现象
分析:
如果数据和RDD有关系,那么可以从Driver端发送到Executor端执行
如果执行完毕,那么可以将结果拉回到Driver端
如果数据和RDD没有关系,因为计算中存在闭包操作,所以RDD会将数据发送到Executor
但是无法将数据从Executor端拉回到Driver端。
如果想要将和RDD无关的数据拉回到Driver端,RDD模型处理不了,需要采用新的数据模型——累加器
doubleAccumulator
collectionAccumulator 累加器
def main(args: Array[String]): Unit = {
// TODO Spark 环境
val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(conf)
// TODO 1. 创建累加器
val wordCountAcc = new MyAcculumator
// TODO 2. 将累加器注册到Spark中
sc.register(wordCountAcc, "WordCount")
val rdd = sc.makeRDD(
List(
"Hello",
"Hello",
"Hello",
"World",
"Hello",
),2
)
rdd.foreach(
word => {
// TODO 3. 使用累加器
wordCountAcc.add(word)
}
)
// TODO 4. 获取累加器的结果
println(wordCountAcc.value)
sc.stop()
}
// TODO 自定义数据累加器(WordCount)
// 1. 继承AccumulatorV2
// 2. 定义数据的泛型
// IN : String
// OUT : mutable.Map[String, Int]
// 3. 重写方法(3(计算) + 3(状态) = 6)
class MyAcculumator extends AccumulatorV2[String, mutable.Map[String, Int]]{
private val wordCountMap = mutable.Map[String, Int]()
// TODO 判断当前累加器是否为初始状态
override def isZero: Boolean = {
wordCountMap.isEmpty
}
// TODO 复制累加器
override def copy(): AccumulatorV2[String, mutable.Map[String, Int]] = {
new MyAcculumator()
}
// TODO 重置累加器
override def reset(): Unit = {
wordCountMap.clear()
}
// TODO 将外部的数据增加到累加器中
override def add(word: String): Unit = {
val oldCnt: Int = wordCountMap.getOrElse(word, 0)
wordCountMap.update(word, oldCnt + 1)
}
// TODO 将多个累加器进行合并
override def merge(other: AccumulatorV2[String, mutable.Map[String, Int]]): Unit = {
other.value.foreach {
case (word, cnt) => {
val oldCnt: Int = wordCountMap.getOrElse(word, 0)
wordCountMap.update(word, oldCnt + cnt)
}
}
}
// TODO 获取累加器的结果
override def value: mutable.Map[String, Int] = {
wordCountMap
}
}
十五:Join现象引出广播变量
def main(args: Array[String]): Unit = {
// TODO Spark 环境
val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(conf)
val rdd1 = sc.makeRDD(
List(
("a", 1),
("b", 2),
)
)
val rdd2 = sc.makeRDD(
List(
("a", 3),
("b", 4),
)
)
// ("a", 1)
// ( a,(1,3))
//rdd1.join(rdd2).collect().foreach(println)
// 计算的对象是以Task为单位进行数据发送的,所以可能会导致大量的数据冗余
// 所以如果计算对象占用比较大的资源,性能会急剧下降
// 为了提供数据访问效率,可以将数据发送给Executor,而不是发给Task。
// RDD数据模型只能给Task发。只能采用其他的数据模型:TODO 广播变量
val map = mutable.Map[String, Int](
("a", 3),
("b", 4)
)
// TODO 使用广播变量
val bc: Broadcast[mutable.Map[String, Int]] = sc.broadcast(map)
rdd1.map {
case (k, v1) => {
// TODO 获取广播变量的值
var v2 = bc.value.getOrElse(k, 0)
(k, (v1, v2))
}
}.collect.foreach(println)
sc.stop()
}
计算的对象是以Task为单位进行数据发送的,所以可能会导致大量的数据冗余 且 如果计算对象占用比较大的资源,性能会急剧下降
为了提供数据访问效率,可以将数据发送给Executor,而不是发给Task。
但是RDD数据模型只能给Task发。只能采用其他的数据模型: 广播变量(可以将数据发送给Executor 使得同一个节点上的Task可以共享一份资源,并且如果有Executor节点与Driver通信比较慢的时候 可以从就近的Executor上获得需要的资源)
十五:SparkSQL(Dataframe DataSet RDD 转换)
Spark + Hive => Shark
Spark SQL是Spark用于结构化数据(structured data)处理的Spark模块。
1.2 SparkSQL特点:
无缝的整合了 SQL 查询和 Spark 编程
SparkSQL可以简化RDD的开发,提高开发效率
15.1:Dataframe 与 DataSet 与 RDD概述
SQL语言本身没有类型
工厂模式生产的对象是有相同的功能的,因为从同一条生产线下来的。
Builder模式要生产某个对象需要经过复杂的过程,但是builder模式会把这些过程顺序确定下来。
Scala中的构造方法有两种,主构造方法和辅助构造方法,辅助构造方法的函数名一定为this。
15.2:Dataframe 与 DataSet 与 RDD
Dataframe : 是SparkSQL中的核心数据模型,是弱类型的模型,不关心数据的类型操作。
因为SQL的局限性,因此在处理数据的时候,Dataframe提供了一种DSL语法(面向对象的语法)
SparkSQL 与 RDD的关系与转换
SparkSQL模块等同于是对SparkCore一个封装,专门用于结构化数据处理的场景
Dataset : 就是使用面向对象的方式操作数据, 类,属性 => 结构 就是
在Dataframe的基础上,增加了类型信息
SparkSQL中存在大量的隐式转换操作,但是一般会导入使用 import时,使用的spark并不是包名,是环境对象的名称。
Dataframe 与 DataSet的区别?
Dataframe是特定类型的DataSet[Row]
说明DataSet兼容Dataframe()是将来用的比较多的
注意:如果从内存中获取数据,spark可以知道数据类型具体是什么。如果是数字,默认作为Int处理;但是从文件中读取的数字,不能确定是什么类型,所以用bigint接收,可以和Long类型转换,但是和Int不能进行转换
SparkSQL技术选型:
15.2:UDF 与 UDAF
package com.atguigu.bigdata.spark.sql01
import org.apache.spark.sql.expressions.Aggregator
import org.apache.spark.sql.{Dataframe, Encoder, Encoders, SparkSession, functions}
object SparkSQL09_Dataframe_UDAF {
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
df.createOrReplaceTempView("user")
// TODO UDAF使用时,需要采用特殊的方式实现自定义操作
// TODO 强类型操作不能应用于弱类型操作,如果想用,那么需要转换functions.udaf
// Spark3.0以后,可以通过转换,让强类型应用在弱类型操作
spark.udf.register("avgAge",functions.udaf(new AvgAgeUDF()))
// TODO : UDF
// 自定义的函数在SQL文使用
spark.sql("select avgAge(age) from user").show()
spark.stop()
}
case class CalcBuffer(var total : Long , var cnt : Long)
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 1. 继承org.apache.spark.sql.expressions.Aggregator
// 2. 定义泛型
// IN : Long
// BUFF : CalcBuffer
// OUT : Long
// 3. 重写方法 (4 + 2(固定) = 6)
class AvgAgeUDF extends Aggregator[Long,CalcBuffer,Long]{
// TODO 缓冲区的初始化
override def zero: CalcBuffer = {
CalcBuffer(0l,0l)
}
// TODO 将输入的数据和缓冲区的数据进行聚合操作
override def reduce(buff: CalcBuffer, a: Long): CalcBuffer ={
buff.total += a
buff.cnt += 1
buff
}
// TODO 多个缓冲区的合并操作
override def merge(b1: CalcBuffer, b2: CalcBuffer): CalcBuffer = {
b1.total += b2.total
b1.cnt += b2.cnt
b1
}
// TODO 计算数据
override def finish(reduction: CalcBuffer): Long = {
reduction.total / reduction.cnt
}
override def bufferEncoder: Encoder[CalcBuffer] = Encoders.product
override def outputEncoder: Encoder[Long] = Encoders.scalaLong
}
}
UDAF老版本:
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
df.createOrReplaceTempView("user")
// TODO UDAF使用时,需要采用特殊的方式实现自定义操作
// TODO 强类型操作不能应用于弱类型操作,如果想用,那么需要转换functions.udaf
// Spark3.0以后,可以通过转换,让强类型应用在弱类型操作
spark.udf.register("avgAge",new AvgAgeUDAF())
// TODO : UDF
// 自定义的函数在SQL文使用
spark.sql("select avgAge(age) from user").show()
spark.stop()
}
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 使用早期Spark版本的UDAF(弱类型)
// 1. 继承UserDefinedAggregateFunction
// 2. 重写方法(8)
class AvgAgeUDAF extends UserDefinedAggregateFunction{
// TODO 输入数据的结构
override def inputSchema: StructType = {
StructType(
Array(
StructField("age",LongType)
)
)
}
// TODO 缓冲区数据的结构
override def bufferSchema: StructType = {
StructType(
Array(
StructField("total",LongType),
StructField("cnt",LongType)
)
)
}
// TODO 输出数据的类型
override def dataType: DataType = LongType
// TODO 计算稳定性
override def deterministic: Boolean = true
// TODO 缓冲区的初始化
override def initialize(buffer: MutableAggregationBuffer): Unit = {
buffer.update(0,0l)
buffer.update(1,0l)
}
// TODO 将输入的数据和缓冲区的数据进行聚合操作
override def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
buffer.update(0,buffer.getLong(0) + input.getLong(0))
buffer.update(1,buffer.getLong(1)+1)
}
// TODO 多个缓冲区的合并操作
override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
buffer1.update(0,buffer1.getLong(0) + buffer2.getLong(0))
buffer1.update(1,buffer1.getLong(1) + buffer2.getLong(1))
}
override def evaluate(buffer: Row): Any = {
buffer.getLong(0) / buffer.getLong(1)
}
}
UDAF_Class老版本:
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
// TODO Spark3.0版本前,强类型的UDAF不能应用于弱类型的SQL中
import spark.implicits._
val udaf: AvgAgeUDAF = new AvgAgeUDAF
val ds: Dataset[User] = df.as[User]
ds.select(udaf.toColumn).show()
spark.stop()
}
case class User(id : Long , name : String , age : Long)
case class CalacBuffer(var total : Long , var cnt : Long)
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 使用早期Spark版本的UDAF(强类型)
// 1. 继承Aggregator
// 2. 定义泛型
// IN : User
// BUFF : CalcBuffer
// OUT : Long
// 3. 重写方法(6)
class AvgAgeUDAF extends Aggregator[User,CalacBuffer,Long]{
override def zero: CalacBuffer = {
CalacBuffer(0l,0L)
}
override def reduce(b: CalacBuffer, user: User): CalacBuffer = {
b.total += user.age
b.cnt += 1
b
}
override def merge(b1: CalacBuffer, b2: CalacBuffer): CalacBuffer = {
b1.total += b2.total
b1.cnt += b2.cnt
b1
}
override def finish(buffer: CalacBuffer): Long = {
buffer.total / buffer.cnt
}
override def bufferEncoder: Encoder[CalacBuffer] = Encoders.product
override def outputEncoder: Encoder[Long] = Encoders.scalaLong
}
15.3:通用读取方式和默认读取方式
user.json is not a Parquet file. expected magic number at tail [80, 65, 82, 49] but found [32, 53, 48, 125]
SparkSQL通用读取的方法,读取的文件为Parquet格式(列式存储格式)
val df = spark.read.load("data/users.parquet")
如果想要读取其他格式的文件也可以,需要额外增加参数进行解析
df.write.mode("append").format("json").save("output")
sparkSQL基于SparkCore
SparkCore读取数据是依赖于Hadoop
Hadoop读取数据是按行读取
SparkSQL读取JSON格式的文件,要求每一行数据必须符合JSON格式
读入数据:
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
//方式1:通用的load方法读取
spark.read.format("jdbc")
.option("url", "jdbc:mysql://linux1:3306/spark-sql")
.option("driver", "com.mysql.jdbc.Driver")
.option("user", "root")
.option("password", "123123")
.option("dbtable", "user")
.load().show
//方式2:通用的load方法读取 参数另一种形式
spark.read.format("jdbc")
.options(Map("url"->"jdbc:mysql://linux1:3306/spark-sql?user=root&password=123123",
"dbtable"->"user","driver"->"com.mysql.jdbc.Driver")).load().show
//方式3:使用jdbc方法读取
val props: Properties = new Properties()
props.setProperty("user", "root")
props.setProperty("password", "123123")
val df: Dataframe = spark.read.jdbc("jdbc:mysql://linux1:3306/spark-sql", "user", props)
df.show
//释放资源
spark.stop()
写入数据:
case class User2(name: String, age: Long)
。。。
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
val rdd: RDD[User2] = spark.sparkContext.makeRDD(List(User2("lisi", 20), User2("zs", 30)))
val ds: Dataset[User2] = rdd.toDS
//方式1:通用的方式 format指定写出类型
//方式2:通过jdbc方法
val props: Properties = new Properties()
props.setProperty("user", "root")
props.setProperty("password", "123123")
ds.write.mode(SaveMode.Append).jdbc("jdbc:mysql://linux1:3306/spark-sql", "user", props)
//释放资源
spark.stop()
十六:SparkStreaming(数据模型是离散流DStrem)
实时 离线 批量 流式
Spark是一个离线的批量数据处理的框架
SparkStreaming是一个准实时,微批次的处理框架
16.1:SparkStreaming知识点
Discretized Stream(离散流)是Spark Streaming的基础抽象,代表持续性的数据流和经过各种Spark原语操作后的结果数据流。在内部实现上,DStream是一系列连续的RDD来表示。每个RDD含有一段时间间隔内的数据。
16.2:DStream
blockingQueue队列:阻塞式队列,有反压机制,可以避免数据挤压。
采集数据的方式:DStream监控目录变化,将RDD放入到离散化流DStream中,自定义数据源。
object SparkStreaming05_DIY {
def main(args: Array[String]): Unit = {
//TODO SparkStreaming 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[*]")
.setAppName("Streaming")
.set("spark.testing.memory", "471859200")
//StreamingContext独享的第二个构造参数表示数据采集周期
// val ssc: StreamingContext = new StreamingContext(conf,new Duration(3 * 1000))
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
val uuidDS: ReceiverInputDStream[String] = ssc.receiverStream(new MyReceiver)
uuidDS.print()
//启动采集器
ssc.start()
// 等待采集器结束
ssc.awaitTermination()
}
class MyReceiver extends Receiver[String](StorageLevel.MEMORY_ONLY){
private var flg = true
override def onStart(): Unit = {
//采集数据
while (flg) {
//采集数据
val uuid: String = UUID.randomUUID().toString
//存储数据
store(uuid)
Thread.sleep(1000)
}
}
override def onStop(): Unit = {
//释放资源
flg = false
}
}
}
十七:DStream转换(有无状态转换)
离散化流无法完成排序操作 ,离散化流其实就是每个时间段RDD的封装 ,离散化流可以转换为RDD实现操作
transform用于将离散化流实现不了的功能进行调用,transform方法是DStream的方法,称之为原语,RDD的方法称之为算子 如果想要在Driver端周期性执行一些逻辑的场合,需要使用transform。
将两个窗口key相同的连接起来
使用updateStateByKey需要对检查点目录进行配置,会使用检查点来保存状态。
reduceByKey原语不会保留中间计算结果,一旦采集周期结束,数据丢弃 ,如果想要保留中间计算结果,需要有状态操作
从检查点中恢复数据
def main(args: Array[String]): Unit = {
//从检查点中恢复数据
val outerSSC = StreamingContext.getOrCreate("cp",() => {
//TODO SparkStreaming 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[*]")
.setAppName("Streaming")
.set("spark.testing.memory", "471859200")
//StreamingContext独享的第二个构造参数表示数据采集周期
// val ssc: StreamingContext = new StreamingContext(conf,new Duration(3 * 1000))
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
ssc.checkpoint("cp")
val socketDS: ReceiverInputDStream[String] = ssc.socketTextStream("localhost", 8888)
val wordDS: DStream[String] = socketDS.flatMap(_.split(" "))
val wordToOneDS: DStream[(String, Int)] = wordDS.map((_, 1))
wordToOneDS.updateStateByKey(
(seq : Seq[Int] , buffer : Option[Int]) => {
Option(seq.sum + buffer.getOrElse(0))
}
).print()
ssc
})
//SparkStreaming (reduceByKey) + Redis内存数据库 = 有状态
outerSSC.start()
outerSSC.awaitTermination()
}
十八:DStream窗口
窗口划过的规律是:数据由无到有再到无(先多后少后变没)
DStream离散流有window方法:wordToOneDS.window(Seconds(9),Seconds(3))可以设置窗口的大小。
window操作中,窗口范围和滑动的幅度应该为采集周期的整数倍
如果窗口范围和滑动的幅度相同, 那么就是滚动窗口,没有重复数据 默认情况下,不使用窗口,也会有窗口操作,窗口范围取值为1个采集周期,滑动幅度也是一个采集周期。
如果窗口范围和滑动的幅度不相同, 那么就是滑动窗口
1>窗口范围大于滑动的幅度, 存在重复数据
2>窗口范围小于滑动的幅度, 存在丢失数据的可能
二十:优雅的关闭
对于流式数据,线程的启动和关闭应该是分开的,属于两个线程。
优雅的关闭,不要强制关闭,关闭前,不再接受数据,而是把手头上的数据处理完成之后再关闭。
二十一:DStream输出
如果想要将DStream进行特殊处理,那么可以转换为底层RDD实现
join算子体现的是数据的关系
join算子将两个数据集中相同的key的数据,连接在一起
zip是将两个数据集位置相同的数据,连接起来与key无关
join 可能存在笛卡尔积 也 存在shuffle()
笛卡尔积 在落盘的时候 如果存在大量相同的key的时候数据量会激增
如果能够使用其它算子实现的功能,那么不推荐使用join
rdd1.join(rdd2).collect().foreach(println)
rdd1.leftOuterJoin(rdd2).collect().foreach(println) rdd1.rightOuterJoin(rdd2).collect().foreach(println) rdd1.fullOuterJoin(rdd2).collect().foreach(println)
cogroup = connect(多个数据集) + group(单一数据集)
cogroup = connect(多个数据集) + group(单一数据集)
方式一:
// 统计出每一个省份每个广告被点击数量排行的Top3 val fileDatas: RDD[String] = sc.textFile("data/agent.log") val groupDatas: RDD[(String, Iterable[String])] = fileDatas.groupBy( line => { val datas: Array[String] = line.split(" ") datas(1) } ) val top3: RDD[(String, List[(String, Int)])] = groupDatas.mapValues( //数据集是List(line, line, line) list => { //数据集是line val adToCount: Map[String, Int] = list.map( line => { val datas: Array[String] = line.split(" ") (datas(4), 1) } //数据集是List((省份,广告)) 按照省份进行分组统计 ).groupBy(_._1).mapValues(_.size) //Map[String, Int] [广告,点击次数] // 在 groupDatas.mapValues( 的架子中都是对value进行处理的 //元组天生的排序 数据类型[广告,点击次数] 前三条 adToCount.toList.sortBy(_._2)(Ordering.Int.reverse).take(3) } ) top3.collect().foreach(println) sc.stop() }方式二:
val fileDatas: RDD[String] = sc.textFile("data/agent.log") // 1. 将广告进行统计分析 (word, cnt) // ((省份1,广告1), 1),((省份2,广告2), 1)((省份1,广告1), 1) // ((省份1,广告1), sum),((省份1,广告2), sum) val reduceDatas: RDD[((String, String), Int)] = fileDatas.map( line => { val datas: Array[String] = line.split(" ") ((datas(1), datas(4)), 1) } ).reduceByKey(_ + _) // 1.5 将统计结果进行格式转换 // (省份1,(广告1, sum)),(省份1,(广告2, sum)) val mapDatas: RDD[(String, (String, Int))] = reduceDatas.map { case ((prv, ad), sum) => { (prv, (ad, sum)) } } // 2. 将统计结果按照省份进行分组 // (省份1, (广告1, sum),(广告2, sum)) val prvGroupDatas: RDD[(String, Iterable[(String, Int)])] = mapDatas.groupByKey() // 3. 将分组后的数据按照点击数量进行排序(降序),取前3名 val top3: RDD[(String, List[(String, Int)])] = prvGroupDatas.mapValues( iter => { iter.toList.sortBy(_._2)(Ordering.Int.reverse).take(3) } ) top3.collect().foreach(println)
十:action行动算子(是对结果进行处理的)
TODO : 所谓的行动(Action)算子,其实就是RDD用于触发作业执行的方法,类似于IO中的read方法
转换算子的返回值为RDD 行动算子的返回值为具体的结果
行动算子和作业的关系 :1 对 1
10.2:reduce
reduce算子可以触发作业的执行,分区内先两两计算,分区间再两两计算
分区内计算都是在Executor。
分区间计算都是在Driver
10.3:count first take takeOrdered
10.4: aggregate fold
10.5: countByKey countByValue
10.5: save相关的算子
rdd.saveAsTextFile("output1")
rdd.saveAsObjectFile("output2")
rdd.saveAsSequenceFile("output3")
10.5: collect foreach
10.6: 序列化闭包检测
一个函数使用了外部的数据,并且改变了这个数据的生命周期 将数据包含到函数内部,形成了一个闭合的环境,这个环境称之为闭包环境,简称闭包
Spark在运行作业之前,会判断程序逻辑中是否包含闭包
如果存在闭包操作,那么需要检测闭包中使用的数据是否能够序列化,如果不能,那么发生错误
这个操作称之为闭包检测
十一:RDD阶段的划分
前一个阶段不执行完,后面的阶段是无法执行的。
十二:依赖关系和血缘关系(宽依赖窄依赖)
十三:持久化
(持久化只针对于当前应用程序,资源使用完之后释放资源,不会给其它节点使用。因此诞生了检查点)
将计算结果缓存起来。
数据的持久化默认将数据保存到内存中,重复使用
wordToOne.cache()
持久化操作也可以保存到磁盘中
wordToOne.persist()
保存级别设置
十三:RDD CheckPoint检查点
java中的finalize方法是Object类中提供的一个方法,在GC准备释放对象所占用的内存空间之前,它将首先调用finalize()方法。
持久化只针对于当前应用程序,资源使用完之后释放资源,不会给其它节点使用。
持久化只针对于当前应用程序
跨应用程序的数据共享需要采用 检查点机制
设置的方式
① :sc.setCheckpointDir("cp") 该检查点的目录一般为分布式目录以便各个节点使用
②:wordToOne.checkpoint()
持久化操作会在血缘关系中增加依赖
检查点一般会将数据保存到分布式文件系统中(HDFS)(新的数据源)
检查点会切断血缘即减少血缘依赖,因为可以把HDFS中相对可靠的数据作为新的数据源。
十四:自定义分区 RDD文件读取与保存
Hash分区:对于给定的key,计算其hashCode,并除以分区个数取余
Range分区:将一定范围内的数据映射到一个分区中,尽量保证每个分区数据均匀,而且分区间有序
十四:累加器(数据模型)(功能比较单一没有shuffle性能相对高)
累加器的现象
分析:
如果数据和RDD有关系,那么可以从Driver端发送到Executor端执行
如果执行完毕,那么可以将结果拉回到Driver端
如果数据和RDD没有关系,因为计算中存在闭包操作,所以RDD会将数据发送到Executor
但是无法将数据从Executor端拉回到Driver端。
如果想要将和RDD无关的数据拉回到Driver端,RDD模型处理不了,需要采用新的数据模型——累加器
doubleAccumulator
collectionAccumulator 累加器
def main(args: Array[String]): Unit = {
// TODO Spark 环境
val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(conf)
// TODO 1. 创建累加器
val wordCountAcc = new MyAcculumator
// TODO 2. 将累加器注册到Spark中
sc.register(wordCountAcc, "WordCount")
val rdd = sc.makeRDD(
List(
"Hello",
"Hello",
"Hello",
"World",
"Hello",
),2
)
rdd.foreach(
word => {
// TODO 3. 使用累加器
wordCountAcc.add(word)
}
)
// TODO 4. 获取累加器的结果
println(wordCountAcc.value)
sc.stop()
}
// TODO 自定义数据累加器(WordCount)
// 1. 继承AccumulatorV2
// 2. 定义数据的泛型
// IN : String
// OUT : mutable.Map[String, Int]
// 3. 重写方法(3(计算) + 3(状态) = 6)
class MyAcculumator extends AccumulatorV2[String, mutable.Map[String, Int]]{
private val wordCountMap = mutable.Map[String, Int]()
// TODO 判断当前累加器是否为初始状态
override def isZero: Boolean = {
wordCountMap.isEmpty
}
// TODO 复制累加器
override def copy(): AccumulatorV2[String, mutable.Map[String, Int]] = {
new MyAcculumator()
}
// TODO 重置累加器
override def reset(): Unit = {
wordCountMap.clear()
}
// TODO 将外部的数据增加到累加器中
override def add(word: String): Unit = {
val oldCnt: Int = wordCountMap.getOrElse(word, 0)
wordCountMap.update(word, oldCnt + 1)
}
// TODO 将多个累加器进行合并
override def merge(other: AccumulatorV2[String, mutable.Map[String, Int]]): Unit = {
other.value.foreach {
case (word, cnt) => {
val oldCnt: Int = wordCountMap.getOrElse(word, 0)
wordCountMap.update(word, oldCnt + cnt)
}
}
}
// TODO 获取累加器的结果
override def value: mutable.Map[String, Int] = {
wordCountMap
}
}
十五:Join现象引出广播变量
def main(args: Array[String]): Unit = {
// TODO Spark 环境
val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(conf)
val rdd1 = sc.makeRDD(
List(
("a", 1),
("b", 2),
)
)
val rdd2 = sc.makeRDD(
List(
("a", 3),
("b", 4),
)
)
// ("a", 1)
// ( a,(1,3))
//rdd1.join(rdd2).collect().foreach(println)
// 计算的对象是以Task为单位进行数据发送的,所以可能会导致大量的数据冗余
// 所以如果计算对象占用比较大的资源,性能会急剧下降
// 为了提供数据访问效率,可以将数据发送给Executor,而不是发给Task。
// RDD数据模型只能给Task发。只能采用其他的数据模型:TODO 广播变量
val map = mutable.Map[String, Int](
("a", 3),
("b", 4)
)
// TODO 使用广播变量
val bc: Broadcast[mutable.Map[String, Int]] = sc.broadcast(map)
rdd1.map {
case (k, v1) => {
// TODO 获取广播变量的值
var v2 = bc.value.getOrElse(k, 0)
(k, (v1, v2))
}
}.collect.foreach(println)
sc.stop()
}
计算的对象是以Task为单位进行数据发送的,所以可能会导致大量的数据冗余 且 如果计算对象占用比较大的资源,性能会急剧下降
为了提供数据访问效率,可以将数据发送给Executor,而不是发给Task。
但是RDD数据模型只能给Task发。只能采用其他的数据模型: 广播变量(可以将数据发送给Executor 使得同一个节点上的Task可以共享一份资源,并且如果有Executor节点与Driver通信比较慢的时候 可以从就近的Executor上获得需要的资源)
十五:SparkSQL(Dataframe DataSet RDD 转换)
Spark + Hive => Shark
Spark SQL是Spark用于结构化数据(structured data)处理的Spark模块。
1.2 SparkSQL特点:
无缝的整合了 SQL 查询和 Spark 编程
SparkSQL可以简化RDD的开发,提高开发效率
15.1:Dataframe 与 DataSet 与 RDD概述
SQL语言本身没有类型
工厂模式生产的对象是有相同的功能的,因为从同一条生产线下来的。
Builder模式要生产某个对象需要经过复杂的过程,但是builder模式会把这些过程顺序确定下来。
Scala中的构造方法有两种,主构造方法和辅助构造方法,辅助构造方法的函数名一定为this。
15.2:Dataframe 与 DataSet 与 RDD
Dataframe : 是SparkSQL中的核心数据模型,是弱类型的模型,不关心数据的类型操作。
因为SQL的局限性,因此在处理数据的时候,Dataframe提供了一种DSL语法(面向对象的语法)
SparkSQL 与 RDD的关系与转换
SparkSQL模块等同于是对SparkCore一个封装,专门用于结构化数据处理的场景
Dataset : 就是使用面向对象的方式操作数据, 类,属性 => 结构 就是
在Dataframe的基础上,增加了类型信息
SparkSQL中存在大量的隐式转换操作,但是一般会导入使用 import时,使用的spark并不是包名,是环境对象的名称。
Dataframe 与 DataSet的区别?
Dataframe是特定类型的DataSet[Row]
说明DataSet兼容Dataframe()是将来用的比较多的
注意:如果从内存中获取数据,spark可以知道数据类型具体是什么。如果是数字,默认作为Int处理;但是从文件中读取的数字,不能确定是什么类型,所以用bigint接收,可以和Long类型转换,但是和Int不能进行转换
SparkSQL技术选型:
15.2:UDF 与 UDAF
package com.atguigu.bigdata.spark.sql01
import org.apache.spark.sql.expressions.Aggregator
import org.apache.spark.sql.{Dataframe, Encoder, Encoders, SparkSession, functions}
object SparkSQL09_Dataframe_UDAF {
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
df.createOrReplaceTempView("user")
// TODO UDAF使用时,需要采用特殊的方式实现自定义操作
// TODO 强类型操作不能应用于弱类型操作,如果想用,那么需要转换functions.udaf
// Spark3.0以后,可以通过转换,让强类型应用在弱类型操作
spark.udf.register("avgAge",functions.udaf(new AvgAgeUDF()))
// TODO : UDF
// 自定义的函数在SQL文使用
spark.sql("select avgAge(age) from user").show()
spark.stop()
}
case class CalcBuffer(var total : Long , var cnt : Long)
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 1. 继承org.apache.spark.sql.expressions.Aggregator
// 2. 定义泛型
// IN : Long
// BUFF : CalcBuffer
// OUT : Long
// 3. 重写方法 (4 + 2(固定) = 6)
class AvgAgeUDF extends Aggregator[Long,CalcBuffer,Long]{
// TODO 缓冲区的初始化
override def zero: CalcBuffer = {
CalcBuffer(0l,0l)
}
// TODO 将输入的数据和缓冲区的数据进行聚合操作
override def reduce(buff: CalcBuffer, a: Long): CalcBuffer ={
buff.total += a
buff.cnt += 1
buff
}
// TODO 多个缓冲区的合并操作
override def merge(b1: CalcBuffer, b2: CalcBuffer): CalcBuffer = {
b1.total += b2.total
b1.cnt += b2.cnt
b1
}
// TODO 计算数据
override def finish(reduction: CalcBuffer): Long = {
reduction.total / reduction.cnt
}
override def bufferEncoder: Encoder[CalcBuffer] = Encoders.product
override def outputEncoder: Encoder[Long] = Encoders.scalaLong
}
}
UDAF老版本:
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
df.createOrReplaceTempView("user")
// TODO UDAF使用时,需要采用特殊的方式实现自定义操作
// TODO 强类型操作不能应用于弱类型操作,如果想用,那么需要转换functions.udaf
// Spark3.0以后,可以通过转换,让强类型应用在弱类型操作
spark.udf.register("avgAge",new AvgAgeUDAF())
// TODO : UDF
// 自定义的函数在SQL文使用
spark.sql("select avgAge(age) from user").show()
spark.stop()
}
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 使用早期Spark版本的UDAF(弱类型)
// 1. 继承UserDefinedAggregateFunction
// 2. 重写方法(8)
class AvgAgeUDAF extends UserDefinedAggregateFunction{
// TODO 输入数据的结构
override def inputSchema: StructType = {
StructType(
Array(
StructField("age",LongType)
)
)
}
// TODO 缓冲区数据的结构
override def bufferSchema: StructType = {
StructType(
Array(
StructField("total",LongType),
StructField("cnt",LongType)
)
)
}
// TODO 输出数据的类型
override def dataType: DataType = LongType
// TODO 计算稳定性
override def deterministic: Boolean = true
// TODO 缓冲区的初始化
override def initialize(buffer: MutableAggregationBuffer): Unit = {
buffer.update(0,0l)
buffer.update(1,0l)
}
// TODO 将输入的数据和缓冲区的数据进行聚合操作
override def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
buffer.update(0,buffer.getLong(0) + input.getLong(0))
buffer.update(1,buffer.getLong(1)+1)
}
// TODO 多个缓冲区的合并操作
override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
buffer1.update(0,buffer1.getLong(0) + buffer2.getLong(0))
buffer1.update(1,buffer1.getLong(1) + buffer2.getLong(1))
}
override def evaluate(buffer: Row): Any = {
buffer.getLong(0) / buffer.getLong(1)
}
}
UDAF_Class老版本:
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
// TODO Spark3.0版本前,强类型的UDAF不能应用于弱类型的SQL中
import spark.implicits._
val udaf: AvgAgeUDAF = new AvgAgeUDAF
val ds: Dataset[User] = df.as[User]
ds.select(udaf.toColumn).show()
spark.stop()
}
case class User(id : Long , name : String , age : Long)
case class CalacBuffer(var total : Long , var cnt : Long)
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 使用早期Spark版本的UDAF(强类型)
// 1. 继承Aggregator
// 2. 定义泛型
// IN : User
// BUFF : CalcBuffer
// OUT : Long
// 3. 重写方法(6)
class AvgAgeUDAF extends Aggregator[User,CalacBuffer,Long]{
override def zero: CalacBuffer = {
CalacBuffer(0l,0L)
}
override def reduce(b: CalacBuffer, user: User): CalacBuffer = {
b.total += user.age
b.cnt += 1
b
}
override def merge(b1: CalacBuffer, b2: CalacBuffer): CalacBuffer = {
b1.total += b2.total
b1.cnt += b2.cnt
b1
}
override def finish(buffer: CalacBuffer): Long = {
buffer.total / buffer.cnt
}
override def bufferEncoder: Encoder[CalacBuffer] = Encoders.product
override def outputEncoder: Encoder[Long] = Encoders.scalaLong
}
15.3:通用读取方式和默认读取方式
user.json is not a Parquet file. expected magic number at tail [80, 65, 82, 49] but found [32, 53, 48, 125]
SparkSQL通用读取的方法,读取的文件为Parquet格式(列式存储格式)
val df = spark.read.load("data/users.parquet")
如果想要读取其他格式的文件也可以,需要额外增加参数进行解析
df.write.mode("append").format("json").save("output")
sparkSQL基于SparkCore
SparkCore读取数据是依赖于Hadoop
Hadoop读取数据是按行读取
SparkSQL读取JSON格式的文件,要求每一行数据必须符合JSON格式
读入数据:
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
//方式1:通用的load方法读取
spark.read.format("jdbc")
.option("url", "jdbc:mysql://linux1:3306/spark-sql")
.option("driver", "com.mysql.jdbc.Driver")
.option("user", "root")
.option("password", "123123")
.option("dbtable", "user")
.load().show
//方式2:通用的load方法读取 参数另一种形式
spark.read.format("jdbc")
.options(Map("url"->"jdbc:mysql://linux1:3306/spark-sql?user=root&password=123123",
"dbtable"->"user","driver"->"com.mysql.jdbc.Driver")).load().show
//方式3:使用jdbc方法读取
val props: Properties = new Properties()
props.setProperty("user", "root")
props.setProperty("password", "123123")
val df: Dataframe = spark.read.jdbc("jdbc:mysql://linux1:3306/spark-sql", "user", props)
df.show
//释放资源
spark.stop()
写入数据:
case class User2(name: String, age: Long)
。。。
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
val rdd: RDD[User2] = spark.sparkContext.makeRDD(List(User2("lisi", 20), User2("zs", 30)))
val ds: Dataset[User2] = rdd.toDS
//方式1:通用的方式 format指定写出类型
//方式2:通过jdbc方法
val props: Properties = new Properties()
props.setProperty("user", "root")
props.setProperty("password", "123123")
ds.write.mode(SaveMode.Append).jdbc("jdbc:mysql://linux1:3306/spark-sql", "user", props)
//释放资源
spark.stop()
十六:SparkStreaming(数据模型是离散流DStrem)
实时 离线 批量 流式
Spark是一个离线的批量数据处理的框架
SparkStreaming是一个准实时,微批次的处理框架
16.1:SparkStreaming知识点
Discretized Stream(离散流)是Spark Streaming的基础抽象,代表持续性的数据流和经过各种Spark原语操作后的结果数据流。在内部实现上,DStream是一系列连续的RDD来表示。每个RDD含有一段时间间隔内的数据。
16.2:DStream
blockingQueue队列:阻塞式队列,有反压机制,可以避免数据挤压。
采集数据的方式:DStream监控目录变化,将RDD放入到离散化流DStream中,自定义数据源。
object SparkStreaming05_DIY {
def main(args: Array[String]): Unit = {
//TODO SparkStreaming 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[*]")
.setAppName("Streaming")
.set("spark.testing.memory", "471859200")
//StreamingContext独享的第二个构造参数表示数据采集周期
// val ssc: StreamingContext = new StreamingContext(conf,new Duration(3 * 1000))
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
val uuidDS: ReceiverInputDStream[String] = ssc.receiverStream(new MyReceiver)
uuidDS.print()
//启动采集器
ssc.start()
// 等待采集器结束
ssc.awaitTermination()
}
class MyReceiver extends Receiver[String](StorageLevel.MEMORY_ONLY){
private var flg = true
override def onStart(): Unit = {
//采集数据
while (flg) {
//采集数据
val uuid: String = UUID.randomUUID().toString
//存储数据
store(uuid)
Thread.sleep(1000)
}
}
override def onStop(): Unit = {
//释放资源
flg = false
}
}
}
十七:DStream转换(有无状态转换)
离散化流无法完成排序操作 ,离散化流其实就是每个时间段RDD的封装 ,离散化流可以转换为RDD实现操作
transform用于将离散化流实现不了的功能进行调用,transform方法是DStream的方法,称之为原语,RDD的方法称之为算子 如果想要在Driver端周期性执行一些逻辑的场合,需要使用transform。
将两个窗口key相同的连接起来
使用updateStateByKey需要对检查点目录进行配置,会使用检查点来保存状态。
reduceByKey原语不会保留中间计算结果,一旦采集周期结束,数据丢弃 ,如果想要保留中间计算结果,需要有状态操作
从检查点中恢复数据
def main(args: Array[String]): Unit = {
//从检查点中恢复数据
val outerSSC = StreamingContext.getOrCreate("cp",() => {
//TODO SparkStreaming 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[*]")
.setAppName("Streaming")
.set("spark.testing.memory", "471859200")
//StreamingContext独享的第二个构造参数表示数据采集周期
// val ssc: StreamingContext = new StreamingContext(conf,new Duration(3 * 1000))
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
ssc.checkpoint("cp")
val socketDS: ReceiverInputDStream[String] = ssc.socketTextStream("localhost", 8888)
val wordDS: DStream[String] = socketDS.flatMap(_.split(" "))
val wordToOneDS: DStream[(String, Int)] = wordDS.map((_, 1))
wordToOneDS.updateStateByKey(
(seq : Seq[Int] , buffer : Option[Int]) => {
Option(seq.sum + buffer.getOrElse(0))
}
).print()
ssc
})
//SparkStreaming (reduceByKey) + Redis内存数据库 = 有状态
outerSSC.start()
outerSSC.awaitTermination()
}
十八:DStream窗口
窗口划过的规律是:数据由无到有再到无(先多后少后变没)
DStream离散流有window方法:wordToOneDS.window(Seconds(9),Seconds(3))可以设置窗口的大小。
window操作中,窗口范围和滑动的幅度应该为采集周期的整数倍
如果窗口范围和滑动的幅度相同, 那么就是滚动窗口,没有重复数据 默认情况下,不使用窗口,也会有窗口操作,窗口范围取值为1个采集周期,滑动幅度也是一个采集周期。
如果窗口范围和滑动的幅度不相同, 那么就是滑动窗口
1>窗口范围大于滑动的幅度, 存在重复数据
2>窗口范围小于滑动的幅度, 存在丢失数据的可能
二十:优雅的关闭
对于流式数据,线程的启动和关闭应该是分开的,属于两个线程。
优雅的关闭,不要强制关闭,关闭前,不再接受数据,而是把手头上的数据处理完成之后再关闭。
二十一:DStream输出
如果想要将DStream进行特殊处理,那么可以转换为底层RDD实现
TODO : 所谓的行动(Action)算子,其实就是RDD用于触发作业执行的方法,类似于IO中的read方法
转换算子的返回值为RDD 行动算子的返回值为具体的结果
行动算子和作业的关系 :1 对 1
行动算子和作业的关系 :1 对 1
reduce算子可以触发作业的执行,分区内先两两计算,分区间再两两计算
分区内计算都是在Executor。
分区间计算都是在Driver
10.3:count first take takeOrdered
10.4: aggregate fold
10.5: countByKey countByValue
10.5: save相关的算子
rdd.saveAsTextFile("output1")
rdd.saveAsObjectFile("output2")
rdd.saveAsSequenceFile("output3")
10.5: collect foreach
10.6: 序列化闭包检测
一个函数使用了外部的数据,并且改变了这个数据的生命周期 将数据包含到函数内部,形成了一个闭合的环境,这个环境称之为闭包环境,简称闭包
Spark在运行作业之前,会判断程序逻辑中是否包含闭包
如果存在闭包操作,那么需要检测闭包中使用的数据是否能够序列化,如果不能,那么发生错误
这个操作称之为闭包检测
十一:RDD阶段的划分
前一个阶段不执行完,后面的阶段是无法执行的。
十二:依赖关系和血缘关系(宽依赖窄依赖)
十三:持久化
(持久化只针对于当前应用程序,资源使用完之后释放资源,不会给其它节点使用。因此诞生了检查点)
将计算结果缓存起来。
数据的持久化默认将数据保存到内存中,重复使用
wordToOne.cache()
持久化操作也可以保存到磁盘中
wordToOne.persist()
保存级别设置
十三:RDD CheckPoint检查点
java中的finalize方法是Object类中提供的一个方法,在GC准备释放对象所占用的内存空间之前,它将首先调用finalize()方法。
持久化只针对于当前应用程序,资源使用完之后释放资源,不会给其它节点使用。
持久化只针对于当前应用程序
跨应用程序的数据共享需要采用 检查点机制
设置的方式
① :sc.setCheckpointDir("cp") 该检查点的目录一般为分布式目录以便各个节点使用
②:wordToOne.checkpoint()
持久化操作会在血缘关系中增加依赖
检查点一般会将数据保存到分布式文件系统中(HDFS)(新的数据源)
检查点会切断血缘即减少血缘依赖,因为可以把HDFS中相对可靠的数据作为新的数据源。
十四:自定义分区 RDD文件读取与保存
Hash分区:对于给定的key,计算其hashCode,并除以分区个数取余
Range分区:将一定范围内的数据映射到一个分区中,尽量保证每个分区数据均匀,而且分区间有序
十四:累加器(数据模型)(功能比较单一没有shuffle性能相对高)
累加器的现象
分析:
如果数据和RDD有关系,那么可以从Driver端发送到Executor端执行
如果执行完毕,那么可以将结果拉回到Driver端
如果数据和RDD没有关系,因为计算中存在闭包操作,所以RDD会将数据发送到Executor
但是无法将数据从Executor端拉回到Driver端。
如果想要将和RDD无关的数据拉回到Driver端,RDD模型处理不了,需要采用新的数据模型——累加器
doubleAccumulator
collectionAccumulator 累加器
def main(args: Array[String]): Unit = {
// TODO Spark 环境
val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(conf)
// TODO 1. 创建累加器
val wordCountAcc = new MyAcculumator
// TODO 2. 将累加器注册到Spark中
sc.register(wordCountAcc, "WordCount")
val rdd = sc.makeRDD(
List(
"Hello",
"Hello",
"Hello",
"World",
"Hello",
),2
)
rdd.foreach(
word => {
// TODO 3. 使用累加器
wordCountAcc.add(word)
}
)
// TODO 4. 获取累加器的结果
println(wordCountAcc.value)
sc.stop()
}
// TODO 自定义数据累加器(WordCount)
// 1. 继承AccumulatorV2
// 2. 定义数据的泛型
// IN : String
// OUT : mutable.Map[String, Int]
// 3. 重写方法(3(计算) + 3(状态) = 6)
class MyAcculumator extends AccumulatorV2[String, mutable.Map[String, Int]]{
private val wordCountMap = mutable.Map[String, Int]()
// TODO 判断当前累加器是否为初始状态
override def isZero: Boolean = {
wordCountMap.isEmpty
}
// TODO 复制累加器
override def copy(): AccumulatorV2[String, mutable.Map[String, Int]] = {
new MyAcculumator()
}
// TODO 重置累加器
override def reset(): Unit = {
wordCountMap.clear()
}
// TODO 将外部的数据增加到累加器中
override def add(word: String): Unit = {
val oldCnt: Int = wordCountMap.getOrElse(word, 0)
wordCountMap.update(word, oldCnt + 1)
}
// TODO 将多个累加器进行合并
override def merge(other: AccumulatorV2[String, mutable.Map[String, Int]]): Unit = {
other.value.foreach {
case (word, cnt) => {
val oldCnt: Int = wordCountMap.getOrElse(word, 0)
wordCountMap.update(word, oldCnt + cnt)
}
}
}
// TODO 获取累加器的结果
override def value: mutable.Map[String, Int] = {
wordCountMap
}
}
十五:Join现象引出广播变量
def main(args: Array[String]): Unit = {
// TODO Spark 环境
val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(conf)
val rdd1 = sc.makeRDD(
List(
("a", 1),
("b", 2),
)
)
val rdd2 = sc.makeRDD(
List(
("a", 3),
("b", 4),
)
)
// ("a", 1)
// ( a,(1,3))
//rdd1.join(rdd2).collect().foreach(println)
// 计算的对象是以Task为单位进行数据发送的,所以可能会导致大量的数据冗余
// 所以如果计算对象占用比较大的资源,性能会急剧下降
// 为了提供数据访问效率,可以将数据发送给Executor,而不是发给Task。
// RDD数据模型只能给Task发。只能采用其他的数据模型:TODO 广播变量
val map = mutable.Map[String, Int](
("a", 3),
("b", 4)
)
// TODO 使用广播变量
val bc: Broadcast[mutable.Map[String, Int]] = sc.broadcast(map)
rdd1.map {
case (k, v1) => {
// TODO 获取广播变量的值
var v2 = bc.value.getOrElse(k, 0)
(k, (v1, v2))
}
}.collect.foreach(println)
sc.stop()
}
计算的对象是以Task为单位进行数据发送的,所以可能会导致大量的数据冗余 且 如果计算对象占用比较大的资源,性能会急剧下降
为了提供数据访问效率,可以将数据发送给Executor,而不是发给Task。
但是RDD数据模型只能给Task发。只能采用其他的数据模型: 广播变量(可以将数据发送给Executor 使得同一个节点上的Task可以共享一份资源,并且如果有Executor节点与Driver通信比较慢的时候 可以从就近的Executor上获得需要的资源)
十五:SparkSQL(Dataframe DataSet RDD 转换)
Spark + Hive => Shark
Spark SQL是Spark用于结构化数据(structured data)处理的Spark模块。
1.2 SparkSQL特点:
无缝的整合了 SQL 查询和 Spark 编程
SparkSQL可以简化RDD的开发,提高开发效率
15.1:Dataframe 与 DataSet 与 RDD概述
SQL语言本身没有类型
工厂模式生产的对象是有相同的功能的,因为从同一条生产线下来的。
Builder模式要生产某个对象需要经过复杂的过程,但是builder模式会把这些过程顺序确定下来。
Scala中的构造方法有两种,主构造方法和辅助构造方法,辅助构造方法的函数名一定为this。
15.2:Dataframe 与 DataSet 与 RDD
Dataframe : 是SparkSQL中的核心数据模型,是弱类型的模型,不关心数据的类型操作。
因为SQL的局限性,因此在处理数据的时候,Dataframe提供了一种DSL语法(面向对象的语法)
SparkSQL 与 RDD的关系与转换
SparkSQL模块等同于是对SparkCore一个封装,专门用于结构化数据处理的场景
Dataset : 就是使用面向对象的方式操作数据, 类,属性 => 结构 就是
在Dataframe的基础上,增加了类型信息
SparkSQL中存在大量的隐式转换操作,但是一般会导入使用 import时,使用的spark并不是包名,是环境对象的名称。
Dataframe 与 DataSet的区别?
Dataframe是特定类型的DataSet[Row]
说明DataSet兼容Dataframe()是将来用的比较多的
注意:如果从内存中获取数据,spark可以知道数据类型具体是什么。如果是数字,默认作为Int处理;但是从文件中读取的数字,不能确定是什么类型,所以用bigint接收,可以和Long类型转换,但是和Int不能进行转换
SparkSQL技术选型:
15.2:UDF 与 UDAF
package com.atguigu.bigdata.spark.sql01
import org.apache.spark.sql.expressions.Aggregator
import org.apache.spark.sql.{Dataframe, Encoder, Encoders, SparkSession, functions}
object SparkSQL09_Dataframe_UDAF {
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
df.createOrReplaceTempView("user")
// TODO UDAF使用时,需要采用特殊的方式实现自定义操作
// TODO 强类型操作不能应用于弱类型操作,如果想用,那么需要转换functions.udaf
// Spark3.0以后,可以通过转换,让强类型应用在弱类型操作
spark.udf.register("avgAge",functions.udaf(new AvgAgeUDF()))
// TODO : UDF
// 自定义的函数在SQL文使用
spark.sql("select avgAge(age) from user").show()
spark.stop()
}
case class CalcBuffer(var total : Long , var cnt : Long)
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 1. 继承org.apache.spark.sql.expressions.Aggregator
// 2. 定义泛型
// IN : Long
// BUFF : CalcBuffer
// OUT : Long
// 3. 重写方法 (4 + 2(固定) = 6)
class AvgAgeUDF extends Aggregator[Long,CalcBuffer,Long]{
// TODO 缓冲区的初始化
override def zero: CalcBuffer = {
CalcBuffer(0l,0l)
}
// TODO 将输入的数据和缓冲区的数据进行聚合操作
override def reduce(buff: CalcBuffer, a: Long): CalcBuffer ={
buff.total += a
buff.cnt += 1
buff
}
// TODO 多个缓冲区的合并操作
override def merge(b1: CalcBuffer, b2: CalcBuffer): CalcBuffer = {
b1.total += b2.total
b1.cnt += b2.cnt
b1
}
// TODO 计算数据
override def finish(reduction: CalcBuffer): Long = {
reduction.total / reduction.cnt
}
override def bufferEncoder: Encoder[CalcBuffer] = Encoders.product
override def outputEncoder: Encoder[Long] = Encoders.scalaLong
}
}
UDAF老版本:
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
df.createOrReplaceTempView("user")
// TODO UDAF使用时,需要采用特殊的方式实现自定义操作
// TODO 强类型操作不能应用于弱类型操作,如果想用,那么需要转换functions.udaf
// Spark3.0以后,可以通过转换,让强类型应用在弱类型操作
spark.udf.register("avgAge",new AvgAgeUDAF())
// TODO : UDF
// 自定义的函数在SQL文使用
spark.sql("select avgAge(age) from user").show()
spark.stop()
}
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 使用早期Spark版本的UDAF(弱类型)
// 1. 继承UserDefinedAggregateFunction
// 2. 重写方法(8)
class AvgAgeUDAF extends UserDefinedAggregateFunction{
// TODO 输入数据的结构
override def inputSchema: StructType = {
StructType(
Array(
StructField("age",LongType)
)
)
}
// TODO 缓冲区数据的结构
override def bufferSchema: StructType = {
StructType(
Array(
StructField("total",LongType),
StructField("cnt",LongType)
)
)
}
// TODO 输出数据的类型
override def dataType: DataType = LongType
// TODO 计算稳定性
override def deterministic: Boolean = true
// TODO 缓冲区的初始化
override def initialize(buffer: MutableAggregationBuffer): Unit = {
buffer.update(0,0l)
buffer.update(1,0l)
}
// TODO 将输入的数据和缓冲区的数据进行聚合操作
override def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
buffer.update(0,buffer.getLong(0) + input.getLong(0))
buffer.update(1,buffer.getLong(1)+1)
}
// TODO 多个缓冲区的合并操作
override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
buffer1.update(0,buffer1.getLong(0) + buffer2.getLong(0))
buffer1.update(1,buffer1.getLong(1) + buffer2.getLong(1))
}
override def evaluate(buffer: Row): Any = {
buffer.getLong(0) / buffer.getLong(1)
}
}
UDAF_Class老版本:
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
// TODO Spark3.0版本前,强类型的UDAF不能应用于弱类型的SQL中
import spark.implicits._
val udaf: AvgAgeUDAF = new AvgAgeUDAF
val ds: Dataset[User] = df.as[User]
ds.select(udaf.toColumn).show()
spark.stop()
}
case class User(id : Long , name : String , age : Long)
case class CalacBuffer(var total : Long , var cnt : Long)
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 使用早期Spark版本的UDAF(强类型)
// 1. 继承Aggregator
// 2. 定义泛型
// IN : User
// BUFF : CalcBuffer
// OUT : Long
// 3. 重写方法(6)
class AvgAgeUDAF extends Aggregator[User,CalacBuffer,Long]{
override def zero: CalacBuffer = {
CalacBuffer(0l,0L)
}
override def reduce(b: CalacBuffer, user: User): CalacBuffer = {
b.total += user.age
b.cnt += 1
b
}
override def merge(b1: CalacBuffer, b2: CalacBuffer): CalacBuffer = {
b1.total += b2.total
b1.cnt += b2.cnt
b1
}
override def finish(buffer: CalacBuffer): Long = {
buffer.total / buffer.cnt
}
override def bufferEncoder: Encoder[CalacBuffer] = Encoders.product
override def outputEncoder: Encoder[Long] = Encoders.scalaLong
}
15.3:通用读取方式和默认读取方式
user.json is not a Parquet file. expected magic number at tail [80, 65, 82, 49] but found [32, 53, 48, 125]
SparkSQL通用读取的方法,读取的文件为Parquet格式(列式存储格式)
val df = spark.read.load("data/users.parquet")
如果想要读取其他格式的文件也可以,需要额外增加参数进行解析
df.write.mode("append").format("json").save("output")
sparkSQL基于SparkCore
SparkCore读取数据是依赖于Hadoop
Hadoop读取数据是按行读取
SparkSQL读取JSON格式的文件,要求每一行数据必须符合JSON格式
读入数据:
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
//方式1:通用的load方法读取
spark.read.format("jdbc")
.option("url", "jdbc:mysql://linux1:3306/spark-sql")
.option("driver", "com.mysql.jdbc.Driver")
.option("user", "root")
.option("password", "123123")
.option("dbtable", "user")
.load().show
//方式2:通用的load方法读取 参数另一种形式
spark.read.format("jdbc")
.options(Map("url"->"jdbc:mysql://linux1:3306/spark-sql?user=root&password=123123",
"dbtable"->"user","driver"->"com.mysql.jdbc.Driver")).load().show
//方式3:使用jdbc方法读取
val props: Properties = new Properties()
props.setProperty("user", "root")
props.setProperty("password", "123123")
val df: Dataframe = spark.read.jdbc("jdbc:mysql://linux1:3306/spark-sql", "user", props)
df.show
//释放资源
spark.stop()
写入数据:
case class User2(name: String, age: Long)
。。。
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
val rdd: RDD[User2] = spark.sparkContext.makeRDD(List(User2("lisi", 20), User2("zs", 30)))
val ds: Dataset[User2] = rdd.toDS
//方式1:通用的方式 format指定写出类型
//方式2:通过jdbc方法
val props: Properties = new Properties()
props.setProperty("user", "root")
props.setProperty("password", "123123")
ds.write.mode(SaveMode.Append).jdbc("jdbc:mysql://linux1:3306/spark-sql", "user", props)
//释放资源
spark.stop()
十六:SparkStreaming(数据模型是离散流DStrem)
实时 离线 批量 流式
Spark是一个离线的批量数据处理的框架
SparkStreaming是一个准实时,微批次的处理框架
16.1:SparkStreaming知识点
Discretized Stream(离散流)是Spark Streaming的基础抽象,代表持续性的数据流和经过各种Spark原语操作后的结果数据流。在内部实现上,DStream是一系列连续的RDD来表示。每个RDD含有一段时间间隔内的数据。
16.2:DStream
blockingQueue队列:阻塞式队列,有反压机制,可以避免数据挤压。
采集数据的方式:DStream监控目录变化,将RDD放入到离散化流DStream中,自定义数据源。
object SparkStreaming05_DIY {
def main(args: Array[String]): Unit = {
//TODO SparkStreaming 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[*]")
.setAppName("Streaming")
.set("spark.testing.memory", "471859200")
//StreamingContext独享的第二个构造参数表示数据采集周期
// val ssc: StreamingContext = new StreamingContext(conf,new Duration(3 * 1000))
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
val uuidDS: ReceiverInputDStream[String] = ssc.receiverStream(new MyReceiver)
uuidDS.print()
//启动采集器
ssc.start()
// 等待采集器结束
ssc.awaitTermination()
}
class MyReceiver extends Receiver[String](StorageLevel.MEMORY_ONLY){
private var flg = true
override def onStart(): Unit = {
//采集数据
while (flg) {
//采集数据
val uuid: String = UUID.randomUUID().toString
//存储数据
store(uuid)
Thread.sleep(1000)
}
}
override def onStop(): Unit = {
//释放资源
flg = false
}
}
}
十七:DStream转换(有无状态转换)
离散化流无法完成排序操作 ,离散化流其实就是每个时间段RDD的封装 ,离散化流可以转换为RDD实现操作
transform用于将离散化流实现不了的功能进行调用,transform方法是DStream的方法,称之为原语,RDD的方法称之为算子 如果想要在Driver端周期性执行一些逻辑的场合,需要使用transform。
将两个窗口key相同的连接起来
使用updateStateByKey需要对检查点目录进行配置,会使用检查点来保存状态。
reduceByKey原语不会保留中间计算结果,一旦采集周期结束,数据丢弃 ,如果想要保留中间计算结果,需要有状态操作
从检查点中恢复数据
def main(args: Array[String]): Unit = {
//从检查点中恢复数据
val outerSSC = StreamingContext.getOrCreate("cp",() => {
//TODO SparkStreaming 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[*]")
.setAppName("Streaming")
.set("spark.testing.memory", "471859200")
//StreamingContext独享的第二个构造参数表示数据采集周期
// val ssc: StreamingContext = new StreamingContext(conf,new Duration(3 * 1000))
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
ssc.checkpoint("cp")
val socketDS: ReceiverInputDStream[String] = ssc.socketTextStream("localhost", 8888)
val wordDS: DStream[String] = socketDS.flatMap(_.split(" "))
val wordToOneDS: DStream[(String, Int)] = wordDS.map((_, 1))
wordToOneDS.updateStateByKey(
(seq : Seq[Int] , buffer : Option[Int]) => {
Option(seq.sum + buffer.getOrElse(0))
}
).print()
ssc
})
//SparkStreaming (reduceByKey) + Redis内存数据库 = 有状态
outerSSC.start()
outerSSC.awaitTermination()
}
十八:DStream窗口
窗口划过的规律是:数据由无到有再到无(先多后少后变没)
DStream离散流有window方法:wordToOneDS.window(Seconds(9),Seconds(3))可以设置窗口的大小。
window操作中,窗口范围和滑动的幅度应该为采集周期的整数倍
如果窗口范围和滑动的幅度相同, 那么就是滚动窗口,没有重复数据 默认情况下,不使用窗口,也会有窗口操作,窗口范围取值为1个采集周期,滑动幅度也是一个采集周期。
如果窗口范围和滑动的幅度不相同, 那么就是滑动窗口
1>窗口范围大于滑动的幅度, 存在重复数据
2>窗口范围小于滑动的幅度, 存在丢失数据的可能
二十:优雅的关闭
对于流式数据,线程的启动和关闭应该是分开的,属于两个线程。
优雅的关闭,不要强制关闭,关闭前,不再接受数据,而是把手头上的数据处理完成之后再关闭。
二十一:DStream输出
如果想要将DStream进行特殊处理,那么可以转换为底层RDD实现
10.5: countByKey countByValue
10.5: save相关的算子
rdd.saveAsTextFile("output1")
rdd.saveAsObjectFile("output2")
rdd.saveAsSequenceFile("output3")
10.5: collect foreach
10.6: 序列化闭包检测
一个函数使用了外部的数据,并且改变了这个数据的生命周期 将数据包含到函数内部,形成了一个闭合的环境,这个环境称之为闭包环境,简称闭包
Spark在运行作业之前,会判断程序逻辑中是否包含闭包
如果存在闭包操作,那么需要检测闭包中使用的数据是否能够序列化,如果不能,那么发生错误
这个操作称之为闭包检测
十一:RDD阶段的划分
前一个阶段不执行完,后面的阶段是无法执行的。
十二:依赖关系和血缘关系(宽依赖窄依赖)
十三:持久化
(持久化只针对于当前应用程序,资源使用完之后释放资源,不会给其它节点使用。因此诞生了检查点)
将计算结果缓存起来。
数据的持久化默认将数据保存到内存中,重复使用
wordToOne.cache()
持久化操作也可以保存到磁盘中
wordToOne.persist()
保存级别设置
十三:RDD CheckPoint检查点
java中的finalize方法是Object类中提供的一个方法,在GC准备释放对象所占用的内存空间之前,它将首先调用finalize()方法。
持久化只针对于当前应用程序,资源使用完之后释放资源,不会给其它节点使用。
持久化只针对于当前应用程序
跨应用程序的数据共享需要采用 检查点机制
设置的方式
① :sc.setCheckpointDir("cp") 该检查点的目录一般为分布式目录以便各个节点使用
②:wordToOne.checkpoint()
持久化操作会在血缘关系中增加依赖
检查点一般会将数据保存到分布式文件系统中(HDFS)(新的数据源)
检查点会切断血缘即减少血缘依赖,因为可以把HDFS中相对可靠的数据作为新的数据源。
十四:自定义分区 RDD文件读取与保存
Hash分区:对于给定的key,计算其hashCode,并除以分区个数取余
Range分区:将一定范围内的数据映射到一个分区中,尽量保证每个分区数据均匀,而且分区间有序
十四:累加器(数据模型)(功能比较单一没有shuffle性能相对高)
累加器的现象
分析:
如果数据和RDD有关系,那么可以从Driver端发送到Executor端执行
如果执行完毕,那么可以将结果拉回到Driver端
如果数据和RDD没有关系,因为计算中存在闭包操作,所以RDD会将数据发送到Executor
但是无法将数据从Executor端拉回到Driver端。
如果想要将和RDD无关的数据拉回到Driver端,RDD模型处理不了,需要采用新的数据模型——累加器
doubleAccumulator
collectionAccumulator 累加器
def main(args: Array[String]): Unit = {
// TODO Spark 环境
val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(conf)
// TODO 1. 创建累加器
val wordCountAcc = new MyAcculumator
// TODO 2. 将累加器注册到Spark中
sc.register(wordCountAcc, "WordCount")
val rdd = sc.makeRDD(
List(
"Hello",
"Hello",
"Hello",
"World",
"Hello",
),2
)
rdd.foreach(
word => {
// TODO 3. 使用累加器
wordCountAcc.add(word)
}
)
// TODO 4. 获取累加器的结果
println(wordCountAcc.value)
sc.stop()
}
// TODO 自定义数据累加器(WordCount)
// 1. 继承AccumulatorV2
// 2. 定义数据的泛型
// IN : String
// OUT : mutable.Map[String, Int]
// 3. 重写方法(3(计算) + 3(状态) = 6)
class MyAcculumator extends AccumulatorV2[String, mutable.Map[String, Int]]{
private val wordCountMap = mutable.Map[String, Int]()
// TODO 判断当前累加器是否为初始状态
override def isZero: Boolean = {
wordCountMap.isEmpty
}
// TODO 复制累加器
override def copy(): AccumulatorV2[String, mutable.Map[String, Int]] = {
new MyAcculumator()
}
// TODO 重置累加器
override def reset(): Unit = {
wordCountMap.clear()
}
// TODO 将外部的数据增加到累加器中
override def add(word: String): Unit = {
val oldCnt: Int = wordCountMap.getOrElse(word, 0)
wordCountMap.update(word, oldCnt + 1)
}
// TODO 将多个累加器进行合并
override def merge(other: AccumulatorV2[String, mutable.Map[String, Int]]): Unit = {
other.value.foreach {
case (word, cnt) => {
val oldCnt: Int = wordCountMap.getOrElse(word, 0)
wordCountMap.update(word, oldCnt + cnt)
}
}
}
// TODO 获取累加器的结果
override def value: mutable.Map[String, Int] = {
wordCountMap
}
}
十五:Join现象引出广播变量
def main(args: Array[String]): Unit = {
// TODO Spark 环境
val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(conf)
val rdd1 = sc.makeRDD(
List(
("a", 1),
("b", 2),
)
)
val rdd2 = sc.makeRDD(
List(
("a", 3),
("b", 4),
)
)
// ("a", 1)
// ( a,(1,3))
//rdd1.join(rdd2).collect().foreach(println)
// 计算的对象是以Task为单位进行数据发送的,所以可能会导致大量的数据冗余
// 所以如果计算对象占用比较大的资源,性能会急剧下降
// 为了提供数据访问效率,可以将数据发送给Executor,而不是发给Task。
// RDD数据模型只能给Task发。只能采用其他的数据模型:TODO 广播变量
val map = mutable.Map[String, Int](
("a", 3),
("b", 4)
)
// TODO 使用广播变量
val bc: Broadcast[mutable.Map[String, Int]] = sc.broadcast(map)
rdd1.map {
case (k, v1) => {
// TODO 获取广播变量的值
var v2 = bc.value.getOrElse(k, 0)
(k, (v1, v2))
}
}.collect.foreach(println)
sc.stop()
}
计算的对象是以Task为单位进行数据发送的,所以可能会导致大量的数据冗余 且 如果计算对象占用比较大的资源,性能会急剧下降
为了提供数据访问效率,可以将数据发送给Executor,而不是发给Task。
但是RDD数据模型只能给Task发。只能采用其他的数据模型: 广播变量(可以将数据发送给Executor 使得同一个节点上的Task可以共享一份资源,并且如果有Executor节点与Driver通信比较慢的时候 可以从就近的Executor上获得需要的资源)
十五:SparkSQL(Dataframe DataSet RDD 转换)
Spark + Hive => Shark
Spark SQL是Spark用于结构化数据(structured data)处理的Spark模块。
1.2 SparkSQL特点:
无缝的整合了 SQL 查询和 Spark 编程
SparkSQL可以简化RDD的开发,提高开发效率
15.1:Dataframe 与 DataSet 与 RDD概述
SQL语言本身没有类型
工厂模式生产的对象是有相同的功能的,因为从同一条生产线下来的。
Builder模式要生产某个对象需要经过复杂的过程,但是builder模式会把这些过程顺序确定下来。
Scala中的构造方法有两种,主构造方法和辅助构造方法,辅助构造方法的函数名一定为this。
15.2:Dataframe 与 DataSet 与 RDD
Dataframe : 是SparkSQL中的核心数据模型,是弱类型的模型,不关心数据的类型操作。
因为SQL的局限性,因此在处理数据的时候,Dataframe提供了一种DSL语法(面向对象的语法)
SparkSQL 与 RDD的关系与转换
SparkSQL模块等同于是对SparkCore一个封装,专门用于结构化数据处理的场景
Dataset : 就是使用面向对象的方式操作数据, 类,属性 => 结构 就是
在Dataframe的基础上,增加了类型信息
SparkSQL中存在大量的隐式转换操作,但是一般会导入使用 import时,使用的spark并不是包名,是环境对象的名称。
Dataframe 与 DataSet的区别?
Dataframe是特定类型的DataSet[Row]
说明DataSet兼容Dataframe()是将来用的比较多的
注意:如果从内存中获取数据,spark可以知道数据类型具体是什么。如果是数字,默认作为Int处理;但是从文件中读取的数字,不能确定是什么类型,所以用bigint接收,可以和Long类型转换,但是和Int不能进行转换
SparkSQL技术选型:
15.2:UDF 与 UDAF
package com.atguigu.bigdata.spark.sql01
import org.apache.spark.sql.expressions.Aggregator
import org.apache.spark.sql.{Dataframe, Encoder, Encoders, SparkSession, functions}
object SparkSQL09_Dataframe_UDAF {
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
df.createOrReplaceTempView("user")
// TODO UDAF使用时,需要采用特殊的方式实现自定义操作
// TODO 强类型操作不能应用于弱类型操作,如果想用,那么需要转换functions.udaf
// Spark3.0以后,可以通过转换,让强类型应用在弱类型操作
spark.udf.register("avgAge",functions.udaf(new AvgAgeUDF()))
// TODO : UDF
// 自定义的函数在SQL文使用
spark.sql("select avgAge(age) from user").show()
spark.stop()
}
case class CalcBuffer(var total : Long , var cnt : Long)
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 1. 继承org.apache.spark.sql.expressions.Aggregator
// 2. 定义泛型
// IN : Long
// BUFF : CalcBuffer
// OUT : Long
// 3. 重写方法 (4 + 2(固定) = 6)
class AvgAgeUDF extends Aggregator[Long,CalcBuffer,Long]{
// TODO 缓冲区的初始化
override def zero: CalcBuffer = {
CalcBuffer(0l,0l)
}
// TODO 将输入的数据和缓冲区的数据进行聚合操作
override def reduce(buff: CalcBuffer, a: Long): CalcBuffer ={
buff.total += a
buff.cnt += 1
buff
}
// TODO 多个缓冲区的合并操作
override def merge(b1: CalcBuffer, b2: CalcBuffer): CalcBuffer = {
b1.total += b2.total
b1.cnt += b2.cnt
b1
}
// TODO 计算数据
override def finish(reduction: CalcBuffer): Long = {
reduction.total / reduction.cnt
}
override def bufferEncoder: Encoder[CalcBuffer] = Encoders.product
override def outputEncoder: Encoder[Long] = Encoders.scalaLong
}
}
UDAF老版本:
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
df.createOrReplaceTempView("user")
// TODO UDAF使用时,需要采用特殊的方式实现自定义操作
// TODO 强类型操作不能应用于弱类型操作,如果想用,那么需要转换functions.udaf
// Spark3.0以后,可以通过转换,让强类型应用在弱类型操作
spark.udf.register("avgAge",new AvgAgeUDAF())
// TODO : UDF
// 自定义的函数在SQL文使用
spark.sql("select avgAge(age) from user").show()
spark.stop()
}
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 使用早期Spark版本的UDAF(弱类型)
// 1. 继承UserDefinedAggregateFunction
// 2. 重写方法(8)
class AvgAgeUDAF extends UserDefinedAggregateFunction{
// TODO 输入数据的结构
override def inputSchema: StructType = {
StructType(
Array(
StructField("age",LongType)
)
)
}
// TODO 缓冲区数据的结构
override def bufferSchema: StructType = {
StructType(
Array(
StructField("total",LongType),
StructField("cnt",LongType)
)
)
}
// TODO 输出数据的类型
override def dataType: DataType = LongType
// TODO 计算稳定性
override def deterministic: Boolean = true
// TODO 缓冲区的初始化
override def initialize(buffer: MutableAggregationBuffer): Unit = {
buffer.update(0,0l)
buffer.update(1,0l)
}
// TODO 将输入的数据和缓冲区的数据进行聚合操作
override def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
buffer.update(0,buffer.getLong(0) + input.getLong(0))
buffer.update(1,buffer.getLong(1)+1)
}
// TODO 多个缓冲区的合并操作
override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
buffer1.update(0,buffer1.getLong(0) + buffer2.getLong(0))
buffer1.update(1,buffer1.getLong(1) + buffer2.getLong(1))
}
override def evaluate(buffer: Row): Any = {
buffer.getLong(0) / buffer.getLong(1)
}
}
UDAF_Class老版本:
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
// TODO Spark3.0版本前,强类型的UDAF不能应用于弱类型的SQL中
import spark.implicits._
val udaf: AvgAgeUDAF = new AvgAgeUDAF
val ds: Dataset[User] = df.as[User]
ds.select(udaf.toColumn).show()
spark.stop()
}
case class User(id : Long , name : String , age : Long)
case class CalacBuffer(var total : Long , var cnt : Long)
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 使用早期Spark版本的UDAF(强类型)
// 1. 继承Aggregator
// 2. 定义泛型
// IN : User
// BUFF : CalcBuffer
// OUT : Long
// 3. 重写方法(6)
class AvgAgeUDAF extends Aggregator[User,CalacBuffer,Long]{
override def zero: CalacBuffer = {
CalacBuffer(0l,0L)
}
override def reduce(b: CalacBuffer, user: User): CalacBuffer = {
b.total += user.age
b.cnt += 1
b
}
override def merge(b1: CalacBuffer, b2: CalacBuffer): CalacBuffer = {
b1.total += b2.total
b1.cnt += b2.cnt
b1
}
override def finish(buffer: CalacBuffer): Long = {
buffer.total / buffer.cnt
}
override def bufferEncoder: Encoder[CalacBuffer] = Encoders.product
override def outputEncoder: Encoder[Long] = Encoders.scalaLong
}
15.3:通用读取方式和默认读取方式
user.json is not a Parquet file. expected magic number at tail [80, 65, 82, 49] but found [32, 53, 48, 125]
SparkSQL通用读取的方法,读取的文件为Parquet格式(列式存储格式)
val df = spark.read.load("data/users.parquet")
如果想要读取其他格式的文件也可以,需要额外增加参数进行解析
df.write.mode("append").format("json").save("output")
sparkSQL基于SparkCore
SparkCore读取数据是依赖于Hadoop
Hadoop读取数据是按行读取
SparkSQL读取JSON格式的文件,要求每一行数据必须符合JSON格式
读入数据:
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
//方式1:通用的load方法读取
spark.read.format("jdbc")
.option("url", "jdbc:mysql://linux1:3306/spark-sql")
.option("driver", "com.mysql.jdbc.Driver")
.option("user", "root")
.option("password", "123123")
.option("dbtable", "user")
.load().show
//方式2:通用的load方法读取 参数另一种形式
spark.read.format("jdbc")
.options(Map("url"->"jdbc:mysql://linux1:3306/spark-sql?user=root&password=123123",
"dbtable"->"user","driver"->"com.mysql.jdbc.Driver")).load().show
//方式3:使用jdbc方法读取
val props: Properties = new Properties()
props.setProperty("user", "root")
props.setProperty("password", "123123")
val df: Dataframe = spark.read.jdbc("jdbc:mysql://linux1:3306/spark-sql", "user", props)
df.show
//释放资源
spark.stop()
写入数据:
case class User2(name: String, age: Long)
。。。
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
val rdd: RDD[User2] = spark.sparkContext.makeRDD(List(User2("lisi", 20), User2("zs", 30)))
val ds: Dataset[User2] = rdd.toDS
//方式1:通用的方式 format指定写出类型
//方式2:通过jdbc方法
val props: Properties = new Properties()
props.setProperty("user", "root")
props.setProperty("password", "123123")
ds.write.mode(SaveMode.Append).jdbc("jdbc:mysql://linux1:3306/spark-sql", "user", props)
//释放资源
spark.stop()
十六:SparkStreaming(数据模型是离散流DStrem)
实时 离线 批量 流式
Spark是一个离线的批量数据处理的框架
SparkStreaming是一个准实时,微批次的处理框架
16.1:SparkStreaming知识点
Discretized Stream(离散流)是Spark Streaming的基础抽象,代表持续性的数据流和经过各种Spark原语操作后的结果数据流。在内部实现上,DStream是一系列连续的RDD来表示。每个RDD含有一段时间间隔内的数据。
16.2:DStream
blockingQueue队列:阻塞式队列,有反压机制,可以避免数据挤压。
采集数据的方式:DStream监控目录变化,将RDD放入到离散化流DStream中,自定义数据源。
object SparkStreaming05_DIY {
def main(args: Array[String]): Unit = {
//TODO SparkStreaming 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[*]")
.setAppName("Streaming")
.set("spark.testing.memory", "471859200")
//StreamingContext独享的第二个构造参数表示数据采集周期
// val ssc: StreamingContext = new StreamingContext(conf,new Duration(3 * 1000))
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
val uuidDS: ReceiverInputDStream[String] = ssc.receiverStream(new MyReceiver)
uuidDS.print()
//启动采集器
ssc.start()
// 等待采集器结束
ssc.awaitTermination()
}
class MyReceiver extends Receiver[String](StorageLevel.MEMORY_ONLY){
private var flg = true
override def onStart(): Unit = {
//采集数据
while (flg) {
//采集数据
val uuid: String = UUID.randomUUID().toString
//存储数据
store(uuid)
Thread.sleep(1000)
}
}
override def onStop(): Unit = {
//释放资源
flg = false
}
}
}
十七:DStream转换(有无状态转换)
离散化流无法完成排序操作 ,离散化流其实就是每个时间段RDD的封装 ,离散化流可以转换为RDD实现操作
transform用于将离散化流实现不了的功能进行调用,transform方法是DStream的方法,称之为原语,RDD的方法称之为算子 如果想要在Driver端周期性执行一些逻辑的场合,需要使用transform。
将两个窗口key相同的连接起来
使用updateStateByKey需要对检查点目录进行配置,会使用检查点来保存状态。
reduceByKey原语不会保留中间计算结果,一旦采集周期结束,数据丢弃 ,如果想要保留中间计算结果,需要有状态操作
从检查点中恢复数据
def main(args: Array[String]): Unit = {
//从检查点中恢复数据
val outerSSC = StreamingContext.getOrCreate("cp",() => {
//TODO SparkStreaming 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[*]")
.setAppName("Streaming")
.set("spark.testing.memory", "471859200")
//StreamingContext独享的第二个构造参数表示数据采集周期
// val ssc: StreamingContext = new StreamingContext(conf,new Duration(3 * 1000))
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
ssc.checkpoint("cp")
val socketDS: ReceiverInputDStream[String] = ssc.socketTextStream("localhost", 8888)
val wordDS: DStream[String] = socketDS.flatMap(_.split(" "))
val wordToOneDS: DStream[(String, Int)] = wordDS.map((_, 1))
wordToOneDS.updateStateByKey(
(seq : Seq[Int] , buffer : Option[Int]) => {
Option(seq.sum + buffer.getOrElse(0))
}
).print()
ssc
})
//SparkStreaming (reduceByKey) + Redis内存数据库 = 有状态
outerSSC.start()
outerSSC.awaitTermination()
}
十八:DStream窗口
窗口划过的规律是:数据由无到有再到无(先多后少后变没)
DStream离散流有window方法:wordToOneDS.window(Seconds(9),Seconds(3))可以设置窗口的大小。
window操作中,窗口范围和滑动的幅度应该为采集周期的整数倍
如果窗口范围和滑动的幅度相同, 那么就是滚动窗口,没有重复数据 默认情况下,不使用窗口,也会有窗口操作,窗口范围取值为1个采集周期,滑动幅度也是一个采集周期。
如果窗口范围和滑动的幅度不相同, 那么就是滑动窗口
1>窗口范围大于滑动的幅度, 存在重复数据
2>窗口范围小于滑动的幅度, 存在丢失数据的可能
二十:优雅的关闭
对于流式数据,线程的启动和关闭应该是分开的,属于两个线程。
优雅的关闭,不要强制关闭,关闭前,不再接受数据,而是把手头上的数据处理完成之后再关闭。
二十一:DStream输出
如果想要将DStream进行特殊处理,那么可以转换为底层RDD实现
rdd.saveAsObjectFile("output2")
rdd.saveAsSequenceFile("output3")
如果存在闭包操作,那么需要检测闭包中使用的数据是否能够序列化,如果不能,那么发生错误
这个操作称之为闭包检测
wordToOne.cache()
持久化操作也可以保存到磁盘中
wordToOne.persist() 保存级别设置
跨应用程序的数据共享需要采用 检查点机制 设置的方式 ① :sc.setCheckpointDir("cp") 该检查点的目录一般为分布式目录以便各个节点使用
②:wordToOne.checkpoint() 持久化操作会在血缘关系中增加依赖
检查点一般会将数据保存到分布式文件系统中(HDFS)(新的数据源)
检查点会切断血缘即减少血缘依赖,因为可以把HDFS中相对可靠的数据作为新的数据源。
如果执行完毕,那么可以将结果拉回到Driver端
如果数据和RDD没有关系,因为计算中存在闭包操作,所以RDD会将数据发送到Executor
但是无法将数据从Executor端拉回到Driver端。 如果想要将和RDD无关的数据拉回到Driver端,RDD模型处理不了,需要采用新的数据模型——累加器 doubleAccumulator collectionAccumulator 累加器
def main(args: Array[String]): Unit = {
// TODO Spark 环境
val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(conf)
// TODO 1. 创建累加器
val wordCountAcc = new MyAcculumator
// TODO 2. 将累加器注册到Spark中
sc.register(wordCountAcc, "WordCount")
val rdd = sc.makeRDD(
List(
"Hello",
"Hello",
"Hello",
"World",
"Hello",
),2
)
rdd.foreach(
word => {
// TODO 3. 使用累加器
wordCountAcc.add(word)
}
)
// TODO 4. 获取累加器的结果
println(wordCountAcc.value)
sc.stop()
}
// TODO 自定义数据累加器(WordCount)
// 1. 继承AccumulatorV2
// 2. 定义数据的泛型
// IN : String
// OUT : mutable.Map[String, Int]
// 3. 重写方法(3(计算) + 3(状态) = 6)
class MyAcculumator extends AccumulatorV2[String, mutable.Map[String, Int]]{
private val wordCountMap = mutable.Map[String, Int]()
// TODO 判断当前累加器是否为初始状态
override def isZero: Boolean = {
wordCountMap.isEmpty
}
// TODO 复制累加器
override def copy(): AccumulatorV2[String, mutable.Map[String, Int]] = {
new MyAcculumator()
}
// TODO 重置累加器
override def reset(): Unit = {
wordCountMap.clear()
}
// TODO 将外部的数据增加到累加器中
override def add(word: String): Unit = {
val oldCnt: Int = wordCountMap.getOrElse(word, 0)
wordCountMap.update(word, oldCnt + 1)
}
// TODO 将多个累加器进行合并
override def merge(other: AccumulatorV2[String, mutable.Map[String, Int]]): Unit = {
other.value.foreach {
case (word, cnt) => {
val oldCnt: Int = wordCountMap.getOrElse(word, 0)
wordCountMap.update(word, oldCnt + cnt)
}
}
}
// TODO 获取累加器的结果
override def value: mutable.Map[String, Int] = {
wordCountMap
}
}
def main(args: Array[String]): Unit = {
// TODO Spark 环境
val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
val sc = new SparkContext(conf)
val rdd1 = sc.makeRDD(
List(
("a", 1),
("b", 2),
)
)
val rdd2 = sc.makeRDD(
List(
("a", 3),
("b", 4),
)
)
// ("a", 1)
// ( a,(1,3))
//rdd1.join(rdd2).collect().foreach(println)
// 计算的对象是以Task为单位进行数据发送的,所以可能会导致大量的数据冗余
// 所以如果计算对象占用比较大的资源,性能会急剧下降
// 为了提供数据访问效率,可以将数据发送给Executor,而不是发给Task。
// RDD数据模型只能给Task发。只能采用其他的数据模型:TODO 广播变量
val map = mutable.Map[String, Int](
("a", 3),
("b", 4)
)
// TODO 使用广播变量
val bc: Broadcast[mutable.Map[String, Int]] = sc.broadcast(map)
rdd1.map {
case (k, v1) => {
// TODO 获取广播变量的值
var v2 = bc.value.getOrElse(k, 0)
(k, (v1, v2))
}
}.collect.foreach(println)
sc.stop()
}
计算的对象是以Task为单位进行数据发送的,所以可能会导致大量的数据冗余 且 如果计算对象占用比较大的资源,性能会急剧下降为了提供数据访问效率,可以将数据发送给Executor,而不是发给Task。
但是RDD数据模型只能给Task发。只能采用其他的数据模型: 广播变量(可以将数据发送给Executor 使得同一个节点上的Task可以共享一份资源,并且如果有Executor节点与Driver通信比较慢的时候 可以从就近的Executor上获得需要的资源)
无缝的整合了 SQL 查询和 Spark 编程
SparkSQL可以简化RDD的开发,提高开发效率工厂模式生产的对象是有相同的功能的,因为从同一条生产线下来的。 Builder模式要生产某个对象需要经过复杂的过程,但是builder模式会把这些过程顺序确定下来。 Scala中的构造方法有两种,主构造方法和辅助构造方法,辅助构造方法的函数名一定为this。
package com.atguigu.bigdata.spark.sql01
import org.apache.spark.sql.expressions.Aggregator
import org.apache.spark.sql.{Dataframe, Encoder, Encoders, SparkSession, functions}
object SparkSQL09_Dataframe_UDAF {
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
df.createOrReplaceTempView("user")
// TODO UDAF使用时,需要采用特殊的方式实现自定义操作
// TODO 强类型操作不能应用于弱类型操作,如果想用,那么需要转换functions.udaf
// Spark3.0以后,可以通过转换,让强类型应用在弱类型操作
spark.udf.register("avgAge",functions.udaf(new AvgAgeUDF()))
// TODO : UDF
// 自定义的函数在SQL文使用
spark.sql("select avgAge(age) from user").show()
spark.stop()
}
case class CalcBuffer(var total : Long , var cnt : Long)
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 1. 继承org.apache.spark.sql.expressions.Aggregator
// 2. 定义泛型
// IN : Long
// BUFF : CalcBuffer
// OUT : Long
// 3. 重写方法 (4 + 2(固定) = 6)
class AvgAgeUDF extends Aggregator[Long,CalcBuffer,Long]{
// TODO 缓冲区的初始化
override def zero: CalcBuffer = {
CalcBuffer(0l,0l)
}
// TODO 将输入的数据和缓冲区的数据进行聚合操作
override def reduce(buff: CalcBuffer, a: Long): CalcBuffer ={
buff.total += a
buff.cnt += 1
buff
}
// TODO 多个缓冲区的合并操作
override def merge(b1: CalcBuffer, b2: CalcBuffer): CalcBuffer = {
b1.total += b2.total
b1.cnt += b2.cnt
b1
}
// TODO 计算数据
override def finish(reduction: CalcBuffer): Long = {
reduction.total / reduction.cnt
}
override def bufferEncoder: Encoder[CalcBuffer] = Encoders.product
override def outputEncoder: Encoder[Long] = Encoders.scalaLong
}
}
UDAF老版本:
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
df.createOrReplaceTempView("user")
// TODO UDAF使用时,需要采用特殊的方式实现自定义操作
// TODO 强类型操作不能应用于弱类型操作,如果想用,那么需要转换functions.udaf
// Spark3.0以后,可以通过转换,让强类型应用在弱类型操作
spark.udf.register("avgAge",new AvgAgeUDAF())
// TODO : UDF
// 自定义的函数在SQL文使用
spark.sql("select avgAge(age) from user").show()
spark.stop()
}
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 使用早期Spark版本的UDAF(弱类型)
// 1. 继承UserDefinedAggregateFunction
// 2. 重写方法(8)
class AvgAgeUDAF extends UserDefinedAggregateFunction{
// TODO 输入数据的结构
override def inputSchema: StructType = {
StructType(
Array(
StructField("age",LongType)
)
)
}
// TODO 缓冲区数据的结构
override def bufferSchema: StructType = {
StructType(
Array(
StructField("total",LongType),
StructField("cnt",LongType)
)
)
}
// TODO 输出数据的类型
override def dataType: DataType = LongType
// TODO 计算稳定性
override def deterministic: Boolean = true
// TODO 缓冲区的初始化
override def initialize(buffer: MutableAggregationBuffer): Unit = {
buffer.update(0,0l)
buffer.update(1,0l)
}
// TODO 将输入的数据和缓冲区的数据进行聚合操作
override def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
buffer.update(0,buffer.getLong(0) + input.getLong(0))
buffer.update(1,buffer.getLong(1)+1)
}
// TODO 多个缓冲区的合并操作
override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
buffer1.update(0,buffer1.getLong(0) + buffer2.getLong(0))
buffer1.update(1,buffer1.getLong(1) + buffer2.getLong(1))
}
override def evaluate(buffer: Row): Any = {
buffer.getLong(0) / buffer.getLong(1)
}
}
UDAF_Class老版本:
def main(args: Array[String]): Unit = {
// TODO SparkSQL 环境
val spark: SparkSession = SparkSession.builder
.master("local[*]")
.appName("SQL")
.config("spark.testing.memory", "471859200")
.getOrCreate()
val df = spark.read.json("data/user.json")
// TODO Spark3.0版本前,强类型的UDAF不能应用于弱类型的SQL中
import spark.implicits._
val udaf: AvgAgeUDAF = new AvgAgeUDAF
val ds: Dataset[User] = df.as[User]
ds.select(udaf.toColumn).show()
spark.stop()
}
case class User(id : Long , name : String , age : Long)
case class CalacBuffer(var total : Long , var cnt : Long)
// TODO SparkSQL中UDAF一般情况下都采用自定义函数类
// 使用早期Spark版本的UDAF(强类型)
// 1. 继承Aggregator
// 2. 定义泛型
// IN : User
// BUFF : CalcBuffer
// OUT : Long
// 3. 重写方法(6)
class AvgAgeUDAF extends Aggregator[User,CalacBuffer,Long]{
override def zero: CalacBuffer = {
CalacBuffer(0l,0L)
}
override def reduce(b: CalacBuffer, user: User): CalacBuffer = {
b.total += user.age
b.cnt += 1
b
}
override def merge(b1: CalacBuffer, b2: CalacBuffer): CalacBuffer = {
b1.total += b2.total
b1.cnt += b2.cnt
b1
}
override def finish(buffer: CalacBuffer): Long = {
buffer.total / buffer.cnt
}
override def bufferEncoder: Encoder[CalacBuffer] = Encoders.product
override def outputEncoder: Encoder[Long] = Encoders.scalaLong
}
SparkSQL通用读取的方法,读取的文件为Parquet格式(列式存储格式)
val df = spark.read.load("data/users.parquet")
如果想要读取其他格式的文件也可以,需要额外增加参数进行解析
df.write.mode("append").format("json").save("output")
sparkSQL基于SparkCore
SparkCore读取数据是依赖于Hadoop
Hadoop读取数据是按行读取
SparkSQL读取JSON格式的文件,要求每一行数据必须符合JSON格式
读入数据:
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
//方式1:通用的load方法读取
spark.read.format("jdbc")
.option("url", "jdbc:mysql://linux1:3306/spark-sql")
.option("driver", "com.mysql.jdbc.Driver")
.option("user", "root")
.option("password", "123123")
.option("dbtable", "user")
.load().show
//方式2:通用的load方法读取 参数另一种形式
spark.read.format("jdbc")
.options(Map("url"->"jdbc:mysql://linux1:3306/spark-sql?user=root&password=123123",
"dbtable"->"user","driver"->"com.mysql.jdbc.Driver")).load().show
//方式3:使用jdbc方法读取
val props: Properties = new Properties()
props.setProperty("user", "root")
props.setProperty("password", "123123")
val df: Dataframe = spark.read.jdbc("jdbc:mysql://linux1:3306/spark-sql", "user", props)
df.show
//释放资源
spark.stop()
写入数据:
case class User2(name: String, age: Long)
。。。
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
//创建SparkSession对象
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
import spark.implicits._
val rdd: RDD[User2] = spark.sparkContext.makeRDD(List(User2("lisi", 20), User2("zs", 30)))
val ds: Dataset[User2] = rdd.toDS
//方式1:通用的方式 format指定写出类型
//方式2:通过jdbc方法
val props: Properties = new Properties()
props.setProperty("user", "root")
props.setProperty("password", "123123")
ds.write.mode(SaveMode.Append).jdbc("jdbc:mysql://linux1:3306/spark-sql", "user", props)
//释放资源
spark.stop()
object SparkStreaming05_DIY {
def main(args: Array[String]): Unit = {
//TODO SparkStreaming 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[*]")
.setAppName("Streaming")
.set("spark.testing.memory", "471859200")
//StreamingContext独享的第二个构造参数表示数据采集周期
// val ssc: StreamingContext = new StreamingContext(conf,new Duration(3 * 1000))
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
val uuidDS: ReceiverInputDStream[String] = ssc.receiverStream(new MyReceiver)
uuidDS.print()
//启动采集器
ssc.start()
// 等待采集器结束
ssc.awaitTermination()
}
class MyReceiver extends Receiver[String](StorageLevel.MEMORY_ONLY){
private var flg = true
override def onStart(): Unit = {
//采集数据
while (flg) {
//采集数据
val uuid: String = UUID.randomUUID().toString
//存储数据
store(uuid)
Thread.sleep(1000)
}
}
override def onStop(): Unit = {
//释放资源
flg = false
}
}
}
def main(args: Array[String]): Unit = {
//从检查点中恢复数据
val outerSSC = StreamingContext.getOrCreate("cp",() => {
//TODO SparkStreaming 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[*]")
.setAppName("Streaming")
.set("spark.testing.memory", "471859200")
//StreamingContext独享的第二个构造参数表示数据采集周期
// val ssc: StreamingContext = new StreamingContext(conf,new Duration(3 * 1000))
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
ssc.checkpoint("cp")
val socketDS: ReceiverInputDStream[String] = ssc.socketTextStream("localhost", 8888)
val wordDS: DStream[String] = socketDS.flatMap(_.split(" "))
val wordToOneDS: DStream[(String, Int)] = wordDS.map((_, 1))
wordToOneDS.updateStateByKey(
(seq : Seq[Int] , buffer : Option[Int]) => {
Option(seq.sum + buffer.getOrElse(0))
}
).print()
ssc
})
//SparkStreaming (reduceByKey) + Redis内存数据库 = 有状态
outerSSC.start()
outerSSC.awaitTermination()
}
1>窗口范围大于滑动的幅度, 存在重复数据
2>窗口范围小于滑动的幅度, 存在丢失数据的可能



