Spark Streaming 將數據保存在msyql中

Spark Streaming持久化設計模式

DStreams輸出操作

  • print:打印driver結點上每個Dstream中的前10個batch元素,常用於開發和調試
  • saveAsTextFiles(prefix, [suffix]):將當前Dstream保存爲文件,每個interval batch的文件名命名規則基於prefix和suffix:"prefix-TIME_IN_MS[.suffix]".
  • saveAsObjectFiles(prefix, [suffix]):將當前的Dstream內容作爲Java可序列化對象的序列化文件進行保存,每個interval batch的文件命名規則基於prefix和suffix:: "prefix-TIME_IN_MS[.suffix]".
  • saveAsHadoopFiles(prefix, [suffix]):將Dstream以hadoop文件的形式進行保存,每個interval batch的文件命名規則基於prefix和suffix:: "prefix-TIME_IN_MS[.suffix]".
  • foreachRDD(func):最通用的輸出操作,可以對從數據流中產生的每一個RDD應用函數_fun_。通常_fun_會將每個RDD中的數據保存到外部系統,如:將RDD保存到文件,或者通過網絡連接保存到數據庫。值得注意的是:_fun_執行在跑應用的driver進程中,並且通常會包含RDD action以促使數據流RDD開始計算。

使用foreachRDD的設計模式

dstream.foreachRDD對於開發而言提供了很大的靈活性,但在使用時也要避免很多常見的坑。我們通常將數據保存到外部系統中的流程是:建立遠程連接->通過連接傳輸數據到遠程系統->關閉連接。針對這個流程我們很直接的想到了下面的程序代碼:

//遍歷數據流中的每一個RDD與已有數據進行匹配
    ds.foreachRDD(r => {
      println("監控到" + r.count() + "條數據")
      if (r.count() > 0) {
          //獲取mysql的連接
          val conn: Connection = MySqlUtil.getConnection
          //遍歷RDD
          r.foreach(tuple => {
            insertIntoMySQL(conn, sql, tuple)
          })
          MySqlUtil.close(conn)
        }
    })
插入數據庫的方法

def insertIntoMySQL(con: Connection, sql: String, data: Tuple8[String, String, String, String, String, String, String, String]): Unit = {
    try {
      val ps = con.prepareStatement(sql)
      ps.setString(1, data._1)
      ps.setString(2, data._2)
      ps.setString(3, data._3)
      ps.setString(4, data._4)
      ps.setString(5, data._5)
      ps.setString(6, data._6)
      ps.setString(7, data._7)
      ps.setString(8, data._8)
      ps.executeUpdate()
      ps.close()
    } catch {
      case exception: Exception =>
        exception.printStackTrace()
    }
  }

spark踩坑記——初試中,對spark的worker和driver進行了整理,我們知道在集羣模式下,上述代碼中的connection需要通過序列化對象的形式從driver發送到worker,但是connection是無法在機器之間傳遞的,即connection是無法序列化的,這樣可能會引起_serialization errors (connection object not serializable)_的錯誤。爲了避免這種錯誤,我們將conenction在worker當中建立,代碼如下:

//遍歷數據流中的每一個RDD與已有數據進行匹配
    ds.foreachRDD(r => {
      println("監控到" + r.count() + "條數據")
      if (r.count() > 0) {
          r.foreach(tuple => {
            //獲取mysql的連接
            val conn: Connection = MySqlUtil.getConnection
            insertIntoMySQL(conn, sql, tuple)
            MySqlUtil.close(conn)
          })
        }
      
    })

似乎這樣問題解決了?但是細想下,我們在每個rdd的每條記錄當中都進行了connection的建立和關閉,這會導致不必要的高負荷並且降低整個系統的吞吐量。所以一個更好的方式是使用_rdd.foreachPartition_即對於每一個rdd的partition建立唯一的連接(注:每個partition是內的rdd是運行在同一worker之上的),代碼如下:
ds.foreachRDD(r => {
      println("監控到" + r.count() + "條數據")
      if (r.count() > 0) {
          //遍歷RDD
          r.foreachPartition(x => {
            //獲取mysql的連接
            while (x.hasNext) {
              val conn: Connection = MySqlUtil.getConnection
              insertIntoMySQL(conn, sql, x.next())
              MySqlUtil.close(conn)
            }
          })
        }
    })

這樣我們降低了頻繁建立連接的負載,通常我們在連接數據庫時會使用連接池,通過持有一個靜態連接池對象,我們可以重複利用connection而進一步優化了連接建立的開銷,從而降低了負載。另外值得注意的是,同數據庫的連接池類似,我們這裏所說的連接池同樣應該是lazy的按需建立連接,並且及時的收回超時的連接。

另外值得注意的是:

  • 如果在spark streaming中使用了多次foreachRDD,它們之間是按照程序順序向下執行的
  • Dstream對於輸出操作的執行策略是lazy的,所以如果我們在foreachRDD中不添加任何RDD action,那麼系統僅僅會接收數據然後將數據丟棄。

Spark訪問Mysql

我們需要有一個可序列化的類來建立Mysql連接,這裏我們利用了Mysql的C3P0連接池

MySQL通用連接類

import java.sql.Connection
import java.util.Properties

import com.mchange.v2.c3p0.ComboPooledDataSource

class MysqlPool extends Serializable {
  private val cpds: ComboPooledDataSource = new ComboPooledDataSource(true)
  private val conf = Conf.mysqlConfig
  try {
    cpds.setJdbcUrl(conf.get("url").getOrElse("jdbc:mysql://127.0.0.1:3306/test_bee?useUnicode=true&characterEncoding=UTF-8"));
    cpds.setDriverClass("com.mysql.jdbc.Driver");
    cpds.setUser(conf.get("username").getOrElse("root"));
    cpds.setPassword(conf.get("password").getOrElse(""))
    cpds.setMaxPoolSize(200)
    cpds.setMinPoolSize(20)
    cpds.setAcquireIncrement(5)
    cpds.setMaxStatements(180)
  } catch {
    case e: Exception => e.printStackTrace()
  }
  def getConnection: Connection = {
    try {
      return cpds.getConnection();
    } catch {
      case ex: Exception =>
        ex.printStackTrace()
        null
    }
  }
}
object MysqlManager {
  var mysqlManager: MysqlPool = _
  def getMysqlManager: MysqlPool = {
    synchronized {
      if (mysqlManager == null) {
        mysqlManager = new MysqlPool
      }
    }
    mysqlManager
  }
}

我們利用c3p0建立Mysql連接池,然後訪問的時候每次從連接池中取出連接用於數據傳輸。

Mysql輸出操作

同樣利用之前的foreachRDD設計模式,將Dstream輸出到mysql的代碼如下:

dstream.foreachRDD(rdd => {
    if (!rdd.isEmpty) {
      rdd.foreachPartition(partitionRecords => {
        //從連接池中獲取一個連接
        val conn = MysqlManager.getMysqlManager.getConnection
        val statement = conn.createStatement
        try {
          conn.setAutoCommit(false)
          partitionRecords.foreach(record => {
            val sql = "insert into table..." // 需要執行的sql操作
            statement.addBatch(sql)
          })
          statement.executeBatch
          conn.commit
        } catch {
          case e: Exception =>
            // do some log
        } finally {
          statement.close()
          conn.close()
        }
      })
    }
})

值得注意的是:

  • 我們在提交Mysql的操作的時候,並不是每條記錄提交一次,而是採用了批量提交的形式,所以需要將conn.setAutoCommit(false),這樣可以進一步提高mysql的效率。
  • 如果我們更新Mysql中帶索引的字段時,會導致更新速度較慢,這種情況應想辦法避免,如果不可避免,那就沒辦法了

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