Spark Streaming(三):DStream的transformation操作

收藏鏈接: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()
  }
}

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章