Рекомендуемая практика прекращения транзакций, распространяющихся при использовании транзакций

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

Итак, это прекрасно:

        using (var ts = new TransactionScope())
        {
            using (var c = DatabaseManager.GetOpenConnection())
            {
                // Do Work
            }
            using (var c = DatabaseManager.GetOpenConnection())
            {
                // Do more work in same transaction using different connection
            }
            ts.Complete();
        }

Но это увеличивает транзакцию:

        using (var ts = new TransactionScope())
        {
            using (var c = DatabaseManager.GetOpenConnection())
            {
                // Do Work
                using (var nestedConnection = DatabaseManager.GetOpenConnection())
                {
                    // Do more work in same transaction using different nested connection - escalated transaction to distributed
                }
            }
            ts.Complete();
        }

Есть ли рекомендуемая практика, чтобы избежать эскалации транзакций таким образом, пока все еще используются вложенные соединения?

Самое лучшее, что я могу придумать на данный момент, это подключение ThreadStatic и повторное использование этого, если параметр Transaction.Current установлен так:

public static class DatabaseManager
{
    private const string _connectionString = "data source=.\\sql2008; initial catalog=test; integrated security=true";

    [ThreadStatic]
    private static SqlConnection _transactionConnection;

    [ThreadStatic] private static int _connectionNesting;

    private static SqlConnection GetTransactionConnection()
    {
        if (_transactionConnection == null)
        {
            Transaction.Current.TransactionCompleted += ((s, e) =>
            {
                _connectionNesting = 0;
                if (_transactionConnection != null)
                {
                    _transactionConnection.Dispose();
                    _transactionConnection = null;
                }
            });

            _transactionConnection = new SqlConnection(_connectionString);
            _transactionConnection.Disposed += ((s, e) =>
            {
                if (Transaction.Current != null)
                {
                    _connectionNesting--;
                    if (_connectionNesting > 0)
                    {
                        // Since connection is nested and same as parent, need to keep it open as parent is not expecting it to be closed!
                        _transactionConnection.ConnectionString = _connectionString;
                        _transactionConnection.Open();
                    }
                    else
                    {
                        // Can forget transaction connection and spin up a new one next time one asked for inside this transaction
                        _transactionConnection = null;
                    }
                }
            });
        }
        return _transactionConnection;
    }

    public static SqlConnection GetOpenConnection()
    {
        SqlConnection connection;
        if (Transaction.Current != null)
        {
            connection = GetTransactionConnection();
            _connectionNesting++;
        }
        else
        {
            connection = new SqlConnection(_connectionString);
        }
        if (connection.State != ConnectionState.Open)
        {
            connection.Open();
        }
        return connection;
    }
}

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

Насколько я могу видеть (используя Reflector для проверки кода), настройки подключения (строка подключения и т.д.) являются reset, и соединение закрыто. Поэтому (теоретически), переустановка строки подключения и открытие соединения при последующих вызовах должна "повторно использовать" соединение и предотвратить эскалацию (и мое первоначальное тестирование согласуется с этим).

Это кажется немного взломанным, хотя... и я уверен, что где-то должна быть лучшая практика, которая заявляет, что не следует продолжать использовать объект после его удаления!

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

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

public static class DatabaseManager
{
    private const string _connectionString = "data source=.\\sql2008; initial catalog=test; integrated security=true; enlist=true;Application Name='jimmy'";

    [ThreadStatic]
    private static bool _transactionHooked;
    [ThreadStatic]
    private static bool _openConnection;

    public static SqlConnection GetOpenConnection()
    {
        var connection = new SqlConnection(_connectionString);
        if (Transaction.Current != null)
        {
            if (_openConnection)
            {
                throw new ApplicationException("Nested connections in transaction not allowed");
            }

            _openConnection = true;
            connection.Disposed += ((s, e) => _openConnection = false);

            if (!_transactionHooked)
            {
                Transaction.Current.TransactionCompleted += ((s, e) =>
                {
                    _openConnection = false;
                    _transactionHooked = false;
                });
                _transactionHooked = true;
            }
        }
        connection.Open();
        return connection;
    }
}

По-прежнему будет стоить менее хакерское решение:)

Ответ 1

Одной из основных причин эскалации транзакций является наличие нескольких (разных) подключений в транзакции. Это почти всегда перерастает в распределенную транзакцию. И это действительно боль.

Вот почему мы гарантируем, что все наши транзакции используют один объект соединения. Есть несколько способов сделать это. По большей части мы используем статический объект потока для хранения объекта соединения, а наши классы, которые выполняют работу с базами данных, используют объект статического подключения потока (который, конечно же, разделяется). Это предотвращает использование нескольких объектов соединений и устраняет эскалацию транзакций. Вы также можете достичь этого, просто передав объект соединения из метода в метод, но это не так чисто, ИМО.