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

目录

一、代码解析:

二、解决方案

1、增加日志记录

2、异步操作

注意事项

3、增加超时机制

4、使用线程池

5、使用信号量或事件

6、监控数据库连接状态


在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) 块,这意味着代码在某个地方被阻塞或挂起,但没有抛出异常。这可能是由于以下几种情况之一:

  1. 死锁或资源争用

    • 数据库连接可能被其他操作占用,导致当前操作无法继续执行。
    • 数据库中的某个存储过程可能存在死锁,导致执行被阻塞。
  2. 无限循环或长时间运行的操作

    • 存储过程中可能存在无限循环或长时间运行的操作,导致执行时间过长。
  3. 网络问题

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

    • 当前线程可能被其他线程或操作阻塞,导致无法继续执行。
  5. 内存或资源耗尽

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

解决方法

  1. 增加日志记录

    • 在关键步骤增加更多的日志记录,以便更好地了解代码执行到哪个步骤。
  2. 检查数据库状态

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

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

    • 确保数据库服务器和应用程序之间的网络连接稳定。
  5. 监控系统资源

    • 监控系统内存、CPU 和其他资源的使用情况,确保没有资源耗尽的情况。
  6. 使用异步操作

    • 考虑将数据库操作改为异步操作,以避免阻塞主线程。

因为问题是偶发的,半年一年出现一次,也找不出什么原因,但是客户会较真,拿这个说事;以上方法,从代码层面只有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);
    }
}

注意事项

  1. 异步方法的返回类型 :异步方法的返回类型通常是 TaskTask<T>,其中 T 是方法的返回类型。
  2. 异步上下文 :确保在异步上下文中调用异步方法,例如在 async 方法中使用 await
  3. 异常处理 :异步方法中的异常处理与同步方法类似,但需要使用 await 关键字捕获异常。

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

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

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

3、增加超时机制

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

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、使用线程池

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

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;
    }
}

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

相关推荐
林鸿群9 分钟前
C#子线程更新主线程UI及委托回调使用示例
开发语言·c#
o0向阳而生0o12 分钟前
63、.NET 异常处理
c#·.net·异常处理
Channing Lewis1 小时前
sql server如何创建表导入excel的数据
数据库·oracle·excel
秃头摸鱼侠1 小时前
MySQL安装与配置
数据库·mysql·adb
UGOTNOSHOT1 小时前
每日八股文6.3
数据库·sql
行云流水行云流水1 小时前
数据库、数据仓库、数据中台、数据湖相关概念
数据库·数据仓库
John Song1 小时前
Redis 集群批量删除key报错 CROSSSLOT Keys in request don‘t hash to the same slot
数据库·redis·哈希算法
IvanCodes2 小时前
七、Sqoop Job:简化与自动化数据迁移任务及免密执行
大数据·数据库·hadoop·sqoop
tonexuan2 小时前
MySQL 8.0 绿色版安装和配置过程
数据库·mysql
JohnYan2 小时前
工作笔记- 记一次MySQL数据移植表空间错误排除
数据库·后端·mysql