定义
Spark SQL
可以通过DataFream
接口操作各种数据源。可以通过关系转换或者临时表来操作DataFrame
。这里我们将介绍通用的数据源加载方法和数据保存方法。
通用加载/保存方法
Spark
默认的数据源格式为Parquet
格式,数据源格式问Parquet
文件的时候,Spark
读取数据的时候不需要指定具体的格式,如果想要修改默认的数据格式,就需要修改spark.sql.sources.default
参数
def main(args: Array[String]): Unit = {
val spark: SparkSession = SparkSession.builder().appName("DataFrameTest").master("local[2]").getOrCreate()
//因为Parquet格式的文件时Spark加载数据的默认格式,所以不需要指定format格式
val personDF: DataFrame = spark.read.load("hdfs://xxxxx:8020/testfile/person.parquet")
personDF.show()
}
如果我们输入的数据文件格式不是Parquet
,那么我们就需要手动指定读取的数据源格式。数据源格式需要指定全名(org.apache.spark.sql.parquet)
,如果手动指定的数据源格式为spark内置格式,只需要指定简称,如json、parquet、jdbc、text、orc、libsvm、csv
。
def main(args: Array[String]): Unit = {
val spark: SparkSession = SparkSession.builder().appName("DataFrameTest").master("local[2]").getOrCreate()
//从本地读取json格式的数据,因为json不是spark默认的数据源格式,所以需要手动指定数据源格式为json
val personDF: DataFrame = spark.read.format("json").load("C:\\Users\\39402\\Desktop\\person.json")
//利用Dataframe的write和save方法将本地读取到的数据以parquet的格式写到HDFS上,
personDF.write.format("parquet").save("hdfs://xxxxx:8020/testfile/person.parquet")
}
除此之外我们也可以把SQL
执行在数据源文件上,下边就是一个例子,它将SQL
运行在文件上。
def main(args: Array[String]): Unit = {
val spark: SparkSession = SparkSession.builder().appName("DataFrameTest").master("local[2]").getOrCreate()
//将SQL执行在数据源的文件上,一定要注意书写格式
spark.sql("select * from parquet.`hdfs://xxxxx:8020/testfile/person.parquet`").show()
}
文件保存选项
当我们的SparkSQL
处理完数据以后,需要向本地或者文件系统保存数据,我们可以利用SaveModel
指定保存策略,,例如文件已经存在就抛异常、覆盖原数据等。
代码例子
def main(args: Array[String]): Unit = {
val spark: SparkSession = SparkSession.builder().appName("DataFrameTest").master("local[2]").getOrCreate()
//从本地读取json格式的数据,因为json不是spark默认的数据源格式,所以需要手动指定数据源格式为json
val personDF: DataFrame = spark.read.format("json").load("C:\\Users\\39402\\Desktop\\person.json")
//指定SaveModel策略为如果文件已存在就抛异常
personDF.write.mode(SaveMode.ErrorIfExists).save("hdfs://xxxx:8020/testfile/person.parquet")
}
SaveModel
保存策略表
Scala/Java | Meaning |
---|---|
SaveMode.ErrorIfExists (default) | 如果数据已经存在,将DataFrame保存到数据源时,则预计会抛出异常。 |
SaveMode.Append | 如果data / table已经存在,将DataFrame保存到数据源时,则DataFrame的内容将被添加到现有数据中。 |
SaveMode.Overwrite | 覆盖模式意味着将DataFrame保存到数据源时,如果data / table已经存在,则现有数据将被DataFrame的内容覆盖。 |
SaveMode.Ignore | 忽略模式意味着,当将DataFrame保存到数据源时,如果数据已经存在,保存操作将不会保存DataFrame的内容,也不会更改现有数据。这与CREATE TABLE IF NOT EXISTSSQL中的类似。 |
Parquet文件
Parquet
是一种列式存储格式,可以很高效的存储具有嵌套的数据,目前是最流行的一种存储格式。SparkSQL
默认的读写数据的格式就是Parquet
,可想而知该格式是多么高效。在海量数据中,我们一般存储在分布式文件系统中,并且以分区的方式存储,也就是按照一定的规律拆分成小的文件。那么Parquet
数据源就可以自动发现并解析分区信息。SparkSQL
自动解析分区的参数为spark.sql.sources.partitionColumnTypeInference=true
,默认是开启,想要关闭就设置成disabled
Hive数据库
Hive
是Hadoop
为了减缓编写MapReduce
程序而存在的一种SQL
引擎,它最终会被翻译成MapReduce
程序。SparkSQL
支持操作现有的Hive
仓库。由于Hive
有很多的依赖,这些依赖不会包含在Spark
中,当在Spark
没有在ClassPath
中发现这些依赖,那么Spark
就会自动加载它们,特别注意的是,这些依赖一定要在所有节点上出现,因为它们需要访问Hive的序列化和反序列库,以便于访问Hive
上的数据。所谓的依赖其实就是将HIVE_HOME/hive-site.xml、HADOOP_HOME/etc/hadoop/core-site.xml、HADOOP_HOME/etc/hadoop/hdfs-site.xml
文件复制到Spark
目录下的conf
目录中。特别注意的是在编译Spark
的时候必须添加Hive
依赖。如果在Spark中运行有关Hive的操作,这个时候如果你的Hive
环境还没有部署好,那么Spark
会在当前的工作目录中创建Hive
的元数据仓库,叫做metastore_db
。利用SparkSQL
运行Hive存储数据的默认目录是HDFS
上的/user/hive/warehouse
。以上环境准备好了,那么我们怎么样才能在Spark中操作Hive呢,请看下边的代码例子。
object HiveDataSourceTest {
//Hive数据存储在HDFS上的目录
val hiveDataPathUrl = "/user/hive/warehouse"
def main(args: Array[String]): Unit = {
//初始化SparkSession,并支持Hive操作,然后hive数据在hdfs上的存储目录。
val spark: SparkSession = SparkSession.builder()
.appName("HiveDataSourceTest")
.master("local[2]") //在Spark集群上运行的时候要去掉
.config("spark.sql.warehouse.dir", hiveDataPathUrl)
.enableHiveSupport()
.getOrCreate()
//利用spark sql创建Hive表
spark.sql("CREATE TABLE IF NOT EXISTS person(name:StRING,age:INT) ROW FORMAT DELIMITED " +
"FIELDS TERMINATED BY ',' ")
//利用spark sql 将数据加载到Hive表中
spark.sql("LOAD DATA LOCAL INPATH '/opt/data/person.json' INTO TABLE person ")
//利用spark sql 查询Hive表
spark.sql("select * from person").show()
//关闭SparkSession
spark.close()
}
}
使用spark-shell操作外部Hive
当我们使用已存在的Hive
仓库,需要操作其上的数据,那我们就需要将HIVE_HOME/conf
下的hive-site.xml
拷贝到SPARK_HOME/conf
下,然后在启动spark-shell
的时候,一定要加上--jars /xx/xx/mysql-connector-java-xx.xx.jar
,因为Hive
元数据存在Mysql
中,Driver
程序需要连接Mysql
。
spark-shell --jars /opt/lib/mysql-connector-java-6.0.1.jar
JSON数据库
SparkSQL
能够自动推测出JSON
数据集的结构,并将它加载成一个DataSet[Row]
,也就是一个DataFrame。可以通过spark.read.json()
的方式去读取JSON
文件或者JSON
字符串,然后返回一个DataFrame
数据集。
def main(args: Array[String]): Unit = {
val spark: SparkSession = SparkSession.builder().appName("DataFrameTest").master("local[2]").getOrCreate()
//第一种读取本地或者文件系统上的json文件
val personDF: DataFrame = spark.read.json("C:\\Users\\39402\\Desktop\\person.json")
//导入隐式转换
import spark.implicits._
//第二种读取json字符串,
val jsonDataSet: Dataset[String] = spark.createDataset("""{"name":"Yin","address":{"city":"Columbus","state":"Ohio"}}""" :: Nil)
//将DataSet转换成RDD,然后作为json方法的参数
spark.read.json(jsonDataSet.rdd).show()
}
JDBC
SparkSQL
也包含了一种能够通过JDBC
读取其他的数据源的数据源。这种方式比使用JdbcRDD
的性能更高。这是因为这种方式的返回值是DataFrame
,这样就可以利用SparkSQL
进行处理,或者是跟其他数据源交互。JBDC
这种方式还有的一种优点就是可以很容易利用Java
或者Python
操作。以下是SparkSQL
利用JDBC
方式操作Mysql
数据库的代码。
package com.lyz.datasource
import java.util.Properties
import org.apache.spark.sql.{DataFrame, SaveMode, SparkSession}
object JDBCDataSourceTest {
def main(args: Array[String]): Unit = {
val spark: SparkSession = SparkSession.builder().appName("DataFrameTest").master("local[2]").getOrCreate()
//SparkSQL利用JDBC读取Mysql数据第一种方式
val table1: DataFrame = spark.read.format("jdbc")
.option("url", "jdbc:mysql://localhost:3306/kettle_test?zeroDateTimeBehavior=convertToNull&characterEncoding=utf8&serverTimezone=GMT&useSSL=false")
.option("dbtable", "local_table")
.option("driver", "com.mysql.cj.jdbc.Driver")
.option("user", "root")
.option("password", "12345")
.option("fetchsize", 5)
.option("batchsize", 10).load()
//SparkSQL利用JDBC读取Mysql数据第二种方式
val connectionProperties: Properties = new Properties()
connectionProperties.put("user", "root")
connectionProperties.put("password", "12345")
val table2: DataFrame = spark.read.jdbc("jdbc:mysql://localhost:3306/kettle_test?zeroDateTimeBehavior=convertToNull&characterEncoding" +
"=utf8&serverTimezone=GMT&useSSL=false", "local_table", connectionProperties)
//SparkSQL利用JDBC写数据到Mysql中的第一种方式
table1.write.mode(SaveMode.Append).format("jdbc").option("url", "jdbc:mysql://localhost:3306/kettle_test?zeroDateTimeBehavior=convertToNull&characterEncoding=utf8&serverTimezone=GMT&useSSL=false")
.option("dbtable", "remote_table")
.option("driver", "com.mysql.cj.jdbc.Driver")
.option("user", "root")
.option("password", "12345")
.option("fetchsize", 5)
.option("batchsize", 10).save()
//SparkSQL利用JDBC写数据到Mysql中的第二种方式
table2.write.mode(SaveMode.Append).jdbc("jdbc:mysql://localhost:3306/kettle_test?zeroDateTimeBehavior=convertToNull&characterEncoding" +
"=utf8&serverTimezone=GMT&useSSL=false", "remote_table", connectionProperties)
}
}
MongDB
package com.lyz
import java.net.InetAddress
import com.mongodb.casbah.commons.MongoDBObject
import com.mongodb.casbah.{MongoClient, MongoClientURI}
import org.apache.spark.SparkConf
import org.apache.spark.sql.{DataFrame, SaveMode, SparkSession}
import org.elasticsearch.action.admin.indices.create.CreateIndexRequest
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest
import org.elasticsearch.action.admin.indices.exists.indices.IndicesExistsRequest
import org.elasticsearch.common.settings.Settings
import org.elasticsearch.common.transport.InetSocketTransportAddress
import org.elasticsearch.transport.client.PreBuiltTransportClient
object dataloader {
val MOVIC_PATH = "D:\\workspace-idea\\java\\recommend-system\\movie-recommend-system\\data-loader\\src\\main\\resources\\movies.csv"
val MONGODB_MOVIE_COLLECTION = "Movie"
//主函数,程序的入口
def main(args: Array[String]): Unit = {
val config = Map(
"spark.cores" -> "local[*]",
"mongodb.url" -> "mongodb://xxxx:27017/recommend",
"mongodb.db" -> "recommend")
//创建一个Spark配置对象
val sparkConf = new SparkConf().setAppName("DataLoader").setMaster(config("spark.cores"))
//创建一个SparkSession
val spark: SparkSession = SparkSession.builder().config(sparkConf).getOrCreate()
import spark.implicits._
//读取Moive数据并转换成DataFrame
val movieDF: DataFrame = spark.sparkContext.textFile(MOVIC_PATH).map(line => {
val attr: Array[String] = line.split("\\^")
Movie(attr(0).toInt, attr(1).trim, attr(2).trim, attr(3).trim, attr(4).trim, attr(5).trim, attr(6).trim, attr(7).trim, attr(8).trim, attr(9).trim)
}).toDF()
//声明一个隐式转换,
implicit val mongoConfig = MongoConfig(config("mongodb.url"), config("mongodb.db"))
saveDataIntoMongodb(movieDF, ratingDF, tagDF)
spark.stop()
}
def saveDataIntoMongodb(movie: DataFrame, rating: DataFrame, tag: DataFrame)(implicit mongoConfig: MongoConfig): Unit = {
val mongoClient = MongoClient(MongoClientURI(mongoConfig.uri))
//判断MongoDB中是否存在了对应的数据库,如果存在就该删除它
mongoClient(mongoConfig.db)(MONGODB_MOVIE_COLLECTION).dropCollection()
//向MongoDB写Movie数据
movie.write
.option("uri", mongoConfig.uri)
.option("collection", MONGODB_MOVIE_COLLECTION)
.mode(SaveMode.Overwrite)
.format("com.mongodb.spark.sql")
.save()
//为mogondb里的表建立索引
//为MongoDB中的Movie表创建索引,索引为mid,并且排序规则为升序
mongoClient(mongoConfig.db)(MONGODB_MOVIE_COLLECTION).createIndex(MongoDBObject("mid" -> 1))
//关闭mongoDB的客户端
mongoClient.close()
}
}
/**
* MongoDB的连接配置
*
* @param uri MongoDB的连接
* @param db MongoDB要操作数据库
*/
case class MongoConfig(uri: String, db: String)
ElasticSearch
package com.lyz
import java.net.InetAddress
import com.mongodb.casbah.commons.MongoDBObject
import com.mongodb.casbah.{MongoClient, MongoClientURI}
import org.apache.spark.SparkConf
import org.apache.spark.sql.{DataFrame, SaveMode, SparkSession}
import org.elasticsearch.action.admin.indices.create.CreateIndexRequest
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest
import org.elasticsearch.action.admin.indices.exists.indices.IndicesExistsRequest
import org.elasticsearch.common.settings.Settings
import org.elasticsearch.common.transport.InetSocketTransportAddress
import org.elasticsearch.transport.client.PreBuiltTransportClient
object dataloader {
val MOVIC_PATH = "D:\\workspace-idea\\java\\recommend-system\\movie-recommend-system\\data-loader\\src\\main\\resources\\movies.csv"
val ES_MOVIE_INDEX = "Movie"
//主函数,程序的入口
def main(args: Array[String]): Unit = {
val config = Map(
"spark.cores" -> "local[*]",
"es.transportHosts" -> "xxxx:9300",
"es.index" -> "recommend",
"es.cluster.name" -> "es-cluster")
//创建一个Spark配置对象
val sparkConf = new SparkConf().setAppName("DataLoader").setMaster(config("spark.cores"))
//创建一个SparkSession
val spark: SparkSession = SparkSession.builder().config(sparkConf).getOrCreate()
import spark.implicits._
//读取Moive数据并转换成DataFrame
val movieDF: DataFrame = spark.sparkContext.textFile(MOVIC_PATH).map(line => {
val attr: Array[String] = line.split("\\^")
Movie(attr(0).toInt, attr(1).trim, attr(2).trim, attr(3).trim, attr(4).trim, attr(5).trim, attr(6).trim, attr(7).trim, attr(8).trim, attr(9).trim)
}).toDF()
//声明一个ES配置的隐式参数
implicit val eSConfig = ESConfig(
config("es.httpHosts"),
config("es.transportHosts"),
config("es.index"),
config("es.cluster.name"))
saveDataIntoEs(movieDF)
spark.stop()
}
def saveDataIntoEs(movie: DataFrame)(implicit eSConfig: ESConfig): Unit = {
//新建一个配置
val esSetting: Settings = Settings.builder().put("cluster.name", "es-cluster").build()
//需要将TransportHosts添加到esClient中
val REGEX_HOST_PORT = "(.+):(\\d+)".r
val esClient = new PreBuiltTransportClient(esSetting)
eSConfig.transportHosts.split(",").foreach {
//模式匹配,忽略掉不合规的URL地址
case REGEX_HOST_PORT(host: String, port: String) =>
esClient.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName(host), port.toInt))
}
//判断es中是否还存在旧的数据索引
if (esClient.admin().indices().exists(new IndicesExistsRequest(eSConfig.index)).actionGet().isExists) {
//删除旧的索引数据
esClient.admin().indices().delete(new DeleteIndexRequest(eSConfig.index))
}
//创建索引
esClient.admin().indices().create(new CreateIndexRequest(eSConfig.index))
movie.write.
option("es.nodes", eSConfig.httpHosts)
.option("es.http.timeout", "100m")
.option("es.mapping.id", "mid")
.mode(SaveMode.Overwrite)
.format("org.elasticsearch.spark.sql")
.save(eSConfig.index + "/" + ES_MOVIE_INDEX)
esClient.close()
}
}
case class Movie(mid: Int, name: String, descri: String, timelong: String, issue: String,
shoot: String, language: String, genres: String, actors: String, directors: String)
/**
* ElasticSearch的连接配置
*
* @param httpHosts Http的主机列表,以,分割
* @param transportHosts Transport主机列表, 以,分割
* @param index 需要操作的索引
* @param clustername ES集群的名称,
*/
case class ESConfig(httpHosts: String, transportHosts: String, index: String, clustername: String)
性能数据调优
在SparkSQL
作业中,我们可利用缓存数据来调优,将数据缓存在内存中,提高执行效率,我们可以利用spark.catalog.cacheTable("tableName")
或者利用dataFrame.cache()
方法来将数据缓存在内存中。删除缓存数据可以调用spark.catalog.uncacheTale("tableName")
来删除缓存数据。我们可以根据实际情况来设置缓存大小等一下参数。spark.conf(key,value)
来设置。下表是具体的参数
参数名称 | 默认值 | 具体的意思 |
---|---|---|
spark.sql.inMemoryColumnarStorage.compressed | true | 是否为每列选择压缩编解码器 |
spark.sql.inMemoryColumnarStorage.batchSize | 10000 | 控制缓存批次大小 |