Рекомендуемая практика для остановки транзакций, преобразующихся в распределенные при использовании транзакции - PullRequest
10 голосов
/ 13 ноября 2010

Использование объекта 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's 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 для проверки кода), настройки соединения (строка соединения и т. Д.) Сбрасываются, а соединение закрывается. Таким образом (теоретически), переустановка строки соединения и открытие соединения при последующих вызовах должно «повторно использовать» соединение и предотвратить эскалацию (и мое первоначальное тестирование с этим согласно).

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

Тем не менее, поскольку я не могу создать подкласс запечатанного 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 Ответ

3 голосов
/ 13 ноября 2010

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

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

...