【spark学习 | sparkstreaming 与kafka】使用sparkstreaming与kafka相结合进行实时处理数据(文件流)

本篇文章分两个部分:

一个是给出写好的生产者和消费者
一个是介绍代码的实现过程

1. 首先完善好的代码

生产者代码

import java.io.{File, RandomAccessFile}
import java.nio.charset.StandardCharsets
import scala.io.Source

object KafkaWordProducer2 {
  def main(args: Array[String]) {
    if (args.length < 3) {
      System.err.println("用法: KafkaWordProducer <metadataBrokerList> <topic> <linesPerSec>")
      System.exit(1)
    }

    val Array(brokers, topic, linesPerSec) = args
    // Kafka生产者属性
    val props = new java.util.HashMap[String, Object]()
    props.put(org.apache.kafka.clients.producer.ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokers)
    props.put(org.apache.kafka.clients.producer.ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
      "org.apache.kafka.common.serialization.StringSerializer")
    props.put(org.apache.kafka.clients.producer.ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
      "org.apache.kafka.common.serialization.StringSerializer")
    val producer = new org.apache.kafka.clients.producer.KafkaProducer[String, String](props)

    // 文件路径
    val filePath = "/home/hadoop/bilibili/streaming/data/part2.txt" // 假设数据文件名为 part2.txt

    // 记录已发送的行数
    var sentLines = 0

    while (true) {
      val file = new File(filePath)
      val bufferedSource = Source.fromFile(file)
      val linesIterator = bufferedSource.getLines().drop(sentLines)

      val linesToSendPerSec = linesPerSec.toInt
      val sleepTime = (1000.0 / linesToSendPerSec).toLong

      while (linesIterator.hasNext) {
        val line = linesIterator.next()
        val message = new org.apache.kafka.clients.producer.ProducerRecord[String, String](topic, null, line)
        producer.send(message)
        Thread.sleep(sleepTime)
        sentLines += 1
      }

      bufferedSource.close()

      // 等待一段时间再次检查文件是否有新内容
      Thread.sleep(5000)

    }

    // 不会执行到这里,因为循环会一直运行
    producer.close()
  }
}
1. 代码介绍

这个代码是一个Kafka消息生产者程序,用于从一个指定的文本文件中/home/hadoop/bilibili/streaming/data/part2.txt读取数据行,并将这些数据行作为消息发送到Kafka的一个特定主题(topic)中。

  1. 导入必要的库

    import java.io.{File, RandomAccessFile}
    import java.nio.charset.StandardCharsets
    import scala.io.Source
    
  2. 定义主对象和main方法

    object KafkaWordProducer2 {
      def main(args: Array[String]) {
    

    这个对象是程序的入口点,main方法是程序的主方法。

  3. 检查命令行参数

    if (args.length < 3) {
      System.err.println("用法: KafkaWordProducer <metadataBrokerList> <topic> <linesPerSec>")
      System.exit(1)
    }
    

    代码要求至少三个命令行参数:Kafka的broker列表、目标主题(topic)和每秒发送的行数。如果参数不足,程序会输出用法说明并退出。

  4. 解析命令行参数

    val Array(brokers, topic, linesPerSec) = args
    

    将命令行参数赋值给变量。

  5. 设置Kafka生产者属性

    val props = new java.util.HashMap[String, Object]()
    props.put(org.apache.kafka.clients.producer.ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokers)
    props.put(org.apache.kafka.clients.producer.ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
      "org.apache.kafka.common.serialization.StringSerializer")
    props.put(org.apache.kafka.clients.producer.ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
      "org.apache.kafka.common.serialization.StringSerializer")
    val producer = new org.apache.kafka.clients.producer.KafkaProducer[String, String](props)
    

    这里设置了Kafka生产者的属性,包括Kafka broker列表和消息的序列化器。

  6. 指定文件路径

    val filePath = "/home/hadoop/bilibili/streaming/data/part2.txt"
    

    设置数据文件的路径。

  7. 记录已发送的行数

    var sentLines = 0
    
  8. 主循环

    while (true) {
      val file = new File(filePath)
      val bufferedSource = Source.fromFile(file)
      val linesIterator = bufferedSource.getLines().drop(sentLines)
    
      val linesToSendPerSec = linesPerSec.toInt
      val sleepTime = (1000.0 / linesToSendPerSec).toLong
    
      while (linesIterator.hasNext) {
        val line = linesIterator.next()
        val message = new org.apache.kafka.clients.producer.ProducerRecord[String, String](topic, null, line)
        producer.send(message)
        Thread.sleep(sleepTime)
        sentLines += 1
      }
    
      bufferedSource.close()
    
      Thread.sleep(5000)
    }
    
    • 打开文件:每次循环都重新打开文件,以便读取新的内容。
    • 跳过已发送的行:使用drop(sentLines)跳过已经发送过的行。
    • 发送消息:从文件中读取行,并创建Kafka消息发送到指定的主题。根据linesPerSec计算每次发送后的休眠时间。
    • 更新已发送行数:每发送一行,sentLines加1。
    • 等待检查新内容:每次发送完所有新行后,等待5秒再次检查文件。
  9. 关闭生产者

    producer.close()
    

    这一行在实际运行中不会执行到,因为上面的while循环我们写的是无限循环。

消费者代码

然后定义消费者,处理我们的类别列,进行词频统计数量,会根据传来的数据实时记录

import org.apache.spark._
import org.apache.spark.streaming._
import org.apache.spark.streaming.kafka010._
import org.apache.spark.streaming.kafka010.LocationStrategies.PreferConsistent
import org.apache.spark.streaming.kafka010.ConsumerStrategies.Subscribe
import org.apache.kafka.common.serialization.StringDeserializer

object KafkaWordCount {
  def main(args: Array[String]) {
    val sparkConf = new SparkConf().setAppName("KafkaWordCount").setMaster("local[2]")
    val ssc = new StreamingContext(sparkConf, Seconds(10))
    ssc.sparkContext.setLogLevel("ERROR")
    ssc.checkpoint("file:///usr/local/spark/mycode/kafka/checkpoint") // 设置检查点,如果存放在HDFS上面,则写成类似ssc.checkpoint("/user/hadoop/checkpoint")这种形式,但是,要启动Hadoop

    val kafkaParams = Map[String, Object](
      "bootstrap.servers" -> "localhost:9092",
      "key.deserializer" -> classOf[StringDeserializer],
      "value.deserializer" -> classOf[StringDeserializer],
      "group.id" -> "use_a_separate_group_id_for_each_stream",
      "auto.offset.reset" -> "latest",
      "enable.auto.commit" -> (true: java.lang.Boolean)
    )

    val topics = Array("wordsender")
    val stream = KafkaUtils.createDirectStream[String, String](
      ssc,
      PreferConsistent,
      Subscribe[String, String](topics, kafkaParams)
    )

    stream.foreachRDD(rdd => {
      val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
      val partitionCounts = rdd
        .flatMap(_.value.split("\n"))
        .map(_.split("\t"))
        .map(arr => (arr(10), 1))
        .reduceByKey(_ + _)
      partitionCounts.foreach(println)
    })

    ssc.start
    ssc.awaitTermination
  }
}

2.代码介绍

这个代码是一个使用Apache Spark Streaming和Kafka进行实时数据处理的程序。以下是代码的详细解释:

  1. 导入必要的库

    import org.apache.spark._
    import org.apache.spark.streaming._
    import org.apache.spark.streaming.kafka010._
    import org.apache.spark.streaming.kafka010.LocationStrategies.PreferConsistent
    import org.apache.spark.streaming.kafka010.ConsumerStrategies.Subscribe
    import org.apache.kafka.common.serialization.StringDeserializer
    
  2. 定义主对象和main方法

    object KafkaWordCount {
      def main(args: Array[String]) {
    
  3. 配置Spark Streaming上下文

    val sparkConf = new SparkConf().setAppName("KafkaWordCount").setMaster("local[2]")
    val ssc = new StreamingContext(sparkConf, Seconds(10))
    ssc.sparkContext.setLogLevel("ERROR")
    ssc.checkpoint("file:///usr/local/spark/mycode/kafka/checkpoint")
    
    • SparkConf用于配置Spark应用。
    • StreamingContext是Spark Streaming的入口点,配置了批次间隔为10秒。
    • 设置日志级别为"ERROR"以减少日志输出。
    • 配置检查点目录用于容错。
  4. 设置Kafka消费者参数

    val kafkaParams = Map[String, Object](
      "bootstrap.servers" -> "localhost:9092",
      "key.deserializer" -> classOf[StringDeserializer],
      "value.deserializer" -> classOf[StringDeserializer],
      "group.id" -> "use_a_separate_group_id_for_each_stream",
      "auto.offset.reset" -> "latest",
      "enable.auto.commit" -> (true: java.lang.Boolean)
    )
    
    • 配置Kafka broker地址、key和value的反序列化器、消费者组ID、offset重置策略等。
  5. 定义要订阅的Kafka主题

    val topics = Array("wordsender")
    
  6. 创建Kafka数据流

    val stream = KafkaUtils.createDirectStream[String, String](
      ssc,
      PreferConsistent,
      Subscribe[String, String](topics, kafkaParams)
    )
    
    • 使用KafkaUtils.createDirectStream方法创建一个直接从Kafka获取数据的DStream。
  7. 处理每个批次的数据

    stream.foreachRDD(rdd => {
      val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
      val partitionCounts = rdd
        .flatMap(_.value.split("\n"))
        .map(_.split("\t"))
        .map(arr => (arr(10), 1))
        .reduceByKey(_ + _)
      partitionCounts.foreach(println)
    })
    
    • foreachRDD方法用于处理每个批次的RDD。
    • offsetRanges用于获取当前批次的偏移量范围。
    • 对每个RDD中的每条记录,首先按行拆分,然后按制表符拆分,提取第11列(arr(10))作为键,并将其映射为键值对(key, 1)
    • 使用reduceByKey对相同键的值进行累加。
    • 将结果打印输出。
  8. 启动流处理并等待终止

    ssc.start
    ssc.awaitTermination
    

2. 代码实现过程

我们使用文件流,我们的数据读取地方,这里我们把数据放在data.txt里面给kafka读取

2.1 代码设计,初始只是可以读取文件作为文件流,一行一行输出到主题中
import scala.io.Source
import java.io.File

object KafkaWordProducer {
  def main(args: Array[String]) {
    if (args.length < 3) {
      System.err.println("用法: KafkaWordProducer <metadataBrokerList> <topic> <linesPerSec>")
      System.exit(1)
    }

    val Array(brokers, topic, linesPerSec) = args
    // Kafka生产者属性
    val props = new java.util.HashMap[String, Object]()
    props.put(org.apache.kafka.clients.producer.ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokers)
    props.put(org.apache.kafka.clients.producer.ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
      "org.apache.kafka.common.serialization.StringSerializer")
    props.put(org.apache.kafka.clients.producer.ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
      "org.apache.kafka.common.serialization.StringSerializer")
    val producer = new org.apache.kafka.clients.producer.KafkaProducer[String, String](props)

    // 读取数据文件并发送消息
    val file = new File("/home/hadoop/bilibili/streaming/data/part2.txt") // 假设数据文件名为 part2.txt
    val bufferedSource = Source.fromFile(file)
    val linesIterator = bufferedSource.getLines()

    val linesToSendPerSec = linesPerSec.toInt
    val sleepTime = (1000.0 / linesToSendPerSec).toLong

    while (linesIterator.hasNext) {
      val line = linesIterator.next()
      val message = new org.apache.kafka.clients.producer.ProducerRecord[String, String](topic, null, line)
      producer.send(message)
      Thread.sleep(sleepTime)
    }

    bufferedSource.close()
    producer.close()
  }
}

代码问题

这个代码我们实现可以读取本地文件,我们制定好每秒输出多少数据,他就会发送推送给监控器,这里他的逻辑是一行一行输出,然后如果发现后面没有数据后就会停止程序,我们肯定不能这样,因为我们的数据是持续不断地,只是可能偶尔没数据,后面是有数据的,所以我们更改代码。

2.2 代码完善1

完善逻辑,我们加入循环,让他一直执行监控这个文件,把他写成死循环,但是这个程序运行后我们发现他的循环是循环发送完所有行数据,如果你新增加了数据,他又会把之前旧的数据和新加的数据循环输出一遍,所以这个代码还是得优化。

import scala.io.Source
import java.io.File

object KafkaWordProducer {
  def main(args: Array[String]) {
    if (args.length < 3) {
      System.err.println("用法: KafkaWordProducer <metadataBrokerList> <topic> <linesPerSec>")
      System.exit(1)
    }

    val Array(brokers, topic, linesPerSec) = args
    // Kafka生产者属性
    val props = new java.util.HashMap[String, Object]()
    props.put(org.apache.kafka.clients.producer.ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokers)
    props.put(org.apache.kafka.clients.producer.ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
      "org.apache.kafka.common.serialization.StringSerializer")
    props.put(org.apache.kafka.clients.producer.ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
      "org.apache.kafka.common.serialization.StringSerializer")
    val producer = new org.apache.kafka.clients.producer.KafkaProducer[String, String](props)

    // 文件路径
    val filePath = "/home/hadoop/bilibili/streaming/data/part2.txt" // 假设数据文件名为 part2.txt

    // 读取数据文件并发送消息
    while (true) {
      val file = new File(filePath)
      val bufferedSource = Source.fromFile(file)
      val linesIterator = bufferedSource.getLines()

      val linesToSendPerSec = linesPerSec.toInt
      val sleepTime = (1000.0 / linesToSendPerSec).toLong

      while (linesIterator.hasNext) {
        val line = linesIterator.next()
        val message = new org.apache.kafka.clients.producer.ProducerRecord[String, String](topic, null, line)
        producer.send(message)
        Thread.sleep(sleepTime)
      }

      bufferedSource.close()

      // 等待一段时间再次检查文件是否有新内容
      Thread.sleep(5000)
    }

    // 不会执行到这里,因为循环会一直运行
    producer.close()
  }
}

2.3代码完善2

我们来使用RandomAccessFile来实现记录他循环的行数,下次我们从这个索引开始来输出。

import java.io.{File, RandomAccessFile}

object KafkaWordProducer {
  def main(args: Array[String]) {
    if (args.length < 3) {
      System.err.println("用法: KafkaWordProducer <metadataBrokerList> <topic> <linesPerSec>")
      System.exit(1)
    }

    val Array(brokers, topic, linesPerSec) = args
    // Kafka生产者属性
    val props = new java.util.HashMap[String, Object]()
    props.put(org.apache.kafka.clients.producer.ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokers)
    props.put(org.apache.kafka.clients.producer.ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
      "org.apache.kafka.common.serialization.StringSerializer")
    props.put(org.apache.kafka.clients.producer.ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
      "org.apache.kafka.common.serialization.StringSerializer")
    val producer = new org.apache.kafka.clients.producer.KafkaProducer[String, String](props)

    // 文件路径
    val filePath = "/home/hadoop/bilibili/streaming/data/part2.txt" // 假设数据文件名为 part2.txt

    // 初始化上次读取的文件位置
    var lastReadPosition = 0L

    // 读取数据文件并发送消息
    while (true) {
      val file = new File(filePath)
      val fileLength = file.length() // 获取文件长度
      if (fileLength > lastReadPosition) { // 如果文件长度大于上次读取的位置,说明有新增数据
        val raf = new RandomAccessFile(file, "r")
        raf.seek(lastReadPosition) // 移动文件指针到上次读取的位置后

        // 从文件中读取新数据并发送消息
        var line: String = null
        while ({ line = raf.readLine(); line != null }) {
          val message = new org.apache.kafka.clients.producer.ProducerRecord[String, String](topic, null, line)
          producer.send(message)
          Thread.sleep((1000.0 / linesPerSec.toInt).toLong)
        }

        raf.close()
        lastReadPosition = fileLength // 更新上次读取的位置为当前文件长度
      }

      // 等待一段时间再次检查文件是否有新内容
      Thread.sleep(5000)
    }

    // 不会执行到这里,因为循环会一直运行
    producer.close()
  }
}

代码问题

运行发现,他传入的数据显示中文乱码,我们得寻找其他方法
这里我们采用的方法是:
记录已发送的行数:在循环外创建一个变量,用于记录已发送的行数。每次发送消息后,将行数加一,并保存这个值。
在下一次循环开始时,从记录的行数开始读取文件并发送新的消息。这样就可以避免重复发送之前已经发送过的内容。

2.4代码完善3
import java.io.{File, RandomAccessFile}
import java.nio.charset.StandardCharsets
import scala.io.Source

object KafkaWordProducer2 {
  def main(args: Array[String]) {
    if (args.length < 3) {
      System.err.println("用法: KafkaWordProducer <metadataBrokerList> <topic> <linesPerSec>")
      System.exit(1)
    }

    val Array(brokers, topic, linesPerSec) = args
    // Kafka生产者属性
    val props = new java.util.HashMap[String, Object]()
    props.put(org.apache.kafka.clients.producer.ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokers)
    props.put(org.apache.kafka.clients.producer.ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
      "org.apache.kafka.common.serialization.StringSerializer")
    props.put(org.apache.kafka.clients.producer.ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
      "org.apache.kafka.common.serialization.StringSerializer")
    val producer = new org.apache.kafka.clients.producer.KafkaProducer[String, String](props)

    // 文件路径
    val filePath = "/home/hadoop/bilibili/streaming/data/part2.txt" // 假设数据文件名为 part2.txt

    // 记录已发送的行数
    var sentLines = 0

    while (true) {
      val file = new File(filePath)
      val bufferedSource = Source.fromFile(file)
      val linesIterator = bufferedSource.getLines().drop(sentLines)

      val linesToSendPerSec = linesPerSec.toInt
      val sleepTime = (1000.0 / linesToSendPerSec).toLong

      while (linesIterator.hasNext) {
        val line = linesIterator.next()
        val message = new org.apache.kafka.clients.producer.ProducerRecord[String, String](topic, null, line)
        producer.send(message)
        Thread.sleep(sleepTime)
        sentLines += 1
      }

      bufferedSource.close()

      // 等待一段时间再次检查文件是否有新内容
      Thread.sleep(5000)

    }

    // 不会执行到这里,因为循环会一直运行
    producer.close()
  }
}

运行发现可以实现我们想用的功能.

最后运行发现实现了我们想要的功能,可以接收后面输入数据,但是不会输出之前的,并且之前输出完会等待新的数据流入
在消费者定义的主题,我们消费者接受数据,我们可以自定义处理逻辑,可以加上数据读取的预处理,去除重复值,保证数据的正确性。
  • 7
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Elik-hb

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值