前言
我们通常使用 sparksql 来读取 mysql, postgres 或者其他数据库中的数据 基本的方式, 一般是使用 DataFrameReader 来进行读取 DataFrame, 然后 转换为 RDD
但是这样 读取是有一些 限制的, 呵呵 因此 有些时候 可能需要自己 扩展一下吧, 当然 可能也存在一些 考虑的不足的地方
呵呵, 最近 在想 自定义一个 JdbcRDD 来实现我期望的效果, 主要的 feature 是在于可以基于 任何字段 进行 partition 吧, 不过当时 还是没有看到 带 predicates 这个 jdbc 方法, 呵呵 不然 就懒得写了
以下调试基于 jdk1.8 + scala2.12 + spark3.0.0
DataFrameReader.jdbc(url, table, properties)
jdbc 这块差不多是有三个方法吧, 我们一个一个的看
首先是 如下第一个经常使用的, table 可以传入 sql, 但是 这个创建的 DataFrame 转换为 RDD 之后只有一个 partition, 可能在 不能完全的发挥 并行计算的优势, 但是这样 计算相比于 查询出数据 然后构造RDD, 是要更加优秀一些
调试一下 JdbcRDD, 看一下 其运行时的一些情况
测试代码大致如下, 下面的调试 几乎都是基于如下测试代码, 不过是 调用 jdbc 的方式存在一些区别
@Test
public void test02DfJdbcRdd() {
String sql = " select * from task where created_on > 0 ";
JavaRDD<Task> rdd = sparkTemplate.readSqlByDataFrame(sql, Task.class);
Object result = rdd.collect();
int x = 0;
// String sql = " select * from task where created_on > 0 ";
// JavaRDD<Task> rdd = sparkTemplate.readSqlByDataFrame(sql, "created_on", 0L,
// System.currentTimeMillis(), 3, Task.class);
// Object result = rdd.collect();
// int x = 0;
// String sql = " select * from task where created_on > 0 ";
// String[] predicates = new String[3];
// String partitionCondTemplate = "(('0'||translate(substr(%s, 0, 8), 'abcdef-', ''))::int %% %s = %s)";
// for (int i = 0; i < predicates.length; i++) {
// predicates[i] = String.format(partitionCondTemplate, "id", predicates.length, i);
// }
// JavaRDD<Task> rdd = sparkTemplate.readSqlByDataFrame(sql, predicates, Task.class);
// Object result = rdd.collect();
// int x = 0;
// String sql = " select * from task where created_on > 0 ";
// JavaRDD<Task> rdd = sparkTemplate.readSqlByMyJdbcRDD(sql, Task.class, 3);
// Object result = rdd.collect();
// int x = 0;
}
JdbcRDD 的运行时状态如下图
这里的 sqlText 的内容大致如下
可以看到的是 基于原有的 业务sql 稍微封装了一下, 然后 传递发送给数据库执行
jdbc(url, table, properties) 创建的是一个 Partition 的 JdbcRDD
SELECT "id","created_on" FROM ( select * from task where created_on > 0 )alias
DataFrameReader.jdbc(url, table, columnName, lowerBound, upperBound, numPartitions, properties)
然后是如下方法, 相比于上面的方法, 可以传入 partition 的字段, 以及他的上下界, 以及 分区的数量
分区的方式主要是根据 range, 吧 lowerBound 和 upperBound 拆分成 numPartitions 个 Range 对应 numPartitions 个 Partition, 每一个 Partition 在原有的 sql 基础上面限定 分区字段的范围来进行查询
JdbcRDD 的运行时状态如下图
partition0 如下
partition1 如下
partition2 如下
根据 lowerBound, upperBound 以及 numPartitions 来进行划分了一下 每一个 Partition 的范围
jdbc(url, table, columnName, lowerBound, upperBound, numPartitions, properties) 创建的是一个 numPartitions 个 Partition 的 JdbcRDD, 每一个 Partition 是根据 lowerBound, upperBound 以及 numPartitions 来进行划分的
每一个 Partition 在原有的业务 sql 限定的基础上面, 只获取当前 范围的数据
但是缺点是 只支持 数字类型 的字段, 并且需要 知道分区列的 lowerBounds, upperBounds
DataFrameReader.jdbc(url, table, predicates, properties)
然后是如下方法, 相比于只传递 url, table, proerties 的 jdbc, 这里会多传递一个 predicates 列表, 主要约束的是 各个 partition 对应的限定条件
这个更加抽象一些, 基于 predicates 也能处理大部分的业务问题了吧, 比如 可以基于这个实现上面的 lowerBounds, upperBounds 的功能
也可以实现其他的自定义的 partition 的方式, 比如 根据创建时间 取模 来进行 partition 的处理
呵呵 当然 我这里的这个 MyJdbcRDD 也是可以通过这个 方法来进行实现, 早知如此, 就不自己写了, 不过还好 多了一些了解
JdbcRDD 的运行时状态如下图
partition0 如下
partition1 如下
partition2 如下
jdbc(url, table, predicates, properties) 创建的是一个 predicates.length 个 Partition 的 JdbcRDD, 每一个 Partition 查询的数据是在原有的业务 sql 上面加上 predicates[i] 的限定
每一个 Partition 在原有的业务 sql 限定的基础上面, 只获取当前 范围的数据
这个就很抽象了, 基本上是可以 实现你想要的任何 Partition 方式
MyJdbcRDD
呵呵 上面看了 官方提供的几个 api 接口
我们这里 来看一下 MyJdbcRDD, 当然 看到了 jdbc(url, table, predicates, properties) 这个之后, 我个人 觉得 MyJdbcRDD 已经是不需要了, ^_^, 不过 还是可以 带着学习的心态 来看一下
当然 处理的方式 在最终的实现类似于 jdbc(url, table, predicates, properties)
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.yocoyt.framework.util.SpringContextUtil;
import org.apache.spark.Partition;
import org.apache.spark.SparkContext;
import org.apache.spark.TaskContext;
import org.apache.spark.rdd.RDD;
import scala.collection.AbstractIterator;
import scala.collection.Iterator;
import scala.collection.mutable.ArrayStack;
import scala.reflect.ClassTag;
import javax.sql.DataSource;
import java.io.Serializable;
import java.sql.*;
/**
* MyJdbcRDD
*
* @author Jerry.X.He <970655147@qq.com>
* @version 1.0
* @date 2020-10-28 17:57
*/
public class MyJdbcRDD<T extends Serializable> extends RDD<T> {
private String originalSql;
private int numSlices;
private Class<T> clazz;
public MyJdbcRDD(SparkContext sc,
Class<T> clazz,
String originalSql,
int numSlices) {
super(sc, new ArrayStack<>(), ClassTag.apply(clazz));
this.numSlices = numSlices;
this.originalSql = originalSql;
this.clazz = clazz;
}
public MyJdbcRDD(SparkContext sc,
Class<T> clazz,
String originalSql) {
this(sc, clazz, originalSql, 20);
}
@Override
public Iterator<T> compute(Partition split, TaskContext context) {
boolean sqlContainsCond = originalSql.toLowerCase().contains("where");
String structureTemplate = sqlContainsCond ? " %s and %s " : " %s where %s ";
String partitionCondTemplate = "(('0'||translate(substr(md5(%s), 0, 8), 'abcdef-', ''))::int %% %s = %s)";
// String compositeKey = "concat(source_card_number, '_', target_card_number)";
String compositeKey = "id";
String partitionCond = String.format(partitionCondTemplate, compositeKey, numSlices, split.index());
String sqlToUse = String.format(structureTemplate, originalSql, partitionCond);
if (numSlices == 1) {
sqlToUse = originalSql;
}
DataSource dataSource = SpringContextUtil.getContext().getBean(DataSource.class);
try {
Connection connection = dataSource.getConnection();
PreparedStatement ps = connection.prepareStatement(sqlToUse);
return new MyIterator<T>(connection, ps.executeQuery(), clazz);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
@Override
public Partition[] getPartitions() {
Partition[] result = new Partition[numSlices];
for (int i = 0; i < numSlices; i++) {
result[i] = new MyJdbcPartition(i);
}
return result;
}
/**
* MyJdbcPartition
*
* @author Jerry.X.He
* @version 1.0
* @date 2020-10-28 18:01
*/
static class MyJdbcPartition implements Partition {
private int idx;
public MyJdbcPartition(int idx) {
this.idx = idx;
}
@Override
public int index() {
return idx;
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public boolean equals(Object other) {
return super.equals(other);
}
}
/**
* $desc
*
* @author Jerry.X.He
* @version 1.0
* @date 2020-10-28 18:09
*/
static class MyIterator<T> extends AbstractIterator<T> {
private Connection connection;
private Class<T> clazz;
private ResultSet rs;
private String[] cloumnNames;
private Boolean hasNext;
public MyIterator(Connection connection, ResultSet rs, Class<T> clazz) {
this.connection = connection;
this.clazz = clazz;
this.rs = rs;
try {
ResultSetMetaData rsMeta = rs.getMetaData();
int colNum = rsMeta.getColumnCount();
cloumnNames = new String[colNum];
for (int i = 0; i < colNum; i++) {
cloumnNames[i] = rsMeta.getColumnName(i + 1);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
@Override
public boolean hasNext() {
try {
if (hasNext != null) {
return hasNext;
}
hasNext = rs.next();
if (!hasNext) {
connection.close();
}
return hasNext;
} catch (Exception e) {
return false;
}
}
@Override
public T next() {
try {
JSONObject res = new JSONObject();
for (int i = 0; i < cloumnNames.length; i++) {
res.put(cloumnNames[i], rs.getString(i + 1));
res.put(underLine2Camel(cloumnNames[i]), rs.getObject(i + 1));
}
return JSON.toJavaObject(res, clazz);
} catch (SQLException e) {
e.printStackTrace();
return null;
} finally {
hasNext = null;
}
}
/**
* 下滑线 转换为 驼峰
*
* @param name name
* @return java.lang.String
* @author Jerry.X.He
* @date 2020-10-29 16:05
*/
public static String underLine2Camel(String name) {
Character underLine = '_';
StringBuilder sb = new StringBuilder(name.length() + 10);
for (int i = 0; i < name.length(); i++) {
char ch = name.charAt(i);
if (!underLine.equals(ch)) {
sb.append(ch);
continue;
}
char nextCh = name.charAt(i + 1);
if (Character.isLowerCase(nextCh) || Character.isUpperCase(nextCh)) {
sb.append(Character.toUpperCase(name.charAt(i + 1)));
// skip '_', the end of loop skip 'nextCh'
i++;
} else {
sb.append(ch);
}
}
return sb.toString();
}
}
}
JdbcRDD 的运行时状态如下图
partition0 如下
partition1 如下
partition2 如下
呵呵, 这么看起来 是不是 就和上面的 jdbc(url, table, predicates, properties) 差不多了
当然 还是官方的 api, 更加健壮, 更加高效, 更加抽象, 更加推荐使用
完