updateStateByKey操作允许您在使用新的信息持续更新时保持任意状态。 要使用这个,你将不得不做两个步骤。
定义状态 - 状态可以是任意数据类型。
定义状态更新功能 - 使用函数指定如何使用上一个状态更新状态,并从输入流中指定新值。
在每个批处理中,Spark将对所有现有的密钥应用状态更新功能,无论它们是否具有批次中的新数据。 如果update函数返回None,则键值对将被消除。
我们来举例说明一下。 假设你想保持在文本数据流中看到的每个单词的运行计数。 这里,运行计数是状态,它是一个整数。 我们将更新功能定义为:
Function2<List<Integer>, Optional<Integer>, Optional<Integer>> updateFunction =
new Function2<List<Integer>, Optional<Integer>, Optional<Integer>>() {
@Override public Optional<Integer> call(List<Integer> values, Optional<Integer> state) {
Integer newSum = ... // add the new values with the previous running count to get the new count
return Optional.of(newSum);
}
};
JavaPairDStream<String, Integer> runningCounts = pairs.updateStateByKey(updateFunction);
将为每个单词调用updateFunction,其中newValues具有1的顺序(来自(单词,1)对)和runningCount具有先前的计数。
请注意,使用updateStateByKey需要配置checkpoint目录,这在checkpointing部分将详细讨论。
// 第一点,如果要使用updateStateByKey算子,就必须设置一个checkpoint目录,开启checkpoint机制
jssc.checkpoint("hdfs://spark001:9000/wordcount_checkpoint");
JavaReceiverInputDStream<String> lines = jssc.socketTextStream("spark001", 9999);
JavaDStream words = lines.flatMap(new FlatMapFunction(){
private static final long serialVersionUID = 1L;
@Override
public Iterable call(String line) throws Exception {
return Arrays.asList(line.split(" "));
}
});
JavaPairDStream pairs = words.mapToPair(new PairFunction(){
private static final long serialVersionUID = 1L;
@Override
public Tuple2 call(String word) throws Exception {
return new Tuple2(word, 1);
}
});
// updateStateByKey,就可以实现直接通过spark维护一份每个单词的全局的统计次数
JavaPairDStream<String, Integer> wordcounts = pairs.updateStateByKey(
// 这里的Optional,相当于scala中的样例类,就是Option,可以理解它代表一个状态,可能之前存在,也可能之前不存在
new Function2<List<Integer>, Optional<Integer>, Optional<Integer>>(){
private static final long serialVersionUID = 1L;
// 实际上,对于每个单词,每次batch计算的时候,都会调用这个函数,第一个参数,values相当于这个batch中,这个key的新的值,
// 可能有多个,比如一个hello,可能有2个1,(hello,1) (hello,1) 那么传入的是(1,1)
// 那么第二个参数表示的是这个key之前的状态,其实泛型的参数是你自己指定的
@Override
public Optional<Integer> call(List<Integer> values, Optional<Integer> state) throws Exception {
// 定义一个全局的计数
Integer newValue = 0;
if(state.isPresent()){
newValue = state.get();
}
for(Integer value : values){
newValue += value;
}
return Optional.of(newValue);
}
});
package com.chb.spark.streaming;
import java.util.Arrays;
import java.util.List;
import org.apache.spark.SparkConf;
import org.apache.spark.api.java.function.FlatMapFunction;
import org.apache.spark.api.java.function.Function2;
import org.apache.spark.api.java.function.PairFunction;
import org.apache.spark.streaming.Durations;
import org.apache.spark.streaming.api.java.JavaDStream;
import org.apache.spark.streaming.api.java.JavaPairDStream;
import org.apache.spark.streaming.api.java.JavaReceiverInputDStream;
import org.apache.spark.streaming.api.java.JavaStreamingContext;
import com.google.common.base.Optional;
import scala.Tuple2;
public class UpdateStateByKeyWordCount {
public static void main(String[] args) {
SparkConf conf = new SparkConf().setAppName("wordcount").setMaster("local[2]");
JavaStreamingContext jssc = new JavaStreamingContext(conf,Durations.seconds(5));
// 第一点,如果要使用updateStateByKey算子,就必须设置一个checkpoint目录,开启checkpoint机制
jssc.checkpoint("hdfs://spark001:9000/wordcount_checkpoint");
JavaReceiverInputDStream lines = jssc.socketTextStream("spark001", 9999);
JavaDStream words = lines.flatMap(new FlatMapFunction(){
private static final long serialVersionUID = 1L;
@Override
public Iterable call(String line) throws Exception {
return Arrays.asList(line.split(" "));
}
});
JavaPairDStream pairs = words.mapToPair(new PairFunction(){
private static final long serialVersionUID = 1L;
@Override
public Tuple2 call(String word) throws Exception {
return new Tuple2(word, 1);
}
});
// updateStateByKey,就可以实现直接通过spark维护一份每个单词的全局的统计次数
JavaPairDStream wordcounts = pairs.updateStateByKey(
// 这里的Optional,相当于scala中的样例类,就是Option,可以理解它代表一个状态,可能之前存在,也可能之前不存在
new Function2, Optional, Optional>(){
private static final long serialVersionUID = 1L;
// 实际上,对于每个单词,每次batch计算的时候,都会调用这个函数,第一个参数,values相当于这个batch中,这个key的新的值,
// 可能有多个,比如一个hello,可能有2个1,(hello,1) (hello,1) 那么传入的是(1,1)
// 那么第二个参数表示的是这个key之前的状态,其实泛型的参数是你自己指定的
@Override
public Optional call(List values, Optional state) throws Exception {
// 定义一个全局的计数
Integer newValue = 0;
if(state.isPresent()){
newValue = state.get();
}
for(Integer value : values){
newValue += value;
}
return Optional.of(newValue);
}
});
wordcounts.print();
jssc.start();
jssc.awaitTermination();
jssc.close();
}
}
goodsID-110::4::9.896811::0::0
goodsID-38::1::8.256732::0::1
goodsID-108::4::4.9478645::0::1
goodsID-183::1::9.024677::0::2
goodsID-166::2::9.436835::-1::0
goodsID-140::3::8.426323::0::0
goodsID-129::2::0.1236406::-1::0
goodsID-171::1::6.1114955::-1::2
goodsID-178::5::5.1479044::0::0
goodsID-66::5::6.0941777::-1::2
goodsID-112::3::7.1715264::-1::0
goodsID-10::5::7.8836794::-1::0
goodsID-180::1::6.3729606::-1::2
package com.chb.sparkstreaming.goods
import org.apache.spark.SparkConf
import org.apache.spark.streaming.StreamingContext
import org.apache.spark.streaming.Seconds
import java.text.NumberFormat
/**
* 统计物品的关注度
*/
object GoodQuota {
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf().setAppName("Goodquota").setMaster("local[*]")
val ssc = new StreamingContext(sparkConf, Seconds(5))
ssc.sparkContext.setCheckpointDir("checkDir")
//监听指定端口, 获取socket中的数据
val datas = ssc.socketTextStream("localhost", 8889)
//消息处理
val quotaDStream = datas.map( line => {
val arrays = line.split("::")
val goodId = arrays(0)
val lookTimes = arrays(1)
val duration = arrays(2)
val iscollect = arrays(3)
val num = arrays(4)
//商品关注度怎么计算呢,这个可能需要一个约定,就是说浏览次数和浏览时间以及购买力度和是否收藏该商品都有一个权重,可能不同的公司觉得不同的选项权重不一样,可能你觉得浏览时间更重要,另一人觉得浏览次数更重要,所以我们事先约定好这个计算公式。我们约定浏览次数的权重为0.8,浏览时间权重为0.6,是否收藏和购买力度都为1。
var quota:Double = lookTimes.toDouble *0.8 + duration.toDouble*0.6 + iscollect.toDouble + num.toDouble
//将指标格式化, 保留三位小数
val numberFormat = NumberFormat.getInstance
numberFormat.setMinimumFractionDigits(3)
quota = numberFormat.format(quota).toDouble
(goodId, quota)
})
//更新关注度
//由于是流式数据,数据每分每秒都在产生,那么计算的关注值也在变化,那么就需要更新这个状态值。使用updateStateByKey来进行操作。这也是这里相对比较难的知识点。
//对初始化的DStream进行Transformation级别的处理,例如map、filter等高阶函数等的编程,来进行具体的数据计算, 在这里是通过updateStateByKey来以Batch Interval为单位来对历史状态进行更新,在这里需要使用checkPoint,用于保存父RDD的值。在Spark1.6.X之后也可以尝试使用mapWithState来进行更新值。
val updateDStream = quotaDStream.updateStateByKey((values:Seq[Double], state:Option[Double]) => {
var updateValue = state.getOrElse(0.0)
for(value<-values){
updateValue += value
}
Option(updateValue)
})
//输出
/* //不可以使用transform
val sortDStream = updateDStream.transform(rdd => {
rdd.map(tuple=>(tuple._2, tuple._1)).sortByKey(false).map(tuple=>(tuple._2, tuple._1))
rdd
})
sortDStream.print(5)*/
updateDStream.foreachRDD(rdd => {
val sortRDd = rdd.map(tuple=>(tuple._2, tuple._1)).sortByKey(false).map(tuple=>(tuple._2, tuple._1))
//此处不可以直接使用rdd.take(5)获取top5
val ts = sortRDd.take(5)
for(tuple<-ts) {
println("商品: " + tuple._1 + "的关注度" + tuple._2)
}
})
/*
updateDStream.foreachRDD(rdd => {
val ts = rdd.map(tuple=>(tuple._2, tuple._1)).sortByKey(false).map(tuple=>(tuple._2, tuple._1)).take(5)
for(tuple<-ts) {
println("商品: " + tuple._1 + "的关注度:" + tuple._2)
}
})*/
ssc.start()
ssc.awaitTermination()
}
}