4. 批量入湖进阶:增量抽取策略(全量覆盖、增量追加、CDC变更捕获)

批量入湖做到这一步,基本的数据搬运已经不成问题了。但真正让数据平台产生价值的,是持续、高效、准确地同步变化数据。说白了,就是怎么把源系统的“新账旧账”一笔笔算清楚,再搬到湖里。

我个人习惯把增量抽取策略分成三个层次:全量覆盖、增量追加、CDC变更捕获。它们不是互斥的,而是根据业务场景和数据量级,逐步演进的关系。你想想看,一个刚起步的数据平台,可能每天跑一次全量就够了;但到了每天几亿条订单的规模,全量覆盖就是灾难,必须上CDC。

核心观点:没有银弹。选择哪种策略,取决于源系统能力、数据量级、业务对延迟的容忍度,以及你的运维成本预算。

增量抽取策略全景图 源系统 MySQL / Oracle / SAP 全量覆盖 TRUNCATE + INSERT 增量追加 时间戳 / 自增ID CDC变更捕获 Binlog / LogMiner Lakehouse Delta Lake / Iceberg / Hudi 💡 策略选择建议 • 全量覆盖:数据量 < 100万行,源表无时间戳,适合T-1离线场景 • 增量追加:数据量 100万~1亿行,有时间戳或自增ID,延迟可接受分钟级 • CDC变更捕获:数据量 > 1亿行,需要秒级延迟,源系统支持Binlog/LogMiner

4.1 全量覆盖:简单粗暴,但别滥用

全量覆盖是最容易理解的策略:每次抽取时,先把目标表清空,再把源表全部数据写进去。嗯,这里要注意,它不等于“每天跑一次全量”。真正的全量覆盖,是幂等的——不管跑多少次,结果都一样。

我在项目中遇到过一个小团队,每天用全量覆盖同步一张5亿行的订单表。结果呢?每天跑6小时,刚好赶上业务高峰期,把数据库连接池打爆了。后来我帮他们改成增量追加,时间从6小时降到15分钟。

什么时候该用全量覆盖?

  • 维度表、码表、配置表——数据量小,变化不频繁
  • 源系统没有时间戳,也没有自增ID
  • 数据量在百万行以内,且业务允许T-1延迟

代码实现其实很简单,但要注意事务控制:

-- 全量覆盖示例(Spark SQL)
-- 1. 先删除目标分区或整表
TRUNCATE TABLE lakehouse.dim_customer;

-- 2. 全量插入
INSERT OVERWRITE lakehouse.dim_customer
SELECT * FROM source_db.customer
WHERE is_active = 1;  -- 只同步有效数据

避坑指南:我曾经见过有人用 DELETE + INSERT 代替 TRUNCATE + INSERT。在Lakehouse里,DELETE不会释放存储空间,久而久之会产生大量小文件。一定要用 INSERT OVERWRITE 或 TRUNCATE。

4.2 增量追加:最常用的策略

增量追加的核心思路是:只抽取从上一次同步之后新增或修改的数据。这需要源表有一个可靠的“水位标记”——通常是时间戳字段或自增ID。

我个人习惯用时间戳 + 自增ID双保险。为什么?因为时间戳可能重复(同一秒内多条记录),自增ID也可能回滚(事务失败后ID跳号)。两个一起用,基本不会漏数据。

水位标记类型 优点 缺点 适用场景
时间戳(updated_at) 直观,业务容易理解 时钟回拨、精度不足 更新不频繁的表
自增ID 单调递增,性能好 无法捕获修改 只追加不修改的日志表
时间戳 + ID 最可靠,几乎不漏数据 需要两个字段 核心业务表

来看一个实际的生产代码片段:

# Python + Spark 增量追加示例
from pyspark.sql import SparkSession

spark = SparkSession.builder \
    .appName("incremental_append") \
    .config("spark.sql.extensions", "io.delta.sql.DeltaSparkSessionExtension") \
    .getOrCreate()

# 读取上次水位
last_watermark = spark.sql("""
    SELECT COALESCE(MAX(watermark_value), '1970-01-01') AS last_ts
    FROM lakehouse.etl_watermark
    WHERE table_name = 'source_db.orders'
""").collect()[0][0]

# 增量抽取
df_incremental = spark.read \
    .format("jdbc") \
    .option("url", "jdbc:mysql://...") \
    .option("dbtable", "orders") \
    .option("predicates", f"updated_at > '{last_watermark}'") \
    .load()

# 追加到Delta表
df_incremental.write \
    .format("delta") \
    .mode("append") \
    .save("/lakehouse/bronze/orders")

# 更新水位
spark.sql(f"""
    INSERT OVERWRITE lakehouse.etl_watermark
    VALUES ('source_db.orders', '{last_watermark}')
""")

关键点:水位表本身也要做增量管理。我建议把水位信息存在Delta表里,利用ACID事务保证水位更新和入湖操作的一致性。否则,万一入湖成功但水位没更新,下次就会重复抽取。

4.3 CDC变更捕获:实时入湖的基石

增量追加有个硬伤:它只能捕获“当前状态”,看不到数据的变化过程。比如一条订单从“待支付”变成“已支付”,增量追加只能看到最终状态。但CDC(Change Data Capture)可以告诉你:什么时间、谁、把哪个字段从什么值改成了什么值。

说白了,CDC就是数据库的“黑匣子”。它读取数据库的事务日志(比如MySQL的Binlog、Oracle的LogMiner、PostgreSQL的WAL),把每一次INSERT、UPDATE、DELETE都解析成事件流。

我记得有一次帮一家金融客户做实时风控,他们要求订单状态变更必须在3秒内入湖。增量追加根本做不到,最后上了Debezium + Kafka + Delta Lake的CDC pipeline,延迟压到了1秒以内。

4.3.1 常见的CDC实现方式

方式 工具 延迟 对源库影响 我的评价
基于日志 Debezium, Maxwell, Canal 秒级 极小(仅读日志) 首选,生产环境最稳
基于触发器 自定义触发器+中间表 秒级 较大(影响源库写入性能) 不推荐,除非没得选
基于时间戳 轮询查询 分钟级 中等(增加查询负载) 适合非核心系统

4.3.2 CDC入湖的数据模型

CDC数据进入Lakehouse后,我建议保留原始变更记录,而不是直接合并成最终状态。这样你可以回溯任意时间点的数据快照。

一个典型的CDC入湖表结构长这样:

-- CDC变更记录表结构
CREATE TABLE lakehouse.cdc_orders (
    -- 业务主键
    order_id        STRING,
    
    -- 变更前数据(Debezium的before字段)
    before_status   STRING,
    before_amount   DECIMAL(10,2),
    
    -- 变更后数据(Debezium的after字段)
    after_status    STRING,
    after_amount    DECIMAL(10,2),
    
    -- CDC元数据
    op_type         STRING,  -- 'c'=创建, 'u'=更新, 'd'=删除
    ts_ms           BIGINT,  -- 变更发生时间(毫秒时间戳)
    file_name       STRING,  -- 来源Binlog文件名
    pos             BIGINT,  -- Binlog中的位置
    
    -- Lakehouse分区字段
    dt              STRING   -- 按天分区
)
USING delta
PARTITIONED BY (dt)
LOCATION '/lakehouse/bronze/cdc_orders';

实战技巧:CDC数据入湖后,我通常会做两层处理:

  1. Bronze层(原始层):保留完整的CDC事件流,不做任何合并,用于审计和回溯
  2. Silver层(清洗层):用MERGE语句将CDC事件合并成当前快照,供下游使用

4.4 三种策略的对比与选择

最后,我用一张表总结一下三种策略的适用场景。你想想看,你的业务属于哪一种?

维度 全量覆盖 增量追加 CDC变更捕获
数据量级 < 100万行 100万 ~ 1亿行 > 1亿行
延迟要求 T-1 分钟级 秒级
源系统改造 无 需要时间戳或自增ID 需要开启Binlog/归档日志
数据一致性 强一致(幂等) 最终一致(可能漏数据) 强一致(基于日志)
运维复杂度 低 中 高(需要维护CDC组件)
典型场景 维度表、码表 订单表、交易流水 实时风控、审计追踪

最后提醒一句:不要一上来就上CDC。我曾经见过一个团队,每天只有几万条数据,却搭了一套Debezium + Kafka + Flink的CDC pipeline。结果运维成本比数据价值还高。从全量覆盖开始,等数据量上来了再逐步演进,这才是务实的做法。