3. 增量触发机制:变更捕获、事件驱动与增量标记算法
增量计算的核心问题其实就一个:怎么知道哪些数据变了?
我刚开始做调度系统的时候,觉得这问题很简单——全量重算不就完了?后来数据量从百万涨到亿级,调度延迟从秒级变成小时级,我才意识到:增量触发才是调度系统的灵魂。没有它,你根本撑不住大规模场景。
这一章,咱们就聊聊增量触发的三种主流手段:变更捕获、事件驱动、增量标记算法。它们不是互斥的,实际项目中往往是组合使用。
3.1 变更捕获:从源头感知变化
变更捕获,说白了就是盯着数据源,看它什么时候变了。我把它分成两类:
| 类型 | 原理 | 典型场景 | 延迟 |
|---|---|---|---|
| 轮询式 | 定时扫描数据源,对比状态 | 数据库表、文件系统 | 秒~分钟级 |
| 监听式 | 订阅数据源的变更事件 | CDC、消息队列、日志 | 毫秒~秒级 |
轮询式实现简单,但有个硬伤:你永远不知道两次扫描之间发生了什么。我见过一个项目,轮询间隔设了30秒,结果用户修改数据后等了半分钟才触发计算,业务方直接炸了。
监听式就好很多。比如MySQL的binlog CDC(Change Data Capture),每次数据变更都会产生一条日志,你只要消费它就行。嗯,这里要注意:CDC的消费顺序必须严格保证,否则增量计算的结果会乱掉。
我的经验:能上监听就别用轮询。如果实在要用轮询,间隔不要超过5秒,并且加上版本号或时间戳做增量对比,别每次都全量扫描。
3.2 事件驱动:让数据自己说话
变更捕获解决了"数据变了"的问题,但谁来告诉调度系统"该干活了"?这就是事件驱动的用武之地。
事件驱动的核心模型很简单:
数据变更 → 产生事件 → 事件总线 → 消费者(调度器) → 触发计算
我习惯把事件分成三个级别:
- 数据事件:某条记录被增删改。粒度最细,适合实时计算。
- 批次事件:一批数据完成写入。适合离线调度。
- 状态事件:某个任务的状态发生变化(比如从"等待"变成"就绪")。这是调度系统内部用的。
实际项目中,我建议用事件总线来解耦生产者和消费者。Kafka、Pulsar、RabbitMQ都行,关键是要保证至少一次投递和顺序消费。为什么?因为增量计算对顺序敏感——你先改了A再改B,和先改B再改A,结果可能完全不同。
我曾经踩过的坑:用Kafka做事件总线时,分区数设少了,导致同一个数据源的事件被分到不同分区,消费端乱序处理,增量计算结果全错了。后来我把分区键设成数据源ID,才解决这个问题。
3.3 增量标记算法:精准定位变化范围
变更捕获和事件驱动告诉你"有变化",但没告诉你"变化了多少"。如果每次触发都把整个数据集重算一遍,那跟全量计算没区别。
增量标记算法要解决的就是这个问题:只标记真正变化的部分,然后只计算这些部分。
我常用的三种标记算法:
| 算法 | 原理 | 适用场景 | 空间开销 |
|---|---|---|---|
| 时间戳标记 | 每条数据记录最后修改时间 | 有统一时间源的系统 | 低 |
| 版本号标记 | 每条数据附带递增版本号 | 分布式环境、无全局时钟 | 低 |
| 位图标记 | 用位图记录每个分片是否变化 | 分片计算、大规模数据 | 极低 |
时间戳标记最直观。但有个问题:分布式系统里,不同机器的时钟可能不同步。你想想看,A机器改了数据,时间戳是10:00:01,B机器改了另一条,时间戳是10:00:00,结果B的变更反而被忽略了——因为调度器认为"只处理10:00:00之后的数据"。
版本号标记就避免了这个问题。每个数据源维护一个全局递增的版本号,每次变更版本号+1。调度器只需要记住"上次处理到版本号N",下次从N+1开始处理就行。我在项目中用Redis的INCR命令实现过,简单可靠。
位图标记是我个人比较喜欢的方式。假设你有1000个数据分片,每个分片用一个bit表示"是否变化"。整个标记只需要125字节。调度器扫描位图,找到所有为1的bit,只处理对应的分片。处理完后把bit清0。这种方式在大规模场景下效率极高。
我的建议:如果数据量在百万级以下,时间戳或版本号就够了。如果数据量在亿级以上,位图标记是更好的选择。我曾经在某个广告系统里用位图标记处理了10亿条用户画像数据,增量计算延迟从30分钟降到了2分钟。
3.4 三种机制的协同工作
这三种机制不是孤立的。实际系统中,它们通常这样配合:
- 变更捕获负责从数据源感知变化(比如监听binlog)
- 事件驱动把变化包装成事件,投递到事件总线
- 增量标记算法在调度器内部维护变化标记,决定哪些任务需要重新计算
举个例子:用户修改了订单表的一条记录。CDC捕获到这条变更,产生一个"订单更新"事件。调度器收到事件后,检查增量标记位图,发现"订单分片3"被标记为"已变化"。于是调度器只触发"订单分片3"对应的计算任务,其他分片保持不变。
你看,整个过程环环相扣,缺一个都不行。
核心原则:变更捕获要快,事件驱动要稳,增量标记要准。快、稳、准,三者缺一不可。
3.5 增量触发机制整体架构
下面这张图展示了三种机制的协作关系:
从这张图你能看到,数据从源头到最终计算,经历了完整的链路。每个环节都有自己的职责,相互配合才能实现高效的增量触发。
3.6 避坑指南
最后,分享几个我踩过的坑:
- 不要过度捕获:变更捕获的粒度太细,会导致事件风暴。比如每秒几万次数据变更,每个都产生事件,调度器根本扛不住。我建议做窗口聚合,比如100ms内的事件合并成一个。
- 事件去重很重要:消息队列的"至少一次投递"意味着同一个事件可能被消费多次。增量计算如果重复执行,结果会出错。我习惯在事件里加一个唯一ID,消费端做幂等处理。
- 增量标记要持久化:如果调度器挂了重启,增量标记丢了,那恢复后只能全量重算。我把标记存在Redis或ZooKeeper里,保证重启后还能接着干。
- 小心"幽灵变更":有时候数据没变,但CDC误报了变更。比如MySQL的binlog在表结构变更时会产生大量伪事件。我建议在增量标记算法里加一个校验步骤,对比前后数据是否真的不同。
我曾经犯过的错:有个项目上线后,增量计算一直正常,突然有一天调度器疯狂触发任务。查了半天,发现是某个开发在测试环境执行了ALTER TABLE,binlog产生了全表级别的伪变更事件,被CDC捕获后当成真实变更投递了。从那以后,我强制要求所有CDC链路必须加数据内容校验,只处理真正有数据变化的变更。