3. Schema变更的常见陷阱:锁表、数据丢失、应用兼容性、回滚困难

Schema变更,说白了就是在给飞驰的汽车换轮胎。你想想看,数据库还在线上跑着,业务请求一个接一个,这时候你要改表结构——加字段、删索引、改类型……稍有不慎,就是一场事故。

我做了十几年数据库,见过太多因为Schema变更翻车的案例。今天咱们就把这四个最常见的陷阱掰开揉碎了讲清楚。

3.1 锁表:你以为只是慢,其实是停服

很多新手DBA觉得,加个字段而已,几秒钟的事。嗯,这里要注意——MySQL 5.6之前,ALTER TABLE会锁住整张表,所有读写操作全部排队等着。你以为只是慢?不,是停服。

我在项目中遇到过一家电商公司,大促前夜要加一个优惠券字段。DBA直接执行了ALTER TABLE,结果订单表被锁了整整8分钟。那8分钟里,用户下不了单,客服电话被打爆。最后复盘时发现,那张表有2亿行数据。

锁表的核心风险点:
  • DDL阻塞DML:MySQL的DDL操作在大多数存储引擎下会锁表
  • 数据量越大,锁越久:千万级以上的表,ALTER可能耗时数十分钟
  • 主从延迟:主库执行DDL时,从库也会被阻塞

怎么破?我个人的习惯是:

  • 用pt-online-schema-change:Percona Toolkit的工具,通过触发器同步数据,几乎不锁表
  • 或者用gh-ost:GitHub开源的方案,更安全,支持暂停和恢复
  • 业务低峰期操作:凌晨3点,你懂的
避坑指南:我曾经在白天直接对500万行的用户表执行ADD COLUMN,结果业务报警响了15分钟。从那以后,我所有DDL都走pt-online-schema-change,哪怕只是加个默认值。

3.2 数据丢失:删掉的不是字段,是钱

数据丢失是Schema变更里最要命的陷阱。你以为删掉的是冗余字段,实际上可能是业务逻辑的依赖。

举个例子:

-- 看起来没用的字段
ALTER TABLE orders DROP COLUMN temp_flag;

-- 但某个定时任务还在读这个字段
SELECT * FROM orders WHERE temp_flag = 'pending';

字段删了,查询直接报错。更可怕的是,有些数据是隐式丢失的——比如你修改字段类型,MySQL会自动做隐式转换,精度丢了,数据变了,你都不知道。

操作类型 数据丢失风险 典型案例
DROP COLUMN 高 删除字段后,依赖该字段的代码全部报错
MODIFY COLUMN 中 DECIMAL(10,2) 改成 DECIMAL(8,2),小数位被截断
CHANGE COLUMN 高 重命名字段后,旧代码写入的数据丢失
DROP INDEX 低 索引删除后,查询变慢,但数据不丢
我的铁律:任何删除或修改字段的操作,必须先做三件事——备份、备份、备份。而且备份要能恢复到变更前的任意时间点。

3.3 应用兼容性:新Schema和旧代码打架

这是最容易被忽略的陷阱。你改了数据库,但应用还没更新。新字段旧代码不认识,旧字段新代码找不到——两边一打架,线上就崩。

我记得有一次,一个团队在用户表加了phone_verified字段,类型是TINYINT。但旧代码还在用phone字段做验证,结果新数据写进去,旧代码读不到验证状态,用户全被踢下线。

为什么会这样?因为Schema变更和应用发布是异步的。你不可能让所有服务同时更新。所以必须考虑向前兼容和向后兼容。

兼容性设计原则:
  • 只加不减:新字段只允许NULL或默认值,旧代码不会报错
  • 只增不删:索引、约束只加不删,除非确认无依赖
  • 分步发布:先改数据库,再更新应用,最后清理旧字段

你想想看,如果必须删字段,怎么办?我建议分三步走:

  1. 第一步:应用层停止写入旧字段,但数据库保留
  2. 第二步:确认所有服务都不再依赖旧字段,删除应用代码
  3. 第三步:数据库执行DROP COLUMN

每一步之间,至少间隔一个发布周期。别急,急就出事。

3.4 回滚困难:变更一时爽,回滚火葬场

这是最让人头疼的。Schema变更不像代码回滚,git revert一下就完事。数据库是有状态的,你改了表结构,数据已经写进去了,想回滚?没那么简单。

举个例子:

-- 变更前:表结构
CREATE TABLE users (
  id INT PRIMARY KEY,
  name VARCHAR(50),
  email VARCHAR(100)
);

-- 变更后:加了phone字段
ALTER TABLE users ADD COLUMN phone VARCHAR(20);

-- 数据已经写入了
INSERT INTO users VALUES (1, '张三', 'zhang@example.com', '13800138000');

-- 想回滚?DROP COLUMN phone 可以,但数据丢了

回滚的难点在于:

  • 数据不可逆:新写入的数据包含新字段,回滚后这些数据就丢了
  • 依赖链复杂:其他表可能已经引用了新字段
  • 时间窗口长:变更后运行越久,回滚代价越大
回滚失败的典型案例:我曾经见过一个团队,把订单表的status字段从VARCHAR改成INT。改完后跑了三天,发现新代码有bug。想回滚?三天里写入的100万条订单,status全是数字,回滚成VARCHAR后全变成乱码。最后只能加班写脚本一个一个修复。

怎么避免?我个人的习惯是:

  • 变更前先设计回滚方案:不是等出问题了再想,而是变更前就写好回滚脚本
  • 用版本号管理Schema:每个变更都对应一个版本,回滚就是执行上一个版本的DDL
  • 保留变更前的快照:用mysqldump或pt-archiver备份数据

3.5 一张图看懂四个陷阱

下面这张图,把四个陷阱的关系和应对策略串起来了。我建议你保存下来,每次做Schema变更前看一眼。

Schema变更四大陷阱与应对策略 Schema变更 锁表 数据丢失 应用兼容性 回滚困难 pt-online-schema-change 备份+版本管理 分步发布+兼容设计 回滚脚本+快照 核心原则 变更前先想好怎么回滚,变更中保证兼容,变更后验证数据 永远不要在业务高峰期做Schema变更

3.6 总结一下

四个陷阱,每个都能让你线上翻车。但说白了,只要记住几个原则:

  • 锁表:用在线DDL工具,别裸跑ALTER
  • 数据丢失:先备份,再操作,最后验证
  • 应用兼容性:只加不减,分步发布
  • 回滚困难:变更前就写好回滚脚本

嗯,这些经验都是用血泪换来的。希望你能少踩几个坑。