Spark结构化流

概述

Structured Stream是基于Spark SQL引擎构建的可伸缩且容错的流处理引擎。使得用户可以像使用Spark SQL操作静态批处理计算一样使用Structured Stream的SQL操作流计算。当流数据继续到达时,Spark SQL引擎将负责递增地,连续地运行它并更新最终结果。使用 Dataset/DataFrame API 实现对实时数据的聚合、event-time 窗口计算以及流到批处理的join操作。最后,系统通过检查点预写日志来确保端到端(end to end)的一次容错保证。简而言之,结构化流提供了快速,可伸缩,容错,端到端的精确一次流处理,而用户不必推理流。在内部,默认情况下,结构化流查询是使用微批量处理引擎处理的,该引擎将数据流作为一系列小批量作业进行处理,从而实现了低至100毫秒的端到端延迟以及一次精确的容错保证。

快速入门

  • 依赖
<dependency>
  <groupId>org.apache.spark</groupId>
  <artifactId>spark-sql_2.11</artifactId>
  <version>2.4.5</version>
</dependency>
  • 编写Driver

val spark = SparkSession
  .builder
  .appName("StructuredNetworkWordCount")
  .master("local[*]")
  .getOrCreate()

spark.sparkContext.setLogLevel("ERROR")
import spark.implicits._

//1.创建输入流(表) 动态表
val lines:DataFrame = spark.readStream
    .format("socket")
    .option("host", "CentOS")
    .option("port", 9999)
    .load()

//2.对动态表执行 SQL 查询
val wordCounts:DataFrame = lines.as[String].flatMap(_.split(" "))
      .groupBy("value").count()

//3.产生StreamQuery对象
val query:StreamingQuery = wordCounts.writeStream
        .outputMode(OutputMode.Update())
        .format("console")
        .start()
query.awaitTermination()

Programming Model

Structured Stream中的关键思想是将实时数据流视为被连续追加的表。这导致了一个新的流处理模型,该模型与批处理模型非常相似。使用者将像在静态表上一样将流计算表示为类似于批处理的标准查询,Spark在无界输入表上将其作为增量查询运行。

Basic Concepts

将输入数据流视为“Input Table”。流上到达的每个数据项都像是将新Row附加到输入表中。

输入表
输入查询将生成“Result Table”。在每个触发间隔(例如,每1秒钟),新行将附加到输入表中,并最终更新“Result Table”(输入查询是一种持续查询)。无论何时更新“Result Table”,我们都希望将更改后的结果行写入外部接收器。

在这里插入图片描述

“Output”定义为写到外部存储器的内容。可以在不同的模式下定义输出:

Complete Mode - 整个更新的Result Table将被写入外部存储器。由存储连接器决定如何处理整个表的写入。

Update Mode - 仅自上次触发以来在Result Table中已更新的行将被写入外部存储(自Spark 2.1.1起可用)。请注意,这与Complete Mode的不同之处在于此模式仅输出自上次触发以来已更改的行。

以上两种模式一般用于存在聚合(状态)算子的计算中。因为只有在Complete Mode或者Update Mode系统才会存储计算的中间结果。如果使用Update模式中不存在聚合操作,该模式等价于Append模式。

  • Append Mode - 自上次触发以来,仅将追加在Result Table中的新行写入到外部存储中。这仅适用于预期结果表中现有行不会更改的查询(不会使用历史数据,做状态更新)。

为了说明此模型的用法,让我们在上面的快速入门的上下文中了解该模型。第一行DataFrame是输入表,最后的wordCounts DataFrame是结果表。请注意,在流lines’DataFrame上生成wordCounts的查询与静态DataFrame完全相同。但是,启动此查询时,Spark将连续检查Socket连接中是否有新数据。如果有新数据,Spark将运行一个“增量”查询,该查询将先前的运行计数与新数据结合起来以计算更新的计数,如下所示。
在这里插入图片描述
请注意,Structured Stream不会存储Input Table。它从流数据源读取最新的可用数据,对其进行增量处理以更新结果,然后丢弃该源数据。它仅保留更新结果所需的最小中间状态数据(例如,前面示例中的中间计数)。

此模型与许多其他流处理引擎明显不同。许多流系统要求用户自己维护运行中的聚合,因此必须考虑容错和数据一致性(至少一次,最多一次或精确一次)。在此模型中,Spark负责在有新数据时更新结果表,从而使用户免于推理。系统通过检查点预写日志来确保端到端(end to end)的精准一次容错保证。

  • 最多一次:如果计算过程中,存在失败,自动忽略,一般只考虑性能,不考虑安全-(丢数据)
  • 至少一次:在故障时候,系统因为重试或者重新计算,导致记录重复参与计算-(不丢数据,重复更新,计算不准确)
  • 精确一次:在故障时候,系统因为重试或者重新计算,不会导致数据丢失或者重复计算-(对数据精准性要求比较高的实时计算场景)

容错语义

提供端到端的精确一次语义是结构化流设计背后的主要目标之一。为此,我们设计了结构化流源接收器执行引擎,以可靠地跟踪处理的确切进度,以便它可以通过重新启动和/或重新处理来处理任何类型的故障。假定每个流源都有偏移量(类似于Kafka偏移量或Kinesis序列号)以跟踪流中的读取位置。引擎使用检查点预写日志来记录每个触发器中正在处理的数据的偏移范围。流接收器被设计为是幂等的,用于后续处理。结合使用可重播的源幂等的接收器,结构化流可以确保在发生任何故障时端到端的一次精确语义。

处理事件时间和延迟数据

事件时间是嵌入数据本身的时间。对于许多应用程序,您可能希望在此事件时间进行操作。例如,如果要获取每分钟由IoT设备生成的事件数,则可能要使用生成数据的时间(即数据中的事件时间),而不是Spark收到的时间。事件时间在此模型中非常自然地表示-设备中的每个事件是表中的一行,而事件时间是该行中的列值。这样一来,基于窗口的聚合(例如,每分钟的事件数)就可以成为事件时间列上一种特殊的分组和聚合类型-每个时间窗口都是一个组,每行可以属于多个窗口/组。因此,可以在静态数据集(例如从收集的设备事件日志中)以及在数据流上一致地定义这样的基于事件-时间-窗口的聚合查询。

此外,此模型自然会根据事件时间处理比预期晚到达的数据。由于Spark正在更新结果表,因此它具有完全控制权,可以在有较晚数据时更新旧聚合,并可以清除旧聚合以限制中间状态数据的大小。从Spark 2.1开始,我们支持水印功能,该功能允许用户指定最新数据的阈值,并允许引擎相应地清除旧状态。这些将在后面的“窗口操作”部分中详细介绍。

Dataset和DataFrame API

从Spark 2.0开始,DataFrame和Dataset可以表示静态的有界数据以及流式无界数据。与静态Dataset/DataFrame类似,您可以使用公共入口点SparkSession从流源Dataset/DataFrame,并对它们应用与静态Dataset/DataFrame相同的操作。可以通过SparkSession.readStream()返回的DataStreamReader接口(Scala / Java / Python文档)创建流式DataFrame。

Input Sources

File source (支持故障容错)

读取写入目录的文件作为数据流。支持的文件格式为text,csv,json,orc,parquet。

name,age,salary,sex,job,deptNo
Michael,29,20000.0,true,MANAGER,1
Andy,30,15000.0,true,SALESMAN,1
Justin,19,8000.0,true,CLERK,1
//name,age,salary,sex,job,deptNo
val userSchema = new StructType()
  .add("name", StringType)
  .add("age", IntegerType)
  .add("salary", DoubleType)
  .add("sex", BooleanType)
  .add("job", StringType)
  .add("deptNo", IntegerType)

  //1.创建输入流(表)
  val lines:DataFrame = spark.readStream
    .schema(userSchema)
    .option("sep", ",")
    .option("header", "true")//去除表头
    .csv("hdfs://CentOS:9000/results/csv")
Socket Source(不支持故障容错)
//1.创建输入流(表)
val lines:DataFrame = spark.readStream
  .format("socket")
  .option("host", "CentOS")
  .option("port", 9999)
  .load()
Rate Source(支持故障容错)

以每秒指定的行数生成数据,每个输出行包含一个时间戳和一个值。其中timestamp是包含消息分发时间的Timestamp类型,而值是包含消息计数的Long类型,从第一行的0开始。此源旨在进行测试和基准测试。

val rate:DataFrame = spark.readStream
  .format("rate")
  .option("rowsPerSecond","1000")
  .load()
√Kafka Source(支持故障恢复)

参考:http://spark.apache.org/docs/latest/structured-streaming-kafka-integration.html

<dependency>
  <groupId>org.apache.spark</groupId>
  <artifactId>spark-sql-kafka-0-10_2.11</artifactId>
  <version>2.4.5</version>
</dependency>
val lines:DataFrame = spark.readStream
  .format("kafka")
  .option("kafka.bootstrap.servers", "CentOS:9092")
  .option("subscribe", "topic01")//option("subscribePattern", "topic.*")
  .load()
  .selectExpr("CAST(key AS STRING)","CAST(value AS STRING)","partition","offset")

Basic Operations

在这里插入图片描述

设备 设备类型 信号量 时间
BYQ-001,BY,1000,2020-03-02 14:56:00
BYQ-002,BY,1000,2020-03-02 14:56:00
case class DeviceData(device: String, deviceType: String, signal: Double, time: String)
val spark = SparkSession
  .builder
  .appName("StructuredNetworkWordCount")
  .master("local[*]")
  .getOrCreate()

  import spark.implicits._
  spark.sparkContext.setLogLevel("ERROR")



  //1.创建输入流(表)
  val inputs:DataFrame = spark.readStream
    .format("kafka")
    .option("kafka.bootstrap.servers", "CentOS:9092")
    .option("subscribePattern", "topic.*")
    .load()
    .selectExpr("CAST(value AS STRING)")
    .as[String].map(line => {
    var ts = line.split(",")
      DeviceData(ts(0), ts(1), ts(2).toDouble, ts(3))
  	}).toDF()
    .groupBy("deviceType", "device")
    .mean("signal")


    //3.产生StreamQuery对象
val query:StreamingQuery = inputs.writeStream
  .outputMode(OutputMode.Complete())
  .format("console")
  .start()
  query.awaitTermination()
val spark = SparkSession
  .builder
  .appName("StructuredNetworkWordCount")
  .master("local[*]")
  .getOrCreate()

  import spark.implicits._
  spark.sparkContext.setLogLevel("ERROR")

  //1.创建输入流(表)
  spark.readStream
  .format("kafka")
  .option("kafka.bootstrap.servers", "CentOS:9092")
  .option("subscribePattern", "topic.*")
  .load()
  .selectExpr("CAST(value AS STRING)")
  .as[String].map(line => {
  var ts = line.split(",")
    DeviceData(ts(0), ts(1), ts(2).toDouble, ts(3))
  }).toDF().createOrReplaceTempView("t_device")


  var sql=
  """
  select device,deviceType,avg(signal)
  from t_device
  group by deviceType,device
  """

  val results = spark.sql(sql)

  //3.产生StreamQuery对象
  val query:StreamingQuery = results.writeStream
    .outputMode(OutputMode.Complete())
    .format("console")
    .start()
    query.awaitTermination()
+-------+----------+-----------+
| device|deviceType|avg(signal)|
+-------+----------+-----------+
|BYQ-002|        BY|     4500.0|

Output Sinks

File sink (Append)

将输出存储到目录。

writeStream
  .outputMode(OutputMode.Append())
  .format("csv")
  .option("sep", ",")
  .option("header", "true")
  .option("inferSchema", "true")
  .option("path", "hdfs://CentOS:9000/structured/json")
  .option("checkpointLocation", "hdfs://CentOS:9000/structured-checkpoints")
  .start()

注意:File Sink只允许用在Append模式,并且支持精准一次的写入。

√Kafka sink(Append, Update, Complete)

将输出存储到Kafka中的一个或多个主题。在这里,我们描述了将流查询和批查询写入Apache Kafka的支持。请注意,Apache Kafka仅支持至少一次写入语义。因此,在向Kafka写入流查询或批处理查询时,某些记录可能会重复。例如,如果Kafka需要重试Broker未确认的消息(即使Broker已经收到并写入了消息记录),就会发生这种情况。由于这些Kafka写语义,结构化流无法阻止此类重复项的发生。写入Kafka的Dataframe在架构中应包含以下几列:

ColumnType
key (optional)string or binary
value (required)string or binary
topic (*optional)string

如果未指定“ topic”配置选项,则topic列为必填项。

<dependency>
  <groupId>org.apache.spark</groupId>
  <artifactId>spark-sql-kafka-0-10_2.11</artifactId>
  <version>2.4.5</version>
</dependency>
.writeStream
  .outputMode(OutputMode.Update())
  .format("kafka")
  .option("checkpointLocation", "hdfs://CentOS:9000/structured-checkpoints-kafka")
  .option("kafka.bootstrap.servers", "CentOS:9092")
  .option("topic", "topic01")//覆盖DF中topic字段
  .start()

必须保证写入的DF中仅仅含有value字符串类型的字段、key可选,如果没有系统认为是null。topic可选,前提是必须在Option中 配置topic,否则必须出现在字段列中。

完整案例

// 1,zhangsan,1,4.5
val inputs = spark.readStream
  .format("socket")
  .option("host", "CentOS")
  .option("port", "9999")
  .load()

  import org.apache.spark.sql.functions._
  val results = inputs.as[String].map(_.split(","))
  .map(ts => (ts(0).toInt, ts(1), ts(2).toInt * ts(3).toDouble))
  .toDF("id", "name", "cost")
  .groupBy("id", "name")
  .agg(sum("cost") as "cost" )
  .as[(Int,String,Double)]
  .map(t=>(t._1+":"+t._2,t._3+""))
  .toDF("key","value") //必须保证出现key,value字段

  //3.产生StreamQuery对象
val query:StreamingQuery = results.writeStream
    .outputMode(OutputMode.Update())
    .format("kafka")
    .option("checkpointLocation", "hdfs://CentOS:9000/structured-checkpoints-kafka")
    .option("kafka.bootstrap.servers", "CentOS:9092")
    .option("topic", "topic01")//覆盖DF中topic字段
    .start()

参考:http://spark.apache.org/docs/latest/structured-streaming-kafka-integration.html

Console sink (for debugging)

每次有触发器时,将输出打印到控制台/ stdout。支持追加和完整输出模式。由于每次触发后都会收集全部输出并将其存储在驱动程序的内存中,因此应在数据量较小时用于调试目的。

writeStream
  .outputMode(OutputMode.Update())
  .format("console")
  .option("numRows", "2")
  .option("truncate", "true")
  .start()
Memory sink (for debugging)

输出作为内存表存储在内存中。支持追加和完整输出模式。当整个输出被收集并存储在Driver程序的内存中时,应将其用于调试低数据量的目的。因此,请谨慎使用。

writeStream
  .outputMode(OutputMode.Complete())
  .format("memory")
  .queryName("t_word")
  .start()

new Thread(){
  override def run(): Unit = {
    while(true){
      Thread.sleep(1000)
        spark.sql("select * from t_word").show()
    }
  }
}.start()
Foreach|ForeachBatch sink

对输出中的记录运行任意输出。使用foreach和foreachBatch操作,您可以在流查询的输出上应用任意操作并编写逻辑。它们的用例略有不同-虽然foreach允许在每一行上使用自定义写逻辑,但是foreachBatch允许在每个微批处理的输出上进行任意操作和自定义逻辑。

ForeachBatch

foreachBatch(…)允许您指定在流查询的每个微批处理的输出数据上执行的函数。

writeStream
  .outputMode(OutputMode.Complete())
  .foreachBatch((ds,batchID)=>{
      ds.write //使用静态批处理的API
      .mode(SaveMode.Overwrite).format("json")
      .save("hdfs://CentOS:9000/results/structured-json")
  })
  .start()

Foreach

class WordCountWriter extends ForeachWriter[Row]{
  override def open(partitionId: Long, epochId: Long): Boolean = {
   // println("打开了链接")
    true //执行process
  }

  override def process(value: Row): Unit = {
    var Row(word,count)=value
    println(word,count)
  }

  override def close(errorOrNull: Throwable): Unit = {
   // println("释放资源")
  }
}
writeStream
      .outputMode(OutputMode.Complete())
      .foreach(new WordCountWriter)
      .start()

Window Operations

Structure Stream与离散流DStream最大的区别在于结构化流提供了基于Event Time语义的窗口处理。同时支持lateDate数据处理机制。

在这里插入图片描述

快速入门
object StructedStreamWordCountWindow {

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

    val spark = SparkSession
      .builder
      .appName("StructuredNetworkWordCount")
      .master("local[*]")
      .getOrCreate()

    import spark.implicits._
    spark.sparkContext.setLogLevel("ERROR")


    // 单词,时间戳
    val lines = spark.readStream
      .format("socket")
      .option("host", "CentOS")
      .option("port", 9999)
      .load()
    import org.apache.spark.sql.functions._

    var words=lines.as[String]
          .map(line=>line.split(","))
          .map(ts=>(ts(0),new Timestamp(ts(1).toLong)))
          .toDF("word","timestamp")

    var wordCounts=  words.groupBy(window($"timestamp", "2 seconds", "2 seconds"), $"word")
      .count()
     .map(row=>{

       //获取window对象
       var start=row.getAs[Row]("window").getAs[Timestamp]("start")
       var end=row.getAs[Row]("window").getAs[Timestamp]("end")
       //获取word字段
       var word=row.getAs[String]("word")
       //获取计数
       var count=row.getAs[Long]("count")

       val sdf = new SimpleDateFormat("HH:mm:ss")

       (sdf.format(start.getTime),sdf.format(end.getTime),word,count)
     })
     .toDF("start","end","word","count")

   //3.产生StreamQuery对象
    val query:StreamingQuery = wordCounts.writeStream
      .outputMode(OutputMode.Complete())
      .format("console")
      .start()
    query.awaitTermination()
  }
}
Late Data & Watermarking

在这里插入图片描述

To enable this, in Spark 2.1, we have introduced watermarking, which lets the engine automatically track the current event time in the data and attempt to clean up old state accordingly. You can define the watermark of a query by specifying the event time column and the threshold on how late the data is expected to be in terms of event time. For a specific window ending at time T, the engine will maintain state and allow late data to update the state until (max event time seen by the engine - late threshold > T).
在spark2.1版本引入watermarking概念,用于告知计算节点,何时丢弃窗口聚合状态。因为流计算是一个长时间运行任务,系统不可能无限制存储一些过旧的状态值。使用watermarking机制,系统可以删除那些过期的状态数据,用于释放内存。每个触发的窗口都有start timeend time属性,计算引擎会保留计算引擎所看到最大event time

watermark时间 = max event time seen by the engine  - late threshold

如果watermarker时间T‘>窗口的end time时间 T 则认为该窗口的计算状态可以丢弃

注意:引入watermarker以后,用户只能使用updateAppend模式,系统才会删除过期数据。

update-水位线没有没过窗口的end time之前,如果有数据落入到该窗口,该窗口会重复触发。

val spark = SparkSession
      .builder
      .appName("StructuredNetworkWordCount")
      .master("local[*]")
      .getOrCreate()

    import spark.implicits._
    spark.sparkContext.setLogLevel("ERROR")

    // 单词,时间戳
    val lines = spark.readStream
      .format("socket")
      .option("host", "CentOS")
      .option("port", 9999)
      .load()
    import org.apache.spark.sql.functions._

    var words=lines.as[String]
          .map(line=>line.split(","))
          .map(ts=>(ts(0),new Timestamp(ts(1).toLong)))
          .toDF("word","timestamp")

    var wordCounts=  words
        .withWatermark("timestamp","1 second")
      .groupBy(window($"timestamp", "2 seconds", "2 seconds"), $"word")
      .count()
     .map(row=>{

       //获取window对象
       var start=row.getAs[Row]("window").getAs[Timestamp]("start")
       var end=row.getAs[Row]("window").getAs[Timestamp]("end")
       //获取word字段
       var word=row.getAs[String]("word")
       //获取计数
       var count=row.getAs[Long]("count")

       val sdf = new SimpleDateFormat("HH:mm:ss")

       (sdf.format(start.getTime),sdf.format(end.getTime),word,count)
     })
     .toDF("start","end","word","count")

   //3.产生StreamQuery对象
    val query:StreamingQuery = wordCounts.writeStream
      .outputMode(OutputMode.Update())
      .format("console")
      .start()
    query.awaitTermination()

Append–水位线没有没过窗口的end time之前,如果有数据落入到该窗口,该窗口不会触发,只会默默的计算,只有当水位线没过窗口end time的时候,才会做出最终输出。

val spark = SparkSession
  .builder
  .appName("StructuredNetworkWordCount")
  .master("local[*]")
  .getOrCreate()

  import spark.implicits._
  spark.sparkContext.setLogLevel("ERROR")

  // 单词,时间戳
  val lines = spark.readStream
  .format("socket")
  .option("host", "CentOS")
  .option("port", 9999)
  .load()
  import org.apache.spark.sql.functions._

  var words=lines.as[String]
  .map(line=>line.split(","))
  .map(ts=>(ts(0),new Timestamp(ts(1).toLong)))
  .toDF("word","timestamp")

  var wordCounts=  words
  .withWatermark("timestamp","1 second")
  .groupBy(window($"timestamp", "2 seconds", "2 seconds"), $"word")
  .count()
  .map(row=>{

    //获取window对象
    var start=row.getAs[Row]("window").getAs[Timestamp]("start")
      var end=row.getAs[Row]("window").getAs[Timestamp]("end")
      //获取word字段
      var word=row.getAs[String]("word")
      //获取计数
      var count=row.getAs[Long]("count")

      val sdf = new SimpleDateFormat("HH:mm:ss")

      (sdf.format(start.getTime),sdf.format(end.getTime),word,count)
  })
  .toDF("start","end","word","count")

  //3.产生StreamQuery对象
  val query:StreamingQuery = wordCounts.writeStream
    .outputMode(OutputMode.Append())
    .format("console")
    .start()
    query.awaitTermination()
Semantic Guarantees of Aggregation with Watermarking
  • 水印延迟(使用withWatermark设置)为“ 2小时”可确保引擎永远不会丢弃任何少于2小时的数据。换句话说,任何在此之前处理的最新数据比事件时间少2小时(以事件时间计)的数据都可以保证得到汇总。
  • 但是,保证仅在一个方向上严格。延迟超过2小时的数据不能保证被删除;它可能会或可能不会聚合。数据延迟更多,引擎处理数据的可能性越小。

Join Operations

自Spark-2.0 Structured Streaming引入的Join的概念(inner和一些外连接)。支持和静态或者动态的Dataset/DataFrame做join操作。

Stream-static Joins
//0.创建spark对象
val spark = SparkSession
  .builder
  .appName("StructuredStreamAndStaticJoin")
  .master("local[*]")
  .getOrCreate()
  spark.sparkContext.setLogLevel("FATAL")
  import spark.implicits._

  val userDF = spark.sparkContext.parallelize(List((1, "zs"), (2, "lisi")))
  .toDF("id", "name")

  // 1 apple 1 4.5
  val lineDF=spark.readStream
  .format("socket")
  .option("host","CentOS")
  .option("port",9999)
  .load()

  val orderItemDF=lineDF.as[String]
  .map(t=>{
    val tokens = t.split("\\s+")
      (tokens(0).toInt,tokens(1),(tokens(2)toInt) * (tokens(3).toDouble))
  }).toDF("uid","item","cost")


  import org.apache.spark.sql.functions._

  var joinDF=orderItemDF.join(userDF,$"id"===$"uid")
  .groupBy("uid","name")
  .agg(sum("cost").as("total_cost"))


  val query = joinDF.writeStream.format("console")
  .outputMode(OutputMode.Update())
  .start()

  query.awaitTermination()

}

注意 stream-static joins 并不是状态的,所以不需要做状态管理,目前还有一些外连接还不支持,目前只支持innerleft outer

Stream-stream Joins

在spark-2.3添加了streaming-streaming的支持 ,实现两个流的join最大的挑战是在于找到一个时间点实现两个流的join,因为这两个流都没有结束。任意一个接受的流可以匹配另外一个流中即将被接受的数据。所以在任意一个流中我们需要接收并将这些数据进行缓存,然后作为当前stream的状态,然后去匹配另外一个的流的后续接收数据,继而生成相应的join的结果集。和Streaming的聚合很类似我们使用watermarker处理late,乱序的数据,限制状态的使用。
Inner Joins with optional Watermarking

内连接可以使用任意一些column作为连接条件,然而在stream计算开始运行的时候 ,流计算的状态会持续的增长,因为必须存储所有传递过来的状态数据,然后和后续的新接收的数据做匹配。为了避免无限制的状态存储。一般需要定义额外的join的条件。例如限制一些old数据如果和新数据时间间隔大于某个阈值就不能匹配。因此可以删除这些陈旧的状态。简单来说需要做以下步骤:

  • 两边流计算需要定义watermarker延迟,这样系统可以知道两个流的时间差值。
  • 定制一下event time的限制条件,这样引擎可以计算出哪些数据old的不再需要了。可以使用一下两种方式定制
    • 时间范围界定例如:JOIN ON leftTime BETWEEN rightTime AND rightTime + INTERVAL 1 HOUR
    • 基于Event-time Window 例如:JOIN ON leftTimeWindow = rightTimeWindow

案例1(Range案例)

//0.创建spark对象
val spark = SparkSession
  .builder
  .appName("StructuredNetworkWordCount")
  .master("local[*]")
  .getOrCreate()


  spark.sparkContext.setLogLevel("FATAL")
  import spark.implicits._

  //001 apple 1 4.5 1566113401000
  //2.创建一个DataFrame
  val orderDF = spark
  .readStream
  .format("socket")
  .option("host", "CentOS")
  .option("port", 9999)
  .load()
  .map(row=>row.getAs[String]("value").split("\\s+"))
  .map(t=>(t(0),t(1),(t(2).toInt * t(3).toDouble),new Timestamp(t(4).toLong)))
  .toDF("uid","item","cost","order_time")

  //001 zhangsan 1566113400000
  val userDF = spark
  .readStream
  .format("socket")
  .option("host", "CentOS")
  .option("port", 8888)
  .load()
  .map(row=>row.getAs[String]("value").split("\\s+"))
  .map(t=>(t(0),t(1),new Timestamp(t(2).toLong)))
  .toDF("id","name","login_time")

  import org.apache.spark.sql.functions.expr

  //系统分别会对 user 和 order 缓存 最近 1 秒 和 2秒 数据,一旦时间过去,系统就无法保证数据状态继续保留
  val loginWatermarker=userDF.withWatermark("login_time","1 second")

  val orderWatermarker=orderDF.withWatermark("order_time","2 seconds")

  //计算订单的时间 & 用户 登陆之后的0~1小时 关联 数据 并且进行join
  val joinDF = loginWatermarker.join(orderWatermarker,
                                     expr("uid=id and order_time >= login_time and  order_time <= login_time + interval 1 second"))

  val query=joinDF.writeStream
  .format("console")
  .outputMode(OutputMode.Append())
  .start()

  query.awaitTermination()

案例二(event time window)

//0.创建spark对象
    val spark = SparkSession
      .builder
      .appName("StructuredNetworkWordCount")
      .master("local[*]")
      .getOrCreate()


    spark.sparkContext.setLogLevel("FATAL")
    import spark.implicits._

    //001 apple 1 4.5 1566113401000
    //2.创建一个DataFrame
    val orderDF = spark
      .readStream
      .format("socket")
      .option("host", "CentOS")
      .option("port", 9999)
      .load()
      .map(row=>row.getAs[String]("value").split("\\s+"))
      .map(t=>(t(0),t(1),(t(2).toInt * t(3).toDouble),new Timestamp(t(4).toLong)))
      .toDF("uid","item","cost","order_time")
      .withColumn("leftWindow",window($"order_time","5 seconds"))


    import org.apache.spark.sql.functions._
    //001 zhangsan 1566113400000
    val userDF = spark
      .readStream
      .format("socket")
      .option("host", "CentOS")
      .option("port", 8888)
      .load()
      .map(row=>row.getAs[String]("value").split("\\s+"))
      .map(t=>(t(0),t(1),new Timestamp(t(2).toLong)))
      .toDF("id","name","login_time")
      .withColumn("rightWindow",window($"login_time","5 seconds"))

    import org.apache.spark.sql.functions.expr

    //系统分别会对 user 和 order 缓存 最近 1 秒 和 2秒 数据,一旦时间过去,系统就无法保证数据状态继续保留
    val loginWatermarker=userDF.withWatermark("login_time","1 second")

    val orderWatermarker=orderDF.withWatermark("order_time","2 seconds")

    //计算订单的时间 & 用户 登陆之后的0~1小时 关联 数据 并且进行join
    val joinDF = loginWatermarker.join(orderWatermarker,
       expr("uid=id and leftWindow = rightWindow"))

    val query=joinDF.writeStream
      .format("console")
      .outputMode(OutputMode.Append())//仅支持Append模式输出
      .start()

    query.awaitTermination()
 |-- window: struct (nullable = false)
 |    |-- start: timestamp (nullable = true)
 |    |-- end: timestamp (nullable = true)
 |-- word: string (nullable = true)
 |-- count: long (nullable = false)
  • 6
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小中.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值