Spark的数据读取及数据保存可以从两个维度来作区分:文件格式以及文件系统。
文件格式分为:Text文件、Json文件、Csv文件、Sequence文件以及Object文件;
文件系统分为:本地文件系统、HDFS以及数据库。
1)数据读取:textFile(String)
2)数据保存:saveAsTextFile(String)
3)代码实现:
object Operate_Text {
def main(args: Array[String]): Unit = {
//1.创建SparkConf并设置App名称
val conf: SparkConf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[1]")
//2.创建SparkContext,该对象是提交Spark App的入口
val sc: SparkContext = new SparkContext(conf)
//3.1 读取输入文件
val inputRDD: RDD[String] = sc.textFile("input/1.txt")
//3.2 保存数据
inputRDD.saveAsTextFile("output")
//4.关闭连接
sc.stop()
}
}
4)注意:如果是集群路径:hdfs://hadoop102:9000/input/1.txt
如果JSON文件中每一行就是一个JSON记录,那么可以通过将JSON文件当做文本文件来读取,然后利用相关的JSON库对每一条数据进行JSON解析。
1)数据准备
在input目录下创建test.json文件,里面存储如下内容。
{"username": "zhangsan","age": 20}
{"username": "lisi","age": 18}
{"username": "wangwu","age": 16}
textFile是一行一行的读入数据,因此需要将json文件清洗成能够识别的格式。
2)代码实现
/**
* 读取Json格式数据
*/
object Spark03_readJson {
def main(args: Array[String]): Unit = {
//创建Spark配置文件对象
val conf: SparkConf = new SparkConf().setAppName("Spark03_readJson ").setMaster("local[*]")
//创建SparkContext
val sc: SparkContext = new SparkContext(conf)
//创建RDD
val rdd: RDD[String] = sc.textFile("E:\\spark-0701\\input\\test.json")
val resRDD: RDD[Option[Any]] = rdd.map(JSON.parseFull)
resRDD.collect().foreach(println)
//关闭连接
sc.stop()
}
}
输出:
Some(Map(username -> zhangsan, age -> 20.0))
Some(Map(username -> lisi, age -> 18.0))
Some(Map(username -> wangwu, age -> 16.0))
注意:使用RDD读取JSON文件处理很复杂,同时SparkSQL集成了很好的处理JSON文件的方式,所以应用中多是采用SparkSQL处理JSON文件。
SequenceFile文件是Hadoop用来存储二进制形式的key-value对而设计的一种平面文件(Flat File)。在SparkContext中,可以调用sequenceFile[keyClass, valueClass](path)
。
代码实现:
object Operate_Sequence {
def main(args: Array[String]): Unit = {
//1.创建SparkConf并设置App名称
val conf: SparkConf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[1]")
//2.创建SparkContext,该对象是提交Spark App的入口
val sc: SparkContext = new SparkContext(conf)
//3.1 创建rdd
val dataRDD: RDD[(Int, Int)] = sc.makeRDD(Array((1,2),(3,4),(5,6)))
//3.2 保存数据为SequenceFile
dataRDD.saveAsSequenceFile("output")
//3.3 读取SequenceFile文件
sc.sequenceFile[Int,Int]("output").collect().foreach(println)
//4.关闭连接
sc.stop()
}
}
2)注意:SequenceFile文件只针对PairRDD
对象文件是将对象序列化后保存的文件,采用Java的序列化机制。可以通过objectFilek,v函数接收一个路径,读取对象文件,返回对应的RDD,也可以通过调用saveAsObjectFile()实现对对象文件的输出。因为是序列化所以要指定类型。
1)代码实现
object Operate_Object {
def main(args: Array[String]): Unit = {
//1.创建SparkConf并设置App名称
val conf: SparkConf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[1]")
//2.创建SparkContext,该对象是提交Spark App的入口
val sc: SparkContext = new SparkContext(conf)
//3.1 创建RDD
val dataRDD: RDD[Int] = sc.makeRDD(Array(1,2,3,4))
//3.2 保存数据
dataRDD.saveAsObjectFile("output")
//3.3 读取数据
sc.objectFile[(Int)]("output").collect().foreach(println)
//4.关闭连接
sc.stop()
}
}
Spark的整个生态系统与Hadoop是完全兼容的,所以对于Hadoop所支持的文件类型或者数据库类型,Spark也同样支持。另外,由于Hadoop的API有新旧两个版本,所以Spark为了能够兼容Hadoop所有的版本,也提供了两套创建操作接口。对于外部存储创建操作而言,hadoopRDD和newHadoopRDD是最为抽象的两个函数接口。
支持通过Java JDBC访问关系型数据库。需要通过JdbcRDD进行,示例如下:
1)添加依赖
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.27version>
dependency>
2)准备数据,建立test数据库下user表
3)从MySql中读取数据:
/**
* 从MySql中读取数据 jdbc
*
* RDD参数:
* sc: SparkContext, Spark程序执行的入口,上下文对象
* getConnection: () => Connection, 获取数据库连接
* sql: String, 执行SQL语句
* lowerBound: Long, 查询的起始位置
* upperBound: Long, 查询的结束位置
* numPartitions: Int, 分区数
* mapRow: (ResultSet) => T = JdbcRDD.resultSetToObjectArray _ 对结果集的处理
*
* 步骤:
* 1.注册驱动
* 2.获取连接
* 3.创建数据库操作对象PrepareStatement
* 4.执行SQL
* 5.处理结果集
* 6.关闭连接
*/
object Spark04_MySQL_read {
def main(args: Array[String]): Unit = {
//创建Spark配置文件对象
val conf: SparkConf = new SparkConf().setAppName("Spark04_MySQL_read ").setMaster("local[*]")
//创建SparkContext
val sc: SparkContext = new SparkContext(conf)
//数据库连接4要素
var driver = "com.mysql.jdbc.Driver"
var url = "jdbc:mysql://hadoop102:3306/test"
var username = "root"
var password = "000000"
var sql: String = "select * from user where id >= ? and id <= ?"
//创建RDD
val resRDD = new JdbcRDD(
sc,
() => {
//注册驱动
Class.forName(driver)
//获取连接
DriverManager.getConnection(url, username, password)
},
sql,
1,
20,
2,
rs => (rs.getInt(1), rs.getString(2), rs.getInt(3))
)
//打印输出
resRDD.collect().foreach(println)
//关闭连接
sc.stop()
}
}
输出:
(1,xuzhu,20)
(2,duanyu,30)
(3,qiaofeng,40)
(4,lisi,20)
(5,zs,30)
4)向MySql中写入数据
object Spark05_MySQL_write {
def main(args: Array[String]): Unit = {
//创建Spark配置文件对象
val conf: SparkConf = new SparkConf().setAppName("Spark05_MySQL_write").setMaster("local[*]")
//创建SparkContext
val sc: SparkContext = new SparkContext(conf)
//数据库连接4要素
var driver = "com.mysql.jdbc.Driver"
var url = "jdbc:mysql://hadoop102:3306/test"
var username = "root"
var password = "000000"
//创建RDD
val rdd: RDD[(String, Int)] = sc.makeRDD(List(("banzhang", 30), ("jingjing", 18)))
rdd.foreachPartition {
//datas是RDD一个分区的数据 每个分区创建一个连接
datas => {
//1.注册驱动
Class.forName(driver)
//2.获取连接
val conn: Connection = DriverManager.getConnection(url, username, password)
//3.创建数据库操作对象PrepareStatement
//声明数据库操作的SQL语句
val sql: String = "insert into user(name,age) values(?,?)"
val ps: PreparedStatement = conn.prepareStatement(sql)
//这里的foreach不是算子,是集合的算子,所以不需要序列化ps
datas.foreach {
case (name, age) => {
ps.setString(1, name)
ps.setInt(2, age)
//4.执行SQL
ps.executeUpdate()
}
}
//6.关闭连接
ps.close()
conn.close()
}
}
//关闭连接
sc.stop()
}
}
累加器:分布式共享只写变量。(Task和Task之间不能读数据)
累加器用来对信息进行聚合,通常在向Spark传递函数时,比如使用map()函数或者用 filter()传条件时,可以使用驱动器程序中定义的变量,但是集群中运行的每个任务都会得到这些变量的一份新的副本,更新这些副本的值也不会影响驱动器中的对应变量。如果我们想实现所有分片处理时更新共享变量的功能,那么累加器可以实现我们想要的效果。
代码示例:
object Spark06_Accumulator {
def main(args: Array[String]): Unit = {
//创建Spark配置文件对象
val conf: SparkConf = new SparkConf().setAppName("Spark06_Accumulator").setMaster("local[*]")
//创建SparkContext
val sc: SparkContext = new SparkContext(conf)
//创建rdd
val rdd: RDD[(String, Int)] = sc.makeRDD(List(("a", 1), ("a", 2), ("a", 3), ("a", 4)))
//遍历求和
var sum: Int = 0
rdd.foreach {
case (word, count) => {
sum += count
}
}
println(sum)
//关闭连接
sc.stop()
}
}
输出:
0
在执行sum的时候只是在excutor上执行,即在副本上执行,而最后的输出是在Driver上的sum,副本上的执行并不会做修改,所以输出0。
要实现将excutor上的数据在Driver上聚合,就需要实现累加器。
通过在驱动器中调用SparkContext.accumulator(initialValue)方法(老版本),创建出存有初始值的累加器。返回值为org.apache.spark.Accumulator[T]对象,其中T是初始值initialValue的类型。Spark闭包里的执行器代码可以使用累加器的+=方法(在Java中是 add)增加累加器的值。驱动器程序可以调用累加器的value属性(在Java中使用value()或setValue())来访问累加器的值。
代码示例:
val rdd: RDD[(String, Int)] = sc.makeRDD(List(("a", 1), ("a", 2), ("a", 3), ("a", 4)))
val sum: LongAccumulator = sc.longAccumulator("myAcc")
rdd.foreach {
case (word, count) => {
sum.add(count) //调用累加器的add方法
}
}
print(sum.value)
//关闭连接
sc.stop()
输出:
10
注意:
(1)工作节点上的任务不能相互访问累加器的值。从这些任务的角度来看,累加器是一个只写变量。
(2)对于要在行动操作中使用的累加器,Spark只会把每个任务对各累加器的修改应用一次。因此,如果想要一个无论在失败还是重复计算时都绝对可靠的累加器,我们必须把它放在foreach()这样的行动操作中。转化操作中累加器可能会发生不止一次更新。
自定义累加器类型的功能在1.X版本中就已经提供了,但是使用起来比较麻烦,在2.0版本后,累加器的易用性有了较大的改进,而且官方还提供了一个新的抽象类:AccumulatorV2来提供更加友好的自定义类型累加器的实现方式。
1)自定义累加器步骤
(1)继承AccumulatorV2,设定输入、输出泛型
(2)重写方法
2)需求:自定义累加器,统计集合中首字母为“H”单词出现的次数。
List(“Hello”, “Hello”, “Hello”, “Hello”, “Hello”, “Spark”, “Spark”)
3)代码实现
/**
* 自定义累加器,统计出RDD中所有以H开头的单词
*/
object Spark07_Accumulator {
def main(args: Array[String]): Unit = {
//创建Spark配置文件对象
val conf: SparkConf = new SparkConf().setAppName("Spark01_CreateRDD_mem").setMaster("local[*]")
//创建SparkContext
val sc: SparkContext = new SparkContext(conf)
//创建RDD
val rdd: RDD[String] = sc.makeRDD(List("Hello", "Hello", "Hello", "Hello", "Hello", "Spark", "Spark"))
//创建累加器对象
val myAcc = new MyAccumulator()
//注册累加器
sc.register(myAcc)
//使用累加器
rdd.foreach {
word => {
myAcc.add(word)
}
}
//输出累加器结果
println(myAcc.value)
//关闭连接
sc.stop()
}
}
//定义一个类,集成AccumulatorV2
//泛型累加器输入和输出数据的类型
class MyAccumulator extends AccumulatorV2[String, mutable.Map[String, Int]] {
//定义一个集合,集合单词以及出现次数
var map = mutable.Map[String, Int]()
//是否为初始状态
override def isZero: Boolean = map.isEmpty
//拷贝
override def copy(): AccumulatorV2[String, mutable.Map[String, Int]] = {
val newAcc = new MyAccumulator
newAcc.map = this.map
newAcc
}
//重置
override def reset(): Unit = map.clear()
//向累加器中添加元素
override def add(v: String): Unit = {
if (v.startsWith("H")) {
map(v) = map.getOrElse(v, 0) + 1 //获取原来的值,并+1
}
}
//合并
override def merge(other: AccumulatorV2[String, mutable.Map[String, Int]]): Unit = {
var map1 = map //当前excutor的map
var map2 = other.value //另一个excutor的map
map = map1.foldLeft(map2) {
//mm表示map2,kv表示map1中的每一个元素
(mm, kv) => {
val k: String = kv._1
val v: Int = kv._2
//根据map1中的元素key,到map2中获取value
mm(k) = mm.getOrElse(k, 0) + v
mm
}
}
}
//获取累加器的值
override def value: mutable.Map[String, Int] = map
}
输出:
Map(Hello -> 5)
广播变量:分布式共享只读变量。
在多个并行操作中(Executor)使用同一个变量,Spark默认会为每个任务(Task)分别发送,这样如果共享比较大的对象,会占用很大工作节点的内存。
广播变量用来高效分发较大的对象。向所有工作节点发送一个较大的只读值,以供一个或多个Spark操作使用。比如,如果你的应用需要向所有节点发送一个较大的只读查询表,甚至是机器学习算法中的一个很大的特征向量,广播变量用起来都很顺手。
1)使用广播变量步骤:
(1)通过对一个类型T的对象调用SparkContext.broadcast创建出一个Broadcast[T]对象,任何可序列化的类型都可以这么实现。
(2)通过value属性访问该对象的值(在Java中为value()方法)。
(3)变量只会被发到各个节点一次,应作为只读值处理(修改这个值不会影响到别的节点)。
2)原理说明
3)代码示例:
object Spark08_Broadcast {
def main(args: Array[String]): Unit = {
//创建Spark配置文件对象
val conf: SparkConf = new SparkConf().setAppName("Spark08_Broadcast").setMaster("local[*]")
//创建SparkContext
val sc: SparkContext = new SparkContext(conf)
//创建RDD 实现类似Join效果(a,(1,4))(b,(2,5),(c,(3,6))
val rdd: RDD[(String, Int)] = sc.makeRDD(List(("a", 1), ("b", 2), ("c", 3)))
val list = List(("a", 4), ("b", 5), ("c", 6))
//创建一个广播变量
val broadcastList: Broadcast[List[(String, Int)]] = sc.broadcast(list)
//合并
val resRDD: RDD[(String, (Int, Int))] = rdd.map {
case (k1, v1) => {
var v3 = 0
for ((k2, v2) <- broadcastList.value) {
if (k1 == k2) {
v3 = v2
}
}
(k1, (v1, v3))
}
}
resRDD.collect().foreach(println)
//关闭连接
sc.stop()
}
}
输出:
(a,(1,4))
(b,(2,5))
(c,(3,6))