Как получить эффективную обработку взаимоблокировок Sql Server на С# с помощью ADO?

У меня есть класс «База данных», который работает как оболочка для ADO.net. Например, когда мне нужно выполнить процедуру, я вызываю Database.ExecuteProcedure(procedureName, parametersAndItsValues).

У нас возникают серьезные проблемы с ситуациями взаимоблокировки в SQL Server 2000. Часть нашей команды работает над кодом sql и транзакциями, чтобы свести к минимуму эти события, но я думаю о том, чтобы сделать этот класс базы данных устойчивым к ситуациям взаимоблокировки.

Мы хотим, чтобы жертва взаимоблокировки повторила попытку, возможно, после некоторой задержки, но я не знаю, возможно ли это. Вот код метода, который мы используем:

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?


person Victor Rodrigues    schedule 26.11.2008    source источник


Ответы (4)


Во-первых, я бы просмотрел свой код 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;
  }
}
person Sam Saffron    schedule 02.12.2008
comment
Рассмотрите возможность добавления короткой задержки в цикле повторных попыток, например Thread.Sleep(100); после retryCount--;, чтобы предотвратить слишком сильное воздействие на SQL Server. - person Doug Knudsen; 13.02.2017

Основываясь на ответе @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;
        }
    });
}
person Neil    schedule 14.07.2011

Если тупиковая ситуация может быть разрешена на уровне данных, это определенно правильный путь. Блокировка подсказок, переработка работы модуля и так далее. Однако 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;
    }
}
person Community    schedule 02.12.2008
comment
Я заинтересован в использовании вышеуказанного решения, но был бы признателен за дополнительную информацию о том, как именно это может работать в моем случае. - person Kobojunkie; 17.10.2010
comment
голосование за случайный таймер, решение Sams забивает сервер sql - person DeveloperChris; 04.05.2011
comment
Я предлагаю последней строкой быть throw;, а не throw exDead; - person Phil Haselden; 17.03.2020

Если у вас возникают проблемы с взаимоблокировками, лучше посмотреть, что делает код SQL. Например, взаимные блокировки эскалации блокировок очень легко создать, если у вас есть сериализуемый уровень изоляции (или любой эквивалент в вашей rdbms) — и их можно смягчить несколькими способами, такими как изменение порядка запросов или (в SQL Server по крайней мере) используя (UPDLOCK) для блокировки записи раньше (чтобы вы не получили конкурирующую блокировку чтения).

Повторная попытка будет смешанной... например, если вы находитесь в TransactionScope, она уже может быть прервана. Но только на пуристском уровне - если у меня возникнут проблемы с общением с БД, я хочу, чтобы мой код паниковал, и паниковал рано... повторная попытка кажется немного хакерской в ​​этом конкретном сценарии.

person Marc Gravell    schedule 26.11.2008
comment
Мы рассматриваем весь sql код и транзакции, это и делается, мы не пытаемся только обмануть дедлок отзывом на c#. Но параллелизм может привести к возникновению взаимоблокировок, и поэтому мы хотим сделать наши приложения более надежными, когда это произойдет. - person Victor Rodrigues; 26.11.2008
comment
Виктор, с тупиками можно и нужно бороться, решая их на уровне данных. После решения вам вообще ничего не нужно делать на С#. - person Dave Markle; 28.11.2008
comment
@Dave - это, возможно, слишком упрощенно - person Marc Gravell; 29.11.2008
comment
@Marc - Верно, но для людей, задающих такие вопросы, в 99% случаев они исходят из опыта, когда у них или у них нет большого опыта в обработке транзакций, и они чрезмерно усложняют проблему, пытаясь решить ее более чем в 1 месте. - person Dave Markle; 30.11.2008