Spark编程基础3RDD编程、Spark SQL

第5章 RDD编程

5.1 RDD编程基础

5.1.1 RDD创建

1.从文件系统中加载数据创建RDD

Spark采用textFile()方法来从文件系统中加载数据创建RDD
该方法把文件的URI作为参数,这个URI可以是:
1本地文件系统的地址
2或者是分布式文件系统HDFS的地址
3或者是Amazon S3的地址等等

(1)从本地文件系统中加载数据创建RDD

scala> val lines = sc.textFile("file:///usr/local/spark/mycode/rdd/word.txt") #都是通过sparkcontext连接的,sc变量来管,不需要我们创建,自动给我们创建好了
lines: org.apache.spark.rdd.RDD[String] = file:///usr/local/spark/mycode/rdd/word.txt MapPartitionsRDD[12] at textFile at <console>:27     #三个/后跟上本地文件的位置

在这里插入图片描述
图 从文件中加载数据生成RDD
(2)从分布式文件系统HDFS中加载数据

scala> val lines = sc.textFile("hdfs://localhost:9000/user/hadoop/word.txt")
scala> val lines = sc.textFile("/user/hadoop/word.txt")
scala> val lines = sc.textFile("word.txt")

对于分布式文件系统三条语句是完全等价的,可以使用其中任意一种方式
user是hadoop的hdfs专属的目录
所以不写全部的路径也可以找到当前用户的目录下的文件

2.通过并行集合(数组)创建RDD

可以调用SparkContext的parallelize方法,在Driver中一个已经存在的集合(数组)上创建。

scala>val array = Array(1,2,3,4,5) #声明一个数组
array: Array[Int] = Array(1, 2, 3, 4, 5)
scala>val rdd = sc.parallelize(array)   #把数据封装到rdd中
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[13] at parallelize at <console>:29

或者,也可以从列表中创建:

scala>val list = List(1,2,3,4,5)
list: List[Int] = List(1, 2, 3, 4, 5)
scala>val rdd = sc.parallelize(list)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[14] at parallelize at <console>:29

在这里插入图片描述
图 从数组创建RDD示意图

5.1.2 RDD操作

1.转换操作

对于RDD而言,每一次转换操作都会产生不同的RDD,供给下一个“转换”使用
转换得到的RDD是惰性求值的,也就是说,整个转换过程只是记录了转换的轨迹,并不会发生真正的计算,只有遇到行动操作时,才会发生真正的计算,开始从血缘关系源头开始,进行物理的转换操作
在这里插入图片描述
表 常用的RDD转换操作API
在这里插入图片描述
1filter(func)

scala>  val  lines =sc.textFile(file:///usr/local/spark/mycode/rdd/word.txt)
scala>  val  linesWithSpark=lines.filter(line => line.contains("Spark")) 

在这里插入图片描述
图 filter()操作实例执行过程示意图

2map(func)
map(func)操作将每个元素传递到函数func中,并将结果返回为一个新的数据集

scala> data=Array(1,2,3,4,5)
scala> val  rdd1= sc.parallelize(data)
scala> val  rdd2=rdd1.map(x=>x+10)

在这里插入图片描述
图 map()操作实例执行过程示意图

map(func)
另外一个实例
scala> val lines = sc.textFile(“file:///usr/local/spark/mycode/rdd/word.txt”)
scala> val words=lines.map(line => line.split(" "))
在这里插入图片描述
图 map()操作实例执行过程示意图

3flatMap(func)

scala> val  lines = sc.textFile("file:///usr/local/spark/mycode/rdd/word.txt")
scala> val  words=lines.flatMap(line => line.split(" "))

在这里插入图片描述
图 flatMap()操作实例执行过程示意图

4groupByKey()
groupByKey()应用于(K,V)键值对的数据集时,返回一个新的(K, Iterable)形式的数据集
在这里插入图片描述
图 groupByKey()操作实例执行过程示意图

5reduceByKey(func)
reduceByKey(func)应用于(K,V)键值对的数据集时,返回一个新的(K, V)形式的数据集,其中的每个值是将每个key传递到函数func中进行聚合后得到的结果
在这里插入图片描述
图 reduceByKey()操作实例执行过程示意图

2.行动操作

行动操作是真正触发计算的地方。Spark程序执行到行动操作时,才会执行真正的计算,从文件中加载数据,完成一次又一次转换操作,最终,完成行动操作得到结果。
表 常用的RDD行动操作API
在这里插入图片描述

scala> val  rdd=sc.parallelize(Array(1,2,3,4,5))
rdd: org.apache.spark.rdd.RDD[Int]=ParallelCollectionRDD[1] at parallelize at <console>:24
scala> rdd.count()
res0: Long = 5
scala> rdd.first()
res1: Int = 1
scala> rdd.take(3)
res2: Array[Int] = Array(1,2,3)
scala> rdd.reduce((a,b)=>a+b)
res3: Int = 15
scala> rdd.collect()
res4: Array[Int] = Array(1,2,3,4,5)
scala> rdd.foreach(elem=>println(elem))
1
2
3
4
5

3.惰性机制

所谓的“惰性机制”是指,整个转换过程只是记录了转换的轨迹,并不会发生真正的计算,只有遇到行动操作时,才会触发“从头到尾”的真正的计算。这里给出一段简单的语句来解释Spark的惰性机制。

scala> val  lines = sc.textFile("data.txt")#从hdfs下的当前用户user下的这个文件
scala> val  lineLengths = lines.map(s => s.length)#不会真正的执行操作
scala> val  totalLength = lineLengths.reduce((a, b) => a + b)#这时候才进行真正的计算,从头到尾的计算一遍

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
每行就是一个元素
按空格进行拆分,每个元素都变成了一个数组
.size把数组包含几个元素求出来
reduce要为他提供一个函数,这个是一个lamda表达式

5.1.3 持久化

在Spark中,RDD采用惰性求值的机制,每次遇到行动操作,都会从头开始执行计算。每次调用行动操作,都会触发一次从头开始的计算(就算前后两次是一样的)。这对于迭代计算而言,代价是很大的,迭代计算经常需要多次重复使用同一组数据
下面就是多次计算同一个RDD的例子:

scala> val  list = List("Hadoop","Spark","Hive")
list: List[String] = List(Hadoop, Spark, Hive)
scala> val  rdd = sc.parallelize(list)
rdd: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[22] at parallelize at <console>:29
scala> println(rdd.count())  //行动操作,触发一次真正从头到尾的计算
3
scala> println(rdd.collect().mkString(","))  //行动操作,触发一次真正从头到尾的计算
Hadoop,Spark,Hive

1可以通过持久化(缓存)机制避免这种重复计算的开销
2可以使用persist()方法对一个RDD标记为持久化
3之所以说“标记为持久化”,是因为出现persist()语句的地方,并不会马上计算生成RDD并把它持久化,而是要等到遇到第一个行动操作触发真正计算以后,才会把计算结果进行持久化
4持久化后的RDD将会被保留在计算节点的内存中被后面的行动操作重复使用

persist()的圆括号中包含的是持久化级别参数:
1persist(MEMORY_ONLY):表示将RDD作为反序列化的对象存储于JVM中,如果内存不足,就要按照LRU原则替换缓存中的内容
2persist(MEMORY_AND_DISK)表示将RDD作为反序列化的对象存储在JVM中,如果内存不足,超出的分区将会被存放在硬盘上
3一般而言,使用cache()方法时,会调用persist(MEMORY_ONLY)
4可以使用unpersist()方法手动地把持久化的RDD从缓存中移除

针对上面的实例,增加持久化语句以后的执行过程如下:

scala> val  list = List("Hadoop","Spark","Hive")
list: List[String] = List(Hadoop, Spark, Hive)
scala> val  rdd = sc.parallelize(list)
rdd: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[22] at parallelize at <console>:29
scala> rdd.cache()  //会调用persist(MEMORY_ONLY),但是,语句执行到这里,并不会缓存rdd,因为这时rdd还没有被计算生成
scala> println(rdd.count()) //第一次行动操作,触发一次真正从头到尾的计算,这时上面的rdd.cache()才会被执行,把这个rdd放到缓存中
3
scala> println(rdd.collect().mkString(",")) //第二次行动操作,不需要触发从头到尾的计算,只需要重复使用上面缓存中的rdd
Hadoop,Spark,Hive

5.1.4 分区

RDD是弹性分布式数据集,通常RDD很大,会被分成很多个分区,分别保存在不同的节点上
1.分区的作用
(1)增加并行度

在这里插入图片描述
上面的红线可以和下面的红线并行执行
在这里插入图片描述
图 RDD分区被保存到不同节点上

(2)减少通信开销

在这里插入图片描述
进行连接操作的时候分区大大减少开销
在这里插入图片描述
图 未分区时对UserData和Events两个表进行连接操作
在这里插入图片描述
图 采用分区以后对UserData和Events两个表进行连接操作

2.RDD分区原则
RDD分区的一个原则是使得分区的个数尽量等于集群中的CPU核心(core)数目
对于不同的Spark部署模式而言(本地模式、Standalone模式、YARN模式、Mesos模式),都可以通过设置spark.default.parallelism这个参数的值,来配置默认的分区数目,一般而言:
*本地模式:默认为本地机器的CPU数目,若设置了local[N],则默认为N
*Apache Mesos:默认的分区数为8
*Standalone或YARN:在“集群中所有CPU核心数目总和”和“2”二者中取较大值作为默认值

3.设置分区的个数
(1)创建RDD时手动指定分区个数
在调用textFile()和parallelize()方法的时候手动指定分区个数即可,语法格式如下:
sc.textFile(path, partitionNum)
其中,path参数用于指定要加载的文件的地址,partitionNum参数用于指定分区个数。

scala> val  array = Array(1,2,3,4,5)
scala> val  rdd = sc.parallelize(array,2)  //设置两个分区

在这里插入图片描述
(2)使用reparititon方法重新设置分区个数
通过转换操作得到新 RDD 时,直接调用 repartition 方法即可。例如:

scala> val  data = sc.textFile("file:///usr/local/spark/mycode/rdd/word.txt",2)
data: org.apache.spark.rdd.RDD[String] = file:///usr/local/spark/mycode/rdd/word.txt MapPartitionsRDD[12] at textFile at <console>:24
scala> data.partitions.size  //显示data这个RDD的分区数量
res2: Int=2
scala> val  rdd = data.repartition(1)  //对data这个RDD进行重新分区
rdd: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[11] at repartition at :26
scala> rdd.partitions.size
res4: Int = 1

在这里插入图片描述
4.自定义分区方法
Spark提供了自带的HashPartitioner(哈希分区)与RangePartitioner(区域分区),能够满足大多数应用场景的需求。与此同时,Spark也支持自定义分区方式,即通过提供一个自定义的Partitioner对象来控制RDD的分区方式,从而利用领域知识进一步减少通信开销。

要实现自定义分区,需要定义一个类,这个自定义类需要继承org.apache.spark.Partitioner类,并实现下面三个方法:
1numPartitions: Int 返回创建出来的分区数
2getPartition(key: Any): Int 返回给定键的分区编号(0到numPartitions-1)
3equals() Java判断相等性的标准方法

实例:根据key值的最后一位数字,写到不同的文件
例如:
10写入到part-00000
11写入到part-00001
.
.
.
19写入到part-00009

import org.apache.spark.{Partitioner, SparkContext, SparkConf}#导入好几个包可以这样括号括起来
//自定义分区类,需要继承org.apache.spark.Partitioner类
class MyPartitioner(numParts:Int) extends Partitioner{
  //覆盖分区数
  override def numPartitions: Int = numParts 
  //覆盖分区号获取函数
  override def getPartition(key: Any): Int = {
    key.toString.toInt%10    
}
}
object TestPartitioner {
  def main(args: Array[String]) {
    val conf=new SparkConf()#配置对象
    val sc=new SparkContext(conf)
    //模拟5个分区的数据
    val data=sc.parallelize(1 to 10,5)
    //根据尾号转变为10个分区,分别写到10个文件
    data.map((_,1)).partitionBy(new MyPartitioner(10)).map(_._1).saveAsTextFile("file:///usr/local/spark/mycode/rdd/partitioner")
  }
} 

在这里插入图片描述
map_占位符, 对每个元素都进行转换,对10个rdd,都变成键值对
10个分区
这样的话,会针对计算结果分到针对的分区中去

打印元素
在这里插入图片描述
用collect把散步在其他节点上的都收集过来

5.1.5 一个综合实例

假设有一个本地文件word.txt,里面包含了很多行文本,每行文本由多个单词构成,单词之间用空格分隔。可以使用如下语句进行词频统计(即统计每个单词出现的次数):

scala> val  lines = sc.  //代码一行放不下,可以在圆点后回车,在下行继续输入
|  textFile("file:///usr/local/spark/mycode/wordcount/word.txt")
scala> val wordCount = lines.flatMap(line => line.split(" ")).
|  map(word => (word, 1)).reduceByKey((a, b) => a + b)
scala> wordCount.collect()
scala> wordCount.foreach(println)

在这里插入图片描述
在实际应用中,单词文件可能非常大,会被保存到分布式文件系统HDFS中,Spark和Hadoop会统一部署在一个集群上
在这里插入图片描述
图 在一个集群中同时部署Hadoop和Spark
在这里插入图片描述
图 在集群中执行词频统计过程示意图

5.2 键值对RDD

RDD编程
四个元素构成RDDmap
join操作,只有key相同时才能进行连接。刚才两个RDD中,对key相等的把它们的fast连接起来。在关系型数据库中连接操作是很普遍的。
求平均值
rdd.mapValues(x=x>(x,1)).reduceByKey((x,y)=>(x._1+y._1,x._2+y._2)).mapValues(x=>(x._1/x._2)).collect
共享变量
有效的减少数据的传输,提前设置一个共享变量,提前放到各个task中设置成只读,就没必要把每一个task都传一遍。
假设我有一个黑名单,让判断是否是黑名单中的人。
进行数据统计,得到一个全局的结果。
缓存,是放在内存中。一旦这样,就不允许在后面进行修改了。广播到executor上面。
把一个普通变量变成广播变量,需要一个包装器。
例子:
Val broadcastVar = sc.broadcast(Array(1,2,3))
大的文件分发时,只会传一个数据。
sc.parallelize(Array(1,2,3,4)).foreach(x=>accum.add(x))
accum.value
最终是driver来调用

文件系统数据读写
把保存在文件中的数据读取出来
本地文件、分布式HDFS、Hase文件、关系型数据库文件
Val textFile=sc.textFile(“file:///usr/local/spark/mycode/wordcount.txt”)
生成的textFile是一个RDD,是一个逻辑的概念
遇到第一个action才会把文件导进来
textFile.first()
文件回写
textFile.saveAsTextFile(“file:///usr/local/spark/mycode/writeback.txt”)
生成的并不是一个文件,生成的是一个writeback目录。里面有两个文件,只有一个分区时part -00000,回写成功 _success

分布式文件系统的读写
Val textFile=sc.textFile(“hdfs://localhost:9000/user/hadoop/word.txt”)
Val textFile=sc.textFile(“/usr/hadoop/word.txt”)
Val textFile=sc.textFile(“word.txt”)
回写还是一样的,生成的还是目录
对于分布式文件系统,还有一种经典的做法,搭建一个每台机器上共享的服务器网盘,所有的机器都可以用路径访问这个网盘上的东西,读多写少

求Top 值
orderid,userid,
单机运行的
日志写的级别是ERROR,只有错误信息才会显示
先生成RDD
Val lines=sc.textFile(“hdfs://localhost:9000/user/hadoop/chapter5”,2)
每个RDD都是一行文本
Var result=lines.filter(line=>(line.trim().length>0)&&(line.split(“,”).length==4)).map(_.split(“,”)(2))
把刚得到的RDD每一行都取出来,用逗号切分,得出一个数组。把数组的第2个下标的值取出来。
.map(x=>(x,toint.””))
把每一个都映射成键值对
其实要调用sortByKey必须是一个键值对的形式
.sortByKey(False)
按降序排序,得到的是顺序的键值对,下面要
.map(x=>x._1).take(5)
只取key,把前面五个取出来
.foreach(x=>{
num=num+1
println(num+”\t”+x)
})

流计算
Spark Streaming
单机是完不成流计算的
RDD队列流
TestRDDQueueStream
创建一个RDD队列,每一个RDD都是一个整形值
Val rddqueue=new scala
Val queueStream = ssc.queueStream(rddQueue)
Val mappedStream = queueSream.map(r=>(r%10,1))
Val reducedStream = mappedStream.reduceByKey(+)
reducedStream.print()
ssc.start()//启动监听,只要RDD监听流过来
for(i<-1 to 10){
rddQueue+=ssc
}
上面的很少用

生产者消费者队列
Kafka组件
apche的顶级项目、Linkenda开发的
与spark\hadoop进行集成使用

Spark SQL

5.2.1 键值对RDD的创建

(1)第一种创建方式:从文件中加载
可以采用多种方式创建Pair RDD,其中一种主要方式是使用map()函数来实现

scala> val lines = sc.textFile("file:///usr/local/spark/mycode/pairrdd/word.txt")#本地文件
lines: org.apache.spark.rdd.RDD[String] = file:///usr/local/spark/mycode/pairrdd/word.txt MapPartitionsRDD[1] at textFile at <console>:27
scala> val pairRDD = lines.flatMap(line => line.split(" ")).map(word => (word,1)) #flatmap中给了一个匿名函数,用空格拆分,形成一个大的集合,把每个单词都转化为一个键值对
pairRDD: org.apache.spark.rdd.RDD[(String, Int)] = MapPartitionsRDD[3] at map at <console>:29
scala> pairRDD.foreach(println)
(i,1)
(love,1)
(hadoop,1)
……

(2)第二种创建方式:通过并行集合(数组)创建RDD

scala> val list = List("Hadoop","Spark","Hive","Spark")
list: List[String] = List(Hadoop, Spark, Hive, Spark)
 
scala> val rdd = sc.parallelize(list)
rdd: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[11] at parallelize at <console>:29
 
scala> val pairRDD = rdd.map(word => (word,1))
pairRDD: org.apache.spark.rdd.RDD[(String, Int)] = MapPartitionsRDD[12] at map at <console>:31
 
scala> pairRDD.foreach(println)
(Hadoop,1)
(Spark,1)
(Hive,1)
(Spark,1)

5.2.2 常用的键值对RDD转换操作

1reduceByKey(func) 使用func函数合并具有相同键的值
2groupByKey() 对具有相同键的值进行分组
3keys 只会把Pair RDD中的key返回形成一个新的RDD
4values 只会把Pair RDD中的value返回形成一个新的RDD。
5sortByKey() 返回一个根据键排序的RDD
6mapValues(func) 对键值对RDD中的每个value都应用一个函数,但是,key不会发生变化
7join
8combineByKey

1.reduceByKey(func)
reduceByKey(func)的功能是,使用func函数合并具有相同键的值

(Hadoop,1)
(Spark,1)
(Hive,1)
(Spark,1)

scala> pairRDD.reduceByKey((a,b)=>a+b).foreach(println)
(Spark,2)
(Hive,1)
(Hadoop,1)

在这里插入图片描述

2.groupByKey()
groupByKey()的功能是,对具有相同键的值进行分组不会做具体的计算功能,只是分组放到一起
比如,对四个键值对(“spark”,1)、(“spark”,2)、(“hadoop”,3)和(“hadoop”,5),采用groupByKey()后得到的结果是:(“spark”,(1,2))和(“hadoop”,(3,5))
(Hadoop,1)
(Spark,1)
(Hive,1)
(Spark,1)

scala> pairRDD.groupByKey()
res15: org.apache.spark.rdd.RDD[(String, Iterable[Int])] = ShuffledRDD[15] at groupByKey at <console>:34

3.reduceByKey和groupByKey的区别
reduceByKey用于对每个key对应的多个value进行merge操作,最重要的是它能够在本地先进行merge操作,并且merge操作可以通过函数自定义

groupByKey也是对每个key进行操作,但只生成一个sequence,groupByKey本身不能自定义函数,需要先用groupByKey生成RDD,然后才能对此RDD通过map进行自定义函数操作

scala>  val words = Array("one", "two", "two", "three", "three", "three")  
  
scala>  val wordPairsRDD = sc.parallelize(words).map(word => (word, 1))  
#6个键值对
scala>  val wordCountsWithReduce = wordPairsRDD.reduceByKey(_ + _)  
#_ + _占位符语法 等同于(a,b)=>a+b,把每个值取出来赋值给占位符,把占位符对应的数字加起来
scala>  val wordCountsWithGroup = wordPairsRDD.groupByKey().map(t => (t._1, t._2.sum))  
# t => (t._1, t._2.sum),把相同的key分到一组,然后map方法delamda表达式对它们进行汇总求值,针对每一个键值对都取出元素求和,依次遍历完3个键值对

上面得到的wordCountsWithReduce和wordCountsWithGroup是完全一样的,但是,它们的内部运算过程是不同的
(1)当采用reduceByKey时,Spark可以在每个分区移动数据之前将待输出数据与一个共用的key结合
在这里插入图片描述
(2)当采用groupByKey时,由于它不接收函数,Spark只能先将所有的键值对(key-value pair)都移动,这样的后果是集群节点之间的开销很大,导致传输延时
在这里插入图片描述

4.keys
keys 只会把Pair RDD中的key返回形成一个新的RDD
(Hadoop,1)
(Spark,1)
(Hive,1)
(Spark,1)

scala> pairRDD.keys
res17: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[17] at keys at <console>:34
scala> pairRDD.keys.foreach(println)
Hadoop
Spark
Hive
Spark

5.values
values 只会把Pair RDD中的value返回形成一个新的RDD。
(Hadoop,1)
(Spark,1)
(Hive,1)
(Spark,1)

scala> pairRDD.values
res0: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[2] at values at <console>:34 
scala> pairRDD.values.foreach(println)
1
1
1
1

6.sortByKey()
sortByKey()的功能是返回一个根据键排序的RDD
(Hadoop,1)
(Spark,1)
(Hive,1)
(Spark,1)

scala> pairRDD.sortByKey()
res0: org.apache.spark.rdd.RDD[(String, Int)] = ShuffledRDD[2] at sortByKey at <console>:34
scala> pairRDD.sortByKey().foreach(println)
(Hadoop,1)
(Hive,1)
(Spark,1)
(Spark,1)

7.sortByKey()必须是一个键值对和sortBy()

scala> val d1 = sc.parallelize(Array((“c",8),(“b“,25),(“c“,17),(“a“,42),(“b“,4),(“d“,9),(“e“,17),(“c“,2),(“f“,29),(“g“,21),(“b“,9)))  #数组给了很多个键值对
scala> d1.reduceByKey(_+_).sortByKey(false).collect  #把所有key相同的值加起来,然后按照降序排序(z~a)
res2: Array[(String, Int)] = Array((g,21),(f,29),(e,17),(d,9),(c,27),(b,38),(a,42)) 


scala> val d2 = sc.parallelize(Array((“c",8),(“b“,25),(“c“,17),(“a“,42),(“b“,4),(“d“,9),(“e“,17),(“c“,2),(“f“,29),(“g“,21),(“b“,9)))  
scala> d2.reduceByKey(_+_).sortBy(_._2,false).collect  #_._2是表示前一个_是占位符依次去取前面的得到的元素,后一个表示取每个元素的第二个数据即键值对的值,根据后面的值进行排序
res4: Array[(String, Int)] = Array((a,42),(b,38),(f,29),(c,27),(g,21),(e,17),(d,9)) 

另一种方法:
在这里插入图片描述
把里面的两个元素进行交换形成新的键值对

8.mapValues(func)
对键值对RDD中的每个value都应用一个函数,但是,key不会发生变化
(Hadoop,1)
(Spark,1)
(Hive,1)
(Spark,1)

scala> pairRDD.mapValues(x => x+1)  #把每个键值对的值都加上1
res2: org.apache.spark.rdd.RDD[(String, Int)] = MapPartitionsRDD[4] at mapValues at <console>:34
scala> pairRDD.mapValues(x => x+1).foreach(println)
(Hadoop,2)
(Spark,2)
(Hive,2)
(Spark,2)

9.join
join就表示内连接。对于内连接,对于给定的两个输入数据集(K,V1)和(K,V2),只有在两个数据集中都存在的key才会被输出,最终得到一个(K,(V1,V2))类型的数据集。

scala> val pairRDD1 = sc.parallelize(Array(("spark",1),("spark",2),("hadoop",3),("hadoop",5)))
pairRDD1: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[24] at parallelize at <console>:27
 
scala> val pairRDD2 = sc.parallelize(Array(("spark","fast")))
pairRDD2: org.apache.spark.rdd.RDD[(String, String)] = ParallelCollectionRDD[25] at parallelize at <console>:27
 
scala> pairRDD1.join(pairRDD2)
res9: org.apache.spark.rdd.RDD[(String, (Int, String))] = MapPartitionsRDD[28] at join at <console>:32
 
scala> pairRDD1.join(pairRDD2).foreach(println)
(spark,(1,fast))
(spark,(2,fast))

10.combineByKey
combineByKey(createCombiner,mergeValue,mergeCombiners,partitioner,mapSideCombine)
createCombiner:在第一次遇到Key时创建组合器函数,将RDD数据集中的V类型值转换C类型值(V => C)
在这里插入图片描述
mergeValue:合并值函数,再次遇到相同的Key时,将createCombiner的C类型值与这次传入的V类型值合并成一个C类型值(C,V)=>C
在这里插入图片描述
mergeCombiners:合并组合器函数,将C类型值两两合并成一个C类型值
partitioner:使用已有的或自定义的分区函数,默认是HashPartitioner
mapSideCombine:是否在map端进行Combine操作,默认为true

例:编程实现自定义Spark合并方案。给定一些销售数据,数据采用键值对的形式<公司,收入>,求出每个公司的总收入和平均收入,保存在本地文件
提示:可直接用sc.parallelize在内存中生成数据,在求每个公司总收入时,先分三个分区进行求和,然后再把三个分区进行合并。只需要编写RDD combineByKey函数的前三个参数的实现

import org.apache.spark.SparkContext
import org.apache.spark.SparkConf
object Combine {
    def main(args: Array[String]) {
        val conf = new SparkConf().setAppName("Combine").setMaster(“local”)
        val sc = new SparkContext(conf)
        val data = sc.parallelize(Array(("company-1",92),("company-1",85),("company-1",82),("company-2",78),("company-2",96),("company-2",85),("company-3",88),("company-3",94),("company-3",80)),3)
        val res = data.combineByKey(
            (income) => (income,1),
            ( acc:(Int,Int), income ) => ( acc._1+income, acc._2+1 ),
            ( acc1:(Int,Int), acc2:(Int,Int) ) => ( acc1._1+acc2._1, acc1._2+acc2._2 )
        ).map{ case (key, value) => (key, value._1, value._1/value._2.toFloat) }
        res.repartition(1).saveAsTextFile("./result")
    }
}

5.2.3 一个综合实例

题目:给定一组键值对(“spark”,2),(“hadoop”,6),(“hadoop”,4),(“spark”,6),键值对的key表示图书名称,value表示某天图书销量,请计算每个键对应的平均值,也就是计算每种图书的每天平均销量。

scala> val rdd = sc.parallelize(Array(("spark",2),("hadoop",6),("hadoop",4),("spark",6)))
rdd: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[38] at parallelize at <console>:27
 
scala> rdd.mapValues(x => (x,1)).reduceByKey((x,y) => (x._1+y._1,x._2 + y._2)).mapValues(x => (x._1 / x._2)).collect()
res22: Array[(String, Int)] = Array((spark,4), (hadoop,5))
#只对value做操作值变成(2,1)(6,1)(4,1)(6,1)      对键值相同的进行合并    只对值变化求平均值  就算出来了两天中每天的平均数

在这里插入图片描述
图 计算图书平均销量过程示意图

5.3 数据读写

共享变量

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.3.1 文件数据读写

1.本地文件系统的数据读写
(1)从文件中读取数据创建RDD

scala> val  textFile = sc.
|  textFile("file:///usr/local/spark/mycode/wordcount/word.txt")

因为Spark采用了惰性机制,在执行转换操作的时候,即使输入了错误的语句,spark-shell也不会马上报错(假设word123.txt不存在)

scala> val  textFile = sc.
|  textFile("file:///usr/local/spark/mycode/wordcount/word123.txt")

在这里插入图片描述
(2)把RDD写入到文本文件中
把textFile变量中的内容再次回写到另外一个文本文件wordback.txt中

scala> val  textFile = sc.
|  textFile("file:///usr/local/spark/mycode/wordcount/word.txt")
scala> textFile.
|  saveAsTextFile("file:///usr/local/spark/mycode/wordcount/writeback")


$ cd /usr/local/spark/mycode/wordcount/writeback/
$ ls

在这里插入图片描述
有几个分区的时候会出现part-0000
_SUCCESS

如果想再次把数据加载在RDD中,只要使用writeback这个目录即可,如下:
scala> val textFile = sc.textFile(“file:///usr/local/spark/mycode/wordcount/writeback”)

2.分布式文件系统HDFS的数据读写
从分布式文件系统HDFS中读取数据,也是采用textFile()方法,可以为textFile()方法提供一个HDFS文件或目录地址,如果是一个文件地址,它会加载该文件,如果是一个目录地址,它会加载该目录下的所有文件的数据

scala> val  textFile = sc.textFile("hdfs://localhost:9000/user/hadoop/word.txt")
scala> textFile.first()

如下三条语句都是等价的:

scala> val textFile = sc.textFile("hdfs://localhost:9000/user/hadoop/word.txt")
scala> val textFile = sc.textFile("/user/hadoop/word.txt")
scala> val textFile = sc.textFile("word.txt")

同样,可以使用saveAsTextFile()方法把RDD中的数据保存到HDFS文件中,命令如下:

scala> textFile.saveAsTextFile("writeback")

3.JSON文件的数据读写
JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式
Spark提供了一个JSON样例数据文件,存放在“/usr/local/spark/examples/src/main/resources/people.json”中

{"name":"Michael"}
{"name":"Andy", "age":30}
{"name":"Justin", "age":19} 

把本地文件系统中的people.json文件加载到RDD中:

scala> val  jsonStr = sc.
|  textFile("file:///usr/local/spark/examples/src/main/resources/people.json")
scala> jsonStr.foreach(println)
{"name":"Michael"}
{"name":"Andy", "age":30}
{"name":"Justin", "age":19}

任务:编写程序完成对JSON数据的解析工作
Scala中有一个自带的JSON库——scala.util.parsing.json.JSON,可以实现对JSON数据的解析
JSON.parseFull(jsonString:String)函数,以一个JSON字符串作为输入并进行解析,如果解析成功则返回一个Some(map: Map[String, Any]),如果解析失败则返回None

在JSONRead.scala代码文件中输入以下内容:

import org.apache.spark.SparkContext
import org.apache.spark.SparkContext._
import org.apache.spark.SparkConf
import scala.util.parsing.json.JSON
object JSONRead {
    def main(args: Array[String]) {
        val inputFile = "file:///usr/local/spark/examples/src/main/resources/people.json"
        val conf = new SparkConf().setAppName("JSONRead")
        val sc = new SparkContext(conf)
        val jsonStrs = sc.textFile(inputFile)
        val result = jsonStrs.map(s => JSON.parseFull(s))
        result.foreach( {r => r match {
                        case Some(map: Map[String, Any]) => println(map)
                        case None => println("Parsing failed")
                        case other => println("Unknown data structure: " + other)
                }
        }
        )
    }
}

将整个应用程序打包成 JAR包
通过 spark-submit 运行程序

$ /usr/local/spark/bin/spark-submit   \
> --class "JSONRead”   \> /usr/local/spark/mycode/json/target/scala-2.11/json-project_2.11-1.0.jar

执行后可以在屏幕上的大量输出信息中找到如下结果:

Map(name -> Michael)
Map(name -> Andy, age -> 30.0)
Map(name -> Justin, age -> 19.0)

5.3.2 读写HBase数据

0.HBase简介

HBase是Google BigTable的开源实现
在这里插入图片描述
1HBase是一个稀疏、多维度、排序的映射表,这张表的索引行键、列族、列限定符和时间戳
2每个值是一个未经解释的字符串,没有数据类型
3用户在表中存储数据,每一行都有一个可排序的行键和任意多的列
4表在水平方向由一个或者多个列族组成,一个列族中可以包含任意多个列,同一个列族里面的数据存储在一起
5列族支持动态扩展,可以很轻松地添加一个列族或列,无需预先定义列的数量以及类型,所有列均以字符串形式存储,用户需要自行进行数据类型转换
6HBase中执行更新操作时,并不会删除数据旧的版本,而是生成一个新的版本,旧有的版本仍然保留(这是和HDFS只允许追加不允许修改的特性相关的)

:HBase采用表来组织数据,表由行和列组成,列划分为若干个列族
:每个HBase表都由若干行组成,每个行由行键(row key)来标识。
列族:一个HBase表被分组成许多“列族”(Column Family)的集合,它是基本的访问控制单元
列限定符:列族里的数据通过列限定符(或列)来定位
单元格:在HBase表中,通过行、列族和列限定符确定一个“单元格”(cell),单元格中存储的数据没有数据类型,总被视为字节数组byte[]
一个单元格一个单元格的进行插入数据
时间戳:每个单元格都保存着同一份数据的多个版本,这些版本采用时间戳进行索引(因为HDFS一次写入就不能再修改了,这样的话不违背只读原则)
在这里插入图片描述
HBase中需要根据行键、列族、列限定符和时间戳来确定一个单元格,因此,可以视为一个“四维坐标”,即[行键, 列族, 列限定符, 时间戳]
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
1.创建一个HBase表
首先,请参照厦门大学数据库实验室博客完成HBase的安装(伪分布式模式):
http://dblab.xmu.edu.cn/blog/install-hbase/
因为HBase是伪分布式模式,需要调用底层的HDFS,所以,请首先在终端中输入下面命令启动Hadoop:
在这里插入图片描述
下面就可以启动HBase,命令如下:
在这里插入图片描述
如果里面已经有一个名称为student的表,请使用如下命令删除:
在这里插入图片描述

下面创建一个student表,要在这个表中录入如下数据:
在这里插入图片描述
在这里插入图片描述创建表名称和列族信息,要先创建列族
在这里插入图片描述 表、行键、列族.列限定符、数据内容
2.配置Spark
把HBase的lib目录下的一些jar文件拷贝到Spark中,这些都是编程时需要引入的jar包,需要拷贝的jar文件包括:所有hbase开头的jar文件、guava-12.0.1.jar、htrace-core-3.1.0-incubating.jar和protobuf-java-2.5.0.jar

$ cd /usr/local/spark/jars
$ mkdir hbase
$ cd hbase
$ cp /usr/local/hbase/lib/hbase*.jar ./
$ cp /usr/local/hbase/lib/guava-12.0.1.jar ./
$ cp /usr/local/hbase/lib/htrace-core-3.1.0-incubating.jar ./
$ cp /usr/local/hbase/lib/protobuf-java-2.5.0.jar ./

3.编写程序读取HBase数据
如果要让Spark读取HBase,就需要使用SparkContext提供的newAPIHadoopRDD这个API将表的内容以RDD的形式加载到Spark中。

import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.hbase._
import org.apache.hadoop.hbase.client._
import org.apache.hadoop.hbase.mapreduce.TableInputFormat
import org.apache.hadoop.hbase.util.Bytes
import org.apache.spark.SparkContext
import org.apache.spark.SparkContext._
import org.apache.spark.SparkConf

//剩余代码见下一页
在SparkOperateHBase.scala文件中输入以下代码:
object SparkOperateHBase {
def main(args: Array[String]) {
    val conf = HBaseConfiguration.create()
    val sc = new SparkContext(new SparkConf())
    //设置查询的表名
    conf.set(TableInputFormat.INPUT_TABLE, "student")
    val stuRDD = sc.newAPIHadoopRDD(conf, classOf[TableInputFormat],
  classOf[org.apache.hadoop.hbase.io.ImmutableBytesWritable],
  classOf[org.apache.hadoop.hbase.client.Result])
    val count = stuRDD.count()
    println("Students RDD Count:" + count)
    stuRDD.cache()
    //遍历输出
    stuRDD.foreach({ case (_,result) =>
        val key = Bytes.toString(result.getRow)
        val name = Bytes.toString(result.getValue("info".getBytes,"name".getBytes))
        val gender = Bytes.toString(result.getValue("info".getBytes,"gender".getBytes))
        val age = Bytes.toString(result.getValue("info".getBytes,"age".getBytes))
        println("Row key:"+key+" Name:"+name+" Gender:"+gender+" Age:"+age)
    })
}
}

在simple.sbt中录入下面内容:

name := "Simple Project"
version := "1.0"
scalaVersion := "2.11.8"
libraryDependencies += "org.apache.spark" %% "spark-core" % "2.1.0"
libraryDependencies += "org.apache.hbase" % "hbase-client" % "1.1.5"
libraryDependencies += "org.apache.hbase" % "hbase-common" % "1.1.5"
libraryDependencies += "org.apache.hbase" % "hbase-server" % "1.1.5"

采用sbt打包,通过 spark-submit 运行程序

$ /usr/local/spark/bin/spark-submit   \
>--driver-class-path /usr/local/spark/jars/hbase/*:/usr/local/hbase/conf  \
>--class "SparkOperateHBase"   \
>/usr/local/spark/mycode/hbase/target/scala-2.11/simple-project_2.11-1.0.jar

必须使用“–driver-class-path”参数指定依赖JAR包的路径,而且必须把“/usr/local/hbase/conf”也加到路径中
执行后得到如下结果:

Students RDD Count:2
Row key:1 Name:Xueqian Gender:F Age:23
Row key:2 Name:Weiliang Gender:M Age:24

4.编写程序向HBase写入数据
下面编写应用程序把表中的两个学生信息插入到HBase的student表中
在这里插入图片描述
在SparkWriteHBase.scala文件中输入下面代码:

import org.apache.hadoop.hbase.HBaseConfiguration  
import org.apache.hadoop.hbase.mapreduce.TableOutputFormat  
import org.apache.spark._  
import org.apache.hadoop.mapreduce.Job  
import org.apache.hadoop.hbase.io.ImmutableBytesWritable  
import org.apache.hadoop.hbase.client.Result  
import org.apache.hadoop.hbase.client.Put  
import org.apache.hadoop.hbase.util.Bytes
在SparkWriteHBase.scala文件中输入下面代码:

//剩余代码见下一页
 object SparkWriteHBase {  
  def main(args: Array[String]): Unit = {  
    val sparkConf = new SparkConf().setAppName("SparkWriteHBase").setMaster("local")  
    val sc = new SparkContext(sparkConf)        
    val tablename = "student"        
    sc.hadoopConfiguration.set(TableOutputFormat.OUTPUT_TABLE, tablename)  
    val job = new Job(sc.hadoopConfiguration)  
    job.setOutputKeyClass(classOf[ImmutableBytesWritable])  
    job.setOutputValueClass(classOf[Result])    
    job.setOutputFormatClass(classOf[TableOutputFormat[ImmutableBytesWritable]])    
    val indataRDD = sc.makeRDD(Array("3,Rongcheng,M,26","4,Guanhua,M,27")) //构建两行记录
    val rdd = indataRDD.map(_.split(',')).map{arr=>{  
      val put = new Put(Bytes.toBytes(arr(0))) //行健的值 
      put.add(Bytes.toBytes("info"),Bytes.toBytes("name"),Bytes.toBytes(arr(1)))  //info:name列的值
      put.add(Bytes.toBytes("info"),Bytes.toBytes("gender"),Bytes.toBytes(arr(2)))  //info:gender列的值
      put.add(Bytes.toBytes("info"),Bytes.toBytes("age"),Bytes.toBytes(arr(3).toInt))  //info:age列的值
      (new ImmutableBytesWritable, put)   
    }}        
    rdd.saveAsNewAPIHadoopDataset(job.getConfiguration())  
  }    
} 


$ /usr/local/spark/bin/spark-submit   \
>--driver-class-path /usr/local/spark/jars/hbase/*:/usr/local/hbase/conf   \
>--class "SparkWriteHBase"   \
>/usr/local/spark/mycode/hbase/target/scala-2.11/simple-project_2.11-1.0.jar

切换到HBase Shell中,执行如下命令查看student表

hbase> scan 'student'
ROW                                    COLUMN+CELL                                                                                                   
 1                                     column=info:age, timestamp=1479640712163, value=23                                                            
 1                                     column=info:gender, timestamp=1479640704522, value=F                                                          
 1                                     column=info:name, timestamp=1479640696132, value=Xueqian                                                      
 2                                     column=info:age, timestamp=1479640752474, value=24                                                            
 2                                     column=info:gender, timestamp=1479640745276, value=M                                                          
 2                                     column=info:name, timestamp=1479640732763, value=Weiliang                                                     
 3                                     column=info:age, timestamp=1479643273142, value=\x00\x00\x00\x1A                                              
 3                                     column=info:gender, timestamp=1479643273142, value=M                                                          
 3                                     column=info:name, timestamp=1479643273142, value=Rongcheng                                                    
 4                                     column=info:age, timestamp=1479643273142, value=\x00\x00\x00\x1B                                              
 4                                     column=info:gender, timestamp=1479643273142, value=M                                                          
 4                                     column=info:name, timestamp=1479643273142, value=Guanhua                                                      
4 row(s) in 0.3240 seconds

读写都是以单元格为单位进行的

5.4 综合案例

5.4.1 案例1:求TOP值

任务描述:orderid,userid,payment,productid
在这里插入图片描述

import org.apache.spark.{SparkConf, SparkContext}
object TopN {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setAppName("TopN").setMaster("local")
    val sc = new SparkContext(conf)
    sc.setLogLevel("ERROR")
    val lines = sc.textFile("hdfs://localhost:9000/user/hadoop/spark/mycode/rdd/examples",2)
    var num = 0;
    val result = lines.filter(line => (line.trim().length > 0) && (line.split(",").length == 4))
      .map(_.split(",")(2))
      .map(x => (x.toInt,""))
      .sortByKey(false)
      .map(x => x._1).take(5)
      .foreach(x => {
        num = num + 1
        println(num + "\t" + x)
      })
  }
}

在这里插入图片描述
每个RDD都是一行文本
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.4.2 案例2:求最大最小值

在这里插入图片描述

import org.apache.spark.{SparkConf, SparkContext}
object MaxAndMin {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setAppName(“MaxAndMin“).setMaster("local")
    val sc = new SparkContext(conf)
    sc.setLogLevel("ERROR")
    val lines = sc.textFile("hdfs://localhost:9000/user/hadoop/spark/chapter5", 2)
 val result = lines.filter(_.trim().length>0).map(line => ("key",line.trim.toInt)).groupByKey().map(x => {
      var min = Integer.MAX_VALUE
      var max = Integer.MIN_VALUE
      for(num <- x._2){
        if(num>max){
          max = num
        }
        if(num<min){
          min = num
        }
      }
      (max,min)
    }).collect.foreach(x => {
      println("max\t"+x._1)
      println("min\t"+x._2)
    })
  }
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.4.3 案例3:文件排序

任务描述:
有多个输入文件,每个文件中的每一行内容均为一个整数。要求读取所有文件中的整数,进行排序后,输出到一个新的文件中,输出的内容个数为每行两个整数,第一个整数为第二个整数的排序位次,第二个整数为原待排序的整数
在这里插入图片描述

import org.apache.spark.SparkContext
import org.apache.spark.SparkContext._
import org.apache.spark.SparkConf
import org.apache.spark.HashPartitioner
object FileSort {
    def main(args: Array[String]) {
        val conf = new SparkConf().setAppName("FileSort")
        val sc = new SparkContext(conf)
        val dataFile = "file:///usr/local/spark/mycode/rdd/data"
        val lines = sc.textFile(dataFile,3)
        var index = 0
        val result = lines.filter(_.trim().length>0).map(n=>(n.trim.toInt,"")).partitionBy(new HashPartitioner(1)).sortByKey().map(t => {
      index += 1
            (index,t._1)
        })
        result.saveAsTextFile("file:///usrl/local/spark/mycode/rdd/examples/result")
    }
}

在这里插入图片描述
在这里插入图片描述
分区的方法,传递一个分区的对象,只分一个区
由三个分区形成一个分区,这样在一个分区内排序就不会出错了
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

5.4.4 案例4:二次排序

在这里插入图片描述
二次排序,具体的实现步骤:

  • 第一步:按照Ordered和Serializable接口实现自定义排序的key
  • 第二步:将要进行二次排序的文件加载进来生成<key,value>类型的RDD
  • 第三步:使用sortByKey基于自定义的Key进行二次排序
  • 第四步:去除掉排序的Key,只保留排序的结果

SecondarySortKey.scala代码如下:

package cn.edu.xmu.spark
class SecondarySortKey(val first:Int,val second:Int) extends Ordered [SecondarySortKey] with Serializable {
def compare(other:SecondarySortKey):Int = {
    if (this.first - other.first !=0) {
         this.first - other.first 
    } else {
      this.second - other.second
    }
  }
}

SecondarySortApp.scala代码如下:

package cn.edu.xmu.spark
import org.apache.spark.SparkConf
import org.apache.spark.SparkContext
object SecondarySortApp {
  def main(args:Array[String]){
     val conf = new SparkConf().setAppName("SecondarySortApp").setMaster("local")
       val sc = new SparkContext(conf)
       val lines = sc.textFile("file:///usr/local/spark/mycode/rdd/examples/file1.txt", 1)
       val pairWithSortKey = lines.map(line=>(new SecondarySortKey(line.split(" ")(0).toInt, line.split(" ")(1).toInt),line))
       val sorted = pairWithSortKey.sortByKey(false)
       val sortedResult = sorted.map(sortedLine =>sortedLine._2)
       sortedResult.collect().foreach (println)
  }
}

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.4.5 案例5:连接操作

任务描述:在推荐领域有一个著名的开放测试集,下载链接是:http://grouplens.org/datasets/movielens/,该测试集包含三个文件,分别是ratings.dat、sers.dat、movies.dat,具体介绍可阅读:README.txt。

请编程实现:通过连接ratings.dat和movies.dat两个文件得到平均得分超过4.0的电影列表,采用的数据集是:ml-1m

编号、名字、风格
用户编号、电影编号、分数、时间
在这里插入图片描述

import org.apache.spark._ 
import SparkContext._ 
object SparkJoin { 
  def main(args: Array[String]) { 
    if (args.length != 3 ){ 
      println("usage is WordCount <rating> <movie> <output>")      
      return 
    } 
   val conf = new SparkConf().setAppName("SparkJoin").setMaster("local")
   val sc = new SparkContext(conf)  
   // Read rating from HDFS file 
   val textFile = sc.textFile(args(0)) 
//extract (movieid, rating) 
    val rating = textFile.map(line => { 
        val fileds = line.split("::") 
        (fileds(1).toInt, fileds(2).toDouble) 
       }) 
 //get (movieid,ave_rating) 
    val movieScores = rating 
       .groupByKey() 
       .map(data => { 
         val avg = data._2.sum / data._2.size 
         (data._1, avg) 
       }) 
// Read movie from HDFS file 
     val movies = sc.textFile(args(1)) 
     val movieskey = movies.map(line => { 
       val fileds = line.split("::") 
        (fileds(0).toInt, fileds(1))   //(MovieID,MovieName)
     }).keyBy(tup => tup._1) 
  
     // by join, we get <movie, averageRating, movieName> 
     val result = movieScores 
       .keyBy(tup => tup._1) 
       .join(movieskey) 
       .filter(f => f._2._1._2 > 4.0) 
       .map(f => (f._1, f._2._1._2, f._2._2._2)) 
  
    result.saveAsTextFile(args(2)) 
  } 
} 

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

第6章 Spark SQL

6.1 Spark SQL简介

6.1.1 从Shark说起

在这里插入图片描述
在这里插入图片描述
Shark即Hive on Spark,为了实现与Hive兼容,Shark在HiveQL方面重用了Hive中HiveQL的解析、逻辑执行计划翻译、执行计划优化等逻辑,可以近似认为仅将物理执行计划从MapReduce作业替换成了Spark作业,通过Hive的HiveQL解析,把HiveQL翻译成Spark上的RDD操作

Shark的出现,使得SQL-on-Hadoop的性能比Hive有了10-100倍的提高

Shark的设计导致了两个问题:
一是执行计划优化完全依赖于Hive,不方便添加新的优化策略
二是因为Spark是线程级并行,而MapReduce是进程级并行,因此,Spark在兼容Hive的实现上存在线程安全问题,导致Shark不得不使用另外一套独立维护的打了补丁的Hive源码分支

2014年6月1日Shark项目和SparkSQL项目的主持人Reynold Xin宣布:停止对Shark的开发,团队将所有资源放SparkSQL项目上,至此,Shark的发展画上了句话,但也因此发展出两个直线:SparkSQL和Hive on Spark
在这里插入图片描述
Spark SQL作为Spark生态的一员继续发展,而不再受限于Hive,只是兼容Hive
Hive on Spark是一个Hive的发展计划,该计划将Spark作为Hive的底层引擎之一,也就是说,Hive将不再受限于一个引擎,可以采用Map-Reduce、Tez、Spark等引擎

6.1.2 Spark SQL设计

Spark SQL在Hive兼容层面仅依赖HiveQL解析、Hive元数据,也就是说,从HQL被解析成抽象语法树(AST)起,就全部由Spark SQL接管了。Spark SQL执行计划生成和优化都由Catalyst(函数式关系查询优化框架)负责
在这里插入图片描述
Spark SQL增加了DataFrame(即带有Schema信息的RDD),使用户可以在Spark SQL中执行SQL语句,数据既可以来自RDD,也可以是Hive、HDFS、Cassandra等外部数据源,还可以是JSON格式的数据
Spark SQL目前支持Scala、Java、Python三种语言,支持SQL-92规范
在这里插入图片描述

6.1.3 为什么推出Spark SQL

在这里插入图片描述
在这里插入图片描述

关系数据库已经很流行
关系数据库在大数据时代已经不能满足要求
1首先,用户需要从不同数据源执行各种操作,包括结构化和非结构化数据
2其次,用户需要执行高级分析,比如机器学习和图像处理
在实际大数据应用中,经常需要融合关系查询和复杂分析算法(比如机器学习或图像处理),但是,缺少这样的系统

Spark SQL填补了这个鸿沟:
1首先,可以提供DataFrame API,可以对内部和外部各种数据源执行各种关系操作
2其次,可以支持大量的数据源和数据分析算法
Spark SQL可以融合:传统关系数据库的结构化数据管理能力和机器学习算法的数据处理能力

6.2 DataFrame概述

1DataFrame的推出,让Spark具备了处理大规模结构化数据的能力,不仅比原有的RDD转化方式更加简单易用,而且获得了更高的计算性能
2Spark能够轻松实现从MySQL到DataFrame的转化,并且支持SQL查询
在这里插入图片描述
3RDD是分布式的 Java对象的集合,但是,对象内部结构对于RDD而言却是不可知的
4DataFrame是一种以RDD为基础的分布式数据集,提供了详细的结构信息

6.3 DataFrame的创建

从Spark2.0以上版本开始,Spark使用全新的SparkSession接口替代Spark1.6中的SQLContext及HiveContext接口来实现其对数据加载、转换、处理等功能。SparkSession实现了SQLContext及HiveContext所有功能

SparkSession 支持从不同的数据源加载数据,并把数据转换成DataFrame,并且支持把DataFrame转换成SQLContext自身中的表,然后使用SQL语句来操作数据。SparkSession亦提供了HiveQL以及其他依赖于Hive的功能的支持

可以通过如下语句创建一个SparkSession对象:

scala> import org.apache.spark.sql.SparkSession
scala> val spark=SparkSession.builder().getOrCreate()

在创建DataFrame之前,为了支持RDD转换为DataFrame及后续的SQL操作,需要通过import语句(即import spark.implicits._)导入相应的包,启用隐式转换。

在创建DataFrame时,可以使用spark.read操作,从不同类型的文件中加载数据创建DataFrame,例如:
spark.read.json(“people.json”):读取people.json文件创建DataFrame;在读取本地文件或HDFS文件时,要注意给出正确的文件路径;

spark.read.parquet("people.parquet"):读取people.parquet文件创建DataFrame;
spark.read.csv("people.csv"):读取people.csv文件创建DataFrame。

一个实例
在“/usr/local/spark/examples/src/main/resources/”这个目录下,这个目录下有两个样例数据people.json和people.txt。
people.json文件的内容如下:

{"name":"Michael"}
{"name":"Andy", "age":30}
{"name":"Justin", "age":19}

people.txt文件的内容如下:

Michael, 29
Andy, 30
Justin, 19

scala> import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.SparkSession
 
scala> val spark=SparkSession.builder().getOrCreate()
spark: org.apache.spark.sql.SparkSession = org.apache.spark.sql.SparkSession@2bdab835
 
//使支持RDDs转换为DataFrames及后续sql操作
scala> import spark.implicits._
import spark.implicits._
 
scala> val df = spark.read.json("file:///usr/local/spark/examples/src/main/resources/people.json")
df: org.apache.spark.sql.DataFrame = [age: bigint, name: string]
 
scala> df.show()
+----+-------+
| age|   name|
+----+-------+
|null|Michael|
|  30|   Andy|
|  19| Justin|
+----+-------+

6.4 DataFrame的保存

可以使用spark.write操作,把一个DataFrame保存成不同格式的文件,例如,把一个名称为df的DataFrame保存到不同格式文件中,方法如下:

df.write.json("people.json“)
df.write.parquet("people.parquet“)
df.write.csv("people.csv")

下面从示例文件people.json中创建一个DataFrame,然后保存成csv格式文件,代码如下:

scala> val peopleDF = spark.read.format("json").
| load("file:///usr/local/spark/examples/src/main/resources/people.json")
scala> peopleDF.select("name", "age").write.format("csv").
| save("file:///usr/local/spark/mycode/sql/newpeople.csv")

6.5 DataFrame的常用操作

可以执行一些常用的DataFrame操作
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

6.6 从RDD转换得到DataFrame

6.6.1 利用反射机制推断RDD模式

在“/usr/local/spark/examples/src/main/resources/”目录下,有个Spark安装时自带的样例数据people.txt,其内容如下:

Michael, 29
Andy, 30
Justin, 19

现在要把people.txt加载到内存中生成一个DataFrame,并查询其中的数据

在利用反射机制推断RDD模式时,需要首先定义一个case class,因为,只有case class才能被Spark隐式地转换为DataFrame

scala> import org.apache.spark.sql.catalyst.encoders.ExpressionEncoder
import org.apache.spark.sql.catalyst.encoders.ExpressionEncoder 
scala> import org.apache.spark.sql.Encoder
import org.apache.spark.sql.Encoder 
scala> import spark.implicits._  //导入包,支持把一个RDD隐式转换为一个DataFrame
import spark.implicits._



scala> case class Person(name: String, age: Long)  //定义一个case class
defined class Person
scala> val peopleDF = spark.sparkContext.
| textFile("file:///usr/local/spark/examples/src/main/resources/people.txt").
| map(_.split(",")).
| map(attributes => Person(attributes(0), attributes(1).trim.toInt)).toDF()
peopleDF: org.apache.spark.sql.DataFrame = [name: string, age: bigint] 
scala> peopleDF.createOrReplaceTempView("people") //**必须注册为临时表才能供下面的查**询使用
scala> val personsRDD = spark.sql("select name,age from people where age > 20")
//最终生成一个DataFrame,下面是系统执行返回的信息
personsRDD: org.apache.spark.sql.DataFrame = [name: string, age: bigint]
scala> personsRDD.map(t => "Name: "+t(0)+ ","+"Age: "+t(1)).show()  //DataFrame中的每个元素都是一行记录,包含name和age两个字段,分别用t(0)和t(1)来获取值
//下面是系统执行返回的信息
+------------------+ 
| value|
+------------------+
|Name:Michael,Age:29|
| Name:Andy,Age:30|
+------------------+

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
封装成一个person对象
在这里插入图片描述

6.6.2 使用编程方式定义RDD模式

当无法提前定义case class时,就需要采用编程方式定义RDD模式。
比如,现在需要通过编程方式把people.txt加载进来生成DataFrame,并完成SQL查询。
在这里插入图片描述

scala> import org.apache.spark.sql.types._
import org.apache.spark.sql.types._
scala> import org.apache.spark.sql.Row
import org.apache.spark.sql.Row
//生成字段
scala> val fields = Array(StructField("name",StringType,true), StructField("age",IntegerType,true))
fields: Array[org.apache.spark.sql.types.StructField] = Array(StructField(name,StringType,true), StructField(age,IntegerType,true))
scala> val schema = StructType(fields)
schema: org.apache.spark.sql.types.StructType = StructType(StructField(name,StringType,true), StructField(age, IntegerType,true))
//从上面信息可以看出,schema描述了模式信息,模式中包含name和age两个字段
//shcema就是“表头”

//下面加载文件生成RDD
scala> val peopleRDD = spark.sparkContext.
| textFile("file:///usr/local/spark/examples/src/main/resources/people.txt")
peopleRDD: org.apache.spark.rdd.RDD[String] = file:///usr/local/spark/examples/src/main/resources/people.txt MapPartitionsRDD[1] at textFile at <console>:26 

//对peopleRDD 这个RDD中的每一行元素都进行解析
scala> val rowRDD = peopleRDD.map(_.split(",")).
|  map(attributes => Row(attributes(0), attributes(1).trim.toInt))
rowRDD: org.apache.spark.rdd.RDD[org.apache.spark.sql.Row] = MapPartitionsRDD[3] at map at <console>:29
//上面得到的rowRDD就是“表中的记录”

//下面把“表头”和“表中的记录”拼装起来
 scala> val peopleDF = spark.createDataFrame(rowRDD, schema)
peopleDF: org.apache.spark.sql.DataFrame = [name: string, age: int]

 //必须注册为临时表才能供下面查询使用
scala> peopleDF.createOrReplaceTempView("people")
 scala> val results = spark.sql("SELECT name,age FROM people")
results: org.apache.spark.sql.DataFrame = [name: string, age: int] 
scala> results.
|  map(attributes => "name: " + attributes(0)+","+"age:"+attributes(1)).
|  show()
+--------------------+
| value|
+--------------------+
|name: Michael,age:29|
| name: Andy,age:30|
| name: Justin,age:19|
+--------------------+

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
挂接起来
在这里插入图片描述

6.6.3 把RDD保存成文件

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

6.7 使用Spark SQL读写数据库

6.7.0 读写parquet

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

6.7.1 通过JDBC连接数据库

1.准备工作
请参考厦门大学数据库实验室博客教程《 Ubuntu安装MySQL 》,在Linux系统中安装好MySQL数据库
教程地址: http://dblab.xmu.edu.cn/blog/install-mysql/
在Linux中启动MySQL数据库

$ service mysql start
$ mysql -u root -p

#屏幕会提示你输入密码
输入下面SQL语句完成数据库和表的创建:

mysql> create database spark;
mysql> use spark;
mysql> create table student (id int(4), name char(20), gender char(4), age int(4));
mysql> insert into student values(1,'Xueqian','F',23);
mysql> insert into student values(2,'Weiliang','M',24);
mysql> select * from student;

下载MySQL的JDBC驱动程序,比如mysql-connector-java-5.1.40.tar.gz
把该驱动程序拷贝到spark的安装目录” /usr/local/spark/jars”下
启动一个spark-shell,启动Spark Shell时,必须指定mysql连接驱动jar包

$ cd /usr/local/spark
$ ./bin/spark-shell  \
--jars /usr/local/spark/jars/mysql-connector-java-5.1.40/mysql-connector-java-5.1.40-bin.jar \
--driver-class-path /usr/local/spark/jars/mysql-connector-java-5.1.40/mysql-connector-java-5.1.40-bin.jar

2.读取MySQL数据库中的数据
执行以下命令连接数据库,读取数据,并显示:

scala> val jdbcDF = spark.read.format("jdbc").
| option("url","jdbc:mysql://localhost:3306/spark").
| option("driver","com.mysql.jdbc.Driver").
| option("dbtable", "student").
| option("user", "root").
| option("password", "hadoop").
| load()
scala> jdbcDF.show()
+---+--------+------+---+
| id| name|gender|age|
+---+--------+------+---+
| 1| Xueqian| F| 23|
| 2|Weiliang| M| 24|
+---+--------+------+---+

3.向MySQL数据库写入数据
在MySQL数据库中创建了一个名称为spark的数据库,并创建了一个名称为student的表
创建后,查看一下数据库内容:
在这里插入图片描述
现在开始在spark-shell中编写程序,往spark.student表中插入两条记录

import java.util.Properties
import org.apache.spark.sql.types._
import org.apache.spark.sql.Row
 
//下面我们设置两条数据表示两个学生信息
val studentRDD = spark.sparkContext.parallelize(Array("3 Rongcheng M 26","4 Guanhua M 27")).map(_.split(" "))
 
//下面要设置模式信息
val schema = StructType(List(StructField("id", IntegerType, true),StructField("name", StringType, true),StructField("gender", StringType, true),StructField("age", IntegerType, true)))
 //下面创建Row对象,每个Row对象都是rowRDD中的一行
val rowRDD = studentRDD.map(p => Row(p(0).toInt, p(1).trim, p(2).trim, p(3).toInt))
 
//建立起Row对象和模式之间的对应关系,也就是把数据和模式对应起来
val studentDF = spark.createDataFrame(rowRDD, schema)
 
//下面创建一个prop变量用来保存JDBC连接参数
val prop = new Properties()
prop.put("user", "root") //表示用户名是root
prop.put("password", "hadoop") //表示密码是hadoop
prop.put("driver","com.mysql.jdbc.Driver") //表示驱动程序是com.mysql.jdbc.Driver
 
//下面就可以连接数据库,采用append模式,表示追加记录到数据库spark的student表中
studentDF.write.mode("append").jdbc("jdbc:mysql://localhost:3306/spark", "spark.student", prop)

可以看一下效果,看看MySQL数据库中的spark.student表发生了什么变化

mysql> select * from student;
+------+-----------+--------+------+
| id | name | gender | age |
+------+-----------+--------+------+
| 1 | Xueqian | F | 23 |
| 2 | Weiliang | M | 24 |
| 3 | Rongcheng | M | 26 |
| 4 | Guanhua | M | 27 |
+------+-----------+--------+------+
4 rows in set (0.00 sec)

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

6.7.2 连接Hive读写数据

1.准备工作
数据仓库(Data Warehouse)是一个面向主题的(Subject Oriented)、集成的(Integrated)、相对稳定的(Non-Volatile)、反映历史变化(Time Variant)的数据集合,用于支持管理决策。
在这里插入图片描述
Hive是一个构建于Hadoop顶层的数据仓库工具
支持大规模数据存储、分析,具有良好的可扩展性

某种程度上可以看作是用户编程接口,本身不存储和处理数据
依赖分布式文件系统HDFS存储数据
依赖分布式并行计算模型MapReduce处理数据

定义了简单的类似SQL 的查询语言——HiveQL
用户可以通过编写的HiveQL语句运行MapReduce任务

可以很容易把原来构建在关系数据库上的数据仓库应用程序移植到Hadoop平台上
是一个可以提供有效、合理、直观组织和使用数据的分析工具

Hive依赖于HDFS 存储数据
Hive依赖于MapReduce 处理数据
在这里插入图片描述
在这里插入图片描述

Hive的安装,请参考厦门大学数据库实验室建设的高校大数据课程公共服务平台上的技术博客:
《Ubuntu安装hive,并配置mysql作为元数据库》
http://dblab.xmu.edu.cn/blog/install-hive/

为了让Spark能够访问Hive,必须为Spark添加Hive支持
Spark官方提供的预编译版本,通常是不包含Hive支持的,需要采用源码编译,编译得到一个包含Hive支持的Spark版本
(1)测试一下自己电脑上已经安装的Spark版本是否支持Hive
启动进入了spark-shell,如果不支持Hive,会显示如下信息:
在这里插入图片描述
如果你当前电脑上的Spark版本包含Hive支持,那么应该显示下面的正确信息:
在这里插入图片描述
(2)采用源码编译方法得到支持Hive的Spark版本
到Spark官网下载源码
http://spark.apache.org/downloads.html
在这里插入图片描述
解压文件

$ cd /home/hadoop/下载 //spark-2.1.0.tgz就在这个目录下面
$ ls #可以看到刚才下载的spark-2.1.0.tgz文件
$ sudo tar -zxf ./spark-2.1.0.tgz -C /home/hadoop/
$ cd /home/hadoop
$ ls #这时可以看到解压得到的文件夹spark-2.1.0

在编译时,需要给出电脑上之前已经安装好的Hadoop的版本

$ hadoop version

运行编译命令,对Spark源码进行编译

$ cd /home/hadoop/spark-2.1.0
$ ./dev/make-distribution.sh —tgz —name h27hive -Pyarn -Phadoop-2.7 -Dhadoop.version=2.7.1 -Phive -Phive-thriftserver -DskipTests

编译成功后会得到文件名“spark-2.1.0-bin-h27hive.tgz”,这个就是包含Hive支持的Spark安装文件

(3)安装支持Hive的Spark版本
Spark的安装详细过程,请参考厦门大学数据库实验室建设的高校大数据课程公共服务平台上的技术博客:
《Spark2.1.0入门:Spark的安装和使用》
博客地址:http://dblab.xmu.edu.cn/blog/1307-2/
启动进入了spark-shell,由于已经可以支持Hive,会显示如下信息:
在这里插入图片描述

2.在Hive中创建数据库和表
假设已经完成了Hive的安装,并且使用的是MySQL数据库来存放Hive的元数据
需要借助于MySQL保存Hive的元数据,首先启动MySQL数据库:

$ service mysql start

由于Hive是基于Hadoop的数据仓库,使用HiveQL语言撰写的查询语句,最终都会被Hive自动解析成MapReduce任务由Hadoop去具体执行,因此,需要启动Hadoop,然后再启动Hive

启动Hadoop:

$ cd /usr/local/hadoop
$ ./sbin/start-all.sh

Hadoop启动成功以后,可以再启动Hive:

$ cd /usr/local/hive
$ ./bin/hive

进入Hive,新建一个数据库sparktest,并在这个数据库下面创建一个表student,并录入两条数据

hive> create database if not exists sparktest;//创建数据库sparktest
hive> show databases; //显示一下是否创建出了sparktest数据库
//下面在数据库sparktest中创建一个表student
hive> create table if not exists sparktest.student(
> id int,
> name string,
> gender string,
> age int);
hive> use sparktest; //切换到sparktest
hive> show tables; //显示sparktest数据库下面有哪些表
hive> insert into student values(1,'Xueqian','F',23); //插入一条记录
hive> insert into student values(2,'Weiliang','M',24); //再插入一条记录
hive> select * from student; //显示student表中的记录

3.连接Hive读写数据
需要修改“/usr/local/sparkwithhive/conf/spark-env.sh”这个配置文件:

export SPARK_DIST_CLASSPATH=$(/usr/local/hadoop/bin/hadoop classpath)
export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64
export CLASSPATH=$CLASSPATH:/usr/local/hive/lib
export SCALA_HOME=/usr/local/scala
export HADOOP_CONF_DIR=/usr/local/hadoop/etc/hadoop
export HIVE_CONF_DIR=/usr/local/hive/conf
export SPARK_CLASSPATH=$SPARK_CLASSPATH:/usr/local/hive/lib/mysql-connector-java-5.1.40-bin.jar

请在spark-shell(包含Hive支持)中执行以下命令从Hive中读取数据:

Scala> import org.apache.spark.sql.Row
Scala> import org.apache.spark.sql.SparkSession 
Scala> case class Record(key: Int, value: String) 
// warehouseLocation points to the default location for managed databases and tables
Scala> val warehouseLocation = "spark-warehouse” 
Scala> val spark = SparkSession.builder().appName("Spark Hive Example").config("spark.sql.warehouse.dir", warehouseLocation).enableHiveSupport().getOrCreate() 
Scala> import spark.implicits._
Scala> import spark.sql
//下面是运行结果
scala> sql("SELECT * FROM sparktest.student").show()
+---+--------+------+---+
| id| name|gender|age|
+---+--------+------+---+
| 1| Xueqian| F| 23|
| 2|Weiliang| M| 24|
+---+--------+------+---+

编写程序向Hive数据库的sparktest.student表中插入两条数据
在插入数据之前,先查看一下已有的2条数据
在这里插入图片描述
编写程序向Hive数据库的sparktest.student表中插入两条数据:

scala> import java.util.Properties
scala> import org.apache.spark.sql.types._
scala> import org.apache.spark.sql.Row 
//下面我们设置两条数据表示两个学生信息
scala> val studentRDD = spark.sparkContext.parallelize(Array("3 Rongcheng M 26","4 Guanhua M 27")).map(_.split(" ")) 
//下面要设置模式信息
scala> val schema = StructType(List(StructField("id", IntegerType, true),StructField("name", StringType, true),StructField("gender", StringType, true),StructField("age", IntegerType, true)))
 //下面创建Row对象,每个Row对象都是rowRDD中的一行
scala> val rowRDD = studentRDD.map(p => Row(p(0).toInt, p(1).trim, p(2).trim, p(3).toInt)) 
//建立起Row对象和模式之间的对应关系,也就是把数据和模式对应起来
scala> val studentDF = spark.createDataFrame(rowRDD, schema)
//查看studentDF
scala> studentDF.show()
+---+---------+------+---+
| id| name|gender|age|
+---+---------+------+---+
| 3|Rongcheng| M| 26|
| 4| Guanhua| M| 27|
+---+---------+------+---+
//下面注册临时表
scala> studentDF.registerTempTable("tempTable")
 
scala> sql("insert into sparktest.student select * from tempTable")

输入以下命令查看Hive数据库内容的变化:
在这里插入图片描述
可以看到,插入数据操作执行成功了!

猜你喜欢

转载自blog.csdn.net/AthlenaA/article/details/85224007