Structured Streaming 编程指南

简介

Structured Streaming 是一个构建在SparkSQL可扩展和容错的流处理引擎.用户可以使用计算静态批处理的SQL表达式实现Streaming计算。内部使用Spark SQL实现对数据流的持续计算和持续输出。用户还可以使用Dataset/DataFrame API实现对流数据的聚合、时间窗口、以及stream-to-batch的处理等,同时Struced Streaming系统借助于checkpoint和 Write Ahead Logs机制可以保证end-to-end exactly-once fault-tolerance保障而不需要对流处理有太多的了解。

默认,底层Structured Streaming使用了micro-batch计算引擎,这种处理方式将数据看成是一些列的small batch jobs 继而实现了100ms 的end-to-end延迟以及exactly-once fault-tolerance 保证。自动Spark-2.3引入了一个新的ow-latency 处理模型Continuous Processing,该模型可以实现低至1 millisecond的end-to-end 的at-least-once 保证机制。 用户可以根据自己的延迟需求自行改变计算模型。

快速入门

以下我们快速的运行一个word count案例,用于接收来自socket的字符并且统计输出最终获得额结果。

  • 导入Maven依赖
<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-sql_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>
  • 编写如下代码
//0.创建spark对象
val spark = SparkSession
  .builder
  .appName("StructuredNetworkWordCount")
  .master("local[*]")
  .getOrCreate()

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

//2.创建一个DataFrame
val lines = spark.readStream
  .format("socket")
  .option("host", "localhost")
  .option("port", 9999)
  .load()

//3.转换为Dataset
var wordCounts=lines.as[String]
  .flatMap(_.split(" "))
  .groupBy("value")
  .count() //计数


//4.将Dataset数据写入到控制台
val query = wordCounts.writeStream
  .outputMode("complete")
  .format("console")
  .start()

query.awaitTermination()

Programming Model

Structured Streaming核心思想是将实时数据流看成是一个持续追加的table,这就引入了一个非常类似批处理的流处理模型。因此用户可以使用类似批处理的模型的SQL表达式计算这种流数据。可以将输入的数据看成是Input Table,后续到来的数据可以看做是新到来的Row,这些Row会被追加到这张表中。
在这里插入图片描述
查询输入数据会产生Result Table.每次数据到来的时候,数据会被插入到Input Table中,这将最终更新到Result Table,当Result Table被更新后最后的改变的状态会被更新到外围磁盘中。

在这里插入图片描述
其中“Output”定义了用户将什么数据写出到外围存储系统。其中“Output”定义了有以下输出模式

  • Complete Mode - 整个更新后的Resut Table数据将会被写到外围存储系统。
  • Append Mode - 仅仅将新插入Result Table的记录写出到外围系统。这种模式只适用于Result Table中的记录只读情况下才可以使用。
  • Update Mode - 只用ResultTable中的被更新的Rows会被写出到外围系统(自从spark-2.1.1)。如果查询中不含有聚合方法改模式等价于Append Mode

注意:每种不同的模式只适用于不同的查询类型,这些问题会在后续章节讨论。

Dataset和DataFrame API

从Spark 2.0开始,DataFrame和Dataset可以表示静态有界数据以及流式计算的无界数据。同有界数据类似,用户可以使用SparkSession从流源创建DataFrame/Dataset.并且可以使用类似静态Dataframe/Dataset相同的操作。

Input Source

File source

读取来自文件目录下的文件,例如:文本格式、csv格式、json、orc、parquet等文本格式。

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

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

//2.创建一个DataFrame
val lines = spark.readStream
  .format("socket")
  .option("host", "localhost")
  .option("port", 9999)
  .load()

//3.对无类型数据做转换 
import org.apache.spark.sql.functions._
var wordCounts= lines.flatMap(t=>t.getAs[String]("value").split(" "))
  .map(w=>(w,1))
  .withColumnRenamed("_1","key")
  .withColumnRenamed("_2","value")
  .groupBy("key")
  .agg(sum("value").alias("count"))

//4.将Dataset数据写入到控制台
val query = wordCounts.writeStream
  .outputMode("update")
  .format("console")
  .start()

query.awaitTermination()
Kafka source

读取来自Kafka的数据。需要配合使用0.10.0+版本

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

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

//2.创建一个DataFrame
val df = spark
    .readStream
    .format("kafka")
    .option("kafka.bootstrap.servers", "localhost:9092")
    .option("subscribe", "topic01")
    .load()

df.printSchema()

import org.apache.spark.sql.expressions.scalalang.typed

//3.对df做结果转换 使用 strong-typed 形式
val wordCount = df.selectExpr("CAST(key AS STRING)",
                              "CAST(value AS STRING)",
                              "partition",
                              "offset",
                              "CAST(timestamp AS LONG)")

    .flatMap(row=>row.getAs[String]("value").split("\\s+"))
    .map((_,1))
    .groupByKey(t=>t._1)
    .agg(typed.sum[(String,Int)](_._2).name("count"))

//4.将结果写出外围系统
var query=wordCount.writeStream
		.outputMode(OutputMode.Update())
    .format("console")
    .start()

//5.启动执行
query.awaitTermination()
Socket source(测试)

以UTF-8格式读取socket连接中的数据。

Output Sinks

File sink(Append Mode Only)
//0.创建spark对象
val spark = SparkSession
    .builder
    .appName("StructuredNetworkWordCount")
    .master("local[*]")
    .config("spark.sql.streaming.checkpointLocation","file:///Users/jiangzz/Desktop/checkpoint")
    .getOrCreate()


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

//2.创建一个DataFrame
val df = spark
    .readStream
    .format("kafka")
    .option("kafka.bootstrap.servers", "localhost:9092")
    .option("subscribe", "topic01")
    .load()

df.printSchema()


//3.对df做结果转换 使用 strong-typed 形式
val wordCount = df.selectExpr("CAST(key AS STRING)",
                              "CAST(value AS STRING)",
                              "partition",
                              "offset",
                              "CAST(timestamp AS LONG)")

.flatMap(row=>row.getAs[String]("value").split("\\s+"))
    .map((_,1))

//4.将结果写出外围系统
var query=wordCount.writeStream
    .outputMode(OutputMode.Append())
    .format("json")//支持parquet、json、csv、orc等常见格式
    .option("path", "file:///Users/jiangzz/Desktop/streaming/json/")
    .start()

//5.启动执行
query.awaitTermination()

由于只能做Append的模式,所以在实际的实战中这种模式只能用作数据清洗。由于Spark为了保证kafka精准一次语义的读取和处理,所以必须设置spark.sql.streaming.checkpointLocation

Kafka sink(Append|Update|Complete)
//0.创建spark对象
val spark = SparkSession
    .builder
    .appName("StructuredNetworkWordCount")
    .master("local[*]")
    .config("spark.sql.streaming.checkpointLocation","file:///Users/jiangzz/Desktop/checkpoint")
    .getOrCreate()


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

//2.创建一个DataFrame
val df = spark
    .readStream
    .format("kafka")
    .option("kafka.bootstrap.servers", "localhost:9092")
    .option("subscribe", "topic01")
    .load()

df.printSchema()

import org.apache.spark.sql.expressions.scalalang.typed

//3.对df做结果转换 使用 strong-typed 形式
val wordCount = df.selectExpr("CAST(key AS STRING)",
              "CAST(value AS STRING)",
              "partition",
              "offset",
              "CAST(timestamp AS LONG)")

    .flatMap(row=>row.getAs[String]("value").split("\\s+"))
    .map((_,1))
    .groupByKey(t=>t._1)
    .agg(typed.sum[(String,Int)](_._2).name("count"))
    .map(t=>(t._1,t._2.toString))
    .toDF("key","value")



//4.将结果写出外围系统
var query=wordCount.writeStream
    .outputMode(OutputMode.Update())
    .format("kafka")
    .option("kafka.bootstrap.servers", "localhost:9092")
    .option("topic", "t_topic02")
    .start()

//5.启动执行
query.awaitTermination()
Foreach sink(Append|Update|Complate)

概算子用户需要定制ForeachWriter[T]将结果写到外围系统,用户接受的的是流中的元素。

  • 定制输出
class CustomRowWriter extends ForeachWriter[Row]{
  var epochId:Long=_
  override def open(partitionId: Long, epochId: Long): Boolean = {
    //println("open:"+partitionId+" epochId"+epochId)
    this.epochId=epochId
    true
  }

  override def process(value: Row): Unit = {
    println("process:"+value+" => "+epochId)
  }

  override def close(errorOrNull: Throwable): Unit = {
    //println("close")
  }
}

这里的 open方法在,每一次微批的时候触发,其中epochId表示计算的批次。一般如果要保证exactly-once语义的处理

//0.创建spark对象
val spark = SparkSession
    .builder
    .appName("StructuredNetworkWordCount")
    .master("local[*]")
    .config("spark.sql.streaming.checkpointLocation","file:///Users/jiangzz/Desktop/checkpoint")
    .getOrCreate()


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

//2.创建一个DataFrame
val df = spark
    .readStream
    .format("kafka")
    .option("kafka.bootstrap.servers", "localhost:9092")
    .option("subscribe", "topic01")
    .load()

df.printSchema()

import org.apache.spark.sql.expressions.scalalang.typed

//3.对df做结果转换 使用 strong-typed 形式
val wordCount = df.selectExpr("CAST(key AS STRING)",
                              "CAST(value AS STRING)",
                              "partition",
                              "offset",
                              "CAST(timestamp AS LONG)")

.flatMap(row=>row.getAs[String]("value").split("\\s+"))
    .map((_,1))
    .groupByKey(t=>t._1)
    .agg(typed.sum[(String,Int)](_._2).name("count"))
    .map(t=>(t._1,t._2.toString))
    .toDF("key","value")

//4.将结果写出外围系统
var query=wordCount.writeStream
    .outputMode(OutputMode.Update())
    .foreach(new CustomRowWriter())
    .start()

//5.启动执行
query.awaitTermination()
Console sink (for debugging)

将结果写到控制台,一般用于数据的测试。

//0.创建spark对象
val spark = SparkSession
    .builder
    .appName("StructuredNetworkWordCount")
    .master("local[*]")
    .config("spark.sql.streaming.checkpointLocation","file:///Users/jiangzz/Desktop/checkpoint")
    .getOrCreate()


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

//2.创建一个DataFrame
val df = spark
    .readStream
    .format("kafka")
    .option("kafka.bootstrap.servers", "localhost:9092")
    .option("subscribe", "topic01")
    .load()

df.printSchema()

import org.apache.spark.sql.expressions.scalalang.typed

//3.对df做结果转换 使用 strong-typed 形式
val wordCount = df.selectExpr("CAST(key AS STRING)",
                              "CAST(value AS STRING)",
                              "partition",
                              "offset",
                              "CAST(timestamp AS LONG)")

.flatMap(row=>row.getAs[String]("value").split("\\s+"))
    .map((_,1))
    .groupByKey(t=>t._1)
    .agg(typed.sum[(String,Int)](_._2).name("count"))
    .map(t=>(t._1,t._2.toString))
    .toDF("key","value")



//4.将结果写出外围系统
var query=wordCount.writeStream
    .outputMode(OutputMode.Update())
    .format("console")
    .start()

//5.启动执行
query.awaitTermination()

Window on Event Time

Structured Streaming使用聚合函数基于EventTime计算window是非常简单的类似于分组聚合。分组聚合是按照指定的column字段对表中的数据进行分组,然后使用聚合函数对用户指定的colun字段进行聚合。于分组不同的是窗口的聚合是基于时间将落入同一个时间区间的数据进行聚合。

为了使得大家清楚的明白,什么是基于时间窗口的聚合,请看下面一张图。下面一张图描绘的是计算10分钟内的单词统计,每间隔5分钟滑动一个时间窗口。

在这里插入图片描述
案例如下:

//0.创建spark对象
val spark = SparkSession
    .builder
    .appName("StructuredNetworkWordCount")
    .master("local[*]")
    .config("spark.sql.streaming.checkpointLocation","file:///Users/jiangzz/Desktop/checkpoint")
    .getOrCreate()

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

//2.创建一个DataFrame
val df = spark
    .readStream
    .format("kafka")
    .option("kafka.bootstrap.servers", "localhost:9092")
    .option("subscribe", "topic01")
    .load()

df.printSchema()

import org.apache.spark.sql.functions._
var sdf=new SimpleDateFormat("HH:mm:ss")

//3.对df做结果转换 使用 strong-typed 形式
val wordCount = df.selectExpr("CAST(key AS STRING)",
                              "CAST(value AS STRING)",
                              "partition",
                              "offset",
                              "timestamp")

.select("value","timestamp")
    .flatMap(row=> row.getAs[String]("value").split("\\s").map(w=>(w,row.getAs[Timestamp]("timestamp"))))
    .toDF("word","timestamp")
    .groupBy(window($"timestamp","20 seconds","10 seconds"),$"word")
    .count()
    .map(r=> (sdf.format(r.getStruct(0).getTimestamp(0)),sdf.format(r.getStruct(0).getTimestamp(1)),r.getString(1),r.getLong(2)))
    .toDF("start","end","key","value")

//4.将结果写出外围系统
var query=wordCount.writeStream
.outputMode(OutputMode.Update())
.format("console")
.start()

//5.启动执行
query.awaitTermination()

late处理和watermarker

默认情况下,Spark会把落入到时间窗口的数据进行聚合操作。但是需要思考的是Event-Time是基于事件的时间戳进行窗口聚合的。那就有可能事件窗口已经触发很久了,但是有一些元素因为某种原因,导致迟到了,这个时候Spark需要将迟到的的数据加入到已经触发的窗口进行重复计算。但是需要注意如果在长时间的流计算过程中,如果不去限定窗口计算的时间,那么意味着Spark要在内存中一直存储窗口的状态,这样是不切实际的,因此Spark提供一种称为watermarker的机制用于限定存储在Spark内存的中计算的中间结果存储的时间,这样系统就可以将已经确定触发过的窗口的中间结果给删除。如果后续还有数据落入到已经删除的窗口中,Spark把这种数据定义为late数据。其中watermarker计算方式max event time seen by engine - late threshold 如果watermarker的取值大于了时间窗口的endtime即可认定该窗口的计算结果就可以被丢弃了。如果此时再由数据落入到已经被丢弃的时间窗口,则认定为迟到的数据,针对迟到的数据Spark默认会丢弃放弃更新。

使用Watermarker丢弃过期窗口状态前提条件:

  • 只支持Update和Append模式的操作。由于Complete Mode 需要所有聚合模式的数据要被保留,因此不能使用Watermarker删除中间计算状态
  • 在聚合的时候必须有一个Event-time column或者基于window时间的聚合。
  • withWatermark必须和聚合算子使用相同的event-time column。例如:df.withWatermark("time", "1 min").groupBy("time2").count()这种调用室不合法的。
  • withWatermark必须在聚合之前被使用。例如:df.groupBy("time").count().withWatermark("time", "1 min")是无效的调用。

watermarker语义保证:

  • 水印延迟(设置为withWatermark)为“2 hours”可确保spark计算永远不会丢弃延迟小于2小时的任何数据。换句话说,任何不到2小时(在事件时间方面)的数据都保证汇总到那时处理的最新数据。
  • 但是,保证只在一个方面的严格。延迟2小时以上的数据不能保证被立即丢弃; 它可能会也可能不会被聚合。延迟越久的数据,被Spark处理的可能性就较小。

Update Mode
在这里插入图片描述

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

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

//2.创建一个DataFrame
val lines = spark
    .readStream
    .format("socket")
    .option("host", "localhost")
    .option("port", 9999)
    .load()

lines.printSchema()

import org.apache.spark.sql.functions._
var sdf=new SimpleDateFormat("HH:mm:ss")

var windowCount=lines.map(row=> (row.getAs[String]("value").split(",")(0),row.getAs[String]("value").split(",")(1).toLong))
    .map(t=>(t._1,new Timestamp(t._2)))
    .toDF("word","timestamp")
    .withWatermark("timestamp","2 seconds") //必须在聚合之前设定
    .groupBy(window($"timestamp","10 seconds","5 seconds"),$"word")//基于时间窗口
    .agg(count("word").as("count"))//对窗口的数据做聚合
    .map(r=> (sdf.format(r.getStruct(0).getTimestamp(0)),sdf.format(r.getStruct(0).getTimestamp(1)),r.getString(1),r.getLong(2)))
    .toDF("start","end","key","value")

var query=windowCount.writeStream
    .outputMode(OutputMode.Update()) //输出模式必须是Update,如果为Complete模式watermarker不起作用
    .format("console")
    .start()

query.awaitTermination()

Append Mode

在这里插入图片描述
和Update Mode类似,但是系统并不会立即触发窗口,只有当当前的watermarker时间>= 窗口的End时间,系统才会把中间结果输出到Result table中,也就是说系统输出的是最终结果。

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


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

//2.创建一个DataFrame
val lines = spark
    .readStream
    .format("socket")
    .option("host", "localhost")
    .option("port", 9999)
    .load()

lines.printSchema()

import org.apache.spark.sql.functions._
var sdf=new SimpleDateFormat("HH:mm:ss")

var windowCount=lines.map(row=> (row.getAs[String]("value").split(",")(0),row.getAs[String]("value").split(",")(1).toLong))
    .map(t=>(t._1,new Timestamp(t._2)))
    .toDF("word","timestamp")
    .withWatermark("timestamp","2 seconds") //必须在聚合之前设定
    .groupBy(window($"timestamp","10 seconds","5 seconds"),$"word")//基于时间窗口
    .agg(count("word").as("count"))//对窗口的数据做聚合
    .map(r=> (sdf.format(r.getStruct(0).getTimestamp(0)),sdf.format(r.getStruct(0).getTimestamp(1)),r.getString(1),r.getLong(2)))
    .toDF("start","end","key","value")

var query=windowCount.writeStream
    .outputMode(OutputMode.Append()) //输出模式必须是Update,如果为Complete模式watermarker不起作用
    .format("console")
    .start()

query.awaitTermination()

Join Operations

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

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


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

//2.创建一个DataFrame
val lines = spark
    .readStream
    .format("socket")
    .option("host", "localhost")
    .option("port", 9999)
    .load()

lines.printSchema()
//{"id":}
val userDF = spark.sparkContext.parallelize(List((1, "zs"), (2, "lisi")))
    .toDF("id", "name")

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

val orderItemDF=lineDF.map(row=>row.getAs[String]("value"))
    .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", "localhost")
  .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", "localhost")
  .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 hour")
val orderWatermarker=orderDF.withWatermark("order_time","2 hours")

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

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

query.awaitTermination()

目前这种特性,支持Append模式,当两个流都有水位线的时候,以最小的为准。

案例二(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", "localhost")
    .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", "localhost")
    .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._

//设置延迟为 2 seconds 水位线
val userWatermarker=userDF
    .withWatermark("login_time","2 seconds")
    .select(window($"login_time","5 seconds"),$"id",$"name")
    .withColumnRenamed("window","rightWindow")

//设置延迟为 2 seconds 水位线
val orderWaterMarker= orderDF.withWatermark("order_time","2 seconds")
.select(window($"order_time","5 seconds"),$"uid",$"item",$"cost")
.withColumnRenamed("window","leftWindow")

//水位线 相比较,watermarker 以最小的为准
var join=userWatermarker.join(orderWaterMarker,expr("id=uid and rightWindow = leftWindow "),"leftOuter")
.drop("leftWindow")

val query = join.writeStream
.format("console")
.outputMode(OutputMode.Update())//支持 Update或者Append 等价
.start()

query.awaitTermination()
  • Outer Joins with Watermarking

outerJoin和内连接不同的是在内连接中不同的是,在内连接中对于时间范围的限定是可选的,但是在outJoin中用户必须指定这些限定参数,这样Spark在计算的时候可以更具时间范围推断出当前的RDD任何时候都不会和任何行匹配。这样才能给移除的状态的数据哪一方补充null并且输出。

//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", "localhost")
    .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", "localhost")
    .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 seconds 和 2 seconds 数据,一旦时间过去,系统就无法保证数据状态继续保留
val loginWatermarker=userDF.withWatermark("login_time","1 second")
val orderWatermarker=orderDF.withWatermark("order_time","2 seconds")

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

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

query.awaitTermination()

例如:假如说左边元素 的event-time是13:04 但是watermarker的值是13:06 也就意味着不可能会有值小于13:06分的数据到来。也就意味着右边没有元素能和左边元素进行join,因此左边元素可以补空输出。

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值