reduceByKey 是按key进行计算,操作的数据是每个批次内的数据(一个采集周期),不能跨批次计算。如果需要实现对历史数据的跨批次统计累加,则需要使用updateStateByKey算子或者mapWithState算子。
package com.sparkscala.streaming
import org.apache.log4j.{Level, Logger}
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.streaming.{Seconds, StreamingContext}
/**
* reduceByKey只能计算一个批次(batch),即Seconds(3)内的数据
*/
object StreamingWordCountScala {
def main(args: Array[String]): Unit = {
Logger.getLogger("org").setLevel(Level.WARN)
//一、初始化程序入口
/**
* local[1] 中括号里面的数字都代表的是启动几个工作线程,默认情况下是一个工作线程。
* 那么作为sparkStreaming 我们至少要开启两个线程,因为其中一个线程用来接收数据,这样另外一个线程用来处理数据。
* Seconds 指的是每次数据数据的时间范围(batch interval)
*/
val conf: SparkConf = new SparkConf().setMaster("local[2]").setAppName(this.getClass.getSimpleName)
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
//二、获取数据流,就是数据源
val lines: ReceiverInputDStream[String] = ssc.socketTextStream("192.168.244.130", 1234)
//三、数据处理
//val result: DStream[(String, Int)] = lines.flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ + _)
val words: DStream[String] = lines.flatMap(_.split(" "))
val wordAndOne: DStream[(String, Int)] = words.map((_, 1))
val wordResult: DStream[(String, Int)] = wordAndOne.reduceByKey(_ + _)
//四、数据输出查看
wordResult.print()
//五、启动任务
ssc.start() //启动
ssc.awaitTermination() //线程等待,等待处理下一批次任务
ssc.stop() //关闭
}
}
updateStateByKey 算子是统计历史所有的数据,实现累加
注意:
以WordCount计算为例:
package com.sparkscala.streaming
import org.apache.log4j.{Level, Logger}
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}
/**
* updateStateByKey操作使得我们可以在用新信息进行更新时保持任意的状态。为使用这个功能,你需要做下面两步:
* 1. 定义状态,状态可以是一个任意的数据类型。
* 2. 定义状态更新函数,用此函数阐明如何使用之前的状态和来自输入流的新值对状态进行更新。
* 使用updateStateByKey需要对检查点目录进行配置,会使用检查点来保存状态
*/
object UpdateStateByKeyDemo {
def main(args: Array[String]): Unit = {
Logger.getLogger("org").setLevel(Level.WARN)
//一、初始化程序入口
val conf: SparkConf = new SparkConf().setMaster("local[2]").setAppName(this.getClass.getSimpleName)
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
//为了实现对历史数据的累加,需要设置检查点目录
ssc.checkpoint("D:\\Java Project\\DATA\\UpdateStateByKeyDemo_checkpoint")
//二、读取数据流,就是数据源
val lines: ReceiverInputDStream[String] = ssc.socketTextStream("hadoop2", 9999)
//三、数据处理
val words: DStream[String] = lines.flatMap(_.split(" "))
val wordAndOne: DStream[(String, Int)] = words.map((_, 1))
/**
* updateStateByKey()内部需要出入一个函数 updateFunc: (Seq[V],Option[S]) => Option[S]
* 参数解释:
* 1、Seq[V]: 表示当前key对应的所有value值
* hadoop 1
* hadoop 1
* hadoop 1
* 会进行分组:
* {hadoop, (1,1,1)} -> values(1,1,1)
*
* 2、Option[S]: 历史状态的记录值,即当前key的历史状态
* 代表上一批次中相同key对应的累加的结果,有可能有值,有可能没有值。此时,获取历史批次的数据时,最好用getOrElse方法。
* Option有两个子类:
* Some 有值
* None 没有值
*
* 返回值:
* Option[S]: 返回的是新的历史状态,这里要的就是key出现的次数
*
*/
val wordResult: DStream[(String, Int)] = wordAndOne.updateStateByKey((values: Seq[Int], state: Option[Int]) => {
val currentCount = values.sum //将目前新进来的批次的所有value值相加
val lastCount = state.getOrElse(0) //取出之前累加统计的历史状态值
Some(currentCount + lastCount) //目前值的和加上历史值,完成状态的更新
})
//四、数据输出
wordResult.print()
//五、启动任务
ssc.start()
ssc.awaitTermination() //线程等待,等待处理下一批次任务
ssc.stop()
}
}
mapWithState:也是用于全局统计key的状态,但是它如果没有数据输入,在没有设置全局输出的情况下,默认不会返回之前的key的状态,类似于增量的感觉。
注意:mapWithState算子比updateStateByKey效率更高,因为:
package com.sparkscala.streaming
import org.apache.log4j.{Level, Logger}
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.dstream.{DStream, MapWithStateDStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, State, StateSpec, StreamingContext, Time}
object MapWithStateDemo {
def main(args: Array[String]): Unit = {
Logger.getLogger("org").setLevel(Level.WARN)
//一、初始化程序入口
val conf: SparkConf = new SparkConf().setMaster("local[2]").setAppName(this.getClass.getSimpleName)
//val sc: SparkContext = new SparkContext(conf)
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
//设置检查点目录
ssc.checkpoint("D:\\Java Project\\DATA\\MapStateByKeyDemo_checkpoint")
//二、读取数据流,也就是数据源
val lines: ReceiverInputDStream[String] = ssc.socketTextStream("hadoop2", 9999)
//三、数据处理
val words: DStream[String] = lines.flatMap(_.split(" "))
val wordAndOne: DStream[(String, Int)] = words.map((_, 1))
//可以设置初始值
val initialRDD: RDD[(String, Long)] = ssc.sparkContext.parallelize(List(("flink", 100L), ("spark", 50L)))
/**
* 示例:假如输入 hadoop hadoop hadoop
* 切分之后变成了:
* hadoop 1
* hadoop 1
* hadoop 1
*
* mapWithState 里面也有byKey操作 -> 在byKey分组的时候顺带就完成了合并的操作
* 经过 mapWithState 里面的 byKey 操作之后,变成了如下:
* {hadoop,(1,1,1) => 3}
* hadoop 3
*
* 假设这个 key 的历史状态是:hadoop 10
* key: hadoop 当前的key
* value: 3 当前key出现的次数
* state: 当前这个key的历史状态
*
* hadoop 3
* hadoop 10
*
* 更新后的状态:hadoop 13
*/
/**
* 状态更新函数 StateSpec.function: ((Time, K, Option[V], State[S]) => Option[T])
* 参数解释:
* currentBatchTime: 表示当前的批次(Batch)的时间
* key: 表示当前需要更新状态的key
* value: 表示当前批次(batch)的key的对应的值
* state: 之前该key的状态值,代表的就是状态(历史状态,也就是上次的结果)
*/
val stateSpec: StateSpec[String, Int, Long, (String, Long)] = StateSpec.function((currentBatchTime: Time, key: String, value: Option[Int], state: State[Long]) => {
val sum = value.getOrElse(0).toLong + state.getOption().getOrElse(0L)
val output = (key, sum)
//更新状态值,如果你的数据没有超时的话
if (!state.isTimingOut()) {
state.update(sum)
}
Some(output) //返回值,要求返回的是key-value类型的
}).initialState(initialRDD) //设置初始值
.numPartitions(2).timeout(Seconds(10))
//timeout:超时。当一个key超过Seconds(10)这个时间没有接收到新数据的时候,这个key以及对应的状态会被移除掉,也就是重新统计。
val result: MapWithStateDStream[String, Int, Long, (String, Long)] = wordAndOne.mapWithState(stateSpec)
//四、数据输出
//result.print() //打印出来发生变化的数据
result.stateSnapshots().print() //打印出来的是全量的数据
//五、启动任务
ssc.start()
ssc.awaitTermination()
ssc.stop()
}
}
package com.sparkscala.streaming
import org.apache.log4j.{Level, Logger}
import org.apache.spark.SparkConf
import org.apache.spark.broadcast.Broadcast
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}
/**
* 通过 transform 算子来实现黑名单过滤的效果
*
* 定义黑名单的规则:$ ? ! 过滤掉
*/
object TransformDemo {
def main(args: Array[String]): Unit = {
Logger.getLogger("org").setLevel(Level.WARN)
//一、设置程序入口
val conf: SparkConf = new SparkConf().setMaster("local[2]").setAppName(this.getClass.getSimpleName)
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
//二、获取数据流,即数据源
val lines: ReceiverInputDStream[String] = ssc.socketTextStream("hadoop2", 9993)
//三、数据处理
val words: DStream[String] = lines.flatMap(_.split(" "))
val wordAndOne: DStream[(String, Int)] = words.map((_, 1))
//具体的黑名单操作 定义黑名单的规则:$ ? ! 过滤掉
//定义黑名单 首先要获取到黑名单,企业中可以从Mysql,Redis里面去获取。
val filterRDD: RDD[(String, Boolean)] = ssc.sparkContext.parallelize(List("$", "?", "!")).map((_, true))
//优化:给过滤的规则数据通过广播变量广播出去
val filterBroadCast: Broadcast[Array[(String, Boolean)]] = ssc.sparkContext.broadcast(filterRDD.collect())
//实现过滤
val filterResult: DStream[(String, Int)] = wordAndOne.transform(rdd => {
val filterRDD2: RDD[(String, Boolean)] = ssc.sparkContext.parallelize(filterBroadCast.value)
/**
* 左外连接join,如果join不上就保留
* (String(key), (Int(1), Option[Boolean]))
* 通过这个option没值 来进行判断
*/
val result: RDD[(String, (Int, Option[Boolean]))] = rdd.leftOuterJoin(filterRDD2)
val joinResult: RDD[(String, (Int, Option[Boolean]))] = result.filter(tuple => {
tuple._2._2.isEmpty //过滤出来我们想要的数据
})
//在Scala里面最后一行就是方法的返回值
joinResult.map(tuple => (tuple._1, tuple._2._1))
//将黑名单字符替换成 * 号
/*val changeResult: RDD[(String, Int)] = result.map(rdd => {
if (rdd._2._2.isEmpty) {
(rdd._1, rdd._2._1)
} else {
("*", 1)
}
})
changeResult*/
})
//实现累加
val finalResult: DStream[(String, Int)] = filterResult.reduceByKey(_ + _)
//四、数据输出
finalResult.print() //打印出来发生变化的数据
//五、启动任务
ssc.start()
ssc.awaitTermination()
ssc.stop()
}
}
reduceByKeyAndWindow 窗口函数允许你在一个滑动的窗口中进行计算。
package com.sparkscala.streaming
import org.apache.log4j.{Level, Logger}
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}
/**
* 比如说我们现在要每隔4秒,统计前6秒内每一个单词出现的次数,这个时候需要用reduceByKeyAndWindow
*/
object WindowDemo {
def main(args: Array[String]): Unit = {
Logger.getLogger("org").setLevel(Level.ERROR)
//一、初始化程序入口
val conf: SparkConf = new SparkConf().setMaster("local[2]").setAppName(this.getClass.getSimpleName)
val ssc: StreamingContext = new StreamingContext(conf, Seconds(2))
//二、获取数据流,即数据源
val lines: ReceiverInputDStream[String] = ssc.socketTextStream("hadoop2", 9995)
//三、数据处理
val words: DStream[String] = lines.flatMap(_.split(" "))
val wordAndOne: DStream[(String, Int)] = words.map((_, 1))
/**
* reduceByKeyAndWindow 需要 3 个参数:
* 一个函数: reduceFunc: (V, V) => V 也就是reduceByKey
* windowDuration 窗口大小,即窗口的持续时间
* slideDuration 滑动间隔,即执行窗口的间隔时间
*
* 请注意:窗口大小和滑动间隔必须是间隔的整数倍
* 间隔:new StreamingContext(conf, Seconds(2))
* 窗口大小:Seconds(6)
* 滑动间隔:Seconds(4)
*
* 如 每隔2秒计算一下最近6秒的单词出现的次数
* reduceByKeyAndWindow((x:Int,y:Int)=>x+y, Seconds(6), Seconds(2))
*/
val result: DStream[(String, Int)] = wordAndOne.reduceByKeyAndWindow((x:Int,y:Int)=>x+y, Seconds(6), Seconds(4))
//四、数据输出
result.print()
//五、启动任务
ssc.start()
ssc.awaitTermination()
ssc.stop()
}
}
SparkStreaming和SparkSQL整合之后,就非常的方便,可以使用SQL的方式操作相应的数据,很方便。
package com.sparkscala.streaming
import org.apache.log4j.{Level, Logger}
import org.apache.spark.SparkConf
import org.apache.spark.sql.{DataFrame, SparkSession}
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}
object StreamAndSQLDemo {
def main(args: Array[String]): Unit = {
Logger.getLogger("org").setLevel(Level.WARN)
//一、设置程序入口
val conf: SparkConf = new SparkConf().setMaster("local[2]").setAppName(this.getClass.getSimpleName)
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
//二、获取数据流,即数据源
val lines: ReceiverInputDStream[String] = ssc.socketTextStream("hadoop2", 9996)
//三、数据处理
//这里必须先转换成DStream才能进行下面的转换 toDF 操作
val words: DStream[String] = lines.flatMap(_.split(" "))
//获取到一个一个的单词
words.foreachRDD(rdd => {
val spark: SparkSession = SparkSession.builder().config(rdd.sparkContext.getConf).getOrCreate()
import spark.implicits._
//隐式转换
val wordDataFrame: DataFrame = rdd.toDF("word")
//注册一个临时视图
wordDataFrame.createOrReplaceTempView("words")
//数据输出
spark.sql("select word, count(*) as totalCount from words group by word").show()
})
//五、启动任务
ssc.start()
ssc.awaitTermination()
ssc.stop()
}
}