数据同步基础:全量与增量协同的起点

做数据同步,说白了就是解决「怎么把数据从 A 点搬到 B 点」的问题。我做了这么多年数据架构,见过太多团队上来就搞 CDC、搞实时流,结果跑了一个月发现数据对不上,最后灰头土脸地回退到全量同步。嗯,这里有个关键认知:全量和增量不是二选一,而是搭档。

这一章,我们先把地基打牢。我会从最朴素的全量同步讲起,逐步深入到 CDC 和日志解析。你想想看,如果连「数据怎么来的」都说不清楚,后面谈增量计算和全量计算的协同,那就是空中楼阁。

全量同步原理

全量同步,顾名思义,就是把源端的数据全部复制一份到目标端。听起来简单,但坑不少。

我个人的习惯是,把全量同步分为三个步骤:

  1. 快照读取:在某个时间点,把源表的所有数据读出来。
  2. 数据清洗与转换:处理空值、格式不一致、主键冲突等问题。
  3. 批量写入:把数据写入目标表,通常是先 truncate 再 insert。

这里有个经典问题:全量同步期间,源数据还在变怎么办?

我在项目中遇到过这种情况——凌晨跑全量同步,跑了两个小时,结果源表被业务改了 10 万条记录。同步完一查,目标端的数据既不是快照时刻的状态,也不是最新状态,而是「中间态大杂烩」。这其实就是一致性边界没控制好。

避坑指南:我曾经在一个金融项目中,因为全量同步没加锁,导致对账系统跑出来差了 300 多万。后来我们强制要求:全量同步必须在业务低峰期,且对源表加共享锁(如果业务允许),或者使用数据库的快照隔离级别。

全量同步的适用场景很明确:

  • 首次数据迁移
  • 数据量较小(百万级以内)
  • 对实时性要求不高
  • 需要做数据全量对账

增量同步原理

增量同步,只同步发生变化的数据。它的核心是「如何识别变化」。

常见的增量同步方式有三种:

方式 原理 优点 缺点
时间戳/版本号 表中有一个 update_time 或 version 字段,同步时只取大于上次同步时间的记录 实现简单,对数据库无侵入 依赖业务字段;删除操作无法感知;时间精度可能不够
触发器 在源表上创建 INSERT/UPDATE/DELETE 触发器,将变更记录写入日志表 能捕获所有 DML 操作 对源库性能有影响;维护成本高
CDC(变更数据捕获) 解析数据库的事务日志或归档日志 无侵入、低延迟、能捕获所有变更 技术复杂度高;不同数据库实现差异大

我个人最推荐的是 CDC,但前提是你得把日志解析机制搞明白。别急,后面会细讲。

小技巧:如果你用时间戳方式做增量,记得处理「时钟回拨」问题。我曾经在跨机房同步时遇到过,源库的服务器时间被 NTP 往回调了 2 秒,结果漏掉了 1000 多条记录。解决方案是:用单调递增的全局事务 ID 替代时间戳。

CDC 技术简介

CDC,全称 Change Data Capture。它的核心思想是:不直接查业务表,而是读数据库的变更日志。

为什么这么做?你想想看,如果每秒有 10 万次写入,你用 SELECT 去轮询业务表,数据库扛得住吗?肯定扛不住。但日志是数据库本来就有的,读日志几乎不增加额外负载。

CDC 的主流实现方式有两种:

  • 基于日志的 CDC:解析 MySQL 的 binlog、PostgreSQL 的 WAL、Oracle 的 Redo Log。这是最推荐的方式。
  • 基于查询的 CDC:定期执行 SELECT 语句,对比前后数据快照。这种方式对数据库压力大,只适合小数据量。

我记得有一次,一个团队用基于查询的 CDC 做实时同步,每 5 秒查一次全表。结果业务高峰期,数据库 CPU 直接飙到 95%。后来我帮他们改成基于日志的 CDC,CPU 降到了 15%。

核心原则:能读日志,就别查表。日志是数据库的「黑匣子」,里面记录了所有变更的完整信息。

日志解析机制

日志解析是 CDC 的「心脏」。我们以 MySQL 的 binlog 为例,讲清楚它到底怎么工作的。

MySQL 的 binlog 有三种格式:

格式 记录内容 特点
STATEMENT 记录执行的 SQL 语句 体积小,但某些 SQL(如 NOW())会导致主从不一致
ROW 记录每一行数据变更前后的值 体积大,但最精确,推荐使用
MIXED 混合模式,默认用 STATEMENT,遇到不确定 SQL 自动切 ROW 折中方案,但不够可控

我个人强烈建议:做 CDC 同步,binlog 格式必须用 ROW。为什么?因为只有 ROW 格式能告诉你「这一行数据原来是什么,现在变成了什么」。STATEMENT 格式只告诉你「执行了 UPDATE ... WHERE id=1」,但不知道改之前的值,这对增量计算来说信息量不够。

日志解析的基本流程是这样的:

  1. 连接源库:模拟一个从库,向主库请求 binlog 流。
  2. 读取事件:binlog 由一系列事件组成,比如 Query_event、Rows_event、Xid_event。
  3. 解析事件:从 Rows_event 中提取出表名、列名、变更前值、变更后值。
  4. 序列化输出:将解析结果转为 JSON 或 Avro 格式,发送到消息队列(如 Kafka)。

下面我用一个简单的伪代码,展示解析 Rows_event 的核心逻辑:

// 伪代码:解析 MySQL binlog Rows_event
function parseRowsEvent(event) {
    // 1. 获取表元数据(表名、列名、列类型)
    tableMeta = getTableMeta(event.tableId);
    
    // 2. 遍历每一行变更
    for (row in event.rows) {
        change = {
            table: tableMeta.name,
            type: row.type,  // INSERT / UPDATE / DELETE
            before: {},      // 变更前的值(DELETE 和 UPDATE 有)
            after: {}        // 变更后的值(INSERT 和 UPDATE 有)
        };
        
        // 3. 按列解析
        for (i = 0; i < tableMeta.columns.length; i++) {
            col = tableMeta.columns[i];
            if (row.type == 'UPDATE' || row.type == 'DELETE') {
                change.before[col.name] = row.beforeValues[i];
            }
            if (row.type == 'UPDATE' || row.type == 'INSERT') {
                change.after[col.name] = row.afterValues[i];
            }
        }
        
        // 4. 输出到消息队列
        kafka.send('cdc_topic', change);
    }
}

注意:binlog 解析有个大坑——表结构变更。如果你在同步过程中改了表结构(比如新增一列),而解析器没有同步更新元数据,那解析出来的数据就会错位。我曾经因为这个原因,把「用户余额」解析成了「用户昵称」,差点酿成生产事故。解决方案是:监听 DDL 事件,动态刷新表元数据缓存。

为了让你更直观地理解全量和增量的关系,我画了一张图:

数据同步核心逻辑:全量 + 增量协同 源数据库 全量快照读取 CDC 日志解析 数据清洗 & 转换 去重 / 合并 / 校验 消息队列 (Kafka) 目标数据库 全量:首次或定期全量同步 增量:实时 CDC 流式同步

这张图展示了全量和增量在数据同步中的协作关系。全量负责「铺底」,增量负责「持续跟进」。两者通过数据清洗层和消息队列衔接,最终写入目标库。

小结

这一章我们聊了数据同步的四个核心知识点:

  • 全量同步:简单直接,但要处理好一致性和锁的问题。
  • 增量同步:核心是识别变化,时间戳、触发器、CDC 各有优劣。
  • CDC 技术:基于日志的 CDC 是业界最佳实践,无侵入、低延迟。
  • 日志解析:以 MySQL binlog 为例,ROW 格式是首选,注意处理 DDL 变更。