Spark Streaming中foreachRDD的使用及闭包问题的产生处理

8 篇文章 0 订阅
1 篇文章 0 订阅

一、前言

         foreachRDD是用来把Spark Streaming的数据sink到外部系统,但是使用的时候,这个算子将会被执行在driver进程中,而从driver到executor必然会涉及到序列化的问题。

二、测试。

需求:把流处理的WC结果写到MySQL

MySQLUtils

object MySQLUtils {

  /**
    * 获取连接
    *
    * @return
    */
  def getConnection(): Connection = {
    Class.forName("com.mysql.jdbc.Driver")
    val connection = DriverManager.getConnection(
      "jdbc:mysql://localhost:3306/demo?useSSL=false&serverTimezone=UTC",
      "root",
      "123456"
    )
    connection
  }

  /**
    * 关闭连接
    *
    * @param connection
    */
  def closeConnection(connection: Connection): Unit = {
    if (connection != null) connection.close()
  }

}

2.1 Version1.0

   stream.map((_, 1)).reduceByKey(_ + _) //统计wc
      .foreachRDD(rdd => { //把结果写入数据库
      val connection = MySQLUtils.getConnection() //executed at the driver
      rdd.foreach(wc => {
        val sql = "insert into wc(word,cnt) values(?,?)"
        val statement = connection.prepareStatement(sql) //executed at the worker
        statement.setString(1, wc._1)
        statement.setInt(2, wc._2)
        statement.execute()
      })
      MySQLUtils.closeConnection(connection)
    })

执行后会报错,Task not serializable,其实是connection不能序列化。

由于foreachRDD这个算子在driver端执行的,而foreach这个算子在executor端执行的,而我们的Connection不是序列化的,所以会报错,其实这是一个闭包问题。

闭包:在函数内部引用了一个外部的变量

2.2 Version2.0

为了解决闭包问题,我们把connection放到foreach中

 val result = stream.flatMap(_.split(",")).map((_, 1)).reduceByKey(_ + _)
    result //统计wc
      .foreachRDD(rdd => { //把结果写入数据库
      rdd.foreach(pair => {
        val connection = MySQLUtils.getConnection() //executed at the driver
        val sql = s"insert into wc(word,cnt) values('${pair._1}', ${pair._2})"
        connection.createStatement().execute(sql)
        MySQLUtils.closeConnection(connection)
      })
    })

这样运行是OK的,数据也写到了MySQL,但是假如我们有一亿条数据,每条数据都要创建关闭connection,很明显这样不行。

2.3 Version3.0

针对2.0的问题,再做进一步优化,针对每一个分区,创建一个连接


    val result = stream.flatMap(_.split(",")).map((_, 1)).reduceByKey(_ + _)

    result.foreachRDD(rdd => {
      rdd.foreachPartition(partition => {
        val connection = MySQLUtils.getConnection() //每个分区创建一个partition
        partition.foreach(pair => {
          val sql = s"insert into wc(word,cnt) values('${pair._1}',${pair._2})"
          connection.createStatement().execute(sql)
        })
        MySQLUtils.closeConnection(connection)
      })
    })

到目前为止,其实已经可以用了,也可以接受了,但是还可以进一步优化,如果数据量很大,分区设置的很多,这样数据库连接还是会很多。

2.4 Version4.0

我们可以使用数据库连接池,根据我们生产上的实际情况设置好连接池的数量,用的时候从连接池里取,用完还回去。

可以借助Scalikejdbc: http://scalikejdbc.org/

1)加入依赖

  <dependency>
                <groupId>org.scalikejdbc</groupId>
                <artifactId>scalikejdbc_${scala.tools.version}</artifactId>
                <version>${scalikejdbc.version}</version>
            </dependency>
            <dependency>
                <groupId>org.scalikejdbc</groupId>
                <artifactId>scalikejdbc-config_${scala.tools.version}</artifactId>
                <version>${scalikejdbc.version}</version>
            </dependency>

2)创建数据库连接配置信息

resource下创建application.conf

db.default.driver = "com.mysql.jdbc.Driver"
db.default.url = "jdbc:mysql://localhost:3306/demo?useSSL=false&serverTimezone=UTC"
db.default.user = "root"
db.default.password = "123456"

db.default.poolInitialSize = 10
db.default.poolMaxSize = 20
db.default.connectionTimeoutMillis = 3000

3)使用

  //拿到结果
    val result = stream.flatMap(_.split(","))
      .map((_, 1))
      .reduceByKey(_ + _)

    DBs.setupAll() //解析配置文件application.conf
    result.foreachRDD(rdd => {
      rdd.foreachPartition(partition => {
        partition.foreach(pair => {
          DB.autoCommit {
            implicit session => {
              // NamedDB(""),如果配置的DB名称不是default可以在使用其进行指定,默认是default名字无需指定
              // 默认就使用了连接池
              SQL("insert into wc(word,cnt) values(?,?)")
                .bind(pair._1, pair._2)
                .update()
                .apply()
            }
          }
        })
      })
    })

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值