Что является причиной "контекста транзакции, используемого другим сеансом"
Я ищу описание корня этой ошибки: "Контекст транзакции используется другим сеансом".
Иногда я получаю его на одном из моих тестов юнитов, поэтому я не могу воспроизвести код провайдера. Но мне интересно, что является "по замыслу" причиной ошибки.
ОБНОВЛЕНИЕ: ошибка возвращается как SqlException из SQL Server 2008. Место, где я получаю сообщение об ошибке, кажется однопоточным. Но, вероятно, у меня есть взаимодействие с юнит-тестами, поскольку я получаю сообщение об ошибке, когда запускаю несколько тестов одновременно (MSTest в VS2008sp1). Но провальный тест выглядит так:
- создать объект и сохранить его внутри DB-транзакции (commit)
- создать TransactionScope
- пытаясь открыть соединение - здесь я получаю SqlException с такой stacktrace:
,
System.Data.SqlClient.SqlException: Transaction context in use by another session.
at System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection)
at System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection)
at System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj)
at System.Data.SqlClient.TdsParser.Run(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj)
at System.Data.SqlClient.TdsParser.TdsExecuteTransactionManagerRequest(Byte[] buffer, TransactionManagerRequestType request, String transactionName, TransactionManagerIsolationLevel isoLevel, Int32 timeout, SqlInternalTransaction transaction, TdsParserStateObject stateObj, Boolean isDelegateControlRequest)
at System.Data.SqlClient.SqlInternalConnectionTds.PropagateTransactionCookie(Byte[] cookie)
at System.Data.SqlClient.SqlInternalConnection.EnlistNonNull(Transaction tx)
at System.Data.SqlClient.SqlInternalConnection.Enlist(Transaction tx)
at System.Data.SqlClient.SqlInternalConnectionTds.Activate(Transaction transaction)
at System.Data.ProviderBase.DbConnectionInternal.ActivateConnection(Transaction transaction)
at System.Data.ProviderBase.DbConnectionPool.GetConnection(DbConnection owningObject)
at System.Data.ProviderBase.DbConnectionFactory.GetConnection(DbConnection owningConnection)
at System.Data.ProviderBase.DbConnectionClosed.OpenConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory)
at System.Data.SqlClient.SqlConnection.Open()
Я нашел эти сообщения:
- http://blogs.msdn.com/asiatech/archive/2009/08/10/system-transaction-may-fail-in-multiple-thread-environment.aspx
- http://msdn.microsoft.com/en-us/library/ff649002.aspx
Но я не могу понять, что означает "Несколько потоков, совместно использующих одну и ту же транзакцию в области транзакции, вызовут следующее исключение:" Контекст транзакции используется другим сеансом. "". Все слова понятны, но не в этом суть.
Я действительно могу разделить системную транзакцию между потоками. Для этого есть даже специальный механизм - класс DependentTransaction и метод Transaction.DependentClone.
Я пытаюсь воспроизвести случай использования из первого поста:
- Основной поток создает транзакцию DTC, получает DependentTransaction (создается с использованием Transaction.Current.DependentClone в основном потоке
- Дочерний поток 1 включается в эту транзакцию DTC, создавая область транзакции на основе зависимой транзакции (передаваемой через конструктор)
- Дочерняя нить 1 открывает соединение
- Дочерний поток 2 включается в транзакцию DTC, создавая область транзакции на основе зависимой транзакции (передаваемой через конструктор)
- Дочерняя нить 2 открывает соединение
с таким кодом:
using System;
using System.Threading;
using System.Transactions;
using System.Data;
using System.Data.SqlClient;
public class Program
{
private static string ConnectionString = "Initial Catalog=DB;Data Source=.;User ID=user;PWD=pwd;";
public static void Main()
{
int MAX = 100;
for(int i =0; i< MAX;i++)
{
using(var ctx = new TransactionScope())
{
var tx = Transaction.Current;
// make the transaction distributed
using (SqlConnection con1 = new SqlConnection(ConnectionString))
using (SqlConnection con2 = new SqlConnection(ConnectionString))
{
con1.Open();
con2.Open();
}
showSysTranStatus();
DependentTransaction dtx = Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete);
Thread t1 = new Thread(o => workCallback(dtx));
Thread t2 = new Thread(o => workCallback(dtx));
t1.Start();
t2.Start();
t1.Join();
t2.Join();
ctx.Complete();
}
trace("root transaction completes");
}
}
private static void workCallback(DependentTransaction dtx)
{
using(var txScope1 = new TransactionScope(dtx))
{
using (SqlConnection con2 = new SqlConnection(ConnectionString))
{
con2.Open();
trace("connection opened");
showDbTranStatus(con2);
}
txScope1.Complete();
}
trace("dependant tran completes");
}
private static void trace(string msg)
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId + " : " + msg);
}
private static void showSysTranStatus()
{
string msg;
if (Transaction.Current != null)
msg = Transaction.Current.TransactionInformation.DistributedIdentifier.ToString();
else
msg = "no sys tran";
trace( msg );
}
private static void showDbTranStatus(SqlConnection con)
{
var cmd = con.CreateCommand();
cmd.CommandText = "SELECT 1";
var c = cmd.ExecuteScalar();
trace("@@TRANCOUNT = " + c);
}
}
Сбой при вызове Complete корневого TransactionScope. Но ошибка другая: необработанное исключение: System.Transactions.TransactionInDoubtException: транзакция находится в сомнении. ---> Пэрид Время ожидания истекло до завершения операции или сервер не отвечает.
Подводя итог: я хочу понять, что означает "контекст транзакции, используемый другим сеансом" и как его воспроизвести.
6 ответов
Уже немного поздно для ответа:) но надеюсь, что это будет полезно для других. Ответ состоит из трех частей:
- Что означает "контекст транзакции, используемый другим сеансом".
- Как воспроизвести ошибку "Контекст транзакции используется другим сеансом".
1. Что означает "контекст транзакции, используемый другим сеансом".
Важное замечание: блокировка контекста транзакции получается непосредственно перед и снимается сразу после взаимодействия между SqlConnection
и SQL Server.
Когда вы выполняете некоторые SQL-запросы, SqlConnection
"выглядит", есть ли какая-либо транзакция, заключающая его Может быть SqlTransaction
("родной" для SqlConnection) или Transaction
от System.Transactions
сборка.
Когда транзакция найдена SqlConnection
использует его для связи с SQL Server, и в данный момент они общаются Transaction
контекст исключительно заблокирован.
Что значит TransactionScope
? Это создает Transaction
и предоставляет.NET Framework Components информацию об этом, так что каждый, включая SqlConnection, может (и по своему замыслу) использовать его.
Так декларируя TransactionScope
мы создаем новую транзакцию, которая доступна для всех "транзакционных" объектов, созданных в текущем Thread
,
Описанная ошибка означает следующее:
- Мы создали несколько
SqlConnections
под тем жеTransactionContext
(что означает, что они относятся к одной и той же транзакции) - Мы спросили эти
SqlConnection
общаться с SQL Server одновременно - Один из них заблокирован током
Transaction
контекст и следующая ошибка
2. Как воспроизвести ошибку "Контекст транзакции используется другим сеансом".
Прежде всего, контекст транзакции используется ("заблокирован") прямо во время выполнения команды sql. Так что точно воспроизвести такое поведение сложно.
Но мы можем попытаться сделать это, запустив несколько потоков, выполняющих относительно длинные операции SQL в рамках одной транзакции. Давайте подготовим стол [dbo].[Persons]
в [tests]
База данных:
USE [tests]
GO
DROP TABLE [dbo].[Persons]
GO
CREATE TABLE [dbo].[Persons](
[Id] [bigint] IDENTITY(1,1) NOT NULL PRIMARY KEY,
[Name] [nvarchar](1024) NOT NULL,
[Nick] [nvarchar](1024) NOT NULL,
[Email] [nvarchar](1024) NOT NULL)
GO
DECLARE @Counter INT
SET @Counter = 500
WHILE (@Counter > 0) BEGIN
INSERT [dbo].[Persons] ([Name], [Nick], [Email])
VALUES ('Sheev Palpatine', 'DarthSidious', 'spalpatine@galaxyempire.gov')
SET @Counter = @Counter - 1
END
GO
И воспроизвести "Контекст транзакции, используемый другим сеансом". ошибка с кодом C# на примере кода Shrike
using System;
using System.Collections.Generic;
using System.Threading;
using System.Transactions;
using System.Data.SqlClient;
namespace SO.SQL.Transactions
{
public static class TxContextInUseRepro
{
const int Iterations = 100;
const int ThreadCount = 10;
const int MaxThreadSleep = 50;
const string ConnectionString = "Initial Catalog=tests;Data Source=.;" +
"User ID=testUser;PWD=Qwerty12;";
static readonly Random Rnd = new Random();
public static void Main()
{
var txOptions = new TransactionOptions();
txOptions.IsolationLevel = IsolationLevel.ReadCommitted;
using (var ctx = new TransactionScope(
TransactionScopeOption.Required, txOptions))
{
var current = Transaction.Current;
DependentTransaction dtx = current.DependentClone(
DependentCloneOption.BlockCommitUntilComplete);
for (int i = 0; i < Iterations; i++)
{
// make the transaction distributed
using (SqlConnection con1 = new SqlConnection(ConnectionString))
using (SqlConnection con2 = new SqlConnection(ConnectionString))
{
con1.Open();
con2.Open();
}
var threads = new List<Thread>();
for (int j = 0; j < ThreadCount; j++)
{
Thread t1 = new Thread(o => WorkCallback(dtx));
threads.Add(t1);
t1.Start();
}
for (int j = 0; j < ThreadCount; j++)
threads[j].Join();
}
dtx.Complete();
ctx.Complete();
}
}
private static void WorkCallback(DependentTransaction dtx)
{
using (var txScope1 = new TransactionScope(dtx))
{
using (SqlConnection con2 = new SqlConnection(ConnectionString))
{
Thread.Sleep(Rnd.Next(MaxThreadSleep));
con2.Open();
using (var cmd = new SqlCommand("SELECT * FROM [dbo].[Persons]", con2))
using (cmd.ExecuteReader()) { } // simply recieve data
}
txScope1.Complete();
}
}
}
}
И в заключение несколько слов о реализации поддержки транзакций в вашем приложении:
- Избегайте многопоточных операций с данными, если это возможно (независимо от загрузки или сохранения). Например сохранить
SELECT
/UPDATE
/ etc... запросы в одной очереди и их обработка однопоточным рабочим; - В многопоточных приложениях используются транзакции. Всегда. Везде. Даже для чтения;
- Не разделяйте одну транзакцию между несколькими потоками. Это вызывает странные, неочевидные, трансцендентные и невоспроизводимые сообщения об ошибках:
- "Контекст транзакции используется другим сеансом.": Несколько одновременных взаимодействий с сервером в рамках одной транзакции;
- "Время ожидания истекло. Время ожидания истекло до завершения операции или сервер не отвечает.": Независящие транзакции были завершены;
- "Сделка под вопросом.";
- ... и я предполагаю много других...
- Не забудьте установить уровень изоляции для
TransactionScope
, По умолчаниюSerializable
но в большинстве случаевReadCommitted
достаточно; - Не забудьте завершить ()
TransactionScope
а такжеDependentTransaction
"Несколько потоков, совместно использующих одну и ту же транзакцию в области транзакции, вызовут следующее исключение:" Контекст транзакции используется другим сеансом "."
Звучит довольно просто. Если вы включите два разных соединения в одну и ту же транзакцию, то попробуйте выполнить команды для каждого из двух соединений одновременно, из разных потоков, может возникнуть конфликт.
Другими словами, один поток выдает команду для одного соединения и удерживает некоторую блокировку в контексте транзакции. Другой поток, используя другое соединение, пытается выполнить команды одновременно и не может заблокировать тот же контекст транзакции, который используется другим потоком.
Вы должны создать DependentTransaction
для каждого потока, а затем внутри потока создать и открыть соединение БД внутри TransacctionScope
с использованием dependentTransaction
в ктор.
//client code / main thread
using (TransactionScope scope = new TransactionScope(TransactionScopeOption.RequiresNew, timeout))
{
Transaction currentTransaction = Transaction.Current;
currentTransaction.TransactionCompleted += OnCompleted;
DependentTransaction dependentTransaction;
int nWorkers = Config.Instance.NumDBComponentWorkers;
for (int i = 0; i < nWorkers; i++)
{
dependentTransaction = currentTransaction.DependentClone(DependentCloneOption.BlockCommitUntilComplete);
this.startWorker(dependentTransaction);
}
do
{
//loop + wait
Thread.Sleep(150);
} while (this.status == DBComponentStatus.Running);
//No errors-commit transaction
if (this.status == DBComponentStatus.Finished && this.onCanCommit())
{
scope.Complete();
}
}
//workers
protected override void startWorker(DependentTransaction dependentTransaction)
{
Thread thread = new Thread(workerMethod);
thread.Start(dependentTransaction);
}
protected override void workerMethod(object transaction)
{
int executedStatements = 0;
DependentTransaction dependentTransaction;
dependentTransaction = transaction as DependentTransaction;
System.Diagnostics.Debug.Assert(dependentTransaction != null); //testing
try
{
//Transaction.Current = dependentTransaction;
using (TransactionScope scope = new TransactionScope(dependentTransaction))
{
using (SqlConnection conn = new SqlConnection(this.GetConnectionString(this.parameters)))
{
/* Perform transactional work here */
conn.Open();
string statement = string.Empty;
using (SqlCommand cmd = conn.CreateCommand())
{
}
}
//No errors-commit transaction
if (this.status == DBComponentStatus.Finished)
{
scope.Complete();
}
}
}
catch (Exception e)
{
this.status = DBComponentStatus.Aborted;
}
finally
{
dependentTransaction.Complete();
dependentTransaction.Dispose();
}
}
Сделайте шаг назад и сосредоточьтесь больше на своем коде, а не на информации о многопоточности.
Если ваш сценарий не предусматривает многопоточность, он может относиться к частям, которые не закрыты, как вы ожидаете.
Возможно, вызываемый вами sql-код не достигает этой инструкции транзакции фиксации. Или что-то еще вовлечено на этом уровне. Возможно, вы использовали экземпляр SqlConnection, устанавливающий транзакцию в коде.net, и повторно используете этот же экземпляр в другом коде, который использует TransactionScope. Попробуйте добавить инструкции (), где это уместно, чтобы убедиться, что все закрыто так, как вы этого ожидаете.
Как бы я справился с этой проблемой при построении операторов Linq с объектами mutlipe, нужно иметь конструктор для каждого класса, который принимает контекст данных, и метод GetDataContext() в каждом классе. при объединении классов я бы создал экземпляры классов, передаваемые в первом классе GetContext()
public class CriterionRepository : ICriterionRepository
{
private Survey.Core.Repository.SqlDataContext _context = new Survey.Core.Repository.SqlDataContext();
public CriterionRepository() { }
public CriterionRepository(Survey.Core.Repository.SqlDataContext context)
{
_context = context;
}
...
public Survey.Core.Repository.SqlDataContext GetDataContext()
{
return _context;
}
}
У меня есть многопоточное приложение, которое выполняет некоторые манипуляции с данными и сохраняет результаты в базе данных. Поскольку разные потоки работают с разными типами данных, написание кода для сбора результатов и их выгрузки в базу данных в одном потоке более обременительно, чем просто каждый поток записывает результаты сам по себе, когда это будет сделано.
Я хотел выполнить это в транзакции, чтобы у меня была возможность отменить всю работу в случае возникновения ошибки в любом из дочерних потоков. Добавление транзакций стало причиной проблем, которые привели меня к этой публикации, но я смог их обработать. Возможен многопоточный доступ к базе данных в одной транзакции. Я даже использую LINQ-to-SQL и SqlBulkCopy вместе в одной транзакции.
Я нашел ответ Ильи Чидякина очень полезным. Вам необходимо передать DependentTransaction каждому потоку и использовать его для создания нового TransactionScope. И вы должны помнить, чтобы зафиксировать и TransactionScope, и DependentTransaction в каждом потоке. Наконец, вы должны подождать, чтобы завершить вашу "оригинальную" транзакцию, пока вся дочерняя работа не будет выполнена. (На самом деле DependentTransaction должна позаботиться об этом, но я уже использовал Thread.Join, чтобы дождаться выполнения всей работы, прежде чем добавлять транзакции в этот проект.)
Ключевым моментом является то, что только один поток может получить доступ к базе данных в любой момент времени. Я просто использовал семафор, чтобы заблокировать доступ к базе данных одному потоку за раз. Поскольку мои потоки проводят большую часть времени за вычислениями и лишь немного времени за записью в базу данных, из-за этого я не понесла снижения производительности... Однако, если ваши потоки часто используют базу данных, это требование может по существу, устраните выигрыш в производительности многопоточности, если вы хотите, чтобы все содержалось в одной транзакции.
Если у вас есть несколько потоков, обращающихся к базе данных одновременно, вы получите исключение с сообщением "Контекст транзакции используется другим сеансом". Если вы забудете зафиксировать все транзакции в каждом потоке, вы получите исключение с сообщением "Транзакция сомневается", когда вы попытаетесь зафиксировать самую внешнюю транзакцию.