前面文章写入如何将hdfs的数据分表插入mysql。这里主要讲解如何进行分库分表抽取。
需求:
公司线上库的一些数据量比较大的表是存在多库多表的。数仓进行数据抽取的时候,需要将这些表统一抽取过来,然后进行合并到一张hive表。
以前的方案:1、通过 mysqluldr2 这个工具将多表抽取到本地,user_1.txt,user_2.txt,user_3.txt…。
2、cat user*.txt > user.txt
3、hdfs dfs -put user.txt
4.load data inpath ‘’ into table …
原来方案的数据文件都要落地,同时源码在某次编译过程中丢失,需要进行重构。
新方案使用spark进行抽取:
1、mysqljdbc读取数据,write到hdfs文件
2、将hdfs文件映射到表插入最终src表
大概代码如下:
sc.parallelize(newImportConfs,1).map(conf=>(conf.getHashCode,conf)).partitionBy(new HashPartitioner(partitionNum)).foreachPartition(
partition =>{
val partitionId = TaskContext.get.partitionId
var conn:Connection = null
var ps:PreparedStatement = null
var rs:ResultSet = null
var os : FSDataOutputStream = null
var fs : FileSystem = null
try {
// 将数据写入hdfs
val conf = new Configuration()
conf.set("fs.defaultFS", "hdfs://nameservice1");
val fileName = tmpTb + "_" + partitionId.toString
val path: String = hdfsPath + "/" + fileName + ".txt";
fs = FileSystem.newInstance(conf);
val pathFile = new Path(path)
//判断path是否存在,存在则append,不存在则create
if (!fs.exists(pathFile)) {
logger.info("文件" + path + "不存在,进行创建")
os = fs.create(pathFile)
} else {
logger.info("文件已存在,先进行删除"+path)
fs.delete(pathFile, true)
os = fs.create(pathFile)
}
partition.foreach(result => {
val importConf = result._2
val jdbcModel = importConf.getJdbcModel
val query = importConf.getQuery
conn = DbUtils.getConnection(jdbcModel)
ps = conn.prepareStatement(query, ResultSet.TYPE_FORWARD_ONLY,
ResultSet.CONCUR_READ_ONLY)
ps.setFetchSize(Integer.MIN_VALUE);
ps.setFetchDirection(ResultSet.FETCH_REVERSE);
rs = ps.executeQuery();
val metaData = rs.getMetaData
val colNum = metaData.getColumnCount
var schemaString = ""
for (x <- 0 to colNum - 1) {
if (x < colNum - 1) {
schemaString = schemaString + metaData.getColumnName(x + 1) + " "
} else {
schemaString = schemaString + metaData.getColumnName(x + 1)
}
}
while (rs.next()) {
var line: String = ""
for (i <- 0 to colNum - 1) {
val colType = metaData.getColumnType(i + 1)
val value: String = getObjectValue(rs.getString(i + 1), colType)
if (i < colNum - 1) {
line = line + value + "\001"
} else {
line = line + value + "\n"
}
}
os.write(line.getBytes("utf-8"))
}
rs.close()
ps.close()
conn.close()
}
)
}catch{
case e:Exception => {
e.printStackTrace()
val result = new StringWriter();
import java.io.PrintWriter
val printWriter = new PrintWriter(result)
e.fillInStackTrace.printStackTrace(printWriter)
throw new Exception("获取数据异常"+e.getMessage+"=="+result.toString)
}
}finally {
if (os != null) {
os.close()
}
if (fs != null) {
fs.close()
}
}
}
总结:
1、mysql读取大量数据是需要做fetchsize设置
ps = conn.prepareStatement(query, ResultSet.TYPE_FORWARD_ONLY,
ResultSet.CONCUR_READ_ONLY)
ps.setFetchSize(Integer.MIN_VALUE);
ps.setFetchDirection(ResultSet.FETCH_REVERSE);
防止一次性读取mysql数据发生omm。设置成流式读取,只会读取一部分数据,这样就能防止JVM OMM
2、原先想着jdbc读出的数据转成一个dataframe,然后通过spark.sql插入到tmp表。后来过程中发现executor端无法将一个list转成rdd。具体报错应该是list转成dataset时会报错。list.toDF()无法使用。根本原因是executor无法读到sparkContext和spark。所以转化时会报空指针。最后改成了直接将数据写到hdfs文件,然后映射到hive表。
3、原先代码是没有加foreachpartition的直接是foreach。然后在里面打开fs,如果存在则append,如果不存在则create。这样有的时候会导致hdfs打开文件异常,报错 file cant create because file is ready being create…错误原因我猜应该是:task再失败情况下会重跑,然后fs虽然close,但是这个失败的fs未释放这个文件的锁,导致后面的文件无法append。