10

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;
    }
}

編集:したがって、上記のコードのように、トランザクションスコープ内にネストされているときに同じ接続を再利用することが答えである場合、トランザクション中にこの接続を破棄することの影響について疑問に思います。

私が見る限り(リフレクターを使用してコードを調べる)、接続の設定(接続文字列など)がリセットされ、接続が閉じられます。したがって (理論的には)、接続文字列を再設定し、後続の呼び出しで接続を開くと、接続が「再利用」され、エスカレーションが防止されます (私の最初のテストではこれに同意しています)。

それは少しハックに思えます...そして、オブジェクトが破棄された後はオブジェクトを使い続けるべきではないと述べているベストプラクティスがどこかにあるに違いありません!

ただし、シールされた 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;
    }
}

ハックの少ないソリューションを引き続き評価します:)

4

1 に答える 1

3

トランザクション エスカレーションの主な理由の 1 つは、トランザクションに複数の (異なる) 接続が含まれている場合です。これはほとんどの場合、分散トランザクションにエスカレートします。そして、それは確かに苦痛です。

これが、すべてのトランザクションが単一の接続オブジェクトを使用していることを確認する理由です。これを行うにはいくつかの方法があります。ほとんどの場合、スレッド静的オブジェクトを使用して接続オブジェクトを保存し、データベースの永続化作業を行うクラスはスレッド静的接続オブジェクトを使用します (もちろん共有されます)。これにより、複数の接続オブジェクトが使用されるのを防ぎ、トランザクションのエスカレーションを排除しました。メソッドからメソッドに接続オブジェクトを渡すだけでこれを実現することもできますが、これは IMO ほどクリーンではありません。

于 2010-11-13T13:27:36.517 に答える