---
title: 数据库迁移的安全网：备份、预演与幂等
url: https://vabora.dev/posts/migration-rollback-safety
author: Vabora
published: 2026-07-24 09:30:00
updated: 2026-09-27 10:33:42
category: 最佳实践
tags: 最佳实践, 运维, 数据库
canonical: https://vabora.dev/posts/migration-rollback-safety
license: 转载与引用请注明出处并保留原文链接
---

# 数据库迁移的安全网：备份、预演与幂等

> 迁移脚本是唯一会直接改生产数据的代码。这篇讲三道安全网：执行前自动备份、--dry-run 预演、以及"可重复执行"的写法——顺便说说为什么回滚要慎用。
## 背景

代码出问题可以回滚发布，数据出问题只能靠备份。而迁移脚本是**唯一一类会直接改生产数据的代码**——它却常常是团队里最没被认真审过的部分。

现实中的翻车方式很固定：加了 `NOT NULL` 列但表里已有数据、索引名与既有的冲突、迁移跑一半失败留下半截状态、`down()` 写错了还不如不写。

## 三道安全网

### 1. 执行前自动备份（默认行为，不是选项）

```bash
php vabora migrate                # 自动先备份数据库
php vabora migrate --no-backup    # 明确跳过（本地开发用）
```

备份是**动作级**的：失败就中止迁移，而不是"备份失败了继续跑"。数据库小的时候备份只要几百毫秒，这笔时间不该省。

### 2. 预演：先看清楚要跑什么

```bash
php vabora migrate --dry-run      # 列出待执行迁移，不写库
php vabora migrate:status         # 已执行 / 未执行对照
```

预演要能回答三个问题：**有几条、各自做什么、有没有已经存在的**。看不到这些就点确认，等于闭眼提交。

### 3. 幂等：让迁移"可重复执行"

迁移写法的第一原则不是 `up/down` 对称，而是**重复执行不炸**：

```php
// 加列：先判断再执行（MySQL 老版本没有 ADD COLUMN IF NOT EXISTS）
if (!Schema::hasColumn('cases', 'deleted_at')) {
    Schema::statement('ALTER TABLE `v_cases` ADD COLUMN `deleted_at` DATETIME NULL');
    Schema::statement('CREATE INDEX `v_idx_cases_deleted_at` ON `v_cases` (`deleted_at`)');
}
```

判断存在的价值在于：迁移记录表丢失、多人并行开发、从旧备份恢复后重跑——这些场景都会让"已经跑过的迁移"再跑一次。幂等让这些场景从事故变成无感。

## 新增列的兼容顺序

字段变更最危险，顺序错了会有一段"新旧代码都不工作"的窗口：

```
1. 加列（可空，带默认值）      → 旧代码不受影响
2. 部署写新列的新代码          → 新代码对旧数据用默认值
3. 回填历史数据（分批，可重跑） → 大表要分批，避免长事务锁表
4. 需要时再加约束（NOT NULL）   → 此时数据已齐
```

反过来（先加 `NOT NULL` 再部署代码）必然失败。**先让数据就位，再收紧约束**。

## 回滚的边界

`migrate:rollback` 能回退最后一批，但要注意：

- **结构可回滚，数据不可回滚**：删掉的列，回滚只能把列加回来，数据没了；
- **回滚不是"撤销部署"**：代码已经切到新版本，回滚数据库会让新代码找不到字段；
- 真正遇到问题时，正确的顺序是 **先回滚代码，再决定要不要回滚结构**。

所以：**破坏性操作（删列、删表）要么分两次发布**（先停用，观察一周再删），**要么先备份再说服自己不会后退**。

## 结果

- 迁移从"手抖一下的事故源"变成常规操作：默认备份 + 预演 + 幂等，三条都做之后，本地与线上跑的是同一套命令；
- 一次真实的救场：恢复旧备份后重跑迁移，因为幂等写法，直接跑通而不需要手工删表。

## 取舍

- **不自动执行线上迁移**：部署流程里把迁移做成独立步骤（人工确认），而不是随代码自动跑。迁移失败与代码部署失败应该是两件可分开处理的事。
- **不为回滚写复杂 `down()`**：`down()` 只做"能安全撤销的结构变更"，涉及数据的一律标注"不可逆"。写一个假的 `down()` 比没有更危险——它给人虚假的安全感。
