Spark 从 0 到 1 学习(9) —— Spark Streaming + Kafka


文章目录

  • Spark 从 0 到 1 学习(9) —— Spark Streaming + Kafka
  • 1. Kafka中的数据消费语义介绍
  • 2. Kafka 的消费模式
  • 2.1 SparkStreaming消费kafka整合介绍基于0.8版本整合方式
  • 2.1.1 Receiver-based Approach(不推荐使用)
  • 2.1.2 Direct Approach (No Receivers)
  • 2.2 解决SparkStreaming与Kafka0.8版本整合数据不丢失方案
  • 2.2.1 方案设计如下:
  • 2.2.2 手动维护 offset,偏移量存入 Redis
  • 2.3 SparkStreaming与Kafka-0-10整合
  • 3. SparkStreaming应用程序如何保证Exactly-Once


1. Kafka中的数据消费语义介绍

在消费 kafka 中的数据的时候,可以有三种语义的保证:

  • at most once:至多一次,数据最多处理一次货这者没有被处理,有数据丢失的情况。
  • at least once:至少一次,数据最少被处理一次,有可能出现重复消费的问题。
  • exactly once:消费一次且仅一次

2. Kafka 的消费模式

Spark Streaming Kafka消费模式有2种:Receiver 模式和 Driect 模式。在 Spark2.x 后去掉了 Receiver 模式。下面我们分别来讲讲这两种模式。

2.1 SparkStreaming消费kafka整合介绍基于0.8版本整合方式

SparkStreaming整合Kafka官方文档

2.1.1 Receiver-based Approach(不推荐使用)

此方法使用 Receiver 接收数据。Receiver 是使用 Kafka 高级消费者 API 实现的。与所有接收器一样,从 Kafka 通过 Receiver 接收的数据存储在 Spark 执行器中,然后由 Spark Streaming 启动的作业处理数据。但是在默认配置下,此方法可能会在失败时丢失数据(请参阅接收器可靠性。为确保零数据丢失,必须在 Spark Streaming 中另外启用 Write Ahead Logs(在Spark 1.2中引入)。这将同步保存所有收到的 Kafka 将数据写入分布式文件系统(例如HDFS)上的预写日志,以便在发生故障时可以恢复所有数据,但是性能不好。

  • pom.xml 文件添加如下:
<properties>
        <spark.version>2.3.3</spark.version>
    </properties>

    <repositories>
        <repository>
            <id>cloudera</id>
            <url>https://repository.cloudera.com/artifactory/cloudera-repos</url>
        </repository>
    </repositories>

    <dependencies>
        <dependency>
            <groupId>org.scala-lang</groupId>
            <artifactId>scala-library</artifactId>
            <version>2.11.8</version>
        </dependency>

        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-core_2.11</artifactId>
            <version>2.3.3</version>
        </dependency>

        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-streaming_2.11</artifactId>
            <version>${spark.version}</version>
        </dependency>


        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-sql_2.11</artifactId>
            <version>2.3.3</version>
        </dependency>

        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-streaming-kafka-0-8_2.11</artifactId>
            <version>2.3.3</version>
        </dependency>

        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-streaming-kafka-0-10_2.11</artifactId>
            <version>2.3.3</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <!-- 限制jdk版本插件 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.0</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <!-- 编译scala需要用到的插件 -->
            <plugin>
                <groupId>net.alchim31.maven</groupId>
                <artifactId>scala-maven-plugin</artifactId>
                <version>3.2.2</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>testCompile</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
  • 核心代码:
import org.apache.spark.streaming.kafka._

 val kafkaStream = KafkaUtils.createStream(streamingContext,
     [ZK quorum], [consumer group id], [per-topic number of Kafka partitions to consume])
  • 代码演示
import org.apache.log4j.{Level, Logger}
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.kafka.KafkaUtils
import org.apache.spark.streaming.{Seconds, StreamingContext}

/**
 * sparkStreaming使用kafka 0.8API基于recevier来接受消息
 */
object KafkaReceiver08 {
  def main(args: Array[String]): Unit = {
    Logger.getLogger("org").setLevel(Level.ERROR)

    //1、创建StreamingContext对象
    val sparkConf= new SparkConf()
      .setAppName("KafkaReceiver08")
      .setMaster("local[2]")
      //开启WAL机制
      .set("spark.streaming.receiver.writeAheadLog.enable", "true")
    val ssc = new StreamingContext(sparkConf,Seconds(2))

    //需要设置checkpoint,将接受到的数据持久化写入到hdfs上
    ssc.checkpoint("hdfs://node01:8020/wal")

    //2、接受kafka数据
    val zkQuorum="hadoop102:2181,hadoop103:2181,hadoop104:2181"
    val groupid="KafkaReceiver08"
    val topics=Map("test" ->1)

    //(String, String) 元组的第一位是消息的key,第二位表示消息的value
    val receiverDstream: ReceiverInputDStream[(String, String)] = KafkaUtils.createStream(ssc,zkQuorum,groupid,topics)


    //3、获取kafka的topic数据
    val data: DStream[String] = receiverDstream.map(_._2)

    //4、单词计数
    val result: DStream[(String, Int)] = data.flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_)

    //5、打印结果
    result.print()

    //6、开启流式计算
    ssc.start()
    ssc.awaitTermination()
  }
}
  • 创建kafka的topic并准备发送消息
cd /kafka_2.11-1.1.0/
bin/kafka-topics.sh --create --partitions 3 --replication-factor 2 --topic test --zookeeper hadoop102:2181,hadoop102:2181,hadoop102:2181
bin/kafka-console-producer.sh --broker-list node01:9092,node02:9092,node03:9092 --topic test
2.1.2 Direct Approach (No Receivers)

这种新的不基于 Receiver 的直接方式,是在 Spark 1.3 中引入的,从而能够确保更加健壮的机制。替代掉使用 Receiver 来接收数据后,这种方式会周期性地查询 Kafka,来获得每个 topic+partition 的最新的 offset,从而定义每个 batch 的 offset 的范围。当处理数据的 job 启动时,就会使用 Kafka 的简单 consumer api 来获取 Kafka 指定 offset 范围的数据。

这种方式有如下优点:

  1. 简化并行读取
    如果要读取多个partition,不需要创建多个输入DStream然后对它们进行union操作。Spark会创建跟Kafka partition一样多的RDD partition,并且会并行从Kafka中读取数据。所以在Kafka partition和RDD partition之间,有一个一对一的映射关系。
  2. 高性能
    如果要保证零数据丢失,在基于receiver的方式中,需要开启WAL机制。这种方式其实效率低下,因为数据实际上被复制了两份,Kafka自己本身就有高可靠的机制,会对数据复制一份,而这里又会复制一份到WAL中。而基于direct的方式,不依赖Receiver,不需要开启WAL机制,只要Kafka中作了数据的复制,那么就可以通过Kafka的副本进行恢复。
  3. 一次且仅一次的事务机制
    基于receiver的方式,是使用Kafka的高阶API来在ZooKeeper中保存消费过的offset的。这是消费Kafka数据的传统方式。这种方式配合着WAL机制可以保证数据零丢失的高可靠性,但是却无法保证数据被处理一次且仅一次,可能会处理两次。因为Spark和ZooKeeper之间可能是不同步的。
  4. 降低资源
    Direct不需要Receivers,其申请的Executors全部参与到计算任务中;而Receiver-based则需要专门的Receivers来读取Kafka数据且不参与计算。因此相同的资源申请,Direct 能够支持更大的业务。
  5. 降低内存
    Receiver-based的Receiver与其他Exectuor是异步的,并持续不断接收数据,对于小业务量的场景还好,如果遇到大业务量时,需要提高Receiver的内存,但是参与计算的Executor并无需那么多的内存。而Direct 因为没有Receiver,而是在计算时读取数据,然后直接计算,所以对内存的要求很低。实际应用中我们可以把原先的10G降至现在的2-4G左右。
  6. 可用性更好
    Receiver-based方法需要Receivers来异步持续不断的读取数据,因此遇到网络、存储负载等因素,导致实时任务出现堆积,但Receivers却还在持续读取数据,此种情况很容易导致计算崩溃。Direct 则没有这种顾虑,其Driver在触发batch计算任务时,才会读取数据并计算。队列出现堆积并不会引起程序的失败。

代码演示:

import kafka.serializer.StringDecoder
import org.apache.log4j.{Level, Logger}
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.streaming.dstream.{DStream, InputDStream, ReceiverInputDStream}
import org.apache.spark.streaming.kafka.KafkaUtils

/**
 * sparkStreaming使用kafka 0.8API基于Direct直连来接受消息
 * spark direct API接收kafka消息,从而不需要经过zookeeper,直接从broker上获取信息。
 */
object KafkaDirect08 {
  def main(args: Array[String]): Unit = {
    Logger.getLogger("org").setLevel(Level.ERROR)

    //1、创建StreamingContext对象
    val sparkConf= new SparkConf()
      .setAppName("KafkaDirect08")
      .setMaster("local[2]")

    val ssc = new StreamingContext(sparkConf,Seconds(2))


    //2、接受kafka数据
    val  kafkaParams=Map(
      "metadata.broker.list"->"hadoop102:9092,hadoop103:9092,hadoop104:9092",
      "group.id" -> "KafkaDirect08"
    )
    val topics=Set("test")

    //使用direct直连的方式接受数据
    val kafkaDstream: InputDStream[(String, String)] = KafkaUtils.createDirectStream[String,String,StringDecoder,StringDecoder](ssc,kafkaParams,topics)

    //3、获取kafka的topic数据
    val data: DStream[String] = kafkaDstream.map(_._2)

    //4、单词计数
    val result: DStream[(String, Int)] = data.flatMap(_.split(" "))
      .map((_,1))
      .reduceByKey(_+_)

    //5、打印结果
    result.print()

    //6、开启流式计算
    ssc.start()
    ssc.awaitTermination()

  }
}

要想保证数据不丢失,最简单的就是靠 checkpoint 的机制,但是 checkpoint 机制有个特点,如果代码升级了,checkpoint 机制就失效了。所以如果想实现数据不丢失,那么就需要自己管理 offset。

2.2 解决SparkStreaming与Kafka0.8版本整合数据不丢失方案

2.2.1 方案设计如下:

一般企业来说无论你是使用哪一套api去消费kafka中的数据,都是设置手动提交偏移量。

如果是自动提交偏移量(默认60s提交一次)这里可能会出现问题?

  1. 数据处理失败了,自动提交了偏移量。会出现数据的丢失。
  2. 数据处理成功了,自动提交偏移量成功(比较理想),但是有可能出现自动提交偏移量失败。会出现把之前消费过的数据再次消费,这里就出现了数据的重复处理。

自动提交偏移量风险比较高,可能会出现数据丢失或者数据被重复处理,一般来说就手动去提交偏移量,这里我们是可以去操作什么时候去提交偏移量,把偏移量的提交通过消费者程序自己去维护。

spark stream kafka 从头消费 sparkstreaming消费kafka精准一次_spark

2.2.2 手动维护 offset,偏移量存入 Redis
  • redis 客户端
import org.apache.commons.pool2.impl.GenericObjectPoolConfig
import redis.clients.jedis.JedisPool

class RedisClient {
  val host = "192.168.0.122"
  val port = 6379
  val timeOut = 3000
  //  延迟加载,使用的时候才会创建
  lazy  val pool = new JedisPool(new GenericObjectPoolConfig(),host,port,timeOut)
}
  • 保存 offset 到 reids
package com.abcft.spark.streaming.kafka

import org.apache.spark.{SparkConf, TaskContext}
import com.abcft.spark.redis.RedisClient
import com.alibaba.fastjson.{JSON, JSONObject}
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.TopicPartition
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.streaming.dstream.InputDStream
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, HasOffsetRanges, KafkaUtils, OffsetRange}
import org.apache.spark.streaming.kafka010.LocationStrategies.PreferConsistent
import org.apache.spark.streaming.{Durations, Seconds, StreamingContext}

import scala.collection.mutable

/**
  * 使用redis来维护 offset
  */
object ManageOffsetUseRedis {

  lazy val redisClient = new RedisClient

  val bootstrapServer = "192.168.0.122:9092"
  val topic = "wc"
  val dbIndex = 1
  val groupId = "wc-consumer"
  val autoOffsetReset = "earliest"
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf();
    conf.setAppName("ManageOffsetUseRedis")
    conf.setMaster("local")
    // 设置每个分区每秒读取多少条数据
    conf.set("spark.streaming.kafka.maxRatePerPartition","10")
    val ssc = new StreamingContext(conf,Durations.seconds(5))
    // 设置日志级别
    ssc.sparkContext.setLogLevel("Error")
    ssc.checkpoint("E:/checkpoint/ManageOffsetUseRedis2/")



    /**
      *  从 redis 中获取消费者 offset
      */

    // 当前 offset
    val currentOffset: mutable.Map[String, String] = getOffset(dbIndex,topic)
    currentOffset.foreach(x=>{println(s" 初始读取到的offset: $x")})

    // 转换成需要的类型
    val frommOffsets = currentOffset.map(offsetMap => {
      new TopicPartition(topic, offsetMap._1.toInt) -> offsetMap._2.toLong
    }).toMap

    val kafkaParams = Map[String,Object] (
      "bootstrap.servers" -> bootstrapServer,
      "key.deserializer" -> classOf[StringDeserializer],
      "value.deserializer" -> classOf[StringDeserializer],
      "group.id" -> groupId,
      "auto.offset.reset" -> autoOffsetReset
    )

    /**
      *  将获取到的消费者 offset 传递给 SparkStreaming
      */
    val stream: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream(
      ssc,
      PreferConsistent,
      ConsumerStrategies.Assign[String, String](frommOffsets.keys.toList, kafkaParams, frommOffsets)
    )



    stream.foreachRDD(rdd =>{
      println("**** 业务处理完成2  ****")
      val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
      rdd.foreachPartition { iter =>
        val o: OffsetRange = offsetRanges(TaskContext.get.partitionId)
        println(s"topic:${o.topic}  partition:${o.partition}  fromOffset:${o.fromOffset}  untilOffset: ${o.untilOffset}")
      }
      //将当前批次最后的所有分区offsets 保存到 Redis中
      saveOffset(offsetRanges)

    })

    ssc.start()
    ssc.awaitTermination()
    ssc.stop()


  }

  def getOffset(db:Int,topic:String) = {
    val jedis = redisClient.pool.getResource
    jedis.select(db)
    val key = topic+":"+groupId
    val result = jedis.hgetAll(key)
    jedis.close()
    if (result.size() == 0) {
      result.put("0","0")
      result.put("1","0")
      result.put("2","0")
    }

    /**
      *  java map 转 scala map
      */
    import scala.collection.JavaConversions.mapAsScalaMap
    val offsetMap: scala.collection.mutable.Map[String,String] = result

    offsetMap
  }

  def saveOffset(offsetRange:Array[OffsetRange]) = {
    val jedis = redisClient.pool.getResource
    val key = topic+":"+groupId
    jedis.select(dbIndex)
    offsetRange.foreach(one =>{
      jedis.hset(key,one.partition.toString,one.untilOffset.toString)
    })
    jedis.close()
  }
}

2.3 SparkStreaming与Kafka-0-10整合

  • 支持0.10版本,或者更高的版本(推荐使用这个版本)
  • 代码演示:
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.log4j.{Level, Logger}
import org.apache.spark.SparkConf
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.dstream.InputDStream
import org.apache.spark.streaming.kafka010._
import org.apache.spark.streaming.{Seconds, StreamingContext}

object KafkaDirect10 {

  def main(args: Array[String]): Unit = {
    Logger.getLogger("org").setLevel(Level.ERROR)

    //1、创建StreamingContext对象
    val sparkConf= new SparkConf()
      .setAppName("KafkaDirect10")
      .setMaster("local[2]")
    val ssc = new StreamingContext(sparkConf,Seconds(2))
    //2、使用direct接受kafka数据
    //准备配置
    val topic =Set("test")
    val kafkaParams=Map(
      "bootstrap.servers" ->"hadoop102:9092,hadoop103:9092,hadoop104:9092",
      "group.id" -> "KafkaDirect10",
      "key.deserializer" -> classOf[StringDeserializer],
      "value.deserializer" -> classOf[StringDeserializer],
      "enable.auto.commit" -> "false"
    )

    val kafkaDStream: InputDStream[ConsumerRecord[String, String]] =
      KafkaUtils.createDirectStream[String, String](
        ssc,
        //数据本地性策略
        LocationStrategies.PreferConsistent,
        //指定要订阅的topic
        ConsumerStrategies.Subscribe[String, String](topic, kafkaParams)
      )
    //3、对数据进行处理
    //如果你想获取到消息消费的偏移,这里需要拿到最开始的这个Dstream进行操作
    //如果你对该DStream进行了其他的转换之后,生成了新的DStream,新的DStream不在保存对应的消息的偏移量
    kafkaDStream.foreachRDD(rdd =>{
      //获取消息内容
      val dataRDD: RDD[String] = rdd.map(_.value())
      //打印
      dataRDD.foreach(line =>{
        println(line)
      })
      //4、提交偏移量信息,把偏移量信息添加到kafka中
      val offsetRanges: Array[OffsetRange] =rdd.asInstanceOf[HasOffsetRanges].offsetRanges
      kafkaDStream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges)
    })
    //5、开启流式计算
    ssc.start()
    ssc.awaitTermination()
  }
}

3. SparkStreaming应用程序如何保证Exactly-Once

一个流式计算如果想要保证 Exactly-Once,那么首先要对这三个点有有要求:

  1. Source支持Replay (数据重放)。
  2. 流计算引擎本身处理能保证Exactly-Once。
  3. Sink支持幂等或事务更新

实现数据被处理且只被处理一次,就需要实现数据结果保存操作与偏移量保存操作在同一个事务中,或者你可以实现幂等操作。

也就是说如果要想让一个SparkStreaming的程序保证Exactly-Once,那么从如下三个角度出发:

  1. 接收数据:从Source中接收数据。
  2. 转换数据:用DStream和RDD算子转换。
  3. 储存数据:将结果保存至外部系统。

如果SparkStreaming程序需要实现Exactly-Once语义,那么每一个步骤都要保证Exactly-Once。

案例演示:

  • pom.xml添加内容如下
<dependency>
    <groupId>org.scalikejdbc</groupId>
    <artifactId>scalikejdbc_2.11</artifactId>
    <version>3.1.0</version>
</dependency>
<dependency>
    <groupId>org.scalikejdbc</groupId>
    <artifactId>scalikejdbc-config_2.11</artifactId>
    <version>3.1.0</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.39</version>
</dependency>
  • 代码开发
import org.apache.kafka.common.TopicPartition
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.SparkConf
import org.apache.spark.sql.SparkSession
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, HasOffsetRanges, KafkaUtils, LocationStrategies}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.slf4j.LoggerFactory
import scalikejdbc.{ConnectionPool, DB, _}
/**
  *    SparkStreaming EOS:
  *      Input:Kafka
  *      Process:Spark Streaming
  *      Output:Mysql
  *
  mysql支持事务操作:
  ()
  
  *      保证EOS:
  *        1、偏移量自己管理,即enable.auto.commit=false,这里保存在Mysql中
  *        2、使用createDirectStream
  *        3、事务输出: 结果存储与Offset提交在Driver端同一Mysql事务中
  */
object SparkStreamingEOSKafkaMysqlAtomic {
  @transient lazy val logger = LoggerFactory.getLogger(this.getClass)

  def main(args: Array[String]): Unit = {

    val topic="topic1"
    val group="spark_app1"

    //Kafka配置
    val kafkaParams= Map[String, Object](
      "bootstrap.servers" -> "node1:6667,node2:6667,node3:6667",
      "key.deserializer" -> classOf[StringDeserializer],
      "value.deserializer" -> classOf[StringDeserializer],
      "auto.offset.reset" -> "latest",
      "enable.auto.commit" -> (false: java.lang.Boolean),
      "group.id" -> group)

    //在Driver端创建数据库连接池
    ConnectionPool.singleton("jdbc:mysql://node3:3306/bigdata", "", "")

    val conf = new SparkConf().setAppName(this.getClass.getSimpleName.replace("$",""))
    val ssc = new StreamingContext(conf,Seconds(5))

    //1)初次启动或重启时,从指定的Partition、Offset构建TopicPartition
    //2)运行过程中,每个Partition、Offset保存在内部currentOffsets = Map[TopicPartition, Long]()变量中
    //3)后期Kafka Topic分区动扩展,在运行过程中不能自动感知
    val initOffset=DB.readOnly(implicit session=>{
      sql"select `partition`,offset from kafka_topic_offset where topic =${topic} and `group`=${group}"
        .map(item=> new TopicPartition(topic, item.get[Int]("partition")) -> item.get[Long]("offset"))
        .list().apply().toMap
    })

    //CreateDirectStream
    //从指定的Topic、Partition、Offset开始消费
    val sourceDStream =KafkaUtils.createDirectStream[String,String](
      ssc,
      LocationStrategies.PreferConsistent,
      ConsumerStrategies.Assign[String,String](initOffset.keys,kafkaParams,initOffset)
    )

    sourceDStream.foreachRDD(rdd=>{
      if (!rdd.isEmpty()){
        val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
        offsetRanges.foreach(offsetRange=>{
          logger.info(s"Topic: ${offsetRange.topic},Group: ${group},Partition: ${offsetRange.partition},fromOffset: ${offsetRange.fromOffset},untilOffset: ${offsetRange.untilOffset}")
        })

        //统计分析
        //将结果收集到Driver端
        val sparkSession = SparkSession.builder.config(rdd.sparkContext.getConf).getOrCreate()
        import sparkSession.implicits._
        val dataFrame = sparkSession.read.json(rdd.map(_.value()).toDS)
        dataFrame.createOrReplaceTempView("tmpTable")
        val result=sparkSession.sql(
          """
            |select
            |   --每分钟
            |   eventTimeMinute,
            |   --每种语言
            |   language,
            |   -- 次数
            |   count(1) pv,
            |   -- 人数
            |   count(distinct(userID)) uv
            |from(
            |   select *, substr(eventTime,0,16) eventTimeMinute from tmpTable
            |) as tmp group by eventTimeMinute,language
          """.stripMargin
        ).collect()

        //在Driver端存储数据、提交Offset
        //结果存储与Offset提交在同一事务中原子执行
        //这里将偏移量保存在Mysql中
        DB.localTx(implicit session=>{

          //结果存储
          result.foreach(row=>{
            sql"""
            insert into twitter_pv_uv (eventTimeMinute, language,pv,uv)
            value (
                ${row.getAs[String]("eventTimeMinute")},
                ${row.getAs[String]("language")},
                ${row.getAs[Long]("pv")},
                ${row.getAs[Long]("uv")}
                )
            on duplicate key update pv=pv,uv=uv
          """.update.apply()
          })

          //Offset提交
          offsetRanges.foreach(offsetRange=>{
            val affectedRows = sql"""
          update kafka_topic_offset set offset = ${offsetRange.untilOffset}
          where
            topic = ${topic}
            and `group` = ${group}
            and `partition` = ${offsetRange.partition}
            and offset = ${offsetRange.fromOffset}
          """.update.apply()

            if (affectedRows != 1) {
              throw new Exception(s"""Commit Kafka Topic: ${topic} Offset Faild!""")
            }
          })
        })
      }
    })

    ssc.start()
    ssc.awaitTermination()
  }

}