В чем причина "контекста транзакции, используемого другим сеансом",
Я ищу описание корня этой ошибки: "Контекст транзакции используется другим сеансом".
Я иногда получаю это в одном из моих unittests, поэтому я не могу воспроизвести код. Но мне интересно, что такое "по дизайну" причина ошибки.
UPDATE: ошибка возвращается как SqlException из SQL Server 2008. Место, где я получаю ошибку, кажется однопоточным. Но, вероятно, у меня есть взаимодействие unittests, поскольку я получаю ошибку, когда запускают сразу несколько тестов (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()
Я нашел эти сообщения:
Но я не могу понять, что "Несколько потоков, разделяющих одну и ту же транзакцию в области транзакций, вызовут следующее исключение:" контекст транзакции, используемый другим сеансом ".). Все слова понятны, но не суть.
Фактически я могу совместно использовать системную транзакцию между потоками. И для этого существует даже специальный механизм - DependentTransaction class и метод Transaction.DependentClone.
Я пытаюсь воспроизвести usecase с первого поста:
- Основной поток создает транзакцию 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);
}
}
Не удается выполнить полный вызов root TransactionScope. Но ошибка отличается:
Необработанное исключение: System.Transactions.TransactionInDoubtException: транзакция вызывает сомнения. --- > pired. Период ожидания истекает до завершения операции или сервер не отвечает.
Подводя итог: хочу понять, что означает "контекст транзакции, используемый другим сеансом", и как его воспроизводить.
Ответы
Ответ 1
Это немного поздно для ответа:), но надеюсь, что это будет полезно для других.
Ответ содержит три части:
- Что означает "контекст транзакции, используемый другим сеансом" .
- Как воспроизвести ошибку "Контекст транзакции, используемый другим сеансом".
1. Что означает "контекст транзакции, используемый другим сеансом" .
Важное замечание: Блокировка контекста транзакции выполняется непосредственно перед выпуском сразу после взаимодействия между SqlConnection
и SQL Server.
Когда вы выполняете некоторый SQL-запрос, SqlConnection
"смотрит", есть ли какая-либо транзакция, обертывающая его. Это может быть SqlTransaction
( "native" для SqlConnection) или Transaction
из System.Transactions
сборки.
Когда найденная транзакция SqlConnection
использует ее для связи с SQL Server, и в настоящий момент они связывают Transaction
контекст исключительно заблокирован.
Что делает TransactionScope
? Он создает Transaction
и предоставляет инфраструктуру компонентов .NET Framework об этом, поэтому каждый, включая 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', '[email protected]')
SET @Counter = @Counter - 1
END
GO
И воспроизведите "контекст транзакции, используемый другим сеансом" . ошибка с кодом С# на основе примера кода 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
Ответ 2
"Несколько потоков, разделяющих один и тот же транзакция в области транзакций приведет к следующему исключению:" Контекст транзакции, используемый другим сессии ".
Звучит довольно просто. Если вы заручитесь двумя разными соединениями в одной транзакции, попробуйте одновременно выдавать команды для каждого из двух подключений из разных потоков, может возникнуть конфликт.
Другими словами, один поток выдает команду на одно соединение и удерживает какой-то замок в контексте транзакции. Другой поток, используя другое соединение, пытается выполнить команды в одно и то же время и не может блокировать тот же контекст транзакции, который используется другим потоком.
Ответ 3
Сделайте шаг назад и сосредоточьтесь больше на своем коде и меньше на информации о нескольких потоках.
Если ваш сценарий не связан с потоками, он может относиться к частям, которые не закрыты, как вы ожидаете.
Возможно, код sql, который вы вызываете, не достигает этой команды транзакции фиксации. Или на этом уровне есть что-то еще. Возможно, вы использовали экземпляр SqlConnection, устанавливающий транзакцию в .net-коде, и повторно используете тот же экземпляр для другого кода, который использует TransactionScope. Попытайтесь добавить с помощью() инструкции, где это необходимо, чтобы убедиться, что все закрыто, как вы ожидаете.
Ответ 4
Как я могу справиться с этой проблемой при создании операторов 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;
}
}
Ответ 5
Вы должны создать DependentTransaction
для каждого потока, а затем внутри потока создать и открыть db-соединение внутри TransacctionScope
с помощью DependentTransaction
в ctor.
//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();
}
}
Ответ 6
У меня есть многопоточное приложение, которое выполняет некоторые манипуляции с данными и сохраняет результаты в базе данных. Поскольку разные потоки работают с различными типами данных, написание кода для сбора результатов и выведение его из базы в один поток является более громоздким, чем просто каждый поток записывает результаты сам по себе, когда это делается.
Я хотел запустить это в транзакции, так что у меня есть возможность вернуть всю работу в случае, если ошибка произошла в любом из дочерних потоков. Добавление транзакций вызвало проблемы, которые привели меня к этой публикации, но я смог проработать их. Возможно многопоточный доступ к базе данных в одной транзакции. Я даже использую LINQ-to-SQL и SqlBulkCopy вместе в одной транзакции.
Я нашел, что Илья Чидякин ответил очень полезно. Вам нужно передать DependentTransaction для каждого потока и использовать его для создания нового TransactionScope. И вам нужно помнить о том, чтобы зафиксировать транзакцию TransactionScope и DependentTransaction в каждом потоке. Наконец, вы должны ждать, чтобы выполнить свою "оригинальную" транзакцию, пока не будет выполнена вся работа с дочерью. (DependentTransaction должен позаботиться об этом, фактически, но я уже использовал Thread.Join, чтобы дождаться выполнения всей работы, прежде чем добавлять транзакции в этот проект.)
Ключевым моментом является то, что только один поток может обращаться к базе данных в любой момент времени. Я просто использовал семафор, чтобы блокировать доступ к базе данных по одному потоку за раз. Поскольку мои потоки тратят большую часть времени на вычисления и всего лишь немного времени на запись в базу данных, я действительно не взял на себя штраф за производительность из-за этого... Однако, если ваши потоки часто используют базу данных, это требование может существенно снижают эффективность работы многопоточности, если вы хотите, чтобы все содержалось в одной транзакции.
Если у вас есть несколько потоков, обращающихся к базе данных сразу, вы получите исключение с сообщением "Контекст транзакции, используемый другим сеансом". Если вы забудете совершить все транзакции в каждом потоке, вы получите сообщение об исключении с сообщением "Сделка сомневается", когда вы пытаетесь совершить транзакцию самого внешнего.