【问题标题】:Thread abort leaves zombie transactions and broken SqlConnection线程中止留下僵尸事务和损坏的 SqlConnection
【发布时间】:2011-06-02 19:20:09
【问题描述】:

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

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

有一个Microsoft Hotfix targeted at .Net2.0 SP1 that was supposed to address this,但我们显然有较新的 DLL (.Net 3.5 SP1) 与此修补程序中列出的版本号不匹配。

谁能解释这种行为,为什么 ThreadAbort 仍然没有正确清理 sql 事务? .Net 3.5 SP1 是否不包含此修补程序,或者此行为在技术上是正确的?

【问题讨论】:

  • 请不要对不使用 Thread.Abort 的 cmets - 我们没有在任何地方使用它。如果您不小心获得了应用程序域回收或其他原因,IIS 抛出的问题只是有时会导致它们。我们没有在代码中的任何地方使用 Thread.Abort :) 我们刚刚注意到这种行为并将其追溯到这种情况 - 示例程序显然是人为的。
  • 如果您没有在代码中的任何地方使用 Thread.Abort,您可能希望将该注释 放入该代码,因为 Thread.Abort 非常显眼地放置在右侧在您在此处发布的代码中间。我知道这是示例代码之类的,但是您应该将注释放在那里,而不是放在注释中。否则你得到那些cmets。
  • 哈哈...我太慢了,无法先发制人。 SqlConnection 在 try/catch 之外的唯一原因是我可以在捕获 ThreadAbort 时尝试重新打开它。这个例子完全是人为的——它并不代表我们的真实代码。我们的交易并不完全是长期的。在非常重的负载下,有问题的查询的执行时间达到了大约 5 秒,这是我们开始注意到问题的时候。再次 - 人为的例子。我们能否请您关注我所询问的实际行为?
  • 但是你已经成为另一个大禁忌的牺牲品,不要发布与生产代码有不同问题的代码的问题。人们会挂断你发布的代码,不管它是多么做作。他们会假设您已将问题缩小到此类代码,并正在寻求帮助以修复发布的代码。
  • @Lasse V. Karlsen 这不是一个不同的问题。它按照描述模拟问题(大概是为了让其他人可以对其进行测试,或者可以在单元测试中对其进行验证)。请注意 TSQL 中包含的 waitfor

标签: c# .net sql-server multithreading thread-abort


【解决方案1】:

由于您将SqlConnection 与池结合使用,因此您的代码永远无法控制关闭连接。游泳池是。在服务器端,当连接真正关闭(套接字关闭)时,待处理的事务将被回滚,但通过池化服务器端永远不会看到连接关闭。如果没有关闭连接(通过套接字/管道/LPC 层的物理断开连接或通过sp_reset_connection 调用),服务器无法中止待处理的事务。所以它真的归结为连接没有正确释放/重置的事实。我不明白您为什么要尝试通过显式中止解除线程来使代码复杂化并尝试重新打开已关闭的事务(这将永远起作用)。您应该简单地将 SqlConnection 包装在 using(...) 块中,隐含的 finally 和连接 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);
            }

        }
    }
}

【讨论】:

  • 如果我放弃线程中止,那么它不是在模拟问题。即使在 using 块内,事务仍然在服务器上打开。运行程序,看看....更改连接以使用 using 块...同样的问题...问题是连接返回到池中而没有清理数据库上的事务。
  • 我们并没有在查询中做任何花哨的事情......这是非常基本的更新语句,没有分布式事务等 - 示例程序只有一个微小的更新语句和一个“等待”和它显示了同样的问题。
  • 我稍微简化了程序以考虑您的一些建议。
  • 我反复测试了这个,实际上经过几次迭代我可以解决这个问题。 ADO.Net 使连接保持打开状态,因此事务不会在服务器上回滚。插入的行仍处于锁定状态。 .Net 3.5 与 R2
  • 我们已经通过电话联系了 MS……我们最终可能会实施反射解决方法。查看这篇文章,了解有关连接池内部结构的一些有趣见解:dotnet.sys-con.com/node/39040
【解决方案2】:

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

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

肯定有其他方法可以强制连接从池中出来并被处理掉。但是,由于缺少 Microsoft 的修补程序,反射似乎是必要的。 ADO.NET API 中的公共方法似乎没有帮助。

【讨论】:

  • MARS不是默认禁用的吗?
猜你喜欢
  • 2014-10-10
  • 2016-03-31
  • 1970-01-01
  • 2010-11-12
  • 1970-01-01
  • 1970-01-01
  • 2011-04-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多