数据库改个字段而已,为什么总能把线上系统搞崩?

举报
Echo_Wish 发表于 2026/08/31 08:04:47 2026/08/31
【摘要】 数据库改个字段而已,为什么总能把线上系统搞崩?

数据库改个字段而已,为什么总能把线上系统搞崩?

数据库 Schema 变更,真正难的从来不是“怎么改”,而是“怎么在业务不停机的情况下改”。

大家好,我是 Echo_Wish

做运维久了,你会发现一个特别有意思的现象:

代码上线,大家已经越来越习惯灰度、回滚、监控、限流。

但一说到数据库变更,很多团队还是:

“改个字段嘛,停一下服务不就行了?”

然后凌晨两点,业务群突然开始疯狂弹消息:

“数据库连接超时了。”

“接口怎么全部 500 了?”

“谁在执行 ALTER TABLE?”

最后发现,罪魁祸首就是一条看起来平平无奇的 SQL:

ALTER TABLE user_info
ADD COLUMN phone VARCHAR(20);

这篇文章就聊一个非常现实的话题:

数据库运维自动化,到底应该怎么做 Schema 在线变更,以及如何保证新旧代码兼容?


一、Schema 变更最大的坑,不是 SQL 写错

很多人理解数据库变更,思路特别简单:

修改表结构
    ↓
发布代码
    ↓
完成

但在线业务真正面对的是:

旧代码 ───────┐
              ├──→ 数据库
新代码 ───────┘

也就是说,在发布过程中,数据库面对的可能不是一套代码,而是两套甚至多套代码同时运行

比如原来的表:

CREATE TABLE user_info (
    id BIGINT PRIMARY KEY,
    name VARCHAR(50)
);

现在产品要求增加手机号。

最直接的方案:

ALTER TABLE user_info
ADD COLUMN phone VARCHAR(20) NOT NULL;

看起来没问题。

但如果线上还有旧版本代码,它执行:

INSERT INTO user_info(id, name)
VALUES (1001, '张三');

数据库就可能直接报错。

问题来了:

明明只是增加一个字段,为什么旧代码也挂了?

因为数据库 Schema 本质上也是一个 API。

这一点,我认为很多团队都低估了。

代码接口有兼容性要求:

API v1
API v2

数据库同样应该有:

Schema v1
Schema v2

而且数据库往往比 HTTP API 更难改。


二、真正靠谱的方案:扩展,而不是破坏

在线 Schema 变更有一个非常重要的思想:

先扩展,再迁移,最后收缩。

也就是我们经常说的:

Expand → Migrate → Contract

这个思想看起来简单,但它几乎可以解决大量数据库发布事故。


三、第一阶段:Expand,先把新结构加进去

比如我们准备增加:

phone

第一步不要急着:

ALTER TABLE user_info
ADD phone VARCHAR(20) NOT NULL;

而应该优先:

ALTER TABLE user_info
ADD phone VARCHAR(20) NULL;

为什么允许 NULL?

因为:

旧代码根本不知道这个字段存在。

旧代码继续:

INSERT INTO user_info(id, name)
VALUES (1001, '张三');

依然可以工作。

与此同时,新代码已经可以开始使用:

SELECT id, name, phone
FROM user_info;

这就形成了一个非常重要的状态:

数据库:
┌────────────────────┐
│ id                 │
│ name               │
│ phone              │ ← 新字段
└────────────────────┘

旧代码 → 只认识 id/name
新代码 → 认识 id/name/phone

数据库已经升级,但旧代码仍然能够正常运行。

这就是兼容性的核心。


四、第二阶段:Migrate,把历史数据慢慢迁过去

字段加好了,并不意味着工作结束。

假设之前已经有:

1000 万用户

现在新增 phone 字段。

你如果直接:

UPDATE user_info
SET phone = 'xxx';

那线上数据库可能直接开始“喘气”。

尤其是大表。

所以数据迁移也必须在线化。

例如:

UPDATE user_info
SET phone = ...
WHERE id > 100000
  AND id <= 110000;

然后循环执行:

10万
10万
10万
10万
...

而不是一次性:

UPDATE user_info
SET phone = ...;

五、为什么数据库自动化不能只自动执行 SQL?

这是我比较想强调的一点。

很多所谓的“数据库自动化平台”,本质上就是:

输入 SQL
↓
执行 SQL
↓
返回结果

这其实不叫数据库运维自动化。

最多只能叫:

SQL 自动执行器。

真正的数据库自动化应该知道:

这张表有多大?
当前 QPS 多高?
有没有长事务?
有没有锁等待?
当前连接数多少?
这个 SQL 会不会锁表?
执行预计影响多少行?
当前是不是业务高峰?
失败以后怎么回滚?

比如我们提交:

ALTER TABLE orders
ADD COLUMN delivery_address VARCHAR(500);

系统至少应该自动检查:

表名:orders
数据量:1.2 亿

当前 QPS:8600

活跃事务:37

长事务:2

数据库负载:72%

DDL 风险:中

建议:
❌ 当前不建议执行

这才叫真正意义上的:

数据库运维自动化。


六、第三阶段:新旧代码双写

这是 Schema 兼容里面非常经典的一招。

例如原来只有:

username

现在准备改成:

user_name

千万不要直接:

ALTER TABLE user_info
CHANGE username user_name VARCHAR(50);

因为旧代码还在使用:

SELECT username
FROM user_info;

直接改名:

旧代码瞬间报错。

更稳妥的方式是:

ALTER TABLE user_info
ADD COLUMN user_name VARCHAR(50);

然后新代码开始:

写 username
写 user_name

也就是双写:

user.setUsername(username);
user.setUserName(username);

读取的时候,新代码优先读取:

String name = user.getUserName();

if (name == null) {
    name = user.getUsername();
}

经过一段时间以后:

旧代码
    ↓
username

新代码
    ↓
user_name

数据逐渐完成迁移

最终才删除旧字段。


七、这就是“Expand → Migrate → Contract”

把整个过程串起来,其实特别清晰。

第一步:Expand

ALTER TABLE user_info
ADD COLUMN user_name VARCHAR(50);

第二步:双写

username
   ↓
┌───────────────┐
│ username      │
│ user_name     │
└───────────────┘

第三步:历史数据迁移

UPDATE user_info
SET user_name = username
WHERE user_name IS NULL
LIMIT 10000;

循环执行。

第四步:新代码完全切换

读取 user_name
写入 user_name

第五步:停止旧字段写入

username ← 不再使用

第六步:Contract

最后才:

ALTER TABLE user_info
DROP COLUMN username;

这时候,旧代码已经彻底退出。

删除字段才是最后一步,而不是第一步。


八、再说一个更容易踩坑的问题:字段删除

很多开发喜欢这样操作:

ALTER TABLE user_info
DROP COLUMN old_field;

然后代码:

user.getOldField();

线上直接:

Unknown column 'old_field'

所以数据库 Schema 变更里面有一个特别重要的原则:

新增字段通常容易兼容,删除字段通常最危险。

因为:

ADD

属于扩展。

而:

DROP
RENAME
TYPE CHANGE

往往属于破坏性变更。

因此数据库自动化平台最好能够自动识别:

ADD COLUMN       → 低风险
ADD INDEX        → 中风险
MODIFY COLUMN    → 高风险
DROP COLUMN      → 极高风险
RENAME COLUMN    → 极高风险

然后采用不同审批策略。


九、索引变更,同样不能想当然

很多线上事故不是因为字段,而是:

加索引。

开发发现:

SELECT *
FROM orders
WHERE user_id = 10001;

很慢。

于是:

CREATE INDEX idx_user_id
ON orders(user_id);

然后:

线上开始抖。

因为在某些数据库和版本、表规模、执行方式下,创建索引可能产生明显的 I/O、CPU 或锁竞争。

所以自动化系统不能只判断:

SQL 是 CREATE INDEX
↓
执行

而应该:

CREATE INDEX
↓
检查数据库类型
↓
检查版本
↓
检查表规模
↓
检查当前负载
↓
判断是否支持 Online DDL
↓
生成执行计划
↓
执行
↓
监控

例如 MySQL 环境下,可以结合具体版本和存储引擎评估:

ALTER TABLE orders
ADD INDEX idx_user_id(user_id),
ALGORITHM=INPLACE,
LOCK=NONE;

但这里也不要形成一个误区:

看到 LOCK=NONE 就认为绝对不会阻塞。

数据库的实际行为还受到具体版本、操作类型、存储引擎、并发事务等因素影响。

所以:

Online DDL ≠ 零风险 DDL。


十、自动化平台最应该做的,其实是“刹车”

我一直觉得:

数据库自动化最重要的能力,不是油门,而是刹车。

比如有人提交:

ALTER TABLE big_table
MODIFY COLUMN amount DECIMAL(10,2);

平台发现:

表数据量:8.7 亿

当前 QPS:12000

数据库 CPU:82%

存在长事务:3

DDL 类型:字段类型修改

风险等级:高

这时候最好的自动化结果不是:

正在执行……

而应该:

❌ 拒绝自动执行

原因:
1. 超大表
2. 高峰期
3. 字段类型发生变化
4. 存在长事务

建议:
低峰期执行
或采用在线 Schema 变更工具

能够拒绝危险操作,本身就是自动化能力。


十一、把 Schema 变更真正纳入 CI/CD

如果团队规模再大一点,我建议直接把数据库变更纳入发布流水线。

例如:

Git
 │
 ├── application
 │
 └── migrations
        │
        ↓
   Schema Analyzer
        │
        ↓
   Compatibility Check
        │
        ↓
   Risk Assessment
        │
        ├── LOW ─────→ 自动执行
        │
        ├── MEDIUM ──→ 人工审批
        │
        └── HIGH ────→ 禁止自动执行
                         │
                         ↓
                       DBA

数据库变更文件也可以进入 Git:

db/
├── V001__create_user.sql
├── V002__add_phone.sql
├── V003__add_user_name.sql
└── V004__create_user_index.sql

这样数据库就不再是:

“谁在生产库上手工改了什么?”

而变成:

“Git 里记录了这次 Schema 演进。”

这才是真正可审计、可追踪的数据库运维。


十二、我最推荐的一套线上 Schema 变更原则

如果让我给团队制定规则,我会简单粗暴地定下面几条。

规则 1:禁止直接修改生产 Schema

不要:

开发人员
    ↓
Navicat
    ↓
生产数据库

而应该:

Git
 ↓
Review
 ↓
自动检查
 ↓
审批
 ↓
执行

规则 2:新增优先,删除滞后

优先:

ADD COLUMN

谨慎:

MODIFY COLUMN

最后:

DROP COLUMN

规则 3:代码必须兼容两个 Schema 版本

发布过程中:

旧代码 + 新 Schema

必须能够运行。

同时:

新代码 + 旧 Schema

最好也能够运行,至少要通过明确设计避免发布窗口中的不兼容。


规则 4:大表禁止“一把梭”

不要:

UPDATE 1亿条数据;

应该:

分批
 ↓
限速
 ↓
监控
 ↓
异常暂停
 ↓
继续

规则 5:自动化必须有“风险等级”

例如:

LOW
 └── ADD NULL COLUMN

MEDIUM
 └── ADD INDEX

HIGH
 ├── MODIFY COLUMN
 ├── RENAME COLUMN
 └── 大表 DDL

CRITICAL
 └── DROP COLUMN

不同等级使用不同审批机制。


十三、最后聊聊我的一个感受

数据库运维发展到现在,真正高级的能力已经不是:

“我会写 SQL。”

而是:

“我能让数据库在业务不停机的情况下安全地演进。”

这两句话,看起来差不多,实际上差得非常远。

以前我们习惯:

数据库是基础设施
代码依赖数据库
数据库由 DBA 管

现在越来越应该把数据库 Schema 当成:

代码的一部分

它需要:

版本管理
自动检测
兼容性分析
灰度发布
风险控制
审计
监控
回滚策略

尤其是微服务、Kubernetes、DevOps 体系越来越成熟以后,应用可以秒级发布,数据库却不能再靠人工凌晨两点改表。

所以我认为未来真正成熟的数据库运维平台,不应该只是帮你“执行 SQL”。

它应该在你提交 SQL 的时候告诉你:

这条 SQL 能不能执行。

在执行之前告诉你:

现在是不是执行的好时机。

执行过程中告诉你:

数据库有没有出现异常。

执行完成以后告诉你:

Schema、代码和数据是不是已经完成迁移。

甚至在你准备删除字段的时候直接拦住:

“等等,还有 3 个服务正在使用这个字段。”

这才是我理解的:

数据库运维自动化。

说到底,Schema 变更不是一次 SQL 操作,而是一次线上系统的“渐进式升级”。

真正靠谱的团队,不是从来不改数据库。

而是:

数据库天天改,业务照样跑;代码持续发布,线上依然稳。

这才是数据库运维自动化真正应该追求的终点。


Echo_Wish|专注云原生、DevOps、数据库与 AI 运维实践

别把数据库当成“存数据的地方”。
它其实是整个业务系统最不能随便动的核心基础设施。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。