线程中止会导致僵尸事务和断开的SqlConnection。

16

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

  1. 启动一个长时间运行的SQL事务。

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

  3. 当线程返回托管代码时,SqlConnection的状态为“已关闭”-但是事务仍在SQL服务器上打开。

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

有一个Microsoft Hotfix针对.NET2.0 SP1,旨在解决这个问题,但是我们显然拥有更新的DLL(.Net 3.5 SP1),它们的版本号与此热修复程序中列出的版本号不匹配。

有人能解释一下这个行为吗?为什么ThreadAbort仍然不能正确清理SQL事务? .NET 3.5 SP1是否不包含此热修补程序,或者这是技术上正确的行为?


4
请勿发表关于不使用Thread.Abort的评论 - 我们在任何地方都没有使用它。这只是生活中的一个事实,如果您意外地获得应用程序域回收或其他情况,IIS可能会导致它们。我们在我们的代码中使用Thread.Abort :) 我们只是注意到了这种行为,并将其追溯到这种情况 - 这个示例程序显然是人为构造的。 - womp
5
如果您的代码中没有使用Thread.Abort函数,您可能需要将该注释放入到代码中,因为Thread.Abort函数在您在此处发布的代码中非常显眼。我知道这只是示范代码之类的东西,但是您应该在那里放置注释,而不是在注释中。否则,您将会收到那些评论。 - Lasse V. Karlsen
1
@Lasse V. Karlsen 我猜这是一个最小化的测试案例,而不是真正的代码 :-) - user166390
4
如果您发帖的代码与您的实际代码存在不同问题,那么您就会遭受另一个大忌——不要发布这样的问题。无论您的代码有多么人为,人们都会被您发布的代码所迷惑。他们会认为您已经将问题缩小到了这种类型的代码上,并且正在请求帮助解决您发布的代码中的问题。 - Lasse V. Karlsen
2
@Lasse V. Karlsen 这不是一个不同的问题。它是按照描述模拟问题(可能是为了让其他人可以测试它或者在单元测试中进行验证)。请注意TSQL中包含的waitfor - user166390
显示剩余12条评论
2个回答

8
由于您正在使用带有连接池的 SqlConnection,因此您的代码从未控制关闭连接。连接池在控制。在服务器端,当连接真正关闭(套接字关闭)时,挂起的事务将被回滚,但是使用连接池时,服务器端从未看到连接关闭。如果没有连接关闭(无论是通过套接字/管道/LPC层的物理断开连接还是通过sp_reset_connection调用),则服务器无法中止挂起的事务。因此,归根结底,连接没有得到适当的释放/重置。我不明白为什么你要试图用明确的线程中止解雇和尝试重新打开已关闭的事务来使代码复杂化(这永远不会起作用)。您应该简单地将 SqlConnection 包装在一个 using(...) 块中,即使在线程中止时也会运行隐含的 finally 和 connection 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);
            }

        }
    }
}

1
我们在查询中并没有采取任何高级的操作... 只是一些基本的更新语句,没有分布式事务等。示例程序只有一个微小的更新语句和一个“waitfor”,它显示了相同的问题。 - womp
我稍微简化了程序,考虑了你的一些建议。 - womp
我在 SQL 2008 R2 上测试了 .Net 4.0 和 .Net 3.5,但事务总是回滚。 - Remus Rusanu
3
经过多次测试,我确实发现在几次迭代之后就能够遇到这个问题。ADO.Net会保持连接处于打开状态,因此服务器上的事务不会被回滚。插入的行仍然被锁定。.Net 3.5与R2的区别。 - Remus Rusanu
1
我们正在与微软通话讨论这个问题... 我们可能最终会实现一个反射解决方案。请查看此文章,了解有关连接池内部的一些有趣见解:http://dotnet.sys-con.com/node/39040 - womp
显示剩余5条评论

4
这是 Microsoft 的 MARS 实现中的一个错误。在连接字符串中禁用 MARS 将使问题消失。
如果您需要 MARS,并且习惯于让应用程序依赖另一个公司的内部实现,请熟悉 http://dotnet.sys-con.com/node/39040,并使用 .NET Reflector 查看连接和池类。您必须在故障发生之前存储 DbConnectionInternal 属性的副本。稍后,使用反射将引用传递给内部池类中的释放方法。这将防止您的连接在 4:00 - 7:40 分钟内闲置。
当然还有其他方法可以强制连接退出池并被处理。但除了 Microsoft 的热修复之外,似乎需要使用反射。ADO.NET API 中的公共方法似乎没有帮助。

MARS 默认是禁用的吗? - Sal

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接