代码示例:
case class Table1(@BeanProperty var goods1: String, @BeanProperty var price1: Int) extends Serializable
case class Table2(@BeanProperty var goods2: String, @BeanProperty var price2: Int) extends Serializable
case class Table3(@BeanProperty var goods3: String, @BeanProperty var price3: Int) extends Serializable
object Stream extends Serializable {
def main(args: Array[String]): Unit = {
val masterUri = sys.props.getOrElse("spark.master", "local[4]")
// 获取 spark 环境
val conf = new SparkConf()
val spark: SparkSession = SparkSession
.builder()
.config(conf)
.master(masterUri)
.getOrCreate()
val sparkContext = spark.sparkContext
val ssc: StreamingContext = new StreamingContext(sparkContext, Seconds(120))
val sqlContext = spark.sqlContext
// ------------------------------------------------------------------------------------------------------------------------------------------------------------
val kafkaParams: mutable.Map[String, Object] = mutable.Map[String, Object](
ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> "kafka01:9092",
ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG -> classOf[StringDeserializer].getCanonicalName,
ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG -> classOf[StringDeserializer].getCanonicalName,
ConsumerConfig.GROUP_ID_CONFIG -> "test-1"
)
// 保存 offset,最后手动提交
val offsetRangesList = mutable.ListBuffer[Array[OffsetRange]]()
val topic1 = Array("topic1")
val tableName1 = "table1"
val inputDS1: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream[String, String](
ssc,
LocationStrategies.PreferConsistent,
ConsumerStrategies.Subscribe[String, String](topic1, kafkaParams)
)
inputDS1.foreachRDD(rdd => {
offsetRangesList += rdd.asInstanceOf[HasOffsetRanges].offsetRanges
})
inputDS1
.map(_.value())
.map(x => JSONUtil.toBean(x, classOf[Table1]))
.foreachRDD((rdd: RDD[Table1]) => {
spark.createDataFrame(rdd).createOrReplaceTempView(tableName1)
})
val topic2 = Array("topic2")
val tableName2 = "table2"
val inputDS2: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream[String, String](
ssc,
LocationStrategies.PreferConsistent,
ConsumerStrategies.Subscribe[String, String](topic2, kafkaParams)
)
inputDS2.foreachRDD(rdd => {
offsetRangesList += rdd.asInstanceOf[HasOffsetRanges].offsetRanges
})
inputDS2
.map(_.value())
.map(x => JSONUtil.toBean(x, classOf[Table2]))
.foreachRDD((rdd: RDD[Table2]) => {
spark.createDataFrame(rdd).createOrReplaceTempView(tableName2)
})
val topic3 = Array("topic3")
val tableName3 = "table3"
val inputDS3: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream[String, String](
ssc,
LocationStrategies.PreferConsistent,
ConsumerStrategies.Subscribe[String, String](topic3, kafkaParams)
)
inputDS3.foreachRDD(rdd => {
offsetRangesList += rdd.asInstanceOf[HasOffsetRanges].offsetRanges
})
// 所有计算和结果写出都在下面维护
inputDS3
.map(_.value())
.map(x => JSONUtil.toBean(x, classOf[Table3]))
.foreachRDD(foreachFunc = (rdd: RDD[Table3]) => {
spark.createDataFrame(rdd).createOrReplaceTempView(tableName3)
// 从 hdfs 读取上一批次的计算结果
val lastDataDF: DataFrame = sqlContext.read.format("csv").option("header", "true").load("hdfs:///spark/latest-data")
lastDataDF.createOrReplaceTempView("last_result")
// 计算最新的结果
val resultDF: DataFrame = spark.sql("真正要执行的 sql 语句")
// 将结算结果进行输出,这里简单调用 show ,只是为了演示
resultDF.show(10)
// 将本批次结果写入 hdfs,供下次计算前初始化使用
resultDF.write.option("header", "true").mode(SaveMode.Overwrite).csv("hdfs:///spark/latest-data")
// 手动提交 offset
for (offsetRanges <- offsetRangesList) {
inputDS3.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges)
}
offsetRangesList.clear()
// 清理掉内存中的结果数据
resultDF.unpersist()
})
ssc.start()
ssc.awaitTermination()
}
}
步骤:
- 分别通过 spark-kafka 提供的
createDirectStream
函数创建连接 kafka 的InputDStream
输入流。 - 先不对输入流做任何处理,而是将输入流的
offsetRanges
放到一个列表中。 - 将输入流进行
map
处理,获取到 kafka 数据对应的value
值,并且将其转化为对应的case class
对象。我的示例中,kafka value 值类型为 JSON。 - 在每个流的
foreachRdd
函数中,将rdd
转化为DataFrame
,然后注册为表。 - 在最后一个的
foreachRdd
函数中,执行具体的sql
语句,然后获取到的结果进行输出。 - 最后手动提交 offset 值,可以使用 spark 提供的将 offset 值提交到 kafka 的方式,也可以采取其他方式,比如放到 mysql、redis等。如果任务场景对数据的一致性要求不是特别严格,可以采用最简单的方式,直接将其提交给 kafka,不过这个提交时机,并不是在调用了
commitAsync
函数后就会直接提交,而是 spark 任务在拉取下批次代码时才会提交,这个需要注册,这是有可能造成数据的重复消费的。 - 清理内存中的结果数据(貌似不清理也可以)。