PHP 怎么平滑迁移数据

wen PHP项目 1

本文目录导读:

PHP 怎么平滑迁移数据

  1. 第一阶段:评估与准备(必做)
  2. 第二阶段:迁移策略(核心)
  3. 第三阶段:PHP 代码层面的注意事项
  4. 第四阶段:回滚预案(容灾)
  5. 总结一下 PHP 侧的实施清单

在 PHP 应用中平滑迁移数据是一项需要精细操作的任务,核心目标是不丢数据、不停机(或极短停机)、可回滚

以下是针对不同规模和数据量的平滑迁移策略,从简单到复杂:

第一阶段:评估与准备(必做)

  1. 数据量评估:表多大?100万行和1亿行的策略完全不同。
  2. 拆解任务:尽量用纯 SQL 处理,减少 PHP 层的复杂逻辑(除非需要复杂的业务转换)。
  3. 备份这是铁律,迁移前必须做全量备份和 Binlog 备份。

第二阶段:迁移策略(核心)

策略 A:停机维护(适合小数据 / 允许短停)

如果数据量在百万级以下,或者业务允许深夜停机,这是最简单可靠的方式。

  • 流程:开启维护模式(PHP 中间件拦截请求) -> 导出旧数据 -> 转换数据 -> 导入新表 -> 切换新代码上线 -> 解除维护。
  • PHP 侧:新建 maintenance.php 在框架入口文件中拦截请求,返回 503。

策略 B:双写 + 回放(平滑迁移经典方案,推荐)

适用于100万 - 5000万行,需要持续服务(几乎不停机)。

核心逻辑:新老并存,影子写入,定时回放,最终切换。

  1. 代码兼容期(双写)

    • 修改 PHP 写入逻辑:业务写入时,同时写入旧表(A)和新表(B)。
    • 关键点:这一步需要确保事务性,Doctrine / Eloquent 支持,使用事务包裹两个写入;如果不支持,可以使用消息队列异步双写,但需接受短暂不一致。
    • 读取仍然走旧表(保证业务稳定)。
  2. 数据快照与回放

    • 使用 mysqldump 或同步工具(如阿里 DTS)将旧表 A 的全量数据导入新表 B(注意忽略重复主键)。
    • 脚本回放:从开始双写的那一刻起,A 表中的增量数据(新增/修改/删除)需要同步到 B。
    • 编写 PHP CLI 脚本或使用 Binlog 订阅(如 Canal 或 Debezium)监听 A 的 Binlog,将变化应用到 B。
    • 验证:比对 A 和 B 的主键数量、校验和(CHECKSUM TABLE)。
  3. 切换读取(灰度)

    • 修改 PHP 读取逻辑,通过配置中心(如 Apollo)或 DB 开关,将读取流量从 A 切换到 B。
    • 建议先切换 10% 流量,观察错误日志和慢查询,再逐步提升到 100%。
  4. 撤销双写

    • 确认 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 代码层面的注意事项

这是最容易踩坑的地方,提供以下建议:

  1. 使用 Repository 模式(必需)

    • 不要在 Controller 或 Service 里直接写 DB::table('old_table')
    • 始终通过 UserRepositoryOrderRepository 来访问数据。
    • 迁移时,只需要修改 Repository 内部的数据源,业务代码不用动。
  2. SQL 兼容性自查

    • 新表的字段类型、索引是否与旧代码的 WHERE/ORDER BY 完全兼容?
    • 例如旧表用的是 int(10),新表迁移到 bigint,或者把旧表的 varchar 改成了 json,PHP 层的数据类型转换逻辑要跟上。
  3. 严格处理 NULL 和默认值

    • 新表如果加了非空约束,而旧数据中有空字符串,迁移脚本必须处理。
    • 在 PHP 数据转换脚本中,写一个验证方法
      private function validateRow(array $row): bool {
      // 检查新表必填字段
      if ($row['status'] === null) return false;
      // 长度检查
      if (mb_strlen($row['name']) > 200) return false;
      return true;
      }
  4. 分批处理(避免内存溢出)

    • 不要在 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;
      }
  5. 设置超长时间与内存限制

    • 如果是 CLI 脚本:set_time_limit(0); ini_set('memory_limit', '512M');(其实更推荐 memory_limit=-1 但要注意保护)。
    • 如果是 Laravel 队列:设置 --timeout=3600

第四阶段:回滚预案(容灾)

  • 状态机:在配置中心(如 config 表或 Redis)存放 migration_statusmigratingstable)。
  • 开关控制:在 PHP 中写一个中间件,读取该状态。
    • 如果从新库读取报错(SQLSTATE[HY000]),自动降级到旧库读取(Try-Catch 切换)。
    • 写操作如果新库失败,必须回滚旧库,并记录 failed_jobs 表。
  • 回滚步骤
    • 立即关掉新库读流量(改开关回旧库)。
    • 如果代码已删除双写逻辑,请保留旧表数据至少一周,以便重建迁移。

PHP 侧的实施清单

  1. Repository 层引入 DataSource 策略(读旧/写新 或 双写)。
  2. 使用 Redisconfig 表作为迁移开关,配置热更新(不用重启 PHP-FPM)。
  3. 编写 CLI 脚本进行分批数据搬运,并打印进度条($i / $total)。
  4. 校验:SELECT COUNT(*) 两个库对比。
  5. 默认情况下,读旧写新 -> 双写 -> 读新写新,也就是分三步走。

如果你能提供具体场景(只是加索引?换数据库引擎?字段类型变更?),我可以给出更贴近实际需求的 SQL 或 PHP 代码片段。

抱歉,评论功能暂时关闭!