引言
数据库 Schema 的变更是后端开发中最危险的操作之一——一个错误的 ALTER TABLE 可能锁表数小时,导致线上服务不可用。迁移(Migration)系统把 Schema 变更纳入版本控制,让数据库结构像代码一样可追溯、可回滚、可在多环境复现。本文从迁移系统原理出发,深入到架构设计(范式、索引、分区)、多环境管理、零停机部署策略与大表在线变更——给 PHP 团队一套数据库治理的工程手册。
前置:/php-mysql-database/(MySQL 优化)、/php-laravel-internals/(框架深入)。
目录
- 1. 迁移系统原理:从手写 SQL 到版本控制
- 2. Laravel Migrations 实战
- 3. 数据库架构设计:范式与反范式
- 4. 索引策略与性能
- 5. 多环境管理:开发测试生产同步
- 6. 回滚与数据修复策略
- 7. 零停机 Schema 变更
- 8. 大表在线变更:pt-online-schema-change 与 gh-ost
- 9. CI/CD 中的迁移自动化
- 10. 速查表与一句话记忆
- 延伸阅读
1. 迁移系统原理:从手写 SQL 到版本控制
1.1 为什么不用手写 SQL
# 问题:
开发改了本地表结构 → 忘了同步给同事 → 同事报错
上线前 DBA 跑脚本 → 顺序错了/漏了/重复了 → 线上故障
回滚时凭记忆手写 DROP → 删错列/丢数据
# 迁移系统解决:
版本化:每个变更一个文件,带时间戳/序号
可重复:php artisan migrate 任意次幂等
可回滚:php artisan migrate:rollback 撤销最近一批
多环境:同一份迁移文件在 dev/test/prod 跑一样结果
1.2 主流迁移工具
| 工具 | 框架 | 特点 |
|---|---|---|
| Laravel Migrations | Laravel | Eloquent Schema Builder,PHP DSL |
| Doctrine Migrations | Symfony/Doctrine | 成熟,支持多种数据库 |
| Phinx | 独立 | CakePHP/RoR 风格,多数据库 |
| Flyway | Java/通用 | 纯 SQL 迁移,企业级 |
| Liquibase | Java/通用 | XML/YAML/JSON 定义,跨语言 |
记忆 迁移系统把 Schema 变更版本化——每个变更一个文件、可重复运行、可回滚、多环境一致;Laravel/Doctrine/Phinx 是 PHP 主流,Flyway/Liquibase 是跨语言企业级。
2. Laravel Migrations 实战
2.1 基本操作
php artisan make:migration create_posts_table
php artisan migrate # 执行未运行的迁移
php artisan migrate:rollback # 回滚最近一批
php artisan migrate:rollback --step=3 # 回滚 3 批
php artisan migrate:fresh --seed # 重建+填充(开发用)
php artisan migrate:status # 查看迁移状态
2.2 Schema Builder 示例
public function up(): void {
Schema::create('posts', function (Blueprint $table) {
$table->id(); # 自增主键
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('title', 255);
$table->text('content');
$table->enum('status', ['draft', 'published'])->default('draft');
$table->timestamp('published_at')->nullable();
$table->timestamps(); # created_at + updated_at
$table->softDeletes(); # deleted_at
$table->index(['status', 'published_at']);
$table->fullText('content'); # MySQL 8+
});
}
public function down(): void {
Schema::dropIfExists('posts');
}
2.3 迁移命名规范
# 格式:动作_表名_补充说明
# 好:create_posts_table、add_slug_to_posts、create_tags_posts_pivot
# 坏:update_db、fix_something
# 原则:看到文件名就知道做了什么
记忆 Laravel migrate 基础——make 创建、migrate 执行、rollback 回滚、status 看状态;Schema Builder 用 PHP DSL 而非手写 SQL,支持索引/全文/软删;命名用「动作_表名_说明」结构。
3. 数据库架构设计:范式与反范式
3.1 三范式回顾
1NF:原子性,每列不可再分
2NF:1NF + 非主键列完全依赖主键(不是部分)
3NF:2NF + 非主键列不传递依赖(不依赖其他非主键)
# 示例:订单表
# 坏:orders(id, customer_name, customer_address, product_name, product_price)
# 好:orders(id, customer_id, product_id, qty) + customers + products
3.2 什么时候反范式
读多写少:冗余字段减少 JOIN(如订单快照存商品名)
报表聚合:物化视图/汇总表(每日统计预计算)
搜索优化:ES 索引冗余存储(标题+摘要+标签)
# 原则:先范式保证一致性,再按查询模式反范式优化
3.3 字段设计原则
# 用合适的数据类型:INT(11) vs BIGINT vs UNSIGNED
# 日期用 DATE/DATETIME/TIMESTAMP(不要用字符串)
# 金额用 DECIMAL(19,4)(不要用 FLOAT/DOUBLE)
# 状态用 TINYINT / ENUM(MySQL 8+)或 lookup 表
# 软删必选:deleted_at(物理删除是灾难)
# JSON 字段:MySQL 5.7+ / MariaDB 10.2+ 支持,但别当文档库用
记忆 先范式保一致性(1NF 原子、2NF 完全依赖、3NF 无传递依赖),再按读多写少/报表/搜索反范式优化;字段设计——金额用 DECIMAL、日期用 TIMESTAMP、状态用 TINYINT/ENUM、软删必选 deleted_at。
4. 索引策略与性能
4.1 索引类型
B-Tree(默认):等值/范围/排序/最左前缀
Hash:等值查找(MEMORY 引擎,InnoDB 不直接支持)
Full-Text:文本搜索(MySQL 8+ 中文支持改善)
Spatial:地理空间(MySQL 5.7+)
4.2 索引设计原则
# WHERE/JOIN/ORDER BY 的字段优先建索引
# 最左前缀:复合索引 (a,b,c) 能支持 a / a,b / a,b,c
# 选择性高的字段放前面(性别选择性低,不适合单独索引)
# 避免冗余:已有 (a,b),不必单独建 (a)
# 覆盖索引:查询字段全在索引里 → 不回表
# 写了索引要验证:EXPLAIN 看是否用上
4.3 索引维护
# 大表建索引会锁表 → 用 pt-online-schema-change 或 gh-ost
# 定期 ANALYZE TABLE 更新统计信息
# 删除未使用的索引(performance_schema 查)
# 索引碎片 → OPTIMIZE TABLE(或大表用 ALTER ENGINE=INNODB)
记忆 索引选型——B-Tree 通用、Full-Text 文本搜索、Spatial 地理;设计原则 WHERE/JOIN/ORDER BY 优先、最左前缀、高选择性在前、覆盖索引不回表;大表改索引用在线工具防锁表。
5. 多环境管理:开发、测试、生产同步
5.1 环境配置分离
# .env.development / .env.testing / .env.production
DB_HOST、DB_DATABASE、DB_USERNAME 按环境配置
# 生产数据库单独用户(最小权限原则)
5.2 数据同步策略
# 结构:迁移文件版本控制 → 所有环境跑同一套迁移
# 数据:
# - 开发:seeders + factories 生成假数据
# - 测试:每个测试用 DatabaseTransactions 回滚
# - 生产:不直接同步数据,用导出/ETL/消息队列
# 敏感数据:生产数据脱敏后才能到开发环境(GDPR/隐私法)
5.3 迁移执行顺序
# 原则:先建表 → 再改表(加字段/索引)→ 再数据迁移 → 再删旧字段
# 禁止:在迁移中做业务逻辑(如遍历全表改数据)→ 用专门的 Artison Command
# CI:migrate 在部署流程的早期执行(代码依赖新表/字段)
记忆 多环境——结构靠迁移文件同步、数据靠 seeders(开发)/Transactions(测试)/ETL(生产);敏感数据脱敏后才能进开发;迁移顺序「先建→再改→再数据→再删」;禁止在迁移里做业务逻辑。
6. 回滚与数据修复策略
6.1 回滚的局限
# down() 中 DROP COLUMN 会丢数据
# 加了 NOT NULL 默认值的列 → 回滚后数据可能不一致
# 外键约束:回滚顺序与正序相反
# 原则:down() 只删表/删列,不处理数据逻辑
6.2 安全回滚策略
# 1) 数据备份:迁移前 mysqldump 或快照
# 2) 逆向迁移:写 down() 谨慎测试
# 3) 分步发布:先加新列(nullable)→ 应用代码改用新列 → 下一版本删旧列
# 4) 数据迁移单独做:大表数据变更用后台 Job,不在迁移里遍历
6.3 数据修复脚本
// 不用迁移做数据修复,用 Artisan Command
class FixUserEmails extends Command {
public function handle() {
User::whereNull('email_verified_at')->chunk(1000, function ($users) {
foreach ($users as $user) {
$user->update(['email_verified_at' => now()]);
}
});
}
}
记忆 回滚局限——down() 删列丢数据、NOT NULL 默认可能不一致;安全策略——先备份、分步发布(加列→代码改用→删旧列)、数据修复用 Command 不用迁移。
7. 零停机 Schema 变更
7.1 扩展-收缩模式(Expand-Contract)
阶段 1(扩展):
- 加新列/表/索引(不影响旧代码)
- 双写:旧代码同时写入新旧列
阶段 2(迁移):
- 后台脚本把旧数据补到新列
阶段 3(收缩):
- 代码切到新列
- 下一版本删旧列
# 全程无停机,但需两次发布
7.2 蓝绿部署中的迁移
# 先绿环境跑迁移(用户还在蓝环境)
# 验证绿环境 → 切流量到绿
# 蓝环境再跑相同迁移(或保持只读)
# 数据库Schema变更在切流量前完成
7.3 不可零停机的变更
# 删列:先确认无代码引用
# 改列类型:可能需要新建列+迁移+删旧
# 重命名:先加别名或视图,逐步切
# 主键变更:最危险,考虑新表+数据迁移+改名
记忆 零停机 = 扩展-收缩模式(加新→双写→补数据→切代码→删旧);蓝绿部署先在新环境跑迁移再切流量;删列/改类型/重命名/主键变更都需分步,确认无引用再删。
8. 大表在线变更:pt-online-schema-change 与 gh-ost
8.1 为什么大表 ALTER 可怕
# MySQL ALTER TABLE 复制全表到新结构 → 锁表/写阻塞
# 1000万行表:ALTER 可能跑 30 分钟,期间写全部卡住
# 风险:主从延迟、连接打满、MQ 积压
8.2 pt-online-schema-change
# Percona Toolkit 工具
pt-online-schema-change \
--alter "ADD COLUMN slug VARCHAR(255)" \
--execute D=mydb,t=posts
原理:
1) 建新表(带变更)
2) 建触发器(同步旧表增量到新表)
3) 分批复制数据(chunk)
4) 重命名(旧→_old,新→原表名)
5) 删旧表
# 注意:有触发器开销、不能用于含外键的表(MySQL 限制)
8.3 gh-ost(GitHub 开源)
# 无触发器设计——用 binlog 订阅增量
gh-ost \
--database=mydb --table=posts \
--alter="ADD COLUMN slug VARCHAR(255)" \
--execute
优势:
- 不建触发器 → 无触发器性能开销
- 可暂停/继续/动态调整复制速度
- 可测试(--test-on-replica)
- 回滚安全(切换前可取消)
8.4 选择建议
< 100 万行:直接 Laravel migrate(ALTER 很快)
100 万 - 1000 万:pt-online-schema-change(简单稳定)
> 1000 万 / 高并发:gh-ost(无触发器、可控)
记忆 大表 ALTER 锁表危险——pt-osc 用触发器同步增量、gh-ost 用 binlog 订阅无触发器;gh-ost 更现代(可暂停/测试/回滚);<100万直接 migrate、100-1000万 pt-osc、>1000万 gh-ost。
9. CI/CD 中的迁移自动化
9.1 部署流程中的迁移
# 1. 构建阶段:composer install
# 2. 测试阶段:php artisan migrate:fresh --seed(CI 用 SQLite)
# 3. 部署阶段:
# a) 代码上传
# b) php artisan migrate --force(非交互,生产必需)
# c) 清除缓存(config/cache/route/view)
# d) 健康检查
# e) 切流量
# 注意:migrate 在代码切换之后、服务启动之前
9.2 迁移测试
# PHPUnit 测试迁移一致性
$this->artisan('migrate:fresh');
$this->artisan('migrate:rollback');
$this->artisan('migrate');
// 验证 rollback 后再 migrate 保持一致
# 数据完整性测试:seed 后查关键表结构和约束
9.3 回滚策略
# 部署脚本带迁移回滚能力
# 如果 migrate 后健康检查失败 → 自动 rollback
# 或:蓝绿部署,绿环境失败不切换
记忆 CI/CD 中 migrate 在代码上传后、服务启动前;生产必须 –force;测试用 migrate:fresh + rollback + migrate 验证一致性;健康检查失败自动 rollback 或蓝绿不切换。
10. 速查表与一句话记忆
| 概念 | 一句话 |
|---|---|
| 迁移 | Schema 版本控制 |
| Laravel migrate | make/migrate/rollback/status |
| 范式 | 1NF 原子、2NF 完全依赖、3NF 无传递 |
| 反范式 | 读多写少时冗余减 JOIN |
| B-Tree | 等值/范围/排序 |
| 最左前缀 | 复合索引前导匹配 |
| 零停机 | 扩展-收缩模式 |
| pt-osc | 触发器同步增量(100-1000万) |
| gh-ost | binlog 订阅无触发器(>1000万) |
| CI 迁移 | –force + 健康检查 + 自动回滚 |
一句话记忆:迁移系统把 Schema 变更纳入版本控制——Laravel make/migrate/rollback/status 日常操作,Schema Builder 用 PHP DSL;数据库设计先范式保一致性(1NF 原子、2NF 完全依赖、3NF 无传递),再按读多写少反范式优化;索引用 B-Tree,设计看 WHERE/JOIN/ORDER BY、最左前缀、高选择性在前、覆盖索引不回表;多环境结构靠迁移同步、数据靠 seeders/transactions/ETL;回滚谨慎——down() 删列丢数据,用扩展-收缩模式做零停机(加新→双写→补数据→切代码→删旧);大表 ALTER 用 pt-osc(触发器,100-1000万)或 gh-ost(binlog,>1000万);CI/CD 中 migrate –force 在代码上传后、健康检查前,失败自动 rollback——「Schema 变更是最危险的操作,版本控制+分步发布+在线工具是三道防线」。
延伸阅读
- /php-mysql-database/ — MySQL 性能优化
- /php-laravel-internals/ — Laravel 框架深入
- 数据库专题 — 数据库通用原理
- MySQL 官方文档
- Percona Toolkit
- gh-ost GitHub
- Laravel Migrations 文档
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。