2. 增量计算核心概念:数据变更捕获、增量视图、状态管理与时间语义

好,咱们进入正题。增量计算听起来高大上,但拆开来看,核心就四个东西:怎么知道数据变了、怎么维护变化后的结果、中间状态怎么管、时间怎么对齐。把这四个点吃透,增量计算你就懂了一半。

我个人习惯把这四个概念比作一个实时数据管道的心脏、大脑、肌肉和脉搏。缺一个,系统就跑不起来,或者跑起来也是歪的。

增量计算四大核心概念 增量计算引擎 CDC 数据变更捕获 增量视图 结果持续更新 状态管理 中间结果存储 时间语义 事件时间 vs 处理时间 四个概念相互依赖:CDC 是输入,增量视图是输出,状态管理是中间层,时间语义是校准器

2.1 数据变更捕获(CDC)——增量计算的起点

CDC 说白了就是「怎么知道数据变了」。没有 CDC,增量计算就是无源之水。

我在项目中遇到过最典型的场景:业务库是 MySQL,每天几亿条订单数据。全量同步一次要跑 6 小时,业务方等不了。后来上了 CDC,延迟压到秒级,效果立竿见影。

常见的 CDC 实现方式有几种:

方式 原理 延迟 对源库影响 典型工具
基于日志 解析 binlog / WAL 毫秒级 极小 Debezium、Canal
基于触发器 数据库触发器写变更表 秒级 较大 自研
基于时间戳 轮询 updated_at 字段 分钟级 中等 DataX、Sqoop
基于 API 调用业务系统变更接口 秒级~分钟级 取决于实现 Kafka Connect

我的建议:生产环境优先选基于日志的方式。它最轻量,对源库几乎无侵入。我曾经见过一个团队用触发器做 CDC,结果业务高峰期触发器把数据库拖垮了——触发器里写了复杂的跨表逻辑,血的教训。

CDC 输出的数据格式也很关键。通常是一条变更事件,包含:

  • 操作类型:INSERT / UPDATE / DELETE
  • 变更前数据(可选):旧值快照
  • 变更后数据:新值快照
  • 元数据:表名、时间戳、事务 ID 等
// 一个典型的 CDC 事件(JSON 格式)
{
  "op": "u",           // 操作类型:c=create, u=update, d=delete
  "before": {          // 变更前数据
    "id": 1001,
    "status": "PAID",
    "amount": 299.00
  },
  "after": {           // 变更后数据
    "id": 1001,
    "status": "SHIPPED",
    "amount": 299.00
  },
  "source": {          // 元数据
    "table": "orders",
    "ts_ms": 1698000000123
  }
}

避坑指南:CDC 的「 exactly-once 」语义是个大坑。我曾经在项目中遇到 binlog 重复投递导致下游重复计算,后来靠 Kafka 的幂等生产者 + 下游去重才解决。记住:CDC 只能保证 at-least-once,exactly-once 需要上下游配合。

2.2 增量视图——让结果「动」起来

增量视图是什么?简单说,就是每次数据变更后,视图自动更新,而不是重新算一遍。

你想想看,传统做法是:每天凌晨跑批,全量聚合,出报表。增量视图的做法是:来一条订单,更新一次销售额。前者是「拍照」,后者是「直播」。

增量视图的实现方式主要有两种:

  1. 物化视图增量刷新:数据库原生支持,比如 PostgreSQL 的 MATERIALIZED VIEW + REFRESH CONCURRENTLY。但刷新粒度是表级,不是行级。
  2. 流式计算中的持续查询:Flink、Kafka Streams 等引擎维护一个「结果表」,每条数据进来后,只更新受影响的那几行聚合结果。

举个例子,计算每分钟的订单金额总和:

-- 传统批处理(每天跑一次)
SELECT TUMBLE(event_time, INTERVAL '1' MINUTE) AS window,
       SUM(amount) AS total_amount
FROM orders
WHERE event_time >= '2024-01-01' AND event_time < '2024-01-02'
GROUP BY TUMBLE(event_time, INTERVAL '1' MINUTE);

-- 增量视图(持续更新)
-- 每条订单到来时,只更新对应分钟窗口的 SUM
-- 引擎内部维护一个 Map<窗口, 累计值> 的状态

关键点:增量视图不是「重新计算」,而是「增量更新」。这意味着引擎需要知道:来了一条新数据,它应该影响哪些旧结果?这背后依赖的是增量计算模型,比如 Flink 的增量聚合算子、Kafka Streams 的 KTable 更新。

2.3 增量状态管理——引擎的「记忆」

增量计算离不开状态。为什么?因为你要记住「到目前为止算到哪了」。

我记得刚接触流计算时,以为每条数据都是独立的。后来才发现,大部分计算都需要跨数据的状态:窗口聚合要记住窗口内的数据、Join 要记住一侧的数据、去重要记住已经见过的 key……

状态管理要解决三个核心问题:

  • 存什么:聚合中间值、窗口缓存、Join 缓冲区、去重集合
  • 存哪里:内存(快但有限)、RocksDB(磁盘但慢)、外部存储(Redis、HBase)
  • 怎么恢复:Checkpoint + 一致性快照
存储后端 读写速度 容量 适用场景
Heap(堆内存) 纳秒级 受限于 JVM 小状态(<10GB)
RocksDB 微秒~毫秒级 磁盘容量 大状态(TB 级)
外部 KV 存储 毫秒级 可扩展 跨作业共享状态

我曾经踩过的坑:状态膨胀导致 OOM。当时一个去重作业,状态里存了所有历史 key,一个月后状态涨到 50GB,RocksDB 频繁 compaction,作业延迟飙升。后来加了 TTL 和 Bloom Filter,才稳住。记住:状态不是无限存的,一定要设过期策略。

2.4 事件时间与处理时间——时间到底听谁的?

这是增量计算里最容易搞混的概念。我见过不少新手,甚至老手,在这里翻车。

事件时间(Event Time):数据实际发生的时间。比如用户下单的时间,由业务系统打戳。

处理时间(Processing Time):数据到达计算引擎的时间。比如 Flink 收到这条数据的时间。

为什么会有两个时间?因为数据会迟到。网络延迟、重试、离线补录……你永远不知道一条数据什么时候到。

举个例子:

  • 用户 10:00:00 下单,事件时间 = 10:00:00
  • 网络抖动,这条数据 10:00:05 才到 Flink,处理时间 = 10:00:05
  • 如果按处理时间算窗口,这条数据会被归到 10:00:05 的窗口,错了!

我的原则:能用事件时间,就别用处理时间。处理时间只适合对时间不敏感的场景,比如简单的过滤、转换。但凡涉及窗口、聚合、Join,一律用事件时间。

事件时间引入了一个新问题:Watermark(水位线)。Watermark 是引擎用来判断「数据是否到齐」的机制。比如 Watermark = 10:00:10,意味着事件时间 <= 10:00:10 的数据已经到齐了,可以触发窗口计算了。

// Flink 中设置事件时间和 Watermark
DataStream<Order> stream = env
    .addSource(kafkaSource)
    .assignTimestampsAndWatermarks(
        WatermarkStrategy.<Order>forBoundedOutOfOrderness(Duration.ofSeconds(5))
            .withTimestampAssigner((event, timestamp) -> event.getEventTime())
    );

// 按事件时间开窗口
stream
    .keyBy(order -> order.getUserId())
    .window(TumblingEventTimeWindows.of(Time.minutes(1)))
    .aggregate(new OrderAggregator());

避坑指南:Watermark 设置太短,大量迟到数据被丢弃;设置太长,窗口迟迟不触发,延迟飙升。我一般建议先设 5-10 秒的乱序容忍度,上线后根据实际迟到率调整。另外,迟到数据要单独处理——用 side output 输出到延迟队列,做补偿计算。

2.5 四个概念如何协同工作?

最后,咱们串一下这四个概念在一条增量计算链路中是怎么配合的:

  1. CDC 从 MySQL binlog 捕获一条订单 UPDATE 事件
  2. 事件被发送到 Kafka,携带事件时间(订单更新时间)
  3. Flink 消费 Kafka,按事件时间分配窗口,Watermark 控制触发时机
  4. 引擎读取状态中该用户的历史聚合值,与新值做增量计算
  5. 更新增量视图(比如实时大屏上的销售额数字)
  6. 定期做 Checkpoint,保存状态快照,保证故障恢复

你看,一环扣一环。任何一个环节出问题,整个链路都会受影响。所以选型时,这四个能力必须一起评估,不能只看单点性能。

一句话总结:CDC 是增量计算的「眼睛」,增量视图是「脸面」,状态管理是「大脑」,时间语义是「心跳」。四者缺一不可。