Подтвердить что ты не робот

Знать, когда повторять или терпеть неудачу при вызове SQL Server из С#?

У меня есть приложение С#, которое извлекает данные из SQL Server, размещенные в несколько уязвимой среде. Я ничего не могу сделать для решения экологических проблем, поэтому мне нужно обращаться с ними как можно более грациозно.

Чтобы сделать это, я хочу повторить операции, которые являются результатом сбоев в инфраструктуре, таких как сбои в сети, серверы SQL, выходящие из строя, поскольку они перезагружаются, тайм-ауты запроса и т.д. В то же время я Не хотите повторять запросы, если они не были выполнены для логических ошибок. Я просто хочу, чтобы те, кто запустили исключение, были до клиента.

Мой вопрос заключается в следующем: как лучше всего различать экологические проблемы (потерянные соединения, тайм-ауты) и другие виды исключений (например, логические ошибки, которые могли бы произойти, даже если бы среда была стабильной).

Существует ли в С# часто используемый шаблон для работы с такими вещами? Например, есть ли свойство, которое я могу проверить на объекте SqlConnection для обнаружения неудачных соединений? Если нет, то как лучше всего подойти к этой проблеме?

Для чего это стоит, мой код не является чем-то особенным:

using (SqlConnection connection = new SqlConnection(myConnectionString))
using (SqlCommand command = connection.CreateCommand())
{
  command.CommandText = mySelectCommand;
  connection.Open();

  using (SqlDataReader reader = command.ExecuteReader())
  {
    while (reader.Read())
    {
      // Do something with the returned data.
    }
  }
}
4b9b3361

Ответ 1

Один единственный SqlException (может) обертывает несколько ошибок SQL Server. Вы можете выполнить итерацию с помощью свойства Errors. Каждая ошибка SqlError:

foreach (SqlError error in exception.Errors)

Каждый SqlError имеет свойство Class, которое можно использовать для грубого определения, можете ли вы повторить попытку или нет (и в случае повторного запроса, если вам нужно также воссоздать соединение). Из MSDN:

  • Class < 10 для ошибок в информации, которую вы передали тогда (вероятно), вы не можете повторить попытку, если сначала вы не исправляете входы.
  • Class от 11 до 16 "генерируются пользователем", то, вероятно, снова вы ничего не сможете сделать, если пользователь сначала не исправляет свои входы. Обратите внимание, что класс 16 включает в себя множество временных ошибок, а класс 13 - для блокировок (благодаря EvZ), поэтому вы можете исключить этот класс, если будете обрабатывать их один за другим.
  • Class от 17 до 24 - это общие ошибки аппаратного и программного обеспечения, и вы можете повторить попытку. Если Class равно 20 или выше, вы должны воссоздать соединение. Обратите внимание, что 22 и 23 могут быть серьезными ошибками аппаратного и программного обеспечения, и вы можете сразу прекратить попытки. 24 указывает на ошибку носителя (что-то пользователь должен быть предупрежден, но вы можете повторить попытку, если это была просто временная ошибка).

Здесь вы можете найти более подробное описание каждого класса .

В общем случае, если вы обрабатываете ошибки в своем классе, вам не нужно точно знать каждую ошибку (используя свойство error.Number или exception.Number, которое является просто ярлыком для первого SqlError в этом списке). У этого есть недостаток, который вы можете повторить, когда это не полезно (или ошибка не может быть восстановлена). Я предлагаю двухэтапный подход:

  • Проверьте известные коды ошибок (введите коды ошибок SELECT * FROM master.sys.messages), чтобы узнать, что вы хотите обработать (зная, как это сделать). Это представление содержит сообщения на всех поддерживаемых языках, поэтому вам может потребоваться их фильтрация с помощью столбца msglangid (например, 1033 для английского).
  • Чтобы все остальное полагалось на класс ошибки, повторите попытку, когда Class составляет 13 или выше 16 (и повторно подключится, если 20 или выше).
  • Ошибки с серьезностью выше 21 (22, 23 и 24) являются серьезными ошибками, и малое ожидание не устранит эти проблемы (сама база данных также может быть повреждена).

Стратегия повторной попытки зависит от ошибки, которую вы обрабатываете: свободных ресурсов, ожидания завершения ожидающей операции, принятия альтернативного действия и т.д. В общем случае вы должны повторять попытку, только если все попробуйте-состояние ":

bool rebuildConnection = true; // First try connection must be open

for (int i=0; i < MaximumNumberOfRetries; ++i) {
    try {
        // (Re)Create connection to SQL Server
        if (rebuildConnection) {
            if (connection != null)
                connection.Dispose();

            // Create connection and open it...
        }

        // Perform your task

        // No exceptions, task has been completed
        break;
    }
    catch (SqlException e) {
        if (e.Errors.Cast<SqlError>().All(x => CanRetry(x))) {
            // What to do? Handle that here, also checking Number property.
            // For Class < 20 you may simply Thread.Sleep(DelayOnError);

            rebuildConnection = e.Errors
                .Cast<SqlError>()
                .Any(x => x.Class >= 20);

            continue; 
        }

        throw;
    }
}

Оберните все в try/finally, чтобы правильно установить соединение. С помощью этой простой фальшивой наивной функции CanRetry():

private static readonly int[] RetriableClasses = { 13, 16, 17, 18, 19, 20, 21, 22, 24 };

private static bool CanRetry(SqlError error) {
    // Use this switch if you want to handle only well-known errors,
    // remove it if you want to always retry. A "blacklist" approach may
    // also work: return false when you're sure you can't recover from one
    // error and rely on Class for anything else.
    switch (error.Number) {
        // Handle well-known error codes, 
    }

    // Handle unknown errors with severity 21 or less. 22 or more
    // indicates a serious error that need to be manually fixed.
    // 24 indicates media errors. They're serious errors (that should
    // be also notified) but we may retry...
    return RetriableClasses.Contains(error.Class); // LINQ...
}

Некоторые довольно сложные способы найти список некритических ошибок здесь.

Обычно я вставляю весь этот (шаблонный) код одним способом (где я могу скрыть все грязные вещи, сделанные для создания/удаления/воссоздания соединения) с этой сигнатурой:

public static void Try(
    Func<SqlConnection> connectionFactory,
    Action<SqlCommand> performer);

Используется следующим образом:

Try(
    () => new SqlConnection(connectionString),
    cmd => {
             cmd.CommandText = "SELECT * FROM master.sys.messages";
             using (var reader = cmd.ExecuteReader()) {
                 // Do stuff
         }
    });

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

Ответ 2

Я не знаю ни одного стандарта, но здесь список исключений Sql-Server, которые я обычно рассматривал как возвращаемые, с добавлением DTC:

catch (SqlException sqlEx)
{
    canRetry = ((sqlEx.Number == 1205) // 1205 = Deadlock
        || (sqlEx.Number == -2) // -2 = TimeOut
        || (sqlEx.Number == 3989) // 3989 = New request is not allowed to start because it should come with valid transaction descriptor
        || (sqlEx.Number == 3965) // 3965 = The PROMOTE TRANSACTION request failed because there is no local transaction active.
        || (sqlEx.Number == 3919) // 3919 Cannot enlist in the transaction because the transaction has already been committed or rolled back
        || (sqlEx.Number == 3903)); // The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION.
}

Что касается повторных попыток, предположите, что случайная задержка добавляется между повторами, чтобы уменьшить вероятность, например, те же 2 транзакции застопорились снова.

С некоторыми связанными с ошибками DTC удалением соединения может быть необходимо (или, в худшем случае, SqlClient.SqlConnection.ClearAllPools()) - в противном случае соединение путаницы будет возвращено в пул.

Ответ 3

Вы можете просто использовать свойства SqlConnectionStringBuilder для повторения соединения sql.

var conBuilder = new SqlConnectionStringBuilder(Configuration["Database:Connection"]); conBuilder.ConnectTimeout = 90; conBuilder.ConnectRetryInterval = 15; conBuilder.ConnectRetryCount = 6;

Примечание: - Требуется .Net 4.5 или новее.

Ответ 4

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

  • Уровень приложения, который вызывает слой "flaky dependency handler"
  • Уровень "flaky dependency handler", который вызывает уровень доступа к данным
  • Уровень доступа к данным, который ничего не знает о flakiness

Вся логика для повторной попытки будет находиться в этом слое обработчика, чтобы не загрязнять уровень доступа к данным с помощью логики, кроме связи с базой данных. (Таким образом, ваш код доступа к данным не нуждается в изменении. И не нужно беспокоиться о "flakiness", если он логически нуждается в изменении для новых функций.)

Образец повторной попытки может основываться на улавливании определенных исключений в контуре счетчика. (Счетчик просто для предотвращения бесконечной повторной попытки.) Что-то вроде этого:

public SomeReturnValue GetSomeData(someIdentifier)
{
    var tries = 0;
    while (tries < someConfiguredMaximum)
    {
        try
        {
            tries++;
            return someDataAccessObject.GetSomeData(someIdentifier);
        }
        catch (SqlException e)
        {
            someLogger.LogError(e);
            // maybe wait for some number of milliseconds?  make the method async if possible
        }
    }
    throw new CustomException("Maximum number of tries has been reached.");
}

Это зациклирует определенное количество раз, повторяет попытку до тех пор, пока он не будет работать или не будет достигнут максимум. После этого максимального числа создается специальное исключение для обработки приложения. Вы можете еще более точно настроить обработку исключений, проверив конкретный SqlException, который был обнаружен. Возможно, на основе сообщения об ошибке вы можете продолжить цикл или выбросить CustomException.

Вы можете дополнительно уточнить эту логику, поймав другие типы исключений, изучив их и т.д. Главное, что эта ответственность изолирована от конкретного логического уровня в приложении, насколько это возможно прозрачно для других слоев. В идеале, уровень обработчика и уровень доступа к данным реализуют одни и те же интерфейсы. Таким образом, если вы когда-либо переместили код в более стабильную среду и больше не нуждаетесь в слове обработчика, было бы тривиально удалить его без каких-либо изменений на прикладном уровне.

Ответ 5

Я не знаю настоящего стандарта. Вы можете попробовать посмотреть Блок приложений для обработки переходных процессов. Он довольно прочный, но может быть слишком "предприимчивым" для некоторых пользователей. Другим подходом может быть использование структуры аспект для ловушки ошибок. Или будет работать хорошая старая попытка/уловка.

Что касается определения того, что нужно повторить, вы, как правило, захотите посмотреть на исключение. SqlException предоставляет довольно немного информации об источнике вашей проблемы, но это может быть болезненным для его анализа. Я собрал некоторый код, чтобы разобрать их и попытаться определить, что повторится, а что нет. Это не поддерживалось некоторое время, поэтому вы, вероятно, должны воспринимать его как отправную точку, а не готовый продукт. Кроме того, это было нацелено на SQL Azure, поэтому оно может не полностью применяться к вашей ситуации (например, дросселирование ресурсов - это специфичная для Azure функция, IIRC).

/// <summary>
/// Helps to extract useful information from SQLExceptions, particularly in SQL Azure
/// </summary>
public class SqlExceptionDetails
{
    public ResourcesThrottled SeriouslyExceededResources { get; private set; }
    public ResourcesThrottled SlightlyExceededResources { get; private set; }
    public OperationsThrottled OperationsThrottled { get; private set; }
    public IList<SqlErrorCode> Errors { get; private set; }
    public string ThrottlingMessage { get; private set; }

    public bool ShouldRetry { get; private set; }
    public bool ShouldRetryImmediately { get; private set; }

    private SqlExceptionDetails()
    {
        this.ShouldRetryImmediately = false;
        this.ShouldRetry = true;
        this.SeriouslyExceededResources = ResourcesThrottled.None;
        this.SlightlyExceededResources = ResourcesThrottled.None;
        this.OperationsThrottled = OperationsThrottled.None;
        Errors = new List<SqlErrorCode>();
    }

    public SqlExceptionDetails(SqlException exception) :this(exception.Errors.Cast<SqlError>())
    {
    }

    public SqlExceptionDetails(IEnumerable<SqlError> errors) : this()
    {
        List<ISqlError> errorWrappers = (from err in errors
                                         select new SqlErrorWrapper(err)).Cast<ISqlError>().ToList();
        this.ParseErrors(errorWrappers);
    }

    public SqlExceptionDetails(IEnumerable<ISqlError> errors) : this()
    {
        ParseErrors(errors);
    }

    private void ParseErrors(IEnumerable<ISqlError> errors)
    {
        foreach (ISqlError error in errors)
        {
            SqlErrorCode code = GetSqlErrorCodeFromInt(error.Number);
            this.Errors.Add(code);

            switch (code)
            {
                case SqlErrorCode.ServerBusy:
                    ParseServerBusyError(error);
                    break;
                case SqlErrorCode.ConnectionFailed:
                    //This is a very non-specific error, can happen for almost any reason
                    //so we can't make any conclusions from it
                    break;
                case SqlErrorCode.DatabaseUnavailable:
                    ShouldRetryImmediately = false;
                    break;
                case SqlErrorCode.EncryptionNotSupported:
                    //this error code is sometimes sent by the client when it shouldn't be
                    //Therefore we need to retry it, even though it seems this problem wouldn't fix itself
                    ShouldRetry = true;
                    ShouldRetryImmediately = true;
                    break;
                case SqlErrorCode.DatabaseWorkerThreadThrottling:
                case SqlErrorCode.ServerWorkerThreadThrottling:
                    ShouldRetry = true;
                    ShouldRetryImmediately = false;
                    break;


                //The following errors are probably not going to resolved in 10 seconds
                //They're mostly related to poor query design, broken DB configuration, or too much data
                case SqlErrorCode.ExceededDatabaseSizeQuota:
                case SqlErrorCode.TransactionRanTooLong:
                case SqlErrorCode.TooManyLocks:
                case SqlErrorCode.ExcessiveTempDBUsage:
                case SqlErrorCode.ExcessiveMemoryUsage:
                case SqlErrorCode.ExcessiveTransactionLogUsage:
                case SqlErrorCode.BlockedByFirewall:
                case SqlErrorCode.TooManyFirewallRules:
                case SqlErrorCode.CannotOpenServer:
                case SqlErrorCode.LoginFailed:
                case SqlErrorCode.FeatureNotSupported:
                case SqlErrorCode.StoredProcedureNotFound:
                case SqlErrorCode.StringOrBinaryDataWouldBeTruncated:
                    this.ShouldRetry = false;
                    break;
            }
        }

        if (this.ShouldRetry && Errors.Count == 1)
        {
            SqlErrorCode code = this.Errors[0];
            if (code == SqlErrorCode.TransientServerError)
            {
                this.ShouldRetryImmediately = true;
            }
        }

        if (IsResourceThrottled(ResourcesThrottled.Quota) ||
            IsResourceThrottled(ResourcesThrottled.Disabled))
        {
            this.ShouldRetry = false;
        }

        if (!this.ShouldRetry)
        {
            this.ShouldRetryImmediately = false;
        }

        SetThrottlingMessage();
    }

    private void SetThrottlingMessage()
    {
        if (OperationsThrottled == Sql.OperationsThrottled.None)
        {
            ThrottlingMessage = "No throttling";
        }
        else
        {
            string opsThrottled = OperationsThrottled.ToString();
            string seriousExceeded = SeriouslyExceededResources.ToString();
            string slightlyExceeded = SlightlyExceededResources.ToString();

            ThrottlingMessage = "SQL Server throttling encountered. Operations throttled: " + opsThrottled
                        + ", Resources Seriously Exceeded: " + seriousExceeded
                        + ", Resources Slightly Exceeded: " + slightlyExceeded;
        }
    }

    private bool IsResourceThrottled(ResourcesThrottled resource)
    {
        return ((this.SeriouslyExceededResources & resource) > 0 ||
                (this.SlightlyExceededResources & resource) > 0);
    }

    private SqlErrorCode GetSqlErrorCodeFromInt(int p)
    {
        switch (p)
        {
            case 40014:
            case 40054:
            case 40133:
            case 40506:
            case 40507:
            case 40508:
            case 40512:
            case 40516:
            case 40520:
            case 40521:
            case 40522:
            case 40523:
            case 40524:
            case 40525:
            case 40526:
            case 40527:
            case 40528:
            case 40606:
            case 40607:
            case 40636:
                return SqlErrorCode.FeatureNotSupported;
        }

        try
        {
            return (SqlErrorCode)p;
        }
        catch
        {
            return SqlErrorCode.Unknown;
        }
    }

    /// <summary>
    /// Parse out the reason code from a ServerBusy error. 
    /// </summary>
    /// <remarks>Basic idea extracted from http://msdn.microsoft.com/en-us/library/gg491230.aspx
    /// </remarks>
    /// <param name="error"></param>
    private void ParseServerBusyError(ISqlError error)
    {
        int idx = error.Message.LastIndexOf("Code:");
        if (idx < 0)
        {
            return;
        }

        string reasonCodeString = error.Message.Substring(idx + "Code:".Length);
        int reasonCode;
        if (!int.TryParse(reasonCodeString, out reasonCode))
        {
            return;
        }

        int opsThrottledInt = (reasonCode & 3);
        this.OperationsThrottled = (OperationsThrottled)(Math.Max((int)OperationsThrottled, opsThrottledInt));


        int slightResourcesMask = reasonCode >> 8;
        int seriousResourcesMask = reasonCode >> 16;
        foreach (ResourcesThrottled resourceType in Enum.GetValues(typeof(ResourcesThrottled)))
        {
            if ((seriousResourcesMask & (int)resourceType) > 0)
            {
                this.SeriouslyExceededResources |= resourceType;
            }
            if ((slightResourcesMask & (int)resourceType) > 0)
            {
                this.SlightlyExceededResources |= resourceType;
            }
        }
    }
}

public interface ISqlError
{
    int Number { get; }
    string Message { get; }
}

public class SqlErrorWrapper : ISqlError
{
    public SqlErrorWrapper(SqlError error)
    {
        this.Number = error.Number;
        this.Message = error.Message;
    }

    public SqlErrorWrapper()
    {
    }

    public int Number { get; set; }
    public string Message { get; set; }
}

/// <summary>
/// Documents some of the ErrorCodes from SQL/SQL Azure. 
/// I have not included all possible errors, only the ones I thought useful for modifying runtime behaviors
/// </summary>
/// <remarks>
/// Comments come from: http://social.technet.microsoft.com/wiki/contents/articles/sql-azure-connection-management-in-sql-azure.aspx
/// </remarks>
public enum SqlErrorCode : int
{
    /// <summary>
    /// We don't recognize the error code returned
    /// </summary>
    Unknown = 0,

    /// <summary>
    /// A SQL feature/function used in the query is not supported. You must fix the query before it will work.
    /// This is a rollup of many more-specific SQL errors
    /// </summary>
    FeatureNotSupported = 1,

    /// <summary>
    /// Probable cause is server maintenance/upgrade. Retry connection immediately.
    /// </summary>
    TransientServerError = 40197,

    /// <summary>
    /// The server is throttling one or more resources. Reasons may be available from other properties
    /// </summary>
    ServerBusy = 40501,

    /// <summary>
    /// You have reached the per-database cap on worker threads. Investigate long running transactions and reduce server load. 
    /// http://social.technet.microsoft.com/wiki/contents/articles/1541.windows-azure-sql-database-connection-management.aspx#Throttling_Limits
    /// </summary>
    DatabaseWorkerThreadThrottling = 10928,

    /// <summary>
    /// The per-server worker thread cap has been reached. This may be partially due to load from other databases in a shared hosting environment (eg, SQL Azure).
    /// You may be able to alleviate the problem by reducing long running transactions.
    /// http://social.technet.microsoft.com/wiki/contents/articles/1541.windows-azure-sql-database-connection-management.aspx#Throttling_Limits
    /// </summary>
    ServerWorkerThreadThrottling = 10929,

    ExcessiveMemoryUsage = 40553,

    BlockedByFirewall = 40615,

    /// <summary>
    /// The database has reached the maximum size configured in SQL Azure
    /// </summary>
    ExceededDatabaseSizeQuota = 40544,

    /// <summary>
    /// A transaction ran for too long. This timeout seems to be 24 hours.
    /// </summary>
    /// <remarks>
    /// 24 hour limit taken from http://social.technet.microsoft.com/wiki/contents/articles/sql-azure-connection-management-in-sql-azure.aspx
    /// </remarks>
    TransactionRanTooLong = 40549,

    TooManyLocks = 40550,

    ExcessiveTempDBUsage = 40551,

    ExcessiveTransactionLogUsage = 40552,

    DatabaseUnavailable = 40613,

    CannotOpenServer = 40532,

    /// <summary>
    /// SQL Azure databases can have at most 128 firewall rules defined
    /// </summary>
    TooManyFirewallRules = 40611,

    /// <summary>
    /// Theoretically means the DB doesn't support encryption. However, this can be indicated incorrectly due to an error in the client library. 
    /// Therefore, even though this seems like an error that won't fix itself, it actually a retryable error.
    /// </summary>
    /// <remarks>
    /// http://social.msdn.microsoft.com/Forums/en/ssdsgetstarted/thread/e7cbe094-5b55-4b4a-8975-162d899f1d52
    /// </remarks>
    EncryptionNotSupported = 20,

    /// <summary>
    /// User failed to connect to the database. This is probably not recoverable.
    /// </summary>
    /// <remarks>
    /// Some good info on more-specific debugging: http://blogs.msdn.com/b/sql_protocols/archive/2006/02/21/536201.aspx
    /// </remarks>
    LoginFailed = 18456,

    /// <summary>
    /// Failed to connect to the database. Could be due to configuration issues, network issues, bad login... hard to tell
    /// </summary>
    ConnectionFailed = 4060,

    /// <summary>
    /// Client tried to call a stored procedure that doesn't exist
    /// </summary>
    StoredProcedureNotFound = 2812,

    /// <summary>
    /// The data supplied is too large for the column
    /// </summary>
    StringOrBinaryDataWouldBeTruncated = 8152
}