简介:这份 C# WinForm 数据库备份与恢复示例面向桌面应用开发者与数据库维护人员,围绕数据丢失、系统故障后的恢复需求,演示 SQLDMO 组件调用与直接执行 T-SQL 语句两条实现路径,帮助读者理解两种方案在控制能力、部署依赖和适用场景上的差异。压缩包共 27 个文件,约 582KB,以 cs 源码、resx 界面资源、exe 可执行程序、txt 说明及 dll 依赖为主,覆盖工程结构、窗体交互与数据库连接等关键部分,便于直接运行对照和拆解学习。已有 502 人学习,说明该示例在同类资料中具有一定参考价值。读者可从中获得可复用的备份恢复代码骨架、界面与业务逻辑组织方式,以及 SQLDMO 与 SQL 语句两种实现的选择思路,并可结合随包说明快速梳理工程目录与运行要点,适合初学者入门,也适合有经验的开发者查漏补缺。
1. 一个备份按钮背后藏着的两种写法:这个 WinForm Demo 解决什么问题
上周帮一位朋友救急:升级脚本跑到一半,数据库连接断了,翻出来的备份是三个月前的。他用的工具明明有备份按钮,点了也提示“备份成功”,文件也真生成了,只是生成在数据库服务器那台机器的 D 盘里,他一直在自己的客户端找。C# WinForm 写的数据库备份与恢复 Demo,看着像练手项目,真正落到现场,考的全是“路径认哪台机器、连接有没有被占用、恢复时文件往哪挪”这几件事。这个 Demo 把备份和恢复各做了两遍:一遍直接拼 T-SQL 执行 BACKUP/RESTORE,一遍用 SMO 对象模型驱动同样的动作。它适合正在给内部管理系统加运维按钮的 WinForm 开发者,也适合想搞清楚一次备份到底经过哪些环节、出问题该去哪一层查的人。
2. 项目骨架与技术栈:WinForm 界面、服务层和数据库访问怎么分工
拿到这类 Demo,先不要急着点按钮。窗体里塞满 SqlCommand 的项目我见过太多,改一个数据库名要在五个事件处理器里翻。这个项目的分层不算复杂,但边界是清楚的:界面只管收集参数和显示进度,备份恢复的业务逻辑放在服务层,连接字符串和数据库元信息单独抽出来。理清这三层,后面看两种实现方式才不会乱。
2.1 目录结构:窗体、服务、模型各管一摊
项目整体是 .NET Framework 4.7.2 的 WinForms 工程,如果你本地是 .NET 6/8,把 csproj 改成 SDK 风格、目标框架换成 net8.0-windows 也能跑,WinForms 的 API 基本没变。目录大致长这样:
DbBackupDemo/ ├── Forms/ │ ├── MainForm.cs 主界面:选择数据库、选路径、两个备份按钮 │ └── MainForm.Designer.cs ├── Services/ │ ├── IBackupService.cs 备份/恢复的统一接口 │ ├── TsqlBackupService.cs 方式一:拼 T-SQL 执行 │ └── SmoBackupService.cs 方式二:SMO 对象模型 ├── Models/ │ ├── BackupRequest.cs 数据库名、备份路径、是否压缩 │ └── BackupResult.cs 成功标志、耗时、消息、文件大小 ├── Helpers/ │ ├── SqlConnectionFactory.cs 连接字符串读取与创建 │ └── PathGuard.cs 路径合法性与目录存在性校验 ├── App.config 连接字符串、默认备份目录 └── Program.csForms里不要出现任何SqlCommand,这是我判断一个 WinForm 数据库项目能不能改的第一条标准。Services里的两个实现类都实现IBackupService,界面通过构造函数注入或者一个简单的工厂切换,工具栏上那个下拉框选“T-SQL 方式 / SMO 方式”,本质上换的就是这个实现。Models里的BackupRequest把一次备份需要的输入打包,避免方法签名越来越长。Helpers里的PathGuard看着不起眼,但它承担了“备份路径到底在哪台机器上”的第一道校验,后面避坑章节会专门说。
2.2 连接配置与依赖:SMO 是这套方案里最大的变数
连接字符串放在App.config的connectionStrings节点里,这一点没什么争议。争议在 SMO 的引用方式。老项目喜欢从 GAC 里 Add Reference 引Microsoft.SqlServer.ConnectionInfo、Microsoft.SqlServer.Smo、Microsoft.SqlServer.SmoExtended这一串,开发机上能跑,拷到客户机上就报“未能加载文件或程序集”。现在更稳妥的做法是通过 NuGet 装Microsoft.SqlServer.SqlManagementObjects,它会把依赖一起带下来,版本对齐由包管理器保证。
<!-- App.config:连接字符串单独放,避免写死在代码里 --> <configuration> <connectionStrings> <add name="MainDb" connectionString="Server=127.0.0.1;Database=DemoDb;User Id=sa;Password=******;TrustServerCertificate=True;" providerName="System.Data.SqlClient" /> </connectionStrings> <appSettings> <!-- 默认备份目录,注意这是数据库服务所在机器上的路径 --> <add key="DefaultBackupDir" value="D:\DbBackup\" /> <!-- 备份方式:Tsql 或 Smo --> <add key="BackupMode" value="Tsql" /> </appSettings> </configuration>这里的参数值得多说两句。Server写127.0.0.1还是写远程 IP,直接决定了备份文件落在哪台机器的磁盘上——BACKUP DATABASE ... TO DISK里的路径是数据库服务进程去解析的,不是你的 WinForm 程序。TrustServerCertificate=True是给新版驱动配自签证书用的,老版本 SQL Server 不加也无所谓。DefaultBackupDir这个配置项我建议在界面上做成可编辑,而不是锁死,因为不同客户的磁盘规划差别很大,锁死之后每次都要改配置文件、重启程序,很折腾。另外注意providerName保持System.Data.SqlClient还是换成Microsoft.Data.SqlClient,要和代码里using的命名空间一致,混用会在运行时抛类型转换异常。
2.3 两种方式的公共抽象:接口先定,后面才好切
先定接口再写实现,这不是为了架构好看,而是因为两种方式的行为差异必须被抹平,否则界面层要写两套按钮逻辑。接口设计得很朴素:
public interface IBackupService { // 备份:返回结果对象,不抛异常,错误信息装在 Result 里 Task<BackupResult> BackupAsync(BackupRequest request, IProgress<int> progress, CancellationToken token); // 恢复:先校验备份文件,再执行还原 Task<BackupResult> RestoreAsync(RestoreRequest request, IProgress<int> progress, CancellationToken token); }IProgress<int>这个参数是两种方式能统一起来的关键。T-SQL 方式靠WITH STATS = 10让 SQL Server 每完成 10% 就往消息通道推一条信息,程序从SqlConnection.InfoMessage事件里解析出百分比;SMO 方式则直接有PercentComplete事件。两者最终都转成 0 到 100 的整数,界面上的进度条只认这个接口,不需要知道底层是谁在干活。CancellationToken也要从一开始就留出来,备份大库动辄十几分钟,用户点了“取消”却毫无反应,体验很差——虽然后面会讲到,T-SQL 的备份其实很难真正中断,但至少能让界面先停止等待。
BackupResult里我一般放四个字段:Success、Message、ElapsedMs、FileSizeBytes。Message不做技术翻译,直接把 SQL Server 返回的原始错误文本带上,排查问题时这句话比任何自定义提示都值钱。
3. 方式一:拼 T-SQL 跑 BACKUP/RESTORE,最直白也最容易翻车
T-SQL 方式的好处是不引入任何额外依赖,只要有连接就能干活,客户机上少装一堆 DLL。坏处是所有细节都要自己拼,数据库名、文件路径、逻辑文件名、单用户模式,一个地方漏了就在现场报错。这一章把备份和恢复两条链路拆开看。
3.1 备份:BACKUP DATABASE 语句怎么拼才安全
核心就一条语句,但拼法有讲究。数据库名不能用参数占位,因为它是标识符不是值,得用QUOTENAME包一层防止奇怪的库名把语句撑破;备份路径可以用参数,SQL Server 支持在TO DISK后面跟变量。
public async Task<BackupResult> BackupAsync(BackupRequest req, IProgress<int> progress, CancellationToken token) { var sw = Stopwatch.StartNew(); // 数据库名走标识符转义,路径走参数化,两者不能混 string sql = $@" BACKUP DATABASE {QuoteName(req.DatabaseName)} TO DISK = @path WITH FORMAT, -- 覆盖同名文件,不加会追加成多卷备份 INIT, -- 显式初始化备份集 CHECKSUM, -- 写入页校验和,恢复时可验证页完整性 COMPRESSION, -- 企业版/开发版有效,标准版会报错,按需去掉 STATS = 10; -- 每 10% 回一条进度消息"; using (var conn = SqlConnectionFactory.Create()) using (var cmd = new SqlCommand(sql, conn)) { cmd.CommandTimeout = 0; // 大库备份不能设超时,0 表示不限 cmd.Parameters.Add("@path", SqlDbType.NVarChar, 500).Value = req.BackupFilePath; // InfoMessage 是 STATS 进度的唯一出口,必须提前挂上 conn.InfoMessage += (s, e) => { foreach (SqlError msg in e.Errors) { int percent; if (TryParsePercent(msg.Message, out percent)) progress?.Report(percent); } }; await conn.OpenAsync(token); await cmd.ExecuteNonQueryAsync(token); } var fi = new FileInfo(req.BackupFilePath); return BackupResult.Ok(sw.ElapsedMilliseconds, fi.Exists ? fi.Length : 0); }逐段说明。FORMAT和INIT一起用,意思是“丢掉这个文件里原有的备份集,重新写一个”。只写INIT不写FORMAT,遇到带多个备份集的文件时行为会比较绕;只写FORMAT不写INIT在多数版本里等价,但显式写出来更清楚。CHECKSUM我建议常开,代价是备份时多算一遍页校验,换来的是RESTORE VERIFYONLY能真正验证页有没有坏。COMPRESSION是个坑:SQL Server 标准版不支持备份压缩,带上这个选项会直接报错 3013 加一个内部错误,所以要么做成配置开关,要么用SERVERPROPERTY('Edition')判断一下再拼。
CommandTimeout = 0这一行是血泪经验。默认 30 秒,小库没事,几十 G 的库跑到一半连接被客户端掐断,备份文件留下一个半成品,看起来还挺像那么回事。InfoMessage事件的挂载时机也要注意,必须在OpenAsync之前,否则前几条进度消息收不到,进度条会从 10% 突然跳出来。
TryParsePercent这个辅助方法干的事情很土:SQL Server 推的消息文本形如“已处理百分之 20。”,中英文环境措辞还不一样,用正则抓数字最稳,抓不到就返回 false 忽略。这里不要试图做严格解析,进度本来就是个估计值,丢一两条不影响。
3.2 恢复:RESTORE FILELISTONLY 加 MOVE 的完整流程
恢复比备份麻烦,因为备份文件里的物理文件路径是备份时那台机器的路径。直接RESTORE DATABASE到一台目录结构不同的机器上,会报“操作系统拒绝指定的路径”或者干脆把文件写到意料之外的地方。正确流程分三步:先读文件清单,再拼MOVE子句,最后执行还原。
public async Task<BackupResult> RestoreAsync(RestoreRequest req, IProgress<int> progress, CancellationToken token) { // 第一步:读取备份集里的逻辑文件名和原始物理路径 var files = new List<(string LogicalName, string Type)>(); using (var conn = SqlConnectionFactory.Create()) { await conn.OpenAsync(token); using (var cmd = new SqlCommand("RESTORE FILELISTONLY FROM DISK = @path", conn)) { cmd.Parameters.Add("@path", SqlDbType.NVarChar, 500).Value = req.BackupFilePath; using (var reader = await cmd.ExecuteReaderAsync(token)) { while (await reader.ReadAsync(token)) { // 列 0 是 LogicalName,列 2 是 Type(D=数据文件,L=日志文件) files.Add((reader.GetString(0), reader.GetString(2))); } } } } // 第二步:为每个逻辑文件生成 MOVE 子句,落到目标目录 var moveClauses = files.Select(f => { string ext = f.Type == "L" ? ".ldf" : ".mdf"; string newPath = Path.Combine(req.TargetDataDir, f.LogicalName + ext); return $"MOVE {QuoteName(f.LogicalName)} TO {QuoteString(newPath)}"; }); // 第三步:切单用户 -> 还原 -> 切回多用户,三步必须在一个连接里连续执行 string sql = $@" ALTER DATABASE {QuoteName(req.DatabaseName)} SET SINGLE_USER WITH ROLLBACK IMMEDIATE; RESTORE DATABASE {QuoteName(req.DatabaseName)} FROM DISK = @path WITH REPLACE, RECOVERY, STATS = 10, {string.Join(",", moveClauses)}; ALTER DATABASE {QuoteName(req.DatabaseName)} SET MULTI_USER;"; // ... 执行逻辑同备份,略去进度挂载部分 }FILELISTONLY返回的结果集里,第 0 列是逻辑文件名,第 2 列是类型(D数据文件、L日志文件)。很多人图省事,直接按数据库名拼_Data.mdf、_Log.ldf,在默认实例上能蒙对,一旦备份来自改过文件名的库就翻车。按逻辑文件名生成目标路径才是稳的。
SINGLE_USER WITH ROLLBACK IMMEDIATE这句要重点说。还原时数据库上只要还有一个连接——包括你自己的连接池里那条空闲连接——就会报“无法获得对数据库的独占访问权”。WITH ROLLBACK IMMEDIATE会强行回滚并踢掉其他连接,代价是别人正在跑的事务会被打断。生产环境用这句之前,界面上最好加一个二次确认,别让手滑的人一键把业务连接全清了。RECOVERY表示还原后直接可用,如果你在做日志链还原,中间步骤要用NORECOVERY,这个 Demo 只做完整备份还原,所以固定RECOVERY。
MOVE子句的路径拼字符串时,注意用单引号包裹并且转义内部单引号,QuoteString就是干这个的。REPLACE表示覆盖同名数据库,不加的话目标库已存在会直接报错。
3.3 进度与错误处理:STATS 回调和 SqlException 分支
T-SQL 方式的错误处理要按SqlException.Number分支,不要一坨catch (Exception ex) { MessageBox.Show(ex.Message); }。常见的几个号码:3201 是备份文件不存在或无法打开,3241 是媒体集校验失败,3101 是数据库正在被使用、需要切单用户,3205 是设备名无效或者路径指向了不存在的目录。界面上按这几个号码给不同的引导文案,比把原始英文错误甩给用户强得多。
进度条这里还有一个体验细节:STATS = 10只在 10、20、30 这些整数点推消息,小库可能只推一两下,大库在最后 10% 停留很久。我的做法是把 0 到 90 映射给STATS消息,90 到 100 留给“执行完成”之后再手动补一刀,否则进度条卡在 90% 不动,用户会以为死机了。
4. 方式二:用 SMO 对象模型备份恢复,把进度条做真
SMO 是 SQL Server 的管理对象模型,本质上是对 T-SQL 的一层封装。它把备份恢复抽象成Backup、Restore两个类,事件、进度、文件重定位都有现成的属性可以用。代价是多一个 NuGet 依赖,以及版本对不齐时那一堆加载异常。
4.1 引入 SMO:Server 与 ServerConnection 的连法
先装包,dotnet add package Microsoft.SqlServer.SqlManagementObjects,它会带上Microsoft.SqlServer.ConnectionInfo等依赖。连接不是直接 newServer(connectionString)那么简单,推荐用ServerConnection包一层,这样可以在不改连接字符串的情况下复用已有的SqlConnection。
using Microsoft.SqlServer.Management.Common; using Microsoft.SqlServer.Management.Smo; // 用已有连接构造 ServerConnection,避免重复处理认证 var sqlConn = SqlConnectionFactory.Create(); var serverConn = new ServerConnection(sqlConn); var server = new Server(serverConn); try { // 访问 server.Databases 之前必须先建立连接 server.ConnectionContext.Connect(); bool exists = server.Databases.Contains(dbName); } finally { // SMO 的连接不会随 SqlConnection 自动关闭,必须显式断 server.ConnectionContext.Disconnect(); }几个容易忽略的点。new Server(sqlConn)这个重载在部分版本里会直接拿连接字符串,不要传一个已经 Open 的SqlConnection进去,容易报“连接已打开”之类的怪错,让 SMO 自己管理开关。server.Databases.Contains这行会触发实际连接,如果前面没Connect(),某些版本下会抛PropertyNotAvailableException,看起来像属性不存在,实际是没连上。最后finally里的Disconnect()一定要写,SMO 的连接泄漏在界面上表现为“操作几次之后突然连不上服务器”,很难往这里想。
如果服务端版本比较老,比如 SQL Server 2012,客户端装的是最新版 SMO 包,可能出现“服务器版本不受支持”的异常。SMO 对象库对服务端版本有兼容矩阵,不是任意组合都能用。稳妥策略是:客户端打包的 SMO 版本不要超过目标环境一到两个大版本,或者在启动时读SERVERPROPERTY('ProductMajorVersion'),低于某个值就禁用 SMO 模式、退回 T-SQL 模式。这个降级开关在模块化项目里是标配,别等到客户现场才发现。
4.2 Backup 对象:Initialize、Checksum 和 PercentComplete
SMO 的备份代码很紧凑,因为选项都变成了属性:
var backup = new Backup { Action = BackupActionType.Database, // 整库备份,不是日志或差异 Database = request.DatabaseName, Initialize = true, // 等价于 T-SQL 的 INIT Checksum = true, // 等价于 CHECKSUM CompressionOption = BackupCompressionOptions.Default, // 按服务端能力自动选 PercentCompleteNotification = 5, // 每 5% 触发一次事件 SkipTapeHeader = true }; backup.Devices.AddDevice(request.BackupFilePath, DeviceType.File); // 进度事件在 SMO 内部线程触发,UI 层必须切回主线程 backup.PercentComplete += (s, e) => { progress?.Report(e.Percent); }; // 完成事件能拿到更细的结果,包括错误信息 backup.Complete += (s, e) => { if (e.Error != null) lastError = e.Error.Message; }; backup.SqlBackup(server);Action这个属性决定备份类型,整库是Database,日志是Log,差异备份要额外配Incremental = true和BackupSetDescription。Initialize = true对应INIT,如果不设,同一路径重复备份会往同一个文件里追加备份集,文件越来越大,恢复时还得指定FILE = n,现场很容易懵。PercentCompleteNotification = 5是采样间隔,调太小事件太密反而拖慢备份,5 到 10 之间比较合适。
注意backup.PercentComplete事件是在 SMO 的内部线程上触发的,如果你直接在里面更新ProgressBar.Value,跨线程异常就来了。传IProgress<int>进去、由界面层的Progress<int>实现负责Invoke,是最省事的写法。SqlBackup是同步方法,要放到Task.Run里跑,否则整个窗体卡住,连“取消”都点不动。
和 T-SQL 方式相比,SMO 备份的路径语义是一样的——还是数据库服务器上的路径。这一点不会因为换了 API 就变,别指望它能把文件直接写到你本地磁盘。
4.3 Restore 对象:RelocateFiles 解决文件路径冲突
SMO 恢复的最大好处是文件重定位不用自己拼MOVE子句。Restore对象有一个RelocateFiles集合,你只要告诉他逻辑文件名和新路径:
var restore = new Restore { Action = RestoreActionType.Database, Database = request.DatabaseName, ReplaceDatabase = true, // 等价于 WITH REPLACE NoRecovery = false // 等价于 RECOVERY }; restore.Devices.AddDevice(request.BackupFilePath, DeviceType.File); // 先读备份集的文件列表,再逐个重定位 restore.ReadFileList(server); foreach (DataRow row in restore.ReadFileList(server).Rows) { string logical = row["LogicalName"].ToString(); string physical = row["PhysicalName"].ToString(); string ext = Path.GetExtension(physical); // 保留原扩展名 string newPath = Path.Combine(request.TargetDataDir, logical + ext); restore.RelocateFiles.Add(new RelocateFile(logical, newPath)); } restore.PercentComplete += (s, e) => progress?.Report(e.Percent); restore.SqlRestore(server);ReadFileList返回的是一个DataTable,列名是LogicalName、PhysicalName、Type。用Path.GetExtension(physical)保留原扩展名是个小技巧,比硬编码.mdf/.ldf稳,因为有些库的数据文件扩展名被改过。RelocateFiles里同一个逻辑文件名只能出现一次,重复添加会抛异常,所以循环前最好用HashSet去个重。
ReplaceDatabase = true对应WITH REPLACE,把目标库覆盖掉。这里同样有“数据库正在使用”的问题,SMO 的做法是先server.KillAllProcesses(dbName)再还原,比手写SINGLE_USER温和一点,但本质一样会踢人。如果目标库不存在,RelocateFiles依然要配,因为文件路径是按备份集里的原始路径来的,不重定位就会往旧路径写。
还有一个和 T-SQL 方式的行为差异值得注意:SMO 的SqlRestore如果目标库名和备份集里的库名不同,属于“还原为新库”,此时日志文件和数据文件必须全部重定位,漏一个就会报文件已存在。这一点在用Restore做“复制一份测试库”的时候特别常见。
5. 避坑与排查:备份文件在、恢复却失败的五个现场
这两种方式我都用在过实际项目里,出问题的姿势高度重合。下面五条是按出现频率排的,每条都按“现象、原因、解决”写,遇到时可以直接对号入座。
5.1 备份提示成功,但在客户端找不到文件
现象:界面上弹出“备份成功”,文件大小也返回了,可用户在自己电脑上翻遍磁盘也没有这个.bak。
原因:TO DISK = @path里的路径是由 SQL Server 服务进程解析的,程序只是把字符串发过去。如果连接的是远程服务器,文件就落在远程服务器的那块磁盘上;返回的文件大小是因为代码里用了FileInfo去读,读的却是客户端本地路径,正好本地也有同名文件,或者干脆读到了一个不存在路径的 0 字节结果。
解决:界面上的路径选择器改成两种模式,一种“服务器端路径”(手工输入),一种“本地路径”(用SaveFileDialog,但只用于后续下载或拷贝)。真正要落本地,得走BACKUP到服务器临时目录,再用共享目录或者文件流把文件拉回来。我一般会在路径输入框旁边加一行灰字提示“此路径位于数据库服务器”,比任何文档都管用。
5.2 恢复报“无法获得对数据库的独占访问权”
现象:RESTORE执行到一半抛错,错误号 3101,提示数据库正在使用。
原因:连接池里还有空闲连接挂着。WinForm 程序里using写得很规范,但 ADO.NET 的连接池默认会保留物理连接一段时间,Dispose只是还给池子,并没有真正关掉。
解决:在执行ALTER DATABASE ... SET SINGLE_USER WITH ROLLBACK IMMEDIATE之前先SqlConnection.ClearAllPools(),把池子清空。这一步要放在业务连接全部释放之后,顺序反了照样报错。如果还有别的进程连着(比如报表工具),那就不是代码能解决的了,得先停掉那些进程,界面上给个明确的提示,别让用户反复点重试。
5.3 恢复报“操作系统拒绝指定的路径”
现象:备份文件明明在,RESTORE却报路径被拒绝,错误号 5133 或者 3205。
原因:两个可能。一是MOVE的目标目录不存在;二是 SQL Server 服务账号对那个目录没有写权限。服务账号不是你的登录账号,你在资源管理器里能进,不代表服务进程能写。
解决:恢复前用PathGuard检查目录存在性,不存在就提示。权限问题只能到服务器上给服务账号授权,这一步没法在程序里绕过,界面上写清楚“请联系管理员为目标目录授予服务账号写权限”,比报一串英文错误有用。
5.4 SMO 模式一启动就报程序集加载失败
现象:切到 SMO 方式,点备份直接弹“未能加载文件或程序集 Microsoft.SqlServer.ConnectionInfo, Version=...”。
原因:引用了 GAC 里的旧版本,或者 NuGet 包的版本和运行环境不匹配。更隐蔽的一种是发布时没有把 SMO 相关 DLL 一起拷到输出目录,开发机上 GAC 里有,客户机上没有。
解决:统一走 NuGet 包,发布时确认输出目录下有Microsoft.SqlServer.*.dll这一组文件。如果项目同时引用了多个来源的 SMO 程序集,用bindingRedirect把版本统一到一个,否则运行时会挑错版本加载。这个问题排查起来很玄学,报错信息只说“加载失败”,不说为什么,所以我在启动时就加一段自检:反射加载Microsoft.SqlServer.Management.Smo.Backup类型,失败了直接把 SMO 模式禁掉并给出提示。
5.5 还原成新库之后,文件路径还是旧库的
现象:用 SMO 或 T-SQL 把备份还原成一个新库名,结果数据文件还是指向原库的mdf,或者报“文件已存在”。
原因:备份集里记录的是备份时的物理路径,还原时如果不重定位,SQL Server 会尝试用原路径,而原路径的文件正被原库占用。
解决:T-SQL 方式用RESTORE FILELISTONLY读出逻辑名,逐个拼MOVE;SMO 方式用RelocateFiles。两条路都要做,没有哪条能省。另外目标目录下如果已经堆了同名的旧文件,REPLACE只覆盖数据库不覆盖文件,得先手动清理,或者在文件名上带上时间戳避免冲突。
6. 二次开发:把它改成能放进运维工具箱的小工具
Demo 能跑通只是起点,真要拿去给运维同事用,还差几块拼图。我按优先级排一下自己的改造顺序,顺便说验证方法。
第一块是备份校验。备份完不验证,等于没备份。加一个RESTORE VERIFYONLY FROM DISK = @path WITH CHECKSUM,执行成功才算这条备份记录有效。SMO 方式对应restore.SqlVerify(server),还能拿到具体的校验结果对象。这个动作很便宜,大库也就几分钟,但能把“备份文件损坏”这类问题挡在恢复之前。验证完把结果写进一张BackupLog表,字段包括数据库名、文件路径、大小、耗时、校验结果,以后查“上周三到底有没有备份成功”不用翻日志文件。
第二块是批量与定时。Demo 是单库操作,实际场景里一台服务器上挂七八个库,逐个点不现实。把服务层的BackupAsync循环包一层,外面套一个Timer或者干脆做成控制台任务由计划任务拉起。批量执行时注意串行而不是并行,多个大库同时备份会把磁盘 IO 打满,业务查询直接拖垮。串行虽然慢,但稳。
第三块是配置外置与静默运行。把备份目录、保留天数、是否压缩、是否校验都挪到配置文件里,程序支持-silent参数时不开窗体,直接跑一遍退出,返回码 0 表示成功、非 0 表示失败。这样它就能被监控系统调起来,失败了自动告警,不用人盯着。
验证改造是否到位,我的做法是搭一个“假故障”清单:把备份目录设成只读、把网络中途断开、把备份文件手工截断一半,然后逐个跑一遍,看程序给的提示是不是能指导下一步操作。提示里只有一句“操作失败”的,一律打回重写。
| 改造项 | 改动位置 | 验证方式 |
|---|---|---|
| 备份校验 | 服务层 BackupAsync 末尾 | 手工损坏 bak 文件,确认校验失败 |
| 批量备份 | 新增 BatchRunner | 三个库连续跑,看日志顺序与耗时 |
| 静默模式 | Program.cs 参数解析 | 计划任务拉起,检查退出码 |
| 保留策略 | 新增 CleanupService | 造几个过期文件,确认被清理 |
最后说一句踩过的坑。早期我图省事,把备份和恢复放在同一个窗体事件里,用Thread.Sleep轮询文件大小当进度,结果大库备份时界面假死,用户以为卡住就强杀进程,备份文件留下半截还提示成功。从那以后我每次加这类耗时操作,都强制走一遍“异步加进度接口加取消令牌”的三件套,哪怕项目再小也不省这一步。希望帮到你。
本文还有配套的精品资源,点击获取