C# 调用SMO还原SQL Server数据库执行查询校验后失败如何解决
问题根因
- 核心原因是ADO.NET默认启用连接池机制:你在
CheckDatabase方法中虽然显式调用了Close()和Dispose()处理数据库连接,但该操作只是将连接放回连接池复用,并没有真正断开和SQL Server的物理连接。此时目标数据库仍然存在活动会话,而SQL Server还原数据库需要独占访问权限,存在活动连接时就会触发还原失败的报错。 - 次要问题:
RestoreDatabase方法中创建的SqlConnection没有手动释放,也会残留活动连接;CheckDatabase中重复写了资源释放逻辑,using语句已经会在代码块退出时自动释放托管资源,finally块中的重复释放属于冗余操作。
解决方案
1. 清空连接池彻底释放残留连接
在执行还原操作前调用SqlConnection.ClearAllPools(),清空所有连接池中的连接,彻底断开之前的活动会话。
2. 还原前设置数据库为单用户模式
还原数据库前先杀掉目标库的所有活动进程,设置为单用户模式,确保还原操作能获得独占访问权,还原完成后再改回多用户模式。
3. 优化冗余代码
给DataReader也加上using自动释放,抛出异常时直接用throw而非throw ex避免丢失原始堆栈信息,删除多余的手动释放逻辑。
4. 修复未释放的连接
RestoreDatabase中的SqlConnection用using包裹,用完立刻释放。
修改后的代码示例
CheckDatabase方法优化
private static void CheckDatabase(string period) { string connString = Config.ConnectionStrings.ConnectionStrings["connString"].ConnectionString; string controlProcesoCommand = "select top 5 * from ControlProceso order by FechaInicio desc;"; // using自动释放连接和命令对象,不需要额外手动释放 using (SqlConnection conn = new SqlConnection(connString)) using (SqlCommand command = new SqlCommand(controlProcesoCommand, conn)) { try { conn.Open(); // reader也用using自动释放,不需要手动Close using (var reader = command.ExecuteReader()) { while (reader.Read()) { int lastPeriodDb = int.Parse(reader["Periodo"].ToString()); int actualPeriod = int.Parse(period); if (period.Equals(reader["Periodo"].ToString())) throw new Exception(Resources.Messages.Period_already_processed); else if (lastPeriodDb > actualPeriod) throw new Exception(Resources.Messages.Period_saved_after_actual_period); else break; } } } catch (Exception ex) { // 直接throw保留原始异常堆栈,不要写throw ex throw; } } }
RestoreDatabase方法优化
private static void RestoreDatabase() { string connString = Config.ConnectionStrings.ConnectionStrings["connString"].ConnectionString; string dbName; string dataSource; // 用using包裹连接,用完立刻释放 using (SqlConnection conn = new SqlConnection(connString)) { dbName = conn.Database; dataSource = conn.DataSource; } // 清空所有连接池,彻底释放之前残留的活动连接 SqlConnection.ClearAllPools(); string restoreFilePath = Config.AppSettings.Settings["RestoreFilePath"].Value; Server myServer = new Server(dataSource); try { // 如果目标数据库存在,先杀所有进程,设置为单用户模式 if (myServer.Databases.Contains(dbName)) { myServer.KillAllProcesses(dbName); myServer.Databases[dbName].DatabaseOptions.UserAccess = DatabaseUserAccess.Single; myServer.Databases[dbName].Alter(TerminationClause.RollbackTransactionsImmediately); } Restore dbRestore = new Restore(); dbRestore.Action = RestoreActionType.Database; dbRestore.Database = dbName; dbRestore.Devices.AddDevice(restoreFilePath, DeviceType.File); dbRestore.ReplaceDatabase = true; dbRestore.SqlRestore(myServer); // 还原完成后改回多用户模式 if (myServer.Databases.Contains(dbName)) { myServer.Databases[dbName].DatabaseOptions.UserAccess = DatabaseUserAccess.Multiple; myServer.Databases[dbName].Alter(); } } catch (Exception ex) { // 异常时尝试恢复为多用户模式,避免后续无法访问数据库 if (myServer.Databases.Contains(dbName) && myServer.Databases[dbName].DatabaseOptions.UserAccess == DatabaseUserAccess.Single) { myServer.Databases[dbName].DatabaseOptions.UserAccess = DatabaseUserAccess.Multiple; myServer.Databases[dbName].Alter(); } throw; } }
可选优化
如果你不想全局清空连接池,也可以在CheckDatabase使用的连接字符串末尾加上Pooling=false,这样该连接用完就会直接物理断开,不会进入连接池。
内容的提问来源于stack exchange,提问作者Bruno Gonzalez Torres
相关产品推荐
相关产品推荐

