Spark消费Kafka并手动维护偏移量到Mysql

首先要明确的是,偏移量指的是消息在kafka中的某个位置,类似于数组的下标,所以我们要做的是消费者在消费过程中把消息消费到了哪一条,把它对应的offset获取到并保存下来。
首先我们要有一个生产消息的生产者,生产者代码如下:

import java.util.Properties
import org.apache.kafka.clients.producer.{KafkaProducer, ProducerRecord}

object SparkProducer {
  def main(args: Array[String]): Unit = {
    writeToKafka("hotitem")
  }
  def writeToKafka(topic: String): Unit ={
    val prop = new Properties()
    prop.put("bootstrap.servers","Hadoop001:9092")
    prop.setProperty("key.serializer","org.apache.kafka.common.serialization.StringSerializer")
    prop.setProperty("value.serializer","org.apache.kafka.common.serialization.StringSerializer")

    //定义一个kafkaproducer
    val producer = new KafkaProducer[String,String](prop)

    //从文件中读取数据发送
    val buff = scala.io.Source.fromFile("D:\\software\\idea\\WorkSpace\\com.Flink.UserBehiviorAnalysis\\HotItemsAnalysis\\src\\main\\resources\\UserBehavior.csv")
    for(line <- buff.getLines()){
      val record = new ProducerRecord[String,String](topic,line)
      producer.send(record)
    }
    producer.close()
  }
}

生产者有了以后,说明我们的kafka里面已经有了消息,那么我们就可以使用消费者来获取消息进行处理了,但是我们要将偏移量保存到mysql,所以我们首先要有一个连接mysql的工具类,代码如下:

import java.sql.DriverManager
import org.apache.kafka.common.TopicPartition
import org.apache.spark.streaming.kafka010.OffsetRange
import scala.collection.mutable

object OffsetUtil {
  def getOffsetMap(groupId:String,topic:String) = {
    val conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/selftest","root","123456")
    val sql = "select * from t_offset where groupid = ? and topic = ?"
    val psmt = conn.prepareStatement(sql)
    psmt.setString(1,groupId)
    psmt.setString(2,topic)
    val rs = psmt.executeQuery()
    val offsetMap = mutable.Map[TopicPartition,Long]()
    while(rs.next()){
      offsetMap += new TopicPartition(rs.getString("topic"),rs.getInt("partition")) -> rs.getLong("offset")
    }
    rs.close()
    psmt.close()
    conn.close()
    offsetMap
  }
  def saveOffsetRanges(groupId:String,offsetRange:Array[OffsetRange]) = {
    val conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/selftest","root","123456")
    val sql = "replace into t_offset (`topic`,`partition`,`groupid`,`offset`) values(?,?,?,?)"
    val psmt = conn.prepareStatement(sql)

    for(e <- offsetRange){
      psmt.setString(1,e.topic)
      psmt.setInt(2,e.partition)
      psmt.setString(3,groupId)
      psmt.setLong(4,e.untilOffset)
      psmt.executeUpdate()
    }
    psmt.close()
    conn.close()
  }
}

现在我们既有了消息,也有了连接MySQL的工具类,接下来就是要消费消息,并且把偏移量的信息通过工具类连接mysql并存储进mysql了。

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.{OffsetRange, _}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.{SparkConf, SparkContext}
import scala.collection.mutable

object SparkOffset {
  def main(args: Array[String]): Unit = {
    //1.创建StreamingContext
    val conf = new SparkConf().setAppName("wc").setMaster("local[*]")
    val sc = new SparkContext(conf)
    sc.setLogLevel("WARN")
    val ssc = new StreamingContext(sc, Seconds(5)) //5表示每5秒对数据进行一次切分形成一个RDD
    //连接Kafka的参数
    val kafkaParams = Map[String, Object](
      "bootstrap.servers" -> "Hadoop001:9092",
      "key.deserializer" -> classOf[StringDeserializer],
      "value.deserializer" -> classOf[StringDeserializer],
      "group.id" -> "SparkKafkaDemo",
      "auto.offset.reset" -> "earliest",
      "enable.auto.commit" -> (false: java.lang.Boolean)
    )
    //需要进行消费的topic,要和生产者的topic对应
    val topics = Array("hotitem")
    //2.使用OffsetUtil连接Kafak获取数据
    //注意:
    //因为kafka消费有两种方式earliest和latest,两者的区别是,在有已经提交的offset时,两者没有任何区别,都是从提交的offset处开始消费,如果没有提交的offset时,earliest会从头开始消费,也就是会读取旧数据,而latest则会从新产生的数据开始消费
    //所以消费方式的选择要根据数据源和业务需求来决定,我的生产者是读取的文件,当没有offset存储时,也需要消费所有数据,所以没有offset时选取earliest方式
    val offsetMap: mutable.Map[TopicPartition, Long] = OffsetUtil.getOffsetMap("SparkKafkaDemo", "hotitem")
    val recordDStream: InputDStream[ConsumerRecord[String, String]] = if (offsetMap.size > 0) { //有记录offset
      println("MySQL中记录了offset,则从该offset处开始消费")
      KafkaUtils.createDirectStream[String, String](ssc,
        LocationStrategies.PreferConsistent, //位置策略,源码强烈推荐使用该策略,会让Spark的Executor和Kafka的Broker均匀对应
        ConsumerStrategies.Subscribe[String, String](topics, kafkaParams, offsetMap)) //消费策略,源码强烈推荐使用该策略
    } else { //没有记录offset
      println("没有记录offset,则直接连接,从earliest开始消费")
      KafkaUtils.createDirectStream[String, String](ssc,
        LocationStrategies.PreferConsistent, //位置策略,源码强烈推荐使用该策略,会让Spark的Executor和Kafka的Broker均匀对应
        ConsumerStrategies.Subscribe[String, String](topics, kafkaParams)) //消费策略,源码强烈推荐使用该策略
    }
    //3.操作数据
    //注意:我们的目标是要自己手动维护偏移量,也就意味着,消费了一小批数据就应该提交一次offset
    //而这一小批数据在DStream的表现形式就是RDD,所以我们需要对DStream中的RDD进行操作
    //而对DStream中的RDD进行操作的API有transform(转换)和foreachRDD(动作)两种算子
    recordDStream.foreachRDD(rdd => {
      if (rdd.count() > 0) { //当前这个批次内有数据
        rdd.foreach(record => println("接收到的Kafk发送过来的数据为:" + record))//数据打印
        //接收到的Kafka发送过来的数据为:ConsumerRecord(topic = hotitem, partition = 0, offset = 1458008, CreateTime = 1596678765095, serialized key size = -1, serialized value size = 36, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = 654062,2899195,3720767,pv,1511690400)
        //注意:通过打印接收到的消息可以看到,里面有我们需要维护的offset,和要处理的数据
        //接下来可以对数据进行处理....或者使用transform返回和之前一样处理
        //处理数据的代码写完了,就该维护offset了,那么为了方便我们对offset的维护/管理,spark提供了一个类,帮我们封装offset的数据
        val offsetRanges: Array[OffsetRange] = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
        for (o <- offsetRanges) {
          //topic=hotitem,partition=1,fromOffset=0,untilOffset=1458483这里打印的是主题,分区号,开始消费位置,消费完成后的位置,这里面的topic,partition,untilOffset是需要保存到mysql的信息
          println(s"topic=${o.topic},partition=${o.partition},fromOffset=${o.fromOffset},untilOffset=${o.untilOffset}")
        }
        //实际中偏移量可以提交到MySQL/Redis中
        OffsetUtil.saveOffsetRanges("SparkKafkaDemo", offsetRanges)
      }
    })
    ssc.start()
    ssc.awaitTermination()
  }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值