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

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

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

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

在这里插入图片描述

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()
      }
    
    }
    

猜你喜欢

转载自blog.csdn.net/dwjf321/article/details/109055643