Spark Structured Streaming去除重复数据Deduplication

Structured Streaming可以使用Deduplication对有无Watermark的流式数据进行去重操作。

  • 无Watermark: 对重复记录到达的时间没有限制。查询会保留所有的过去记录作为状态用于去重。

  • 有Watermark: 对重复记录到达的时间有限制。查询会根据水印删除旧的状态数据。

本文总结Deduplication的使用及注意事项。

测试数据

// 测试数据,如下:
// eventTime: 北京时间
 {"eventTime": "2016-01-01 10:02:00" ,"eventType": "browse/click" ,"userID":"1"}

代码实现

import java.sql.Timestamp
import java.time.format.DateTimeFormatter
import java.time.{LocalDateTime, ZoneId}

import com.bigdata.structured.streaming.sink.FileSink
import org.apache.spark.sql.functions._
import org.apache.spark.sql.streaming.Trigger
import org.apache.spark.sql.types.DataType
import org.apache.spark.sql.{SparkSession, functions}
import org.slf4j.LoggerFactory

/**
  * Author: Wang Pei
  * Summary:
  *   Structured Streaming 去除重复数据Deduplication
  */
object Deduplication {

  lazy val logger = LoggerFactory.getLogger(FileSink.getClass)

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

    val spark = SparkSession.builder().master("local[3]").appName(this.getClass.getSimpleName.replace("$", "")).getOrCreate()
    import spark.implicits._

    // 注册UDF
    spark.udf.register("timezoneToTimestamp", timezoneToTimestamp _)
    // 定义Kafka JSON Schema
    val jsonSchema ="""{"type":"struct","fields":[{"name":"eventTime","type":"string","nullable":true},{"name":"eventType","type":"string","nullable":true},{"name":"userID","type":"string","nullable":true}]}"""

    // InputTable
    val inputTable = spark
      .readStream
      .format("kafka")
      .option("kafka.bootstrap.servers", "kafka01:9092,kafka02:9092,kafka03:9092")
      .option("subscribe", "test_1")
      .load()

    // ResultTable
    val resultTable = inputTable
      .select(from_json(col("value").cast("string"), DataType.fromJson(jsonSchema)).as("value"))
      .select($"value.*")
      .withColumn("timestamp", functions.callUDF("timezoneToTimestamp", functions.col("eventTime"),lit("yyyy-MM-dd HH:mm:ss"),lit("GMT+8")))
      .filter($"timestamp".isNotNull && $"eventType".isNotNull && $"userID".isNotNull)
      // 重复记录到达的时间上限
      .withWatermark("timestamp", "10 seconds")
      .dropDuplicates("timestamp","eventType","userID")
      .select($"eventTime",$"eventType",$"userID")

    // Query Start
    val query = resultTable
      .writeStream
      .format("console")
      .option("truncate", "false")
      .outputMode("append")
      .trigger(Trigger.ProcessingTime("2 seconds"))
      .start()

    query.awaitTermination()

  }

  /**
    * 带时区的时间转换为Timestamp
    *
    * @param dateTime
    * @param dataTimeFormat
    * @param dataTimeZone
    * @return
    */
  def timezoneToTimestamp(dateTime: String, dataTimeFormat: String, dataTimeZone: String): Timestamp = {
    var output: Timestamp = null
    try {
      if (dateTime != null) {
        val format = DateTimeFormatter.ofPattern(dataTimeFormat)
        val eventTime = LocalDateTime.parse(dateTime, format).atZone(ZoneId.of(dataTimeZone));
        output = new Timestamp(eventTime.toInstant.toEpochMilli)
      }
    } catch {
      case ex: Exception => logger.error("时间转换异常..." + dateTime, ex)
    }
    output
  }
}

调式验证

序号RecordEventTime当前Watermark输出
1{“eventTime”: “2016-01-10 10:01:50”,“eventType”: “browse”,“userID”:“1”}2016-01-10 10:01:502016-01-10 10:01:40yes
2{“eventTime”: “2016-01-10 10:01:50”,“eventType”: “click”,“userID”:“1”}2016-01-10 10:01:502016-01-10 10:01:40yes
3{“eventTime”: “2016-01-10 10:01:55”,“eventType”: “browse”,“userID”:“1”}2016-01-10 10:01:552016-01-10 10:01:45yes
4{“eventTime”: “2016-01-10 10:01:55”,“eventType”: “click”,“userID”:“1”}2016-01-10 10:01:552016-01-10 10:01:45yes
5{“eventTime”: “2016-01-10 10:01:50”,“eventType”: “browse”,“userID”:“1”}2016-01-10 10:01:502016-01-10 10:01:45重复数据不输出
6{“eventTime”: “2016-01-10 10:01:50”,“eventType”: “click”,“userID”:“1”}2016-01-10 10:01:502016-01-10 10:01:45重复数据不输出
7{“eventTime”: “2016-01-10 10:02:00”,“eventType”: “click”,“userID”:“1”}2016-01-10 10:02:002016-01-10 10:01:50yes
8{“eventTime”: “2016-01-10 10:01:50”,“eventType”: “browse”,“userID”:“1”}2016-01-10 10:01:502016-01-10 10:01:50过期数据不输出
9{“eventTime”: “2016-01-10 10:01:50”,“eventType”: “click”,“userID”:“1”}2016-01-10 10:01:502016-01-10 10:01:50过期数据不输出
10{“eventTime”: “2016-01-10 10:01:51”,“eventType”: “click”,“userID”:“1”}2016-01-10 10:01:512016-01-10 10:01:50yes

重点说明与总结

  1. 这里设置的重复数据最大迟到时间为10秒。

  2. Watermark实际上是Timestamp类型,格式如:2016-01-01T02:02:00.000Z,这里为方便对齐比较,将Watermark加了8小时。

  3. 状态清除: 当前Watermark>=EventTime时,清除该EventTime对应的状态(数据)。

  4. 第5条第6条, 与状态中的数据重复,不输出。

  5. 第8条第9条,这两条数据是过期数据,不输出。同时,由于当前Watermark>=EventTime,状态中维护的老数据被清除。

  6. dropDuplicates方法可以传多个列名作为唯一键。即支持dropDuplicates(field1、field2、field3 ...)

  7. dropDuplicates方法不可用在聚合之后。即通过聚合生成的DataFrame、DataSet不能再调用dropDuplicates

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值