16

このような振る舞いは起こらないはずだと思います。シナリオは次のとおりです。

  1. 長時間実行されるSQLトランザクションを開始します。

  2. sqlコマンドを実行したスレッドは中止されます(コードではありません!)

  3. スレッドがマネージコードに戻ると、SqlConnectionの状態は「Closed」になりますが、トランザクションはSQLサーバーで開いたままです。

  4. SQLConnectionを再度開くことができ、トランザクションでロールバックを呼び出そうとすることはできますが、効果はありません(この動作を期待するわけではありません。要点は、データベース上のトランザクションにアクセスしてロールする方法がないということです。戻る。)

問題は、スレッドが異常終了したときにトランザクションが適切にクリーンアップされないことです。これは、.Net 1.1、2.0、および2.0SP1の問題でした。.Net3.5SP1を実行しています。

これは、問題を説明するサンプルプログラムです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.Data.SqlClient;
using System.Threading;

namespace ConsoleApplication1
{
    class Run
    {
        static Thread transactionThread;

        public class ConnectionHolder : IDisposable
        {
            public void Dispose()
            {
            }

            public void executeLongTransaction()
            {
                Console.WriteLine("Starting a long running transaction.");
                using (SqlConnection _con = new SqlConnection("Data Source=<YourServer>;Initial Catalog=<YourDB>;Integrated Security=True;Persist Security Info=False;Max Pool Size=200;MultipleActiveResultSets=True;Connect Timeout=30;Application Name=ConsoleApplication1.vshost"))
                {
                    try
                    {
                        SqlTransaction trans = null;
                        trans = _con.BeginTransaction();

                        SqlCommand cmd = new SqlCommand("update <YourTable> set Name = 'XXX' where ID = @0; waitfor delay '00:00:05'", _con, trans);
                        cmd.Parameters.Add(new SqlParameter("0", 340));
                        cmd.ExecuteNonQuery();

                        cmd.Transaction.Commit();

                        Console.WriteLine("Finished the long running transaction.");
                    }
                    catch (ThreadAbortException tae)
                    {
                        Console.WriteLine("Thread - caught ThreadAbortException in executeLongTransaction - resetting.");
                        Console.WriteLine("Exception message: {0}", tae.Message);
                    }
                }
            }
        }

        static void killTransactionThread()
        {
            Thread.Sleep(2 * 1000);

            // We're not doing this anywhere in our real code.  This is for simulation
            // purposes only!
            transactionThread.Abort();

            Console.WriteLine("Killing the transaction thread...");
        }

        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main(string[] args)
        {
            using (var connectionHolder = new ConnectionHolder())
            {
                transactionThread = new Thread(connectionHolder.executeLongTransaction);
                transactionThread.Start();

                new Thread(killTransactionThread).Start();

                transactionThread.Join();

                Console.WriteLine("The transaction thread has died.  Please run 'select * from sysprocesses where open_tran > 0' now while this window remains open. \n\n");

                Console.Read();
            }
        }
    }
}

これに対処するはずの.Net2.0SP1を対象としたMicrosoftHotfixがありますが、この修正プログラムに記載されているバージョン番号と一致しない新しいDLL(.Net 3.5 SP1)があることは明らかです。

誰かがこの動作を説明できますか、そしてなぜThreadAbortがまだSQLトランザクションを適切にクリーンアップしていないのですか?.Net 3.5 SP1にはこの修正プログラムが含まれていませんか、それともこの動作は技術的に正しいですか?

4

2 に答える 2

8

プーリングで使用SqlConnectionしているため、コードが接続を閉じることを制御することはありません。プールはです。サーバー側では、接続が本当に閉じられた(ソケットが閉じられた)ときに保留中のトランザクションがロールバックされますが、プールを使用すると、サーバー側は接続が閉じられたことを認識しません。接続が閉じられない場合(ソケット/パイプ/ LPCレイヤーでの物理的な切断またはsp_reset_connection呼び出しによる)、サーバーは保留中のトランザクションを中止できません。つまり、接続が適切に解放/リセットされないという事実に要約されます。明示的なスレッドアボートの却下でコードを複雑にし、閉じられたトランザクションを再開しようとしている理由がわかりません(これは機能しません)。SqlConnectionを単純にラップする必要がありますusing(...)ブロック、暗黙のfinally、および接続Disposeは、スレッドアボートでも実行されます。

私の推奨は、物事を単純に保ち、派手なスレッドの中止処理を捨てて、それをプレーンな「using」ブロックに置き換えること(using(connection) {using(transaction) {code; commit () }}です。

もちろん、トランザクションコンテキストをサーバー内の別のスコープに伝播しないことを前提としています(sp_getbindtokenおよびフレンドを使用せず、分散トランザクションに登録しません)。

この小さなプログラムは、Thread.Abortが接続を適切に閉じ、トランザクションがロールバックされることを示しています。

using System;
using System.Data.SqlClient;
using testThreadAbort.Properties;
using System.Threading;
using System.Diagnostics;

namespace testThreadAbort
{
    class Program
    {
        static AutoResetEvent evReady = new AutoResetEvent(false);
        static long xactId = 0;

        static void ThreadFunc()
        {
            using (SqlConnection conn = new SqlConnection(Settings.Default.conn))
            {
                conn.Open();
                using (SqlTransaction trn = conn.BeginTransaction())
                {
                    // Retrieve our XACTID
                    //
                    SqlCommand cmd = new SqlCommand("select transaction_id from sys.dm_tran_current_transaction", conn, trn);
                    xactId = (long) cmd.ExecuteScalar();
                    Console.Out.WriteLine("XactID: {0}", xactId);

                    cmd = new SqlCommand(@"
insert into test (a) values (1); 
waitfor delay '00:01:00'", conn, trn);

                    // Signal readyness and wait...
                    //
                    evReady.Set();
                    cmd.ExecuteNonQuery();

                    trn.Commit();
                }
            }

        }

        static void Main(string[] args)
        {
            try
            {
                using (SqlConnection conn = new SqlConnection(Settings.Default.conn))
                {
                    conn.Open();
                    SqlCommand cmd = new SqlCommand(@"
if  object_id('test') is not null
begin
    drop table test;
end
create table test (a int);", conn);
                    cmd.ExecuteNonQuery();
                }


                Thread thread = new Thread(new ThreadStart(ThreadFunc));
                thread.Start();
                evReady.WaitOne();
                Thread.Sleep(TimeSpan.FromSeconds(5));
                Console.Out.WriteLine("Aborting...");
                thread.Abort();
                thread.Join();
                Console.Out.WriteLine("Aborted");

                Debug.Assert(0 != xactId);

                using (SqlConnection conn = new SqlConnection(Settings.Default.conn))
                {
                    conn.Open();

                    // checked if xactId is still active
                    //
                    SqlCommand cmd = new SqlCommand("select count(*) from  sys.dm_tran_active_transactions where transaction_id = @xactId", conn);
                    cmd.Parameters.AddWithValue("@xactId", xactId);

                    object count = cmd.ExecuteScalar();
                    Console.WriteLine("Active transactions with xactId {0}: {1}", xactId, count);

                    // Check count of rows in test (would block on row lock)
                    //
                    cmd = new SqlCommand("select count(*) from  test", conn);
                    count = cmd.ExecuteScalar();
                    Console.WriteLine("Count of rows in text: {0}", count);
                }
            }
            catch (Exception e)
            {
                Console.Error.Write(e);
            }

        }
    }
}
于 2011-06-02T21:33:58.073 に答える
4

これは、MicrosoftのMARS実装のバグです。接続文字列でMARSを無効にすると、問題は解決します。

MARSが必要で、アプリケーションを他社の内部実装に依存させることに慣れている場合は、http://dotnet.sys-con.com/node/39040に精通し、.NET Reflectorを分解して、接続とプールを確認してください。クラス。障害が発生する前に、DbConnectionInternalプロパティのコピーを保存する必要があります。後で、リフレクションを使用して、内部プーリングクラスの割り当て解除メソッドへの参照を渡します。これにより、接続が4:00〜7:40分間長引くのを防ぐことができます。

接続をプールから強制的に外して破棄する方法は他にもあります。ただし、Microsoftの修正プログラムがないため、反映が必要なようです。ADO.NETAPIのパブリックメソッドは役に立たないようです。

于 2011-11-08T19:01:37.823 に答える