Spark Streaming筆記

目錄

 

一、概念

二、Dstream入門

1、Dstream創建

2、RDD隊列(瞭解)

3、根據端口號採集數據

4、自定義數據源

5、Kafka數據源(重點)

三、DStream轉換

1、無狀態轉化操作

2、有狀態轉化操作(重點)

四、案例

1、WordCount案例實操(單次、累計)

2、把sparkstreaming獲取到的數據存在redis裏


一、概念

Spark Streaming用於流式數據的處理。Spark Streaming支持的數據輸入源很多,例如:Kafka、Flume、Twitter、ZeroMQ和簡單的TCP套接字等等。數據輸入後可以用Spark的高度抽象原語如:map、reduce、join、window等進行運算。而結果也能保存在很多地方,如HDFS,數據庫等。

和Spark基於RDD的概念很相似,Spark Streaming使用離散化流(discretized stream)作爲抽象表示,叫作DStream。DStream 是隨時間推移而收到的數據的序列。在內部,每個時間區間收到的數據都作爲 RDD 存在,而DStream是由這些RDD所組成的序列(因此得名“離散化”)。

二、Dstream入門

1、Dstream創建

(1)文件數據源

streamingContext.textFileStream(dataDirectory)

Spark Streaming 將會監控指定目錄並不斷處理移動進來的文件,目前不支持嵌套目錄。

注意事項:

1)文件需要有相同的數據格式;

2)文件進入 dataDirectory的方式需要通過移動或者重命名來實現;

3)一旦文件移動進目錄,則不能再修改,即便修改了也不會讀取新數據;

案例:  (初步測試本地不成功)

原因: 這路徑如果hdfs的路徑 你直接hadoop fs -put
到你的監測路徑就可以,如果是本地目錄用file:///home/data
你不能移動文件到這個目錄,必須用流的形式寫入到這個目錄形成文件才能被監測到。

import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.streaming.dstream.DStream

object FileStream {

  def main(args: Array[String]): Unit = {

    //1.初始化Spark配置信息
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("StreamWordCount")

    //2.初始化SparkStreamingContext
    val ssc: StreamingContext = new StreamingContext(sparkConf,Seconds(5))

    //3.監控文件夾創建DStream
    val dirStream: DStream[String] = ssc.textFileStream("hdfs://hdp-1:9000/spark")

    //4.將每一行數據做切分,形成一個個單詞
    val wordStreams: DStream[String] = dirStream.flatMap(_.split(","))

    //5.將單詞映射成元組(word,1)
    val wordAndOneStreams = wordStreams.map((_, 1))

    //6.將相同的單詞次數做統計
    val wordAndCountStreams = wordAndOneStreams.reduceByKey(_ + _)

    //7.打印
    wordAndCountStreams.print()

    //8.啓動SparkStreamingContext
    ssc.start()
    ssc.awaitTermination()
  }
}

 

SparkStreaming的輸出方式:
1、print()         打印DStream中每個批次的前十個元素。用於開發調試。
2、saveAsTextFiles(prefix, [suffix])          保存爲文本文件。每個批處理間隔的文件名基於前綴和後綴生成:“prefix-TIME_IN_MS [.suffix]”。
3、saveAsObjectFiles(prefix, [suffix])        將DStream的內容序列化爲Java對象,並保存到SequenceFiles。每個批處理間隔的文件名基於前綴和後綴生成:“prefix-TIME_IN_MS [.suffix]”。
4、saveAsHadoopFiles(prefix, [suffix])      將DStream的內容保存爲Hadoop文件。每個批處理間隔的文件名基於前綴和後綴生成:“prefix-TIME_IN_MS [.suffix]”。
5、foreachRDD(func) 最通用的輸出方式,它將函數func應用於從流生成的每個RDD。此函數應將每個RDD中的數據推送到外部系統,例如將RDD保存到文件,或通過網絡將其寫入數據庫。

2、RDD隊列(瞭解)

用法及說明:使用ssc.queueStream(queueOfRDDs)創建DStream,每個推送到這個隊列中的RDD,都會作爲一個DStream處理。

案例:循環創建幾個RDD,將RDD放入隊列。通過SparkStream創建Dstream,計算WordCount

import org.apache.spark.SparkConf
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.dstream.{DStream, InputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}

import scala.collection.mutable
object RDDStream {
  def main(args: Array[String]) {

    //1.初始化Spark配置信息
    val conf = new SparkConf().setMaster("local[*]").setAppName("RDDStream")

    //2.初始化SparkStreamingContext
    val ssc = new StreamingContext(conf, Seconds(4))

    //3.創建RDD隊列
    val rddQueue = new mutable.Queue[RDD[Int]]()

    //4.創建QueueInputDStream
    val inputStream = ssc.queueStream(rddQueue,oneAtATime = false)

    //5.處理隊列中的RDD數據
    val mappedStream = inputStream.map((_,1))
    val reducedStream = mappedStream.reduceByKey(_ + _)

    //6.打印結果
    reducedStream.print()

    //7.啓動任務
    ssc.start()

//8.循環創建並向RDD隊列中放入RDD
    for (i <- 1 to 5) {
      rddQueue += ssc.sparkContext.makeRDD(1 to 300, 10)
      Thread.sleep(2000)
    }
    ssc.awaitTermination()
  }
}

3、根據端口號採集數據

val sparkConf = new SparkConf().setAppName("NetworkWordCount").setMaster("local[2]")
val ssc = new StreamingContext(sparkConf, Seconds(10))   //每隔十秒刷新採集一次

/*創建文本輸入流,並進行詞頻統計 reduceByKey 結果不累加*/
val lines = ssc.socketTextStream("hdp-1", 9999)

4、自定義數據源

繼承Receiver,並實現onStart、onStop方法來自定義數據源採集。

自定義receiver

import java.io.{BufferedReader, InputStreamReader}
import java.net.Socket
import java.nio.charset.StandardCharsets
import org.apache.spark.storage.StorageLevel
import org.apache.spark.streaming.receiver.Receiver

class CustomerReceiver(host: String, port: Int) extends Receiver[String](StorageLevel.MEMORY_ONLY) {

  //最初啓動的時候,調用該方法,作用爲:讀數據並將數據發送給Spark
  override def onStart(): Unit = {
    new Thread("Socket Receiver") {     //線程可以一直存在,一直執行任務
      override def run() {
        receive()
      }
    }.start()
  }
  //讀數據並將數據發送給Spark
  def receive(): Unit = {
    //創建一個Socket
    var socket: Socket = new Socket(host, port)
    //定義一個變量,用來接收端口傳過來的數據
    var input: String = null
    //創建一個BufferedReader用於讀取端口傳來的數據
    val reader = new BufferedReader(new InputStreamReader(socket.getInputStream, StandardCharsets.UTF_8))
    //讀取數據
    input = reader.readLine()
    //當receiver沒有關閉並且輸入數據不爲空,則循環發送數據給Spark
    while (!isStopped() && input != null) {
      if("END".equals(input)){ //發送END就結束收集,此時發送數據不再收集,但是程序會一直執行
        return
      }else{
        store(input)     //store保存讀取到的數據
        input = reader.readLine()
      }
    }
    //跳出循環則關閉資源
    reader.close()
    socket.close()
    //重啓任務
    restart("restart")
  }
  override def onStop(): Unit = {}
}

在StreamingContext聲明Receiver

object FileStreamTwo{
  def main(args: Array[String]): Unit = {
    //1.初始化Spark配置信息
    val sparkConf = new SparkConf().setMaster("local[*]")
      .setAppName("StreamWordCount")
    //2.初始化SparkStreamingContext
    val ssc = new StreamingContext(sparkConf, Seconds(5))
    
    //3.創建自定義receiver的Streaming
    val lineStream = ssc.receiverStream(new CustomerReceiver("hdp-1", 9999))
    
    //4.將每一行數據做切分,形成一個個單詞
    lineStream.flatMap(_.split(",")).map((_, 1)).reduceByKey(_ + _).print()
    //8.啓動SparkStreamingContext
    ssc.start()
    ssc.awaitTermination()
  }
}

5、Kafka數據源(重點)

引入依賴:(有坑,切記版本與(scala、spark、kafka)環境對應,否則存在報錯現象)

        <!--SparkStreaming-->
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-streaming_2.11</artifactId>
            <version>2.3.1</version>
<!--            <scope>provided</scope>-->        <!--  打包linux用 -->    
        </dependency>
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-streaming-kafka_2.11</artifactId>
            <version>1.5.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.kafka</groupId>
            <artifactId>kafka-clients</artifactId>
            <version>2.1.1</version>
        </dependency>
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.kafka.KafkaUtils
import org.apache.spark.streaming.{Seconds, StreamingContext}
import redis.clients.jedis.{Jedis}

object WithKafka {
  def main(args: Array[String]): Unit = {
    val sparkConf: SparkConf = new SparkConf().setAppName("23").setMaster("local[*]")
    val ssc = new StreamingContext(sparkConf,Seconds(5))
    val kafkaDStream: ReceiverInputDStream[(String, String)] = KafkaUtils.createStream(
      ssc,
      "hdp-1:2181", //zookeeper
      "dstream", //消費者組groupid
      Map("dstream" -> 3) //map中存放多個topic主題,格式爲:<topic,分區數>
    )
//  tuple=>tuple._2.split(",")是因爲kafka傳過來數據是<k,v> k默認null,v是我們輸入的值
    val value: DStream[String] = kafkaDStream.flatMap(tuple=>tuple._2.split(","))
//    value.map((_, 1)).reduceByKey(_ + _).print()
    val reduceDStream: DStream[(String, Int)] = value.map((_, 1)).reduceByKey(_ + _)

    reduceDStream.foreachRDD(rdd=>rdd.foreachPartition(r=>{
      var jedis: Jedis = null
        try {
          jedis = JedisPoolUtil.getConnection
        jedis.auth("123456")
        r.foreach(date => {
//hash <key,map>  map<String,String>    hincrBy(hashName,mapkey,mapvalue) Redis Hincrby 命令用於爲哈希表中的字段值加上指定增量值。
          jedis.hincrBy("sparkstreaming", date._1, date._2)
        })
      }catch {
        case ex: Exception =>
          ex.printStackTrace()
      } finally {
        if (jedis != null) jedis.close()
      }
    }))
    ssc.start()
    ssc.awaitTermination()
  }
}

在linux運行kafka創建topic、啓動生產者

 ./kafka-topics.sh --zookeeper hdp-1:2181 --create --topic dstream --partitions 3 --replication-factor 2

./kafka-console-producer.sh --broker-list hdp-1:9092 --topic dstrean

啓動redis

 ./redis-server ../redis.conf 

注意點:reducebykey是無狀態轉移,只記錄一個週期seconds統計的數據,redis中hincry是追加數據,所以也會實現累加

坑2:spark2.0有可能報錯java.lang.NoClassDefFoundError: org/apache/spark/Logging錯誤

解決方案:在對應項目執行類的src/main/java下創建文件夾org.apache.spark,把logging jar包代碼拷貝trait中

        streaming是自定義Sources文件夾,同java一樣

三、DStream轉換

1、無狀態轉化操作

2、有狀態轉化操作(重點)

updateStateByKey() 的結果會是一個新的 DStream,其內部的 RDD 序列是由每個時間區間對應的(鍵,狀態)對組成的。

updateStateByKey操作使得我們可以在用新信息進行更新時保持任意的狀態。爲使用這個功能,你需要做下面兩步: 

1. 定義狀態,狀態可以是一個任意的數據類型。 

2. 定義狀態更新函數,用此函數闡明如何使用之前的狀態和來自輸入流的新值對狀態進行更新。

使用updateStateByKey需要對檢查點目錄進行配置,會使用檢查點來保存狀態

1、updateStateByKey()

import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.kafka.KafkaUtils

//TODO updateStateByKey實現累加wordcount,從kafka獲取
object leijia {
  def main(args: Array[String]): Unit = {
    val sparkConf: SparkConf = new SparkConf().setAppName("23").setMaster("local[*]")
    val ssc = new StreamingContext(sparkConf,Seconds(5))
//    ssc.checkpoint("check")
    ssc.sparkContext.setCheckpointDir("check")
    val kafkaDStream: ReceiverInputDStream[(String, String)] = KafkaUtils.createStream(
      ssc,
      "hdp-1:2181", //zookeeper
      "dstream", //消費者組groupid
      Map("dstream" -> 3) //map中存放多個topic主題,格式爲:<topic,分區數>
    )

    val value: DStream[String] = kafkaDStream.flatMap(tuple=>tuple._2.split(","))
    val maped: DStream[(String, Int)] = value.map((_, 1))
    val stateDStream: DStream[(String, Int)] = maped.updateStateByKey {
      case (seq, buffer) => {
        //seq是序列,即maped的value,即1 1 1 1 的序列,是舊的值,buffer是內存中的數據,getOrElse如果獲取不到,默認是0,是本次週期採集的數據
        val sum: Int = buffer.getOrElse(0) + seq.sum
//        seq.foldLeft(0)(_ + _)  //0初始值, _+_ 累加
        Option(sum)   //Some(sum)   Option包括Some和None
      }
    }
    stateDStream.print()
    ssc.start()
    ssc.awaitTermination()
  }
}

2、Window Operations

Window Operations可以設置窗口的大小和滑動窗口的間隔來動態的獲取當前Steaming的允許狀態。基於窗口的操作會在一個比 StreamingContext 的批次間隔更長的時間範圍內,通過整合多個批次的結果,計算出整個窗口的結果。

關於Window的操作有如下原語:

(1)window(windowLength, slideInterval): 基於對源DStream窗化的批次進行計算返回一個新的Dstream

(2)countByWindow(windowLength, slideInterval):返回一個滑動窗口計數流中的元素。

(3)reduceByWindow(func, windowLength, slideInterval):通過使用自定義函數整合滑動區間流元素來創建一個新的單元素流。

(4)reduceByKeyAndWindow(func, windowLength, slideInterval, [numTasks]):當在一個(K,V)對的DStream上調用此函數,會返回一個新(K,V)對的DStream,此處通過對滑動窗口中批次數據使用reduce函數來整合每個key的value值。Note:默認情況下,這個操作使用Spark的默認數量並行任務(本地是2),在集羣模式中依據配置屬性(spark.default.parallelism)來做grouping。你可以通過設置可選參數numTasks來設置不同數量的tasks。

(5)reduceByKeyAndWindow(func, invFunc, windowLength, slideInterval, [numTasks]):這個函數是上述函數的更高效版本,每個窗口的reduce值都是通過用前一個窗的reduce值來遞增計算。通過reduce進入到滑動窗口數據並”反向reduce”離開窗口的舊數據來實現這個操作。一個例子是隨着窗口滑動對keys的“加”“減”計數。通過前邊介紹可以想到,這個函數只適用於”可逆的reduce函數”,也就是這些reduce函數有相應的”反reduce”函數(以參數invFunc形式傳入)。如前述函數,reduce任務的數量通過可選參數來配置。注意:爲了使用這個操作,檢查點必須可用。 

(6)countByValueAndWindow(windowLength,slideInterval, [numTasks]):對(K,V)對的DStream調用,返回(K,Long)對的新DStream,其中每個key的值是其在滑動窗口中頻率。如上,可配置reduce任務數量。

reduceByWindow() 和 reduceByKeyAndWindow() 讓我們可以對每個窗口更高效地進行歸約操作。它們接收一個歸約函數,在整個窗口上執行,比如 +。除此以外,它們還有一種特殊形式,通過只考慮新進入窗口的數據和離開窗口的數據,讓 Spark 增量計算歸約結果。這種特殊形式需要提供歸約函數的一個逆函數,比 如 + 對應的逆函數爲 -。對於較大的窗口,提供逆函數可以大大提高執行效率

def main(args: Array[String]): Unit = {
    val sparkConf: SparkConf = new SparkConf().setAppName("23").setMaster("local[*]")
    val ssc = new StreamingContext(sparkConf,Seconds(3))

    val kafkaDStream: ReceiverInputDStream[(String, String)] = KafkaUtils.createStream(
      ssc,
      "hdp-1:2181", //zookeeper
      "dstream", //消費者組groupid
      Map("dstream" -> 3) //map中存放多個topic主題,格式爲:<topic,分區數>
    )
    //windows(windowLength, slideInterval) 
    // 窗口大小,滑動步長(都是週期整數倍) 即3個週期數據,一次移動一個週期
    val windowed: DStream[(String, String)] = kafkaDStream.window(Seconds(9),Seconds(3))
    windowed.flatMap(t=>(t._2.split(" "))).map((_,1)).reduceByKey(_+_).print()

    ssc.start()
    ssc.awaitTermination()
  }

kafka0.10版本util方式消費kafka,且windows方法會報序列化錯誤,需要優化將ConsumerRecord類註冊爲使用Kyro序列化

object windows {
  def main(args: Array[String]): Unit = {
    val sparkConf: SparkConf = new SparkConf().setAppName("23").setMaster("local[*]")
      .set("spark.serializer", "org.apache.spark.serializer.KryoSerializer") //將ConsumerRecord類註冊爲使用Kyro序列化
    val ssc = new StreamingContext(sparkConf,Seconds(3))

    val inputDstream: InputDStream[ConsumerRecord[String, String]] = MyKafkaUtil.getKafkaStream("window", ssc)

    //windows(windowLength, slideInterval)
    // 窗口大小,滑動步長(都是週期整數倍) 即3個週期數據,一次移動一個週期
    val windowed: DStream[ConsumerRecord[String, String]] = inputDstream.window(Seconds(9),Seconds(3))
    windowed.map(t=>t.value()).map((_,1)).reduceByKey(_+_).print()

//    inputDstream.map(t=>t.value()).map((_,1)).reduceByKey(_+_).print()

    ssc.start()
    ssc.awaitTermination()
  }
}
package com.bgd.realtime.util

import org.apache.kafka.common.serialization.StringDeserializer
import java.util.Properties

import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.spark.streaming.StreamingContext
import org.apache.spark.streaming.dstream.InputDStream
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, KafkaUtils, LocationStrategies}

object MyKafkaUtil {
  private val properties: Properties = PropertiesUtil.load("config.properties")
  val broker_list = properties.getProperty("kafka.broker.list")

  // kafka消費者配置
  val kafkaParam = Map(
    "bootstrap.servers" -> broker_list,//用於初始化鏈接到集羣的地址
    "key.deserializer" -> classOf[StringDeserializer],
    "value.deserializer" -> classOf[StringDeserializer],
    //用於標識這個消費者屬於哪個消費團體
    "group.id" -> "gmall_consumer_group",
    //如果沒有初始化偏移量或者當前的偏移量不存在任何服務器上,可以使用這個配置屬性
    //可以使用這個配置,latest自動重置偏移量爲最新的偏移量
    "auto.offset.reset" -> "latest",
    //如果是true,則這個消費者的偏移量會在後臺自動提交,但是kafka宕機容易丟失數據
    //如果是false,會需要手動維護kafka偏移量
    "enable.auto.commit" -> (true: java.lang.Boolean)
  )

  // 創建DStream,返回接收到的輸入數據
  // LocationStrategies:根據給定的主題和集羣地址創建consumer
  // LocationStrategies.PreferConsistent:持續的在所有Executor之間分配分區
  // ConsumerStrategies:選擇如何在Driver和Executor上創建和配置Kafka Consumer
  // ConsumerStrategies.Subscribe:訂閱一系列主題

  def getKafkaStream(topic: String,ssc:StreamingContext): InputDStream[ConsumerRecord[String,String]]={
    val dStream = KafkaUtils.createDirectStream[String,String](ssc, LocationStrategies.PreferConsistent,ConsumerStrategies.Subscribe[String,String](Array(topic),kafkaParam))
    dStream
  }
}

3、Join

連接操作(leftOuterJoin, rightOuterJoin, fullOuterJoin也可以),可以連接Stream-Stream,windows-stream to windows-stream、stream-dataset、Stream-Stream Joins

val stream1: DStream[String, String] = ...
val stream2: DStream[String, String] = ...
val joinedStream = stream1.join(stream2)

val windowedStream1 = stream1.window(Seconds(20))
val windowedStream2 = stream2.window(Minutes(1))
val joinedStream = windowedStream1.join(windowedStream2)
Stream-dataset joins
val dataset: RDD[String, String] = ...
val windowedStream = stream.window(Seconds(20))...
val joinedStream = windowedStream.transform { rdd => rdd.join(dataset) }

4、Transform

Transform原語允許DStream上執行任意的RDD-to-RDD函數。即使這些函數並沒有在DStream的API中暴露出來,通過該函數可以方便的擴展Spark API。該函數每一批次調度一次。其實也就是對DStream中的RDD應用轉換。

比如下面的例子,在進行單詞統計的時候,想要過濾掉spam的信息。

val spamInfoRDD = ssc.sparkContext.newAPIHadoopRDD(...) // RDD containing spam information

val cleanedDStream = wordCounts.transform { rdd =>
  rdd.join(spamInfoRDD).filter(...) // join data stream with spam information to do data cleaning
  ...
}

四、案例

1、WordCount案例實操(單次、累計)

  需求:使用netcat工具向9999端口不斷的發送數據,通過SparkStreaming讀取端口數據並統計不同單詞出現的次數

<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-streaming_2.11</artifactId>
    <version>2.1.1</version>
</dependency>

//單次統計:只處理實時發過來的數據,之前的數據不統計

import org.apache.spark.{HashPartitioner, SparkConf}
import org.apache.spark.streaming.{Seconds, StreamingContext}

object NetworkWordCount {
  def main(args: Array[String]) {
    val sparkConf = new SparkConf().setAppName("NetworkWordCount").setMaster("local[2]")
    val ssc = new StreamingContext(sparkConf, Seconds(10))   //每隔十秒刷新採集一次

    /*創建文本輸入流,並進行詞頻統計 reduceByKey 結果不累加*/
    val lines = ssc.socketTextStream("hdp-1", 9999)
    lines.flatMap(_.split(" ")).map(x => (x, 1)).reduceByKey(_ + _).print()

    /*啓動服務*/
    ssc.start()
    /*等待服務結束*/
    ssc.awaitTermination()
  }
}

啓動程序並通過NetCat發送數據:

[xin@hdp-1 spark]$ nc -lk 9999
hello xin

//累計統計   之前的單詞也統計

object NetworkUpdateStateWordCount {
  /**
   * String : 單詞 hello
   * Seq[Int] :單詞在當前批次出現的次數
   * Option[Int] : 歷史結果
   */
  //    val i : Int = 11;
  val updateFunc = (iter: Iterator[(String, Seq[Int], Option[Int])]) => {
    //iter.flatMap(it=>Some(it._2.sum + it._3.getOrElse(0)).map(x=>(it._1,x)))
    iter.flatMap{case(x,y,z)=>Some(y.sum + z.getOrElse(0)).map(m=>(x, m))}
  }

  def main(args: Array[String]) {
    //    LoggerLevel.setStreamingLogLevels()
    val conf = new SparkConf().setMaster("local[2]").setAppName("NetworkUpdateStateWordCount")
    val ssc = new StreamingContext(conf, Seconds(5))
    //做checkpoint 寫入共享存儲中
    ssc.checkpoint("E:/hadoop/hdpdata/checkpoint")
    val lines = ssc.socketTextStream("hdp-1", 9999)
    //val result = lines.flatMap(_.split(" ")).map((_, 1)).reduceByKey(_+_)
    //updateStateByKey結果可以累加但是需要傳入一個自定義的累加函數:updateFunc
    val results = lines.flatMap(_.split(" ")).map((_,1)).updateStateByKey(updateFunc, new HashPartitioner(ssc.sparkContext.defaultParallelism), true)
    results.print()
    ssc.start()
    ssc.awaitTermination()
  }
}

2、把sparkstreaming獲取到的數據存在redis裏

class AndRdis {
}
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.DStream
import org.apache.spark.streaming.{Seconds, StreamingContext}
import redis.clients.jedis.Jedis

object NetworkWordCountToRedis {
  def main(args: Array[String]) {
    val sparkConf = new SparkConf().setAppName("NetworkWordCountToRedis").setMaster("local[*]")
    val ssc = new StreamingContext(sparkConf, Seconds(5))

    /*創建文本輸入流,並進行詞頻統計*/
    val lines = ssc.socketTextStream("hdp-1", 9999)
    val pairs: DStream[(String, Int)] = lines.flatMap(_.split(" ")).map(x => (x, 1)).reduceByKey(_ + _)
    /*保存數據到Redis*/
    pairs.foreachRDD { rdd =>        //foreachRDD遍歷DStream裏面的RDD
      rdd.foreachPartition { partitionOfRecords =>    //遍歷每個分區
        var jedis: Jedis = null
        try {
          jedis = JedisPoolUtil.getConnection     //獲得jedis對象
          jedis.auth("123456")
          //hash <key,map>  map<String,String>    hincrBy(hashName,mapkey,mapvalue) Redis Hincrby 命令用於爲哈希表中的字段值加上指定增量值。
          partitionOfRecords.foreach(record => jedis.hincrBy("wordCount", record._1, record._2)) 
        } catch {
          case ex: Exception =>
            ex.printStackTrace()
        } finally {
          if (jedis != null) jedis.close()
        }
      }
    }
    ssc.start()
    ssc.awaitTermination()
  }
}
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class JedisPoolUtil {
    /* 聲明爲volatile防止指令重排序 */
    private static volatile JedisPool jedisPool = null;
    private static final String HOST = "hdp-1";
    private static final int PORT = 6379;

    /* 雙重檢查鎖實現懶漢式單例 */
    public static Jedis getConnection() {
        if (jedisPool == null) {
            synchronized (JedisPoolUtil.class) {
                if (jedisPool == null) {
                    JedisPoolConfig config = new JedisPoolConfig();
                    config.setMaxTotal(30);
                    config.setMaxIdle(10);
                    jedisPool = new JedisPool(config, HOST, PORT);
                }
            }
        }
        return jedisPool.getResource();
    }
}

 

發佈了78 篇原創文章 · 獲贊 20 · 訪問量 7789
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章