线程中止离开僵尸事务并破坏SqlConnection

wom*_*omp 16 .net c# sql-server multithreading thread-abort

我觉得这种行为不应该发生.这是场景:

  1. 启动一个长期运行的SQL事务.

  2. 运行sql命令的线程被中止(不是我们的代码!)

  3. 当线程返回托管代码时,SqlConnection的状态为"已关闭" - 但该事务仍在sql server上打开.

  4. SQLConnection可以重新打开,你可以尝试在事务上调用回滚,但它没有效果(不是我期望这种行为.重点是没有办法访问数据库上的事务并滚动它背部.)

问题只是在线程中止时没有正确清理事务.这是.Net 1.1,2.0和2.0 SP1的问题.我们正在运行.Net 3.5 SP1.

这是一个说明问题的示例程序.

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();
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

有一个针对.Net2.0 SP1Microsoft Hotfix应该解决这个问题,但我们显然有更新的DLL(.Net 3.5 SP1)与此修补程序中列出的版本号不匹配.

任何人都可以解释这种行为,以及为什么ThreadAbort 仍然没有正确清理sql事务?.Net 3.5 SP1不包含此修补程序,还是这种技术上正确的行为?

Rem*_*anu 8

由于您正在使用SqlConnection池,因此您的代码永远无法控制关闭连接.游泳池是.在服务器端,当连接真正关闭(套接字关闭)时,将回滚挂起的事务,但是在池中服务器端永远不会看到连接关闭.没有连接关闭(通过套接字/管道/ LPC层的物理断开或通过sp_reset_connection调用),服务器无法中止挂起的事务.所以它真的归结为连接没有正确释放/重置的事实.我不明白为什么你试图通过显式线程中止解雇来复杂化代码并尝试重新打开一个封闭的事务(这将永远不会起作用).你应该简单地将SqlConnection包装在一个using(...)块中,隐含的最终和连接Dispose将在线程中止时运行.

我的建议是保持简单,抛弃花哨的线程中止处理,并用一个简单的"使用"块替换它(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);
            }

        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 我反复测试了这个,确实经过几次迭代我可以解决这个问题.ADO.Net保持连接处于打开状态,因此事务不会在服务器上回滚.插入的行仍然被锁定..Net 3.5 vs. R2 (3认同)

小智 4

这是 Microsoft MARS 实现中的一个错误。在连接字符串中禁用 MARS 将使问题消失。

如果您需要 MARS,并且愿意让您的应用程序依赖于另一家公司的内部实现,请熟悉http://dotnet.sys-con.com/node/39040,分解 .NET Reflector,然后查看连接和池类。发生故障之前,您必须存储 DbConnectionInternal 属性的副本。稍后,使用反射将引用传递给内部池类中的释放方法。这将使您的连接不再持续 4:00 - 7:40 分钟。

当然还有其他方法可以将连接强制移出池并进行处置。不过,由于缺乏微软的修补程序,反思似乎是必要的。ADO.NET API 中的公共方法似乎没有帮助。