(1)添加依赖
org.apache.spark
spark-core_2.11
2.1.1
mysql
mysql-connector-java
8.0.16
(2)Mysql读取
package com.atguigu
import java.sql.{DriverManager,SQLException}
import org.apache.spark.rdd.JdbcRDD
import org.apache.spark.{SparkConf,SparkContext}
object MysqlRDD {
def main(args: Array[String]): Unit ={
//1.创建spark配置信息
val sparkConf: SparkConf=new SparkConf()
.setMaster("local[*]").setAppName("JdbcRDD")
//2.创建SparkContext
val sc=new SparkContext(sparkConf)
//3.定义连接mysql的参数
val driver= "com.mysql.cj.jdbc.Driver"val url= "jdbc:mysql://localhost:3306/sensor?serverTimezone=UTC"val userName= "root"val passWd= "******"try {
//创建JdbcRDD
val rdd=new JdbcRDD(
sc,()=> {
Class.forName(driver)
DriverManager.getConnection(url, userName,passWd)
},
"select * from `user` where `userid` between ? and ?;",
1,
1,
1,r=> (r.getInt(1), r.getString(2))
)
//打印最后结果
println(rdd.count())
rdd.foreach(println)
}catch{
case ex:SQLException=> println("sql exception occur!")
case ex:Exception=> println("other exception occur")
}finally {
sc.stop()
}
}
}
JdbcRDD的构造函数参数如下:
classJdbcRDD[T: ClassTag](
sc: SparkContext,
getConnection: ()=>Connection,
sql: String,
lowerBound: Long,//第一个占位符的值
upperBound: Long,//第二个占位符的值
numPartitions: Int,
mapRow: (ResultSet)=> T =JdbcRDD.resultSetToObjectArray _)//这个函数用于从resultset中获取特定的列,返回作为RDD分区中的元素extends RDD[T](sc, Nil) with Logging {
注意:一个select语句必须带两个占位符,第一个占位符的值为lowerbound,第二个占位符的值为upperbound。
那么:如果我指向查询一条记录呢,sql语句该怎么书写?
"select * from `user` where `userid` between ? and ?;"将lowerbound与upperbound设置为相同的值即可。
msyql写入
def main(args: Array[String]) {
val sparkConf= new SparkConf().setMaster("local[2]").setAppName("HBaseApp")
val sc= newSparkContext(sparkConf)
val data= sc.parallelize(List((1,"javok"),(2,"holuwo")))try{
data.foreachPartition(insertData)//对每个分区,调用一次insertData函数,将分区中的元素插入数据库
}catch{case ex:SQLException => println("sql exception occur!")case ex:Exception => println("other exception occur")
}finally{
sc.stop()
}
}
def insertData(iterator: Iterator[(Int,String)]): Unit={
Class.forName ("com.mysql.cj.jdbc.Driver").newInstance()
val conn=java.sql.DriverManager
.getConnection("jdbc:mysql://localhost:3306/sensor?serverTimezone=UTC", "root", "******")
iterator.foreach(data=>{
val ps= conn.prepareStatement("insert into rddtable('id','name') values (?,?)")
ps.setInt(1,data._1)
ps.setString(2, data._2)
ps.executeUpdate()
conn.close()
})
}
注意:这里采用了foreachPartition算子而不是foreach算子,分析如下:
如果有100条数据需要插入,它们分布在4个分区,如果采用foreach算子,就需要调用insertdata方法100次,创建和释放数据库连接100次;而如果采用foreachPartition算子,创建和释放连接只需要4次 。
所以,对于数据库连接这种敏感性强的资源,使用foreachPartittion算子具有重大意义
之前一直错误地认为foreach方法是在driver端执行的,因此传入insertdata方法不会导致序列化问题。后来又发现foreach执行在executor端,那么问题又来了:这里要传递一个insertdata方法给各个executor端执行,为啥没有出现像《RDD(九)——序列化问题》中的问题。
原因在于,insertdata方法是定义在一个object中的。下面的代码演示了,将该方法定义在另一个object中,以对象名引用该方法:
object InsertDataUtil {
def insertData(iterator: Iterator[(Int,String)])={
Class.forName("com.mysql.cj.jdbc.Driver").newInstance()
val conn=java.sql.DriverManager
.getConnection("jdbc:mysql://localhost:3306/sensor?serverTimezone=UTC", "root", "613692")
iterator.foreach(data=>{
val ps= conn.prepareStatement("INSERT INTO rddtable VALUES (?,?)")
ps.setInt(1, data._1)
ps.setString(2, data._2)
ps.executeUpdate()
conn.close()1;
})
}
}
...
data.foreachPartition(InsertDataUtil.insertData)
并没有出现序列化问题。原因在于object的引用方式类似于一个静态类引用静态方法,没有对象被创建,因此不会导致序列化问题。
如果将其定义在一个class里,会导致同样的序列化问题。
遇到的坑:MySQL数据库版本与MySQL驱动版本的不兼容导致的问题
我的MySQL数据库版本是8.0以上,如果采用5.0的驱动,会抛出异常:java.sql.SQLException: Unable to load authentication plugin 'caching_sha2_password'.更新到8.0的驱动即可。
同时,8.0版本的驱动名称变为:com.mysql.cj.jdbc.Driver;否则会报错:“无法找到driver”;url也要加上时区信息: url = "jdbc:mysql://localhost:3306/sensor?serverTimezone=UTC";否则会报错:“无法确定时区”。