Struct Stream
一、概述
Structured Stream是基于Spark SQL
引擎构建的可伸缩且容错的流处理引擎。使得⽤户可以像使⽤SparkSQL操作静态批处理计算⼀样使⽤Structured Stream的SQL操作流计算。
当流数据继续到达时,SparkSQL引擎将负责递增地,连续地运⾏它并更新最终结果。使⽤Dataset/DataFrame API 实现对实时数据的聚合、event-time 窗⼝计算以及流到批处理join操作。最后,系统通过 检查点
和预写⽇志
来确保端到端(end to end)的⼀次容错证。简⽽⾔之,结构化流提供了快速,可伸缩,容错,端到端的精确⼀次流处理,⽽⽤户不必推理流。
在内部,默认情况下,结构化流查询是使⽤ 微批量处理引擎
处理的,该引擎将数流作为⼀系列⼩批量作业进⾏处理,从⽽实现了低⾄100毫秒的端到端延迟以及⼀次精确的容错保证。
二、快速入门
- 依赖
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.11</artifactId>
<version>2.4.5</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql_2.11</artifactId>
<version>2.4.5</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming_2.11</artifactId>
<version>2.4.3</version>
</dependency>
- 案例
package com.baizhi.one
import org.apache.spark.sql.{DataFrame, SparkSession}
object QuickStart {
def main(args: Array[String]): Unit = {
//1.创建sparkSession
val session = SparkSession.builder().appName("quickStart").master("local[*]").getOrCreate()
//设置日志级别
session.sparkContext.setLogLevel("FATAL")
//引入隐式文件
import session.implicits._
//获取输入
val dataFrame = session.readStream
.format("socket")
.option("host", "hbase")
.option("port", 9999)
.load()
//操作转换
dataFrame.printSchema()
val wordCounts: DataFrame = dataFrame.as[String]
.flatMap(_.split(" "))
.groupBy("value")
.count()
//写出
val query = wordCounts.writeStream
.outputMode("complete")
.format("console")
.start()
//等待系统关闭
query.awaitTermination()
}
}
三、处理模式
Structured Streaming核心思想是将实时数据流看成是一个持续追加的table,这就引入了一个非常类似批处理的流处理模型。因此用户可以使用类似批处理的模型的SQL表达式计算这种流数据。可以将输入的数据看成是Input Table,后续到来的数据可以看做是新到来的Row,这些Row会被追加到这张表中。
原文链接:https://blog.csdn.net/weixin_38231448/article/details/99991076
查询输入数据会产生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
☆
Spark负责在有新数据时更新结果表,从⽽使⽤户免于推理。系统通过 检查点
和 预写日志
来确保端到端(end to end)的精准⼀次容错保证。
- 最多⼀次:如果计算过程中,存在失败,⾃动忽略,⼀般只考虑性能,不考虑安全-(丢数据)
- ⾄少⼀次:在故障时候,系统因为重试或者重新计算,导致记录重复参与计算-(不丢数据,重复更新,计算不准确)
- 精确⼀次:在故障时候,系统因为重试或者重新计算,不会导致数据丢失或者重复计算(对数据精准性要求⽐较⾼的实时计算场景)
四、容错语义
提供端到端的精确⼀次语义是结构化流设计背后的主要⽬标之⼀。为此,我们设计了 结构化流源
, 接收器
和执⾏引擎
,以可靠地跟踪处理的确切进度,以便它可以通过重新启动和/或重新处理来处理任何类型的故障。假定每个流源
都有偏移量(类似于Kafka偏移量或Kinesis序列号)以跟踪流中的读取位置。引擎使⽤ 检查点
和预写⽇志
来记录每个触发器中正在处理的数据的偏移范围。流接收器被设计为是 幂等
的,⽤于处理后处理。结合使⽤ 可重播的源
和幂等的接收器
,结构化流可以确保在发⽣任何故障时端到端的⼀次精确语义。
五、Input Source
①.File Source
支持故障容错
package com.baizhi.source
import org.apache.spark
import org.apache.spark.sql.streaming.OutputMode
import org.apache.spark.sql.types.{BooleanType, DoubleType, IntegerType, StringType, StructType}
import org.apache.spark.sql.{DataFrame, SparkSession}
object CsvInput {
def main(args: Array[String]): Unit = {
//创建sparkSession
val sparkSession = SparkSession.builder().master("local[*]").appName("InputSource").getOrCreate()
//设置日志级别
sparkSession.sparkContext.setLogLevel("FATAL")
//引入隐式文件
import sparkSession.implicits._
//定义表结构体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 = sparkSession.readStream
.schema(userSchema)
.option("sep", ",")//分隔符
.option("header", "true")//去除表头
.csv("hdfs://hbase:9000/03_02/csv")
//2.对表进行查询
lines.printSchema()
//产生StreamQuery对象
val query = lines.writeStream
.outputMode(OutputMode.Append()) //输出模式 - 追加
.format("console") //输出路径 - 控制台
.start()
query.awaitTermination()
}
}
②.Socket Source
不支持故障容错
package com.baizhi.source
import org.apache.spark.sql.streaming.OutputMode
import org.apache.spark.sql.{DataFrame, SparkSession}
object SocketSource {
def main(args: Array[String]): Unit = {
//创建sparkSession
val sparkSession = SparkSession.builder().master("local[*]").appName("InputSource").getOrCreate()
//设置日志级别
sparkSession.sparkContext.setLogLevel("FATAL")
//引入隐式文件
import sparkSession.implicits._
//1.创建输⼊流(表)
val lines:DataFrame = sparkSession.readStream
.format("socket")//读流模式
.option("host","hbase")//ip
.option("port",9999)//端口
.load()
//2.对表进行查询
lines.printSchema()
val dat = lines.as[String]
.flatMap(_.split(" "))
.groupBy("value").count()
//产生StreamQuery对象
val query = dat.writeStream
.outputMode(OutputMode.Update()) //输出模式 - 追加
.format("console") //输出路径 - 控制台
.start()
query.awaitTermination()
}
}
③.Rate Source
支持故障容错
以每秒指定的⾏数⽣成数据,每个输出⾏包含⼀个时间戳和⼀个值。其中timestamp是包含消息分发时间的Timestamp类型,⽽值是包含消息计数的Long类型,从第⼀⾏的0开始。此源旨在进⾏测试和基准测试。
//1.创建输⼊流(表)
val lines:DataFrame = sparkSession.readStream
.format("rate")
.option("rowsPerSecond","1000")
.load()
//2.对表进行查询
lines.printSchema()
//3.产生StreamQuery对象
val query = lines.writeStream
.outputMode(OutputMode.Update()) //输出模式 - 追加
.format("console") //输出路径 - 控制台
.start()
④.☆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>
- 设定一个
IOT
设备,以此设备发送数据为例
- 定义一个样例类
case class DeviceData(device: String, deviceType: String, signal: Double, time:
String)
- 读取卡夫卡数据
package com.baizhi.kafka
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.streaming.{OutputMode, StreamingQuery}
object KafkaSource {
def main(args: Array[String]): Unit = {
//创建 ss
val spark = SparkSession
.builder
.appName("StructuredNetworkWordCount")
.master("local[*]")
.getOrCreate()
//引入隐式文件
import spark.implicits._
//设置日志级别
spark.sparkContext.setLogLevel("ERROR")
//1.创建输⼊流(表)
spark.readStream
.format("kafka") //kafka源
.option("kafka.bootstrap.servers", "hbase: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") //映射表
//书写sql语句
var sql=
"""
select device,deviceType,avg(signal)
from t_device
group by deviceType,device
"""
//执行sql
val results = spark.sql(sql)
//3.产⽣StreamQuery对象
val query:StreamingQuery = results.writeStream
.outputMode(OutputMode.Complete())
.format("console")
.start()
query.awaitTermination()
}
}
六、Output Sink
①.File Sink (A)
File Sink 只允许用在Append模式,支持精准一次的写入
package com.baizhi.sink
import org.apache.spark.sql.streaming.{OutputMode, StreamingQuery}
import org.apache.spark.sql.{DataFrame, SparkSession}
object FileSink {
def main(args: Array[String]): Unit = {
//1.创建sparkSession
val session = SparkSession.builder().appName("quickStart").master("local[*]").getOrCreate()
//设置日志级别
session.sparkContext.setLogLevel("FATAL")
//引入隐式文件
import session.implicits._
//获取输入
val inputs = session.readStream
.format("kafka")
.option("kafka.bootstrap.servers", "hbase:9092")
.option("subscribePattern", "topic.*")
.load()
//写出
val query:StreamingQuery = inputs.writeStream
.outputMode(OutputMode.Append())
.format("csv")
.option("sep", ",")
.option("header", "true")//去除表头
.option("inferSchema", "true")//参照表信息
.option("path", "hdfs://hbase:9000/03_02/fileSink")
.option("checkpointLocation", "hdfs://hbase:9000/03_02/checkpoint_fileSink")
.start()
query.awaitTermination()
}
}
②.☆Kafka Sink (3)
http://spark.apache.org/docs/latest/structured-streaming-kafka-integration.html
Apache Kafka仅⽀持⾄少⼀次
写⼊语义 。因此,在向Kafka写⼊流查询或批处理查询时,某些记录可能会重复。例如,如果Kafka需要重试Broker未确认的消息(即使Broker已经收到并写⼊了消息记录),就会发⽣这种情况。由于这些Kafka写语义,结构化流⽆法阻⽌此类重复项发⽣。
Column | Type |
---|---|
key (optional) | string or binary |
value(requied) | string or binary |
topic(*optional) | string |
必须保证写⼊的DF中仅仅含有 value 字符串类型的字段、 key 可选,如果没有系统认为是null。 topic可选,前提是必须在Option中 配置topic,否则必须出现在字段列中。
package com.baizhi.kafka
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.streaming.{OutputMode, StreamingQuery}
// 1,zhangsan,1,4.5
object KafkaSInk {
def main(args: Array[String]): Unit = {
//1.创建sparkSession
val spark = SparkSession.builder().appName("quickStart").master("local[*]").getOrCreate()
//设置日志级别
spark.sparkContext.setLogLevel("FATAL")
//引入隐式文件
import spark.implicits._
//读流
val inputs = spark.readStream
.format("socket")
.option("host", "hbase")
.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://hbase:9000/03_02/checkPoint_kafkaSink")
.option("kafka.bootstrap.servers", "hbase:9092")
.option("topic", "topic01")//覆盖DF中topic字段
.start()
query.awaitTermination()
}
}
③.Console Sink
每次有触发器时,将输出打印到控制台/ stdout。⽀持追加和完整输出模式。由于每次触发后都会收集全部输出并将其存储在驱动程序的内存中,因此应在数据量较⼩时⽤于调试⽬的。
writeStream
.outputMode(OutputMode.Update())
.format("console")
.option("numRows", "2") //只取2行
.option("truncate", "true")//截断表
.start()
④.Memory Sink
输出作为内存表存储在内存中。⽀持追加和完整输出模式。当整个输出被收集并存储在驱动程序的内存中时,应将其⽤于调试低数据量的⽬的。因此,请谨慎使⽤。
package com.baizhi.sink
import org.apache.spark.sql.{DataFrame, SparkSession}
object MemorySink {
def main(args: Array[String]): Unit = {
//1.创建sparkSession
val session = SparkSession.builder().appName("quickStart").master("local[*]").getOrCreate()
//设置日志级别
session.sparkContext.setLogLevel("FATAL")
//引入隐式文件
import session.implicits._
//获取输入
val dataFrame = session.readStream
.format("socket")
.option("host", "hbase")
.option("port", 9999)
.load()
//操作转换
dataFrame.printSchema()
val wordCounts: DataFrame = dataFrame.as[String]
.flatMap(_.split(" "))
.groupBy("value")
.count()
//写出
val query = wordCounts.writeStream
.outputMode("complete")
.format("memory")
.queryName("t_word")
.start()
new Thread(new Runnable {
override def run(): Unit = {
Thread.sleep(1000)
//开启子线程,每一秒查一次表中数据
session.sql("select * from t_word").show()
}
}).start()
query.awaitTermination()
}
}
⑤.Foreach Sink
ForeachBatch、Foreach
对输出中的记录运⾏任意输出。使⽤foreach和foreachBatch操作,您可以在流查询的输出上应⽤任意操作并编写逻辑。它们的⽤例略有不同-虽然foreach
允许在每⼀⾏上使⽤⾃定义写逻辑,但是foreachBatch
允许在每个微批处理的输出上进⾏任意操作和⾃定义逻辑。
- foreachBatch(…)允许您指定在流查询的每个微批处理的输出数据上执⾏的函数。
package com.baizhi.sink
import org.apache.spark.sql.streaming.OutputMode
import org.apache.spark.sql.{DataFrame, SaveMode, SparkSession}
object ForeachBatchSink {
def main(args: Array[String]): Unit = {
//1.创建sparkSession
val session = SparkSession.builder().appName("quickStart").master("local[*]").getOrCreate()
//设置日志级别
session.sparkContext.setLogLevel("FATAL")
//引入隐式文件
import session.implicits._
//获取输入
val dataFrame = session.readStream
.format("socket")
.option("host", "hbase")
.option("port", 9999)
.load()
//操作转换
dataFrame.printSchema()
val wordCounts: DataFrame = dataFrame.as[String]
.flatMap(_.split(" "))
.groupBy("value")
.count()
//写出
val query = wordCounts.writeStream.outputMode(OutputMode.Complete())
.foreachBatch((ds,batchID)=>{
ds.write //使⽤静态批处理的API
.mode(SaveMode.Overwrite).format("json")
.save("hdfs://hbase:9000/03_02/foreachBatchSink")
})
.start()
query.awaitTermination()
}
}
Foreach
package com.baizhi.sink
import org.apache.spark.sql.{ForeachWriter, Row}
import redis.clients.jedis.{Jedis, JedisPool}
class TeacherDefinedForeach extends ForeachWriter[Row]{
//因为需要序列化连接对象,使用懒加载
lazy val jedisPool:JedisPool=createJedisPool()
var jedis:Jedis=null
def createJedisPool(): JedisPool = {
new JedisPool("hbase",6379)
}
override def open(partitionId: Long, epochId: Long): Boolean = {
jedis=jedisPool.getResource
true //执行process
}
override def process(value: Row): Unit = {
var Row(word,count)=value
println(word,count)
jedis.set(word.toString,count.toString)
}
override def close(errorOrNull: Throwable): Unit = {
jedis.close()
}
}
测试
//写出
val query = wordCounts.writeStream.outputMode(OutputMode.Complete())
.foreach(new MydefinedForeach) //使用自定义foreach
.start()
七、Window Operations
☆.Watermarking
迟到的数据
与 水位线
在spark2.1版本引⼊watermarkering概念,⽤于告知计算节点,何时丢弃窗⼝聚合状
态。因为流计算是⼀个⻓时间运⾏任务,系统不可能⽆限制存储⼀些过旧的状态值。
前提条件
:
1.只支持Update和Append模式的操作
2.在聚合的时候必须有一个Event-time column或者基于window时间的聚合
3.withWatermark必须和聚合算子使用相同的event-time column
4.withWatermark必须在聚合之前被使用。
语义保证
:
1.水印延迟(设置为withWatermark
)为“2 hours”可确保spark计算永远不会丢弃延迟小于2小时的任何数据。换句话说,任何不到2小时(在事件时间方面)的数据都保证汇总到那时处理的最新数据
2.保证只在一个方面的严格。延迟2小时以上的数据不能保证被立即丢弃; 它可能会也可能不会被聚合。延迟越久的数据,被Spark处理的可能性就较小
Update
模式
package com.baizhi.window
import java.sql.Timestamp
import java.text.SimpleDateFormat
import org.apache.spark.sql.streaming.{OutputMode, StreamingQuery}
import org.apache.spark.sql.{DataFrame, Row, SparkSession}
object WaterMark {
def main(args: Array[String]): Unit = {
//1.创建sparkSession
val spark = SparkSession.builder().appName("WaterMark").master("local[*]").getOrCreate()
//设置日志级别
spark.sparkContext.setLogLevel("FATAL")
//2.读流
val lines: DataFrame = spark.readStream
.format("socket")
.option("host", "hbase")
.option("port", 9999)
.load()
//引入隐式文件
import spark.implicits._
import org.apache.spark.sql.functions._
val frame: DataFrame = lines.as[String]
.map(_.split("\\s+"))
.map(x => (x(0), new Timestamp(x(1).toLong)))
.toDF("word", "timeStamp") //映射成一个含有时间列的表
.withWatermark("timeStamp", "1 seconds")
.groupBy(window($"timeStamp", "4 seconds", "2 seconds"), $"word") //分组
.count()
//处理语句
frame.printSchema()
val result: DataFrame = frame //统计
.map(y => {
val start = y.getAs[Row]("window").getAs[Timestamp]("start") //获得时间起始
val end = y.getAs[Row]("window").getAs[Timestamp]("end") //获得时间终止
val word = y.getAs[String]("word") //获取字段
val count = y.getAs[Long]("count") //获取数量
//定义时间解析
val sdf = new SimpleDateFormat("HH:mm:ss")
(sdf.format(start), sdf.format(end), word, count)
}).toDF("start", "end", "word", "count") //映射成 类型
//3.写流
val query: StreamingQuery = result.writeStream
.format("console")
.outputMode(OutputMode.Update()) //输出模式
.start()
query.awaitTermination()
}
}
root
|-- window: struct (nullable = false)
| |-- start: timestamp (nullable = true)
| |-- end: timestamp (nullable = true)
|-- word: string (nullable = true)
|-- count: long (nullable = false)
如果watermarker时间
>end time时间
则认为该窗⼝的计算状态可以 丢弃
update
-⽔位线没有没过窗⼝的end time之前,如果有数据落⼊到该窗⼝,该窗⼝会重复触发。Append
–⽔位线没有没过窗⼝的end time之前,如果有数据落⼊到该窗⼝,该窗⼝不会触发,只会默默的计算,只有当⽔位线没过窗⼝end time的时候,才会做出最终输出。
八、Join
⾃Spark-2.0 Structured Streaming
引⼊的Join的概念(inner和⼀些外连接)。⽀持和静态或者动态的Dataset/DataFrame做join操作。
①.static
package com.baizhi.join
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.streaming.OutputMode
object StaticJoin {
def main(args: Array[String]): Unit = {
//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 45 读流
val lineDF=spark.readStream
.format("socket")
.option("host","hbase")
.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._
//join 操作
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()
}
}
②.Range
- 在spark-2.3添加了streaming-streaming的⽀持 ,实现两个流的join最⼤的挑战是在于找到⼀个时间点实现两个流的join,因为这两个流都没有结束。任意⼀个接受的流可以匹配另外⼀个流中即将被接受的数据。所以在任意⼀个流中我们需要接收并将这些数据进⾏缓存,然后作为当前stream的状态,然后去匹配另外⼀个的流的后续接收数据,继⽽⽣成相应的join的结果集。和Streaming的聚合很类似我们使⽤watermarker处理late,乱序的数据,限制状态的使⽤。
为了避免⽆限制的状态存储。⼀般需要定义额外的join的条件
-
两边流计算需要定义watermarker延迟,这样系统可以知道两个流的时间差值。
-
定制⼀下event time的限制条件,这样引擎可以计算出哪些数据old的不再需要了。可以使⽤⼀下两种⽅式定制
时间范围界定例如: JOIN ON leftTime BETWEEN rightTime AND rightTime +INTERVAL 1 HOUR
基于Event-time Window 例如: JOIN ON leftTimeWindow = rightTimeWindow
package com.baizhi.join
import java.sql.Timestamp
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.streaming.OutputMode
object RangeJoin {
def main(args: Array[String]): Unit = {
//0.创建spark对象
val spark = SparkSession
.builder
.appName("StructuredNetworkWordCount")
.master("local[*]")
.getOrCreate()
spark.sparkContext.setLogLevel("FATAL")
import spark.implicits._
//2.创建⼀个订单的输入 9999端口 001 apple 1 4.5 1566113401000
val orderDF = spark
.readStream
.format("socket")
.option("host", "hbase")
.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 创建一个用户的输入 8888
val userDF = spark
.readStream
.format("socket")
.option("host", "hbase")
.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._
//系统分别会对 user 和 order 缓存 最近 1 秒 和 2秒 数据,⼀旦时间过去,系统就⽆法保证数据状态继续
val loginWatermarker=userDF.withWatermark("login_time","1 second")
val orderWatermarker=orderDF.withWatermark("order_time","2 seconds")
//计算订单的时间 & ⽤户 登陆之后的0~1s 关联 数据 并且进⾏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
基于eventTime进行join
package com.baizhi.join
import java.sql.Timestamp
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.streaming.OutputMode
object EventTimeJoin {
def main(args: Array[String]): Unit = {
//0.创建spark对象
val spark = SparkSession
.builder
.appName("StructuredNetworkWordCount")
.master("local[*]")
.getOrCreate()
//设置日志级别
spark.sparkContext.setLogLevel("FATAL")
//引入隐式文件
import spark.implicits._
import org.apache.spark.sql.functions._
//2.创建⼀个订单输入 001 apple 1 4.5 1566113401000
val orderDF = spark
.readStream
.format("socket")
.option("host", "hbase")
.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"))
//创建⼀个用户输入 001 zhangsan 1566113400000
val userDF = spark
.readStream
.format("socket")
.option("host", "hbase")
.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"))
//作水位线标记
//系统分别会对 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()
}
}
- LeftOut
//计算订单的时间 & 用户 登陆之后的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")