Skip to content

回滚策略 ​

数据库迁移的回滚策略是保证系统稳定性的关键。本章将深入探讨 EF Core 中的各种回滚机制,包括自动回滚、手动回滚、部分回滚和灾难恢复。

目录 ​


1. 为什么需要回滚策略 ​

1.1 常见失败场景 ​

csharp
// 场景1: 迁移过程中出现异常
public partial class AddNewColumn : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.AddColumn<int>(
            name: "Priority",
            table: "Tasks",
            type: "int",
            nullable: false,
            defaultValue: 0);
        
        // 💥 如果这里失败,列会被添加吗?
        migrationBuilder.Sql("UPDATE Tasks SET Priority = CalculatePriority()");
    }
}

// 场景2: 数据迁移违反约束
migrationBuilder.Sql(@"
    UPDATE Products
    SET Price = OldPrice * 1.1
    WHERE CategoryId = 5;
");
// 💥 如果某些产品的价格超过限制,会触发 CHECK 约束失败

// 场景3: 外键约束冲突
migrationBuilder.DropColumn(
    name: "CategoryId",
    table: "Products");
// 💥 如果有外键引用该列,操作会失败

1.2 回滚策略的重要性 ​

为什么要制定回滚策略?

  1. 数据完整性保护 - 防止部分应用的迁移导致数据不一致
  2. 业务连续性保障 - 快速恢复服务,减少停机时间
  3. 风险控制 - 降低生产环境变更的风险
  4. 合规要求 - 满足审计和监管要求

回滚的层次:

Level 1: 事务内自动回滚 (毫秒级)
Level 2: Down 方法回滚 (秒级)
Level 3: 版本回退 (分钟级)
Level 4: 数据库还原 (小时级)
Level 5: 灾难恢复 (小时到天级)

2. 事务性回滚 ​

2.1 默认事务行为 ​

EF Core 默认将整个迁移包裹在一个事务中。

工作原理:

csharp
// EF Core 内部实现(伪代码)
using var transaction = connection.BeginTransaction();
try
{
    foreach (var operation in migration.Operations)
    {
        ExecuteOperation(operation);
    }
    transaction.Commit();
}
catch
{
    transaction.Rollback();  // 自动回滚所有更改
    throw;
}

实际示例:

csharp
public partial class SafeMigration : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        // 所有这些操作都在同一个事务中
        
        migrationBuilder.AddColumn<string>(
            name: "Email",
            table: "Customers",
            type: "nvarchar(200)",
            nullable: true);
        
        migrationBuilder.AddColumn<string>(
            name: "Phone",
            table: "Customers",
            type: "nvarchar(20)",
            nullable: true);
        
        // 如果这个 SQL 失败,上面两个列的添加也会被回滚
        migrationBuilder.Sql("UPDATE Customers SET Email = LOWER(Email)");
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropColumn(name: "Email", table: "Customers");
        migrationBuilder.DropColumn(name: "Phone", table: "Customers");
    }
}

验证事务回滚:

csharp
public async Task TestMigrationRollback()
{
    using var context = new AppDbContext(options);
    
    try
    {
        await context.Database.MigrateAsync();
    }
    catch (Exception ex)
    {
        // 迁移失败,所有更改已自动回滚
        
        // 验证:表结构应该保持不变
        var columns = await context.Database
            .SqlQueryRaw<string>("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Customers'")
            .ToListAsync();
        
        Console.WriteLine(string.Join(", ", columns));
        // 输出: Id, Name, ... (不包含 Email, Phone)
    }
}

2.2 禁用事务控制 ​

某些操作需要在事务外执行。

场景1: 创建索引(SQL Server ONLINE 选项)

csharp
public partial class CreateIndexOnline : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        // ONLINE = ON 要求在事务外执行
        migrationBuilder.Sql(@"
            CREATE NONCLUSTERED INDEX IX_Products_Name
            ON Products(Name)
            WITH (ONLINE = ON, MAXDOP = 4)
        ", suppressTransaction: true);  // ⚠️ 禁用事务
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.Sql("DROP INDEX IF EXISTS IX_Products_Name ON Products");
    }
}

场景2: 大型表的重建

csharp
public partial class RebuildLargeTable : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        // 重建索引 - 必须在事务外
        migrationBuilder.Sql(@"
            ALTER INDEX ALL ON LargeTable REBUILD 
            WITH (ONLINE = ON, SORT_IN_TEMPDB = ON)
        ", suppressTransaction: true);
    }
}

⚠️ 风险提示:

csharp
// ❌ 危险: 如果后续操作失败,无法回滚
public partial class RiskyMigration : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        // 操作1: 在事务外执行(无法回滚)
        migrationBuilder.Sql("ALTER TABLE LargeTable ADD NewColumn INT", 
            suppressTransaction: true);
        
        // 操作2: 如果这里失败,操作1无法回滚!
        migrationBuilder.Sql("UPDATE LargeTable SET NewColumn = ComplexCalculation()");
    }
}

// ✅ 安全: 手动处理回滚
public partial class SafeMigrationWithManualRollback : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        try
        {
            migrationBuilder.Sql("ALTER TABLE LargeTable ADD NewColumn INT", 
                suppressTransaction: true);
            
            migrationBuilder.Sql("UPDATE LargeTable SET NewColumn = ComplexCalculation()");
        }
        catch
        {
            // 手动回滚
            migrationBuilder.Sql("ALTER TABLE LargeTable DROP COLUMN NewColumn", 
                suppressTransaction: true);
            throw;
        }
    }
}

3. Down 方法回滚 ​

3.1 Down 方法基础 ​

每个迁移都应该提供对应的 Down 方法,用于撤销该迁移的更改。

基本模式:

csharp
public partial class AddCustomerContactInfo : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.AddColumn<string>(
            name: "Email",
            table: "Customers",
            type: "nvarchar(200)",
            nullable: false,
            defaultValue: "");
        
        migrationBuilder.AddColumn<string>(
            name: "Phone",
            table: "Customers",
            type: "nvarchar(20)",
            nullable: true);
        
        migrationBuilder.CreateIndex(
            name: "IX_Customers_Email",
            table: "Customers",
            column: "Email",
            unique: true);
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        // 完全撤销 Up 方法的更改
        migrationBuilder.DropIndex(
            name: "IX_Customers_Email",
            table: "Customers");
        
        migrationBuilder.DropColumn(
            name: "Phone",
            table: "Customers");
        
        migrationBuilder.DropColumn(
            name: "Email",
            table: "Customers");
    }
}

3.2 复杂 Down 方法 ​

当 Up 方法包含数据迁移时,Down 方法需要更复杂的逻辑。

示例: 列拆分的数据回滚:

csharp
public partial class SplitCustomerFullName : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        // 添加新列
        migrationBuilder.AddColumn<string>(
            name: "FirstName",
            table: "Customers",
            type: "nvarchar(100)",
            nullable: true);
        
        migrationBuilder.AddColumn<string>(
            name: "LastName",
            table: "Customers",
            type: "nvarchar(100)",
            nullable: true);
        
        // 迁移数据
        migrationBuilder.Sql(@"
            UPDATE Customers
            SET 
                FirstName = LEFT(FullName, CHARINDEX(' ', FullName) - 1),
                LastName = RIGHT(FullName, LEN(FullName) - CHARINDEX(' ', FullName))
            WHERE CHARINDEX(' ', FullName) > 0
        ");
        
        // 删除旧列
        migrationBuilder.DropColumn(
            name: "FullName",
            table: "Customers");
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        // 恢复旧列
        migrationBuilder.AddColumn<string>(
            name: "FullName",
            table: "Customers",
            type: "nvarchar(200)",
            nullable: true);
        
        // 合并数据回来
        migrationBuilder.Sql(@"
            UPDATE Customers
            SET FullName = FirstName + ' ' + LastName
            WHERE FirstName IS NOT NULL AND LastName IS NOT NULL
        ");
        
        // 删除新列
        migrationBuilder.DropColumn(name: "FirstName", table: "Customers");
        migrationBuilder.DropColumn(name: "LastName", table: "Customers");
    }
}

3.3 不可逆迁移的处理 ​

某些迁移是不可逆的(如数据删除)。

csharp
public partial class RemoveDeprecatedFields : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        // 删除废弃字段及其数据
        migrationBuilder.DropColumn(name: "LegacyCode", table: "Products");
        migrationBuilder.DropColumn(name: "OldDescription", table: "Products");
        
        // 清空废弃表的数据
        migrationBuilder.Sql("DELETE FROM DeprecatedTable");
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        // ⚠️ 无法恢复已删除的数据
        
        // 只能恢复表结构
        migrationBuilder.AddColumn<string>(
            name: "LegacyCode",
            table: "Products",
            type: "nvarchar(50)",
            nullable: true,
            defaultValue: "");
        
        migrationBuilder.AddColumn<string>(
            name: "OldDescription",
            table: "Products",
            type: "nvarchar(500)",
            nullable: true,
            defaultValue: "");
        
        // 记录警告
        migrationBuilder.Sql(@"
            PRINT 'WARNING: Data cannot be recovered after this rollback.';
            PRINT 'Please restore from backup if needed.';
        ");
    }
}

最佳实践:

csharp
// 对于不可逆操作,添加明确的注释和警告
protected override void Down(MigrationBuilder migrationBuilder)
{
    throw new NotSupportedException(
        "This migration cannot be rolled back because it permanently deletes data. " +
        "To revert, you must restore from a database backup.");
}

4. 迁移版本回退 ​

4.1 回退到指定版本 ​

使用 dotnet ef database update 命令可以回退到任意历史版本。

命令行操作:

bash
# 回退到上一个迁移
dotnet ef database update PreviousMigrationName

# 回退到特定迁移
dotnet ef database update 20240101120000_InitialCreate

# 回退到初始状态(删除所有表)
dotnet ef database update 0

# 查看当前迁移状态
dotnet ef migrations list

C# API 调用:

csharp
public async Task RollbackToVersion(string targetMigration)
{
    using var context = new AppDbContext(options);
    
    // 回退到指定迁移
    await context.Database.MigrateAsync(targetMigration);
}

public async Task RollbackLastMigration()
{
    using var context = new AppDbContext(options);
    
    // 获取已应用的迁移列表
    var appliedMigrations = await context.Database
        .GetAppliedMigrationsAsync();
    
    if (appliedMigrations.Any())
    {
        // 获取最后一个迁移
        var lastMigration = appliedMigrations.Last();
        
        // 回退到前一个版本
        var previousMigration = appliedMigrations.Count > 1 
            ? appliedMigrations[appliedMigrations.Count - 2]
            : null;
        
        await context.Database.MigrateAsync(previousMigration);
    }
}

4.2 批量回滚脚本 ​

生成回滚 SQL 脚本:

bash
# 生成从当前版本回退到指定版本的 SQL 脚本
dotnet ef migrations script CurrentMigration TargetMigration -o rollback.sql

# 生成回退上一个迁移的脚本
dotnet ef migrations script LastMigration SecondLastMigration -o rollback.sql

# 生成完整回滚脚本(回到初始状态)
dotnet ef migrations script LastMigration 0 -o full_rollback.sql

自动化回滚脚本:

powershell
# PowerShell 回滚脚本
param(
    [string]$TargetMigration,
    [string]$ConnectionString
)

Write-Host "Starting rollback to: $TargetMigration" -ForegroundColor Green

try {
    # 生成 SQL 脚本
    dotnet ef migrations script --idempotent --output rollback_$TargetMigration.sql
    
    # 审查脚本(可选)
    Write-Host "Generated SQL script: rollback_$TargetMigration.sql" -ForegroundColor Yellow
    
    # 应用回滚
    dotnet ef database update $TargetMigration --connection $ConnectionString
    
    Write-Host "Rollback completed successfully!" -ForegroundColor Green
}
catch {
    Write-Host "Rollback failed: $_" -ForegroundColor Red
    exit 1
}

4.3 程序化回滚 ​

在应用程序中实现自动回滚逻辑:

csharp
public class MigrationService
{
    private readonly IServiceProvider _serviceProvider;
    private readonly ILogger<MigrationService> _logger;
    
    public MigrationService(IServiceProvider serviceProvider, ILogger<MigrationService> logger)
    {
        _serviceProvider = serviceProvider;
        _logger = logger;
    }
    
    public async Task<bool> ApplyMigrationWithRollback(string targetMigration = null)
    {
        using var scope = _serviceProvider.CreateScope();
        var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        
        try
        {
            _logger.LogInformation($"Applying migration: {targetMigration ?? "latest"}");
            
            if (string.IsNullOrEmpty(targetMigration))
            {
                await context.Database.MigrateAsync();
            }
            else
            {
                await context.Database.MigrateAsync(targetMigration);
            }
            
            _logger.LogInformation("Migration applied successfully");
            return true;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Migration failed, attempting automatic rollback");
            
            try
            {
                // 尝试回滚到上一个稳定版本
                var stableVersion = GetLastStableVersion();
                
                _logger.LogWarning($"Rolling back to stable version: {stableVersion}");
                await context.Database.MigrateAsync(stableVersion);
                
                _logger.LogInformation("Rollback completed");
                return false;
            }
            catch (Exception rollbackEx)
            {
                _logger.LogCritical(rollbackEx, "Automatic rollback failed!");
                throw new AggregateException("Migration and rollback both failed", ex, rollbackEx);
            }
        }
    }
    
    private string GetLastStableVersion()
    {
        // 从配置或数据库中获取最后一个稳定的迁移版本
        // 这里可以从配置文件、数据库或硬编码返回
        return "20240101120000_StableRelease";
    }
}

5. 部分回滚与保存点 ​

5.1 使用保存点进行部分回滚 ​

SQL Server 2019+ 和其他现代数据库支持保存点(Savepoints),允许部分回滚。

基本用法:

csharp
public partial class MigrationWithSavepoints : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        // 创建保存点
        migrationBuilder.Sql("SAVE TRANSACTION BeforeComplexOperation");
        
        try
        {
            // 操作1: 添加列
            migrationBuilder.AddColumn<int>(
                name: "Priority",
                table: "Tasks",
                type: "int",
                nullable: false,
                defaultValue: 0);
            
            // 操作2: 更新数据(可能失败)
            migrationBuilder.Sql("UPDATE Tasks SET Priority = CalculatePriority()");
            
            // 如果成功,释放保存点
            migrationBuilder.Sql("RELEASE SAVEPOINT BeforeComplexOperation");
        }
        catch
        {
            // 回滚到保存点
            migrationBuilder.Sql("ROLLBACK TRANSACTION BeforeComplexOperation");
            throw;
        }
    }
}

多个保存点:

csharp
public partial class MultiStepMigration : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        // 步骤1
        migrationBuilder.Sql("SAVE TRANSACTION Step1");
        try
        {
            migrationBuilder.AddColumn<string>(
                name: "Email",
                table: "Customers",
                type: "nvarchar(200)",
                nullable: true);
            
            migrationBuilder.Sql("RELEASE SAVEPOINT Step1");
        }
        catch
        {
            migrationBuilder.Sql("ROLLBACK TRANSACTION Step1");
            throw;
        }
        
        // 步骤2
        migrationBuilder.Sql("SAVE TRANSACTION Step2");
        try
        {
            migrationBuilder.AddColumn<string>(
                name: "Phone",
                table: "Customers",
                type: "nvarchar(20)",
                nullable: true);
            
            migrationBuilder.Sql("RELEASE SAVEPOINT Step2");
        }
        catch
        {
            // 只回滚步骤2,步骤1保持
            migrationBuilder.Sql("ROLLBACK TRANSACTION Step2");
            throw;
        }
    }
}

5.2 条件回滚 ​

根据运行时条件决定是否回滚。

csharp
public partial class ConditionalRollbackMigration : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.Sql("SAVE TRANSACTION BeforeDataMigration");
        
        try
        {
            // 数据迁移
            migrationBuilder.Sql(@"
                INSERT INTO NewTable (Id, Name, Value)
                SELECT Id, Name, Value
                FROM OldTable
                WHERE IsActive = 1
            ");
            
            // 检查迁移结果
            var migrationResult = migrationBuilder.Query<int>(
                "SELECT COUNT(*) FROM NewTable").FirstOrDefault();
            
            if (migrationResult == 0)
            {
                // 如果没有迁移任何数据,回滚
                migrationBuilder.Sql("ROLLBACK TRANSACTION BeforeDataMigration");
                throw new InvalidOperationException("No data was migrated");
            }
            
            migrationBuilder.Sql("RELEASE SAVEPOINT BeforeDataMigration");
        }
        catch
        {
            migrationBuilder.Sql("ROLLBACK TRANSACTION BeforeDataMigration");
            throw;
        }
    }
}

6. 灾难恢复策略 ​

6.1 数据库备份与还原 ​

迁移前备份:

bash
# SQL Server 备份
sqlcmd -S localhost -Q "BACKUP DATABASE MyApp TO DISK = 'C:\Backups\MyApp_PreMigration.bak'"

# PostgreSQL 备份
pg_dump -U postgres MyApp > MyApp_PreMigration.sql

# MySQL 备份
mysqldump -u root -p MyApp > MyApp_PreMigration.sql

程序化备份:

csharp
public class DatabaseBackupService
{
    private readonly string _connectionString;
    private readonly ILogger<DatabaseBackupService> _logger;
    
    public DatabaseBackupService(string connectionString, ILogger<DatabaseBackupService> logger)
    {
        _connectionString = connectionString;
        _logger = logger;
    }
    
    public async Task<string> CreateBackupBeforeMigration()
    {
        var timestamp = DateTime.UtcNow.ToString("yyyyMMdd_HHmmss");
        var backupPath = $"C:\\Backups\\MyApp_{timestamp}.bak";
        
        try
        {
            using var connection = new SqlConnection(_connectionString);
            await connection.OpenAsync();
            
            var commandText = $"BACKUP DATABASE MyApp TO DISK = '{backupPath}' WITH INIT, COMPRESSION";
            using var command = new SqlCommand(commandText, connection);
            
            _logger.LogInformation("Creating database backup: {BackupPath}", backupPath);
            await command.ExecuteNonQueryAsync();
            
            _logger.LogInformation("Backup created successfully: {BackupPath}", backupPath);
            return backupPath;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to create backup");
            throw;
        }
    }
    
    public async Task RestoreFromBackup(string backupPath)
    {
        try
        {
            using var connection = new SqlConnection(_connectionString);
            await connection.OpenAsync();
            
            // 需要先断开其他连接
            var killConnectionsCommand = new SqlCommand(
                "ALTER DATABASE MyApp SET SINGLE_USER WITH ROLLBACK IMMEDIATE", connection);
            await killConnectionsCommand.ExecuteNonQueryAsync();
            
            // 还原数据库
            var restoreCommand = new SqlCommand(
                $"RESTORE DATABASE MyApp FROM DISK = '{backupPath}' WITH REPLACE", connection);
            await restoreCommand.ExecuteNonQueryAsync();
            
            // 恢复多用户模式
            var multiUserCommand = new SqlCommand(
                "ALTER DATABASE MyApp SET MULTI_USER", connection);
            await multiUserCommand.ExecuteNonQueryAsync();
            
            _logger.LogInformation("Database restored from: {BackupPath}", backupPath);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to restore backup");
            throw;
        }
    }
}

6.2 时间点恢复(PITR) ​

利用事务日志恢复到特定时间点。

SQL Server:

sql
-- 恢复到迁移前的时间点
RESTORE DATABASE MyApp
FROM DISK = 'C:\Backups\MyApp_Full.bak'
WITH NORECOVERY;

RESTORE LOG MyApp
FROM DISK = 'C:\Backups\MyApp_Log.trn'
WITH STOPAT = '2024-01-01 12:00:00',  -- 迁移前的时间
     RECOVERY;

PostgreSQL:

sql
-- postgresql.conf 配置
wal_level = replica
archive_mode = on
archive_command = 'cp %p /archive/%f'

-- 恢复到指定时间点
SELECT pg_restore_start_time('2024-01-01 12:00:00');

6.3 灾难恢复计划(DRP) ​

完整的灾难恢复流程:

markdown
## 灾难恢复 SOP

### 1. 评估情况
- 确定故障类型(迁移失败/数据损坏/硬件故障)
- 评估影响范围和数据丢失程度
- 确定恢复目标时间点

### 2. 通知相关人员
- DBA 团队
- 开发团队
- 运维团队
- 业务负责人

### 3. 选择恢复策略
- 策略A: 回滚到上一个迁移版本
- 策略B: 从备份还原
- 策略C: 切换到备用数据库
- 策略D: 时间点恢复

### 4. 执行恢复
- 按照选定策略执行恢复
- 监控恢复进度
- 验证数据完整性

### 5. 验证测试
- 运行集成测试
- 验证关键业务流程
- 检查数据一致性

### 6. 恢复服务
- 重新开放访问
- 监控系统稳定性
- 收集性能指标

### 7. 事后分析
- 记录事故时间线
- 分析根本原因
- 制定改进措施
- 更新应急预案

7. 蓝绿部署与零停机回滚 ​

7.1 蓝绿部署架构 ​

基本原理:

              ┌─────────────┐
    Request → │   Load      │
              │   Balancer  │
              └──────┬──────┘
                     │
          ┌──────────┴──────────┐
          │                     │
    ┌─────▼─────┐       ┌──────▼──────┐
    │  Blue     │       │   Green     │
    │  (Active) │       │  (Standby)  │
    │  v1.0     │       │   v1.1      │
    └───────────┘       └─────────────┘

实施步骤:

csharp
// 步骤1: 准备 Green 环境
// - 部署新版本应用
// - 运行数据库迁移
// - 运行健康检查

public async Task PrepareGreenEnvironment()
{
    // 克隆数据库
    await CloneDatabase("MyApp_Blue", "MyApp_Green");
    
    // 在 Green 环境运行迁移
    var greenContext = CreateGreenDbContext();
    await greenContext.Database.MigrateAsync();
    
    // 健康检查
    var isHealthy = await HealthCheck(greenContext);
    if (!isHealthy)
    {
        throw new InvalidOperationException("Green environment health check failed");
    }
}

// 步骤2: 切换流量
public async Task SwitchTraffic()
{
    // 更新负载均衡器配置
    await UpdateLoadBalancerConfig(activeEnvironment: "Green");
    
    // 监控错误率
    var errorRate = await MonitorErrorRate(minutes: 5);
    if (errorRate > 0.05)  // 错误率超过5%
    {
        // 立即切回 Blue
        await RollbackToBlue();
    }
}

// 步骤3: 回滚策略
public async Task RollbackToBlue()
{
    // 快速切回 Blue 环境
    await UpdateLoadBalancerConfig(activeEnvironment: "Blue");
    
    // 清理 Green 环境
    await CleanupGreenEnvironment();
}

7.2 数据库级别的零停机回滚 ​

向后兼容的迁移策略:

csharp
// 第1步: 添加新列(不删除旧列)
public partial class AddNewColumnWithoutRemovingOld : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        // 添加新列
        migrationBuilder.AddColumn<string>(
            name: "Email_New",
            table: "Customers",
            type: "nvarchar(200)",
            nullable: true);
        
        // 双写:同时更新新旧列
        migrationBuilder.Sql(@"
            CREATE TRIGGER tr_Customers_DualWrite
            ON Customers
            AFTER INSERT, UPDATE
            AS
            BEGIN
                UPDATE c
                SET c.Email_New = i.Email_Old
                FROM Customers c
                INNER JOIN inserted i ON c.Id = i.Id
            END
        ");
    }
}

// 第2步: 迁移历史数据
public partial class MigrateHistoricalData : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.Sql(@"
            UPDATE Customers
            SET Email_New = Email_Old
            WHERE Email_New IS NULL AND Email_Old IS NOT NULL
        ");
    }
}

// 第3步: 切换应用到新列
// (应用代码改为使用 Email_New)

// 第4步: 删除旧列(在确认稳定后)
public partial class RemoveOldColumn : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.Sql("DROP TRIGGER IF EXISTS tr_Customers_DualWrite");
        
        migrationBuilder.DropColumn(
            name: "Email_Old",
            table: "Customers");
        
        migrationBuilder.RenameColumn(
            name: "Email_New",
            table: "Customers",
            newName: "Email");
    }
}

8. 最佳实践与监控 ​

8.1 回滚策略最佳实践 ​

✅ 应该做的:

  1. 始终测试 Down 方法

    bash
    # 测试回滚
    dotnet ef database update TargetMigration
    dotnet ef database update PreviousMigration  # 测试 Down
    dotnet ef database update TargetMigration    # 再次应用
  2. 迁移前备份数据库

    csharp
    public async Task SafeMigrate()
    {
        // 1. 备份
        var backupPath = await backupService.CreateBackupBeforeMigration();
        
        try
        {
            // 2. 迁移
            await context.Database.MigrateAsync();
        }
        catch
        {
            // 3. 失败时还原
            await backupService.RestoreFromBackup(backupPath);
            throw;
        }
    }
  3. 在小规模环境先测试

    yaml
    # CI/CD 流水线
    stages:
      - test_migration_on_staging
      - backup_production
      - deploy_to_production
      - verify_health
      - cleanup_backup  # 保留一定时间
  4. 设置合理的超时和重试

    csharp
    public async Task MigrateWithRetry(int maxRetries = 3)
    {
        for (int i = 0; i < maxRetries; i++)
        {
            try
            {
                await context.Database.MigrateAsync();
                return;  // 成功
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "Migration attempt {Attempt} failed", i + 1);
                
                if (i == maxRetries - 1) throw;  // 最后一次,抛出异常
                
                await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, i)));  // 指数退避
            }
        }
    }

8.2 监控和告警 ​

监控指标:

csharp
public class MigrationMonitor
{
    private readonly ILogger<MigrationMonitor> _logger;
    private readonly IMetricsCollector _metrics;
    
    public async Task MonitorMigrationProgress()
    {
        var stopwatch = Stopwatch.StartNew();
        
        try
        {
            // 监控迁移时长
            await context.Database.MigrateAsync();
            
            var duration = stopwatch.Elapsed;
            _metrics.RecordMetric("migration_duration_seconds", duration.TotalSeconds);
            
            // 如果迁移时间过长,发出告警
            if (duration.TotalMinutes > 30)
            {
                _logger.LogWarning("Migration took {Duration} minutes!", duration.TotalMinutes);
                await SendAlert("Migration duration exceeded threshold");
            }
        }
        catch (Exception ex)
        {
            _metrics.RecordMetric("migration_failures", 1);
            await SendAlert($"Migration failed: {ex.Message}");
            throw;
        }
    }
    
    private async Task SendAlert(string message)
    {
        // 发送告警到 Slack/PagerDuty/邮件等
        await notificationService.Send(message);
    }
}

健康检查:

csharp
public class DatabaseHealthCheck : IHealthCheck
{
    private readonly AppDbContext _context;
    
    public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context)
    {
        try
        {
            // 检查数据库连接
            var canConnect = await _context.Database.CanConnectAsync();
            if (!canConnect)
            {
                return HealthCheckResult.Unhealthy("Cannot connect to database");
            }
            
            // 检查待应用的迁移
            var pendingMigrations = await _context.Database.GetPendingMigrationsAsync();
            if (pendingMigrations.Any())
            {
                return HealthCheckResult.Degraded(
                    $"Pending migrations: {string.Join(", ", pendingMigrations)}");
            }
            
            // 检查最近的应用错误
            var recentErrors = await GetRecentMigrationErrors();
            if (recentErrors > 0)
            {
                return HealthCheckResult.Warning("Recent migration errors detected");
            }
            
            return HealthCheckResult.Healthy();
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy(ex.Message);
        }
    }
}

8.3 回滚决策树 ​

mermaid
graph TD
    A[迁移失败] --> B{错误类型?}
    B -->|语法错误| C[修复代码,重新迁移]
    B -->|数据验证失败| D[清理数据,重试或回滚]
    B -->|约束冲突| E[修复约束,重试或回滚]
    B -->|超时| F[增加超时或分批处理]
    B -->|硬件故障| G[切换到备用系统]
    
    D --> H{可以修复?}
    H -->|是| I[修复后重试]
    H -->|否| J[执行回滚]
    
    E --> K{可以修复?}
    K -->|是| L[修复后重试]
    K -->|否| J
    
    J --> M{回滚成功?}
    M -->|是| N[分析原因,修复后重新迁移]
    M -->|否| O[从备份还原]
    
    O --> P[还原成功?]
    P -->|是| N
    P -->|否| Q[启动灾难恢复流程]

总结 ​

回滚策略是数据库迁移安全保障的核心:

多层次回滚体系 ​

  1. 事务级回滚 (毫秒级) - 自动回滚失败的事务
  2. 迁移级回滚 (秒级) - 使用 Down 方法撤销更改
  3. 版本级回滚 (分钟级) - 回退到历史版本
  4. 备份还原 (小时级) - 从备份完全恢复
  5. 灾难恢复 (小时到天级) - 启用备用站点

关键原则 ​

✅ 必须遵守:

  • 始终提供并测试 Down 方法
  • 迁移前自动备份数据库
  • 在测试环境验证迁移和回滚
  • 监控迁移过程并设置告警
  • 制定详细的灾难恢复计划

⚠️ 特别注意:

  • 某些操作无法在事务中执行(需手动处理回滚)
  • 数据删除操作不可逆(必须备份)
  • 大规模迁移应分批次进行
  • 准备好快速回滚的自动化脚本

🎯 零停机目标:

  • 采用蓝绿部署架构
  • 使用向后兼容的渐进式迁移
  • 实现自动化健康检查和故障切换

掌握这些回滚策略,你可以自信地应对任何迁移失败场景!

基于 MIT 许可发布