如何使用 ADO 在 C# 中获得高效的 Sql Server 死锁处理?

Posted

技术标签:

【中文标题】如何使用 ADO 在 C# 中获得高效的 Sql Server 死锁处理?【英文标题】:How to get efficient Sql Server deadlock handling in C# with ADO? 【发布时间】:2010-09-24 03:40:51 【问题描述】:

我有一个作为 ADO.net 包装器的“数据库”类。例如,当我需要执行一个过程时,我调用 Database.ExecuteProcedure(procedureName, parametersAndItsValues)。

我们在 SQL Server 2000 中遇到了死锁情况的严重问题。我们团队的一部分正在研究 sql 代码和事务以最大限度地减少这些事件,但我正在考虑使这个 Database 类对死锁情况具有鲁棒性。

我们希望死锁受害者在延迟一段时间后重试,但我不知道这是否可能。这是我们使用的方法的代码:

public int ExecuteQuery(string query)

    int rows = 0;

    try
    
        Command.Connection = Connection;
        Command.CommandType = CommandType.Text;

        if(DatabaseType != enumDatabaseType.ORACLE)
          Command.CommandText = query;
        else
          Command.CommandText ="BEGIN " +  query + " END;";



        if (DatabaseType != enumDatabaseType.SQLCOMPACT)
            Command.CommandTimeout = Connection.ConnectionTimeout;

        if (Connection.State == ConnectionState.Closed)
            Connection.Open();

        rows = Command.ExecuteNonQuery();
    
    catch (Exception exp)
    
        //Could I add here any code to handle it?
        throw new Exception(exp.Message);
    
    finally
    
        if (Command.Transaction == null)
        
            Connection.Close();
            _connection.Dispose();
            _connection = null;
            Command.Dispose();
            Command = null;
        
    
    return rows;

我可以在 catch 块中进行这种处理吗?

【问题讨论】:

【参考方案1】:

如果可以在数据层解决死锁,那肯定是要走的路。锁定提示,重新设计模块的工作方式等等。不过,NoLock 并不是万能药——有时由于事务完整性的原因无法使用它,而且我遇到过使用 NoLock 的所有相关表读取直接(尽管很复杂)数据的情况,这仍然会导致其他查询阻塞。

无论如何——如果你不能在数据层解决它,那又如何

bool OK = false;
Random Rnd = new Random();

while(!OK)

    try
    
        rows = Command.ExecuteNonQuery();
        OK = true;
    
    catch(Exception exDead)
    
        if(exDead.Message.ToLower().Contains("deadlock"))
            System.Threading.Thread.Sleep(Rnd.Next(1000, 5000));
        else
            throw exDead;
    

【讨论】:

我对使用上述解决方案很感兴趣,但希望获得更多关于这对我的案例究竟如何起作用的信息。 支持随机计时器,Sams 解决方案重创 sql server 我建议最后一行应该是throw; 而不是throw exDead;【参考方案2】:

基于@Sam 的回复,我提出了一个通用的重试包装方法:

private static T Retry<T>(Func<T> func)

    int count = 3;
    TimeSpan delay = TimeSpan.FromSeconds(5);
    while (true)
    
        try
        
            return func();
        
        catch(SqlException e)
        
            --count;
            if (count <= 0) throw;

            if (e.Number == 1205)
                _log.Debug("Deadlock, retrying", e);
            else if (e.Number == -2)
                _log.Debug("Timeout, retrying", e);
            else
                throw;

            Thread.Sleep(delay);
        
    


private static void Retry(Action action)

    Retry(() =>  action(); return true; );


// Example usage
protected static void Execute(string connectionString, string commandString)

    _log.DebugFormat("SQL Execute \"0\" on 1", commandString, connectionString);

    Retry(() => 
        using (SqlConnection connection = new SqlConnection(connectionString))
        using (SqlCommand command = new SqlCommand(commandString, connection))
            command.ExecuteNonQuery();
    );


protected static T GetValue<T>(string connectionString, string commandString)

    _log.DebugFormat("SQL Scalar Query \"0\" on 1", commandString, connectionString);

    return Retry(() =>  
        using (SqlConnection connection = new SqlConnection(connectionString))
        using (SqlCommand command = new SqlCommand(commandString, connection))
        
            object value = command.ExecuteScalar();
            if (value is DBNull) return default(T);
            return (T) value;
        
    );

【讨论】:

【参考方案3】:

首先,我将查看我的 SQL 2000 代码并了解为什么会发生这种死锁。解决这个问题可能会隐藏一个更大的问题(例如,缺少索引或错误的查询)。

其次,我会检查我的架构,以确认确实需要如此频繁地调用死锁语句(select count(*) from bob 是否必须每秒调用 100 次?)。

但是,如果您确实需要一些死锁支持并且您的 SQL 或体系结构中没有错误,请尝试以下几行。 (注意:我不得不将这种技术用于每秒支持数千个查询的系统,并且很少会遇到死锁)

int retryCount = 3;
bool success = false;  
while (retryCount > 0 && !success) 

  try
  
     // your sql here
     success = true; 
   
  catch (SqlException exception)
  
     if (exception.Number != 1205)
     
       // a sql exception that is not a deadlock 
       throw; 
     
     // Add delay here if you wish. 
     retryCount--; 
     if (retryCount == 0) throw;
  

【讨论】:

考虑在Thread.Sleep(100); 之后在retryCount--; 之后在重试循环中添加一个短暂的延迟,以防止对SQL Server 造成如此严重的打击。【参考方案4】:

如果遇到死锁问题,最好看看 SQL 代码在做什么。例如,如果您具有可序列化的隔离级别(或 rdbms 中的任何等效隔离级别),那么锁升级死锁很容易创建 - 并且可以通过几种方式缓解,例如重新排序查询,或者(在 SQL Server至少)使用 (UPDLOCK) 更早地获得写锁(这样你就不会获得竞争读锁)。

重试将是混合的......例如,如果您在 TransactionScope 中,它可能已经中止。但只是在纯粹主义者的层面上 - 如果我在与 db 交谈时遇到问题,我希望我的代码恐慌,并尽早恐慌......在这种特殊情况下重试似乎有点 hacky。

【讨论】:

我们正在考虑所有 sql 代码和事务,这正在完成,我们不只是试图通过在 c# 中调用来欺骗死锁。但是并发会造成死锁牺牲品,因此我们希望在发生这种情况时让我们的应用更加健壮。 Victor - 你可以而且应该通过在数据层解决死锁来处理它们。解决后,您无需在 C# 中执行任何操作。 @Dave - 这可能过于简单了 @Marc - 没错,但对于提出这类问题的人来说,99% 的时间他们来自于他们或他们在交易处理方面没有丰富经验的背景,他们试图在不止一个地方解决问题,从而使问题过于复杂。

以上是关于如何使用 ADO 在 C# 中获得高效的 Sql Server 死锁处理?的主要内容,如果未能解决你的问题,请参考以下文章

通过 C# 使用 ado.net 将值插入 SQL Server 数据库

C# 如何确定SQL Server 中数据表是否存在

如何使用 ADO.net 在视图中包含相关对象

ADO.NET 中参数化 SQL 命令的最终形式

ADO.NET SQL

使用 ADO.NET 将数据表从内存推送到 SQL Server