常见 Schema 变更类型:你一定会遇到的六种情况

做实时同步,说白了就是跟表结构变更打交道。我做了这么多年,几乎每个项目都会碰到。今天咱们就把最常见的六种变更类型掰开揉碎讲清楚。

你想想看,业务在迭代,表结构怎么可能不变?关键是变的时候,你的同步链路能不能扛住。

核心观点:不是所有变更都致命,但每一种都需要你心里有数。

1. 新增列(ADD COLUMN)

这是最温柔的变更。上游加一列,下游如果不在意,基本没影响。

但要注意——新增列有没有默认值,差别很大。

  • 有默认值的新增列:比如 ALTER TABLE users ADD COLUMN age INT DEFAULT 18;。这种变更,CDC 事件里会带上新列和默认值。下游如果没做 schema 映射,直接忽略就行。
  • 无默认值的新增列:比如 ALTER TABLE users ADD COLUMN nickname VARCHAR(50);。MySQL 里会给 NULL,但有些数据库(比如 PostgreSQL)会报错。嗯,这里要注意。

我的习惯:新增列时,尽量给默认值。不是为了下游,而是为了线上 DDL 不锁表。但如果你用 pt-online-schema-change 这类工具,默认值反而不是必须的。

我在项目中遇到过,某次上游加了 20 个列,全是 NULL 默认。下游的 JSON 解析器直接炸了——因为字段太多,内存溢出了。后来我加了个字段白名单,只同步需要的列。

2. 删除列(DROP COLUMN)

这个比新增麻烦。上游删了一列,下游如果还在读这列的数据,就会报错。

举个例子:

-- 上游执行
ALTER TABLE users DROP COLUMN age;

-- 下游还在消费这条数据
{
  "id": 1,
  "name": "张三",
  "age": 25  // 这列已经不存在了
}

为什么会这样?因为 CDC 事件是异步的。上游删列后,下游可能还在消费之前产生的 binlog 事件。如果你用的是 Debezium 这类工具,它会自动感知 schema 变更,但如果你是自己解析 binlog,就得小心了。

避坑指南:我曾经遇到一个案例,上游 DBA 直接 DROP COLUMN,下游 Kafka 消费者还在用固定 schema 反序列化,结果整个消费组卡住了。恢复花了 2 小时。后来我强制要求:删除列必须走灰度流程,先标记废弃,再物理删除。

3. 修改列数据类型(MODIFY COLUMN TYPE)

这是最坑的变更之一。为什么?因为数据类型变了,下游的序列化/反序列化可能直接崩。

原类型 新类型 风险等级 说明
INT BIGINT 低 范围扩大,下游一般能兼容
VARCHAR(50) VARCHAR(100) 低 长度增加,不影响
INT VARCHAR(10) 高 类型体系变了,下游可能解析失败
DECIMAL(10,2) DECIMAL(12,4) 中 精度变化,下游计算可能溢出
DATETIME TIMESTAMP 高 时区处理不同,数据可能偏差

我个人习惯是:数据类型变更,必须走 schema registry 的兼容性检查。Avro 的 BACKWARD 兼容模式能帮你拦截大部分问题。

4. 重命名列(RENAME COLUMN)

这个变更,说实话,在实时同步里是最头疼的。因为 CDC 事件里,列名变了,但下游的映射关系还是旧的。

举个例子:

-- 上游执行
ALTER TABLE users RENAME COLUMN name TO full_name;

-- 下游还在用 name 字段
INSERT INTO target_table (id, name) VALUES (?, ?);  // 报错!name 不存在

为什么难处理?因为重命名列,在 binlog 里不会同时保留新旧两个列名。你只能通过 schema 变更事件来感知。如果你没有监听 schema 变更事件,那下游就彻底断了。

我的建议:如果业务允许,重命名列时,先新增一个同名新列,把数据同步过去,再删除旧列。这样 CDC 链路不会断。虽然麻烦,但安全。

我曾经在一个金融项目里,上游 DBA 直接 RENAME 了主键列。结果下游的 upsert 逻辑全废了,因为主键名变了。那次我加了 3 天班才把数据对齐回来。

5. 修改列默认值(ALTER DEFAULT)

这个变更,很多人觉得不重要。其实不然。

默认值变更,影响的是新写入的数据。已经存在的数据不会变。但如果你下游在做数据回放(比如从 Kafka 重新消费),那默认值不同,可能导致数据不一致。

-- 上游执行
ALTER TABLE users ALTER COLUMN status SET DEFAULT 'active';

-- 下游如果重新消费历史数据
-- 历史数据里 status 可能是 NULL,但新数据是 'active'
-- 下游的合并逻辑需要处理这种差异

嗯,这里要注意:默认值变更,在 CDC 事件里通常不会体现。你只能通过 DDL 事件来感知。所以,如果你依赖默认值做业务判断,一定要在同步链路里加上默认值映射逻辑。

6. 新增/删除表(CREATE/DROP TABLE)

这个变更,在实时同步里属于结构性变更。

  • 新增表:如果你用的是全量+增量同步模式,新增表需要触发一次全量同步。否则,新表的数据永远不会出现在下游。
  • 删除表:下游如果还在消费这张表的数据,会直接报错。而且,删除表后,binlog 里可能还有未消费完的事件。

避坑指南:我曾经遇到一个案例,上游 DBA 删了一张表,但下游的 Flink 任务还在运行。结果 Flink 的 checkpoint 一直失败,因为找不到表的 schema。恢复方法是:手动停掉任务,清理状态,重新启动。那次影响了 3 个小时的数据同步。

我个人习惯是:新增表走审批流程,删除表先 rename 再物理删除。这样至少有个缓冲期。

知识体系总览

下面这张图,把六种变更类型和它们的核心影响整理了一下。你可以保存下来,做方案设计时对照着看。

常见 Schema 变更类型与核心影响 Schema 变更类型 新增列 删除列 修改数据类型 重命名列 修改默认值 新增/删除表 各变更类型的核心影响 • 新增列:下游 schema 需扩展,有默认值则影响小 • 删除列:下游消费可能报错,需提前感知 DDL • 修改数据类型:序列化/反序列化可能失败,需兼容性检查 • 重命名列:CDC 事件列名变化,下游映射需同步更新 • 修改默认值:新老数据默认值不一致,回放时需处理 • 新增/删除表:结构性变更,需触发全量同步或停任务

小结一下

这六种变更类型,你迟早都会遇到。我个人建议:

  • 新增列:给默认值,下游做字段白名单
  • 删除列:先标记废弃,再物理删除
  • 修改数据类型:走 schema registry 兼容性检查
  • 重命名列:先增后删,避免直接 RENAME
  • 修改默认值:下游做默认值映射
  • 新增/删除表:走审批流程,rename 缓冲