Spark JDBC系列--取數的四種方式

本文旨在介紹 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情況等綜合考慮,線上環境的操作,建議進行讀寫分離,即讀備庫,寫主庫。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章