Czuję, że takie zachowanie nie powinno mieć miejsca. Oto scenariusz:
Rozpocznij długotrwałą transakcję sql.
Wątek, który uruchomił polecenie sql zostanie przerwany (nie przez nasz kod!)
Kiedy wątek wróci do Managed kod, stan SqlConnection to „Zamknięte” – ale transakcja jest nadal otwarte na serwerze sql.
SQLConnection można ponownie otworzyć, i możesz spróbować wywołać wycofanie transakcja, ale nie ma efekt (nie żebym oczekiwał takiego zachowania. Chodzi o to, że nie ma możliwości uzyskania dostępu do transakcji w bazie danych i wycofania jej.)
Problem polega po prostu na tym, że transakcja nie jest prawidłowo czyszczona po przerwaniu wątku. To był problem z .Net 1.1, 2.0 i 2.0 SP1. Korzystamy z platformy .Net 3.5 SP1.
Oto przykładowy program ilustrujący problem.
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();
}
}
}
}
Istnieje poprawka Microsoft skierowana do .Net2.0 SP1, która miała rozwiązać ten problem, ale my oczywiście mają nowsze biblioteki DLL (.Net 3.5 SP1), które nie są zgodne z numerami wersji wymienionymi w tej poprawce.
Czy ktoś może wyjaśnić to zachowanie i dlaczego ThreadAbort nadal nie czyści prawidłowo transakcji sql? Czy .Net 3.5 SP1 nie zawiera tej poprawki, czy to zachowanie jest technicznie poprawne?
2 odpowiedzi
Jest to błąd w implementacji MARS firmy Microsoft. Wyłączenie MARS w ciągu połączenia sprawi, że problem zniknie.
Jeśli potrzebujesz usługi MARS i nie masz nic przeciwko uzależnieniu swojej aplikacji od wewnętrznej implementacji innej firmy, zapoznaj się z http: //dotnet.sys-con.com/node/39040, uwolnij .NET Reflector i spójrz na klasy połączeń i puli. Musisz przechowywać kopię właściwości DbConnectionInternal przed awarią. Później użyj refleksji, aby przekazać odwołanie do metody cofania alokacji w klasie wewnętrznej puli. Spowoduje to zatrzymanie połączenia przez 4:00 - 7:40 minut.
Z pewnością istnieją inne sposoby na wymuszenie wyjęcia połączenia z basenu i wyrzucenie go. Jednak bez poprawki od Microsoftu, konieczna wydaje się refleksja. Wydaje się, że metody publiczne w interfejsie API ADO.NET nie pomagają.
Ponieważ używasz SqlConnection
z pulą, Twój kod nigdy nie kontroluje zamykania połączeń. Basen jest. Po stronie serwera oczekująca transakcja zostanie wycofana, gdy połączenie zostanie naprawdę zamknięte (zamknięte gniazdo), ale w przypadku łączenia w pulę po stronie serwera nigdy nie nastąpi zamknięcie połączenia. Bez zamknięcia połączenia (przez fizyczne rozłączenie w warstwie gniazda/potoku/LPC lub przez wywołanie sp_reset_connection
), serwer nie może przerwać oczekującej transakcji. Tak naprawdę sprowadza się to do tego, że połączenie nie zostaje poprawnie zwolnione/zresetowane. Nie rozumiem, dlaczego próbujesz skomplikować kod przez wyraźne odrzucenie wątku i próbujesz ponownie otworzyć zamkniętą transakcję (to nigdy zadziała). Powinieneś po prostu zawinąć SqlConnection w blok using(...)
, sugerowane w końcu i połączenie Dispose zostanie uruchomione nawet po przerwaniu wątku.
Moim zaleceniem byłoby, aby wszystko było proste, porzuć fantazyjną obsługę przerywania wątku i zastąp ją zwykłym blokiem „używania” (using(connection) {using(transaction) {code; commit () }}
.
Oczywiście zakładam, że nie przenosisz kontekstu transakcji do innego zakresu na serwerze (nie używasz sp_getbindtoken
i znajomych, a Ty nie rejestrujesz się w transakcjach rozproszonych).
Ten mały program pokazuje, że Thread.Abort prawidłowo zamyka połączenie, a transakcja jest wycofywana:
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);
}
}
}
}
Podobne pytania
Nowe pytania
c#
C # (wymawiane „patrz ostro”) jest językiem programowania wysokiego poziomu, statycznie typowanym, wieloparadygmatowym opracowanym przez firmę Microsoft. Kod C # zwykle jest przeznaczony dla rodziny narzędzi Microsoft .NET i czasów wykonywania, do których należą między innymi .NET Framework, .NET Core i Xamarin. Użyj tego tagu w przypadku pytań dotyczących kodu napisanego w C # lub C # formalnej specyfikacji.
waitfor
w TSQL.