Appearance
回滚策略
数据库迁移的回滚策略是保证系统稳定性的关键。本章将深入探讨 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 回滚策略的重要性
为什么要制定回滚策略?
- 数据完整性保护 - 防止部分应用的迁移导致数据不一致
- 业务连续性保障 - 快速恢复服务,减少停机时间
- 风险控制 - 降低生产环境变更的风险
- 合规要求 - 满足审计和监管要求
回滚的层次:
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 listC# 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 回滚策略最佳实践
✅ 应该做的:
始终测试 Down 方法
bash# 测试回滚 dotnet ef database update TargetMigration dotnet ef database update PreviousMigration # 测试 Down dotnet ef database update TargetMigration # 再次应用迁移前备份数据库
csharppublic async Task SafeMigrate() { // 1. 备份 var backupPath = await backupService.CreateBackupBeforeMigration(); try { // 2. 迁移 await context.Database.MigrateAsync(); } catch { // 3. 失败时还原 await backupService.RestoreFromBackup(backupPath); throw; } }在小规模环境先测试
yaml# CI/CD 流水线 stages: - test_migration_on_staging - backup_production - deploy_to_production - verify_health - cleanup_backup # 保留一定时间设置合理的超时和重试
csharppublic 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[启动灾难恢复流程]总结
回滚策略是数据库迁移安全保障的核心:
多层次回滚体系
- 事务级回滚 (毫秒级) - 自动回滚失败的事务
- 迁移级回滚 (秒级) - 使用 Down 方法撤销更改
- 版本级回滚 (分钟级) - 回退到历史版本
- 备份还原 (小时级) - 从备份完全恢复
- 灾难恢复 (小时到天级) - 启用备用站点
关键原则
✅ 必须遵守:
- 始终提供并测试 Down 方法
- 迁移前自动备份数据库
- 在测试环境验证迁移和回滚
- 监控迁移过程并设置告警
- 制定详细的灾难恢复计划
⚠️ 特别注意:
- 某些操作无法在事务中执行(需手动处理回滚)
- 数据删除操作不可逆(必须备份)
- 大规模迁移应分批次进行
- 准备好快速回滚的自动化脚本
🎯 零停机目标:
- 采用蓝绿部署架构
- 使用向后兼容的渐进式迁移
- 实现自动化健康检查和故障切换
掌握这些回滚策略,你可以自信地应对任何迁移失败场景!