Struct Streaming

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写语义,结构化流⽆法阻⽌此类重复项发⽣。

ColumnType
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")
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值