数据同步基础:全量与增量协同的起点
做数据同步,说白了就是解决「怎么把数据从 A 点搬到 B 点」的问题。我做了这么多年数据架构,见过太多团队上来就搞 CDC、搞实时流,结果跑了一个月发现数据对不上,最后灰头土脸地回退到全量同步。嗯,这里有个关键认知:全量和增量不是二选一,而是搭档。
这一章,我们先把地基打牢。我会从最朴素的全量同步讲起,逐步深入到 CDC 和日志解析。你想想看,如果连「数据怎么来的」都说不清楚,后面谈增量计算和全量计算的协同,那就是空中楼阁。
全量同步原理
全量同步,顾名思义,就是把源端的数据全部复制一份到目标端。听起来简单,但坑不少。
我个人的习惯是,把全量同步分为三个步骤:
- 快照读取:在某个时间点,把源表的所有数据读出来。
- 数据清洗与转换:处理空值、格式不一致、主键冲突等问题。
- 批量写入:把数据写入目标表,通常是先 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」,但不知道改之前的值,这对增量计算来说信息量不够。
日志解析的基本流程是这样的:
- 连接源库:模拟一个从库,向主库请求 binlog 流。
- 读取事件:binlog 由一系列事件组成,比如 Query_event、Rows_event、Xid_event。
- 解析事件:从 Rows_event 中提取出表名、列名、变更前值、变更后值。
- 序列化输出:将解析结果转为 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 各有优劣。
- CDC 技术:基于日志的 CDC 是业界最佳实践,无侵入、低延迟。
- 日志解析:以 MySQL binlog 为例,ROW 格式是首选,注意处理 DDL 变更。