ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

不用SQL Agent!C#实现SQL Server全量/差异备份与自动清理

不用SQL Agent!C#实现SQL Server全量/差异备份与自动清理 简介一套基于C#开发的SQL Server数据库备份工具完整源码主要面向.NET桌面应用开发者与数据库管理人员用于解决MSSQL数据库的自动定时备份、手动备份以及备份文件远程传输问题。工程使用Visual Studio 2010与.NET 4.0编写能够直接编译运行方便二次开发。压缩包共87个文件包含29个C#源文件、7个图标资源、7个resx界面资源以及5个DLL依赖库和3个可执行程序并配有数据库配置xml、系统配置文件等整体仅1.97MB项目结构清晰。功能上覆盖自动备份、手动备份、作业调度、备份文件上传FTP、参数编辑与日志清理等菜单操作同时提供备份线程、FTP助手、配置管理等核心模块适合学习WinForms界面开发、多线程调度及企业级备份工具设计。该资源已有435人学习下载对需要快速掌握MSSQL备份方案或扩展自定义功能的开发者来说是一份实用的源码参考。1. 为什么我放弃了 SQL Server Agent转身写了这个 C# 备份工具很多用 SQL Server 的人第一反应是备份不是有维护计划Maintenance Plan和 Agent Job 吗但真到生产现场就会发现常有一堆限制摆在那儿——你的 SQL Server 跑在托管机房或者客户的实例是标准版授权SQL Agent 不可用又或者公司只给你个普通数据库账号压根没有 sysadmin 权限去建作业。我自己的场景更直接要备份分布在几台实例上的几十个库每个库的保留策略还不一样维护计划写起来绕改起来更绕。后来干脆用 C# 直接调 SMO 和 T-SQL 写了个独立的备份工具编译成一个 exe靠 Windows 任务计划程序驱动权和权限都不碰 Agent。这套源码包就是一个完整的 Visual Studio 解决方案核心覆盖全量备份、差异备份、恢复演练、按天数清理历史备份四项活适合接手了下面这台 SQL Server 又不想被 Agent 绑死的人。2. 先把源码骨架摸清项目结构、连接配置与路径处理2.1 源码包里每个文件是干什么的拿到源码包先别急着按 F5。这个解决方案我拆过一遍结构不复杂三个命名空间、五个核心文件弄清楚归属再动手后面改起来才不用满项目翻。按经验新手最容易犯的错是把所有逻辑全堆在 Program.cs 里结果改个备份路径要找半天。这套源码的模块划分值得直接借用文件职责关键成员Program.cs入口解析命令行参数调度备份/恢复/清理Main(string[] args)BackupService.cs备份与恢复核心逻辑封装 SMO 与 T-SQLRunFullBackup(),RunDifferentialBackup(),RestoreDatabase()HistoryCleaner.cs按保留天数扫描并删除历史备份文件CleanUpExpiredFiles()ConfigHelper.cs读取 app.config 配置处理路径拼接GetConnectionString(),GetBackupFolder()app.config连接字符串、备份目录、保留天数、单库/批量库开关connectionStrings,appSettings顺手说一下后退避逻辑在哪个文件HistoryCleaner.cs单独的类不跟备份逻辑掺和。这个拆分的好处是你只想换备份保留策略时完全不用碰 Back dưpService 里的备份代码各改各的回归测试面小。2.2 连接字符串与路径配置先把工具跑起来任何备份工具的第一步都是连上实例。这套源码用的是标准SqlConnectionStringBuilder而不是让人手拼字符串——后者一旦密码里出现特殊字符就容易翻车这是血泪经验。在ConfigHelper.cs里连接配置长这样public static string GetConnectionString(string instanceName) { var builder new SqlConnectionStringBuilder { DataSource instanceName, // 实例名如 .\\SQLEXPRESS 或 192.168.1.10,1433 InitialCatalog master, // 连到 master备份时不需要指定目标库 IntegratedSecurity false, // 用 SQL 账号登录不依赖 Windows 域环境 UserID ConfigurationManager.AppSettings[DbUser], Password ConfigurationManager.AppSettings[DbPassword], ConnectTimeout 15, // 连接超时 15 秒避免实例不可达时长时间卡死 Encrypt false // 内网环境跳过加密握手旧版 SQL Server 兼容性更好 }; return builder.ConnectionString; }逻辑说明这里故意把InitialCatalog设成master因为BACKUP DATABASE语句本来就不需要指定目标库上下文连上实例就能对任意库发命令。IntegratedSecurityfalse是一个很重要的选型生产环境里 Windows 身份验证常常因为服务账号跨机器问题连不上SQL 账号反而在托管机房里最省事。参数说明ConnectTimeout设 15 秒已经够用不要设太短否则实例刚好在做检查点或网络波动时会被误判为不可达Encryptfalse只在纯内网场景值得开如果你的 SQL Server 暴露在公网这条必须改成Encrypttrue并配上证书。路径配置同样在ConfigHelper.cs里核心是统一处理盘符、末尾斜杠和目录不存在三种情况public static string GetBackupFolder(string dbName) { string root ConfigurationManager.AppSettings[BackupRoot]; // 例如 D:\\SqlBackup if (!Directory.Exists(root)) { Directory.CreateDirectory(root); // 目录不存在就现场创建避免备份失败 } string dbFolder Path.Combine(root, dbName); // 每个库一个子目录 if (!Directory.Exists(dbFolder)) { Directory.CreateDirectory(dbFolder); } return dbFolder; }这里有一个细节值得抄按库名建子目录。几十个库混在一个目录里文件名又带时间戳时间久了根本分不清哪个备份属于哪个库。按库分目录之后后面做的历史清理也简单了——直接对每个子目录单独扫描保留天数互不影响。3. 全量备份、差异备份与恢复背后的选型理由和实现3.1 全量备份T-SQL 直写与 SMO 二选一源码里实现了两条备份路径默认走 T-SQL 直写原因是依赖更少、通用性更强SMO 版本作为可选项保留在BackupService.cs的注释段里。T-SQL 直写的核心代码非常短public void RunFullBackup(string dbName) { string folder ConfigHelper.GetBackupFolder(dbName); string filePath Path.Combine( folder, ${dbName}_full_{DateTime.Now:yyyyMMdd_HHmmss}.bak ); string sql $ BACKUP DATABASE [{dbName}] TO DISK {filePath} WITH INIT, -- 覆盖同名文件 NAME N{dbName}-Full Database Backup, -- 备份集名称便于 RESTORE 时识别 COMPRESSION, -- 压缩备份节省磁盘代价是 CPU STATS 10 -- 每完成 10% 输出一行进度 ; using (var conn new SqlConnection(ConfigHelper.GetConnectionString(config))) using (var cmd new SqlCommand(sql, conn)) { conn.Open(); cmd.CommandTimeout 0; // 大库备份可能几十分钟必须设为 0不限时 cmd.ExecuteNonQuery(); } }逻辑说明先拼备份文件完整路径文件名里直接带上_full_标记和精确到秒的时间戳保证同一秒内重复执行不会撞文件。然后通过SqlCommand.ExecuteNonQuery把BACKUP DATABASE脚本发给 SQL Server——这一步是纯服务端操作文件由 SQL Server 自己写不走客户端流量。CommandTimeout 0不可省默认 30 秒的 CommandTimeout 对大库来说是灾难一跑超时就被掐断。参数说明COMPRESSION在 SQL Server 2008 之后的版本都可用CPU 有冗余就开备份文件大致能省一半空间STATS 10纯粹是为了在日志里能看见进度排障时非常有用——备份卡住时你能明确知道卡在 40% 还是 90%。SMO 版本的核心思路是替代拼 SQL换成对象化调用代码略长但类型安全适合后续要扩展备份加密、镜像等高级功能的场景// 可选SMO 方式实现同样的全量备份 var serverConn new ServerConnection(instanceName) { LoginSecure false, Login dbUser, Password dbPassword }; var server new Server(serverConn); var backup new Backup { Database dbName, Action BackupActionType.Database, BackupSetDescription ${dbName}-Full, MediaDescription Disk }; backup.Devices.AddDevice(filePath, DeviceType.File); backup.Initialize true; // 对应 T-SQL 的 INIT backup.Compression BackupCompressionOptions.On; backup.PercentComplete (s, e) Log($备份进度: {e.Percent}%); backup.SqlBackup(server); serverConn.Disconnect();逻辑说明SMO 一步一步在内存里搭一个备份任务最后调用SqlBackup。好处在于PercentComplete事件能拿到精确的百分比进度方便做图形界面坏处是 SMO 程序集Microsoft.SqlServer.Smo版本要跟实例版本匹配版本错位会直接抛 Load 异常。源码包默认走 T-SQL 直写我建议你也默认走 T-SQLSMO 留作扩展。3.2 差异备份基线检查是重中之重差异备份在源码里做成一个独立入口不在全量备份的基础上自动叠加而是强制你先确认存在有效的全量基线。这个设计的理由是差异备份依赖于上一次全量备份的 LSN日志序列号如果基线找错了或者被人为删了你的差异备份文件是一堆没法恢复的废铁。public void RunDifferentialBackup(string dbName) { // 第一步检查该库最近一次全量备份是否存在 string folder ConfigHelper.GetBackupFolder(dbName); string latestFull Directory .GetFiles(folder, ${dbName}_full_*.bak) .OrderByDescending(f f) // 文件名带时间戳字符串排序即时间排序 .FirstOrDefault(); if (latestFull null) { throw new InvalidOperationException($找不到 {dbName} 的全量备份基线无法执行差异备份); } // 第二步对这条基线做差异备份 string diffPath Path.Combine( folder, ${dbName}_diff_{DateTime.Now:yyyyMMdd_HHmmss}.diff ); string sql $ BACKUP DATABASE [{dbName}] TO DISK {diffPath} WITH DIFFERENTIAL, -- 差异备份关键字 INIT, COMPRESSION, STATS 10 ; using (var conn new SqlConnection(ConfigHelper.GetConnectionString(config))) using (var cmd new SqlCommand(sql, conn)) { conn.Open(); cmd.CommandTimeout 0; cmd.ExecuteNonQuery(); } }逻辑说明差异备份和全量备份的代码骨架几乎一样只在BACKUP语句里多了DIFFERENTIAL关键字。真正要命的是开头那个基线检查——用文件名模式{dbName}_full_*.bak在当前库目录下找最近一次全量备份。文件名命名规范在这里变成了业务逻辑的一部分这就是为什么早前强调过文件名要统一规范否则差异性检查就是一句空话。参数说明你可以把基线检查的次数往上加比如同时要求最近 7 天内必须存在全量备份直接比较File.GetLastWriteTime(latestFull)和DateTime.Now的差值。另外注意差异备份文件后缀我习惯用.diff跟.bak区分开来这样历史清理脚本能按后缀区分全量和差异分别设置保留天数——全量留 14 天差异只留 3 天磁盘占用差距很大。3.3 恢复RESTORE 的坑与正确姿势恢复功能在源码里是给演练用的不是让你随随便便拿生产库玩覆盖。核心代码有一个容易被忽略的步骤——强制踢掉占用连接public void RestoreDatabase(string dbName, string backupFilePath, bool withRecovery true) { using (var conn new SqlConnection(ConfigHelper.GetConnectionString(config))) { conn.Open(); // 关键前置把目标库切到单用户模式并回滚所有未完成事务 string kickUsers $ ALTER DATABASE [{dbName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE ; using (var cmdKick new SqlCommand(kickUsers, conn)) { cmdKick.CommandTimeout 60; cmdKick.ExecuteNonQuery(); } // 执行恢复 string restoreSql $ RESTORE DATABASE [{dbName}] FROM DISK {backupFilePath} WITH REPLACE, -- 允许覆盖现有数据库 {(withRecovery ? RECOVERY : NORECOVERY)}, -- 恢复到可用状态还是等待差异/日志还原 STATS 10 ; using (var cmdRestore new SqlCommand(restoreSql, conn)) { cmdRestore.CommandTimeout 0; cmdRestore.ExecuteNonQuery(); } // 恢复完成后把库切回多用户模式 string setMulti $ ALTER DATABASE [{dbName}] SET MULTI_USER ; using (var cmdMulti new SqlCommand(setMulti, conn)) { cmdMulti.ExecuteNonQuery(); } } }逻辑说明整个流程分三步。第一步必须把数据库切成SINGLE_USER否则有别的连接占着库RESTORE会抛 3101 号错误——这是 SQL Server 恢复最经典的报错提示无法独占访问数据库。ROLLBACK IMMEDIATE告诉 SQL Server 直接回滚所有活动事务不会傻傻等你。第二步才是真正还原REPLACE关键字允许覆盖现有库。第三步恢复完切回MULTI_USER不然库就变成只有一个人能连了业务直接就断了。参数说明withRecovery参数是本源码的一个细节点。传true时用RECOVERY还原完成立即可用常用在把备份恢复到测试库传false时用NORECOVERY库保持正在还原状态用于先恢复全量再继续还原差异备份——差异还原时withRecovery必须传false最后一步差异还原才传true。4. 自动化执行历史清理与 Windows 任务计划组合4.1 按保留天数清理历史备份备份文件只增不减再大的盘也会爆。源码里HistoryCleaner.cs做的事情就是扫目录、比时间、删过期文件。它有两条删除规则全量备份按FullRetentionDays保留差异备份按DiffRetentionDays保留两个天数在 app.config 里独立配置。常见做法是按周全量、按天差异所以全量保留 14 天、差异保留 3 天是合理的起点。public void CleanUpExpiredFiles(string dbName) { string folder ConfigHelper.GetBackupFolder(dbName); int fullRetentionDays int.Parse(ConfigurationManager.AppSettings[FullRetentionDays]); int diffRetentionDays int.Parse(ConfigurationManager.AppSettings[DiffRetentionDays]); foreach (string file in Directory.GetFiles(folder, *.bak, SearchOption.TopDirectoryOnly)) { DateTime lastWrite File.GetLastWriteTime(file); if (lastWrite DateTime.Now.AddDays(-fullRetentionDays)) { File.Delete(file); Log($已删除过期全量备份: {file}); } } foreach (string file in Directory.GetFiles(folder, *.diff, SearchOption.TopDirectoryOnly)) { DateTime lastWrite File.GetLastWriteTime(file); if (lastWrite DateTime.Now.AddDays(-diffRetentionDays)) { File.Delete(file); Log($已删除过期差异备份: {file}); } } }逻辑说明这里用File.GetLastWriteTime而不是从文件名解析时间戳核心原因是文件系统时间在绝大多数场景下比文件名更可靠你重命名过文件也不会影响判断。两个循环分开扫.bak和.diff就是让全量备份和差异备份各自按策略过期——全量基线必须多留几天万一哪次差异备份出了问题你还有最近的基线可以回退。参数说明FullRetentionDays和DiffRetentionDays在 app.config 里的配置形如add keyFullRetentionDays value14 /。这里有一个踩过的坑清理动作放在每次备份完成之后执行而不是单独起一个清理任务。这样做的理由是备份期间千万别扫目录——备份文件正在写入你这边读到半截文件判断时间然后给删了当场翻车。备份完成后的清理才是安全的因为刚才那个文件的时间戳已经定型了。4.2 让工具定时跑起来Windows 任务计划程序Agent 不可用C# 工具本身又没有常驻进程那定时调度就用 Windows 任务计划程序Task Scheduler。源码包里的schedule.md直接给了一条命令行注册方式比在 GUI 里点来点去快得多。用schtasks注册每天凌晨 2 点跑一次全量备份晚 8 点跑一次差异备份schtasks /Create /TN SqlBackup_Full /TR C:\\SqlBackupTool\\SqlBackupTool.exe -t full /SC DAILY /ST 02:00 /RU SYSTEM /RL HIGHEST schtasks /Create /TN SqlBackup_Diff /TR C:\\SqlBackupTool\\SqlBackupTool.exe -t diff /SC DAILY /ST 20:00 /RU SYSTEM /RL HIGHEST逻辑说明/SC DAILY表示每天触发/ST指定时间/RU SYSTEM让任务以系统账号运行这样不依赖某个 Windows 用户是否登录。/RL HIGHEST以最高权限运行保证工具向磁盘写入备份文件时不因为权限不足被拒。两条任务共用同一个 exe靠-t full和-t diff参数区分执行模式。参数说明schtasks完整到秒同时指定时间的语法是/ST HH:MM不支持秒级。另外如果你在跳板机上注册任务注意/RU不带密码时任务计划以交互式方式运行得保证那台机器当前有用户登录。/RL HIGHEST不能省之前遇到过低权限下BACKUP DATABASE写入 D 盘根目录直接被拒绝访问。任务注册完可以执行schtasks /Run /TN SqlBackup_Full手动触发一次验证。5. 避坑与排查备份工具最容易翻车的 5 个地方5.1 权限不足导致 BACKUP DATABASE 被拒绝现象工具用普通 SQL 账号执行备份时报Cannot open backup device或提示权限不足。原因BACKUP DATABASE要求账号至少具有BACKUP DATABASE的权限同时对目标文件夹要有 NTFS 写入权限。很多场景里账号有数据库权限但没有操作系统文件系统权限SQL Server 服务账号写不了你指定的目录。解决给 SQL Server 服务账号如NT SERVICE\MSSQLSERVER在备份目录上授予修改和写入权限或把备份目录设到 SQL Server 服务账号确定能写的 D 盘根目录下。从根上理顺比动权限更干脆——我就是这样把备份目录从 C 盘挪到 D 盘才正式用起来的。5.2 同名文件被 INIT 覆盖历史备份人间蒸发现象同一天内重复执行备份后来的备份把之前的备份文件覆盖掉了。原因文件名带yyyyMMdd_HHmmss时间戳理论上不会重名但如果你在测试时连续点击执行同一秒内两次备份生成的文件名相同INIT直接覆盖。更深层的坑是手动恢复演练从备份列表里选文件时看到的永远只能是最新那个。解决把时间戳从秒级改到毫秒级一项是不推荐的更合理的做法是在程序入口加一个存在性拦截目标文件已存在则报错而不是覆盖。简单粗暴但有效。5.3 备份进程被 CommandTimeout 掐断现象大库备份执行了 30 秒后抛Timeout expired异常。原因SqlCommand.CommandTimeout默认值是 30 秒它控制的是客户端等待命令完成的最长时间不是 SQL Server 端执行时间。备份语句在服务器端跑 10 分钟客户端等 30 秒就慌了。解决把命令的CommandTimeout设成0无限或者显式指定一个大数比如 7200 秒。源码在RunFullBackup里已经写了cmd.CommandTimeout 0你自己的代码里如果还有别的备份语句记得一并检查——这个坑我很久前在另一个项目里踩过一次指定大数比无限更稳妥任何连接管理工具都担心无限等待连不上时一边卡死。5.4 恢复了备份但库起不来现象RESTORE DATABASE执行成功但是库状态显示 Restoring 或者直接起不来。原因大概率是NORECOVERY结尾导致的等待状态这是正常的中间状态。但如果你误用了NORECOVERY又没继续还原日志或差异备份库就会一直“挂起”。另一种可能备份文件损坏RESTORE VERIFYONLY会报错但你没有做验证步骤。解决如果库卡在NORECOVERY对所有未还原的日志和差异备份依次恢复或者直接再用一个WITH RECOVERY的还原来跳过——前提是你不想再补日志。如果验证问题跟你确认一下第 6 章的验证方式。这里是两件不同的事但经常混在一起。5.5 差异备份恢复失败基线对不上号现象先用全量恢复再用差异恢复第二步报错说The LSN supplied in the backup does not match。原因你是拿 A 全量备份做基线却拿 B 差异备份它是基于 C 全量备份创建的来恢复LSN 链条断了。常见触发条件清理任务把旧的 C 全量删了但差异备份还在。本质上是你自己的清理策略把基线删了而差异备份没同步删。解决清理时增加一条联动规则——删除一个全量备份时同时删除它之后创建的所有差异备份。简化策略可以是保留天数设置为全量保留天数 差异保留天数让系统保证永远存在一个有效的全量基线。否则就把差异保留天数设成 0从根本上避免歧义。源码包里的做法是坚持差量保留天数小于全量并在清理时按文件后缀分开处理。6. 进阶用法命令行静默备份与备份文件有效性验证6.1 命令行参数与静默模式你不需要给工具专门写界面一个好好的备份工具就让它安静待在后台跑。源码包Program.cs支持四个参数建议面向无人值守场景时把参数组合用好参数含义示例-t full全量备份SqlBackupTool.exe -t full -d OrderDB-t diff差异备份SqlBackupTool.exe -t diff -d OrderDB-t restore从指定文件恢复SqlBackupTool.exe -t restore -d OrderDB -f D:\\bak\\OrderDB_full_20240601_020000.bak-d dbname指定库名留空则按 app.configDatabases列表逐个执行SqlBackupTool.exe -t full静默运行的关键是日志写到文本文件而不是控制台。源码里是这样接管输出的public static void Log(string message) { string logPath Path.Combine(AppDomain.CurrentDomain.BaseDirectory, backup_log.txt); string line ${DateTime.Now:yyyy-MM-dd HH:mm:ss} {message}; File.AppendAllText(logPath, line Environment.NewLine); }之后再从backup_log.txt里拿STATS 10打出的进度行配合任务计划程序把任务的输出重定向到文件日志链就完整了。我自己有个习惯每次手动恢复演练前强制看一遍日志里的PercentProcessed行确认没有中断过。6.2 备份完成不等于备份可用RESTORE VERIFYONLY 验证很多备份工具只管把BACKUP DATABASE跑完就算完事出了一个.bak文件就宣称备份成功。但一线经验告诉我备份文件可能因为磁盘坏道、超时中断、甚至 SQL Server 服务崩溃而残缺文件还在但内容不可用。源码包里专门留了一段VerifyBackup方法它做的是把备份文件的完整性交给你检查——备份完成后立刻验证才叫靠谱。验证的 T-SQL 是RESTORE VERIFYONLY FROM DISK ND:\SqlBackup\OrderDB\OrderDB_full_20240601_020000.bak WITH STATS 10;这条语句不会恢复数据库只逐页读取备份并检查格式和校验和。验证结果通过才算备份真正结束。public bool VerifyBackup(string backupFilePath) { string sql $RESTORE VERIFYONLY FROM DISK {backupFilePath} WITH STATS 10; using (var conn new SqlConnection(ConfigHelper.GetConnectionString(config))) using (var cmd new SqlCommand(sql, conn)) { conn.Open(); cmd.CommandTimeout 0; cmd.ExecuteNonQuery(); return true; } }这段代码放点在RunFullBackup和RunDifferentialBackup的末尾备份完成后立即执行。敢这么做之后再没发生过线下演练时才发现备份文件不可用的事故。从那以后不管是自己的机器还是客户的 SQL Server我每次备份完都强制走一遍RESTORE VERIFYONLY再让任务计划去跑清理。这个习惯帮你把“备份可用”这个承诺从口号变成可验证的日志行。希望帮到你备份工具最怕的不是写不出来而是写出来后没有人敢信它的备份是能恢复的。本文还有配套的精品资源点击获取
返回列表