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
}
}
调式验证
序号 | Record | EventTime | 当前Watermark | 输出 |
---|---|---|---|---|
1 | {“eventTime”: “2016-01-10 10:01:50”,“eventType”: “browse”,“userID”:“1”} | 2016-01-10 10:01:50 | 2016-01-10 10:01:40 | yes |
2 | {“eventTime”: “2016-01-10 10:01:50”,“eventType”: “click”,“userID”:“1”} | 2016-01-10 10:01:50 | 2016-01-10 10:01:40 | yes |
3 | {“eventTime”: “2016-01-10 10:01:55”,“eventType”: “browse”,“userID”:“1”} | 2016-01-10 10:01:55 | 2016-01-10 10:01:45 | yes |
4 | {“eventTime”: “2016-01-10 10:01:55”,“eventType”: “click”,“userID”:“1”} | 2016-01-10 10:01:55 | 2016-01-10 10:01:45 | yes |
5 | {“eventTime”: “2016-01-10 10:01:50”,“eventType”: “browse”,“userID”:“1”} | 2016-01-10 10:01:50 | 2016-01-10 10:01:45 | 重复数据不输出 |
6 | {“eventTime”: “2016-01-10 10:01:50”,“eventType”: “click”,“userID”:“1”} | 2016-01-10 10:01:50 | 2016-01-10 10:01:45 | 重复数据不输出 |
7 | {“eventTime”: “2016-01-10 10:02:00”,“eventType”: “click”,“userID”:“1”} | 2016-01-10 10:02:00 | 2016-01-10 10:01:50 | yes |
8 | {“eventTime”: “2016-01-10 10:01:50”,“eventType”: “browse”,“userID”:“1”} | 2016-01-10 10:01:50 | 2016-01-10 10:01:50 | 过期数据不输出 |
9 | {“eventTime”: “2016-01-10 10:01:50”,“eventType”: “click”,“userID”:“1”} | 2016-01-10 10:01:50 | 2016-01-10 10:01:50 | 过期数据不输出 |
10 | {“eventTime”: “2016-01-10 10:01:51”,“eventType”: “click”,“userID”:“1”} | 2016-01-10 10:01:51 | 2016-01-10 10:01:50 | yes |
重点说明与总结
-
这里设置的重复数据最大迟到时间为10秒。
-
Watermark实际上是Timestamp类型,格式如:2016-01-01T02:02:00.000Z,这里为方便对齐比较,将Watermark加了8小时。
-
状态清除: 当前Watermark>=EventTime时,清除该EventTime对应的状态(数据)。
-
第5条
、第6条
, 与状态中的数据重复,不输出。 -
第8条
、第9条
,这两条数据是过期数据,不输出。同时,由于当前Watermark>=EventTime,状态中维护的老数据被清除。 -
dropDuplicates
方法可以传多个列名作为唯一键。即支持dropDuplicates(field1、field2、field3 ...)
。 -
dropDuplicates
方法不可用在聚合之后。即通过聚合生成的DataFrame、DataSet不能再调用dropDuplicates
。