C#程序调用Sql Server存储过程异常处理:调用存储过程后不返回、不抛异常的解决方案

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 本文分析了C#程序操作Sql Server数据库时偶发的不返回、不抛异常问题,并提出了解决思路。首先解析了一个执行存储过程的函数`ExecuteProcedure`,其功能是调用存储过程并返回影响行数。针对代码执行被阻塞但无异常的情况,文章总结了可能原因,如死锁、无限循环或网络问题等。随后提供了多种解决方案:1) 增加日志定位问题;2) 使用异步操作提升响应性;3) 设置超时机制避免阻塞;4) 利用线程池分离主线程;5) 通过信号量同步线程;6) 监控数据库连接状态确保可用性。这些方法可有效应对数据库操作中的潜在问题,保障程序稳定性。


目录

一、代码解析:

二、解决方案

在C#程序操作Sql Server数据库的实际应用中,若异常就会抛出异常,我们还能找到异常的原因,进一步去解决;但偶发的不返回、也不抛出异常的情况真是令人头疼,下面我将从C#程序代码层面去分析解决思路,先上代码

    /// <summary>
    /// 执行存储过程,返回影响的行数
    /// </summary>
    /// <param name="procedureName">存储过程名</param>
    /// <param name="parameters">自定义参数数组,根据调用数据库的种类,给参数中相应的数组赋值</param>
    /// <returns></returns>
    public int ExecuteProcedure(string procedureName, object[] aParas)
    {
        SqlCommand cmd = new SqlCommand();
        try
        {
            PrepareCommand(con, cmd, null, CommandType.StoredProcedure, procedureName, aParas);
            int rows = cmd.ExecuteNonQuery();
            cmd.Parameters.Clear();
            return rows;
        }
        catch (Exception err)
        {
            throw err;
        }
    } 

一、代码解析:
我们解析下这个函数,这个也是一个比较简单常用的方法:

这个函数的作用是执行一个存储过程,并返回该存储过程执行后影响的行数。下面是对函数各个部分的详细解释:

函数定义

///
/// 执行存储过程,返回影响的行数
///
///

存储过程名
/// 自定义参数数组,根据调用数据库的种类,给参数中相应的数组赋值
///
public int ExecuteProcedure(string procedureName, object[] aParas)
summary: 该函数用于执行存储过程并返回影响的行数。
param: procedureName 是存储过程的名称。
param: parameters 是一个自定义参数数组,根据调用数据库的种类,给参数中相应的数组赋值。
returns: 返回存储过程执行后影响的行数。
函数实现

public int ExecuteProcedure(string procedureName, object[] aParas)
{
//创建SqlCommand对象
SqlCommand cmd = new SqlCommand();
try
{
//调用PrepareCommand方法来设置SqlCommand对象的参数,包括连接对象con、命令对象cmd、命令类型(这里是存储过程)、存储过程名称和参数数组。
PrepareCommand(con, cmd, null, CommandType.StoredProcedure, procedureName, aParas);
//使用ExecuteNonQuery方法执行存储过程,并返回影响的行数。
int rows = cmd.ExecuteNonQuery();
//清除SqlCommand对象的参数,以便下次使用。
cmd.Parameters.Clear();
return rows;
}
catch (Exception err)
{
//捕获并抛出任何可能发生的异常。
throw err;
}
}
这个函数的主要作用是执行一个存储过程,并返回该存储过程执行后影响的行数。它通过设置SqlCommand对象的参数,执行存储过程,并处理可能发生的异常。

二、解决方案
问题:在实际应用中遇到这种情况,即在调用 ExecuteProcedure 方法后,没有到达 return rows; 语句,也没有进入 catch (Exception err) 块,这意味着代码在某个地方被阻塞或挂起,但没有抛出异常。这可能是由于以下几种情况之一:

数据库连接可能被其他操作占用,导致当前操作无法继续执行。
数据库中的某个存储过程可能存在死锁,导致执行被阻塞。

存储过程中可能存在无限循环或长时间运行的操作,导致执行时间过长。

数据库服务器或网络连接可能出现问题,导致请求无法完成。

当前线程可能被其他线程或操作阻塞,导致无法继续执行。

系统内存或资源可能耗尽,导致无法继续执行。
解决方法

在关键步骤增加更多的日志记录,以便更好地了解代码执行到哪个步骤。

检查数据库服务器的状态,查看是否有死锁或其他问题。
使用数据库管理工具监控数据库性能和资源使用情况。

检查存储过程的逻辑,确保没有无限循环或长时间运行的操作。
优化存储过程的性能,减少资源消耗。

确保数据库服务器和应用程序之间的网络连接稳定。

监控系统内存、CPU 和其他资源的使用情况,确保没有资源耗尽的情况。

考虑将数据库操作改为异步操作,以避免阻塞主线程。
因为问题是偶发的,半年一年出现一次,也找不出什么原因,但是客户会较真,拿这个说事;以上方法,从代码层面只有1、6适用 ,当然我们也可以通过控制超时 时间来处理,实用解决方案如下:

1、增加日志记录
关键步骤增加日志记录,以便更好地了解代码执行情况:

public int ExecuteProcedure(string procedureName, object[] aParas)
{
SqlCommand cmd = new SqlCommand();
try
{
// 增加日志记录
Log("Preparing command for procedure: " + procedureName);
PrepareCommand(con, cmd, null, CommandType.StoredProcedure, procedureName, aParas);

    // 增加日志记录
    Log("Executing command for procedure: " + procedureName);
    int rows = cmd.ExecuteNonQuery();

    // 增加日志记录
    Log("Command executed successfully, rows affected: " + rows);
    cmd.Parameters.Clear();
    return rows;
}
catch (Exception err)
{
    // 增加日志记录
    Log("Exception occurred: " + err.Message);
    throw err;
}

}

private void Log(string message)
{
// 实现日志记录逻辑,例如写入文件或数据库
Console.WriteLine(message);
}
通过增加日志记录,可以更好地了解代码执行到哪个步骤,从而更容易定位问题,这种特殊情况也只能定位到走哪一行就没往下走了。

2、异步操作
将数据库操作改为异步操作可以提高应用程序的响应性和性能,特别是在处理大量数据或长时间运行的操作时。

使用 ExecuteNonQueryAsync 方法

SqlCommand 类提供了 ExecuteNonQueryAsync 方法,用于异步执行 SQL 命令。以下是修改后的 ExecuteProcedure 方法:

using System.Data;
using System.Data.SqlClient;
using System.Threading.Tasks;

public async Task ExecuteProcedureAsync(string procedureName, object[] aParas)
{
SqlCommand cmd = new SqlCommand();
try
{
// 增加日志记录
Log("Preparing command for procedure: " + procedureName);
PrepareCommand(con, cmd, null, CommandType.StoredProcedure, procedureName, aParas);

    // 异步执行命令
    Log("Executing command for procedure: " + procedureName);
    int rows = await cmd.ExecuteNonQueryAsync();

    // 增加日志记录
    Log("Command executed successfully, rows affected: " + rows);
    cmd.Parameters.Clear();
    return rows;
}
catch (Exception err)
{
    // 增加日志记录
    Log("Exception occurred: " + err.Message);
    throw err;
}

}

private void Log(string message)
{
// 实现日志记录逻辑,例如写入文件或数据库
Console.WriteLine(message);
}
调用异步方法

在调用异步方法时,需要使用 await 关键字,并且调用方法也必须是异步的。例如:

public async Task SomeMethodAsync()
{
string procedureName = "YourStoredProcedureName";
object[] parameters = new object[] { / 参数值 / };

try
{
    int rowsAffected = await ExecuteProcedureAsync(procedureName, parameters);
    Console.WriteLine("Rows affected: " + rowsAffected);
}
catch (Exception ex)
{
    Console.WriteLine("Exception: " + ex.Message);
}

}
注意事项
异步方法的返回类型:异步方法的返回类型通常是 Task 或 Task,其中 T 是方法的返回类型。
异步上下文:确保在异步上下文中调用异步方法,例如在 async 方法中使用 await。
异常处理:异步方法中的异常处理与同步方法类似,但需要使用 await 关键字捕获异常。
通过将数据库操作改为异步操作,可以提高应用程序的性能和响应性,特别是在处理大量数据或长时间运行的操作时。

?????但是并不是所有程序都适合异步操作,那么将如何处理呢?

如果程序逻辑不能使用异步方式,但仍然遇到在调用 ExecuteProcedure 方法后没有到达 return rows; 语句的问题,可以考虑以下几种方法来解决这个问题:

3、增加超时机制
为数据库操作增加超时机制,确保操作不会无限期地阻塞。可以使用 SqlCommand 的 CommandTimeout 属性来设置超时时间。

public int ExecuteProcedure(string procedureName, object[] aParas)
{
SqlCommand cmd = new SqlCommand();
try
{
// 设置命令超时时间为30秒
cmd.CommandTimeout = 30;

    PrepareCommand(con, cmd, null, CommandType.StoredProcedure, procedureName, aParas);
    int rows = cmd.ExecuteNonQuery();
    cmd.Parameters.Clear();
    return rows;
}
catch (Exception err)
{
    throw err;
}

}
这个超时机制在特殊情况下没有用,亲测没用,就算不设置CommandTimeout属性的值,也有默认的吧,特殊情况等多久都没返回没异常。

4、使用线程池
将数据库操作放在单独的线程中执行,并在主线程中等待结果。可以使用 ThreadPool 或 Task 来实现这一点。

using System.Threading;

public int ExecuteProcedure(string procedureName, object[] aParas)
{
int rows = 0;
bool isCompleted = false;
Exception exception = null;

ThreadPool.QueueUserWorkItem(_ =>
{
    SqlCommand cmd = new SqlCommand();
    try
    {
        PrepareCommand(con, cmd, null, CommandType.StoredProcedure, procedureName, aParas);
        rows = cmd.ExecuteNonQuery();
        cmd.Parameters.Clear();
    }
    catch (Exception err)
    {
        exception = err;
    }
    finally
    {
        isCompleted = true;
    }
});

// 等待操作完成或超时
DateTime startTime = DateTime.Now;
while (!isCompleted && (DateTime.Now - startTime).TotalSeconds < 30)
{
    Thread.Sleep(100);
}

if (exception != null)
{
    throw exception;
}

if (!isCompleted)
{
    throw new TimeoutException("Database operation timed out.");
}

return rows;

}
5、使用信号量或事件
使用信号量或事件来同步主线程和数据库操作线程。

using System.Threading;

public int ExecuteProcedure(string procedureName, object[] aParas)
{
int rows = 0;
Exception exception = null;
ManualResetEvent completionEvent = new ManualResetEvent(false);

ThreadPool.QueueUserWorkItem(_ =>
{
    SqlCommand cmd = new SqlCommand();
    try
    {
        PrepareCommand(con, cmd, null, CommandType.StoredProcedure, procedureName, aParas);
        rows = cmd.ExecuteNonQuery();
        cmd.Parameters.Clear();
    }
    catch (Exception err)
    {
        exception = err;
    }
    finally
    {
        completionEvent.Set();
    }
});

// 等待操作完成或超时
if (!completionEvent.WaitOne(TimeSpan.FromSeconds(30)))
{
    throw new TimeoutException("Database operation timed out.");
}

if (exception != null)
{
    throw exception;
}

return rows;

}
6、监控数据库连接状态
在执行数据库操作之前,检查数据库连接的状态,确保连接是可用的。

public int ExecuteProcedure(string procedureName, object[] aParas)
{
SqlCommand cmd = new SqlCommand();
try
{
// 检查数据库连接状态
if (con.State != ConnectionState.Open)
{
con.Open();
}

    PrepareCommand(con, cmd, null, CommandType.StoredProcedure, procedureName, aParas);
    int rows = cmd.ExecuteNonQuery();
    cmd.Parameters.Clear();
    return rows;
}
catch (Exception err)
{
    throw err;
}

}

通过以上方法,可以在不改变程序逻辑的情况下,增加超时机制、使用线程池、信号量或事件来解决数据库操作阻塞的问题。这些方法可以帮助确保数据库操作在合理的时间内完成,并在出现异常时及时处理。

相关实践学习
使用SQL语句管理索引
本次实验主要介绍如何在RDS-SQLServer数据库中,使用SQL语句管理索引。
SQL Server on Linux入门教程
SQL Server数据库一直只提供Windows下的版本。2016年微软宣布推出可运行在Linux系统下的SQL Server数据库,该版本目前还是早期预览版本。本课程主要介绍SQLServer On Linux的基本知识。 相关的阿里云产品:云数据库RDS&nbsp;SQL Server版 RDS SQL Server不仅拥有高可用架构和任意时间点的数据恢复功能,强力支撑各种企业应用,同时也包含了微软的License费用,减少额外支出。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/sqlserver
相关文章
|
8月前
|
SQL 安全 网络安全
SQL安装程序规则错误解决方案
在安装SQL Server时,遇到安装程序规则错误是一个比较常见的问题
|
8月前
|
SQL 安全 Windows
SQL安装程序规则错误解析与解决方案
在安装SQL Server时,用户可能会遇到安装程序规则错误的问题,这些错误通常与系统配置、权限设置、依赖项缺失或版本不兼容等因素有关
|
8月前
|
SQL 安全 关系型数据库
SQL错误代码1303解析与解决方案:深入理解并应对权限问题
在数据库管理和开发过程中,遇到错误代码是常见的事情,每个错误代码都代表着一种特定的问题
|
8月前
|
SQL 数据库
执行 Transact-SQL 语句或批处理时发生了异常。 (Microsoft.SqlServer.ConnectionInfo)之解决方案
执行 Transact-SQL 语句或批处理时发生了异常。 (Microsoft.SqlServer.ConnectionInfo)之解决方案
1018 0
|
2月前
|
SQL 数据库 数据安全/隐私保护
数据库数据恢复——sql server数据库被加密的数据恢复案例
SQL server数据库数据故障: SQL server数据库被加密,无法使用。 数据库MDF、LDF、log日志文件名字被篡改。 数据库备份被加密,文件名字被篡改。
|
3月前
|
SQL 数据库连接 Linux
数据库编程:在PHP环境下使用SQL Server的方法。
看看你吧,就像一个调皮的小丑鱼在一片广阔的数据库海洋中游弋,一路上吞下大小数据如同海中的珍珠。不管有多少难关,只要记住这个流程,剩下的就只是探索未知的乐趣,沉浸在这个充满挑战的数据库海洋中。
85 16
|
9月前
|
SQL 数据库
数据库数据恢复—SQL Server数据库报错“错误823”的数据恢复案例
SQL Server附加数据库出现错误823,附加数据库失败。数据库没有备份,无法通过备份恢复数据库。 SQL Server数据库出现823错误的可能原因有:数据库物理页面损坏、数据库物理页面校验值损坏导致无法识别该页面、断电或者文件系统问题导致页面丢失。
194 13
数据库数据恢复—SQL Server数据库报错“错误823”的数据恢复案例
|
4月前
|
SQL 数据库
数据库数据恢复—SQL Server报错“错误 823”的数据恢复案例
SQL Server数据库附加数据库过程中比较常见的报错是“错误 823”,附加数据库失败。 如果数据库有备份则只需还原备份即可。但是如果没有备份,备份时间太久,或者其他原因导致备份不可用,那么就需要通过专业手段对数据库进行数据恢复。
|
5月前
|
数据库 Windows
SqlServer数据恢复—SqlServer数据库所在分区损坏的数据恢复案例
一块硬盘上存放的SqlServer数据库,windows server操作系统+NTFS文件系统。由于误操作导致分区损坏,需要恢复硬盘里的SqlServer数据库数据。
|
7月前
|
SQL 存储 Linux
从配置源到数据库初始化一步步教你在CentOS 7.9上安装SQL Server 2019
【11月更文挑战第16天】本文介绍了在 CentOS 7.9 上安装 SQL Server 2019 的详细步骤,包括配置系统源、安装 SQL Server 2019 软件包以及数据库初始化,确保 SQL Server 正常运行。
326 4