本篇文章分两个部分:
一个是给出写好的生产者和消费者
一个是介绍代码的实现过程
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)中。
-
导入必要的库:
import java.io.{File, RandomAccessFile} import java.nio.charset.StandardCharsets import scala.io.Source
-
定义主对象和main方法:
object KafkaWordProducer2 { def main(args: Array[String]) {
这个对象是程序的入口点,
main
方法是程序的主方法。 -
检查命令行参数:
if (args.length < 3) { System.err.println("用法: KafkaWordProducer <metadataBrokerList> <topic> <linesPerSec>") System.exit(1) }
代码要求至少三个命令行参数:Kafka的broker列表、目标主题(topic)和每秒发送的行数。如果参数不足,程序会输出用法说明并退出。
-
解析命令行参数:
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)
这里设置了Kafka生产者的属性,包括Kafka broker列表和消息的序列化器。
-
指定文件路径:
val filePath = "/home/hadoop/bilibili/streaming/data/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) }
- 打开文件:每次循环都重新打开文件,以便读取新的内容。
- 跳过已发送的行:使用
drop(sentLines)
跳过已经发送过的行。 - 发送消息:从文件中读取行,并创建Kafka消息发送到指定的主题。根据
linesPerSec
计算每次发送后的休眠时间。 - 更新已发送行数:每发送一行,
sentLines
加1。 - 等待检查新内容:每次发送完所有新行后,等待5秒再次检查文件。
-
关闭生产者:
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进行实时数据处理的程序。以下是代码的详细解释:
-
导入必要的库:
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
-
定义主对象和main方法:
object KafkaWordCount { def main(args: Array[String]) {
-
配置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"以减少日志输出。
- 配置检查点目录用于容错。
-
设置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重置策略等。
-
定义要订阅的Kafka主题:
val topics = Array("wordsender")
-
创建Kafka数据流:
val stream = KafkaUtils.createDirectStream[String, String]( ssc, PreferConsistent, Subscribe[String, String](topics, kafkaParams) )
- 使用
KafkaUtils.createDirectStream
方法创建一个直接从Kafka获取数据的DStream。
- 使用
-
处理每个批次的数据:
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
对相同键的值进行累加。 - 将结果打印输出。
-
启动流处理并等待终止:
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()
}
}
运行发现可以实现我们想用的功能.