文章目录

  • 前言
  • 概述
  • Spark和MR的数据处理流程对比
  • Spark的组成示意图
  • Spark模块
  • Spark特点
  • Spark的运行模式
  • Spark官方测试案例
  • SparkWebUI
  • Spark通用运行简易流程
  • Spark核心概念
  • RDD特点
  • WordCount案例
  • 数据分区
  • 算子
  • 转换算子
  • 行动算子
  • 序列化
  • 血缘关系:
  • RDD的持久化和检查点:
  • RDD的分区器:
  • 文件数据的读取和存储
  • 广播变量:
  • 累加器:
  • 自定义累加器:
  • 案例:练习: 计算每个省份广告点击量的TopN
  • SparkSQL
  • RDD和DataFrame的交互:
  • DataSet
  • RDD、DataFrame、DataSet转换关系图
  • SparkSql自定义函数
  • SparkSql的文件读取
  • Spark整合外部Hive
  • SparkStreaming(流式处理)
  • SparkStreaming架构
  • SparkStreaming WordCount案例
  • DStream创建
  • 自定义数据源:
  • Kafka数据源
  • DStream的转换:
  • SparkStreaming的window操作
  • DStream的输出:
  • SparkStreaming中累加器和广播变量
  • GitHub:
  • DF常用操作:
  • 总结



前言



Spark作为一代比较成熟的大数据计算引擎,在其大数据领域占据一席之地,Spark应该是每个在大数据领域奋斗的人必不可少的技能。
本文分享本菜鸟的Spark学习笔记,笔记中偏实操性的内容比较多,原理性的东西比较少,对新手来说可能有点不大友好。整理的东西相对来说有点多,显得有些乱,请谅解!
笨鸟先飞,熟能生巧。
比心心 ~


提示:以下是本篇文章正文内容,下面案例可供参考

概述

Spark在多任务之间基于内存处理。
计算单元:RDD
Spark相较于MapReduce提供了丰富的数据处理模型,更方便在并行场合下使用

Spark和MR的数据处理流程对比

spark实训总结范文 spark实训个人总结_大数据


Spark的组成示意图

spark实训总结范文 spark实训个人总结_序列化_02


Spark模块

spark实训总结范文 spark实训个人总结_scala_03


Spark特点

快速、易用、通用、兼容性

Spark的运行模式

Local
Standalone
Yarn  yarn-client yarn-cluster(生产环境)
Mesos

bin/spark-submit \
--class org.apache.spark.examples.SparkPi \
--master yarn \
--deploy-mode client \
./examples/jars/spark-examples_2.11-2.1.1.jar 100

Spark官方测试案例

bin/spark-submit \
--class org.apache.spark.examples.SparkPi \
--master local[2] \
./examples/jars/spark-examples_2.11-2.1.1.jar 100

--master  运行环境  

local  一个线程
local[K]  k个线程
local[*]  CPU核数线程

SparkWebUI

http://hadoop201:4040

Spark通用运行简易流程

spark实训总结范文 spark实训个人总结_大数据_04


Spark核心概念

Master
Worker
DriverProgram
Executor
RDDs
ClusterManagers
DeployMode  运行模式
Task:任务。每一个分区分配一个Task
Job:一个job对应一个action
Stage:阶段,上一阶段结束才能开始下阶段;Stage由Task组成
DAG: 有向无环图

RDD特点

弹性
分区
只读
依赖
缓存
checkpoint

WordCount案例

导入pom文件:
<dependencies>    
	<dependency>       
		<groupId>org.apache.spark</groupId>        
		<artifactId>spark-core_2.11</artifactId>        
		<version>2.1.1</version>    
	</dependency>
</dependencies>

<build>    
	<plugins>        <!-- 打包插件, 否则 scala 类不会编译并打包进去 -->        
		<plugin>            
			<groupId>net.alchim31.maven</groupId>           
			 <artifactId>scala-maven-plugin</artifactId>            
			 <version>3.4.6</version>            
			 <executions>                
				 <execution>                    
					 <goals>                        
						 <goal>compile</goal>                        
						 <goal>testCompile</goal>                   
					 </goals>                
				 </execution>           
			 </executions>        
		</plugin>    
	</plugins>
</build>

object WordCount {
  def main(args: Array[String]): Unit = {
    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
    val sc: SparkContext = new SparkContext(conf)

    sc.textFile("input")
      .flatMap(_.split(" "))
      .map((_,1))
      .reduceByKey(_ + _)
      .collect()
      .foreach(println)
    sc.stop()
  }
}

数据分区

//在内存中创建RDD,分区数默认为当前环境cores(cpu核数)的值。
val inputRDD: RDD[Int] = sc.parallelize(list)
val inputRDD: RDD[Int] = sc.makeRDD(list)
//也可以通过第二个参数指定分区数。
val inputRDD: RDD[Int] = sc.makeRDD(list,2)

//通过文件创建RDD,分区数默认为当前环境cores(cpu核数)和 2 的最小值
val inputRDD: RDD[String] = sc.textFile("input")
//也可以通过第二个参数来设置最小分区数,但是实际上根据源码中的算法得出最终的分区数
// 最小分区数只会对最终分区数有一定的影响
val inputRDD: RDD[String] = sc.textFile("input",3)

算子

https://www.cnblogs.com/kpsmile/p/10434390.html

转换算子

单Value类型:
    map()
    mapPartitions() 
    注意:以下形式效率更高,一次拿取一个partition中的数据,然后内存里做map
        缺点:有可能会造成内存溢出
        inputRDD.mapPartitions(datas=>{
              datas.map(_ * 2)
        })
     mapPartitionsWithIndex()
    flatMap()
    glom()    // 将每个分区封装为数组集合
    groupBy()
    filter()
    sample()  //随机取样。 参数1:是否放回 2:抽样比较熟知  3:随机数种子,一般默认或当前的时间
    distinct()  //去重, 参数可以改变分区
    coalesce() //缩减分区, 第二个参数决定是否shuffle; 
    repartition() // 重分区, 一定会shuffle, 底层调用的就是coalesce()
    sortBy() // Tuple类型默认先比较第一个,然后第二个...; 参数可以决定升序还是降序
    pipe()   //管道,针对每一个分区,把RDD中的数据通过管道传递给shell命令或脚本。每个分区执行一次这个命令
    
 双Value类型:
    union()  //并集
    subtract()  // 交集
    cartesian()  // 笛卡尔积
    zip()   // 拉链。 注意:两个RDD的数量和分区数都必须相同
    
Key-Value类型:
    //RDD本身并不提供对key-value类型数据的操作,而是需要进行隐式转换,转换成PairRDDFunctions进行操作。
    partitionBy()  //根据指定的规则进行重分区; 参数需要传入一个Partitioner
    reduceByKey()  // 相较与groupByKey会对数据分区内进行提前聚合(shuffle前),效率更高
                    // 需要分区内和分区间的计算规则一样
    groupByKey()
    aggregateByKey()() //聚合。 两个参数列表, 1:zeroValue初始值  2:分区内计算规则,分区间计算规则
    foldByKey()()  //聚合。 两个参数列表: 当分区内和分区间的计算规则一样时,可以使用
    combineByKey()  // 三个参数:1. 将分区内第一个值进行结构的转换 2.分区内计算规则 3.分区间计算规则 
                    // 由于无法推断出类型,计算规则中需要标明数据类型。
    sortByKey()
    mapValues()  // 只对value'进行处理,返回值中含有key
    join() // 相同的key进行连接: (k,v) (k,w) => (k,(v,w))
        leftOuterJoin()
        rightOuterJoin()
        fullOuterJoin()
    cogroup()  //先在自己集合上做一个聚合,然后再和另一个集合聚合

行动算子

reduce()
collect()
count()
take()
first()
takeOrdered()
aggregate()()    // 注意:分区间的计算也会使用zeroValue
saveAsTextFile()
saveAsSequenceFile()
saveAsObjectFile()
countByKey()
countByValue()
foreach()  // 会将数据发送到Executor

序列化

涉及到Driver和Executor之间对象传递的类都需要实现序列化
class User extends Serializable    // 闭包检测
样例类默认序列化

Kryo序列化:
    java序列化太大;Kryo更简洁。Spark2.0内部开始使用Kryo。
    使用Kryo也要继承Serializable。
     
使用:需要在SparkConf中声明序列化方式和类; 类也要继承Serializable。
val conf: SparkConf = new SparkConf()            
.setAppName("SerDemo")           
.setMaster("local[*]")           
// 替换默认的序列化机制            
.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")            
// 注册需要使用 kryo 序列化的自定义类            .
.registerKryoClasses(Array(classOf[Searcher]))    
    
Driver: 算子之外运行位置
Executor; 算子运行位置

血缘关系:

rdd.toDebugString   // 查看血缘关系
rdd.dependencies    // 查看依赖关系

// 判断依据:数据是否打乱重组
窄依赖:oneToOne
宽依赖:oneToSome   // shuffle 一定是宽依赖

RDD的持久化和检查点:

cache()  //  底层 peresist() ,确定存储的级别; 会增加血缘关系,不改变原有的血缘关系
        //聚合算子默认缓存

checkpoint()  // 可以将计算结果保存到检查点中长时间存储。
        // 检查点操作一般会重头执行一遍完整的流程,所以一般会和cache()联合使用。
        // cache方法和checkpoint方法没有关联性,可以随意放置
        // 检查点会切断血缘

RDD的分区器:

k-v类型才需要分区器,Spark目前支持Hash分区和Range分区。
指定的规则其实就是分区规则:分区器
也可以自定义分区器:
// 自定义分区器
// 1. 继承Partitioner
// 2. 重写方法
class MyPartitioner(num:Int) extends Partitioner {
    // 获取分区数量
    override def numPartitions: Int = {
        num
    }
    
    // 根据数据key获取所在的分区号码(从0开始)
    override def getPartition(key: Any): Int = {
        if ( key.isInstanceOf[String] ) {
           val keyString: String = key.asInstanceOf[String]
            if ( keyString == "cba" ) {
                0
            } else if ( keyString == "nba" ) {
                1
            } else {
                2
            }
        } else {
            2
        }
    }
}

文件数据的读取和存储

textFile(path)  // 读取Text文件
saveAsTextFile(path) //存储Text文件

import scala.util.parsing.json.JSON
rdd.map(JSON.parseFull)  // 解析json字符串;JSON字符串必须在一行读取Json文件
// 注意:按行读,一行中应该满足json格式
// 注意:每一行必须满足json格式,不然会返回None

sequenceFile[keyClass, valueClass](path) //读取文件
saveAsSequenceFile(path) //保存为sequenceFile文件

// 可以将对象序列化保存下来,采用java的序列化机制。就是objectFile
objectFile[(String,Int)](path)  //读取
saveAsObjectFile(path) //存储

// 从HDFS读写文件(集群执行不需要添加前缀)
// 注意:端口要与core-site.xml中设置的端口号一致
val inputRDD: RDD[String] = sc.textFile("hdfs://192.168.2.201:9000/hdfsTest/test1.txt")
inputRDD.saveAsTextFile("hdfs://192.168.2.201:9000/hdfsTest/test")

// 从MySQL数据读取文件
// 
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.27</version>
</dependency>

// 读
object JDBCDemo {
    def main(args: Array[String]): Unit = {
        val conf = new SparkConf().setAppName("Practice").setMaster("local[2]")
        val sc = new SparkContext(conf)
        //定义连接mysql的参数
        val driver = "com.mysql.jdbc.Driver"
        val url = "jdbc:mysql://hadoop201:3306/rdd"
        val userName = "root"
        val passWd = "aaa"

        val rdd = new JdbcRDD(
            sc,
            () => {
                Class.forName(driver)
                DriverManager.getConnection(url, userName, passWd)
            },
            "select id, name from user where id >= ? and id <= ?",
            1,
            20,
            2,
            result => (result.getInt(1), result.getString(2))
        )
        rdd.collect.foreach(println)
    }
}

// 写
object JDBCDemo2 {
    def main(args: Array[String]): Unit = {
        val conf = new SparkConf().setAppName("Practice").setMaster("local[2]")
        val sc = new SparkContext(conf)
        //定义连接mysql的参数
        val driver = "com.mysql.jdbc.Driver"
        val url = "jdbc:mysql://hadoop201:3306/rdd"
        val userName = "root"
        val passWd = "aaa"

        val rdd: RDD[(Int, String)] = sc.parallelize(Array((110, "police"), (119, "fire")))
        // 对每个分区执行 参数函数
        rdd.foreachPartition(it => {
            Class.forName(driver)
            val conn: Connection = DriverManager.getConnection(url, userName, passWd)
            it.foreach(x => {
                val statement: PreparedStatement = conn.prepareStatement("insert into user values(?, ?)")
                statement.setInt(1, x._1)
                statement.setString(2, x._2)
                statement.executeUpdate()
            })
        })
    }
}

//  从 Hbase 读写文件
<dependency>
    <groupId>org.apache.hbase</groupId>
    <artifactId>hbase-server</artifactId>
    <version>1.3.1</version>
</dependency>

<dependency>
    <groupId>org.apache.hbase</groupId>
    <artifactId>hbase-client</artifactId>
    <version>1.3.1</version>
</dependency>

// 读
object HBaseDemo {
    def main(args: Array[String]): Unit = {
        val conf = new SparkConf().setAppName("Practice").setMaster("local[2]")
        val sc = new SparkContext(conf)

        val hbaseConf: Configuration = HBaseConfiguration.create()
        hbaseConf.set("hbase.zookeeper.quorum", "hadoop201,hadoop202,hadoop203")
        hbaseConf.set(TableInputFormat.INPUT_TABLE, "student")

        val rdd: RDD[(ImmutableBytesWritable, Result)] = sc.newAPIHadoopRDD(
            hbaseConf,
            classOf[TableInputFormat],
            classOf[ImmutableBytesWritable],
            classOf[Result])

        val rdd2: RDD[String] = rdd.map {
            case (_, result) => Bytes.toString(result.getRow)
        }
        rdd2.collect.foreach(println)
        sc.stop()
    }
}

// 写
object HBaseDemo2 {
    def main(args: Array[String]): Unit = {
        val conf = new SparkConf().setAppName("Practice").setMaster("local[2]")
        val sc = new SparkContext(conf)

        val hbaseConf = HBaseConfiguration.create()
        hbaseConf.set("hbase.zookeeper.quorum", "hadoop201,hadoop202,hadoop203")
        hbaseConf.set(TableOutputFormat.OUTPUT_TABLE, "student")
        // 通过job来设置输出的格式的类
        val job = Job.getInstance(hbaseConf)
        job.setOutputFormatClass(classOf[TableOutputFormat[ImmutableBytesWritable]])
        job.setOutputKeyClass(classOf[ImmutableBytesWritable])
        job.setOutputValueClass(classOf[Put])

        val initialRDD = sc.parallelize(List(("100", "apple", "11"), ("200", "banana", "12"), ("300", "pear", "13")))
        val hbaseRDD = initialRDD.map(x => {
            val put = new Put(Bytes.toBytes(x._1))
            put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name"), Bytes.toBytes(x._2))
            put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("weight"), Bytes.toBytes(x._3))
            (new ImmutableBytesWritable(), put)
        })
        hbaseRDD.saveAsNewAPIHadoopDataset(job.getConfiguration)
    }
}

广播变量:

分布式共享只读变量
https://blog.csdn.net/Android_xue/article/details/79780463/

简单案例:
object BroadcastTest {
  def main(args: Array[String]): Unit = {
    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("BroadcastTest")
    val sc: SparkContext = new SparkContext(conf)

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

    val listBroadcast: Broadcast[List[(Int, Int)]] = sc.broadcast(list)

    inputRDD.map{
      case (k1, v1) =>{
        var v2:Int = 0
        for (elem <- listBroadcast.value) {
          if(elem._1 == k1){
            v2 = elem._2
          }
        }
        (k1,(v1,v2))
      }
    }.collect().foreach(println)
    sc.stop()
  }
}

累加器:

分布式共享只写变量
(只写:不同的Executor写入的值不能读)
https://blog.csdn.net/Android_xue/article/details/79780463/

自带的累加器:longAccumulator、doubleAccumulator、collectionAccumulator

简单案例:
object AccumulatorTest {
  def main(args: Array[String]): Unit = {
    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("AccumulatorTest")
    val sc: SparkContext = new SparkContext(conf)

    val inputRDD: RDD[(String, Int)] = sc.makeRDD(List(("a", 1), ("a", 2), ("a", 3), ("a", 4), ("a", 5)))

    val sum: LongAccumulator = sc.longAccumulator("sum")

    inputRDD.foreach{
      case (word, count)=>{
        sum.add(count)
      }
    }
    println(sum.value)
    sc.stop()
  }
}

自定义累加器:

class MyAccumulate extends AccumulatorV2[String, mutable.Map[String, Int]]{

  private var resultMap = mutable.Map[String, Int]()

  // 是否为空
  override def isZero: Boolean = {
    resultMap.isEmpty
  }

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

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

  // 添加
  override def add(v: String): Unit = {
    resultMap(v) = resultMap.getOrElse(v, 0) + 1
  }

  // 合并
  override def merge(other: AccumulatorV2[String, mutable.Map[String, Int]]): Unit = {
    resultMap = resultMap.foldLeft(other.value)(
      (innerMap, kv) => {
        innerMap(kv._1) =  innerMap.getOrElse(kv._1, 0) + kv._2
        innerMap
      }
    )
  }

  // 获取value
  override def value: mutable.Map[String, Int] = resultMap
}

案例:练习: 计算每个省份广告点击量的TopN

/**
 *  练习: 计算每个省份广告点击量的TopN
 *  数据模型 :1516609143867 6 7 64 16
 *            时间戳  省份  城市  用户  广告
 */
object SparkExerDemo {
  def main(args: Array[String]): Unit = {
    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkExerDemo")
    val sc: SparkContext = new SparkContext(conf)

    // 1.读取文件
    val inputRDD: RDD[String] = sc.textFile("WordCountTest/src/main/resources/agent.log")

    // 2.对数据进行转型 (省份-广告,1)
    val mapRDD: RDD[(String, Int)] = inputRDD.map(line => {
      val words: Array[String] = line.split(" ")
      (words(1) + "_" + words(4), 1)
    })

    // 3.聚合,求得每个省份每个广告的和 (省份-广告,sum)
    val adsAndSumRDD: RDD[(String, Int)] = mapRDD.reduceByKey(_ + _)

    // 4. 转型: (省份-广告,sum)=> (省份,(广告,sum))
    // map 和case一起使用,模式匹配;
    val adsAndCitySumRDD: RDD[(String, (String, Int))] = adsAndSumRDD.map {
      // (ads, sum) 会报错;推断错误
      case (ads, sum) => {
        val words: Array[String] = ads.split("_")
        (words(0), (words(1), sum))
      }
    }

    // 5.对数据进行分组
    val resultRDD: RDD[(String, Iterable[(String, Int)])] = adsAndCitySumRDD.groupByKey()

    // 6.对同组的数据进行排序取前三
    val finalRDD: RDD[(String, List[(String, Int)])] = resultRDD.mapValues(iter => {
      iter.toList.sortWith(
        (left, right) => {
          left._2 > right._2
        }
      ).take(3)
    })

    finalRDD.collect().foreach(println)

    sc.stop()
  }
}

SparkSQL

Spark SQL能够将Spark SQL转换成RDD,然后执行,执行效率非常快

提供了两个编程抽象:
DataFrame
DataSet

Spark SQL可以清楚的知道该数据集中包含哪些列,每列的名称各是类型各是什么
RDD不知道

Spark SQL具有查询优化器,可以对sql进行优化,得到高效的执行流程

DataFrame是DataSet的特列,DataFrame=DataSet[Row]

SparkSession内部封装了SparkContext

创建DataFrame:
1.通过Spark的数据源创建
2.通过已知的RDD来创建
3.通过查询一个HIVE表来创建

DataFrame语法风格:
SQL:	spark.sql("select *  from people")
DSL:	(面向对象)  不需要创建临时视图,可以直接进行一些查询操作

注意:
临时视图只能在当前session有效,在新的Session中无效
可以创建全局视图,访问全局视图需要全路径:  global_tmp.xxx

pom.xml
<dependency>    
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-sql_2.11</artifactId>
    <version>2.1.1</version>
</dependency>


SQL:
------------------------------------------------------ 
//读取json文件
val df = spark.read.json("/opt/module/spark-local/examples/src/main/resources/employees.json")

//展示结果
df.show

//创建临时表
df.createOrReplaceTempView("people")

//sql查询,并展示
spark.sql("select * from people").show

//创建全局临时视图
df.createGlobalTempView("people")

//简历新的Session去执行sql操作
spark.newSession.sql("select * from global_temp.people")
--------------------------------------------------
DSL:

//查看Schema信息
df.printSchema

//查询等操作
df.select($"name").show
df.select("name").show
df.select("name", "age").show
df.select($"name", $"age" + 1).show
df.filter($"age" > 21).show
df.groupBy("age").count.show

注意:设计到运算的时候, 每列都必须使用$

RDD和DataFrame的交互:

涉及到RDD,DataFrame,DataSet之间的操作时,需要导入import spark.implicits._
注意:上一行的spark不是包名,.而是表示SparkSession的那个对象,所以必须先创建SparkSession对象再导入
    
//rdd=>DataFormat
rdd2.toDF("name", "age").show

//通过样例类反射转换
case class People(name :String, age: Int)
val rdd2 = rdd1.map(line => { val paras = line.split(", "); People(paras(0), paras(1).toInt) })

//通过API的方式转换    ||  rdd=>DataFormat  rdd.toDF("id","name")  ||Dataframe => rdd  frame.rdd
    就是说rdd,编程DataFrame需要提供结构,frame到rdd,去除结构
 // 创建Spark环境配置对象
        val conf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL02_DataFrame01")
        
        // 创建上下文环境对象
        val spark = SparkSession.builder().config(conf).getOrCreate()
        
        // 导入隐式转换规则 : A -> B
        // 这里的spark不是包名,是上下文环境的对象名称
        import spark.implicits._
        
       //  从RDD转换为DataFrame
        val dataRDD: RDD[(Int, String, Int)] = spark.sparkContext.makeRDD(List((1, "zhangsan", 30), (2, "lisi", 40), (3, "wangwu", 50)))
        
        // 给RDD数据增加结构信息
        val dataDF: DataFrame = dataRDD.toDF("id", "name", "age")
    
        //dataDF.show()
        // 将DataFrame转换为RDD,得到的数据类型是ROW
        val rdd: RDD[Row] = dataDF.rdd
        
        //rdd.foreach(row=>row.get)
        
        // 释放资源
        spark.close()

DataSet

DataSet:强类型,既需要知道数据的结构,又要知道数据的类型
//使用样例类得到DataSet
case class Person(name: String, age: Int)
val ds = Seq(Person("lisi", 20), Person("zs", 21)).toDS
//使用基本类型的序列得到DataSet
val ds = Seq(1,2,3,4,5,6).toDS

注意:实际使用很少把序列转换成DataSet,更多的是通过RDD来得到DataSet

=========================================
RDD和DataSet的交互:
    使用反射来推断包含特定类对象的RDD的schema
    样例类定义了表结构:样例类参数名通过反射被读到,然后成为列名
    case class Person(name: String, age: Long)
    peopleRDD.map(line => {val para = line.split(",");Person(para(0),para(1).trim.toInt)}).toDS

从DataSet到RDD:
    val ds = Seq(Person("lisi", 40), Person("zs", 20)).toDS
    val rdd = ds.rdd
    
================================================
DataFrame和DataSet之间的交互:
    DataFrame=>DataSet   提供数据类型
        val df = spark.read.json("examples/src/main/resources/people.json")
        case class People(name: String, age: Long)
        val ds = df.as[People]

    DataSet=>DataFrame
        val df = ds.toDF

============================================
RDD,DataFrame,DataSet共性:
    惰性机制
    注意import spark.implicits._ 的使用
    
===================================
区别:
    DataFrame每一行的数据固定为ROW
    DataFrame是DataSet的特例

RDD、DataFrame、DataSet转换关系图

spark实训总结范文 spark实训个人总结_大数据_05


SparkSql自定义函数

自定义UDF函数:
spark.udf.register("toUpper", (str:String) => {
  str.toUpperCase
})

一,自定义UDAF类:
    // 自定义聚合函数
// 1. 继承UserDefinedAggregateFunction
// 2. 重写 方法
class AvgAgeUDAF extends UserDefinedAggregateFunction{
    
    // 输入数据的结构信息
    override def inputSchema: StructType = {
        StructType(Array( StructField("age", LongType) ))
    }
    
    // 缓冲区的数据结构信息
    override def bufferSchema: StructType = {
        StructType(Array( StructField("sum", LongType), StructField("count", LongType) ))
    }
    
    // 计算结果的类型
    override def dataType: DataType = DoubleType
    
    // 稳定性
    override def deterministic: Boolean = true
    
    // 初始化
    override def initialize(buffer: MutableAggregationBuffer): Unit = {
        buffer(0) = 0L
        buffer(1) = 0L
    }
    
    // 更新缓冲区的数据
    override def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
        buffer(0) = buffer.getLong(0) + input.getLong(0)
        buffer(1) = buffer.getLong(1) + 1L
    }
    
    // 合并缓冲区
    override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
        buffer1(0) = buffer1.getLong(0) + buffer2.getLong(0)
        buffer1(1) = buffer1.getLong(1) + buffer2.getLong(1)
    }
    
    // 计算结果
    override def evaluate(buffer: Row): Any = {
        buffer.getLong(0).toDouble / buffer.getLong(1)
    }
}

主类中使用方法:
      // 创建自定义聚合函数
        val udaf = new AvgAgeUDAF()
        // 注册自定义函数
        spark.udf.register("avgAge", udaf)
          // 应用聚合函数
        spark.sql("select avgAge(age) from user").show()
        
二,自定义UDAF类
case class UserX( username:String, age:Long )
case class AvgBuffer( var sum:Long, var count:Long )

// 自定义聚合函数 (强类型)0
// 1. 继承Aggregator
// 2. 重写 方法
class AvgAgeUDAFClass extends Aggregator[UserX, AvgBuffer, Double]{
    override def zero: AvgBuffer = {
        AvgBuffer(0L,0L)
    }
    
    override def reduce(buff: AvgBuffer, in: UserX): AvgBuffer = {
        buff.sum = buff.sum + in.age
        buff.count = buff.count + 1L
        buff
    }
    
    override def merge(buff1: AvgBuffer, buff2: AvgBuffer): AvgBuffer = {
        buff1.sum = buff1.sum + buff2.sum
        buff1.count = buff1.count + buff2.count
        buff1
    }
    
    override def finish(buff: AvgBuffer): Double = {
        buff.sum.toDouble / buff.count
    }
    
    override def bufferEncoder: Encoder[AvgBuffer] = Encoders.product
    
    override def outputEncoder: Encoder[Double] = Encoders.scalaDouble
}

使用方法:
     // 创建自定义聚合函数
        val udaf = new AvgAgeUDAFClass()
        // 将聚合函数当成查询的列
        val column: TypedColumn[UserX, Double] = udaf.toColumn

        // 用户自定义聚合函数 : UDAF
        val df: DataFrame = spark.read.json("input/user.json")
        val ds: Dataset[UserX] = df.as[UserX]
        
        // 采用DSL语法
        ds.select(column).show()

        // 释放资源
        spark.close()

SparkSql的文件读取

默认的数据源是parquet
通用加载:spark.read.load
保存数据:df.write.save

al peopleDF = spark.read.format("json").load("examples/src/main/resources/people.json")
peopleDF.select("name", "age").write.format("parquet").save("namesAndAges.parquet")

// 通用的加载功能 : 默认加载的数据格式为:parquet
        val df: DataFrame = spark.read.format("json").load("input/user.json")
 // 通用的加载功能 : 默认加载的数据格式为:parquet
        val df: DataFrame = spark.read.format("json").load("input/user.json")
        // spark.read.load
        // spark.write.save
        // 通用的保存功能 : 默认保存的数据格式为:parquet
        df.write.mode("overwrite").format("json").save("output")
        
// 访问JDBC
        val jdbcDF = spark.read
            .format("jdbc")
            .option("url", "jdbc:mysql://linux1:3306/rdd")
            .option("user", "root")
            .option("password", "000000")
            .option("dbtable", "user")
            .load()
    
//jdbcDF.show()
        jdbcDF.write
            .format("jdbc")
            //.mode("append")
            .option("url", "jdbc:mysql://linux1:3306/rdd")
            .option("user", "root")
            .option("password", "000000")
            .option("dbtable", "user3")
            .save()
对于JDBC数据源的读写:
读:
import org.apache.spark.sql.SparkSession

object JDBCDemo {
    def main(args: Array[String]): Unit = {
        val spark: SparkSession = SparkSession
            .builder()
            .master("local[*]")
            .appName("Test")
            .getOrCreate()
        import spark.implicits._

        val jdbcDF = spark.read
            .format("jdbc")
            .option("url", "jdbc:mysql://hadoop201:3306/rdd")
            .option("user", "root")
            .option("password", "aaa")
            .option("dbtable", "user")
            .load()
        jdbcDF.show
    }
}
写:
    object JDBCDemo3 {


    def main(args: Array[String]): Unit = {
        val spark: SparkSession = SparkSession
            .builder()
            .master("local[*]")
            .appName("Test")
            .getOrCreate()
        import spark.implicits._
        val rdd: RDD[User1] = spark.sparkContext.parallelize(Array(User1("lisi", 20), User1("zs", 30)))
        val ds: Dataset[User1] = rdd.toDS
        ds.write
            .format("jdbc")
            .option("url", "jdbc:mysql://hadoop201:3306/rdd")
            .option("user", "root")
            .option("password", "aaa")
            .option("dbtable", "user")
            .mode(SaveMode.Append)
            .save()
        val props: Properties = new Properties()
        props.setProperty("user", "root")
        props.setProperty("password", "aaa")
        ds.write.mode(SaveMode.Append).jdbc("jdbc:mysql://hadoop201:3306/rdd", "user", props)
    }
}

case class User1(name: String, age: Long)

spark实训总结范文 spark实训个人总结_scala_06


Spark整合外部Hive

Spark 要接管 Hive 需要把 hive-site.xml copy 到conf/目录下.
把 Mysql 的驱动 copy 到 jars/目录下.
如果访问不到hdfs, 则需要把core-site.xml和hdfs-site.xml 拷贝到conf/目录下.

代码中:
  拷贝 hive-site.xml 到 resources 目录下。
pom.xml:
<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-hive_2.11</artifactId>
    <version>2.1.1</version>
</dependency>

val spark: SparkSession = SparkSession
  .builder()
  .master("local[2]")
  .appName("AreaClickApp")
  .enableHiveSupport()
  .getOrCreate()

SparkStreaming(流式处理)

spark实训总结范文 spark实训个人总结_scala_07


注意:

spark Streaming中,处理数据的单位是一批而不是单条,数据采集却是逐条进行的

批处理间隔

DStream:
连续的数据流
在内部是由一个RDD序列来表示的


SparkStreaming架构

spark实训总结范文 spark实训个人总结_spark_08


SparkStreaming WordCount案例

pom.xml文件:
<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-streaming_2.11</artifactId>
    <version>2.1.1</version>
</dependency>

优雅的关闭:
ssc.start()
ssc.awaitTermination()

pom.xml文件:
<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-streaming_2.11</artifactId>
    <version>2.1.1</version>
</dependency>

优雅的关闭:
ssc.start()
ssc.awaitTermination()

WordCount:
object StreamingWordCount {
  def main(args: Array[String]): Unit = {
    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("StreamingWordCount")
    val ssc = new StreamingContext(conf, Seconds(3))

    val inputStream: ReceiverInputDStream[String] = ssc.socketTextStream("127.0.0.1", 9999)
    val flatMapRDD: DStream[String] = inputStream.flatMap(
      line => {
        val words: Array[String] = line.split(" ")
        words
      }
    )
    val mapRDD: DStream[(String, Int)] = flatMapRDD.map((_, 1))
    val resultRDD: DStream[(String, Int)] = mapRDD.reduceByKey(_ + _)
    resultRDD.print()

    ssc.start()
    ssc.awaitTermination()
  }
}

自定义数据源:
class MySource(host: String, port: Int) extends Receiver[String](StorageLevel.MEMORY_ONLY) {
  override def onStart(): Unit = {
    // 启动一个新的线程来接收数据
    new Thread("Socket Receiver"){
      override def run(): Unit = {
        receive()
      }
    }.start()
  }

  override def onStop(): Unit = {

  }

  def receive() = {
    val socket = new Socket(host, port)
    val reader = new BufferedReader(
      new InputStreamReader(socket.getInputStream, StandardCharsets.UTF_8)
    )
    var line:String = reader.readLine()
    while (!isStopped() && line != null){
      store(line)
      line = reader.readLine()
    }
    reader.close()
    socket.close()

    restart("Trying to connect again")
  }
}



注意事项:
1.一旦StreamingContext已经启动,则不能在添加新的streamingcomputations
2.一旦一个StreamingContext已经停止,他也就不能在重启
3.在一个JVM内,同一时间智能启动一个StreamingContext
4.stop()的方式停止StreamingContext,也会把SparkContext停掉.如果仅仅想停止StreamingContext,则应该这样:stop(false)
5.一个SparkContext可以重用去创建StreamingContext,前提是以前的StreamingContext已经停掉,并且SparkContext没有被停掉

DStream创建

可以使用ssc.queueStream(queueOfRDDs)来创建DStream,每一个推送到这个队列中的RDD,都会作为一个DStream处理

object SparkStreamingTest2 {
  def main(args: Array[String]): Unit = {
    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Test")
    val scc: StreamingContext = new StreamingContext(conf, Seconds(5))
    val sc: SparkContext = scc.sparkContext
    
    //创建一个可变队列
    val queue: mutable.Queue[RDD[Int]] = mutable.Queue[RDD[Int]]()
    val rddDS: InputDStream[Int] = scc.queueStream(queue, true)
    rddDS.reduce(_+_).print()
    scc.start()
    
    //循环的方式向队列中添加RDD
    for (elem <- 1 to 5){
      queue += sc.parallelize(1 to 100)
      Thread.sleep(2000)
    }

    scc.awaitTermination()
  }
}

自定义数据源:

其实就是自定义接收器
需要继承Receiver,并实现onStart,onStop方法来自定义数据源采集

需求:自定义数据源,实现监控某个款口号,获取该端口号内容
自定义数据源
object MySource{
    def apply(host: String, port: Int): MySource = new MySource(host, port)
}

class MySource(host: String, port: Int) extends Receiver[String](StorageLevel.MEMORY_ONLY){
    /*
    接收器启动的时候调用该方法. This function must initialize all resources (threads, buffers, etc.) necessary for receiving data.
    这个函数内部必须初始化一些读取数据必须的资源
    该方法不能阻塞, 所以 读取数据要在一个新的线程中进行.
     */
    override def onStart(): Unit = {

        // 启动一个新的线程来接收数据
        new Thread("Socket Receiver"){
            override def run(): Unit = {
                receive()
            }
        }.start()
    }
    // 此方法用来接收数据
    def receive()={
        val socket = new Socket(host, port)
        val reader = new BufferedReader(new InputStreamReader(socket.getInputStream, StandardCharsets.UTF_8))
        var line: String = null
        // 当 receiver没有关闭, 且reader读取到了数据则循环发送给spark
        while (!isStopped && (line = reader.readLine()) != null ){
            // 发送给spark
            store(line)
        }
        // 循环结束, 则关闭资源
        reader.close()
        socket.close()

        // 重启任务
        restart("Trying to connect again")
    }
    override def onStop(): Unit = {
    }
}


object MySourceDemo {
    def main(args: Array[String]): Unit = {
        val conf = new SparkConf().setAppName("StreamingWordCount").setMaster("local[*]")
        // 1. 创建SparkStreaming的入口对象: StreamingContext  参数2: 表示事件间隔
        val ssc = new StreamingContext(conf, Seconds(5))
        // 2. 创建一个DStream
        val lines: ReceiverInputDStream[String] = ssc.receiverStream[String](MySource("hadoop201", 9999))
        // 3. 一个个的单词
        val words: DStream[String] = lines.flatMap(_.split("""\s+"""))
        // 4. 单词形成元组
        val wordAndOne: DStream[(String, Int)] = words.map((_, 1))
        // 5. 统计单词的个数
        val count: DStream[(String, Int)] = wordAndOne.reduceByKey(_ + _)
        //6. 显示
        count.print
        //7. 启动流式任务开始计算
        ssc.start()
        //8. 等待计算结束才退出主程序
        ssc.awaitTermination()
        ssc.stop(false)
    }
}


nc -lk 9999  进行测试

Kafka数据源

高级Api和低级Api的区别: 低级Api对于Offset的操作更加灵活。

pom.xml
<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-streaming-kafka-0-8_2.11</artifactId>
    <version>2.1.1</version>
</dependency>


object HighKafka {
    def main(args: Array[String]): Unit = {
        val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("HighKafka")
        val ssc = new StreamingContext(conf, Seconds(3))
       
        // kafka 参数
        //kafka参数声明
        val brokers = "hadoop201:9092,hadoop202:9092,hadoop203:9092"
        val topic = "first"
        val group = "bigdata"
        val deserialization = "org.apache.kafka.common.serialization.StringDeserializer"
        val kafkaParams = Map(
            ConsumerConfig.GROUP_ID_CONFIG -> group,
            ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> brokers,
            ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG -> deserialization,
            ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG -> deserialization
        )
        val dStream = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder](
            ssc, kafkaParams, Set(topic))
       
        dStream.print()
       
        ssc.start()
        ssc.awaitTermination()
    }
}


object HighKafka2 {
   
    def createSSC(): StreamingContext = {
        val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("HighKafka")
        val ssc = new StreamingContext(conf, Seconds(3))
        // 偏移量保存在 checkpoint 中, 可以从上次的位置接着消费
        ssc.checkpoint("./ck1")
        // kafka 参数
        //kafka参数声明
        val brokers = "hadoop201:9092,hadoop202:9092,hadoop203:9092"
        val topic = "first"
        val group = "bigdata"
        val deserialization = "org.apache.kafka.common.serialization.StringDeserializer"
        val kafkaParams = Map(
            "zookeeper.connect" -> "hadoop201:2181,hadoop202:2181,hadoop203:2181",
            ConsumerConfig.GROUP_ID_CONFIG -> group,
            ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> brokers,
            ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG -> deserialization,
            ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG -> deserialization
        )
        val dStream = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder](
            ssc, kafkaParams, Set(topic))
       
        dStream.print()
        ssc
    }
   
    def main(args: Array[String]): Unit = {
       
        val ssc: StreamingContext = StreamingContext.getActiveOrCreate("./ck1", () => createSSC())
        ssc.start()
        ssc.awaitTermination()
    }
}


低级API:
    object LowKafka {
    // 获取 offset
    def getOffset(kafkaCluster: KafkaCluster, group: String, topic: String): Map[TopicAndPartition, Long] = {
        // 最终要返回的 Map
        var topicAndPartition2Long: Map[TopicAndPartition, Long] = Map[TopicAndPartition, Long]()
       
        // 根据指定的主体获取分区信息
        val topicMetadataEither: Either[Err, Set[TopicAndPartition]] = kafkaCluster.getPartitions(Set(topic))
        // 判断分区是否存在
        if (topicMetadataEither.isRight) {
            // 不为空, 则取出分区信息
            val topicAndPartitions: Set[TopicAndPartition] = topicMetadataEither.right.get
            // 获取消费消费数据的进度
            val topicAndPartition2LongEither: Either[Err, Map[TopicAndPartition, Long]] =
                kafkaCluster.getConsumerOffsets(group, topicAndPartitions)
            // 如果没有消费进度, 表示第一次消费
            if (topicAndPartition2LongEither.isLeft) {
                // 遍历每个分区, 都从 0 开始消费
                topicAndPartitions.foreach {
                    topicAndPartition => topicAndPartition2Long = topicAndPartition2Long + (topicAndPartition -> 0)
                }
            } else { // 如果分区有消费进度
                // 取出消费进度
                val current: Map[TopicAndPartition, Long] = topicAndPartition2LongEither.right.get
                topicAndPartition2Long ++= current
            }
        }
        // 返回分区的消费进度
        topicAndPartition2Long
    }
   
    // 保存消费信息
    def saveOffset(kafkaCluster: KafkaCluster, group: String, dStream: InputDStream[String]) = {
       
        dStream.foreachRDD(rdd => {
            var map: Map[TopicAndPartition, Long] = Map[TopicAndPartition, Long]()
            // 把 RDD 转换成HasOffsetRanges对
            val hasOffsetRangs: HasOffsetRanges = rdd.asInstanceOf[HasOffsetRanges]
            // 得到 offsetRangs
            val ranges: Array[OffsetRange] = hasOffsetRangs.offsetRanges
            ranges.foreach(range => {
                // 每个分区的最新的 offset
                map += range.topicAndPartition() -> range.untilOffset
            })
            kafkaCluster.setConsumerOffsets(group,map)
        })
    }
   
    def main(args: Array[String]): Unit = {
        val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("HighKafka")
        val ssc = new StreamingContext(conf, Seconds(3))
        // kafka 参数
        //kafka参数声明
        val brokers = "hadoop201:9092,hadoop202:9092,hadoop203:9092"
        val topic = "first"
        val group = "bigdata"
        val deserialization = "org.apache.kafka.common.serialization.StringDeserializer"
        val kafkaParams = Map(
            "zookeeper.connect" -> "hadoop201:2181,hadoop202:2181,hadoop203:2181",
            ConsumerConfig.GROUP_ID_CONFIG -> group,
            ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> brokers,
            ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG -> deserialization,
            ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG -> deserialization
        )
        // 读取 offset
        val kafkaCluster = new KafkaCluster(kafkaParams)
        val fromOffset: Map[TopicAndPartition, Long] = getOffset(kafkaCluster, group, topic)
        val dStream = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder, String](
            ssc,
            kafkaParams,
            fromOffset,
            (message: MessageAndMetadata[String, String]) => message.message()
        )
        dStream.print()
        // 保存 offset
        saveOffset(kafkaCluster, group, dStream)
        ssc.start()
        ssc.awaitTermination()
    }
}

DStream的转换:

map(func)
flatMap(func)
filter(func)
repartition(numPartitions)
union(otherStream)
count()
reduce(func)
countByValue()
reduceByKey(func, [numTasks])
join(otherStream, [numTasks])
cogroup(otherStream, [numTasks])
transform(func)  // 能实现比较复杂的转化, 能在转换的同时进行一些其他的操作。
updateStateByKey(func)

updateStateByKey操作允许在使用新信息不断更新状态的同时能够保留他的状态.
    需要做两件事情:
定义状态. 状态可以是任意数据类型
定义状态更新函数. 指定一个函数, 这个函数负责使用以前的状态和新值来更新状态.

def updateFunction(newValue: Seq[Int], runningCount: Option[Int]): Option[Int] = {
            // 新的总数和状态进行求和操作
            val newCount: Int = (0 /: newValue) (_ + _) + runningCount.getOrElse(0)
            Some(newCount)
        }
        // 设置检查点: 使用updateStateByKey必须设置检查点
        ssc.sparkContext.setCheckpointDir("hdfs://hadoop201:9000/checkpoint")
        val stateDS: DStream[(String, Int)] = wordAndOne.updateStateByKey[Int](updateFunction _)
        //结束
        
        // 显示
        stateDS.print

SparkStreaming的window操作

注意:窗口的大小 和 窗口的滑动长度要和 采集的时间 成整数倍的关系

object WindowTest {
  def main(args: Array[String]): Unit = {
    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("WindowTest")
    val ssc = new StreamingContext(conf, Seconds(2))

    val inputDS: ReceiverInputDStream[String] = ssc.socketTextStream("127.0.0.1", 9999)
    // 窗口长度为6s, 滑动为4s
//    val winDS: DStream[String] = inputDS.window(Seconds(6), Seconds(4))
//     滚动窗口
    val winDS: DStream[String] = inputDS.window(Seconds(4))

    val flatMapRDD: DStream[String] = winDS.flatMap(
      line => {
        val words: Array[String] = line.split(" ")
        words
      }
    )
    val mapRDD: DStream[(String, Int)] = flatMapRDD.map((_, 1))
    val resultRDD: DStream[(String, Int)] = mapRDD.reduceByKey(_ + _)
    resultRDD.print()

    ssc.start()
    ssc.awaitTermination()
  }
}

窗口的其他操作:
reduceByKeyAndWindow(reduceFunc: (V, V) => V, windowDuration: Duration)
reduceByKeyAndWindow(reduceFunc: (V, V) => V, invReduceFunc: (V, V) => V, windowDuration: Duration, slideDuration: Duration)
window(windowLength, slideInterval)
countByWindow(windowLength, slideInterval)
countByValueAndWindow(windowLength, slideInterval, [numTasks])

DStream的输出:

print()
saveAsTextFiles(prefix, [suffix])
saveAsObjectFiles(prefix, [suffix])
saveAsHadoopFiles(prefix, [suffix])
foreachRDD(func)

注意:
连接不能写在driver层面(序列化);
如果写在foreach则每个RDD中的每一条数据都创建,得不偿失;
增加foreachPartition,在分区创建(获取)。

SparkStreaming中累加器和广播变量

和RDD中的用法完全一样


GitHub:

https://github.com/MrXuSS/SparkExer


DF常用操作:

总结

以上就是本菜鸟的Spark学习笔记,拿出来和大家分享一下,还是有点小乱,但是感觉还能看,谅解。有关大数据相关的问题都可以与本菜鸟一起讨论,一起学习。

笨鸟先飞,熟能生巧。

比心心~