收藏鏈接:https://www.jb51.net/article/163065.htm
1、updateStateByKey
-
作用
可以讓我們爲每個key維護一份state,並持續不斷的更新該state; -
使用
1、首先,要定義一個state,可以是任意的數據類型;
2、其次,要定義state更新函數——指定一個函數如何使用之前state和新值來更新state; -
注意:
1、對於每個batch,Spark都會爲每個之前已經存在的key去應用一次state更新函數,無論這個key在batch中是否有新的數據;
2、如果state更新函數返貨none,那麼key對應state就會被刪除;
3、對於每個新出現的key,也會執行state更新函數;
4、updateStateByKey操作,要求必須開啓Checkpoint機制;
package cn.spark.study.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;
/**
* 基於updateStateByKey算子實現緩存機制的實時wordcount程序
*/
public class UpdateStateByKeyWordCount {
public static void main(String[] args) {
SparkConf conf = new SparkConf()
.setMaster("local[2]")
.setAppName("UpdateStateByKeyWordCount");
JavaStreamingContext jssc = new JavaStreamingContext(conf, Durations.seconds(5));
// 如果要使用updateStateByKey算子,就必須設置一個checkpoint目錄,開啓checkpoint機制
// 因爲要長期保存一份key的state的話,那麼spark streaming是要求必須用checkpoint的,
//以便於在內存數據丟失的時候,可以從checkpoint中恢復數據
// 開啓checkpoint機制
jssc.checkpoint("hdfs://spark1:9000/wordcount_checkpoint");
// 然後先實現wordcount邏輯
JavaReceiverInputDStream<String> lines = jssc.socketTextStream("localhost", 9999);
JavaDStream<String> words = lines.flatMap(new FlatMapFunction<String, String>() {
private static final long serialVersionUID = 1L;
@Override
public Iterable<String> call(String line) throws Exception {
return Arrays.asList(line.split(" "));
}
});
JavaPairDStream<String, Integer> pairs = words.mapToPair(
new PairFunction<String, String, Integer>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, Integer> call(String word)
throws Exception {
return new Tuple2<String, Integer>(word, 1);
}
});
// 關鍵點在這裏,之前的話,直接就是pairs.reduceByKey
// 然後,就可以得到每個時間段的batch對應的RDD,計算出來的單詞計數
// 然後,可以打印出那個時間段的單詞計數
// 但是,如果要統計每個單詞的全局的計數呢?
// 就是說,統計從程序啓動開始,到現在爲止,一個單詞出現的次數,那麼就之前的方式就不好實現
// 就必須基於redis這種緩存,或者是mysql這種db,來實現累加
// 但是,我們的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個,(hello, 1) (hello, 1),那麼傳入的是(1,1)
// 第二個參數,就是指的是這個key之前的狀態,state,其中泛型的類型是你自己指定的
@Override
public Optional<Integer> call(List<Integer> values,
Optional<Integer> state) throws Exception {
// 首先定義一個全局的單詞計數
Integer oldValue = 0;
// 其次,判斷,state是否存在,如果不存在,說明是一個key第一次出現
// 如果存在,說明這個key之前已經統計過全局的次數了
if(state.isPresent()) {
oldValue = state.get();
}
// 接着,將本次新出現的值,都累加到newValue上去,就是一個key目前的全局的統計
// 次數
for(Integer value : values) {
oldValue += value;
}
return Optional.of(oldValue );
}
});
// 到這裏爲止,相當於是,每個batch過來先計算到pairs DStream,
// 然後就會執行全局的updateStateByKey算子,updateStateByKey返回的JavaPairDStream,
// 其實就代表了每個key的全局的計數
wordCounts.print();
jssc.start();
jssc.awaitTermination();
jssc.close();
}
}
package cn.spark.study.streaming
import org.apache.spark.SparkConf
import org.apache.spark.streaming.StreamingContext
import org.apache.spark.streaming.Seconds
object UpdateStateByKeyWordCount {
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
.setMaster("local[2]")
.setAppName("UpdateStateByKeyWordCount")
val ssc = new StreamingContext(conf, Seconds(5))
ssc.checkpoint("hdfs://spark1:9000/wordcount_checkpoint")
val lines = ssc.socketTextStream("spark1", 9999)
val words = lines.flatMap { _.split(" ") }
val pairs = words.map { word => (word, 1) }
val wordCounts = pairs.updateStateByKey((values: Seq[Int], state: Option[Int]) => {
var oldValue = state.getOrElse(0)
for(value <- values) {
oldValue += value
}
Option(oldValue )
})
wordCounts.print()
ssc.start()
ssc.awaitTermination()
}
}
2、transform
transform操作,應用在DStream上時,可以用於執行任意的RDD到RDD的轉換操作;
它可以用於實現,DStream API中所沒有提供的操作;比如說,DStream API中,並沒有提供將一個DStream中的每個batch,與一個特定的RDD進行join的操作。但是我們自己就可以使用transform操作來實現該功能。
DStream.join(),只能join其他DStream,表示在DStream每個batch的RDD計算出來之後,會去跟其他DStream的RDD進行join。
package cn.spark.study.streaming;
import java.util.ArrayList;
import java.util.List;
import org.apache.spark.SparkConf;
import org.apache.spark.api.java.JavaPairRDD;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.function.Function;
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;
/**
* 基於transform的實時廣告計費日誌黑名單過濾
* 用戶對我們的網站上的廣告可以進行點擊
* 點擊之後,要進行實時計費,點一下,算一次錢
* 但是,對於那些幫助某些無良商家刷廣告的人,那麼我們有一個黑名單
* 只要是黑名單中的用戶點擊的廣告,我們就給過濾掉
*/
public class TransformBlacklist {
@SuppressWarnings("deprecation")
public static void main(String[] args) {
SparkConf conf = new SparkConf()
.setMaster("local[2]")
.setAppName("TransformBlacklist");
JavaStreamingContext jssc = new JavaStreamingContext(conf, Durations.seconds(5));
// 先做一份模擬的黑名單RDD
List<Tuple2<String, Boolean>> blacklist = new ArrayList<Tuple2<String, Boolean>>();
blacklist.add(new Tuple2<String, Boolean>("tom", true));
final JavaPairRDD<String, Boolean> blacklistRDD = jssc.sc().parallelizePairs(blacklist);
// 這裏的日誌格式,簡化一下,就是date username的方式
JavaReceiverInputDStream<String> adsClickLogDStream = jssc.socketTextStream("hadoop1", 9999);
// 所以,要先對輸入的數據,進行一下轉換操作,變成(username, date username)
// 以便於後面對每個batch RDD,與定義好的黑名單RDD進行join操作
JavaPairDStream<String, String> userAdsClickLogDStream = adsClickLogDStream.mapToPair(
new PairFunction<String, String, String>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, String> call(String adsClickLog)
throws Exception {
return new Tuple2<String, String>(
adsClickLog.split(" ")[1], adsClickLog);
}
});
// 然後,執行transform操作,將每個batch的RDD,與黑名單RDD進行join、filter、map等操作
// 實時進行黑名單過濾
JavaDStream<String> validAdsClickLogDStream = userAdsClickLogDStream.transform(
new Function<JavaPairRDD<String,String>, JavaRDD<String>>() {
private static final long serialVersionUID = 1L;
@Override
public JavaRDD<String> call(JavaPairRDD<String, String> userAdsClickLogRDD)
throws Exception {
// 這裏爲什麼用左外連接?
// 因爲,並不是每個用戶都存在於黑名單中的
// 所以,如果直接用join,那麼沒有存在於黑名單中的數據,會無法join到
// 就給丟棄掉了
// 所以,這裏用leftOuterJoin,就是說,哪怕一個user不在黑名單RDD中,沒有join到
// 也還是會被保存下來的
JavaPairRDD<String, Tuple2<String, Optional<Boolean>>> joinedRDD =
userAdsClickLogRDD.leftOuterJoin(blacklistRDD);
// 連接之後,執行filter算子
JavaPairRDD<String, Tuple2<String, Optional<Boolean>>> filteredRDD =
joinedRDD.filter(
new Function<Tuple2<String,
Tuple2<String,Optional<Boolean>>>, Boolean>() {
private static final long serialVersionUID = 1L;
@Override
public Boolean call(
Tuple2<String,
Tuple2<String, Optional<Boolean>>> tuple)
throws Exception {
// 這裏的tuple,就是每個用戶,對應的訪問日誌,和在黑名單中
// 的狀態
if(tuple._2._2().isPresent() &&
tuple._2._2.get()) {
return false;
}
return true;
}
});
// 此時,filteredRDD中,就只剩下沒有被黑名單過濾的用戶點擊了
// 進行map操作,轉換成我們想要的格式
JavaRDD<String> validAdsClickLogRDD = filteredRDD.map(
new Function<Tuple2<String,Tuple2<String,Optional<Boolean>>>, String>() {
private static final long serialVersionUID = 1L;
@Override
public String call(
Tuple2<String, Tuple2<String, Optional<Boolean>>> tuple)
throws Exception {
return tuple._2._1;
}
});
return validAdsClickLogRDD;
}
});
// 打印有效的廣告點擊日誌
// 其實在真實企業場景中,這裏後面就可以走寫入kafka、ActiveMQ等這種中間件消息隊列
// 然後再開發一個專門的後臺服務,作爲廣告計費服務,執行實時的廣告計費,這裏就是隻拿到了有效的廣告點擊
validAdsClickLogDStream.print();
jssc.start();
jssc.awaitTermination();
jssc.close();
}
}
package cn.spark.study.streaming
import org.apache.spark.SparkConf
import org.apache.spark.streaming.StreamingContext
import org.apache.spark.streaming.Seconds
object TransformBlacklist {
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
.setMaster("local[2]")
.setAppName("TransformBlacklist")
val ssc = new StreamingContext(conf, Seconds(5))
val blacklist = Array(("tom", true))
val blacklistRDD = ssc.sparkContext.parallelize(blacklist, 5)
val adsClickLogDStream = ssc.socketTextStream("spark1", 9999)
val userAdsClickLogDStream = adsClickLogDStream
.map { adsClickLog => (adsClickLog.split(" ")(1), adsClickLog) }
val validAdsClickLogDStream = userAdsClickLogDStream.transform(userAdsClickLogRDD => {
val joinedRDD = userAdsClickLogRDD.leftOuterJoin(blacklistRDD)
val filteredRDD = joinedRDD.filter(tuple => {
if(tuple._2._2.getOrElse(false)) {
false
} else {
true
}
})
val validAdsClickLogRDD = filteredRDD.map(tuple => tuple._2._1)
validAdsClickLogRDD
})
validAdsClickLogDStream.print()
ssc.start()
ssc.awaitTermination()
}
}
3、window滑動窗口
Spark Streaming提供了滑動窗口操作的支持,從而讓我們可以對一個滑動窗口內的數據執行計算操作。每次掉落在窗口內的RDD的數據,會被聚合起來執行計算操作,然後生成的RDD,會作爲window DStream的一個RDD。比如下圖中,就是對每三秒鐘的數據執行一次滑動窗口計算,這3秒內的3個RDD會被聚合起來進行處理,然後過了兩秒鐘,又會對最近三秒內的數據執行滑動窗口計算。所以每個滑動窗口操作,都必須指定兩個參數,窗口長度以及滑動間隔,而且這兩個參數值都必須是batch間隔的整數倍。(Spark Streaming對滑動窗口的支持,是比Storm更加完善和強大的)
image.png
image.png
Demo:熱點搜索詞滑動統計,每隔10秒鐘,統計最近60秒鐘的搜索詞的搜索頻次,並打印出排名最靠前的3個搜索詞以及出現次數
package cn.spark.study.streaming;
import java.util.List;
import org.apache.spark.SparkConf;
import org.apache.spark.api.java.JavaPairRDD;
import org.apache.spark.api.java.function.Function;
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 scala.Tuple2;
/**
* 基於滑動窗口的熱點搜索詞實時統計
*/
public class WindowHotWord {
public static void main(String[] args) {
SparkConf conf = new SparkConf()
.setMaster("local[2]")
.setAppName("WindowHotWord");
JavaStreamingContext jssc = new JavaStreamingContext(conf, Durations.seconds(1));
// 這裏的搜索日誌的格式是
// leo hello
// tom world
JavaReceiverInputDStream<String> searchLogsDStream = jssc.socketTextStream("spark1", 9999);
// 將搜索日誌給轉換成,只有一個搜索詞,即可
JavaDStream<String> searchWordsDStream = searchLogsDStream.map(new Function<String, String>() {
private static final long serialVersionUID = 1L;
@Override
public String call(String searchLog) throws Exception {
return searchLog.split(" ")[1];
}
});
// 將搜索詞映射爲(searchWord, 1)的tuple格式
JavaPairDStream<String, Integer> searchWordPairDStream = searchWordsDStream.mapToPair(
new PairFunction<String, String, Integer>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, Integer> call(String searchWord)
throws Exception {
return new Tuple2<String, Integer>(searchWord, 1);
}
});
// 針對(searchWord, 1)的tuple格式的DStream,執行reduceByKeyAndWindow,滑動窗口操作
// 第二個參數,是窗口長度,這裏是60秒
// 第三個參數,是滑動間隔,這裏是10秒
// 也就是說,每隔10秒鐘,將最近60秒的數據,作爲一個窗口,進行內部的RDD的聚合,
// 然後統一對一個RDD進行後續計算
// 所以,到之前的searchWordPairDStream爲止,其實,都是不會立即進行計算的
// 而是隻是放在那裏
// 然後,等待我們的滑動間隔到了以後,10秒鐘到了,會將之前60秒的RDD,因爲一個batch間隔是,5秒,
//所以之前 60秒,就有12個RDD,給聚合起來,然後,統一執行redcueByKey操作
// 所以這裏的reduceByKeyAndWindow,是針對每個窗口執行計算的,而不是針對某個DStream中的RDD
JavaPairDStream<String, Integer> searchWordCountsDStream =
searchWordPairDStream.reduceByKeyAndWindow(new Function2<Integer, Integer, Integer>() {
private static final long serialVersionUID = 1L;
@Override
public Integer call(Integer v1, Integer v2) throws Exception {
return v1 + v2;
}
}, Durations.seconds(60), Durations.seconds(10));
// 把之前60秒收集到的單詞的統計次數執行transform操作,
// 因爲,一個窗口,就是一個60秒鐘的數據,會變成一個RDD,然後,對這一個RDD
// 根據每個搜索詞出現的頻率進行排序,然後獲取排名前3的熱點搜索詞
JavaPairDStream<String, Integer> finalDStream = searchWordCountsDStream.transformToPair(
new Function<JavaPairRDD<String,Integer>, JavaPairRDD<String,Integer>>() {
private static final long serialVersionUID = 1L;
@Override
public JavaPairRDD<String, Integer> call(
JavaPairRDD<String, Integer> searchWordCountsRDD) throws Exception {
// 執行搜索詞和出現頻率的反轉
JavaPairRDD<Integer, String> countSearchWordsRDD = searchWordCountsRDD
.mapToPair(new PairFunction<Tuple2<String,Integer>, Integer, String>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<Integer, String> call(
Tuple2<String, Integer> tuple)
throws Exception {
return new Tuple2<Integer, String>(tuple._2, tuple._1);
}
});
// 然後執行降序排序
JavaPairRDD<Integer, String> sortedCountSearchWordsRDD = countSearchWordsRDD
.sortByKey(false);
// 然後再次執行反轉,變成(searchWord, count)的這種格式
JavaPairRDD<String, Integer> sortedSearchWordCountsRDD = sortedCountSearchWordsRDD
.mapToPair(new PairFunction<Tuple2<Integer,String>, String, Integer>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, Integer> call(
Tuple2<Integer, String> tuple)
throws Exception {
return new Tuple2<String, Integer>(tuple._2, tuple._1);
}
});
// 然後用take(),獲取排名前3的熱點搜索詞
List<Tuple2<String, Integer>> hogSearchWordCounts =
sortedSearchWordCountsRDD.take(3);
for(Tuple2<String, Integer> wordCount : hogSearchWordCounts) {
System.out.println(wordCount._1 + ": " + wordCount._2);
}
return searchWordCountsRDD;
}
});
// 這個無關緊要,只是爲了觸發job的執行,所以必須有output操作
finalDStream.print();
jssc.start();
jssc.awaitTermination();
jssc.close();
}
}
package cn.spark.study.streaming
import org.apache.spark.SparkConf
import org.apache.spark.streaming.StreamingContext
import org.apache.spark.streaming.Seconds
object WindowHotWord {
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
.setMaster("local[2]")
.setAppName("WindowHotWord")
val ssc = new StreamingContext(conf, Seconds(1))
val searchLogsDStream = ssc.socketTextStream("spark1", 9999)
val searchWordsDStream = searchLogsDStream.map { _.split(" ")(1) }
val searchWordPairsDStream = searchWordsDStream.map { searchWord => (searchWord, 1) }
val searchWordCountsDSteram = searchWordPairsDStream.reduceByKeyAndWindow(
(v1: Int, v2: Int) => v1 + v2,
Seconds(60),
Seconds(10))
val finalDStream = searchWordCountsDSteram.transform(searchWordCountsRDD => {
val countSearchWordsRDD = searchWordCountsRDD.map(tuple => (tuple._2, tuple._1))
val sortedCountSearchWordsRDD = countSearchWordsRDD.sortByKey(false)
val sortedSearchWordCountsRDD = sortedCountSearchWordsRDD.map(tuple => (tuple._1, tuple._2))
val top3SearchWordCounts = sortedSearchWordCountsRDD.take(3)
for(tuple <- top3SearchWordCounts) {
println(tuple)
}
searchWordCountsRDD
})
finalDStream.print()
ssc.start()
ssc.awaitTermination()
}
}