目录
在C#程序操作Sql Server数据库的实际应用中,若异常就会抛出异常,我们还能找到异常的原因,进一步去解决;但偶发的不返回、也不抛出异常的情况真是令人头疼,下面我将从C#程序代码层面去分析解决思路,先上代码
cs
/// <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;
}
}
一、代码解析:
我们解析下这个函数,这个也是一个比较简单常用的方法:
这个函数的作用是执行一个存储过程,并返回该存储过程执行后影响的行数。下面是对函数各个部分的详细解释:
函数定义
cs
/// <summary>
/// 执行存储过程,返回影响的行数
/// </summary>
/// <param name="procedureName">存储过程名</param>
/// <param name="parameters">自定义参数数组,根据调用数据库的种类,给参数中相应的数组赋值</param>
/// <returns></returns>
public int ExecuteProcedure(string procedureName, object[] aParas)
- summary: 该函数用于执行存储过程并返回影响的行数。
- param :
procedureName
是存储过程的名称。 - param :
parameters
是一个自定义参数数组,根据调用数据库的种类,给参数中相应的数组赋值。 - returns: 返回存储过程执行后影响的行数。
函数实现
cs
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、增加日志记录
关键步骤增加日志记录,以便更好地了解代码执行情况:
cs
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
方法:
cs
using System.Data;
using System.Data.SqlClient;
using System.Threading.Tasks;
public async Task<int> 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
关键字,并且调用方法也必须是异步的。例如:
cs
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>
,其中T
是方法的返回类型。 - 异步上下文 :确保在异步上下文中调用异步方法,例如在
async
方法中使用await
。 - 异常处理 :异步方法中的异常处理与同步方法类似,但需要使用
await
关键字捕获异常。
通过将数据库操作改为异步操作,可以提高应用程序的性能和响应性,特别是在处理大量数据或长时间运行的操作时。
?????但是并不是所有程序都适合异步操作,那么将如何处理呢?
如果程序逻辑不能使用异步方式,但仍然遇到在调用 ExecuteProcedure
方法后没有到达 return rows;
语句的问题,可以考虑以下几种方法来解决这个问题:
3、增加超时机制
为数据库操作增加超时机制,确保操作不会无限期地阻塞。可以使用 SqlCommand
的 CommandTimeout
属性来设置超时时间。
cs
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
来实现这一点。
cs
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、使用信号量或事件
使用信号量或事件来同步主线程和数据库操作线程。
cs
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、监控数据库连接状态
在执行数据库操作之前,检查数据库连接的状态,确保连接是可用的。
cs
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;
}
}
通过以上方法,可以在不改变程序逻辑的情况下,增加超时机制、使用线程池、信号量或事件来解决数据库操作阻塞的问题。这些方法可以帮助确保数据库操作在合理的时间内完成,并在出现异常时及时处理。