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

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

  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=;Initial Catalog=;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  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..."); } ///  /// The main entry point for the application. ///  [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.0 SP1的Microsoft Hotfix应该解决这个问题 ,但我们显然有更新的DLL(.Net 3.5 SP1)与此修复程序中列出的版本号不匹配。

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

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

如果您需要MARS,并且习惯于使您的应用程序依赖于其他公司的内部实现,请熟悉http://dotnet.sys-con.com/node/39040 ,打破.NET Reflector,并查看连接和池类。 您必须在发生故障之前存储DbConnectionInternal属性的副本。 稍后,使用reflection将引用传递给内部池类中的释放方法。 这将阻止您的连接延迟4:00 – 7:40分钟。

肯定还有其他方法可以强制连接出池并进行处理。 但是,如果没有Microsoft的修补程序,reflection似乎是必要的。 ADO.NET API中的公共方法似乎没有帮助。

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

我的建议是保持简单,抛弃花哨的线程中止处理并用普通的’using’块替换它(using(connection) {using(transaction) {code; commit () }}

当然,我假设您没有将事务上下文传播到服务器中的不同范围(您不使用sp_getbindtoken和friends,并且您不注册分布式事务)。

这个小程序显示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); } } } }