Spark三之Spark Streaming

Spark Streaming

一 、Spark Streaming概述

Spark Streaming用于流式数据(实时数据)的处理。Spark Streaming支持的数据输入源很多,例如:Kafka、Flume、ZeroMQ和简单的TCP套接字等等。数据输入后可以用Spark的高度抽象原语如:map、reduce、window等进行运算。而结果也能保存在很多地方,如HDFS,数据库等。

流式数据(无限数据集):源源不断产生的数据

Kafka(为流式数据处理存储数据) —> spark streaming (原语)算子进行数据处理 --> HDFS/Hbase/MySQL

原语 约等 RDD中算子
在这里插入图片描述

批处理 VS 流处理区别

数据形式数据量计算延迟
批处理静态数据(有限数据集)数据量级大 - GB+延迟高 分钟|小时
流处理动态数据(无限数据集)数据量级小 - Byte+延迟低 ms | s

目前主流流处理框架:Kafka Streaming | Storm | Spark Streaming | Flink

Kafka Streaming : kafka框架自己做的流处理框架 (小众)

Storm : 相对早期

Spark Streaming : 底层基于RDD构建一个流式处理框架 spark 1.6

Structure Streaming : 底层基于Spark SQL构建一个流式处理框架 spark 2.0

Flink : 和spark没有关系,不过也是apache下的顶级项目,应用于流式处理(流式处理(实时性)比较优秀)

二、DStream入门

需求:使用netcat工具向9999端口不断的发送数据,通过SparkStreaming读取端口数据并统计不同单词出现的次数

需要安装nectcat插件:yum install -y nc
  1. 添加依赖
 <dependency>
     <groupId>org.apache.spark</groupId>
     <artifactId>spark-core_2.11</artifactId>
     <version>2.4.3</version>
</dependency>

<dependency>
     <groupId>org.apache.spark</groupId>
     <artifactId>spark-streaming_2.11</artifactId>
     <version>2.4.3</version>
</dependency>
  1. 编写代码
package day5
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}
object SparkStreamingNetcat {
  def main(args: Array[String]): Unit = {
    //1.初始化Spark配置信息,创建StreamingContext对象
    val con: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkStreamingNetcat")
    //初始化SparkStreamingContext
    val ssc = new StreamingContext(con, Seconds(5))
    //2.读取9999端口,获取socket网络数据
    val dstream: ReceiverInputDStream[String] = ssc.socketTextStream("spark56", 9999)
    //3.通过算子计算接收的数据
    val ds2: DStream[(String, Int)] = dstream.flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ + _)
      /*
       *//3.通过监控端口创建DStream,读进来的数据为一行行
       *val lineStreams = ssc.socketTextStream("spark56", 9999)
       *//将每一行数据做切分,形成一个个单词
       *val wordStreams = lineStreams.flatMap(_.split(" "))
       *//将单词映射成元组(word,1)
       *val wordAndOneStreams = wordStreams.map((_, 1))
	   *//将相同的单词次数做统计
       *val ds2 = wordAndOneStreams.reduceByKey(_+_)
      */
    //4.执行ds2的输出操作  
    ds2.print()
    //5.启动SparkStreamingContext
    ssc.start()
    ssc.awaitTermination()    //等待计算完成,不会停止,需要手动停止
  }
}
  1. 先启动监听再启动程序并通过NetCat发送数据
[root@spark0 spark-2.4.3-bin-hadoop2.7]# nc -lk 9999
hello hello hello world
//运行结果 
hello,3
world,1

三、WordCount程序解析

Discretized Stream(简称:DStream)是Spark Streaming的基础抽象,代表持续性的数据流和经过各种Spark原语操作后的结果数据流。在内部实现上,DStream是一系列连续的RDD来表示。每个RDD含有一段时间间隔内的数据,如下图:
在这里插入图片描述

对数据的操作也是按照RDD为单位来进行的
在这里插入图片描述

计算过程由Spark engine来完成
在这里插入图片描述

四、DStream创建

开发步骤

  • 构建StreamingContext(sparkconf,Seconds(n)).
  • 设置数据的Receiver(Basic|Advance)

    receiver  数据接收器决定了要处理数据的接收形式
    上面的代码对应:ssc.socketTextStream(....)
    
  • 使用DStream(Macro Batch RDD)转换算子和输出算子

  • 启动流计算ssc.start()

  • 等待系统关闭流计算ssc.awaitTermination()

​ Spark Streaming原生支持一些不同的数据源。一些“核心”数据源已经被打包到Spark Streaming 的 Maven 工件中,而其他的一些则可以通过 spark-streaming-kafka 等附加工件获取(加个依赖)。每个接收器都以 Spark 执行器程序中一个长期运行的任务的形式运行,因此会占据分配给应用的 CPU 核心。此外,我们还需要有可用的 CPU 核心来处理数据。这意味着如果要运行多个接收器,就必须至少有和接收器数目相同的核心数,还要加上用来完成计算所需要的核心数。例如,如果我们想要在流计算应用中运行 10 个接收器,那么至少需要为应用分配 11 个 CPU 核心。所以如果在本地模式运行,不能使用local[1]。

Spark Streaming编写程序,在本地程序不能写local的原因,至少模拟一个
receiver数据接收器:占用执行线程,一直运行
算子:线程处理接收的数据

1.文件数据源

​ 文件数据流:能够读取所有HDFS API兼容的文件系统文件,通过textFileStream方法进行读取,Spark Streaming 将会监控 dataDirectory 目录并不断处理移动进来的文件,记住目前不支持嵌套目录。

streamingContext.textFileStream(dataDirectory)

注意事项:

1)文件需要有相同的数据格式;

2)文件进入 dataDirectory的方式需要通过移动来实现;

3)一旦文件移动进目录,则不能再修改,即便修改了也不会读取新数据;

package day5
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}
object FileStreaming {
  def main(args: Array[String]): Unit = {
    //1.初始化Spark配置信息
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("FileStream")
    //2.初始化SparkStreamingContext
    val ssc = new StreamingContext(sparkConf, Seconds(5))
    //3.监控文件夹创建DStream
    val dirStream = ssc.textFileStream("file:///d:/test/fileStream")
    //4.将每一行数据做切分,形成一个个单词
    val wordStreams = 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()
  }
}

2.Kafka数据源

kafka分区一一对应着Spark Streaming的底层RDD的分区

需求:通过Spark Streaming从Kafka读取数据,并将读取过来的数据做简单计算(Word Count),最终打印到控制台

  1. 添加依赖
<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-streaming-kafka-0-10_2.11</artifactId>
    <version>2.4.3</version>
</dependency>

<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>0.11.0.0</version>
</dependency>
下述代码报错jar包冲突:
<dependency>
   <groupId>org.apache.kafka</groupId>
   <artifactId>kafka-clients</artifactId>
   <version>0.11.0.0</version>
   <exclusions>
     <exclusion>
     	<groupId>net.jpountz.lz4</groupId>
     	<artifactId>lz4</artifactId>
  	 </exclusion>
  </exclusions>
</dependency>
  1. 编写代码
package day5
import org.apache.kafka.clients.consumer.{ConsumerConfig, ConsumerRecord}
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{DStream, InputDStream}
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, KafkaUtils, LocationStrategies}
import org.apache.spark.streaming.{Seconds, StreamingContext}
object Test3_Kafka_Streaming {
  def main(args: Array[String]): Unit = {
    //初始化Spark配置信息
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Test3_Kafka_Streaming")
    //初始化SparkStreamingContext
    val ssc = new StreamingContext(sparkConf, Seconds(5))
    //创建一个map,设置Kafka消费者对应的参数,将kafka参数映射为map
    val map = Map[String,String](
      ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG->"kafka51:9092,kafka52:9092,kafka53:9092",
      ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG->"org.apache.kafka.common.serialization.StringDeserializer",
      ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG->"org.apache.kafka.common.serialization.StringDeserializer",
      ConsumerConfig.GROUP_ID_CONFIG->"g1"
    )
    //使用KafkaUtils工具类创建一个DStream,DStream里面存储的是从Kafka消费的消息对象
    var dStream: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream[String, String](
      // SparkStreamingContext实例
      ssc,    
      //Kafka分区数据存储策略   LocationStrategies.PreferBrokers   (executor和kafka broker在相同的node)
      //推荐使用,实际开发使用LocationStrategies.PreferConsistent   (kafka的数据分区)
      LocationStrategies.PreferConsistent,
      //订阅topic
      ConsumerStrategies.Subscribe[String,String](List("topica"), map)
    )
    //调用dStream1对象获取集合中的元素是ConsumerRecord对象,所以想要获取操作的值,需要.value()
    val dStream1: DStream[(String, Int)] = dStream.flatMap(v => v.value().split(" "))
      .map((_, 1)).reduceByKey(_ + _)
    dStream1.print()
    //启动SparkStreaming
    ssc.start()
    ssc.awaitTermination()
  }
}

五、DStream转换

​ DStream上的原语与RDD的类似,分为Transformations(转换)和Output Operations(输出)两种,此外转换操作中还有一些比较特殊的原语,如:updateStateByKey()、transform()以及各种Window相关的原语。

5.1 无状态转化操作

​ 无状态转化操作就是把简单的RDD转化操作应用到每个批次上,也就是转化DStream中的每一个RDD。部分无状态转化操作列在了下表中。注意,针对键值对的DStream转化操作(比如 reduceByKey())要添加**import StreamingContext._**才能在Scala中使用。
在这里插入图片描述

​ 需要记住的是,尽管这些函数看起来像作用在整个流上一样,但事实上每个DStream在内部是由许多RDD(批次)组成,且无状态转化操作是分别应用到每个RDD上的。例如,reduceByKey()会归约每个时间区间中的数据,但不会归约不同区间之间的数据。

例子:对用户输入的数字进行累加
dstream1.reduce(_+_)只会处理某一个时间段的数据StreamingContext(conf,Seconds(10))对这10秒内的值累加,reduce再对下十秒进行处理,只针对某一段时间内的进行操作,不会对两段时间进行操作。

举个例子,在之前的wordcount程序中,我们只会统计5秒内接收到的数据的单词个数,而不会累加。

5.2 有状态转化操作(重点)

1) UpdateStateByKey

UpdateStateByKey其实就是dStream中的一个算子(这个算子是rdd中没有的)

批次:什么算一个批次,创建StreamingContext对象时传递的Seconds(10)这个时间,那么这个时间范围内接受的数据,就是一个批次的数据。如果从启动到30秒,那么我们就可以说获取了3个批次的数据。

跨批次的需求场景:双11,实时的公布销售的金额,需要跨批次记录销售额。

​ UpdateStateByKey原语用于记录历史记录,有时,我们需要在 DStream 中跨批次维护状态(例如流计算中累加wordcount)。针对这种情况,updateStateByKey() 为我们提供了对一个状态变量的访问,用于键值对形式的 DStream。给定一个由(键,事件)对构成的 DStream,并传递一个指定如何根据新的事件 更新每个键对应状态的函数,它可以构建出一个新的 DStream,其内部数据为(键,状态) 对。

updateStateByKey() 的结果会是一个新的 DStream,其内部的 RDD 序列是由每个时间区间对应的(键,状态)对组成的。

updateStateByKey操作使得我们可以在用新信息进行更新时保持任意的状态。为使用这个功能,你需要做下面两步:

  • 定义状态,状态可以是一个任意的数据类型。

  • 定义状态更新函数,用此函数阐明如何使用之前的状态和来自输入流的新值对状态进行更新。

    使用updateStateByKey需要对检查点目录进行配置,会使用检查点来保存状态

    flume   传输的数据	事件
    //需求:计算新的输入数据的值 和 之前的数据进行累加,将结果更新到状态中
    (hello,Seq(1,1,1))
    (world,Seq(1,1))
    val dStream = XX.flatMap(_.split(" ")).map((_,1)).updateStateByKey((v1:Seq,v2:Option)=> {
          //updateStateByKey算子无法做类型自动推断,需要写上类型
          val current: Int = v1.foldLeft(0)(_+_)	//加强版,可以提供初始化值0
          val pervious = v2.getOrElse(0)
          Some(current+pervious)		//将新的结果更新到状态中
    })
    

使用UpdateStateByKey更新word count代码,实现数据累加统计

package day6
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}
object Test_UpdateStateByKey {
  def main(args: Array[String]): Unit = {
    //初始化Spark配置信息
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Test_UpdateStateByKey")
    //初始化StreamingContext
    val ssc = new StreamingContext(sparkConf, Seconds(10))
    //设置检查点目录(检查点目录建议是hdfs)
    ssc.checkpoint("file:///d:/checkpoing_streaming")
    val ds1: ReceiverInputDStream[String] = ssc.socketTextStream("spark56", 9999)
    //无状态转换,每次转换计算不会记录之前的数据
    //val ds2: DStream[(String, Int)] = ds1.flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ + _)
    //有状态转换,使用updateStateByKey来更新状态,统计从运行开始以来单词总的次数
    //hello hello world  -->
    /**
     *  (hello,1)
     *  (hello,1)
     *  (hello,1)
     */
    val ds3: DStream[(String, Int)] = ds1
                                      .flatMap(_.split(" "))
                                      .map((_, 1)).updateStateByKey((v1:Seq[Int],v2:Option[Int])=> {
      //updateStateByKey算子无法做类型自动推断,需要写上类型
      //v1:和当前处理这个批次要处理的数据有关
      //v1是一个集合   [1,1,1]
      val current: Int = v1.foldLeft(0)(_+_)    //v1.reduce(_+_)会报错,因为初始值为0,此处使用foldLeft()
      //v2:存储的之前批次计算的数据结果
      //第一次计算,不存在之前的数据计算结果,那么v2就是None
      //否则v2是Some存储之前的计算结果
      val pervious: Int = v2.getOrElse(0)
      Some(current+pervious)
    })
    //ds2.print()
    ds3.print()
    ssc.start()
    ssc.awaitTermination()
  }
}
//定义一个方法也可以
// 定义更新状态方法,参数values为当前批次单词频度,state为以往批次单词频度
val updateFunc = (values: Seq[Int], state: Option[Int]) => {
      val currentCount = values.foldLeft(0)(_ + _)
      val previousCount = state.getOrElse(0)
      Some(currentCount + previousCount)
}

//1.初始化Spark配置信息
val sparkConf = new SparkConf().setMaster("local[*]").setAppName("StateWordCount")
//2.初始化SparkStreamingContext
val ssc = new StreamingContext(sparkConf, Seconds(5))
ssc.checkpoint("file:///f:/streamCheck")
//3.通过监控端口创建DStream,读进来的数据为一行行
val lines  = ssc.socketTextStream("spark0", 9999)
//将每一行数据做切分,形成一个个单词
val words  = lines.flatMap(_.split(" "))
//将单词映射成元组(word,1)
val pairs  = words.map((_, 1))
// 使用updateStateByKey来更新状态,统计从运行开始以来单词总的次数
val stateDstream = pairs.updateStateByKey[Int](updateFunc)
stateDstream.print()
//启动SparkStreamingContext
ssc.start()
ssc.awaitTermination()

在这里插入图片描述

2) Window Operations

前言:通过scala中的sliding体会窗口和滑动的概念

//sliding(size: Int)      			size代表窗口显示的元素个数
//sliding(size: Int, step: Int)     step代表窗口滑动步长
val iter: Iterator[List[Int]] = List(1,2,3,4,5).sliding(3)	//step默认值为1可以省略
for (elem <- iter) {
   println(elem)
}
//运行结果
List(1, 2, 3)
List(2, 3, 4)
List(3, 4, 5)

在这里插入图片描述

​ Window Operations可以设置窗口的大小和滑动窗口的间隔来动态的获取当前Streaming的允许状态。基于窗口的操作会在一个比 StreamingContext 的批次间隔更长的时间范围内,通过整合多个批次的结果,计算出整个窗口的结果。
在这里插入图片描述

//5秒一个批次,窗口时长10秒,步长5秒。
package day6
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
object Test_Window {
  def main(args: Array[String]): Unit = {
    //1.创建StreamingContext对象
    val con: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Test_Window")
    val ssc = new StreamingContext(con, Seconds(5))
    //2.读取9999端口,获取socket网络数据
    val dstream: ReceiverInputDStream[String] = ssc.socketTextStream("spark56", 9999)
    //3.通过算子计算接收的数据
    val ds2: DStream[(String, Int)] = dstream.flatMap(_.split(" "))
                                             .map((_, 1))
                                             .window(Seconds(15),Seconds(10))   //要写在聚合函数前面
                                             .reduceByKey(_ + _)
    //4.执行ds2的输出操作
    ds2.print()
    //5.启动stream
    ssc.start()
    ssc.awaitTermination()    //等待计算完成,不会停止,需要手动停止
  }
}

在这里插入图片描述

注意:所有基于窗口的操作都需要两个参数,分别为窗口时长以及滑动步长,两者都必须是 StreamContext 的批次间隔的整数倍。

​ 窗口时长控制每次计算最近的多少个批次的数据,其实就是最近的 windowDuration/batchInterval 个批次。如果有一个以 10 秒为批次间隔的源 DStream,要创建一个最近 30 秒的时间窗口(即最近 3 个批次),就应当把 windowDuration 设为 30 秒。而滑动步长的默认值与批次间隔相等,用来控制对新的 DStream 进行计算的间隔。如果源 DStream 批次间隔为 10 秒,并且我们只希望每两个批次计算一次窗口结果, 就应该把滑动步长设置为 20 秒。
在这里插入图片描述

WordCount第三版:5秒一个批次,窗口时长10秒,步长5秒。

//1.初始化Spark配置信息
val sparkConf = new SparkConf().setMaster("local[*]").setAppName("WindowWordCount")
//2.初始化SparkStreamingContext
val ssc = new StreamingContext(sparkConf, Seconds(5))
//3.通过监控端口创建DStream,读进来的数据为一行行
val lines  = ssc.socketTextStream("spark0", 9999)
//4.5秒一个批次,窗口10秒,滑步5秒。
val value: DStream[String] = lines.window(Seconds(10),Seconds(5))
//5.将每一行数据做切分,形成一个个单词
val words  = value.flatMap(_.split(" "))
//6.将单词映射成元组(word,1)
val pairs  = words.map((_, 1))
val wordCounts =pairs.reduceByKey(_+_)
wordCounts.print()
//启动SparkStreamingContext
ssc.start()
ssc.awaitTermination()

关于Window的操作有如下原语:

(1)window(windowLength, slideInterval): 基于对源DStream窗口的批次进行计算返回一个新的Dstream

(2)countByWindow(windowLength, slideInterval):返回一个滑动窗口计数流中的元素。

(3)reduceByWindow(func, windowLength, slideInterval):通过使用自定义函数整合滑动区间流元素来创建一个新的单元素流。等同于.window(…).reduce(…)

(4)reduceByKeyAndWindow(func, windowLength, slideInterval, [numTasks]):当在一个(K,V)对的DStream上调用此函数,会返回一个新(K,V)对的DStream,此处通过对滑动窗口中批次数据使用reduce函数来整合每个key的value值。

val dstream2: DStream[(String, Int)] =dstream.flatMap(_.split(" "))
                                             .map((_,1))
                                             .reduceByKeyAndWindow((v1:Int,v2:Int)=>v1+v2,		//当前窗口数据累加
                                             Seconds(15),Seconds(10))

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任务的数量通过可选参数来配置。

注意:为了使用这个操作,检查点必须可用。

reduceByWindow(func, windowLength, slideInterval)
reduceByKeyAndWindow(func, invFunc, windowLength, slideInterval, [numTasks])
//这个函数看起来和上边的差了一个参数,其实实现细节差距非常大。
//通过算子计算接收的数据
val dstream2 = dstream.flatMap(_.split(" "))
                      .map((_,1)).reduceByKeyAndWindow(
                               (v1:Int,v2:Int)=>v1+v2,  	//将新进入的数据累加到之前窗口上
                               (v1:Int,v2:Int)=>v1-v2,  	//去除离开窗口的数据
                               Seconds(15),
                               Seconds(10),
                               filterFunc= v1=> v1._2 != 0)
适用场景:
	1.窗口时长大,滑动步长小,导致窗口之间重叠部分大
	2.reduce操作时可逆,否则也没有办法进行可逆操作
	例如::可加的数据可以减,多乘的数据不一定可以相除逆回去。

两个小错误:

​ 一、chevkpoint设置检查点

​ 二、参数少写一个的时候,写上参数名字覆盖不写的参数

(6)countByValueAndWindow(windowLength,slideInterval, [numTasks]):对(K,V)对的DStream调用,返回(K,Long)对的新DStream,其中每个key的值是其在滑动窗口中频率。如上,可配置reduce任务数量。

上述有些算子会报错:The CheckPoint directory has not set

解决办法:ssc.checkpoint(“file:///d:/checkpoint_streaming”)
在这里插入图片描述

reduceByWindow() 和 reduceByKeyAndWindow() 让我们可以对每个窗口更高效地进行归约操作。它们接收一个归约函数,在整个窗口上执行,比如 +。除此以外,它们还有一种特殊形式,通过只考虑新进入窗口的数据和离开窗口的数据,让 Spark 增量计算归约结果。这种特殊形式需要提供归约函数的一个逆函数,比 如 + 对应的逆函数为 -。对于较大的窗口,提供逆函数可以大大提高执行效率
在这里插入图片描述

val ipDStream = accessLogsDStream.map(logEntry => (logEntry.getIpAddress(), 1))
val ipCountDStream = ipDStream.reduceByKeyAndWindow(
  {(x, y) => x + y},            // 加上新进入窗口的批次中的元素 
  {(x, y) => x - y},            // 移除离开窗口的老批次中的元素
  Seconds(30),                  // 窗口时长
  Seconds(10))                  // 滑动步长
3) Transform操作

可以说是把DStream转换为RDD对象。

dstream对象中有一个算子叫做transform可以将DStream转换为RDD,因为DStream底层本身就是rdd。

什么时候需要transform操作?

dstream无法完成某个操作,但是它封装的rdd对象可以。

​ Transform操作允许任意RDD-to-RDD类型的函数被应用在一个DStream上。通过它可以在DStream上使用任何没有在DStream API中暴露的任意RDD操作。

​ 比如说,DStream API中,并没有提供将一个DStream中的每个batch,与一个特定的RDD进行join的操作。但是我们自己就可以使用transform操作来实现该功能。

//1.初始化Spark配置信息
val sparkConf = new SparkConf().setMaster("local[*]").setAppName("TransformWordCount")
//2.初始化SparkStreamingContext
val ssc = new StreamingContext(sparkConf, Seconds(5))
//模拟黑名单(rdd对象)
val blackList: RDD[(String, Boolean)] = ssc.sparkContext.makeRDD(List(("zhangsan",true),("lisi",true)))
//3.通过监控端口创建DStream,读进来的数据为一行行
val dStream: ReceiverInputDStream[String] = ssc.socketTextStream("spark56", 9999)

//左连接(zhangsan,zhangsan#20) 连接	(zhangsan,true)		-->(zhangsan,(zhangsan#20,true)
//模拟用户日志 zhangsan#20    ————>(zhangsan,zhangsan#20)
dStream.map(v=>(v.split("#")(0),v)).transform(rdd=>{
	val rdd2: RDD[(String, (String, Option[Boolean]))] = rdd.leftOuterJoin(blackList)
	rdd2.filter(v=>{
			v._2._2.getOrElse(false) == false
		}).map(x => (x._1,x._2._1))
	}).print()

//启动SparkStreamingContext
ssc.start()
ssc.awaitTermination()

六、DStream输出

​ 输出操作指定了对流数据经转化操作得到的数据所要执行的操作(例如把结果推入外部数据库或输出到屏幕上)。与RDD中的惰性求值类似,如果一个DStream及其派生出的DStream都没有被执行输出操作,那么这些DStream就都不会被求值。如果StreamingContext中没有设定输出操作,整个context就都不会启动。

输出操作如下:

(1)print():在运行流程序的驱动结点上打印DStream中每一批次数据的最开始10个元素。这用于开发和调试。

(2)saveAsTextFiles(prefix, [suffix]):以text文件形式存储这个DStream的内容。每一批次的存储文件名基于参数中的prefix和suffix。”prefix-Time_IN_MS[.suffix]”.

例如:dstream2.saveAsTextFiles("file:///f:/result/a")
生成的目录为 a-时间戳

(3)saveAsObjectFiles(prefix,[suffix]):以Java对象序列化的方式将Stream中的数据保存为 SequenceFiles . 每一批次的存储文件名基于参数中的为"prefix-TIME_IN_MS[.suffix]".

(4)saveAsHadoopFiles(prefix,[suffix]):将Stream中的数据保存为 Hadoop files. 每一批次的存储文件名基于参数中的为"prefix-TIME_IN_MS[.suffix]"。

(5)foreachRDD(func):这是最通用的输出操作,即将函数 func 用于产生于 stream的每一个RDD。其中参数传入的函数func应该实现将每一个RDD中数据推送到外部系统,如将RDD存入文件或者通过网络将其写入数据库。注意:函数func在运行流应用的驱动中被执行,同时其中一般函数RDD操作从而强制其对于流RDD的运算。

​ 通用的输出操作foreachRDD(),它用来对DStream中的RDD运行任意计算。这和transform() 有些类似,都可以让我们访问任意RDD。在foreachRDD()中,可以重用我们在Spark中实现的所有行动操作。

比如,常见的用例之一是把数据写到诸如MySQL的外部数据库中。 注意:

(1)连接不能写在driver层面;

(2)如果写在foreach则每个RDD都创建,得不偿失;

(3)增加foreachPartition,在分区创建。

create table t_words(
	id int primary key auto_increment,
    name varchar(50),
    c int,
    time TIMESTAMP
)
package day6
import java.sql.{DriverManager, Timestamp}
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}

object Test_foreachRDD {
  def main(args: Array[String]): Unit = {
    //1.创建StreamingContext对象
    val conf = new SparkConf().setMaster("local[2]").setAppName("Test_foreachRDD")
    val ssc = new StreamingContext(conf,Seconds(10))
    //2.读取9999端口,获取socket网络数据
    val dstream: ReceiverInputDStream[String] = ssc.socketTextStream("spark56",9999)
    //3.通过算子计算接收的数据
    val dstream2: DStream[(String, Int)] =
      dstream.flatMap(_.split(" "))
        .map((_,1)).reduceByKey(_+_)
    //4.执行dstream2的输出操作,将计算结果保存在数据库中
    dstream2.foreachRDD((rdd,time)=>{
        //rdd就是dstream对象底层封装的rdd,time就是当前时间戳
        rdd.foreachPartition(iter=>{
            val conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test1","root","123456")
            iter.foreach( v =>{   // v是一个元组  v._1是单词  v._2是出现次数
                val ps = conn.prepareStatement("insert into t_words values(null,?,?,?)")
                ps.setString(1,v._1)
                ps.setInt(2,v._2)
                ps.setTimestamp(3,new Timestamp(time.milliseconds))
                ps.executeUpdate()
            })
            conn.close()
        })
    })
    //5.启动
    ssc.start()
    ssc.awaitTermination()    //等待计算完成
  }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值