本文目录导读:

在 PHP 应用中平滑迁移数据是一项需要精细操作的任务,核心目标是不丢数据、不停机(或极短停机)、可回滚。
以下是针对不同规模和数据量的平滑迁移策略,从简单到复杂:
第一阶段:评估与准备(必做)
- 数据量评估:表多大?100万行和1亿行的策略完全不同。
- 拆解任务:尽量用纯 SQL 处理,减少 PHP 层的复杂逻辑(除非需要复杂的业务转换)。
- 备份:这是铁律,迁移前必须做全量备份和 Binlog 备份。
第二阶段:迁移策略(核心)
策略 A:停机维护(适合小数据 / 允许短停)
如果数据量在百万级以下,或者业务允许深夜停机,这是最简单可靠的方式。
- 流程:开启维护模式(PHP 中间件拦截请求) -> 导出旧数据 -> 转换数据 -> 导入新表 -> 切换新代码上线 -> 解除维护。
- PHP 侧:新建
maintenance.php在框架入口文件中拦截请求,返回 503。
策略 B:双写 + 回放(平滑迁移经典方案,推荐)
适用于100万 - 5000万行,需要持续服务(几乎不停机)。
核心逻辑:新老并存,影子写入,定时回放,最终切换。
-
代码兼容期(双写)
- 修改 PHP 写入逻辑:业务写入时,同时写入旧表(A)和新表(B)。
- 关键点:这一步需要确保事务性,Doctrine / Eloquent 支持,使用事务包裹两个写入;如果不支持,可以使用消息队列异步双写,但需接受短暂不一致。
- 读取仍然走旧表(保证业务稳定)。
-
数据快照与回放
- 使用
mysqldump或同步工具(如阿里 DTS)将旧表 A 的全量数据导入新表 B(注意忽略重复主键)。 - 脚本回放:从开始双写的那一刻起,A 表中的增量数据(新增/修改/删除)需要同步到 B。
- 编写 PHP CLI 脚本或使用 Binlog 订阅(如 Canal 或 Debezium)监听 A 的 Binlog,将变化应用到 B。
- 验证:比对 A 和 B 的主键数量、校验和(
CHECKSUM TABLE)。
- 使用
-
切换读取(灰度)
- 修改 PHP 读取逻辑,通过配置中心(如 Apollo)或 DB 开关,将读取流量从 A 切换到 B。
- 建议先切换 10% 流量,观察错误日志和慢查询,再逐步提升到 100%。
-
撤销双写
- 确认 B 稳定后,修改 PHP 代码,移除向 A 写入的逻辑。
- 至此,迁移完成。
策略 C:借助工具(商业/云方案)
如果数据量在亿级,建议不要自己用 PHP 处理,直接用云厂商的工具。
- DTS (Data Transmission Service):例如阿里云、腾讯云的 DTS,它可以一边同步旧库,一边切换。
操作:创建同步任务,将 A 库同步到 B 库,DTS 会追平延迟,然后你在 PHP 层直接修改连接串指向 B 库即可(DTS 通常会保证最终一致性)。
- pt-online-schema-change(Percona Toolkit):适合在 MySQL 原表上进行 DDL(增删改字段)的平滑操作,它通过创建临时表、触发器实现,不影响外部访问。
第三阶段:PHP 代码层面的注意事项
这是最容易踩坑的地方,提供以下建议:
-
使用 Repository 模式(必需)
- 不要在 Controller 或 Service 里直接写
DB::table('old_table')。 - 始终通过
UserRepository或OrderRepository来访问数据。 - 迁移时,只需要修改 Repository 内部的数据源,业务代码不用动。
- 不要在 Controller 或 Service 里直接写
-
SQL 兼容性自查
- 新表的字段类型、索引是否与旧代码的 WHERE/ORDER BY 完全兼容?
- 例如旧表用的是
int(10),新表迁移到bigint,或者把旧表的varchar改成了json,PHP 层的数据类型转换逻辑要跟上。
-
严格处理 NULL 和默认值
- 新表如果加了非空约束,而旧数据中有空字符串,迁移脚本必须处理。
- 在 PHP 数据转换脚本中,写一个验证方法:
private function validateRow(array $row): bool { // 检查新表必填字段 if ($row['status'] === null) return false; // 长度检查 if (mb_strlen($row['name']) > 200) return false; return true; }
-
分批处理(避免内存溢出)
- 不要在 PHP 脚本中一次性
SELECT *。 - 使用
chunkById(Laravel) 或LIMIT/OFFSET+WHERE id > last_id ORDER BY id ASC来游标分页。 - 代码示例(伪代码):
$lastId = 0; while (true) { $rows = $oldModel->where('id', '>', $lastId)->orderBy('id')->limit(1000)->get(); if ($rows->isEmpty()) break; foreach ($rows as $row) { $newData = transform($row); // 你的转换逻辑 $newModel->insert($newData); } $lastId = $rows->last()->id; }
- 不要在 PHP 脚本中一次性
-
设置超长时间与内存限制
- 如果是 CLI 脚本:
set_time_limit(0); ini_set('memory_limit', '512M');(其实更推荐memory_limit=-1但要注意保护)。 - 如果是 Laravel 队列:设置
--timeout=3600。
- 如果是 CLI 脚本:
第四阶段:回滚预案(容灾)
- 状态机:在配置中心(如
config表或 Redis)存放migration_status(migrating,stable)。 - 开关控制:在 PHP 中写一个中间件,读取该状态。
- 如果从新库读取报错(
SQLSTATE[HY000]),自动降级到旧库读取(Try-Catch 切换)。 - 写操作如果新库失败,必须回滚旧库,并记录
failed_jobs表。
- 如果从新库读取报错(
- 回滚步骤:
- 立即关掉新库读流量(改开关回旧库)。
- 如果代码已删除双写逻辑,请保留旧表数据至少一周,以便重建迁移。
PHP 侧的实施清单
- 在
Repository层引入DataSource策略(读旧/写新 或 双写)。 - 使用
Redis或config表作为迁移开关,配置热更新(不用重启 PHP-FPM)。 - 编写 CLI 脚本进行分批数据搬运,并打印进度条(
$i/$total)。 - 校验:
SELECT COUNT(*)两个库对比。 - 默认情况下,读旧写新 -> 双写 -> 读新写新,也就是分三步走。
如果你能提供具体场景(只是加索引?换数据库引擎?字段类型变更?),我可以给出更贴近实际需求的 SQL 或 PHP 代码片段。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播