本文旨在介紹 Spark 通過JDBC讀取數據庫數據的四種API
調用前準備
對於不同的數據庫,需要在spark的環境中添加對應的依賴包,如:
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.1.4</version>
</dependency>
四種API簡介
1.單分區模式
函數:
def jdbc(url: String, table: String, properties: Properties): DataFrame
使用示例:
val url = "jdbc:mysql://mysqlHost:3306/database"
val tableName = "table"
// 設置連接用戶&密碼
val prop = new java.util.Properties
prop.setProperty("user","username")
prop.setProperty("password","pwd")
// 取得該表數據
val jdbcDF = sqlContext.read.jdbc(url,tableName,prop)
// 一些操作
....
從入參可以看出,只需要傳入JDBC URL、表名及對應的賬號密碼Properties即可。但是計算此DF的分區數後發現,這種不負責任的寫法,併發數是1
jdbcDF.rdd.partitions.size=1
操作大數據集時,spark對MySQL的查詢語句等同於可怕的:select * from table;
,而單個分區會把數據都集中在一個executor,當遇到較大數據集時,都會產生不合理的資源佔用:MySQL可能hang住,spark可能會OOM,所以不推薦生產環境使用;
2.指定Long型column字段的分區模式
函數:
def jdbc(
url: String,
table: String,
columnName: String,
lowerBound: Long,
upperBound: Long,
numPartitions: Int,
connectionProperties: Properties): DataFrame
使用id
做分片字段的示例:
val url = "jdbc:mysql://mysqlHost:3306/database"
val tableName = "table"
val columnName = "id"
val lowerBound = getMinId()
val upperBound = getMaxId()
val numPartitions = 200
// 設置連接用戶&密碼
val prop = new java.util.Properties
prop.setProperty("user","username")
prop.setProperty("password","pwd")
// 取得該表數據
val jdbcDF = sqlContext.read.jdbc(url,tableName, columnName, lowerBound, upperBound,numPartitions,prop)
// 一些操作
....
從入參可以看出,通過指定 id
這個數字型的column
作爲分片鍵,並設置最大最小值和指定的分區數,可以對數據庫的數據進行併發讀取。是不是numPartitions
傳入多少,分區數就一定是多少呢?其實不然,通過對源碼的分析可知:
if upperBound-lowerBound >= numPartitions:
jdbcDF.rdd.partitions.size = numPartitions
else
jdbcDF.rdd.partitions.size = upperBound-lowerBound
拉取數據時,spark會按numPartitions
均分最大最小ID,然後進行併發查詢,並最終轉換成RDD,例如:
入參爲:
lowerBound=1, upperBound=1000, numPartitions=10
對應查詢語句組爲:
JDBCPartition(id < 101 or id is null,0),
JDBCPartition(id >= 101 AND id < 201,1),
JDBCPartition(id >= 201 AND id < 301,2),
JDBCPartition(id >= 301 AND id < 401,3),
JDBCPartition(id >= 401 AND id < 501,4),
JDBCPartition(id >= 501 AND id < 601,5),
JDBCPartition(id >= 601 AND id < 701,6),
JDBCPartition(id >= 701 AND id < 801,7),
JDBCPartition(id >= 801 AND id < 901,8),
JDBCPartition(id >= 901,9)
建議在使用此方式進行分片時,需要評估好 numPartitions
的個數,防止單片數據過大;同時需要column
字段的索引建立
情況,防止查詢語句出現慢SQL
影響取數效率。
如果column
的數字是離散型的,爲了防止拉取時出現過多空分區,以及不必要的一些數據傾斜,需要使用特殊手段進行處理,具體可以參考Spark JDBC系列--讀取優化。
3.高自由度的分區模式
函數:
def jdbc(
url: String,
table: String,
predicates: Array[String],
connectionProperties: Properties): DataFrame
使用給定分區數組的示例:
/**
* 將近90天的數據進行分區讀取
* 每一天作爲一個分區,例如
* Array(
* "2015-09-17" -> "2015-09-18",
* "2015-09-18" -> "2015-09-19",
* ...)
**/
def getPredicates = {
val cal = Calendar.getInstance()
cal.add(Calendar.DATE, -90)
val array = ArrayBuffer[(String,String)]()
for (i <- 0 until 90) {
val start = new SimpleDateFormat("yyyy-MM-dd").format(cal.getTime())
cal.add(Calendar.DATE, +1)
val end = new SimpleDateFormat("yyyy-MM-dd").format(cal.getTime())
array += start -> end
}
val predicates = array.map {
case (start, end) => s"gmt_create >= '$start' AND gmt_create < '$end'"
}
predicates.toArray
}
val predicates = getPredicates
//鏈接操作
...
從函數可以看出,分區數組是多個並行的自定義where語句
,且分區數爲數據size:
jdbcDF.rdd.partitions.size = predicates.size
建議在使用此方式進行分片時,需要評估好 predicates.size
的個數,防止防止單片數據過大;同時需要自定義where語句
的查詢效率,防止查詢語句出現慢SQL
影響取數效率。
4.自定義option參數模式
函數示例:
val jdbcDF = sparkSession.sqlContext.read.format("jdbc")
.option("url", url)
.option("driver", "com.mysql.jdbc.Driver")
.option("dbtable", "table")
.option("user", "user")
.option("partitionColumn", "id")
.option("lowerBound", 1)
.option("upperBound", 10000)
.option("fetchsize", 100)
.option("xxx", "xxx")
.load()
從函數可以看出,option模式其實是一種開放接口,spark會根據具體的參數,來決定使用上述三種方式中的某一種。
所有支持的參數具體可以參考官方文檔:官方JDBC配置文檔 此處附上截圖:
結語
JDBC的讀取性能受很多條件影響,需要根據不同的數據庫,表,索引,數據量,spark集羣的executor情況等綜合考慮,線上環境的操作,建議進行讀寫分離,即讀備庫,寫主庫。