You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Scala中仅追加Event Hub新增数据至Delta表(非流处理)

不用结构化流实现Event Hub新数据增量读取并追加到Delta表

核心思路是手动维护Event Hub的消费偏移量,用一个Delta表充当checkpoint存储每次读取的结束位置,每次批处理时从这个位置开始读取新数据,读完后更新偏移量,以此实现只处理新增数据。

步骤1:创建存储偏移量的Checkpoint表

先建一个专门存消费位置的Delta表,用来记录每次读取Event Hub后的最新偏移量(仅第一次运行时执行):

import org.apache.spark.sql.types._

val checkpointSchema = StructType(Seq(
  StructField("partitionId", StringType, nullable = false),
  StructField("offset", StringType, nullable = false),
  StructField("sequenceNumber", LongType, nullable = false),
  StructField("enqueuedTime", TimestampType, nullable = false)
))

// 初始化空的checkpoint表
spark.createDataFrame(spark.sparkContext.emptyRDD[Row], checkpointSchema)
  .write
  .format("delta")
  .mode("overwrite")
  .save("/path/to/checkpoint-delta-table")

步骤2:每次批处理的核心逻辑

每次运行批任务时,先读取checkpoint表获取上次的消费位置,以此作为本次读取Event Hub的起点;处理完数据后,把本次的最新偏移量更新回checkpoint表:

import org.apache.spark.sql.functions._

// 1. 读取checkpoint表,获取上次消费位置
val checkpointDF = spark.read.format("delta").load("/path/to/checkpoint-delta-table")

// 2. 配置Event Hub读取参数
val baseEventhubParams = Map(
  "eventhubs.namespace" -> "你的命名空间",
  "eventhubs.name" -> "你的Event Hub名称",
  "eventhubs.sas.key" -> "你的SAS密钥",
  "eventhubs.sas.key.name" -> "你的SAS密钥名称"
)

val finalEventhubParams = if (checkpointDF.isEmpty) {
  // 第一次运行,使用你原来的起始时间戳配置
  baseEventhubParams + ("eventhubs.startingPosition" -> """{"enqueuedTime":"2024-01-01T00:00:00Z","isInclusive":true}""")
} else {
  // 非首次运行,按分区生成起始位置JSON
  val startingPositions = checkpointDF
    .select("partitionId", "offset")
    .rdd
    .map(row => (row.getString(0), row.getString(1)))
    .collectAsMap()
    .map { case (partition, offset) =>
      s""""$partition": {"offset":"$offset","isInclusive":false}"""
    }
    .mkString("{", ",", "}")
  
  baseEventhubParams + ("eventhubs.startingPosition" -> startingPositions)
}

// 3. 读取Event Hub数据
val ehDF = spark.read
  .format("eventhubs")
  .options(finalEventhubParams)
  .load()

// 4. 执行你的数据转换逻辑(关联、选择等)
// 先提取body中的业务数据,再做自定义转换
val transformedDF = ehDF
  .select(
    from_json(col("body").cast(StringType), 你的业务数据Schema).alias("data"),
    col("partitionId"),
    col("offset"),
    col("sequenceNumber"),
    col("enqueuedTime")
  )
  .select("data.*", "partitionId", "offset", "sequenceNumber", "enqueuedTime")
  // 这里添加你的关联、过滤、选择操作
  .join(其他表, Seq("关联字段"), "left")
  .select("需要保留的字段", "partitionId", "offset", "sequenceNumber", "enqueuedTime")

// 5. 将转换后的数据追加到目标Delta表
transformedDF
  .drop("partitionId", "offset", "sequenceNumber", "enqueuedTime") // 移除不需要的元数据字段
  .write
  .format("delta")
  .mode("append")
  .save("/path/to/target-delta-table")

// 6. 更新checkpoint表:按分区保留最新的消费位置
val latestOffsetsDF = ehDF
  .groupBy("partitionId")
  .agg(max("sequenceNumber").alias("maxSeqNo"))
  .join(ehDF, Seq("partitionId", "sequenceNumber"), "inner")
  .select("partitionId", "offset", "sequenceNumber", "enqueuedTime")

latestOffsetsDF
  .write
  .format("delta")
  .mode("overwrite")
  .save("/path/to/checkpoint-delta-table")

关键说明

  • 偏移量准确性:通过按分区取最大的sequenceNumber来锁定每个分区的最新消费位置,确保下次读取不会重复处理已有数据。
  • 调度要求:这种方式是批处理模式,需要借助调度工具(如Airflow、Azure Data Factory)定期运行,周期可根据你的数据推送频率调整。
  • 失败恢复:如果批任务执行失败,checkpoint表不会更新,下次运行会从上次成功的位置重新读取,避免数据丢失或重复写入。

内容的提问来源于stack exchange,提问作者Tamás Godányi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 00:49:58